Rework libjpeg Auxiliary supports to address some problems (#638)

This commit is contained in:
Chuck Walbourn 2025-10-23 18:56:48 -07:00 committed by GitHub
parent f3b184489b
commit f04d5cb47f
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
6 changed files with 125 additions and 56 deletions

View File

@ -91,9 +91,10 @@ namespace
{
jpeg_error_mgr err;
jpeg_decompress_struct dec;
JPEG_FLAGS userFlags;
public:
JPEGDecompress() : err{}, dec{}
JPEGDecompress(JPEG_FLAGS flags) : err{}, dec{}, userFlags(flags)
{
jpeg_std_error(&err);
err.error_exit = &OnJPEGError;
@ -110,23 +111,14 @@ namespace
jpeg_stdio_src(&dec, fin);
}
static DXGI_FORMAT TranslateColor(J_COLOR_SPACE colorspace) noexcept
DXGI_FORMAT TranslateColor(J_COLOR_SPACE colorspace) noexcept
{
switch (colorspace)
{
case JCS_GRAYSCALE: // 1 component
return DXGI_FORMAT_R8_UNORM;
case JCS_RGB: // 3 component, Standard RGB
return DXGI_FORMAT_R8G8B8A8_UNORM;
case JCS_CMYK: // 4 component. WIC retuns this for CMYK
return DXGI_FORMAT_R8G8B8A8_UNORM;
#ifdef LIBJPEG_TURBO_VERSION
case JCS_EXT_RGBX: // 4 component
case JCS_EXT_RGBA:
return DXGI_FORMAT_R8G8B8A8_UNORM;
case JCS_RGB565:
return DXGI_FORMAT_B5G6R5_UNORM;
#endif
return (userFlags & JPEG_FLAGS_DEFAULT_LINEAR) ? DXGI_FORMAT_R8G8B8A8_UNORM : DXGI_FORMAT_R8G8B8A8_UNORM_SRGB;
case JCS_YCbCr: // 3 component, YCbCr
default:
return DXGI_FORMAT_UNKNOWN;
@ -146,7 +138,8 @@ namespace
{
throw std::runtime_error{ "unexpected out_color_space in jpeg_decompress_struct" };
}
if (metadata.format == DXGI_FORMAT_R8G8B8A8_UNORM)
if (metadata.format == DXGI_FORMAT_R8G8B8A8_UNORM
|| metadata.format == DXGI_FORMAT_R8G8B8A8_UNORM_SRGB)
{
metadata.miscFlags2 |= TEX_ALPHA_MODE_OPAQUE;
}
@ -168,19 +161,22 @@ namespace
return S_OK;
}
#if !defined(LIBJPEG_TURBO_VERSION)
#ifndef LIBJPEG_TURBO_VERSION
// shift pixels with padding in reverse order (to make it work in-memory)
void ShiftPixels(ScratchImage& image) noexcept
void ShiftPixels(const Image& image) noexcept
{
size_t num_pixels = dec.output_width * dec.output_height;
uint8_t* dst = image.GetPixels();
const uint8_t* src = dst;
for (size_t i = num_pixels - 1; i > 0; i -= 1)
uint8_t* scanline = image.pixels;
for (size_t y = 0; y < image.height; ++y)
{
dst[4*i + 0] = src[3*i + 0];
dst[4*i + 1] = src[3*i + 1];
dst[4*i + 2] = src[3*i + 2];
dst[4*i + 3] = 0;
for (size_t i = (image.width - 1); i > 0; i -= 1)
{
scanline[4*i + 0] = scanline[3*i + 0];
scanline[4*i + 1] = scanline[3*i + 1];
scanline[4*i + 2] = scanline[3*i + 2];
scanline[4*i + 3] = 0xff;
}
scanline += image.rowPitch;
}
}
#endif
@ -199,31 +195,26 @@ namespace
break;
}
GetMetadata(metadata);
// if the JPEG library doesn't have color space conversion, it should be ERROR_NOT_SUPPORTED
if (dec.jpeg_color_space == JCS_YCCK)
{
// CMYK is the known case
if (dec.out_color_space == JCS_CMYK)
return HRESULT_E_NOT_SUPPORTED;
}
if (auto hr = image.Initialize2D(metadata.format, metadata.width, metadata.height, metadata.arraySize, metadata.mipLevels); FAILED(hr))
if (auto hr = image.Initialize2D(metadata.format, metadata.width, metadata.height, 1u, 1u); FAILED(hr))
return hr;
#ifdef LIBJPEG_TURBO_VERSION
// grayscale is the only color space which uses 1 component
if (dec.out_color_space != JCS_GRAYSCALE)
// if there is no proper conversion to 4 component, E_FAIL...
dec.out_color_space = JCS_EXT_RGBX;
{
dec.out_color_space = JCS_EXT_RGBA;
}
#endif
if (jpeg_start_decompress(&dec) == false)
return E_FAIL;
uint8_t* dest = image.GetPixels();
const size_t stride = dec.output_width * static_cast<size_t>(dec.output_components);
const auto img = *image.GetImage(0, 0, 0);
std::vector<JSAMPROW> rows(dec.output_height);
for (size_t i = 0u; i < dec.output_height; ++i)
rows[i] = dest + (stride * i);
{
rows[i] = img.pixels + (i * img.rowPitch);
}
JDIMENSION leftover = dec.output_height;
while (leftover > 0)
@ -231,14 +222,18 @@ namespace
JDIMENSION consumed = jpeg_read_scanlines(&dec, &rows[dec.output_scanline], leftover);
leftover -= consumed;
}
if (jpeg_finish_decompress(&dec) == false)
return E_FAIL;
#if !defined(LIBJPEG_TURBO_VERSION)
#ifndef LIBJPEG_TURBO_VERSION
// if NOT TurboJPEG, we need to make 3 component images to 4 component image
if (dec.out_color_space != JCS_GRAYSCALE)
ShiftPixels(image);
{
ShiftPixels(img);
}
#endif
return S_OK;
}
@ -251,11 +246,12 @@ namespace
class JPEGCompress final
{
jpeg_error_mgr err{};
jpeg_compress_struct enc{};
jpeg_error_mgr err;
jpeg_compress_struct enc;
JPEG_FLAGS userFlags;
public:
JPEGCompress() : err{}, enc{}
JPEGCompress(JPEG_FLAGS flags) : err{}, enc{}, userFlags(flags)
{
jpeg_std_error(&err);
err.error_exit = &OnJPEGError;
@ -272,7 +268,6 @@ namespace
jpeg_stdio_dest(&enc, fout);
}
/// @todo More correct DXGI_FORMAT mapping
HRESULT WriteImage(const Image& image) noexcept(false)
{
switch (image.format)
@ -283,34 +278,74 @@ namespace
break;
#ifdef LIBJPEG_TURBO_VERSION
case DXGI_FORMAT_R8G8B8A8_UNORM:
case DXGI_FORMAT_R8G8B8A8_UNORM_SRGB:
enc.input_components = 4;
enc.in_color_space = JCS_EXT_RGBA;
break;
case DXGI_FORMAT_B8G8R8A8_UNORM:
case DXGI_FORMAT_B8G8R8A8_UNORM_SRGB:
enc.input_components = 4;
enc.in_color_space = JCS_EXT_BGRA;
break;
case DXGI_FORMAT_B8G8R8X8_UNORM:
case DXGI_FORMAT_B8G8R8X8_UNORM_SRGB:
enc.input_components = 4;
enc.in_color_space = JCS_EXT_BGRX;
break;
#else
case DXGI_FORMAT_R8G8B8A8_UNORM:
case DXGI_FORMAT_R8G8B8A8_UNORM_SRGB:
enc.input_components = 3;
enc.in_color_space = JCS_RGB;
break;
#endif
default:
return HRESULT_E_NOT_SUPPORTED;
}
// TODO: Add sRGB intent?
enc.image_width = static_cast<JDIMENSION>(image.width);
enc.image_height = static_cast<JDIMENSION>(image.height);
jpeg_set_defaults(&enc);
jpeg_set_quality(&enc, 100, true);
// we will write a row each time ...
jpeg_start_compress(&enc, true);
const size_t stride = enc.image_width * static_cast<size_t>(enc.input_components);
while (enc.next_scanline < enc.image_height)
#ifndef LIBJPEG_TURBO_VERSION
if (enc.input_components == 3)
{
JSAMPROW rows[1]{ image.pixels + stride * enc.next_scanline };
jpeg_write_scanlines(&enc, rows, 1);
const size_t stride = enc.image_width * static_cast<size_t>(enc.input_components);
auto scanline = std::make_unique<uint8_t[]>(stride);
JSAMPROW rows[1]{ scanline.get() };
while (enc.next_scanline < enc.image_height)
{
// Copy 4 to 3 components
const uint8_t* src = image.pixels + enc.next_scanline * image.rowPitch;
uint8_t* dst = scanline.get();
for(size_t i=0; i < image.width; ++i)
{
dst[3*i + 0] = src[4*i + 0];
dst[3*i + 1] = src[4*i + 1];
dst[3*i + 2] = src[4*i + 2];
}
jpeg_write_scanlines(&enc, rows, 1);
}
}
else
#endif
{
while (enc.next_scanline < enc.image_height)
{
JSAMPROW rows[1]{ image.pixels + image.rowPitch * enc.next_scanline };
jpeg_write_scanlines(&enc, rows, 1);
}
}
jpeg_finish_compress(&enc);
return S_OK;
}
@ -320,6 +355,7 @@ namespace
_Use_decl_annotations_
HRESULT DirectX::GetMetadataFromJPEGFile(
const wchar_t* file,
JPEG_FLAGS flags,
TexMetadata& metadata)
{
if (!file)
@ -328,7 +364,7 @@ HRESULT DirectX::GetMetadataFromJPEGFile(
try
{
auto fin = OpenFILE(file);
JPEGDecompress decoder{};
JPEGDecompress decoder(flags);
decoder.UseInput(fin.get());
return decoder.GetHeader(metadata);
}
@ -353,6 +389,7 @@ HRESULT DirectX::GetMetadataFromJPEGFile(
_Use_decl_annotations_
HRESULT DirectX::LoadFromJPEGFile(
const wchar_t* file,
JPEG_FLAGS flags,
TexMetadata* metadata,
ScratchImage&image)
{
@ -364,7 +401,7 @@ HRESULT DirectX::LoadFromJPEGFile(
try
{
auto fin = OpenFILE(file);
JPEGDecompress decoder{};
JPEGDecompress decoder(flags);
decoder.UseInput(fin.get());
if (!metadata)
return decoder.GetImage(image);
@ -394,6 +431,7 @@ HRESULT DirectX::LoadFromJPEGFile(
_Use_decl_annotations_
HRESULT DirectX::SaveToJPEGFile(
const Image& image,
JPEG_FLAGS flags,
const wchar_t* file)
{
if (!file)
@ -402,7 +440,7 @@ HRESULT DirectX::SaveToJPEGFile(
try
{
auto fout = CreateFILE(file);
JPEGCompress encoder{};
JPEGCompress encoder(flags);
encoder.UseOutput(fout.get());
return encoder.WriteImage(image);
}

View File

@ -18,16 +18,39 @@
namespace DirectX
{
enum JPEG_FLAGS : uint32_t
{
JPEG_FLAGS_NONE = 0x0,
JPEG_FLAGS_DEFAULT_LINEAR = 0x1,
// Return non-SRGB formats intead of sRGB
};
DIRECTX_TEX_API HRESULT __cdecl GetMetadataFromJPEGFile(
_In_z_ const wchar_t* szFile,
JPEG_FLAGS flags,
_Out_ TexMetadata& metadata);
DIRECTX_TEX_API HRESULT __cdecl LoadFromJPEGFile(
_In_z_ const wchar_t* szFile,
JPEG_FLAGS flags,
_Out_opt_ TexMetadata* metadata,
_Out_ ScratchImage& image);
DIRECTX_TEX_API HRESULT __cdecl SaveToJPEGFile(
_In_ const Image& image,
JPEG_FLAGS flags,
_In_z_ const wchar_t* szFile);
#ifdef __clang__
#pragma clang diagnostic push
#pragma clang diagnostic ignored "-Wdeprecated-dynamic-exception-spec"
#pragma clang diagnostic ignored "-Wextra-semi-stmt"
#endif
DEFINE_ENUM_FLAG_OPERATORS(JPEG_FLAGS);
#ifdef __clang__
#pragma clang diagnostic pop
#endif
}

View File

@ -85,6 +85,8 @@ For a full change history, see [CHANGELOG.md](https://github.com/microsoft/Direc
* The CMake projects require 3.21 or later. VS 2019 users will need to install a standalone version of CMake 3.21 or later and add it to their PATH.
* Starting with the October 2025 release, the _Auxiliary_ functions for loading and saving JPEG and PNG files now take a flag parameter after the filename.
* Starting with the March 2025 release, Windows 7 and Windows 8.0 support has been retired.
* Starting with the July 2022 release, the ``bool forceSRGB`` parameter for the **CreateTextureEx** and **CreateShaderResourceViewEx** functions is now a ``CREATETEX_FLAGS`` typed enum bitmask flag parameter. This may have a _breaking change_ impact to client code. Replace ``true`` with ``CREATETEX_FORCE_SRGB`` and ``false`` with ``CREATETEX_DEFAULT``.

View File

@ -618,7 +618,7 @@ namespace
#endif
#ifdef USE_LIBJPEG
case CODEC_JPEG:
return SaveToJPEGFile(img, szOutputFile);
return SaveToJPEGFile(img, JPEG_FLAGS_NONE, szOutputFile);
#endif
#ifdef USE_LIBPNG
case CODEC_PNG:
@ -1423,7 +1423,7 @@ int __cdecl wmain(_In_ int argc, _In_z_count_(argc) wchar_t* argv[])
#ifdef USE_LIBJPEG
else if (_wcsicmp(ext.c_str(), L".jpg") == 0 || _wcsicmp(ext.c_str(), L".jpeg") == 0)
{
hr = LoadFromJPEGFile(curpath.c_str(), &info, *image);
hr = LoadFromJPEGFile(curpath.c_str(), JPEG_FLAGS_NONE, &info, *image);
if (FAILED(hr))
{
wprintf(L" FAILED (%08X%ls)\n", static_cast<unsigned int>(hr), GetErrorDesc(hr));

View File

@ -2157,7 +2157,13 @@ int __cdecl wmain(_In_ int argc, _In_z_count_(argc) wchar_t* argv[])
#ifdef USE_LIBJPEG
else if (_wcsicmp(ext.c_str(), L".jpg") == 0 || _wcsicmp(ext.c_str(), L".jpeg") == 0)
{
hr = LoadFromJPEGFile(curpath.c_str(), &info, *image);
JPEG_FLAGS jpegFlags = JPEG_FLAGS_NONE;
if (dwOptions & (UINT64_C(1) << OPT_IGNORE_SRGB_METADATA))
{
jpegFlags |= JPEG_FLAGS_DEFAULT_LINEAR;
}
hr = LoadFromJPEGFile(curpath.c_str(), jpegFlags, &info, *image);
if (FAILED(hr))
{
wprintf(L" FAILED (%08X%ls)\n", static_cast<unsigned int>(hr), GetErrorDesc(hr));
@ -3800,7 +3806,7 @@ int __cdecl wmain(_In_ int argc, _In_z_count_(argc) wchar_t* argv[])
#endif
#ifdef USE_LIBJPEG
case CODEC_JPEG:
hr = SaveToJPEGFile(img[0], destName.c_str());
hr = SaveToJPEGFile(img[0], JPEG_FLAGS_NONE, destName.c_str());
break;
#endif
#ifdef USE_LIBPNG

View File

@ -585,7 +585,7 @@ namespace
#ifdef USE_LIBJPEG
else if (_wcsicmp(ext.c_str(), L".jpg") == 0 || _wcsicmp(ext.c_str(), L".jpeg") == 0)
{
return LoadFromJPEGFile(fileName, &info, *image);
return LoadFromJPEGFile(fileName, JPEG_FLAGS_NONE, &info, *image);
}
#endif
#ifdef USE_LIBPNG
@ -639,7 +639,7 @@ namespace
#endif
#ifdef USE_LIBJPEG
case CODEC_JPEG:
return SaveToJPEGFile(*image, fileName);
return SaveToJPEGFile(*image, JPEG_FLAGS_NONE, fileName);
#endif
#ifdef USE_LIBPNG
case CODEC_PNG: