Files
UnrealEngineUWP/Engine/Source/Runtime/ImageWrapper/Private/ImageWrapperModule.cpp
charles bloom 04ffabc485 ImageWrapper and import/export refactor
FImage is now the standard preferred type for a bag of pixels
FImageView can point at pixels without owning an allocation
ERawImageFormat (FImage) converts to ETextureSourceFormat
FImageUtils provides generic load/save and get/set from FImage
major cleanup in the ImageWrappers
new preferred API is through ImageWrapperModule Compress/Decompress
SetRaw/GetRaw functions cleaned up to not have undefined behavior on unexpected formats
ImageWrapper output added for HDR,BMP,TGA
RGBA32F format added and supported throughout import/export
EditorFactories import/export made more generic, most image types handled the same way using FImage now
Deprecate old TSF RGBA order pixel formats
Fix many crashes or bad handling of unusual pixel formats
Pixel access functions should be used instead of switches on pixel type

#preflight 6230ade7e65a7e65d68a187c
#rb julien.stjean,martins.mozeiko,dan.thompson,fabian.giesen

[CL 19397199 by charles bloom in ue5-main branch]
2022-03-15 18:29:37 -04:00

401 lines
10 KiB
C++

// Copyright Epic Games, Inc. All Rights Reserved.
#include "ImageWrapperPrivate.h"
#include "CoreTypes.h"
#include "Modules/ModuleManager.h"
#include "Formats/BmpImageWrapper.h"
#include "Formats/ExrImageWrapper.h"
#include "Formats/HdrImageWrapper.h"
#include "Formats/IcnsImageWrapper.h"
#include "Formats/IcoImageWrapper.h"
#include "Formats/JpegImageWrapper.h"
#include "Formats/PngImageWrapper.h"
#include "Formats/TgaImageWrapper.h"
#include "Formats/TiffImageWrapper.h"
#include "IImageWrapperModule.h"
DEFINE_LOG_CATEGORY(LogImageWrapper);
namespace
{
static const uint8 IMAGE_MAGIC_PNG[] = {0x89, 0x50, 0x4E, 0x47, 0x0D, 0x0A, 0x1A, 0x0A};
static const uint8 IMAGE_MAGIC_JPEG[] = {0xFF, 0xD8, 0xFF};
static const uint8 IMAGE_MAGIC_BMP[] = {0x42, 0x4D};
static const uint8 IMAGE_MAGIC_ICO[] = {0x00, 0x00, 0x01, 0x00};
static const uint8 IMAGE_MAGIC_EXR[] = {0x76, 0x2F, 0x31, 0x01};
static const uint8 IMAGE_MAGIC_ICNS[] = {0x69, 0x63, 0x6E, 0x73};
// Binary for #?RADIANCE
static const uint8 IMAGE_MAGIC_HDR[] = {0x23, 0x3f, 0x52, 0x41, 0x44, 0x49, 0x41, 0x4e, 0x43, 0x45, 0x0a};
// Binary for #?RGBE
static const uint8 IMAGE_MAGIC_HDR2[] = {0x23, 0x3f, 0x52, 0x47, 0x42, 0x45 };
// Tiff has two magic bytes sequence
static const uint8 IMAGE_MAGIC_TIFF_LITTLE_ENDIAN[] = {0x49, 0x49, 0x2A, 0x00};
static const uint8 IMAGE_MAGIC_TIFF_BIG_ENDIAN[] = {0x4D, 0x4D, 0x00, 0x2A};
/** Internal helper function to verify image signature. */
template <int32 MagicCount> bool StartsWith(const uint8* Content, int64 ContentSize, const uint8 (&Magic)[MagicCount])
{
if (ContentSize < MagicCount)
{
return false;
}
for (int32 I = 0; I < MagicCount; ++I)
{
if (Content[I] != Magic[I])
{
return false;
}
}
return true;
}
}
/**
* Image Wrapper module.
*/
class FImageWrapperModule
: public IImageWrapperModule
{
public:
//~ IImageWrapperModule interface
virtual TSharedPtr<IImageWrapper> CreateImageWrapper(const EImageFormat InFormat) override
{
TRACE_CPUPROFILER_EVENT_SCOPE(ImageWrapper.Create);
TSharedPtr<IImageWrapper> ImageWrapper;
// Allocate a helper for the format type
switch(InFormat)
{
#if WITH_UNREALPNG
case EImageFormat::PNG:
ImageWrapper = MakeShared<FPngImageWrapper>();
break;
#endif // WITH_UNREALPNG
#if WITH_UNREALJPEG
case EImageFormat::JPEG:
ImageWrapper = MakeShared<FJpegImageWrapper>();
break;
case EImageFormat::GrayscaleJPEG:
ImageWrapper = MakeShared<FJpegImageWrapper>(1);
break;
#endif //WITH_UNREALJPEG
case EImageFormat::BMP:
ImageWrapper = MakeShared<FBmpImageWrapper>();
break;
case EImageFormat::ICO:
ImageWrapper = MakeShared<FIcoImageWrapper>();
break;
#if WITH_UNREALEXR
case EImageFormat::EXR:
ImageWrapper = MakeShared<FExrImageWrapper>();
break;
#endif
case EImageFormat::ICNS:
ImageWrapper = MakeShared<FIcnsImageWrapper>();
break;
case EImageFormat::TGA:
ImageWrapper = MakeShared<FTgaImageWrapper>();
break;
case EImageFormat::HDR:
ImageWrapper = MakeShared<FHdrImageWrapper>();
break;
#if WITH_LIBTIFF
case EImageFormat::TIFF:
ImageWrapper = MakeShared<UE::ImageWrapper::Private::FTiffImageWrapper>();
break;
#endif // WITH_LIBTIFF
default:
break;
}
return ImageWrapper;
}
virtual EImageFormat DetectImageFormat(const void* CompressedData, int64 CompressedSize) override
{
EImageFormat Format = EImageFormat::Invalid;
if (StartsWith((uint8*) CompressedData, CompressedSize, IMAGE_MAGIC_PNG))
{
Format = EImageFormat::PNG;
}
else if (StartsWith((uint8*) CompressedData, CompressedSize, IMAGE_MAGIC_JPEG))
{
Format = EImageFormat::JPEG; // @Todo: Should we detect grayscale vs non-grayscale?
}
else if (StartsWith((uint8*) CompressedData, CompressedSize, IMAGE_MAGIC_BMP))
{
Format = EImageFormat::BMP;
}
else if (StartsWith((uint8*) CompressedData, CompressedSize, IMAGE_MAGIC_ICO))
{
Format = EImageFormat::ICO;
}
else if (StartsWith((uint8*) CompressedData, CompressedSize, IMAGE_MAGIC_EXR))
{
Format = EImageFormat::EXR;
}
else if (StartsWith((uint8*) CompressedData, CompressedSize, IMAGE_MAGIC_ICNS))
{
Format = EImageFormat::ICNS;
}
else if (StartsWith((uint8*)CompressedData, CompressedSize, IMAGE_MAGIC_HDR) ||
StartsWith((uint8*)CompressedData, CompressedSize, IMAGE_MAGIC_HDR2))
{
Format = EImageFormat::HDR;
}
else if (StartsWith((uint8*)CompressedData, CompressedSize, IMAGE_MAGIC_TIFF_LITTLE_ENDIAN))
{
Format = EImageFormat::TIFF;
}
else if (StartsWith((uint8*)CompressedData, CompressedSize, IMAGE_MAGIC_TIFF_BIG_ENDIAN))
{
Format = EImageFormat::TIFF;
}
else if ( FTgaImageWrapper::IsTGAHeader(CompressedData, CompressedSize) )
{
Format = EImageFormat::TGA;
}
return Format;
}
virtual const TCHAR * GetExtension(EImageFormat Format) override
{
switch(Format)
{
case EImageFormat::PNG: return TEXT("png");
case EImageFormat::JPEG: return TEXT("jpg");
case EImageFormat::GrayscaleJPEG: return TEXT("jpg");
case EImageFormat::BMP: return TEXT("bmp");
case EImageFormat::ICO: return TEXT("ico");
case EImageFormat::EXR: return TEXT("exr");
case EImageFormat::ICNS: return TEXT("icns");
case EImageFormat::TGA: return TEXT("tga");
case EImageFormat::HDR: return TEXT("hdr");
case EImageFormat::TIFF: return TEXT("tiff");
case EImageFormat::Invalid:
default:
check(0);
return nullptr;
}
}
virtual EImageFormat GetImageFormatFromExtension(const TCHAR * Name) override
{
const TCHAR * Dot = FCString::Strrchr(Name,TEXT('.'));
if ( Dot )
{
Name = Dot+1;
}
if ( FCString::Stricmp(Name,TEXT("png")) == 0 )
{
return EImageFormat::PNG;
}
else if ( FCString::Stricmp(Name,TEXT("jpg")) == 0
|| FCString::Stricmp(Name,TEXT("jpeg")) == 0 )
{
return EImageFormat::JPEG;
}
else if ( FCString::Stricmp(Name,TEXT("bmp")) == 0 )
{
return EImageFormat::BMP;
}
else if ( FCString::Stricmp(Name,TEXT("ico")) == 0 )
{
return EImageFormat::ICO;
}
else if ( FCString::Stricmp(Name,TEXT("exr")) == 0 )
{
return EImageFormat::EXR;
}
else if ( FCString::Stricmp(Name,TEXT("icns")) == 0 )
{
return EImageFormat::ICNS;
}
else if ( FCString::Stricmp(Name,TEXT("tga")) == 0 )
{
return EImageFormat::TGA;
}
else if ( FCString::Stricmp(Name,TEXT("hdr")) == 0 )
{
return EImageFormat::HDR;
}
else if ( FCString::Stricmp(Name,TEXT("tiff")) == 0 ||
FCString::Stricmp(Name,TEXT("tif")) == 0 )
{
return EImageFormat::TIFF;
}
else
{
UE_LOG(LogImageWrapper,Warning,TEXT("GetImageFormatFromExtension not found : %s\n"),Name);
return EImageFormat::Invalid;
}
}
virtual ERawImageFormat::Type ConvertRGBFormat(ERGBFormat RGBFormat,int BitDepth,bool * bIsExactMatch) override
{
return IImageWrapper::ConvertRGBFormat(RGBFormat,BitDepth,bIsExactMatch);
}
virtual void ConvertRawImageFormat(ERawImageFormat::Type RawFormat, ERGBFormat & OutFormat,int & OutBitDepth) override
{
IImageWrapper::ConvertRawImageFormat(RawFormat,OutFormat,OutBitDepth);
}
virtual EImageFormat GetDefaultOutputFormat(ERawImageFormat::Type RawFormat) override
{
switch(RawFormat)
{
case ERawImageFormat::G8:
case ERawImageFormat::BGRA8:
case ERawImageFormat::RGBA16:
case ERawImageFormat::G16:
return EImageFormat::PNG;
case ERawImageFormat::BGRE8:
return EImageFormat::HDR;
case ERawImageFormat::RGBA16F:
case ERawImageFormat::RGBA32F:
case ERawImageFormat::R16F:
return EImageFormat::EXR;
default:
check(0);
return EImageFormat::Invalid;
}
}
virtual bool DecompressImage(const void* InCompressedData, int64 InCompressedSize, FImage & OutImage) override
{
TRACE_CPUPROFILER_EVENT_SCOPE(ImageWrapper.Decompress);
EImageFormat ImageFormat = DetectImageFormat(InCompressedData,InCompressedSize);
if ( ImageFormat == EImageFormat::Invalid )
{
return false;
}
TSharedPtr<IImageWrapper> ImageWrapper = CreateImageWrapper(ImageFormat);
if ( ! ImageWrapper.IsValid() )
{
return false;
}
if ( ! ImageWrapper->SetCompressed(InCompressedData,InCompressedSize) )
{
return false;
}
if ( ! ImageWrapper->GetRawImage(OutImage) )
{
return false;
}
return true;
}
virtual bool CompressImage(TArray64<uint8> & OutData, EImageFormat ToFormat, const FImageView & InImage, int32 Quality) override
{
TRACE_CPUPROFILER_EVENT_SCOPE(ImageWrapper.Compress);
TSharedPtr<IImageWrapper> ImageWrapper = CreateImageWrapper(ToFormat);
if ( ! ImageWrapper.IsValid() )
{
return false;
}
ERGBFormat RGBFormat;
int BitDepth;
ConvertRawImageFormat(InImage.Format,RGBFormat,BitDepth);
// can't save slices :
check(InImage.NumSlices == 1 );
FImageView WriteImage = InImage;
FImage TempImage;
if ( ! ImageWrapper->CanSetRawFormat(RGBFormat,BitDepth) )
{
// output image format may not support this pixel format
// in that case conversion is needed
//
// @@!! warn or set a flag when this conversion is lossy?
ERawImageFormat::Type NewFormat = ImageWrapper->GetSupportedRawFormat(InImage.Format);
check( NewFormat != InImage.Format );
// we will write to the image wrapper using "NewFormat"
// do a blit to convert InImage to NewFormat
//
TempImage.Init(InImage.SizeX,InImage.SizeY,NewFormat,ERawImageFormat::GetDefaultGammaSpace(NewFormat));
WriteImage = TempImage;
FImageCore::CopyImage(InImage,WriteImage);
// re-get the RGBFormat we will set :
ConvertRawImageFormat(NewFormat,RGBFormat,BitDepth);
check( ImageWrapper->CanSetRawFormat(RGBFormat,BitDepth) );
}
else if ( InImage.GetGammaSpace() != ERawImageFormat::GetDefaultGammaSpace(InImage.Format) )
{
// ImageWrapper SetRaw() does not accept gamma information
// it assumes U8 = SRGB and Float = Linear
// eg. if you save a U8 surface with linear gamma
// go ahead and write the U8 bytes without changing them
// I think that's probably what is intended
// when someone writes Linear U8 to PNG they don't want me to do a gamma transform
// @@!! TODO: write the gamma setting to the output when possible, eg. for DDS output
}
if ( ! ImageWrapper->SetRaw(WriteImage.RawData,WriteImage.GetImageSizeBytes(),WriteImage.SizeX,WriteImage.SizeY,RGBFormat,BitDepth) )
{
return false;
}
OutData = ImageWrapper->GetCompressed(Quality);
if ( OutData.IsEmpty() )
{
return false;
}
return true;
}
public:
//~ IModuleInterface interface
virtual void StartupModule() override { }
virtual void ShutdownModule() override { }
};
IMPLEMENT_MODULE(FImageWrapperModule, ImageWrapper);