| /* |
| * Copyright 2016 Google Inc. |
| * |
| * Use of this source code is governed by a BSD-style license that can be |
| * found in the LICENSE file. |
| */ |
| |
| #include "include/core/SkICC.h" |
| #include "include/core/SkStream.h" |
| #include "include/private/SkFixed.h" |
| #include "src/core/SkAutoMalloc.h" |
| #include "src/core/SkColorSpacePriv.h" |
| #include "src/core/SkEndian.h" |
| #include "src/core/SkICCPriv.h" |
| #include "src/core/SkMD5.h" |
| #include "src/core/SkUtils.h" |
| |
| #include <cmath> |
| #include <string> |
| #include <vector> |
| |
| // The number of input and output channels. |
| static constexpr size_t kNumChannels = 3; |
| |
| // The D50 illuminant. |
| constexpr float kD50_x = 0.9642f; |
| constexpr float kD50_y = 1.0000f; |
| constexpr float kD50_z = 0.8249f; |
| |
| // This is like SkFloatToFixed, but rounds to nearest, preserving as much accuracy as possible |
| // when going float -> fixed -> float (it has the same accuracy when going fixed -> float -> fixed). |
| // The use of double is necessary to accommodate the full potential 32-bit mantissa of the 16.16 |
| // SkFixed value, and so avoiding rounding problems with float. Also, see the comment in SkFixed.h. |
| static SkFixed float_round_to_fixed(float x) { |
| return sk_float_saturate2int((float)floor((double)x * SK_Fixed1 + 0.5)); |
| } |
| |
| static uint16_t float_round_to_unorm16(float x) { |
| x = x * 65535.f + 0.5; |
| if (x > 65535) return 65535; |
| if (x < 0) return 0; |
| return static_cast<uint16_t>(x); |
| } |
| |
| struct ICCHeader { |
| // Size of the profile (computed) |
| uint32_t size; |
| |
| // Preferred CMM type (ignored) |
| uint32_t cmm_type = 0; |
| |
| // Version 4.3 or 4.4 if CICP is included. |
| uint32_t version = SkEndian_SwapBE32(0x04300000); |
| |
| // Display device profile |
| uint32_t profile_class = SkEndian_SwapBE32(kDisplay_Profile); |
| |
| // RGB input color space; |
| uint32_t data_color_space = SkEndian_SwapBE32(kRGB_ColorSpace); |
| |
| // Profile connection space. |
| uint32_t pcs = SkEndian_SwapBE32(kXYZ_PCSSpace); |
| |
| // Date and time (ignored) |
| uint8_t creation_date_time[12] = {0}; |
| |
| // Profile signature |
| uint32_t signature = SkEndian_SwapBE32(kACSP_Signature); |
| |
| // Platform target (ignored) |
| uint32_t platform = 0; |
| |
| // Flags: not embedded, can be used independently |
| uint32_t flags = 0x00000000; |
| |
| // Device manufacturer (ignored) |
| uint32_t device_manufacturer = 0; |
| |
| // Device model (ignored) |
| uint32_t device_model = 0; |
| |
| // Device attributes (ignored) |
| uint8_t device_attributes[8] = {0}; |
| |
| // Relative colorimetric rendering intent |
| uint32_t rendering_intent = SkEndian_SwapBE32(1); |
| |
| // D50 standard illuminant (X, Y, Z) |
| uint32_t illuminant_X = SkEndian_SwapBE32(float_round_to_fixed(kD50_x)); |
| uint32_t illuminant_Y = SkEndian_SwapBE32(float_round_to_fixed(kD50_y)); |
| uint32_t illuminant_Z = SkEndian_SwapBE32(float_round_to_fixed(kD50_z)); |
| |
| // Profile creator (ignored) |
| uint32_t creator = 0; |
| |
| // Profile id checksum (ignored) |
| uint8_t profile_id[16] = {0}; |
| |
| // Reserved (ignored) |
| uint8_t reserved[28] = {0}; |
| |
| // Technically not part of header, but required |
| uint32_t tag_count = 0; |
| }; |
| |
| static sk_sp<SkData> write_xyz_tag(float x, float y, float z) { |
| uint32_t data[] = { |
| SkEndian_SwapBE32(kXYZ_PCSSpace), |
| 0, |
| SkEndian_SwapBE32(float_round_to_fixed(x)), |
| SkEndian_SwapBE32(float_round_to_fixed(y)), |
| SkEndian_SwapBE32(float_round_to_fixed(z)), |
| }; |
| return SkData::MakeWithCopy(data, sizeof(data)); |
| } |
| |
| static sk_sp<SkData> write_para_tag(const skcms_TransferFunction& fn) { |
| SkDynamicMemoryWStream s; |
| s.write32(SkEndian_SwapBE32(kTAG_ParaCurveType)); |
| s.write32(0); |
| if (fn.a == 1.f && fn.b == 0.f && fn.c == 0.f && fn.d == 0.f && fn.e == 0.f && fn.f == 0.f) { |
| s.write32(SkEndian_SwapBE16(kExponential_ParaCurveType)); |
| s.write32(SkEndian_SwapBE32(float_round_to_fixed(fn.g))); |
| } else { |
| s.write32(SkEndian_SwapBE16(kGABCDEF_ParaCurveType)); |
| s.write32(SkEndian_SwapBE32(float_round_to_fixed(fn.g))); |
| s.write32(SkEndian_SwapBE32(float_round_to_fixed(fn.a))); |
| s.write32(SkEndian_SwapBE32(float_round_to_fixed(fn.b))); |
| s.write32(SkEndian_SwapBE32(float_round_to_fixed(fn.c))); |
| s.write32(SkEndian_SwapBE32(float_round_to_fixed(fn.d))); |
| s.write32(SkEndian_SwapBE32(float_round_to_fixed(fn.e))); |
| s.write32(SkEndian_SwapBE32(float_round_to_fixed(fn.f))); |
| } |
| return s.detachAsData(); |
| } |
| |
| static bool nearly_equal(float x, float y) { |
| // A note on why I chose this tolerance: transfer_fn_almost_equal() uses a |
| // tolerance of 0.001f, which doesn't seem to be enough to distinguish |
| // between similar transfer functions, for example: gamma2.2 and sRGB. |
| // |
| // If the tolerance is 0.0f, then this we can't distinguish between two |
| // different encodings of what is clearly the same colorspace. Some |
| // experimentation with example files lead to this number: |
| static constexpr float kTolerance = 1.0f / (1 << 11); |
| return ::fabsf(x - y) <= kTolerance; |
| } |
| |
| static bool nearly_equal(const skcms_TransferFunction& u, |
| const skcms_TransferFunction& v) { |
| return nearly_equal(u.g, v.g) |
| && nearly_equal(u.a, v.a) |
| && nearly_equal(u.b, v.b) |
| && nearly_equal(u.c, v.c) |
| && nearly_equal(u.d, v.d) |
| && nearly_equal(u.e, v.e) |
| && nearly_equal(u.f, v.f); |
| } |
| |
| static bool nearly_equal(const skcms_Matrix3x3& u, const skcms_Matrix3x3& v) { |
| for (int r = 0; r < 3; r++) { |
| for (int c = 0; c < 3; c++) { |
| if (!nearly_equal(u.vals[r][c], v.vals[r][c])) { |
| return false; |
| } |
| } |
| } |
| return true; |
| } |
| |
| static constexpr uint32_t kCICPPrimariesSRGB = 1; |
| static constexpr uint32_t kCICPPrimariesP3 = 12; |
| static constexpr uint32_t kCICPPrimariesRec2020 = 9; |
| |
| static uint32_t get_cicp_primaries(const skcms_Matrix3x3& toXYZD50) { |
| if (nearly_equal(toXYZD50, SkNamedGamut::kSRGB)) { |
| return kCICPPrimariesSRGB; |
| } else if (nearly_equal(toXYZD50, SkNamedGamut::kDisplayP3)) { |
| return kCICPPrimariesP3; |
| } else if (nearly_equal(toXYZD50, SkNamedGamut::kRec2020)) { |
| return kCICPPrimariesRec2020; |
| } |
| return 0; |
| } |
| |
| static constexpr uint32_t kCICPTrfnSRGB = 1; |
| static constexpr uint32_t kCICPTrfn2Dot2 = 4; |
| static constexpr uint32_t kCICPTrfnLinear = 8; |
| static constexpr uint32_t kCICPTrfnPQ = 16; |
| static constexpr uint32_t kCICPTrfnHLG = 18; |
| |
| static uint32_t get_cicp_trfn(const skcms_TransferFunction& fn) { |
| switch (classify_transfer_fn(fn)) { |
| case Bad_TF: |
| return 0; |
| case sRGBish_TF: |
| if (nearly_equal(fn, SkNamedTransferFn::kSRGB)) { |
| return kCICPTrfnSRGB; |
| } else if (nearly_equal(fn, SkNamedTransferFn::k2Dot2)) { |
| return kCICPTrfn2Dot2; |
| } else if (nearly_equal(fn, SkNamedTransferFn::kLinear)) { |
| return kCICPTrfnLinear; |
| } |
| break; |
| case PQish_TF: |
| // All PQ transfer functions are mapped to the single PQ value, |
| // ignoring their SDR white level. |
| return kCICPTrfnPQ; |
| break; |
| case HLGish_TF: |
| // All HLG transfer functions are mapped to the single HLG value. |
| return kCICPTrfnHLG; |
| break; |
| case HLGinvish_TF: |
| return 0; |
| } |
| return 0; |
| } |
| |
| static std::string get_desc_string(const skcms_TransferFunction& fn, |
| const skcms_Matrix3x3& toXYZD50, |
| uint32_t cicp_trfn, |
| uint32_t cicp_primaries) { |
| // Use a unique string for sRGB. |
| if (cicp_trfn == kCICPPrimariesSRGB && cicp_primaries == kCICPTrfnSRGB) { |
| return "sRGB"; |
| } |
| |
| // If available, use the named CICP primaries and transfer function. |
| if (cicp_primaries && cicp_trfn) { |
| std::string result; |
| switch (cicp_primaries) { |
| case kCICPPrimariesSRGB: |
| result += "sRGB"; |
| break; |
| case kCICPPrimariesP3: |
| result += "Display P3"; |
| break; |
| case kCICPPrimariesRec2020: |
| result += "Rec2020"; |
| break; |
| default: |
| result += "Unknown"; |
| break; |
| } |
| result += " Gamut with "; |
| switch (cicp_trfn) { |
| case kCICPTrfnSRGB: |
| result += "sRGB"; |
| break; |
| case kCICPTrfnLinear: |
| result += "Linear"; |
| break; |
| case kCICPTrfn2Dot2: |
| result += "2.2"; |
| break; |
| case kCICPTrfnPQ: |
| result += "PQ"; |
| break; |
| case kCICPTrfnHLG: |
| result += "HLG"; |
| break; |
| default: |
| result += "Unknown"; |
| break; |
| } |
| result += " Transfer"; |
| return result; |
| } |
| |
| // Fall back to a prefix plus md5 hash. |
| SkMD5 md5; |
| md5.write(&toXYZD50, sizeof(toXYZD50)); |
| md5.write(&fn, sizeof(fn)); |
| SkMD5::Digest digest = md5.finish(); |
| std::string md5_hexstring(2 * sizeof(SkMD5::Digest), ' '); |
| for (unsigned i = 0; i < sizeof(SkMD5::Digest); ++i) { |
| uint8_t byte = digest.data[i]; |
| md5_hexstring[2 * i + 0] = SkHexadecimalDigits::gUpper[byte >> 4]; |
| md5_hexstring[2 * i + 1] = SkHexadecimalDigits::gUpper[byte & 0xF]; |
| } |
| return "Google/Skia/" + md5_hexstring; |
| } |
| |
| static sk_sp<SkData> write_text_tag(const std::string& text) { |
| uint32_t header[] = { |
| SkEndian_SwapBE32(kTAG_TextType), // Type signature |
| 0, // Reserved |
| SkEndian_SwapBE32(1), // Number of records |
| SkEndian_SwapBE32(12), // Record size (must be 12) |
| SkEndian_SwapBE32(SkSetFourByteTag('e', 'n', 'U', 'S')), // English USA |
| SkEndian_SwapBE32(2 * text.length()), // Length of string in bytes |
| SkEndian_SwapBE32(28), // Offset of string |
| }; |
| SkDynamicMemoryWStream s; |
| s.write(header, sizeof(header)); |
| for (size_t i = 0; i < text.length(); i++) { |
| // Convert ASCII to big-endian UTF-16. |
| s.write8(0); |
| s.write8(text[i]); |
| } |
| s.padToAlign4(); |
| return s.detachAsData(); |
| } |
| |
| // Write a CICP tag. |
| static sk_sp<SkData> write_cicp_tag(uint32_t primaries, uint32_t trfn) { |
| SkDynamicMemoryWStream s; |
| SkASSERT(primaries); |
| SkASSERT(trfn); |
| s.write32(SkEndian_SwapBE32(kTAG_cicp)); // Type signature |
| s.write32(0); // Reserved |
| s.write8(primaries); // Color primaries |
| s.write8(trfn); // Transfer characteristics |
| s.write8(0); // RGB matrix |
| s.write8(1); // Full range |
| return s.detachAsData(); |
| } |
| |
| // Perform a matrix-vector multiplication. Overwrite the input vector with the result. |
| static void skcms_Matrix3x3_apply(const skcms_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; |
| } |
| |
| // Convert the specified coordinate in XYZD50 to the fixed-point Lab |
| // representation. |
| static void xyzd50_to_Lab_fixed16(const float* xyz, uint16_t* Lab) { |
| float v[3] = { |
| xyz[0] / kD50_x, |
| xyz[1] / kD50_y, |
| xyz[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); |
| } |
| float L = v[1] * 116.0f - 16.0f; |
| float a = (v[0] - v[1]) * 500.0f; |
| float b = (v[1] - v[2]) * 200.0f; |
| Lab[0] = float_round_to_unorm16(L * (1 / 100.f)); |
| Lab[1] = float_round_to_unorm16((a + 128.0f) * (1 / 255.0f)); |
| Lab[2] = float_round_to_unorm16((b + 128.0f) * (1 / 255.0f)); |
| } |
| |
| // Compute the tone mapping gain for luminance value L. The gain should be |
| // applied after the transfer function is applied. |
| float compute_tone_map_gain(const skcms_TransferFunction& fn, float L) { |
| if (L <= 0.f) { |
| return 1.f; |
| } |
| if (skcms_TransferFunction_isPQish(&fn)) { |
| // 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 (skcms_TransferFunction_isHLGish(&fn)) { |
| // 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; |
| } |
| |
| // Write a lookup table based curve, potentially including tone mapping. |
| static sk_sp<SkData> write_curv_tag(const skcms_TransferFunction& fn, |
| uint32_t value_count, |
| bool tone_map) { |
| SkDynamicMemoryWStream s; |
| s.write32(SkEndian_SwapBE32(kTAG_CurveType)); // Type |
| s.write32(0); // Reserved |
| s.write32(SkEndian_SwapBE32(value_count)); // Value count |
| for (uint32_t x_index = 0; x_index < value_count; ++x_index) { |
| float x = x_index / (value_count - 1.f); |
| x = skcms_TransferFunction_eval(&fn, x); |
| if (tone_map) { |
| x *= compute_tone_map_gain(fn, x); |
| } |
| s.write16(SkEndian_SwapBE16(float_round_to_unorm16(x))); |
| } |
| return s.detachAsData(); |
| } |
| |
| // Write a 3D lookup table from the specified space to Lab, potentially including tone mapping. |
| sk_sp<SkData> write_to_lab_clut(const skcms_TransferFunction& src_fn, |
| const skcms_Matrix3x3& src_to_XYZD50, |
| uint32_t grid_size, |
| bool tone_map) { |
| // Compute the matrices to convert from source to Rec2020, and from Rec2020 to XYZD50. |
| skcms_Matrix3x3 src_to_rec2020; |
| const skcms_Matrix3x3 rec2020_to_XYZD50 = SkNamedGamut::kRec2020; |
| { |
| skcms_Matrix3x3 XYZD50_to_rec2020; |
| skcms_Matrix3x3_invert(&rec2020_to_XYZD50, &XYZD50_to_rec2020); |
| src_to_rec2020 = skcms_Matrix3x3_concat(&XYZD50_to_rec2020, &src_to_XYZD50); |
| } |
| |
| SkDynamicMemoryWStream s; |
| for (size_t i = 0; i < 16; ++i) { |
| s.write8(i < 3 ? grid_size : 0); // Grid size |
| } |
| s.write8(2); // Grid byte width |
| s.write8(0); // Reserved |
| s.write8(0); // Reserved |
| s.write8(0); // Reserved |
| |
| size_t index[kNumChannels] = {0}; |
| for (index[0] = 0; index[0] < grid_size; ++index[0]) { |
| for (index[1] = 0; index[1] < grid_size; ++index[1]) { |
| for (index[2] = 0; index[2] < grid_size; ++index[2]) { |
| float rgb[3] = { |
| index[0] / (grid_size - 1.f), |
| index[1] / (grid_size - 1.f), |
| index[2] / (grid_size - 1.f), |
| }; |
| |
| // Convert the source signal to linear. |
| for (size_t i = 0; i < kNumChannels; ++i) { |
| rgb[i] = skcms_TransferFunction_eval(&src_fn, rgb[i]); |
| } |
| |
| // Convert source gamut to Rec2020. |
| skcms_Matrix3x3_apply(&src_to_rec2020, rgb); |
| |
| // Compute the luminance of the signal. |
| constexpr float kLr = 0.2627f; |
| constexpr float kLg = 0.6780f; |
| constexpr float kLb = 0.0593f; |
| float L = rgb[0] * kLr + rgb[1] * kLg + rgb[2] * kLb; |
| |
| if (tone_map) { |
| // Compute the tone map gain based on the luminance. |
| float tone_map_gain = compute_tone_map_gain(src_fn, L); |
| |
| // Apply the tone map gain. |
| for (size_t i = 0; i < kNumChannels; ++i) { |
| rgb[i] *= tone_map_gain; |
| } |
| } |
| |
| // Convert from Rec2020-linear to XYZD50. |
| skcms_Matrix3x3_apply(&rec2020_to_XYZD50, rgb); |
| |
| // Convert from XYZD50 to fixed16 Lab. |
| uint16_t Lab[3] = {0}; |
| xyzd50_to_Lab_fixed16(rgb, Lab); |
| |
| for (size_t i = 0; i < kNumChannels; ++i) { |
| s.write16(SkEndian_SwapBE16(Lab[i])); |
| } |
| } |
| } |
| } |
| return s.detachAsData(); |
| } |
| |
| // Write an A2B or B2A tag for a 3D lookup table. |
| sk_sp<SkData> write_mAB_or_mBA_tag(uint32_t type, |
| uint32_t grid_size, |
| const skcms_TransferFunction& src_fn, |
| const skcms_Matrix3x3& src_toXYZD50) { |
| // The "B" curve is required, and will be the identity. |
| sk_sp<SkData> b_curve = write_para_tag(SkNamedTransferFn::kLinear); |
| size_t b_curve_offset = 32; |
| |
| // The CLUT and "B" curve may be omitted if the mapping we are creating is |
| // the identity. |
| sk_sp<SkData> clut; |
| size_t clut_offset = 0; |
| sk_sp<SkData> a_curve; |
| size_t a_curve_offset = 0; |
| if (grid_size >= 2) { |
| // The CLUT will convert from the source to tone mapped Lab. |
| clut = write_to_lab_clut(src_fn, src_toXYZD50, grid_size, /*tone_map=*/true); |
| clut_offset = b_curve_offset + 3 * b_curve->size(); |
| |
| // The "A" curve is required (because the CLUT was provided), and it is |
| // the identity. |
| a_curve = write_para_tag(SkNamedTransferFn::kLinear); |
| a_curve_offset = clut_offset + clut->size(); |
| } |
| |
| SkDynamicMemoryWStream s; |
| s.write32(SkEndian_SwapBE32(type)); // Type signature |
| s.write32(0); // Reserved |
| s.write8(kNumChannels); // Input channels |
| s.write8(kNumChannels); // Output channels |
| s.write16(0); // Reserved |
| s.write32(SkEndian_SwapBE32(b_curve_offset)); // B curve offset |
| s.write32(SkEndian_SwapBE32(0)); // Matrix offset (ignored) |
| s.write32(SkEndian_SwapBE32(0)); // M curve offset (ignored) |
| s.write32(SkEndian_SwapBE32(clut_offset)); // CLUT offset |
| s.write32(SkEndian_SwapBE32(a_curve_offset)); // A curve offset |
| SkASSERT(s.bytesWritten() == b_curve_offset); |
| for (size_t i = 0; i < kNumChannels; ++i) { |
| s.write(b_curve->data(), b_curve->size()); |
| } |
| if (grid_size >= 2) { |
| SkASSERT(s.bytesWritten() == clut_offset); |
| s.write(clut->data(), clut->size()); |
| SkASSERT(s.bytesWritten() == a_curve_offset); |
| for (size_t i = 0; i < kNumChannels; ++i) { |
| s.write(a_curve->data(), a_curve->size()); |
| } |
| } |
| return s.detachAsData(); |
| } |
| |
| sk_sp<SkData> SkWriteICCProfileInternal(const skcms_TransferFunction& in_fn, |
| const skcms_Matrix3x3& toXYZD50, |
| uint32_t tone_map_grid_size, |
| uint32_t tone_map_curv_size) { |
| // Some PQ and HLG input functions are scaled. Replace them here with an |
| // un-scaled version. |
| skcms_TransferFunction fn = in_fn; |
| switch (classify_transfer_fn(in_fn)) { |
| case PQish_TF: |
| fn = SkNamedTransferFn::kPQ; |
| break; |
| case HLGish_TF: |
| fn = SkNamedTransferFn::kHLG; |
| fn.f = 1 / 12.f - 1.f; |
| break; |
| default: |
| break; |
| } |
| |
| // Compute the CICP primaries and transfer function, if they can be |
| // identified. |
| uint32_t cicp_primaries = get_cicp_primaries(toXYZD50); |
| uint32_t cicp_trfn = get_cicp_trfn(fn); |
| if (classify_transfer_fn(fn) != sRGBish_TF) { |
| // Non-sRGB-ish transfer functions can only be represented by CICP. IF |
| // the transfer function is not sRGB-ish, and we don't have a CICP |
| // representation, then fail. |
| if (!cicp_primaries || !cicp_trfn) { |
| return nullptr; |
| } |
| } |
| ICCHeader header; |
| |
| std::vector<std::pair<uint32_t, sk_sp<SkData>>> tags; |
| |
| // Compute profile description tag |
| std::string description = get_desc_string(fn, toXYZD50, cicp_trfn, cicp_primaries); |
| tags.emplace_back(kTAG_desc, write_text_tag(description)); |
| |
| // Compute XYZ tags |
| 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)); |
| |
| // If this is an HLG or PQ profile, include a CICP tag and provide a LUT for tone mapping. |
| if (cicp_trfn == kCICPTrfnPQ || cicp_trfn == kCICPTrfnHLG) { |
| // The CICP tag is present in ICC 4.4, so update the header's version. |
| header.version = SkEndian_SwapBE32(0x04400000); |
| tags.emplace_back(kTAG_cicp, write_cicp_tag(cicp_primaries, cicp_trfn)); |
| |
| // Provide a 3D lookup table for the transformation to Lab. |
| if (tone_map_grid_size >= 2) { |
| header.pcs = SkEndian_SwapBE32(kLAB_PCSSpace); |
| tags.emplace_back(kTAG_A2B0, |
| write_mAB_or_mBA_tag(kTAG_mABType, tone_map_grid_size, fn, toXYZD50)); |
| // Provide a no-op B2A0 lookup table. If this tag is not provided, then several macOS |
| // applications (e.g, Preview) will also ignore the A2B0 table. |
| tags.emplace_back(kTAG_B2A0, |
| write_mAB_or_mBA_tag(kTAG_mBAType, |
| /*grid_size=*/0, |
| fn, |
| toXYZD50)); |
| } |
| |
| // Provide a 1D transfer function, if requested. |
| if (tone_map_curv_size >= 2) { |
| // Represent the transfer function parametrically. |
| tags.emplace_back(kTAG_rTRC, write_curv_tag(fn, tone_map_curv_size, /*tone_map=*/true)); |
| // Use empty data to indicate that the entry should use the previous tag's |
| // data. |
| tags.emplace_back(kTAG_gTRC, SkData::MakeEmpty()); |
| tags.emplace_back(kTAG_bTRC, SkData::MakeEmpty()); |
| } |
| } else { |
| // Represent the transfer function parametrically. |
| tags.emplace_back(kTAG_rTRC, write_para_tag(fn)); |
| // Use empty data to indicate that the entry should use the previous tag's |
| // data. |
| tags.emplace_back(kTAG_gTRC, SkData::MakeEmpty()); |
| tags.emplace_back(kTAG_bTRC, SkData::MakeEmpty()); |
| } |
| |
| // Compute copyright tag |
| tags.emplace_back(kTAG_cprt, write_text_tag("Google Inc. 2016")); |
| |
| // Compute the size of the profile. |
| size_t tag_data_size = 0; |
| for (const auto& tag : tags) { |
| tag_data_size += tag.second->size(); |
| } |
| size_t tag_table_size = kICCTagTableEntrySize * tags.size(); |
| size_t profile_size = kICCHeaderSize + tag_table_size + tag_data_size; |
| |
| // Write the header. |
| header.size = SkEndian_SwapBE32(profile_size); |
| header.tag_count = SkEndian_SwapBE32(tags.size()); |
| |
| SkAutoMalloc profile(profile_size); |
| uint8_t* ptr = (uint8_t*)profile.get(); |
| memcpy(ptr, &header, sizeof(header)); |
| ptr += sizeof(header); |
| |
| // Write the tag table. Track the offset and size of the previous tag to |
| // compute each tag's offset. An empty SkData indicates that the previous |
| // tag is to be reused. |
| size_t last_tag_offset = sizeof(header) + tag_table_size; |
| size_t last_tag_size = 0; |
| for (const auto& tag : tags) { |
| if (!tag.second->isEmpty()) { |
| last_tag_offset = last_tag_offset + last_tag_size; |
| last_tag_size = tag.second->size(); |
| } |
| uint32_t tag_table_entry[3] = { |
| SkEndian_SwapBE32(tag.first), |
| SkEndian_SwapBE32(last_tag_offset), |
| SkEndian_SwapBE32(last_tag_size), |
| }; |
| memcpy(ptr, tag_table_entry, sizeof(tag_table_entry)); |
| ptr += sizeof(tag_table_entry); |
| } |
| |
| // Write the tags. |
| for (const auto& tag : tags) { |
| if (tag.second->isEmpty()) continue; |
| memcpy(ptr, tag.second->data(), tag.second->size()); |
| ptr += tag.second->size(); |
| } |
| |
| SkASSERT(profile_size == static_cast<size_t>(ptr - (uint8_t*)profile.get())); |
| return SkData::MakeFromMalloc(profile.release(), profile_size); |
| } |
| |
| sk_sp<SkData> SkWriteICCProfile(const skcms_TransferFunction& fn, const skcms_Matrix3x3& toXYZD50) { |
| // The grid size for 3D LUTs. |
| const uint32_t kGridSize = 17; |
| return SkWriteICCProfileInternal(fn, toXYZD50, kGridSize, 17); |
| } |