844 lines
26 KiB
C++
844 lines
26 KiB
C++
// Copyright Epic Games, Inc. All Rights Reserved.
|
|
|
|
#include "Formats/ExrImageWrapper.h"
|
|
#include "ImageWrapperPrivate.h"
|
|
#include "Math/GuardedInt.h"
|
|
|
|
#include "ColorManagement/ColorSpace.h"
|
|
#include "Containers/StringConv.h"
|
|
#include "HAL/PlatformTime.h"
|
|
#include "Math/Float16.h"
|
|
#include "ImageCoreUtils.h"
|
|
|
|
#if WITH_UNREALEXR
|
|
|
|
FExrImageWrapper::FExrImageWrapper()
|
|
: FImageWrapperBase()
|
|
{
|
|
}
|
|
|
|
class FMemFileOut : public Imf::OStream
|
|
{
|
|
public:
|
|
//-------------------------------------------------------
|
|
// A constructor that opens the file with the given name.
|
|
// The destructor will close the file.
|
|
//-------------------------------------------------------
|
|
|
|
FMemFileOut(const char fileName[]) :
|
|
Imf::OStream(fileName),
|
|
Pos(0)
|
|
{
|
|
}
|
|
|
|
// InN must be 32bit to match the abstract interface.
|
|
virtual void write(const char c[/*n*/], int32 InN)
|
|
{
|
|
int64 SrcN = (int64)InN;
|
|
int64 DestPost = Pos + SrcN;
|
|
if (DestPost > Data.Num())
|
|
{
|
|
Data.AddUninitialized(FMath::Max(Data.Num() * 2, DestPost) - Data.Num());
|
|
}
|
|
|
|
for (int64 i = 0; i < SrcN; ++i)
|
|
{
|
|
Data[Pos + i] = c[i];
|
|
}
|
|
Pos += SrcN;
|
|
}
|
|
|
|
|
|
//---------------------------------------------------------
|
|
// Get the current writing position, in bytes from the
|
|
// beginning of the file. If the next call to write() will
|
|
// start writing at the beginning of the file, tellp()
|
|
// returns 0.
|
|
//---------------------------------------------------------
|
|
|
|
uint64_t tellp() override
|
|
{
|
|
return Pos;
|
|
}
|
|
|
|
|
|
//-------------------------------------------
|
|
// Set the current writing position.
|
|
// After calling seekp(i), tellp() returns i.
|
|
//-------------------------------------------
|
|
|
|
void seekp(uint64_t pos) override
|
|
{
|
|
Pos = pos;
|
|
}
|
|
|
|
|
|
int64 Pos;
|
|
TArray64<uint8> Data;
|
|
};
|
|
|
|
|
|
class FMemFileIn : public Imf::IStream
|
|
{
|
|
public:
|
|
//-------------------------------------------------------
|
|
// A constructor that opens the file with the given name.
|
|
// The destructor will close the file.
|
|
//-------------------------------------------------------
|
|
|
|
FMemFileIn(const void* InData, int64 InSize)
|
|
: Imf::IStream("")
|
|
, Data((const char *)InData)
|
|
, Size(InSize)
|
|
, Pos(0)
|
|
{
|
|
// InSize is signed but "Size" member is unsigned
|
|
if (InSize < 0)
|
|
{
|
|
Size = 0;
|
|
// MSVC runtime_error vtable size changes based on module bEnableExceptions setting
|
|
//throw std::runtime_error("FMemFileIn: Negative size passed to EXR parser");
|
|
// MSVC std::exception has a char * constructor, but that is not portable, do not use
|
|
throw std::exception();
|
|
}
|
|
}
|
|
|
|
//------------------------------------------------------
|
|
// Read from the stream:
|
|
//
|
|
// read(c,n) reads n bytes from the stream, and stores
|
|
// them in array c. If the stream contains less than n
|
|
// bytes, or if an I/O error occurs, read(c,n) throws
|
|
// an exception. If read(c,n) reads the last byte from
|
|
// the file it returns false, otherwise it returns true.
|
|
//------------------------------------------------------
|
|
|
|
// InN must be 32bit to match the abstract interface.
|
|
virtual bool read (char Out[/*n*/], int32 Count)
|
|
{
|
|
// return false if EOF is hit
|
|
// OpenEXR mostly ignores this return value, you must throw to get error handling
|
|
|
|
FGuardedInt64 NextPosition = FGuardedInt64(Pos) + Count;
|
|
if (Count < 0 ||
|
|
NextPosition.InvalidOrGreaterThan(Size))
|
|
{
|
|
//throw std::runtime_error("FMemFileIn: Exr read out of bounds");
|
|
throw std::exception();
|
|
}
|
|
|
|
memcpy(Out,Data+Pos,Count);
|
|
Pos += Count; // == NextPosition
|
|
|
|
if ( Pos == Size )
|
|
{
|
|
return false;
|
|
}
|
|
else
|
|
{
|
|
return true;
|
|
}
|
|
}
|
|
|
|
//--------------------------------------------------------
|
|
// Get the current reading position, in bytes from the
|
|
// beginning of the file. If the next call to read() will
|
|
// read the first byte in the file, tellg() returns 0.
|
|
//--------------------------------------------------------
|
|
|
|
uint64_t tellg() override
|
|
{
|
|
return Pos;
|
|
}
|
|
|
|
//-------------------------------------------
|
|
// Set the current reading position.
|
|
// After calling seekg(i), tellg() returns i.
|
|
//-------------------------------------------
|
|
|
|
void seekg(uint64_t pos) override
|
|
{
|
|
Pos = pos;
|
|
}
|
|
|
|
int64_t size() override
|
|
{
|
|
// Size was verified as non-negative in the constructor.
|
|
return static_cast<int64_t>(Size);
|
|
}
|
|
|
|
private:
|
|
|
|
const char* Data;
|
|
uint64 Size;
|
|
uint64 Pos;
|
|
};
|
|
|
|
// these are the channel names we write
|
|
// we will read whatever channel names are in the file
|
|
const char* cChannelNamesRGBA[] = { "R", "G", "B", "A" };
|
|
const char* cChannelNamesBGRA[] = { "B", "G", "R", "A" };
|
|
//const char* cChannelNamesGray[] = { "G" }; // is that green or gray ?
|
|
const char* cChannelNamesGray[] = { "Y" }; // pretty sure "Y" is more standard for gray
|
|
|
|
static int32 GetChannelNames(ERGBFormat InRGBFormat, const char* const*& OutChannelNames)
|
|
{
|
|
int32 ChannelCount;
|
|
|
|
switch (InRGBFormat)
|
|
{
|
|
case ERGBFormat::RGBA:
|
|
case ERGBFormat::RGBAF:
|
|
OutChannelNames = cChannelNamesRGBA;
|
|
ChannelCount = UE_ARRAY_COUNT(cChannelNamesRGBA);
|
|
break;
|
|
|
|
case ERGBFormat::BGRA:
|
|
OutChannelNames = cChannelNamesBGRA;
|
|
ChannelCount = UE_ARRAY_COUNT(cChannelNamesBGRA);
|
|
break;
|
|
|
|
case ERGBFormat::Gray:
|
|
case ERGBFormat::GrayF:
|
|
OutChannelNames = cChannelNamesGray;
|
|
ChannelCount = UE_ARRAY_COUNT(cChannelNamesGray);
|
|
break;
|
|
|
|
default:
|
|
OutChannelNames = nullptr;
|
|
ChannelCount = 0;
|
|
}
|
|
|
|
return ChannelCount;
|
|
}
|
|
|
|
|
|
bool FExrImageWrapper::CanSetRawFormat(const ERGBFormat InFormat, const int32 InBitDepth) const
|
|
{
|
|
// only set float formats
|
|
// integer formats should be converted before reaching ImageWrapper
|
|
return (InFormat == ERGBFormat::RGBAF || InFormat == ERGBFormat::GrayF) && (InBitDepth == 16 || InBitDepth == 32);
|
|
}
|
|
|
|
ERawImageFormat::Type FExrImageWrapper::GetSupportedRawFormat(const ERawImageFormat::Type InFormat) const
|
|
{
|
|
switch(InFormat)
|
|
{
|
|
case ERawImageFormat::RGBA16F:
|
|
case ERawImageFormat::RGBA32F:
|
|
case ERawImageFormat::R16F:
|
|
case ERawImageFormat::R32F:
|
|
return InFormat; // directly supported
|
|
case ERawImageFormat::G8:
|
|
return ERawImageFormat::R16F; // needs conversion
|
|
case ERawImageFormat::BGRA8:
|
|
case ERawImageFormat::BGRE8:
|
|
return ERawImageFormat::RGBA16F; // needs conversion
|
|
case ERawImageFormat::G16:
|
|
case ERawImageFormat::RGBA16:
|
|
return ERawImageFormat::RGBA32F; // needs conversion
|
|
default:
|
|
check(0);
|
|
return ERawImageFormat::BGRA8;
|
|
}
|
|
}
|
|
|
|
bool FExrImageWrapper::SetCompressed(const void* InCompressedData, int64 InCompressedSize)
|
|
{
|
|
check(InCompressedData);
|
|
check(InCompressedSize > 0);
|
|
|
|
// reset variables :
|
|
FileChannelNames.Empty();
|
|
|
|
// Check the magic value in advance to avoid spamming the log with EXR parsing errors.
|
|
if (InCompressedSize < sizeof(uint32) || *(uint32*)InCompressedData != Imf::MAGIC)
|
|
{
|
|
return false;
|
|
}
|
|
|
|
if (!FImageWrapperBase::SetCompressed(InCompressedData, InCompressedSize))
|
|
{
|
|
return false;
|
|
}
|
|
|
|
// openEXR can throw exceptions when parsing invalid data.
|
|
try
|
|
{
|
|
FMemFileIn MemFile(CompressedData.GetData(), CompressedData.Num());
|
|
Imf::InputFile ImfFile(MemFile);
|
|
Imf::Header ImfHeader = ImfFile.header();
|
|
Imf::ChannelList ImfChannels = ImfHeader.channels();
|
|
Imath::Box2i ImfDataWindow = ImfHeader.dataWindow();
|
|
Width = ImfDataWindow.max.x - ImfDataWindow.min.x + 1;
|
|
Height = ImfDataWindow.max.y - ImfDataWindow.min.y + 1;
|
|
|
|
bool bHasOnlyHALFChannels = true;
|
|
int32 ChannelCount = 0;
|
|
|
|
for (Imf::ChannelList::Iterator Iter = ImfChannels.begin(); Iter != ImfChannels.end(); ++Iter)
|
|
{
|
|
++ChannelCount;
|
|
|
|
bHasOnlyHALFChannels = bHasOnlyHALFChannels && Iter.channel().type == Imf::HALF;
|
|
|
|
FileChannelNames.Add( MakeUniqueCString(Iter.name()) );
|
|
}
|
|
|
|
if ( ChannelCount == 0 )
|
|
{
|
|
SetError( TEXT("EXR has no channels") );
|
|
return false;
|
|
}
|
|
|
|
BitDepth = (ChannelCount && bHasOnlyHALFChannels) ? 16 : 32;
|
|
|
|
// EXR uint32 channels are currently not supported, therefore input channels are always treated as float channels.
|
|
// Note that channels inside the EXR file are indexed by name, therefore can be decoded in any RGB order.
|
|
|
|
if (ChannelCount == 1 )
|
|
{
|
|
Format = ERGBFormat::GrayF;
|
|
}
|
|
else
|
|
{
|
|
Format = ERGBFormat::RGBAF;
|
|
}
|
|
}
|
|
catch (const std::exception& Exception)
|
|
{
|
|
TStringConversion<TStringConvert<char, TCHAR>> Convertor(Exception.what());
|
|
UE_LOG(LogImageWrapper, Error, TEXT("Cannot parse EXR image header: %s"), Convertor.Get());
|
|
SetError(Convertor.Get());
|
|
return false;
|
|
}
|
|
|
|
if ( ! FImageCoreUtils::IsImageImportPossible(Width,Height) )
|
|
{
|
|
SetError(TEXT("Image dimensions are not possible to import"));
|
|
return false;
|
|
}
|
|
|
|
return true;
|
|
}
|
|
|
|
void FExrImageWrapper::Compress(int32 Quality)
|
|
{
|
|
check(RawData.Num());
|
|
|
|
// Ensure we haven't already compressed the file.
|
|
if (CompressedData.Num())
|
|
{
|
|
return;
|
|
}
|
|
|
|
const double StartTime = FPlatformTime::Seconds();
|
|
|
|
Imf::PixelType ImfPixelType;
|
|
TArray64<uint8> ConvertedRawData;
|
|
bool bNeedsConversion = false;
|
|
|
|
if (BitDepth == 8)
|
|
{
|
|
// uint8 channels are linearly converted into FFloat16 channels.
|
|
// note: NO GAMMA CORRECTION
|
|
// (this is no longer used in the modern FImage based APIs, conversion to float should be done before calling this)
|
|
ConvertedRawData.SetNumUninitialized(sizeof(FFloat16) * RawData.Num());
|
|
FFloat16* Output = reinterpret_cast<FFloat16*>(ConvertedRawData.GetData());
|
|
for (int64 i = 0; i < RawData.Num(); ++i)
|
|
{
|
|
Output[i] = FFloat16(RawData[i] / 255.f);
|
|
}
|
|
ImfPixelType = Imf::HALF;
|
|
bNeedsConversion = true;
|
|
}
|
|
else
|
|
{
|
|
ImfPixelType = (BitDepth == 16) ? Imf::HALF : Imf::FLOAT;
|
|
}
|
|
|
|
const TArray64<uint8>& PixelData = bNeedsConversion ? ConvertedRawData : RawData;
|
|
|
|
const char* const* ChannelNames;
|
|
int32 ChannelCount = GetChannelNames(Format, ChannelNames);
|
|
check((int64)ChannelCount * Width * Height * BitDepth == RawData.Num() * 8);
|
|
|
|
int32 BytesPerChannelPixel = (ImfPixelType == Imf::HALF) ? 2 : 4;
|
|
TArray<TArray64<uint8>> ChannelData;
|
|
ChannelData.SetNum(ChannelCount);
|
|
|
|
for (int32 c = 0; c < ChannelCount; ++c)
|
|
{
|
|
ChannelData[c].SetNumUninitialized((int64)BytesPerChannelPixel * Width * Height);
|
|
}
|
|
|
|
// EXR channels are compressed non-interleaved.
|
|
for (int64 OffsetNonInterleaved = 0, OffsetInterleaved = 0; OffsetInterleaved < PixelData.Num(); OffsetNonInterleaved += BytesPerChannelPixel)
|
|
{
|
|
for (int32 c = 0; c < ChannelCount; ++c)
|
|
{
|
|
for (int32 b = 0; b < BytesPerChannelPixel; ++b, ++OffsetInterleaved)
|
|
{
|
|
ChannelData[c][OffsetNonInterleaved + b] = PixelData[OffsetInterleaved];
|
|
}
|
|
}
|
|
}
|
|
|
|
Imf::Compression ImfCompression = (Quality == (int32)EImageCompressionQuality::Uncompressed) ? Imf::Compression::NO_COMPRESSION : Imf::Compression::ZIP_COMPRESSION;
|
|
Imf::Header ImfHeader(Width, Height, 1, Imath::V2f(0, 0), 1, Imf::LineOrder::INCREASING_Y, ImfCompression);
|
|
Imf::FrameBuffer ImfFrameBuffer;
|
|
|
|
for (int32 c = 0; c < ChannelCount; ++c)
|
|
{
|
|
ImfHeader.channels().insert(ChannelNames[c], Imf::Channel(ImfPixelType));
|
|
ImfFrameBuffer.insert(ChannelNames[c], Imf::Slice(ImfPixelType, (char*)ChannelData[c].GetData(), BytesPerChannelPixel, (size_t)BytesPerChannelPixel * Width));
|
|
}
|
|
|
|
// Write the working color space into EXR chromaticities
|
|
const UE::Color::FColorSpace& WCS = UE::Color::FColorSpace::GetWorking();
|
|
Imf::Chromaticities Chromaticities = {
|
|
IMATH_NAMESPACE::V2f((float)WCS.GetRedChromaticity().X, (float)WCS.GetRedChromaticity().Y),
|
|
IMATH_NAMESPACE::V2f((float)WCS.GetGreenChromaticity().X, (float)WCS.GetGreenChromaticity().Y),
|
|
IMATH_NAMESPACE::V2f((float)WCS.GetBlueChromaticity().X, (float)WCS.GetBlueChromaticity().Y),
|
|
IMATH_NAMESPACE::V2f((float)WCS.GetWhiteChromaticity().X, (float)WCS.GetWhiteChromaticity().Y),
|
|
};
|
|
Imf::addChromaticities(ImfHeader, Chromaticities);
|
|
|
|
FMemFileOut MemFile("");
|
|
int64 MemFileLength;
|
|
|
|
{
|
|
// This scope ensures that IMF::Outputfile creates a complete file by closing the file when it goes out of scope.
|
|
// To complete the file, EXR seeks back into the file and writes the scanline offsets when the file is closed,
|
|
// which moves the tellp location. So file length is stored in advance for later use.
|
|
|
|
Imf::OutputFile ImfFile(MemFile, ImfHeader, FPlatformMisc::NumberOfCoresIncludingHyperthreads());
|
|
ImfFile.setFrameBuffer(ImfFrameBuffer);
|
|
ImfFile.writePixels(Height);
|
|
MemFileLength = MemFile.tellp();
|
|
}
|
|
|
|
CompressedData = MoveTemp(MemFile.Data);
|
|
CompressedData.SetNum(MemFileLength);
|
|
|
|
const double DeltaTime = FPlatformTime::Seconds() - StartTime;
|
|
UE_LOG(LogImageWrapper, Verbose, TEXT("Compressed image in %.3f seconds"), DeltaTime);
|
|
}
|
|
|
|
void FExrImageWrapper::Uncompress(const ERGBFormat InFormat, const int32 InBitDepth)
|
|
{
|
|
check(CompressedData.Num());
|
|
|
|
// Ensure we haven't already uncompressed the file.
|
|
if (RawData.Num() && InFormat == Format && InBitDepth == BitDepth)
|
|
{
|
|
return;
|
|
}
|
|
|
|
FString ErrorMessage;
|
|
|
|
if (InBitDepth == 16 && (InFormat == ERGBFormat::RGBA || InFormat == ERGBFormat::Gray))
|
|
{
|
|
// Before ERGBFormat::RGBAF and ERGBFormat::GrayF were introduced, 16-bit ERGBFormat::RGBA and ERGBFormat::Gray were used to describe float pixel formats.
|
|
// ERGBFormat::RGBA and ERGBFormat::Gray should now only be used for integer channels, while EXR format doesn't support 16-bit integer channels.
|
|
const TCHAR* FormatName = (InFormat == ERGBFormat::RGBA) ? TEXT("RGBA") : TEXT("Gray");
|
|
ErrorMessage = FString::Printf(TEXT("Usage of 16-bit ERGBFormat::%s raw format for decompressing float EXR channels is deprecated, please use ERGBFormat::%sF instead."), FormatName, FormatName);
|
|
}
|
|
else if (InBitDepth != 16 && InBitDepth != 32)
|
|
{
|
|
ErrorMessage = TEXT("Unsupported bit depth, expected 16 or 32.");
|
|
}
|
|
else if (InFormat != ERGBFormat::RGBAF && InFormat != ERGBFormat::GrayF)
|
|
{
|
|
// EXR uint32 channels are currently not supported
|
|
ErrorMessage = TEXT("Unsupported RGB format, expected ERGBFormat::RGBAF or ERGBFormat::GrayF.");
|
|
}
|
|
|
|
if (!ErrorMessage.IsEmpty())
|
|
{
|
|
UE_LOG(LogImageWrapper, Error, TEXT("Cannot decompress EXR image: %s."), *ErrorMessage);
|
|
SetError(*ErrorMessage);
|
|
return;
|
|
}
|
|
|
|
// use FileChannelNames[] from SetCompressed
|
|
// so that we ask for channels that are actually in the file
|
|
const char* ChannelNames[4] = { };
|
|
int32 ChannelCount;
|
|
if ( InFormat == ERGBFormat::GrayF )
|
|
{
|
|
// load the one channel into slot 0 regardless of name
|
|
// output format will be R16F or R32F , so it will look red
|
|
check( FileChannelNames.Num() == 1 );
|
|
ChannelNames[0] = FileChannelNames[0].Get();
|
|
ChannelCount = 1;
|
|
}
|
|
else
|
|
{
|
|
const char * ChannelNamesDesiredChars = "RGBA";
|
|
ChannelCount = 4;
|
|
|
|
// try to find channel names in the file which correspond to RGBA
|
|
// by looking at the last char of the file channel name
|
|
// and trying to match it to ChannelNamesDesiredChars
|
|
// (so that eg. "rgb.R" maps to channel 0)
|
|
TArray<const char *> FileChannelNamesUnused;
|
|
for(int FileChannelNamesI=0;FileChannelNamesI<FileChannelNames.Num();FileChannelNamesI++)
|
|
{
|
|
const char * Str = FileChannelNames[FileChannelNamesI].Get();
|
|
check( Str && *Str );
|
|
char LastChar = toupper( Str[ strlen(Str)-1 ] );
|
|
|
|
for(int ChannelIndex=0;ChannelIndex<4;ChannelIndex++)
|
|
{
|
|
char DesiredChar = ChannelNamesDesiredChars[ChannelIndex];
|
|
if ( LastChar == DesiredChar && ChannelNames[ChannelIndex] == nullptr )
|
|
{
|
|
ChannelNames[ChannelIndex] = Str;
|
|
Str = nullptr;
|
|
break;
|
|
}
|
|
}
|
|
|
|
if ( Str )
|
|
{
|
|
FileChannelNamesUnused.Add( Str );
|
|
}
|
|
}
|
|
|
|
// if there are channel names in the file which were not mapped
|
|
// and we have not found 4 channels
|
|
// then just stuff the file channel names into channels in arbitrary order
|
|
// eg. if the EXR has channel names like "X","Y","Z" lets go ahead and load them into RGB
|
|
for(int ChannelIndex=0;ChannelIndex<4;ChannelIndex++)
|
|
{
|
|
if ( ChannelNames[ChannelIndex] )
|
|
{
|
|
continue;
|
|
}
|
|
|
|
if ( ! FileChannelNamesUnused.IsEmpty() )
|
|
{
|
|
ChannelNames[ChannelIndex] = FileChannelNamesUnused.Pop(EAllowShrinking::No);
|
|
}
|
|
else
|
|
{
|
|
// stuff something that is not nullptr
|
|
// and won't be found in the file
|
|
// channel will be filled with default value
|
|
const char * DefaultChannelNames[4] =
|
|
{
|
|
"default.R","default.G","default.B","default.A"
|
|
};
|
|
ChannelNames[ChannelIndex] = DefaultChannelNames[ChannelIndex];
|
|
}
|
|
}
|
|
|
|
UE_LOG(LogImageWrapper, Verbose, TEXT("Reading EXR with Channel Names: %s %s %s %s"),
|
|
ANSI_TO_TCHAR(ChannelNames[0]),
|
|
ANSI_TO_TCHAR(ChannelNames[1]),
|
|
ANSI_TO_TCHAR(ChannelNames[2]),
|
|
ANSI_TO_TCHAR(ChannelNames[3]));
|
|
}
|
|
|
|
check(ChannelCount == 1 || ChannelCount == 4);
|
|
|
|
TArray<TArray64<uint8>> ChannelData;
|
|
ChannelData.SetNum(ChannelCount);
|
|
|
|
Imf::PixelType ImfPixelType = (InBitDepth == 16) ? Imf::HALF : Imf::FLOAT;
|
|
int32 BytesPerChannelPixel = (ImfPixelType == Imf::HALF) ? 2 : 4;
|
|
|
|
// openEXR can throw exceptions when parsing invalid data.
|
|
try
|
|
{
|
|
Imf::FrameBuffer ImfFrameBuffer;
|
|
FMemFileIn MemFile(CompressedData.GetData(), CompressedData.Num());
|
|
Imf::InputFile ImfFile(MemFile);
|
|
Imf::Header ImfHeader = ImfFile.header();
|
|
Imath::Box2i ImfDataWindow = ImfHeader.dataWindow();
|
|
for (int32 c = 0; c < ChannelCount; ++c)
|
|
{
|
|
ChannelData[c].SetNumUninitialized((int64)BytesPerChannelPixel * Width * Height);
|
|
|
|
// if you ask for a channel name that is not in the file data, you will get back DefaultValue
|
|
// Use 1.0 as a default value for the alpha channel, in case if it is not present in the EXR, use 0.0 for all other channels.
|
|
bool bIsAlphaChannel = false;
|
|
// only treat a channel named "A" as alpha if it got in slot 3 of RGBA
|
|
// for example TinyEXR writes out all 1-channel gray images with a channel name "A", we do not treat that as alpha
|
|
if ( c == 3 )
|
|
{
|
|
// should be true for DefaultChannelNames[3]
|
|
bIsAlphaChannel = toupper( ChannelNames[c][ strlen(ChannelNames[c]) -1 ] ) == 'A';
|
|
}
|
|
double DefaultValue = bIsAlphaChannel ? 1.0 : 0.0;
|
|
|
|
int64 DataWindowOffset = (ImfDataWindow.min.x + ImfDataWindow.min.y * Width) * ((int64)BytesPerChannelPixel);
|
|
check( (ImfDataWindow.max.x - ImfDataWindow.min.x + 1) == Width );
|
|
check( (ImfDataWindow.max.y - ImfDataWindow.min.y + 1) == Height );
|
|
|
|
// OpenExr does this offset with the pointers cast to intptr_t ; see ImfFrameBuffer.cpp
|
|
char* ChannelBase = (char*)((intptr_t)ChannelData[c].GetData() - DataWindowOffset);
|
|
|
|
ImfFrameBuffer.insert(ChannelNames[c], Imf::Slice(ImfPixelType, ChannelBase, BytesPerChannelPixel, (size_t)BytesPerChannelPixel * Width, 1, 1, DefaultValue));
|
|
}
|
|
ImfFile.setFrameBuffer(ImfFrameBuffer);
|
|
ImfFile.readPixels(ImfDataWindow.min.y, ImfDataWindow.max.y);
|
|
}
|
|
catch (const std::exception& Exception)
|
|
{
|
|
TStringConversion<TStringConvert<char, TCHAR>> Convertor(Exception.what());
|
|
UE_LOG(LogImageWrapper, Error, TEXT("Cannot decompress EXR image: %s"), Convertor.Get());
|
|
SetError(Convertor.Get());
|
|
return;
|
|
}
|
|
|
|
// EXR channels are compressed non-interleaved.
|
|
int64 BytesPerChannel = (int64)BytesPerChannelPixel * Width * Height;
|
|
RawData.SetNumUninitialized(BytesPerChannel * ChannelCount);
|
|
|
|
const uint8 * ChannelDataPointers[4];
|
|
check(ChannelCount == 1 || ChannelCount == 4);
|
|
for (int32 c = 0; c < ChannelCount; ++c)
|
|
{
|
|
ChannelDataPointers[c] = ChannelData[c].GetData();
|
|
}
|
|
|
|
if ( InBitDepth == 16 )
|
|
{
|
|
FFloat16 * Out = (FFloat16 *)&RawData[0];
|
|
|
|
for (int64 OffsetNonInterleaved = 0; OffsetNonInterleaved < BytesPerChannel; OffsetNonInterleaved += 2)
|
|
{
|
|
for (int32 c = 0; c < ChannelCount; ++c)
|
|
{
|
|
const FFloat16 * In = (const FFloat16 *)&ChannelDataPointers[c][OffsetNonInterleaved];
|
|
|
|
*Out = In->GetClampedFinite();
|
|
|
|
//check( ! FMath::IsNaN( Out->GetFloat() ) );
|
|
//check( Out->GetFloat() == In->GetFloat() || ! isfinite( In->GetFloat() ) );
|
|
|
|
Out++;
|
|
}
|
|
}
|
|
}
|
|
else
|
|
{
|
|
check( InBitDepth == 32 );
|
|
|
|
float * Out = (float *)&RawData[0];
|
|
|
|
for (int64 OffsetNonInterleaved = 0; OffsetNonInterleaved < BytesPerChannel; OffsetNonInterleaved += 4)
|
|
{
|
|
for (int32 c = 0; c < ChannelCount; ++c)
|
|
{
|
|
const float * In = (const float *)&ChannelDataPointers[c][OffsetNonInterleaved];
|
|
|
|
float f = *In;
|
|
|
|
// sanitize inf and nan :
|
|
if ( f >= -FLT_MAX && f <= FLT_MAX )
|
|
{
|
|
// finite, leave it
|
|
// nans will fail all compares so not go in here
|
|
}
|
|
else if ( f > FLT_MAX )
|
|
{
|
|
// +inf
|
|
f = FLT_MAX;
|
|
}
|
|
else if ( f < -FLT_MAX )
|
|
{
|
|
// -inf
|
|
f = -FLT_MAX;
|
|
}
|
|
else
|
|
{
|
|
// nan
|
|
f = 0.f;
|
|
}
|
|
|
|
*Out = f;
|
|
|
|
//check( ! FMath::IsNaN( *Out ) );
|
|
|
|
Out++;
|
|
}
|
|
}
|
|
}
|
|
|
|
Format = InFormat;
|
|
BitDepth = InBitDepth;
|
|
}
|
|
|
|
#elif WITH_UNREALEXR_MINIMAL
|
|
|
|
FExrImageWrapper::FExrImageWrapper()
|
|
: FImageWrapperBase()
|
|
{
|
|
}
|
|
|
|
bool FExrImageWrapper::SetCompressed(const void* InCompressedData, int64 InCompressedSize)
|
|
{
|
|
return false;
|
|
}
|
|
|
|
void FExrImageWrapper::Compress(int32 Quality)
|
|
{
|
|
check(RawData.Num());
|
|
|
|
// Ensure we haven't already compressed the file.
|
|
if (CompressedData.Num())
|
|
{
|
|
return;
|
|
}
|
|
|
|
const double StartTime = FPlatformTime::Seconds();
|
|
|
|
// conditions enforced by CanSetRawFormat :
|
|
check(BitDepth == 32);
|
|
check(Format == ERGBFormat::RGBAF);
|
|
|
|
constexpr uint32 EXRHeaderSize = 313;
|
|
|
|
// Indicates the offset, from the start of the file, to get access to the 'full data' of the row
|
|
const uint32 LineOffsetTableSize = 8 * Height;
|
|
const uint64 TotalHeaderSize = EXRHeaderSize + LineOffsetTableSize;
|
|
|
|
// We force use half float as output
|
|
const uint32 PerChannelStride = Width * sizeof(FFloat16);
|
|
|
|
// We store RGB, not A
|
|
const uint32 PixelDataRowSize = PerChannelStride * 3;
|
|
|
|
// Full Row is Row Y coordinate + size of pixel data + pixel data
|
|
const uint32 FullDataRowSize = 2 * sizeof(uint32) + PixelDataRowSize;
|
|
|
|
// Final data allocation
|
|
uint32 CompressedDataReservedSize = TotalHeaderSize + Height * FullDataRowSize;
|
|
CompressedData.Reserve(CompressedDataReservedSize);
|
|
|
|
auto AddDataU32 = [&](uint32 Value) { CompressedData.Append((uint8*)&Value, sizeof(uint32)); };
|
|
// Based on example header at https://www.openexr.com/documentation/openexrfilelayout.pdf
|
|
{
|
|
auto AddHeaderString = [&](const char* Value) { int32 Len = FCStringAnsi::Strlen(Value); CompressedData.Append((uint8*)Value, Len + 1); };
|
|
|
|
// magic number, version, flags
|
|
AddDataU32(0x01312f76); AddDataU32(0x00000002);
|
|
|
|
// Attribute name / type / size: 18 bytes per channel + 1 terminating byte
|
|
AddHeaderString("channels"); AddHeaderString("chlist"); AddDataU32(55);
|
|
//name type (u8,f16,f32) pLinear/reserved xSampling ySampling
|
|
AddHeaderString("B"); AddDataU32(1); AddDataU32(0); AddDataU32(1); AddDataU32(1);
|
|
AddHeaderString("G"); AddDataU32(1); AddDataU32(0); AddDataU32(1); AddDataU32(1);
|
|
AddHeaderString("R"); AddDataU32(1); AddDataU32(0); AddDataU32(1); AddDataU32(1);
|
|
// Separator
|
|
CompressedData.Add(0);
|
|
|
|
// Attribute name / type / size: compression expects a single byte
|
|
AddHeaderString("compression"); AddHeaderString("compression"); AddDataU32(1);
|
|
// NO_COMPRESSION = 0, RLE = 1, ZIPS = 2, ZIP = 3, PIZ = 4, PXR24= 5, B44= 6, B44A= 7,
|
|
CompressedData.Add(0);
|
|
|
|
// Attribute name / type / size
|
|
AddHeaderString("dataWindow"); AddHeaderString("box2i"); AddDataU32(16);
|
|
// Top Left / Bottom Right
|
|
AddDataU32(0); AddDataU32(0); AddDataU32(Width - 1); AddDataU32(Height - 1);
|
|
|
|
// Attribute name / type / size
|
|
AddHeaderString("displayWindow"); AddHeaderString("box2i"); AddDataU32(16);
|
|
AddDataU32(0); AddDataU32(0); AddDataU32(Width - 1); AddDataU32(Height - 1);
|
|
|
|
// Attribute name / type / size
|
|
AddHeaderString("lineOrder"); AddHeaderString("lineOrder"); AddDataU32(1);
|
|
// INCREASING_Y = 0, DECREASING_Y = 1, RANDOM_Y = 2
|
|
CompressedData.Add(0);
|
|
|
|
// Attribute name / type / size
|
|
AddHeaderString("pixelAspectRatio"); AddHeaderString("float"); AddDataU32(4);
|
|
// 1.0f
|
|
AddDataU32(0x3f800000);
|
|
|
|
// Attribute name / type / size
|
|
AddHeaderString("screenWindowCenter"); AddHeaderString("v2f"); AddDataU32(8);
|
|
// 0.0f / 0.0f
|
|
AddDataU32(0); AddDataU32(0);
|
|
|
|
// Attribute name / type / size
|
|
AddHeaderString("screenWindowWidth"); AddHeaderString("float"); AddDataU32(4);
|
|
// 1.0f
|
|
AddDataU32(0x3f800000);
|
|
|
|
// end of header
|
|
CompressedData.Add(0);
|
|
|
|
// Sanity check to make sure header size pre-allocation is still up-to-date
|
|
check(CompressedData.Num() == EXRHeaderSize);
|
|
}
|
|
|
|
uint64 LineOffset = TotalHeaderSize;
|
|
// Line Offset table
|
|
for (int32 RowIndex = 0; RowIndex < Height; ++RowIndex)
|
|
{
|
|
CompressedData.Append((uint8*)&LineOffset, sizeof(uint64));
|
|
LineOffset += FullDataRowSize;
|
|
}
|
|
check(CompressedData.Num() == TotalHeaderSize);
|
|
|
|
// Raw Data output
|
|
const FLinearColor* SrcPixelData = (FLinearColor*)RawData.GetData();
|
|
for (int32 RowIndex = 0; RowIndex < Height; ++RowIndex, SrcPixelData += Width)
|
|
{
|
|
uint32 OutEXRFileDataRowBeginSize = CompressedData.Num();
|
|
|
|
AddDataU32(RowIndex);
|
|
AddDataU32(PixelDataRowSize);
|
|
|
|
// Layout is BBBB..BBGGGG..GGRRRR..RR
|
|
for (int32 ColIndex = 0; ColIndex < Width; ++ColIndex)
|
|
{
|
|
FFloat16 B16(SrcPixelData[ColIndex].B);
|
|
CompressedData.Append((uint8*)&B16.Encoded, sizeof(B16.Encoded));
|
|
}
|
|
|
|
for (int32 ColIndex = 0; ColIndex < Width; ++ColIndex)
|
|
{
|
|
FFloat16 G16(SrcPixelData[ColIndex].G);
|
|
CompressedData.Append((uint8*)&G16.Encoded, sizeof(G16.Encoded));
|
|
}
|
|
|
|
for (int32 ColIndex = 0; ColIndex < Width; ++ColIndex)
|
|
{
|
|
FFloat16 R16(SrcPixelData[ColIndex].R);
|
|
CompressedData.Append((uint8*)&R16.Encoded, sizeof(R16.Encoded));
|
|
}
|
|
|
|
check(CompressedData.Num() == OutEXRFileDataRowBeginSize + FullDataRowSize);
|
|
}
|
|
|
|
// make sure we have written all the data we reserved in the first place
|
|
check(CompressedData.Num() == CompressedDataReservedSize);
|
|
|
|
const double DeltaTime = FPlatformTime::Seconds() - StartTime;
|
|
UE_LOG(LogImageWrapper, Verbose, TEXT("written image in %.3f seconds"), DeltaTime);
|
|
}
|
|
|
|
void FExrImageWrapper::Uncompress(const ERGBFormat InFormat, const int32 InBitDepth)
|
|
{
|
|
ensure(false);
|
|
UE_LOG(LogImageWrapper, Error, TEXT("FExrImageWrapper::Uncompress is not supported"));
|
|
}
|
|
|
|
bool FExrImageWrapper::CanSetRawFormat(const ERGBFormat InFormat, const int32 InBitDepth) const
|
|
{
|
|
return (InFormat == ERGBFormat::RGBAF) && (InBitDepth == 32);
|
|
}
|
|
|
|
ERawImageFormat::Type FExrImageWrapper::GetSupportedRawFormat(const ERawImageFormat::Type InFormat) const
|
|
{
|
|
return ERawImageFormat::RGBA32F;
|
|
}
|
|
|
|
#endif // WITH_UNREALEXR
|