Files
UnrealEngineUWP/Engine/Source/Runtime/ImageWrapper/Private/Formats/JpegImageWrapper.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

389 lines
9.8 KiB
C++

// Copyright Epic Games, Inc. All Rights Reserved.
#include "JpegImageWrapper.h"
#include "Math/Color.h"
#include "Misc/ScopeLock.h"
#include "ImageWrapperPrivate.h"
#if WITH_UNREALJPEG
#ifdef __clang__
#pragma clang diagnostic push
#pragma clang diagnostic ignored "-Wshadow"
#if PLATFORM_UNIX || PLATFORM_MAC
#pragma clang diagnostic ignored "-Wshift-negative-value" // clang 3.7.0
#endif
#endif
#if WITH_LIBJPEGTURBO
THIRD_PARTY_INCLUDES_START
#pragma push_macro("DLLEXPORT")
#undef DLLEXPORT // libjpeg-turbo defines DLLEXPORT as well
#include "turbojpeg.h"
#pragma pop_macro("DLLEXPORT")
THIRD_PARTY_INCLUDES_END
#else
PRAGMA_DISABLE_SHADOW_VARIABLE_WARNINGS
#include "jpgd.h"
#include "jpgd.cpp"
#include "jpge.h"
#include "jpge.cpp"
PRAGMA_ENABLE_SHADOW_VARIABLE_WARNINGS
#endif // WITH_LIBJPEGTURBO
#ifdef __clang__
#pragma clang diagnostic pop
#endif
DEFINE_LOG_CATEGORY_STATIC(JPEGLog, Log, All);
#if WITH_LIBJPEGTURBO
namespace
{
int ConvertTJpegPixelFormat(ERGBFormat InFormat)
{
switch (InFormat)
{
case ERGBFormat::BGRA: return TJPF_BGRA;
case ERGBFormat::Gray: return TJPF_GRAY;
case ERGBFormat::RGBA: return TJPF_RGBA;
default: return TJPF_RGBA;
}
}
}
#endif // WITH_LIBJPEGTURBO
// Only allow one thread to use JPEG decoder at a time (it's not thread safe)
FCriticalSection GJPEGSection;
/* FJpegImageWrapper structors
*****************************************************************************/
FJpegImageWrapper::FJpegImageWrapper(int32 InNumComponents)
: FImageWrapperBase()
, NumComponents(InNumComponents)
#if WITH_LIBJPEGTURBO
, Compressor(tjInitCompress())
, Decompressor(tjInitDecompress())
#endif // WITH_LIBJPEGTURBO
{ }
#if WITH_LIBJPEGTURBO
FJpegImageWrapper::~FJpegImageWrapper()
{
FScopeLock JPEGLock(&GJPEGSection);
if (Compressor)
{
tjDestroy(Compressor);
}
if (Decompressor)
{
tjDestroy(Decompressor);
}
}
#endif // WITH_LIBJPEGTURBO
/* FImageWrapperBase interface
*****************************************************************************/
bool FJpegImageWrapper::SetCompressed(const void* InCompressedData, int64 InCompressedSize)
{
#if WITH_LIBJPEGTURBO
return SetCompressedTurbo(InCompressedData, InCompressedSize);
#else
// jpgd doesn't support 64-bit sizes.
if (InCompressedSize < 0 || InCompressedSize > MAX_uint32)
{
return false;
}
jpgd::jpeg_decoder_mem_stream jpeg_memStream((uint8*)InCompressedData, (uint32)InCompressedSize);
jpgd::jpeg_decoder decoder(&jpeg_memStream);
if (decoder.get_error_code() != jpgd::JPGD_SUCCESS)
return false;
bool bResult = FImageWrapperBase::SetCompressed(InCompressedData, InCompressedSize);
// We don't support 16 bit jpegs
BitDepth = 8;
Width = decoder.get_width();
Height = decoder.get_height();
switch (decoder.get_num_components())
{
case 1:
Format = ERGBFormat::Gray;
break;
case 3:
Format = ERGBFormat::RGBA;
break;
default:
return false;
}
return bResult;
#endif
}
// CanSetRawFormat returns true if SetRaw will accept this format
bool FJpegImageWrapper::CanSetRawFormat(const ERGBFormat InFormat, const int32 InBitDepth) const
{
return ((InFormat == ERGBFormat::RGBA || InFormat == ERGBFormat::BGRA || InFormat == ERGBFormat::Gray) && InBitDepth == 8);
}
// returns InFormat if supported, else maps to something supported
ERawImageFormat::Type FJpegImageWrapper::GetSupportedRawFormat(const ERawImageFormat::Type InFormat) const
{
switch(InFormat)
{
case ERawImageFormat::G8:
case ERawImageFormat::BGRA8:
return InFormat; // directly supported
case ERawImageFormat::BGRE8:
case ERawImageFormat::RGBA16:
case ERawImageFormat::RGBA16F:
case ERawImageFormat::RGBA32F:
case ERawImageFormat::G16:
case ERawImageFormat::R16F:
return ERawImageFormat::BGRA8; // needs conversion
default:
check(0);
return ERawImageFormat::BGRA8;
};
}
bool FJpegImageWrapper::SetRaw(const void* InRawData, int64 InRawSize, const int32 InWidth, const int32 InHeight, const ERGBFormat InFormat, const int32 InBitDepth, const int32 InBytesPerRow)
{
check((InFormat == ERGBFormat::RGBA || InFormat == ERGBFormat::BGRA || InFormat == ERGBFormat::Gray) && InBitDepth == 8);
bool bResult = FImageWrapperBase::SetRaw(InRawData, InRawSize, InWidth, InHeight, InFormat, InBitDepth, InBytesPerRow);
return bResult;
}
void FJpegImageWrapper::Compress(int32 Quality)
{
if (Quality == 0)
{
//default
Quality = 85;
}
else if (Quality == (int32)EImageCompressionQuality::Uncompressed)
{
// fix = 1 (Uncompressed) was previously treated as max-compress
Quality = 100;
}
else
{
ensure(Quality >= 1 && Quality <= 100);
#define JPEG_QUALITY_MIN 40
// JPEG should not be used below quality JPEG_QUALITY_MIN
Quality = FMath::Clamp(Quality, JPEG_QUALITY_MIN, 100);
}
#if WITH_LIBJPEGTURBO
CompressTurbo(Quality);
#else
if (CompressedData.Num() == 0)
{
FScopeLock JPEGLock(&GJPEGSection);
check(RawData.Num());
check(Width > 0);
check(Height > 0);
// re-order components if required - JPEGs expect RGBA
if(RawFormat == ERGBFormat::BGRA)
{
FColor* Colors = (FColor*)RawData.GetData();
const int32 NumColors = RawData.Num() / 4;
for(int32 ColorIndex = 0; ColorIndex < NumColors; ColorIndex++)
{
Colors[ColorIndex].B = Colors[ColorIndex].B ^ Colors[ColorIndex].R;
Colors[ColorIndex].R = Colors[ColorIndex].R ^ Colors[ColorIndex].B;
Colors[ColorIndex].B = Colors[ColorIndex].B ^ Colors[ColorIndex].R;
}
}
CompressedData.Reset(RawData.Num());
CompressedData.AddUninitialized(RawData.Num());
// Note: OutBufferSize intentionally uses int64_t type as that's what jpge::compress_image_to_jpeg_file_in_memory expects.
// UE int64 type is not compatible with int64_t on all compilers (int64_t may be `long`, while int64 is `long long`).
int64_t OutBufferSize = CompressedData.Num();
jpge::params Parameters;
Parameters.m_quality = Quality;
bool bSuccess = jpge::compress_image_to_jpeg_file_in_memory(
CompressedData.GetData(), OutBufferSize, Width, Height, NumComponents, RawData.GetData(), Parameters);
check(bSuccess);
CompressedData.RemoveAt((int64)OutBufferSize, CompressedData.Num() - (int64)OutBufferSize);
}
#endif
}
void FJpegImageWrapper::Uncompress(const ERGBFormat InFormat, int32 InBitDepth)
{
#if WITH_LIBJPEGTURBO
UncompressTurbo(InFormat, InBitDepth);
#else
// Ensure we haven't already uncompressed the file.
if (RawData.Num() != 0)
{
return;
}
// Get the number of channels we need to extract
int Channels = 0;
if ((InFormat == ERGBFormat::RGBA || InFormat == ERGBFormat::BGRA) && InBitDepth == 8)
{
Channels = 4;
}
else if (InFormat == ERGBFormat::Gray && InBitDepth == 8)
{
Channels = 1;
}
else
{
check(false);
return;
}
FScopeLock JPEGLock(&GJPEGSection);
check(CompressedData.Num());
int32 NumColors;
uint8* OutData = jpgd::decompress_jpeg_image_from_memory(
CompressedData.GetData(), CompressedData.Num(), &Width, &Height, &NumColors, Channels, (int)InFormat);
if (OutData)
{
RawData.Reset(Width * Height * Channels);
RawData.AddUninitialized(Width * Height * Channels);
FMemory::Memcpy(RawData.GetData(), OutData, RawData.Num());
FMemory::Free(OutData);
}
else
{
UE_LOG(LogImageWrapper, Error, TEXT("JPEG Decompress Error"));
RawData.Empty();
}
#endif
}
#if WITH_LIBJPEGTURBO
bool FJpegImageWrapper::SetCompressedTurbo(const void* InCompressedData, int64 InCompressedSize)
{
FScopeLock JPEGLock(&GJPEGSection);
check(Decompressor);
int ImageWidth;
int ImageHeight;
int SubSampling;
int ColorSpace;
if (tjDecompressHeader3(Decompressor, reinterpret_cast<const uint8*>(InCompressedData), InCompressedSize, &ImageWidth, &ImageHeight, &SubSampling, &ColorSpace) != 0)
{
return false;
}
const bool bResult = FImageWrapperBase::SetCompressed(InCompressedData, InCompressedSize);
// set after call to base SetCompressed as it will reset members
Width = ImageWidth;
Height = ImageHeight;
BitDepth = 8; // We don't support 16 bit jpegs
Format = SubSampling == TJSAMP_GRAY ? ERGBFormat::Gray : ERGBFormat::RGBA;
return bResult;
}
void FJpegImageWrapper::CompressTurbo(int32 Quality)
{
if (CompressedData.Num() == 0)
{
FScopeLock JPEGLock(&GJPEGSection);
// Quality mapping should have already been done
check( Quality >= JPEG_QUALITY_MIN && Quality <= 100 );
check(Compressor);
check(RawData.Num());
check(Width > 0);
check(Height > 0);
const int PixelFormat = ConvertTJpegPixelFormat(RawFormat);
const int Subsampling = TJSAMP_420;
const int Flags = TJFLAG_NOREALLOC | TJFLAG_FASTDCT;
unsigned long OutBufferSize = tjBufSize(Width, Height, Subsampling);
CompressedData.SetNum(OutBufferSize);
unsigned char* OutBuffer = CompressedData.GetData();
const bool bSuccess = tjCompress2(Compressor, RawData.GetData(), Width, RawBytesPerRow, Height, PixelFormat, &OutBuffer, &OutBufferSize, Subsampling, Quality, Flags) == 0;
check(bSuccess);
CompressedData.SetNum(OutBufferSize);
}
}
void FJpegImageWrapper::UncompressTurbo(const ERGBFormat InFormat, int32 InBitDepth)
{
// Ensure we haven't already uncompressed the file.
if (RawData.Num() != 0)
{
return;
}
// Get the number of channels we need to extract
int Channels = 0;
if ((InFormat == ERGBFormat::RGBA || InFormat == ERGBFormat::BGRA) && InBitDepth == 8)
{
Channels = 4;
}
else if (InFormat == ERGBFormat::Gray && InBitDepth == 8)
{
Channels = 1;
}
else
{
check(false);
}
FScopeLock JPEGLock(&GJPEGSection);
check(Decompressor);
check(CompressedData.Num());
RawData.Reset(Width * Height * Channels);
RawData.AddUninitialized(Width * Height * Channels);
const int PixelFormat = ConvertTJpegPixelFormat(InFormat);
const int Flags = TJFLAG_NOREALLOC | TJFLAG_FASTDCT;
if (tjDecompress2(Decompressor, CompressedData.GetData(), CompressedData.Num(), RawData.GetData(), Width, 0, Height, PixelFormat, Flags) != 0)
{
UE_LOG(LogImageWrapper, Error, TEXT("JPEG Decompress Error"));
RawData.Empty();
return;
}
}
#endif // WITH_LIBJPEGTURBO
#endif //WITH_JPEG