Files
UnrealEngine/Engine/Source/Runtime/ImageWrapper/Private/Formats/HdrImageWrapper.cpp
2025-05-18 13:04:45 +08:00

670 lines
16 KiB
C++

// Copyright Epic Games, Inc. All Rights Reserved.
#include "Formats/HdrImageWrapper.h"
#include "ImageWrapperPrivate.h"
#include "ImageCoreUtils.h"
#define LOCTEXT_NAMESPACE "HdrImageWrapper"
namespace UE::ImageWrapper::Private::HdrImageWrapper
{
FText GetMalformedHeaderErrorMessage()
{
return LOCTEXT("MalformedHeader", "The file header is malformed. The HDR image is likely corrupted.");
}
FText GetEndOfBufferErrorMessage()
{
return LOCTEXT("EndOFBufferError", "Reached the end of the buffer before finishing decompressing the HDR. The HDR image is likely corrupted.");
}
FText GetMalformedScanlineErrorMessage()
{
return LOCTEXT("MalformedScanline", "Compressed data for HDR scanline is malformed. The HDR image is likely to be corrupted.");
}
}
bool FHdrImageWrapper::SetCompressedFromView(TArrayView64<const uint8> Data)
{
// CompressedData is just a View, does not take a copy
CompressedData = Data;
if (CompressedData.Num() < 11)
{
return FailHeaderParsing();
}
const uint8* FileDataPtr = CompressedData.GetData();
char Line[256];
if (!GetHeaderLine(FileDataPtr, Line))
{
return FailHeaderParsing();
}
if (FCStringAnsi::Strcmp(Line, "#?RADIANCE") != 0 &&
FCStringAnsi::Strcmp(Line, "#?RGBE") != 0)
{
return FailHeaderParsing();
}
// Read header lines: free-form, keep going until we hit a blank line
bool bHasFormat = false;
for (;;)
{
if (!GetHeaderLine(FileDataPtr, Line))
{
return FailHeaderParsing();
}
// Blank line denotes end of header
if (!Line[0])
break;
const char* Cursor = Line;
// Format specified?
if (ParseMatchString(Cursor, "FORMAT="))
{
bHasFormat = true;
// Currently we only support RGBE
if (FCStringAnsi::Strcmp(Cursor, "32-bit_rle_rgbe") != 0)
{
SetAndLogError(LOCTEXT("WrongFormatError", "The HDR image uses an unsupported format. Only the 32-bit_rle_rgbe format is supported."));
FreeCompressedData();
return false;
}
}
}
// If we got through the header without it mentioning a format, the file is malformed.
if (!bHasFormat)
{
return FailHeaderParsing();
}
// Read one more line which specifies the resolution
if (!GetHeaderLine(FileDataPtr, Line))
{
return FailHeaderParsing();
}
int ImageWidth;
int ImageHeight;
if ( !ParseImageSize(Line, &ImageWidth, &ImageHeight) ||
ImageWidth <= 0 || ImageHeight <= 0 )
{
// If we don't like the resolution line (our parser is very strict), log what it was
// as a breadcrumb for debugging.
FString BadResolutionLine(Line);
UE_LOG(LogImageWrapper, Display, TEXT("HDR bad resolution line was: \"%s\""), *BadResolutionLine);
SetAndLogError(LOCTEXT("InvalidSizeError", "The HDR image specifies an invalid size."));
FreeCompressedData();
return false;
}
Width = ImageWidth;
Height = ImageHeight;
RGBDataStart = FileDataPtr;
if ( ! FImageCoreUtils::IsImageImportPossible(Width,Height) )
{
SetAndLogError(LOCTEXT("ImpossibleImport","Image dimensions are not possible to import"));
return false;
}
return true;
}
bool FHdrImageWrapper::SetCompressed(const void* InCompressedData, int64 InCompressedSize)
{
// takes copy
CompressedDataHolder.Reset(InCompressedSize);
CompressedDataHolder.Append((const uint8*)InCompressedData, InCompressedSize);
return SetCompressedFromView(MakeArrayView(CompressedDataHolder));
}
// CanSetRawFormat returns true if SetRaw will accept this format
bool FHdrImageWrapper::CanSetRawFormat(const ERGBFormat InFormat, const int32 InBitDepth) const
{
return InFormat == ERGBFormat::BGRE && InBitDepth == 8;
}
// returns InFormat if supported, else maps to something supported
ERawImageFormat::Type FHdrImageWrapper::GetSupportedRawFormat(const ERawImageFormat::Type InFormat) const
{
// only writes one format :
return ERawImageFormat::BGRE8;
}
bool FHdrImageWrapper::SetRaw(const void* InRawData, int64 InRawSize, const int32 InWidth, const int32 InHeight, const ERGBFormat InFormat, const int32 InBitDepth, const int32 InBytesPerRow)
{
if ( ! CanSetRawFormat(InFormat,InBitDepth) )
{
UE_LOG(LogImageWrapper, Warning, TEXT("ImageWrapper unsupported format; check CanSetRawFormat; %d x %d"), (int)InFormat,InBitDepth);
return false;
}
if (InWidth <= 0 || InHeight <= 0)
{
UE_LOG(LogImageWrapper, Warning, TEXT("ImageWrapper HDR unsupported size %d x %d"), InWidth, InHeight);
return false;
}
RawDataHolder.Empty(InRawSize);
RawDataHolder.Append((const uint8 *)InRawData,InRawSize);
Width = InWidth;
Height = InHeight;
check( InFormat == ERGBFormat::BGRE );
check( InBitDepth == 8 );
return true;
}
TArray64<uint8> FHdrImageWrapper::GetCompressed(int32 Quality)
{
// must have set BGRE8 raw data :
int64 NumPixels = (int64)Width * Height;
check( RawDataHolder.Num() == NumPixels * 4 );
char Header[MAX_SPRINTF];
int32 HeaderLen = FCStringAnsi::Sprintf(Header, "#?RADIANCE\nFORMAT=32-bit_rle_rgbe\n\n-Y %d +X %d\n", Height, Width);
if (HeaderLen <= 0) // Error during Sprintf for whatever reason?
{
return TArray64<uint8>();
}
FreeCompressedData();
TArray64<uint8> CompressedDataArray;
CompressedDataArray.SetNumUninitialized(HeaderLen + NumPixels * 4);
uint8 * CompressedPtr = CompressedDataArray.GetData();
memcpy(CompressedPtr,Header,HeaderLen);
CompressedPtr += HeaderLen;
// just put the BGRE8 bytes
// but need to swizzle to RGBE8 order:
const uint8* FromBytes = RawDataHolder.GetData();
for(int64 i=0;i<NumPixels;i++)
{
CompressedPtr[i * 4 + 0] = FromBytes[i * 4 + 2];
CompressedPtr[i * 4 + 1] = FromBytes[i * 4 + 1];
CompressedPtr[i * 4 + 2] = FromBytes[i * 4 + 0];
CompressedPtr[i * 4 + 3] = FromBytes[i * 4 + 3];
}
return MoveTemp(CompressedDataArray);
}
bool FHdrImageWrapper::GetRaw(const ERGBFormat InFormat, int32 InBitDepth, TArray64<uint8>& OutRawData)
{
if (InFormat != ERGBFormat::BGRE || InBitDepth != 8)
{
SetAndLogError(LOCTEXT("UnSupportedFormatORBitDepth", "The format and/or the bit depth is not supported by the HdrImageWrapper. Only the BGRE format and a bitdepth of 8 is supported"));
return false;
}
if (!IsCompressedImageValid())
{
return false;
}
const int64 SizeRawImageInBytes = (int64)Width * Height * 4;
OutRawData.Reset(SizeRawImageInBytes);
OutRawData.AddUninitialized(SizeRawImageInBytes);
const uint8* FileDataPtr = RGBDataStart;
const uint8* FileDataEnd = CompressedData.GetData() + CompressedData.Num();
for (int32 Y = 0; Y < Height; ++Y)
{
if (!DecompressScanline(&(OutRawData[(int64)Width * Y * 4]), FileDataPtr, FileDataEnd))
{
OutRawData.Empty();
return false;
}
}
return true;
}
IMAGEWRAPPER_API bool FHdrImageWrapper::GetRaw(const ERGBFormat InFormat, int32 InBitDepth, FDecompressedImageOutput& OutDecompressedImage)
{
TArray64<uint8> OutRawBuffer;
if (!GetRaw(InFormat, InBitDepth, OutRawBuffer))
{
return false;
}
OutDecompressedImage.MipMapImage.AddMipImage(MoveTemp(OutRawBuffer), GetWidth(), GetHeight());
return true;
}
int64 FHdrImageWrapper::GetWidth() const
{
return Width;
}
int64 FHdrImageWrapper::GetHeight() const
{
return Height;
}
int32 FHdrImageWrapper::GetBitDepth() const
{
return 8;
}
ERGBFormat FHdrImageWrapper::GetFormat() const
{
return ERGBFormat::BGRE;
}
const FText& FHdrImageWrapper::GetErrorMessage() const
{
return ErrorMessage;
}
void FHdrImageWrapper::SetAndLogError(const FText& InText)
{
ErrorMessage = InText;
UE_LOG(LogImageWrapper, Error, TEXT("%s"), *InText.ToString());
}
bool FHdrImageWrapper::FailHeaderParsing()
{
SetAndLogError(UE::ImageWrapper::Private::HdrImageWrapper::GetMalformedHeaderErrorMessage());
FreeCompressedData();
return false;
}
bool FHdrImageWrapper::FailUnexpectedEOB()
{
SetAndLogError(UE::ImageWrapper::Private::HdrImageWrapper::GetEndOfBufferErrorMessage());
return false;
}
bool FHdrImageWrapper::FailMalformedScanline()
{
SetAndLogError(UE::ImageWrapper::Private::HdrImageWrapper::GetMalformedScanlineErrorMessage());
return false;
}
bool FHdrImageWrapper::GetHeaderLine(const uint8*& BufferPos, char Line[256])
{
const uint8* EndOfBuffer = CompressedData.GetData() + CompressedData.Num();
for(int i = 0; i < 256; ++i)
{
// May never read past end of buffer
check(BufferPos <= EndOfBuffer); // == may happen, > is a bug.
if (BufferPos >= EndOfBuffer)
{
return false;
}
if (*BufferPos == 0)
{
// We don't allow NUL characters in header, that's malformed
return false;
}
else if (*BufferPos == '\n') // Line break -> end of line in HDR (but CRs are not significant!)
{
// Terminate the line, we're good to go, but do consume the newline.
// We insert the terminating 0 here, and this is the only path where we return true,
// thus guaranteeing that successfully returned lines are 0-terminated.
BufferPos++;
Line[i] = 0;
return true;
}
// All other characters go into the line verbatim.
Line[i] = *BufferPos++;
}
// Falling out of the loop after reading 256 chars, we consider a malformed line.
return false;
}
// InOutCursor points to current parse cursor, into a 0-terminated string.
// InExpected is a 0-terminated string that we expect an exact match to.
//
// On success, return true and point InOutCursor at the end of the matched string.
// On failure, return false and leave InOutCursor where it is.
bool FHdrImageWrapper::ParseMatchString(const char*& InOutCursor, const char* InExpected)
{
const char* Cursor = InOutCursor;
while (*InExpected)
{
if (*Cursor != *InExpected)
{
return false;
}
// The two bytes match, advance.
++Cursor;
++InExpected;
}
InOutCursor = Cursor;
return true;
}
// Like atoi, but we return the final cursor, have an error reporting mechanism
// for overflows, and don't accept signed numbers (or '+' signs for that matter).
//
// InOutCursor points into a properly 0-terminated string, so we can just keep
// reading while chars are between '0' and '9' (since NUL is not).
bool FHdrImageWrapper::ParsePositiveInt(const char*& InOutCursor, int* OutValue)
{
// We require the string to start with a digit
if (*InOutCursor < '0' || *InOutCursor > '9')
{
return false;
}
int Value = *InOutCursor - '0'; // can't overflow.
++InOutCursor;
// Keep consuming digits in the digit string
while (*InOutCursor >= '0' && *InOutCursor <= '9')
{
int Digit = *InOutCursor - '0';
++InOutCursor;
int64 NewValue = (int64)Value * 10 + Digit;
// Overflow?
if (NewValue > TNumericLimits<int>::Max())
{
return false;
}
// We just checked it's in range.
Value = (int)NewValue;
}
*OutValue = Value;
return true;
}
// InLine is known 0-terminated.
bool FHdrImageWrapper::ParseImageSize(const char* InLine, int* OutWidth, int* OutHeight)
{
// We only support the (default) -Y <height> +X <width> form of the image size.
if (!ParseMatchString(InLine, "-Y "))
{
return false;
}
if (!ParsePositiveInt(InLine, OutHeight))
{
return false;
}
if (!ParseMatchString(InLine, " +X "))
{
return false;
}
if (!ParsePositiveInt(InLine, OutWidth))
{
return false;
}
return *InLine == 0; // All bytes in line must be consumed
}
// Trivial helper func to make repetitive error checks easier to read.
// InCursor <= InEnd is assumed; check that we can read InAmount more bytes without
// going past InEnd.
bool FHdrImageWrapper::HaveBytes(const uint8* InCursor, const uint8* InEnd, int InAmount)
{
// InCursor <= InEnd; InEnd - InCursor gives us the number of bytes left.
// (Do it this way instead of "InCursor + InAmount <= InEnd" because the latter
// might overflow.)
return InEnd - InCursor >= InAmount;
}
bool FHdrImageWrapper::DecompressScanline(uint8* Out, const uint8*& In, const uint8* InEnd)
{
// minimum and maximum scanline length for encoding
const int32 MINELEN = 8;
const int32 MAXELEN = 0x7fff;
if (Width < MINELEN || Width > MAXELEN)
{
return OldDecompressScanline(Out, In, InEnd, Width, false);
}
if (!HaveBytes(In, InEnd, 1))
{
return false;
}
uint8 Red = *In;
if(Red != 2)
{
return OldDecompressScanline(Out, In, InEnd, Width, false);
}
++In;
if (!HaveBytes(In, InEnd, 3))
{
return false;
}
uint8 Green = *In++;
uint8 Blue = *In++;
uint8 Exponent = *In++;
if(Green != 2 || (Blue & 128))
{
*Out++ = Blue;
*Out++ = Green;
*Out++ = Red;
*Out++ = Exponent;
return OldDecompressScanline(Out, In, InEnd, Width - 1, true);
}
for(uint32 ChannelRead = 0; ChannelRead < 4; ++ChannelRead)
{
// The file is in RGBE but we decompress in BGRE So swap the red and blue
uint8 CurrentToWrite = ChannelRead;
if (ChannelRead == 0)
{
CurrentToWrite = 2;
}
else if (ChannelRead == 2)
{
CurrentToWrite = 0;
}
const uint8* LocalIn = In;
uint8* OutSingleChannel = Out + CurrentToWrite;
int32 MultiRunIndex = 0;
while ( MultiRunIndex < Width )
{
if (!HaveBytes(LocalIn, InEnd, 1))
{
return FailUnexpectedEOB();
}
uint8 Current = *LocalIn++;
if (Current > 128)
{
// Actual run
int Count = Current & 0x7f;
if (!HaveBytes(LocalIn, InEnd, 1))
{
return FailUnexpectedEOB();
}
Current = *LocalIn++;
// Run needs to stay within scan line.
if (Width - MultiRunIndex < Count)
{
return FailMalformedScanline();
}
for(int RunIndex = 0; RunIndex < Count; ++RunIndex)
{
*OutSingleChannel = Current;
OutSingleChannel += 4;
}
MultiRunIndex += Count;
}
else
{
// Literal run.
int Count = Current;
// Do one check up front whether we have enough data bytes following
if (!HaveBytes(LocalIn, InEnd, Count))
{
return FailUnexpectedEOB();
}
// Literal run needs to stay within scan line.
if (Width - MultiRunIndex < Count)
{
return FailMalformedScanline();
}
for(int RunIndex = 0; RunIndex < Count; ++RunIndex)
{
// All buffer checks were done up front.
*OutSingleChannel = *LocalIn++;
OutSingleChannel += 4;
}
MultiRunIndex += Count;
}
}
In = LocalIn;
}
return true;
}
bool FHdrImageWrapper::OldDecompressScanline(uint8* Out, const uint8*& InCodedScanline, const uint8* InEnd, int32 Length, bool bInitialRunAllowed)
{
const uint8* In = InCodedScanline; // Copy to local var
int32 Shift = 0;
// If an initial run is not allowed, set Shift to 32, which will make us fail if the first thing we
// see in this scanline is a run, but will be cleared after the first non-run pixel.
if (!bInitialRunAllowed)
{
Shift = 32;
}
while (Length > 0)
{
if (!HaveBytes(In, InEnd, 4))
{
return FailUnexpectedEOB();
}
uint8 Red = *In++;
uint8 Green = *In++;
uint8 Blue = *In++;
uint8 Exponent = *In++;
if(Red == 1 && Green == 1 && Blue == 1)
{
// It's not illegal to hit Shift=32 (say after a non-trivial run with Shift=24 in a giant image),
// but since we have a 32-bit width limit, there is just no legitimate reason to have another run
// once Shift=32, it should always be followed by literals.
//
// We also use this to catch runs at the start of a scanline when there's no pixels to repeat yet,
// by initializing Shift=32 on the first pixel of a scanline (we sometimes get called with one pixel
// already decoded, so this is conditional).
if (Shift >= 32)
{
return FailMalformedScanline();
}
// Doing Count calculation in 64 bits to avoid overflow concerns when Shift=24.
int64 Count = (int64)Exponent << Shift;
if (Count > Length)
{
return FailMalformedScanline();
}
Length -= Count;
// Read previous pixel. See comments on top of function and handling of Shift >= 32 above for why
// we are guaranteed to have a previous pixel in the scanline when we get here.
Red = *(Out - 4);
Green = *(Out - 3);
Blue = *(Out - 2);
Exponent = *(Out - 1);
while (Count > 0)
{
*Out++ = Blue;
*Out++ = Green;
*Out++ = Red;
*Out++ = Exponent;
--Count;
}
Shift += 8;
}
else
{
*Out++ = Blue;
*Out++ = Green;
*Out++ = Red;
*Out++ = Exponent;
Shift = 0;
--Length;
}
}
// On successful decode, copy read cursor back
InCodedScanline = In;
return true;
}
bool FHdrImageWrapper::IsCompressedImageValid() const
{
return CompressedData.Num() > 0 && RGBDataStart;
}
void FHdrImageWrapper::FreeCompressedData()
{
CompressedData = TArrayView64<const uint8>();
RGBDataStart = nullptr;
CompressedDataHolder.Empty();
}
bool FHdrImageWrapper::SupportsMetadata() const
{
return false;
}
void FHdrImageWrapper::AddMetadata(const FString&, const FString&)
{
}
bool FHdrImageWrapper::TryGetMetadata(const FString&, FString&) const
{
return false;
}
#undef LOCTEXT_NAMESPACE