From 7c7639903835e89be1bd5c18bf79c50c4a9bf1e0 Mon Sep 17 00:00:00 2001 From: Brecht Van Lommel Date: Thu, 27 Nov 2025 02:44:44 +0100 Subject: [PATCH] feat(JXL): CICP read and write support for JPEG XL The JPEG XL color encoding metadata only supports a subset of CICP. So for example srgb_p3d65_display and pq_rec2020_display are supported, but g26_xyzd65_display is not. Custom primaries, custom white point and arbitrary gamma could be used to support more, but I didn't implement that. Signed-off-by: Brecht Van Lommel --- src/jpegxl.imageio/jxlinput.cpp | 30 ++++++++++++++++- src/jpegxl.imageio/jxloutput.cpp | 56 ++++++++++++++++++++++++++++++++ testsuite/jxl/ref/out.txt | 23 +++++++++++++ testsuite/jxl/run.py | 3 ++ 4 files changed, 111 insertions(+), 1 deletion(-) diff --git a/src/jpegxl.imageio/jxlinput.cpp b/src/jpegxl.imageio/jxlinput.cpp index 9ed3b1c45b..b0f90cf4f1 100644 --- a/src/jpegxl.imageio/jxlinput.cpp +++ b/src/jpegxl.imageio/jxlinput.cpp @@ -16,6 +16,7 @@ #include #include +#include #include #include #include @@ -54,7 +55,6 @@ class JxlInput final : public ImageInput { std::string m_filename; int m_next_scanline; // Which scanline is the next to read? uint32_t m_channels; - JxlColorEncoding m_color_encoding; JxlDecoderPtr m_decoder; JxlResizableParallelRunnerPtr m_runner; std::unique_ptr m_config; // Saved copy of configuration spec @@ -224,6 +224,8 @@ JxlInput::open(const std::string& name, ImageSpec& newspec) JxlDataType jxl_data_type; TypeDesc m_data_type; uint32_t bits = 0; + JxlColorEncoding color_encoding {}; + bool have_color_encoding = false; for (;;) { JxlDecoderStatus status = JxlDecoderProcessInput(m_decoder.get()); @@ -300,6 +302,16 @@ JxlInput::open(const std::string& name, ImageSpec& newspec) errorfmt("JxlDecoderGetColorAsICCProfile failed\n"); return false; } + + // Get the color encoding of the pixel data + // This will return JXL_DEC_ERR for a valid file without color + // encoding information, so don't report an error. + if (JXL_DEC_SUCCESS + == JxlDecoderGetColorAsEncodedProfile( + m_decoder.get(), JXL_COLOR_PROFILE_TARGET_DATA, + &color_encoding)) { + have_color_encoding = true; + } } else if (status == JXL_DEC_NEED_IMAGE_OUT_BUFFER) { DBG std::cout << "JXL_DEC_NEED_IMAGE_OUT_BUFFER\n"; @@ -348,6 +360,7 @@ JxlInput::open(const std::string& name, ImageSpec& newspec) m_spec = ImageSpec(info.xsize, info.ysize, m_channels, m_data_type); + // Read ICC profile if (m_icc_profile.size() && m_icc_profile.data()) { m_spec.attribute("ICCProfile", TypeDesc(TypeDesc::UINT8, m_icc_profile.size()), @@ -365,6 +378,21 @@ JxlInput::open(const std::string& name, ImageSpec& newspec) } } + // Read CICP from color encoding. Custom primaries, custom white point and + // arbitrary gamma not supported currently. + if (have_color_encoding && color_encoding.primaries != JXL_PRIMARIES_CUSTOM + && color_encoding.white_point != JXL_WHITE_POINT_CUSTOM + && color_encoding.transfer_function != JXL_TRANSFER_FUNCTION_GAMMA) { + const int cicp[4] = { color_encoding.primaries, + color_encoding.transfer_function, 0 /* RGB */, + 1 /* Full range */ }; + m_spec.attribute("CICP", TypeDesc(TypeDesc::INT, 4), cicp); + const ColorConfig& colorconfig(ColorConfig::default_colorconfig()); + string_view interop_id = colorconfig.get_color_interop_id(cicp); + if (!interop_id.empty()) + m_spec.attribute("oiio:ColorSpace", interop_id); + } + newspec = m_spec; return true; } diff --git a/src/jpegxl.imageio/jxloutput.cpp b/src/jpegxl.imageio/jxloutput.cpp index de82467fa7..1a4a64e858 100644 --- a/src/jpegxl.imageio/jxloutput.cpp +++ b/src/jpegxl.imageio/jxloutput.cpp @@ -6,6 +6,7 @@ #include #include +#include #include #include #include @@ -538,6 +539,8 @@ JxlOutput::save_image(const void* data) return false; } + bool wrote_colorspace = false; + // Write the ICC profile, if available const ParamValue* icc_profile_parameter = m_spec.find_attribute( "ICCProfile"); @@ -551,6 +554,59 @@ JxlOutput::save_image(const void* data) length)) { errorfmt("JxlEncoderSetICCProfile failed\n"); } + wrote_colorspace = true; + } + } + + // Write CICP + const ColorConfig& colorconfig(ColorConfig::default_colorconfig()); + const ParamValue* p = m_spec.find_attribute("CICP", + TypeDesc(TypeDesc::INT, 4)); + string_view colorspace = m_spec.get_string_attribute("oiio:ColorSpace"); + cspan cicp = (p) ? p->as_cspan() + : (!wrote_colorspace) ? colorconfig.get_cicp(colorspace) + : cspan(); + if (!cicp.empty()) { + // JXL only has a subset of CICP, only write if supported. Custom + // primaries and white point are not currently used but could help + // support more CICP codes. + JxlColorEncoding color_encoding {}; + color_encoding.primaries = JxlPrimaries(cicp[0]); + color_encoding.transfer_function = JxlTransferFunction(cicp[1]); + color_encoding.color_space = JXL_COLOR_SPACE_RGB; + + bool supported_primaries = false; + bool supported_transfer = false; + + switch (color_encoding.primaries) { + case JXL_PRIMARIES_SRGB: + case JXL_PRIMARIES_2100: + case JXL_PRIMARIES_P3: + supported_primaries = true; + color_encoding.white_point = JXL_WHITE_POINT_D65; + break; + case JXL_PRIMARIES_CUSTOM: // Not an actual CICP code in JXL + break; + } + + switch (color_encoding.transfer_function) { + case JXL_TRANSFER_FUNCTION_709: + case JXL_TRANSFER_FUNCTION_UNKNOWN: + case JXL_TRANSFER_FUNCTION_LINEAR: + case JXL_TRANSFER_FUNCTION_SRGB: + case JXL_TRANSFER_FUNCTION_PQ: + case JXL_TRANSFER_FUNCTION_DCI: + case JXL_TRANSFER_FUNCTION_HLG: supported_transfer = true; break; + case JXL_TRANSFER_FUNCTION_GAMMA: // Not an actual CICP code + break; + } + + if (supported_primaries && supported_transfer) { + if (JXL_ENC_SUCCESS + != JxlEncoderSetColorEncoding(m_encoder.get(), + &color_encoding)) { + errorfmt("JxlEncoderSetColorEncoding failed\n"); + } } } diff --git a/testsuite/jxl/ref/out.txt b/testsuite/jxl/ref/out.txt index 040910d3c9..83b97fbba6 100644 --- a/testsuite/jxl/ref/out.txt +++ b/testsuite/jxl/ref/out.txt @@ -19,3 +19,26 @@ tahoe-icc.jxl : 128 x 96, 3 channel, uint8 jpegxl ICCProfile:profile_size: 560 ICCProfile:profile_version: "2.1.0" ICCProfile:rendering_intent: "Perceptual" +Reading tahoe-cicp-pq.jxl +tahoe-cicp-pq.jxl : 128 x 96, 3 channel, uint8 jpegxl + SHA-1: 069F1A3E5567349C2D34E535B29913029EF1B09C + channel list: R, G, B + CICP: 9, 16, 0, 1 + ICCProfile: 0, 0, 16, 248, 106, 120, 108, 32, 4, 64, 0, 0, 109, 110, 116, 114, ... [4344 x uint8] + ICCProfile:attributes: "Reflective, Glossy, Positive, Color" + ICCProfile:cmm_type: 1786276896 + ICCProfile:color_space: "RGB" + ICCProfile:copyright: "CC0" + ICCProfile:creation_date: "2019:12:01 00:00:00" + ICCProfile:creator_signature: "6a786c20" + ICCProfile:device_class: "Display device profile" + ICCProfile:flags: "Not Embedded, Independent" + ICCProfile:manufacturer: "0" + ICCProfile:model: "0" + ICCProfile:platform_signature: "Apple Computer, Inc." + ICCProfile:profile_connection_space: "CIELAB" + ICCProfile:profile_description: "RGB_D65_202_Per_PeQ" + ICCProfile:profile_size: 4344 + ICCProfile:profile_version: "4.4.0" + ICCProfile:rendering_intent: "Perceptual" + oiio:ColorSpace: "pq_rec2020_display" diff --git a/testsuite/jxl/run.py b/testsuite/jxl/run.py index d112da67b1..78b7a19eba 100755 --- a/testsuite/jxl/run.py +++ b/testsuite/jxl/run.py @@ -10,6 +10,9 @@ command += info_command ("tahoe-icc.jxl", safematch=True) command += oiiotool ("tahoe-icc.jxl --iccwrite test-jxl.icc") +command += oiiotool ("../common/tahoe-tiny.tif --cicp \"9,16,9,1\" -o tahoe-cicp-pq.jxl") +command += info_command ("tahoe-cicp-pq.jxl", safematch=True) + outputs = [ "test-jxl.icc", "out.txt"