diff --git a/lib/include/ultrahdr/icc.h b/lib/include/ultrahdr/icc.h index 5e88346..214074b 100644 --- a/lib/include/ultrahdr/icc.h +++ b/lib/include/ultrahdr/icc.h @@ -121,6 +121,7 @@ static constexpr uint32_t kTAG_wtpt = SetFourByteTag('w', 't', 'p', 't'); static constexpr uint32_t kTAG_rTRC = SetFourByteTag('r', 'T', 'R', 'C'); static constexpr uint32_t kTAG_gTRC = SetFourByteTag('g', 'T', 'R', 'C'); static constexpr uint32_t kTAG_bTRC = SetFourByteTag('b', 'T', 'R', 'C'); +static constexpr uint32_t kTAG_chad = SetFourByteTag('c', 'h', 'a', 'd'); static constexpr uint32_t kTAG_cicp = SetFourByteTag('c', 'i', 'c', 'p'); static constexpr uint32_t kTAG_cprt = SetFourByteTag('c', 'p', 'r', 't'); static constexpr uint32_t kTAG_A2B0 = SetFourByteTag('A', '2', 'B', '0'); @@ -130,27 +131,32 @@ static constexpr uint32_t kTAG_CurveType = SetFourByteTag('c', 'u', 'r', 'v'); static constexpr uint32_t kTAG_mABType = SetFourByteTag('m', 'A', 'B', ' '); static constexpr uint32_t kTAG_mBAType = SetFourByteTag('m', 'B', 'A', ' '); static constexpr uint32_t kTAG_ParaCurveType = SetFourByteTag('p', 'a', 'r', 'a'); +static constexpr uint32_t kTAG_s15Fixed16ArrayType = SetFourByteTag('s', 'f', '3', '2'); +// All these tables are derived using function skcms_PrimariesToXYZD50() at +// https://cs.android.com/android/platform/superproject/main/+/main:external/skia/modules/skcms/skcms.cc static constexpr Matrix3x3 kSRGB = {{ - // ICC fixed-point (16.16) representation, taken from skcms. Please keep them exactly in sync. - // 0.436065674f, 0.385147095f, 0.143066406f, - // 0.222488403f, 0.716873169f, 0.060607910f, - // 0.013916016f, 0.097076416f, 0.714096069f, - {FixedToFloat(0x6FA2), FixedToFloat(0x6299), FixedToFloat(0x24A0)}, - {FixedToFloat(0x38F5), FixedToFloat(0xB785), FixedToFloat(0x0F84)}, - {FixedToFloat(0x0390), FixedToFloat(0x18DA), FixedToFloat(0xB6CF)}, + {0.43606575f, 0.38515151f, 0.14307842f}, + {0.22249318f, 0.71688701f, 0.06061981f}, + {0.01392392f, 0.09708132f, 0.71409936f}, }}; static constexpr Matrix3x3 kDisplayP3 = {{ - {0.515102f, 0.291965f, 0.157153f}, - {0.241182f, 0.692236f, 0.0665819f}, - {-0.00104941f, 0.0418818f, 0.784378f}, + {0.51514644f, 0.29200998f, 0.15713925f}, + {0.24120032f, 0.69222254f, 0.06657714f}, + {-0.00105014f, 0.04187827f, 0.78427647f}, }}; static constexpr Matrix3x3 kRec2020 = {{ - {0.673459f, 0.165661f, 0.125100f}, - {0.279033f, 0.675338f, 0.0456288f}, - {-0.00193139f, 0.0299794f, 0.797162f}, + {0.67351546f, 0.16569726f, 0.12508295f}, + {0.27905901f, 0.67531801f, 0.04562299f}, + {-0.00193243f, 0.02997783f, 0.7970592f}, +}}; + +static constexpr Matrix3x3 adaptation_matrix = {{ + {1.04792979f, 0.02294687f, -0.05019227f}, + {0.02962781f, 0.99043443f, -0.0170738f}, + {-0.00924304f, 0.01505519f, 0.75187428f}, }}; static constexpr uint32_t kCICPPrimariesUnSpecified = 2; @@ -185,19 +191,14 @@ static inline Fixed float_round_to_fixed(float x) { return float_saturate2int((float)floor((double)x * Fixed1 + 0.5)); } -static inline uint16_t float_round_to_unorm16(float x) { - x = x * 65535.f + 0.5f; - if (x > 65535) return 65535; +// Convert a float to a uInt16Number, with 0.0 mapping go 0 and 1.0 mapping to |one|. +static inline uint16_t float_to_uInt16Number(float x, uint16_t one) { + x = x * one + 0.5; + if (x > one) return one; if (x < 0) return 0; return static_cast(x); } -static inline void float_to_table16(const float f, uint8_t* table_16) { - *reinterpret_cast(table_16) = Endian_SwapBE16(float_round_to_unorm16(f)); -} - -static inline bool isfinitef_(float x) { return 0 == x * 0; } - struct ICCHeader { // Size of the profile (computed) uint32_t size; @@ -243,24 +244,26 @@ struct ICCHeader { class IccHelper { private: - static constexpr uint32_t kTrcTableSize = 65; static constexpr uint32_t kGridSize = 17; static constexpr size_t kNumChannels = 3; + static std::shared_ptr make_empty() { return std::make_shared(0); } static std::shared_ptr write_text_tag(const char* text); static std::string get_desc_string(const uhdr_color_transfer_t tf, const uhdr_color_gamut_t gamut); static std::shared_ptr write_xyz_tag(float x, float y, float z); static std::shared_ptr write_trc_tag(const int table_entries, const void* table_16); static std::shared_ptr write_trc_tag(const TransferFunction& fn); - static float compute_tone_map_gain(const uhdr_color_transfer_t tf, float L); + static std::shared_ptr write_chad_tag(); static std::shared_ptr write_cicp_tag(uint32_t color_primaries, uint32_t transfer_characteristics); static std::shared_ptr write_mAB_or_mBA_tag(uint32_t type, bool has_a_curves, const uint8_t* grid_points, - const uint8_t* grid_16); - static void compute_lut_entry(const Matrix3x3& src_to_XYZD50, float rgb[3]); + const uint8_t* grid_16, bool has_m_curves, + Matrix3x3* toXYZD50); + static void compute_lut_entry(uhdr_color_transfer_t tf, uhdr_color_gamut_t cg, float rgb[3]); static std::shared_ptr write_clut(const uint8_t* grid_points, const uint8_t* grid_16); + static std::shared_ptr write_matrix(const Matrix3x3* matrix); // Checks if a set of xyz tags is equivalent to a 3x3 Matrix. Each input // tag buffer assumed to be at least kColorantTagSize in size. @@ -271,7 +274,8 @@ class IccHelper { // Output includes JPEG embedding identifier and chunk information, but not // APPx information. static std::shared_ptr writeIccProfile(const uhdr_color_transfer_t tf, - const uhdr_color_gamut_t gamut); + const uhdr_color_gamut_t gamut, + bool write_tonemap_icc = false); // NOTE: this function is not robust; it can infer gamuts that IccHelper // writes out but should not be considered a reference implementation for // robust parsing of ICC profiles or their gamuts. diff --git a/lib/include/ultrahdr/jpegr.h b/lib/include/ultrahdr/jpegr.h index 2b7bbaa..e1e1670 100644 --- a/lib/include/ultrahdr/jpegr.h +++ b/lib/include/ultrahdr/jpegr.h @@ -486,10 +486,12 @@ class JpegR { * * \param[in] gainmap_img gainmap image descriptor * \param[in] jpeg_enc_obj jpeg encoder object handle + * \param[in] write_tonemap_icc write tonemap details in icc header * * \return uhdr_error_info_t #UHDR_CODEC_OK if operation succeeds, uhdr_codec_err_t otherwise. */ - uhdr_error_info_t compressGainMap(uhdr_raw_image_t* gainmap_img, JpegEncoderHelper* jpeg_enc_obj); + uhdr_error_info_t compressGainMap(uhdr_raw_image_t* gainmap_img, JpegEncoderHelper* jpeg_enc_obj, + bool write_tonemap_icc = false); /*!\brief This method is called to separate base image and gain map image from compressed * ultrahdr image diff --git a/lib/src/icc.cpp b/lib/src/icc.cpp index 05ef18d..ed3ae90 100644 --- a/lib/src/icc.cpp +++ b/lib/src/icc.cpp @@ -22,106 +22,6 @@ namespace ultrahdr { -static void Matrix3x3_apply(const Matrix3x3* m, float* x) { - float y0 = x[0] * m->vals[0][0] + x[1] * m->vals[0][1] + x[2] * m->vals[0][2]; - float y1 = x[0] * m->vals[1][0] + x[1] * m->vals[1][1] + x[2] * m->vals[1][2]; - float y2 = x[0] * m->vals[2][0] + x[1] * m->vals[2][1] + x[2] * m->vals[2][2]; - x[0] = y0; - x[1] = y1; - x[2] = y2; -} - -bool Matrix3x3_invert(const Matrix3x3* src, Matrix3x3* dst) { - double a00 = src->vals[0][0]; - double a01 = src->vals[1][0]; - double a02 = src->vals[2][0]; - double a10 = src->vals[0][1]; - double a11 = src->vals[1][1]; - double a12 = src->vals[2][1]; - double a20 = src->vals[0][2]; - double a21 = src->vals[1][2]; - double a22 = src->vals[2][2]; - - double b0 = a00 * a11 - a01 * a10; - double b1 = a00 * a12 - a02 * a10; - double b2 = a01 * a12 - a02 * a11; - double b3 = a20; - double b4 = a21; - double b5 = a22; - - double determinant = b0 * b5 - b1 * b4 + b2 * b3; - - if (determinant == 0) { - return false; - } - - double invdet = 1.0 / determinant; - if (invdet > +FLT_MAX || invdet < -FLT_MAX || !isfinitef_((float)invdet)) { - return false; - } - - b0 *= invdet; - b1 *= invdet; - b2 *= invdet; - b3 *= invdet; - b4 *= invdet; - b5 *= invdet; - - dst->vals[0][0] = (float)(a11 * b5 - a12 * b4); - dst->vals[1][0] = (float)(a02 * b4 - a01 * b5); - dst->vals[2][0] = (float)(+b2); - dst->vals[0][1] = (float)(a12 * b3 - a10 * b5); - dst->vals[1][1] = (float)(a00 * b5 - a02 * b3); - dst->vals[2][1] = (float)(-b1); - dst->vals[0][2] = (float)(a10 * b4 - a11 * b3); - dst->vals[1][2] = (float)(a01 * b3 - a00 * b4); - dst->vals[2][2] = (float)(+b0); - - for (int r = 0; r < 3; ++r) - for (int c = 0; c < 3; ++c) { - if (!isfinitef_(dst->vals[r][c])) { - return false; - } - } - return true; -} - -static Matrix3x3 Matrix3x3_concat(const Matrix3x3* A, const Matrix3x3* B) { - Matrix3x3 m = {{{0, 0, 0}, {0, 0, 0}, {0, 0, 0}}}; - for (int r = 0; r < 3; r++) - for (int c = 0; c < 3; c++) { - m.vals[r][c] = A->vals[r][0] * B->vals[0][c] + A->vals[r][1] * B->vals[1][c] + - A->vals[r][2] * B->vals[2][c]; - } - return m; -} - -static void float_XYZD50_to_grid16_lab(const float* xyz_float, uint8_t* grid16_lab) { - float v[3] = { - xyz_float[0] / kD50_x, - xyz_float[1] / kD50_y, - xyz_float[2] / kD50_z, - }; - for (size_t i = 0; i < 3; ++i) { - v[i] = v[i] > 0.008856f ? cbrtf(v[i]) : v[i] * 7.787f + (16 / 116.0f); - } - const float L = v[1] * 116.0f - 16.0f; - const float a = (v[0] - v[1]) * 500.0f; - const float b = (v[1] - v[2]) * 200.0f; - const float Lab_unorm[3] = { - L * (1 / 100.f), - (a + 128.0f) * (1 / 255.0f), - (b + 128.0f) * (1 / 255.0f), - }; - // This will encode L=1 as 0xFFFF. This matches how skcms will interpret the - // table, but the spec appears to indicate that the value should be 0xFF00. - // https://crbug.com/skia/13807 - for (size_t i = 0; i < 3; ++i) { - reinterpret_cast(grid16_lab)[i] = - Endian_SwapBE16(float_round_to_unorm16(Lab_unorm[i])); - } -} - std::string IccHelper::get_desc_string(const uhdr_color_transfer_t tf, const uhdr_color_gamut_t gamut) { std::string result; @@ -245,32 +145,6 @@ std::shared_ptr IccHelper::write_trc_tag(const TransferFunction& fn) return dataStruct; } -float IccHelper::compute_tone_map_gain(const uhdr_color_transfer_t tf, float L) { - if (L <= 0.f) { - return 1.f; - } - if (tf == UHDR_CT_PQ) { - // The PQ transfer function will map to the range [0, 1]. Linearly scale - // it up to the range [0, 10,000/203]. We will then tone map that back - // down to [0, 1]. - constexpr float kInputMaxLuminance = 10000 / 203.f; - constexpr float kOutputMaxLuminance = 1.0; - L *= kInputMaxLuminance; - - // Compute the tone map gain which will tone map from 10,000/203 to 1.0. - constexpr float kToneMapA = kOutputMaxLuminance / (kInputMaxLuminance * kInputMaxLuminance); - constexpr float kToneMapB = 1.f / kOutputMaxLuminance; - return kInputMaxLuminance * (1.f + kToneMapA * L) / (1.f + kToneMapB * L); - } - if (tf == UHDR_CT_HLG) { - // Let Lw be the brightness of the display in nits. - constexpr float Lw = 203.f; - const float gamma = 1.2f + 0.42f * std::log(Lw / 1000.f) / std::log(10.f); - return std::pow(L, gamma - 1.f); - } - return 1.f; -} - std::shared_ptr IccHelper::write_cicp_tag(uint32_t color_primaries, uint32_t transfer_characteristics) { std::shared_ptr dataStruct = std::make_shared(kCicpTagSize); @@ -283,37 +157,35 @@ std::shared_ptr IccHelper::write_cicp_tag(uint32_t color_primaries, return dataStruct; } -void IccHelper::compute_lut_entry(const Matrix3x3& src_to_XYZD50, float rgb[3]) { - // Compute the matrices to convert from source to Rec2020, and from Rec2020 to XYZD50. - Matrix3x3 src_to_rec2020; - const Matrix3x3 rec2020_to_XYZD50 = kRec2020; - { - Matrix3x3 XYZD50_to_rec2020; - Matrix3x3_invert(&rec2020_to_XYZD50, &XYZD50_to_rec2020); - src_to_rec2020 = Matrix3x3_concat(&XYZD50_to_rec2020, &src_to_XYZD50); - } - - // Convert the source signal to linear. - for (size_t i = 0; i < kNumChannels; ++i) { - rgb[i] = pqOetf(rgb[i]); - } - - // Convert source gamut to Rec2020. - Matrix3x3_apply(&src_to_rec2020, rgb); - - // Compute the luminance of the signal. - float L = bt2100Luminance({{{rgb[0], rgb[1], rgb[2]}}}); - - // Compute the tone map gain based on the luminance. - float tone_map_gain = compute_tone_map_gain(UHDR_CT_PQ, L); - - // Apply the tone map gain. - for (size_t i = 0; i < kNumChannels; ++i) { - rgb[i] *= tone_map_gain; +std::shared_ptr IccHelper::write_chad_tag() { + std::shared_ptr dataStruct = std::make_shared(44); + dataStruct->write32(Endian_SwapBE32(kTAG_s15Fixed16ArrayType)); // Type signature + dataStruct->write32(0); // Reserved + for (int i = 0; i < 3; ++i) { + for (int j = 0; j < 3; ++j) { + dataStruct->write32(Endian_SwapBE32(float_round_to_fixed(adaptation_matrix.vals[i][j]))); + } } + return dataStruct; +} - // Convert from Rec2020-linear to XYZD50. - Matrix3x3_apply(&rec2020_to_XYZD50, rgb); +void IccHelper::compute_lut_entry(uhdr_color_transfer_t tf, uhdr_color_gamut_t cg, float rgb[3]) { + Color hdr_rgb = {{{rgb[0], rgb[1], rgb[2]}}}; + float headroom = 1.0f; + if (tf == UHDR_CT_HLG) { + hdr_rgb = hlgInvOetf(hdr_rgb); + LuminanceFn hdrLuminanceFn = getLuminanceFn(cg); + hdr_rgb = hlgOotf(hdr_rgb, hdrLuminanceFn); + headroom = kHlgMaxNits / kSdrWhiteNits; + } else if (tf == UHDR_CT_PQ) { + hdr_rgb = pqInvOetf(hdr_rgb); + headroom = kPqMaxNits / kSdrWhiteNits; + } + GlobalTonemapOutputs tonemapped = + globalTonemap({hdr_rgb.r, hdr_rgb.g, hdr_rgb.b}, headroom, false); + rgb[0] = tonemapped.rgb_out[0]; + rgb[1] = tonemapped.rgb_out[1]; + rgb[2] = tonemapped.rgb_out[2]; } std::shared_ptr IccHelper::write_clut(const uint8_t* grid_points, @@ -343,72 +215,111 @@ std::shared_ptr IccHelper::write_clut(const uint8_t* grid_points, return dataStruct; } +std::shared_ptr IccHelper::write_matrix(const Matrix3x3* matrix) { + std::shared_ptr dataStruct = std::make_shared(12 * 4); + // See layout details in section "10.12.5 Matrix". + for (int i = 0; i < 3; ++i) { + for (int j = 0; j < 3; ++j) { + dataStruct->write32(Endian_SwapBE32(float_round_to_fixed(matrix->vals[i][j]))); + } + } + for (int i = 0; i < 3; ++i) { + dataStruct->write32(Endian_SwapBE32(float_round_to_fixed(0.f))); + } + return dataStruct; +} + std::shared_ptr IccHelper::write_mAB_or_mBA_tag(uint32_t type, bool has_a_curves, const uint8_t* grid_points, - const uint8_t* grid_16) { - const size_t b_curves_offset = 32; - std::shared_ptr b_curves_data[kNumChannels]; - std::shared_ptr a_curves_data[kNumChannels]; - size_t clut_offset = 0; - std::shared_ptr clut; - size_t a_curves_offset = 0; + const uint8_t* grid_16, + bool has_m_curves, + Matrix3x3* toXYZD50) { + size_t offset = 32; // The "B" curve is required. + size_t b_curves_offset = offset; + std::shared_ptr b_curves_data[kNumChannels]; for (size_t i = 0; i < kNumChannels; ++i) { b_curves_data[i] = write_trc_tag(kLinear_TransFun); + offset += b_curves_data[i]->getLength(); } - // The "A" curve and CLUT are optional. - if (has_a_curves) { - clut_offset = b_curves_offset; - for (size_t i = 0; i < kNumChannels; ++i) { - clut_offset += b_curves_data[i]->getLength(); - } + // The CLUT. + size_t clut_offset = 0; + std::shared_ptr clut; + if (grid_points) { + clut_offset = offset; clut = write_clut(grid_points, grid_16); + offset += clut->getLength(); + } - a_curves_offset = clut_offset + clut->getLength(); + // The A curves. + size_t a_curves_offset = 0; + std::shared_ptr a_curves_data[kNumChannels]; + if (has_a_curves) { + a_curves_offset = offset; for (size_t i = 0; i < kNumChannels; ++i) { a_curves_data[i] = write_trc_tag(kLinear_TransFun); + offset += a_curves_data[i]->getLength(); } } - int total_length = b_curves_offset; - for (size_t i = 0; i < kNumChannels; ++i) { - total_length += b_curves_data[i]->getLength(); + // The matrix. + size_t matrix_offset = 0; + std::shared_ptr matrix_data; + if (toXYZD50) { + matrix_offset = offset; + matrix_data = write_matrix(toXYZD50); + offset += matrix_data->getLength(); } - if (has_a_curves) { - total_length += clut->getLength(); + + // The "M" curves. + size_t m_curves_offset = 0; + std::shared_ptr m_curves_data[kNumChannels]; + if (has_m_curves) { + m_curves_offset = offset; for (size_t i = 0; i < kNumChannels; ++i) { - total_length += a_curves_data[i]->getLength(); + m_curves_data[i] = write_trc_tag(kLinear_TransFun); + offset += m_curves_data[i]->getLength(); } } - std::shared_ptr dataStruct = std::make_shared(total_length); + + std::shared_ptr dataStruct = std::make_shared(offset); dataStruct->write32(Endian_SwapBE32(type)); // Type signature dataStruct->write32(0); // Reserved dataStruct->write8(kNumChannels); // Input channels dataStruct->write8(kNumChannels); // Output channels dataStruct->write16(0); // Reserved dataStruct->write32(Endian_SwapBE32(b_curves_offset)); // B curve offset - dataStruct->write32(Endian_SwapBE32(0)); // Matrix offset (ignored) - dataStruct->write32(Endian_SwapBE32(0)); // M curve offset (ignored) + dataStruct->write32(Endian_SwapBE32(matrix_offset)); // Matrix offset + dataStruct->write32(Endian_SwapBE32(m_curves_offset)); // M curve offset dataStruct->write32(Endian_SwapBE32(clut_offset)); // CLUT offset dataStruct->write32(Endian_SwapBE32(a_curves_offset)); // A curve offset for (size_t i = 0; i < kNumChannels; ++i) { - if (dataStruct->write(b_curves_data[i]->getData(), b_curves_data[i]->getLength())) { - return dataStruct; - } + dataStruct->write(b_curves_data[i]->getData(), b_curves_data[i]->getLength()); } - if (has_a_curves) { + if (clut) { dataStruct->write(clut->getData(), clut->getLength()); + } + if (has_a_curves) { for (size_t i = 0; i < kNumChannels; ++i) { dataStruct->write(a_curves_data[i]->getData(), a_curves_data[i]->getLength()); } } + if (toXYZD50) { + dataStruct->write(matrix_data->getData(), matrix_data->getLength()); + } + if (has_m_curves) { + for (size_t i = 0; i < kNumChannels; ++i) { + dataStruct->write(m_curves_data[i]->getData(), m_curves_data[i]->getLength()); + } + } return dataStruct; } std::shared_ptr IccHelper::writeIccProfile(uhdr_color_transfer_t tf, - uhdr_color_gamut_t gamut) { + uhdr_color_gamut_t gamut, + bool write_tonemap_icc) { ICCHeader header; std::vector>> tags; @@ -417,6 +328,7 @@ std::shared_ptr IccHelper::writeIccProfile(uhdr_color_transfer_t tf, std::string desc = get_desc_string(tf, gamut); tags.emplace_back(kTAG_desc, write_text_tag(desc.c_str())); + // Compute primaries. Matrix3x3 toXYZD50; switch (gamut) { case UHDR_CG_BT_709: @@ -432,49 +344,30 @@ std::shared_ptr IccHelper::writeIccProfile(uhdr_color_transfer_t tf, // Should not fall here. return nullptr; } - - // Compute primaries. - { - tags.emplace_back(kTAG_rXYZ, - write_xyz_tag(toXYZD50.vals[0][0], toXYZD50.vals[1][0], toXYZD50.vals[2][0])); - tags.emplace_back(kTAG_gXYZ, - write_xyz_tag(toXYZD50.vals[0][1], toXYZD50.vals[1][1], toXYZD50.vals[2][1])); - tags.emplace_back(kTAG_bXYZ, - write_xyz_tag(toXYZD50.vals[0][2], toXYZD50.vals[1][2], toXYZD50.vals[2][2])); - } + tags.emplace_back(kTAG_rXYZ, + write_xyz_tag(toXYZD50.vals[0][0], toXYZD50.vals[1][0], toXYZD50.vals[2][0])); + tags.emplace_back(kTAG_gXYZ, + write_xyz_tag(toXYZD50.vals[0][1], toXYZD50.vals[1][1], toXYZD50.vals[2][1])); + tags.emplace_back(kTAG_bXYZ, + write_xyz_tag(toXYZD50.vals[0][2], toXYZD50.vals[1][2], toXYZD50.vals[2][2])); // Compute white point tag (must be D50) tags.emplace_back(kTAG_wtpt, write_xyz_tag(kD50_x, kD50_y, kD50_z)); // Compute transfer curves. - if (tf != UHDR_CT_PQ) { - if (tf == UHDR_CT_HLG) { - std::vector trc_table; - trc_table.resize(kTrcTableSize * 2); - for (uint32_t i = 0; i < kTrcTableSize; ++i) { - float x = i / (kTrcTableSize - 1.f); - float y = hlgOetf(x); - y *= compute_tone_map_gain(tf, y); - float_to_table16(y, &trc_table[2 * i]); - } - - tags.emplace_back(kTAG_rTRC, - write_trc_tag(kTrcTableSize, reinterpret_cast(trc_table.data()))); - tags.emplace_back(kTAG_gTRC, - write_trc_tag(kTrcTableSize, reinterpret_cast(trc_table.data()))); - tags.emplace_back(kTAG_bTRC, - write_trc_tag(kTrcTableSize, reinterpret_cast(trc_table.data()))); - } else if (tf == UHDR_CT_SRGB) { - tags.emplace_back(kTAG_rTRC, write_trc_tag(kSRGB_TransFun)); - tags.emplace_back(kTAG_gTRC, write_trc_tag(kSRGB_TransFun)); - tags.emplace_back(kTAG_bTRC, write_trc_tag(kSRGB_TransFun)); - } else if (tf == UHDR_CT_LINEAR) { - tags.emplace_back(kTAG_rTRC, write_trc_tag(kLinear_TransFun)); - tags.emplace_back(kTAG_gTRC, write_trc_tag(kLinear_TransFun)); - tags.emplace_back(kTAG_bTRC, write_trc_tag(kLinear_TransFun)); - } + if (tf == UHDR_CT_SRGB) { + tags.emplace_back(kTAG_rTRC, write_trc_tag(kSRGB_TransFun)); + // Use empty data to indicate that the entry should use the previous tag's + // data. + tags.emplace_back(kTAG_gTRC, make_empty()); + // Use empty data to indicate that the entry should use the previous tag's + // data. + tags.emplace_back(kTAG_bTRC, make_empty()); } + // Chroma adaptation matrix + tags.emplace_back(kTAG_chad, write_chad_tag()); + // Compute CICP - for hdr images icc profile shall contain cicp. if (tf == UHDR_CT_HLG || tf == UHDR_CT_PQ || tf == UHDR_CT_LINEAR) { // The CICP tag is present in ICC 4.4, so update the header's version. @@ -502,10 +395,14 @@ std::shared_ptr IccHelper::writeIccProfile(uhdr_color_transfer_t tf, tags.emplace_back(kTAG_cicp, write_cicp_tag(color_primaries, transfer_characteristics)); } - // Compute A2B0. - if (tf == UHDR_CT_PQ) { - std::vector a2b_grid; - a2b_grid.resize(kGridSize * kGridSize * kGridSize * kNumChannels * 2); + // Compute A2B, B2A (PQ and HLG only). + if (write_tonemap_icc && (tf == UHDR_CT_PQ || tf == UHDR_CT_HLG)) { + // The uInt16Number used to encoude XYZ values has 1.0 map to 0x8000. + // See section "6.3.4.2 General PCS encoding" and Table 11. + constexpr uint16_t kOne16XYZ = 0x8000; + + std::vector a2b_grid; + a2b_grid.resize(kGridSize * kGridSize * kGridSize * kNumChannels); size_t a2b_grid_index = 0; for (uint32_t r_index = 0; r_index < kGridSize; ++r_index) { for (uint32_t g_index = 0; g_index < kGridSize; ++g_index) { @@ -515,9 +412,11 @@ std::shared_ptr IccHelper::writeIccProfile(uhdr_color_transfer_t tf, g_index / (kGridSize - 1.f), b_index / (kGridSize - 1.f), }; - compute_lut_entry(toXYZD50, rgb); - float_XYZD50_to_grid16_lab(rgb, &a2b_grid[a2b_grid_index]); - a2b_grid_index += 6; + compute_lut_entry(tf, gamut, rgb); + // Write the result to the LUT. + for (const auto& c : rgb) { + a2b_grid[a2b_grid_index++] = Endian_SwapBE16(float_to_uInt16Number(c, kOne16XYZ)); + } } } } @@ -528,17 +427,10 @@ std::shared_ptr IccHelper::writeIccProfile(uhdr_color_transfer_t tf, grid_points[i] = kGridSize; } - auto a2b_data = write_mAB_or_mBA_tag(kTAG_mABType, - /* has_a_curves */ true, grid_points, grid_16); + auto a2b_data = write_mAB_or_mBA_tag(kTAG_mABType, true, grid_points, grid_16, true, &toXYZD50); tags.emplace_back(kTAG_A2B0, std::move(a2b_data)); - } - // Compute B2A0. - if (tf == UHDR_CT_PQ) { - auto b2a_data = write_mAB_or_mBA_tag(kTAG_mBAType, - /* has_a_curves */ false, - /* grid_points */ nullptr, - /* grid_16 */ nullptr); + auto b2a_data = write_mAB_or_mBA_tag(kTAG_mBAType, false, nullptr, nullptr, false, nullptr); tags.emplace_back(kTAG_B2A0, std::move(b2a_data)); } @@ -580,8 +472,10 @@ std::shared_ptr IccHelper::writeIccProfile(uhdr_color_transfer_t tf, uint32_t last_tag_offset = sizeof(header) + tag_table_size; uint32_t last_tag_size = 0; for (const auto& tag : tags) { - last_tag_offset = last_tag_offset + last_tag_size; - last_tag_size = tag.second->getLength(); + if (tag.second->getLength()) { + last_tag_offset = last_tag_offset + last_tag_size; + last_tag_size = tag.second->getLength(); + } uint32_t tag_table_entry[3] = { Endian_SwapBE32(tag.first), Endian_SwapBE32(last_tag_offset), diff --git a/lib/src/jpegr.cpp b/lib/src/jpegr.cpp index 8ce700f..630fc28 100644 --- a/lib/src/jpegr.cpp +++ b/lib/src/jpegr.cpp @@ -214,7 +214,7 @@ uhdr_error_info_t JpegR::encodeJPEGR(uhdr_raw_image_t* hdr_intent, uhdr_compress // compress gain map JpegEncoderHelper jpeg_enc_obj_gm; - UHDR_ERR_CHECK(compressGainMap(gainmap.get(), &jpeg_enc_obj_gm)); + UHDR_ERR_CHECK(compressGainMap(gainmap.get(), &jpeg_enc_obj_gm, true /* write tonemap in icc */)); uhdr_compressed_image_t gainmap_compressed = jpeg_enc_obj_gm.getCompressedImage(); std::shared_ptr icc = IccHelper::writeIccProfile(UHDR_CT_SRGB, sdr_intent->cg); @@ -518,9 +518,10 @@ uhdr_error_info_t JpegR::convertYuv(uhdr_raw_image_t* image, uhdr_color_gamut_t } uhdr_error_info_t JpegR::compressGainMap(uhdr_raw_image_t* gainmap_img, - JpegEncoderHelper* jpeg_enc_obj) { + JpegEncoderHelper* jpeg_enc_obj, bool write_tonemap_icc) { if (!kWriteXmpMetadata) { - std::shared_ptr icc = IccHelper::writeIccProfile(gainmap_img->ct, gainmap_img->cg); + std::shared_ptr icc = + IccHelper::writeIccProfile(gainmap_img->ct, gainmap_img->cg, write_tonemap_icc); return jpeg_enc_obj->compressImage(gainmap_img, mMapCompressQuality, icc->getData(), icc->getLength()); }