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

View File

@ -18,16 +18,39 @@
namespace DirectX 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( DIRECTX_TEX_API HRESULT __cdecl GetMetadataFromJPEGFile(
_In_z_ const wchar_t* szFile, _In_z_ const wchar_t* szFile,
JPEG_FLAGS flags,
_Out_ TexMetadata& metadata); _Out_ TexMetadata& metadata);
DIRECTX_TEX_API HRESULT __cdecl LoadFromJPEGFile( DIRECTX_TEX_API HRESULT __cdecl LoadFromJPEGFile(
_In_z_ const wchar_t* szFile, _In_z_ const wchar_t* szFile,
JPEG_FLAGS flags,
_Out_opt_ TexMetadata* metadata, _Out_opt_ TexMetadata* metadata,
_Out_ ScratchImage& image); _Out_ ScratchImage& image);
DIRECTX_TEX_API HRESULT __cdecl SaveToJPEGFile( DIRECTX_TEX_API HRESULT __cdecl SaveToJPEGFile(
_In_ const Image& image, _In_ const Image& image,
JPEG_FLAGS flags,
_In_z_ const wchar_t* szFile); _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. * 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 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``. * 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 #endif
#ifdef USE_LIBJPEG #ifdef USE_LIBJPEG
case CODEC_JPEG: case CODEC_JPEG:
return SaveToJPEGFile(img, szOutputFile); return SaveToJPEGFile(img, JPEG_FLAGS_NONE, szOutputFile);
#endif #endif
#ifdef USE_LIBPNG #ifdef USE_LIBPNG
case CODEC_PNG: case CODEC_PNG:
@ -1423,7 +1423,7 @@ int __cdecl wmain(_In_ int argc, _In_z_count_(argc) wchar_t* argv[])
#ifdef USE_LIBJPEG #ifdef USE_LIBJPEG
else if (_wcsicmp(ext.c_str(), L".jpg") == 0 || _wcsicmp(ext.c_str(), L".jpeg") == 0) 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)) if (FAILED(hr))
{ {
wprintf(L" FAILED (%08X%ls)\n", static_cast<unsigned int>(hr), GetErrorDesc(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 #ifdef USE_LIBJPEG
else if (_wcsicmp(ext.c_str(), L".jpg") == 0 || _wcsicmp(ext.c_str(), L".jpeg") == 0) 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)) if (FAILED(hr))
{ {
wprintf(L" FAILED (%08X%ls)\n", static_cast<unsigned int>(hr), GetErrorDesc(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 #endif
#ifdef USE_LIBJPEG #ifdef USE_LIBJPEG
case CODEC_JPEG: case CODEC_JPEG:
hr = SaveToJPEGFile(img[0], destName.c_str()); hr = SaveToJPEGFile(img[0], JPEG_FLAGS_NONE, destName.c_str());
break; break;
#endif #endif
#ifdef USE_LIBPNG #ifdef USE_LIBPNG

View File

@ -585,7 +585,7 @@ namespace
#ifdef USE_LIBJPEG #ifdef USE_LIBJPEG
else if (_wcsicmp(ext.c_str(), L".jpg") == 0 || _wcsicmp(ext.c_str(), L".jpeg") == 0) 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 #endif
#ifdef USE_LIBPNG #ifdef USE_LIBPNG
@ -639,7 +639,7 @@ namespace
#endif #endif
#ifdef USE_LIBJPEG #ifdef USE_LIBJPEG
case CODEC_JPEG: case CODEC_JPEG:
return SaveToJPEGFile(*image, fileName); return SaveToJPEGFile(*image, JPEG_FLAGS_NONE, fileName);
#endif #endif
#ifdef USE_LIBPNG #ifdef USE_LIBPNG
case CODEC_PNG: case CODEC_PNG: