diff options
Diffstat (limited to 'third_party/jpeg-xl/lib/jxl/cms/color_encoding_cms.h')
-rw-r--r-- | third_party/jpeg-xl/lib/jxl/cms/color_encoding_cms.h | 623 |
1 files changed, 623 insertions, 0 deletions
diff --git a/third_party/jpeg-xl/lib/jxl/cms/color_encoding_cms.h b/third_party/jpeg-xl/lib/jxl/cms/color_encoding_cms.h new file mode 100644 index 0000000000..db61f820ca --- /dev/null +++ b/third_party/jpeg-xl/lib/jxl/cms/color_encoding_cms.h @@ -0,0 +1,623 @@ +// Copyright (c) the JPEG XL Project Authors. All rights reserved. +// +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +#ifndef LIB_JXL_CMS_COLOR_ENCODING_CMS_H_ +#define LIB_JXL_CMS_COLOR_ENCODING_CMS_H_ + +#include <jxl/cms_interface.h> +#include <jxl/color_encoding.h> +#include <jxl/types.h> + +#include <cmath> +#include <cstdint> +#include <cstring> +#include <utility> +#include <vector> + +#include "lib/jxl/base/status.h" + +namespace jxl { +namespace cms { + +using IccBytes = std::vector<uint8_t>; + +// Returns whether the two inputs are approximately equal. +static inline bool ApproxEq(const double a, const double b, + double max_l1 = 1E-3) { + // Threshold should be sufficient for ICC's 15-bit fixed-point numbers. + // We have seen differences of 7.1E-5 with lcms2 and 1E-3 with skcms. + return std::abs(a - b) <= max_l1; +} + +// (All CIE units are for the standard 1931 2 degree observer) + +// Color space the color pixel data is encoded in. The color pixel data is +// 3-channel in all cases except in case of kGray, where it uses only 1 channel. +// This also determines the amount of channels used in modular encoding. +enum class ColorSpace : uint32_t { + // Trichromatic color data. This also includes CMYK if a kBlack + // ExtraChannelInfo is present. This implies, if there is an ICC profile, that + // the ICC profile uses a 3-channel color space if no kBlack extra channel is + // present, or uses color space 'CMYK' if a kBlack extra channel is present. + kRGB, + // Single-channel data. This implies, if there is an ICC profile, that the ICC + // profile also represents single-channel data and has the appropriate color + // space ('GRAY'). + kGray, + // Like kRGB, but implies fixed values for primaries etc. + kXYB, + // For non-RGB/gray data, e.g. from non-electro-optical sensors. Otherwise + // the same conditions as kRGB apply. + kUnknown + // NB: don't forget to update EnumBits! +}; + +// Values from CICP ColourPrimaries. +enum class WhitePoint : uint32_t { + kD65 = 1, // sRGB/BT.709/Display P3/BT.2020 + kCustom = 2, // Actual values encoded in separate fields + kE = 10, // XYZ + kDCI = 11, // DCI-P3 + // NB: don't forget to update EnumBits! +}; + +// Values from CICP ColourPrimaries +enum class Primaries : uint32_t { + kSRGB = 1, // Same as BT.709 + kCustom = 2, // Actual values encoded in separate fields + k2100 = 9, // Same as BT.2020 + kP3 = 11, + // NB: don't forget to update EnumBits! +}; + +// Values from CICP TransferCharacteristics +enum class TransferFunction : uint32_t { + k709 = 1, + kUnknown = 2, + kLinear = 8, + kSRGB = 13, + kPQ = 16, // from BT.2100 + kDCI = 17, // from SMPTE RP 431-2 reference projector + kHLG = 18, // from BT.2100 + // NB: don't forget to update EnumBits! +}; + +enum class RenderingIntent : uint32_t { + // Values match ICC sRGB encodings. + kPerceptual = 0, // good for photos, requires a profile with LUT. + kRelative, // good for logos. + kSaturation, // perhaps useful for CG with fully saturated colors. + kAbsolute, // leaves white point unchanged; good for proofing. + // NB: don't forget to update EnumBits! +}; + +// Chromaticity (Y is omitted because it is 1 for white points and implicit for +// primaries) +struct CIExy { + double x = 0.0; + double y = 0.0; +}; + +struct PrimariesCIExy { + CIExy r; + CIExy g; + CIExy b; +}; + +// Serializable form of CIExy. +struct Customxy { + static constexpr uint32_t kMul = 1000000; + static constexpr double kRoughLimit = 4.0; + static constexpr int32_t kMin = -0x200000; + static constexpr int32_t kMax = 0x1FFFFF; + + int32_t x = 0; + int32_t y = 0; + + CIExy GetValue() const { + CIExy xy; + xy.x = x * (1.0 / kMul); + xy.y = y * (1.0 / kMul); + return xy; + } + + Status SetValue(const CIExy& xy) { + bool ok = (std::abs(xy.x) < kRoughLimit) && (std::abs(xy.y) < kRoughLimit); + if (!ok) return JXL_FAILURE("X or Y is out of bounds"); + x = static_cast<int32_t>(roundf(xy.x * kMul)); + if (x < kMin || x > kMax) return JXL_FAILURE("X is out of bounds"); + y = static_cast<int32_t>(roundf(xy.y * kMul)); + if (y < kMin || y > kMax) return JXL_FAILURE("Y is out of bounds"); + return true; + } + + bool IsSame(const Customxy& other) const { + return (x == other.x) && (y == other.y); + } +}; + +static inline Status WhitePointFromExternal(const JxlWhitePoint external, + WhitePoint* out) { + switch (external) { + case JXL_WHITE_POINT_D65: + *out = WhitePoint::kD65; + return true; + case JXL_WHITE_POINT_CUSTOM: + *out = WhitePoint::kCustom; + return true; + case JXL_WHITE_POINT_E: + *out = WhitePoint::kE; + return true; + case JXL_WHITE_POINT_DCI: + *out = WhitePoint::kDCI; + return true; + } + return JXL_FAILURE("Invalid WhitePoint enum value %d", + static_cast<int>(external)); +} + +static inline Status PrimariesFromExternal(const JxlPrimaries external, + Primaries* out) { + switch (external) { + case JXL_PRIMARIES_SRGB: + *out = Primaries::kSRGB; + return true; + case JXL_PRIMARIES_CUSTOM: + *out = Primaries::kCustom; + return true; + case JXL_PRIMARIES_2100: + *out = Primaries::k2100; + return true; + case JXL_PRIMARIES_P3: + *out = Primaries::kP3; + return true; + } + return JXL_FAILURE("Invalid Primaries enum value"); +} + +static inline Status RenderingIntentFromExternal( + const JxlRenderingIntent external, RenderingIntent* out) { + switch (external) { + case JXL_RENDERING_INTENT_PERCEPTUAL: + *out = RenderingIntent::kPerceptual; + return true; + case JXL_RENDERING_INTENT_RELATIVE: + *out = RenderingIntent::kRelative; + return true; + case JXL_RENDERING_INTENT_SATURATION: + *out = RenderingIntent::kSaturation; + return true; + case JXL_RENDERING_INTENT_ABSOLUTE: + *out = RenderingIntent::kAbsolute; + return true; + } + return JXL_FAILURE("Invalid RenderingIntent enum value"); +} + +struct CustomTransferFunction { + // Highest reasonable value for the gamma of a transfer curve. + static constexpr uint32_t kMaxGamma = 8192; + static constexpr uint32_t kGammaMul = 10000000; + + bool have_gamma = false; + + // OETF exponent to go from linear to gamma-compressed. + uint32_t gamma = 0; // Only used if have_gamma_. + + // Can be kUnknown. + TransferFunction transfer_function = + TransferFunction::kSRGB; // Only used if !have_gamma_. + + TransferFunction GetTransferFunction() const { + JXL_ASSERT(!have_gamma); + return transfer_function; + } + void SetTransferFunction(const TransferFunction tf) { + have_gamma = false; + transfer_function = tf; + } + + bool IsUnknown() const { + return !have_gamma && (transfer_function == TransferFunction::kUnknown); + } + bool IsSRGB() const { + return !have_gamma && (transfer_function == TransferFunction::kSRGB); + } + bool IsLinear() const { + return !have_gamma && (transfer_function == TransferFunction::kLinear); + } + bool IsPQ() const { + return !have_gamma && (transfer_function == TransferFunction::kPQ); + } + bool IsHLG() const { + return !have_gamma && (transfer_function == TransferFunction::kHLG); + } + bool Is709() const { + return !have_gamma && (transfer_function == TransferFunction::k709); + } + bool IsDCI() const { + return !have_gamma && (transfer_function == TransferFunction::kDCI); + } + + double GetGamma() const { + JXL_ASSERT(have_gamma); + return gamma * (1.0 / kGammaMul); // (0, 1) + } + Status SetGamma(double new_gamma) { + if (new_gamma < (1.0 / kMaxGamma) || new_gamma > 1.0) { + return JXL_FAILURE("Invalid gamma %f", new_gamma); + } + + have_gamma = false; + if (ApproxEq(new_gamma, 1.0)) { + transfer_function = TransferFunction::kLinear; + return true; + } + if (ApproxEq(new_gamma, 1.0 / 2.6)) { + transfer_function = TransferFunction::kDCI; + return true; + } + // Don't translate 0.45.. to kSRGB nor k709 - that might change pixel + // values because those curves also have a linear part. + + have_gamma = true; + gamma = roundf(new_gamma * kGammaMul); + transfer_function = TransferFunction::kUnknown; + return true; + } + + bool IsSame(const CustomTransferFunction& other) const { + if (have_gamma != other.have_gamma) { + return false; + } + if (have_gamma) { + if (gamma != other.gamma) { + return false; + } + } else { + if (transfer_function != other.transfer_function) { + return false; + } + } + return true; + } +}; + +static inline Status ConvertExternalToInternalTransferFunction( + const JxlTransferFunction external, TransferFunction* internal) { + switch (external) { + case JXL_TRANSFER_FUNCTION_709: + *internal = TransferFunction::k709; + return true; + case JXL_TRANSFER_FUNCTION_UNKNOWN: + *internal = TransferFunction::kUnknown; + return true; + case JXL_TRANSFER_FUNCTION_LINEAR: + *internal = TransferFunction::kLinear; + return true; + case JXL_TRANSFER_FUNCTION_SRGB: + *internal = TransferFunction::kSRGB; + return true; + case JXL_TRANSFER_FUNCTION_PQ: + *internal = TransferFunction::kPQ; + return true; + case JXL_TRANSFER_FUNCTION_DCI: + *internal = TransferFunction::kDCI; + return true; + case JXL_TRANSFER_FUNCTION_HLG: + *internal = TransferFunction::kHLG; + return true; + case JXL_TRANSFER_FUNCTION_GAMMA: + return JXL_FAILURE("Gamma should be handled separately"); + } + return JXL_FAILURE("Invalid TransferFunction enum value"); +} + +// Compact encoding of data required to interpret and translate pixels to a +// known color space. Stored in Metadata. Thread-compatible. +struct ColorEncoding { + // Only valid if HaveFields() + WhitePoint white_point = WhitePoint::kD65; + Primaries primaries = Primaries::kSRGB; // Only valid if HasPrimaries() + RenderingIntent rendering_intent = RenderingIntent::kRelative; + + // When false, fields such as white_point and tf are invalid and must not be + // used. This occurs after setting a raw bytes-only ICC profile, only the + // ICC bytes may be used. The color_space_ field is still valid. + bool have_fields = true; + + IccBytes icc; // Valid ICC profile + + ColorSpace color_space = ColorSpace::kRGB; // Can be kUnknown + bool cmyk = false; + + // "late sync" fields + CustomTransferFunction tf; + Customxy white; // Only used if white_point == kCustom + Customxy red; // Only used if primaries == kCustom + Customxy green; // Only used if primaries == kCustom + Customxy blue; // Only used if primaries == kCustom + + // Returns false if the field is invalid and unusable. + bool HasPrimaries() const { + return (color_space != ColorSpace::kGray) && + (color_space != ColorSpace::kXYB); + } + + size_t Channels() const { return (color_space == ColorSpace::kGray) ? 1 : 3; } + + PrimariesCIExy GetPrimaries() const { + JXL_DASSERT(have_fields); + JXL_ASSERT(HasPrimaries()); + PrimariesCIExy xy; + switch (primaries) { + case Primaries::kCustom: + xy.r = red.GetValue(); + xy.g = green.GetValue(); + xy.b = blue.GetValue(); + return xy; + + case Primaries::kSRGB: + xy.r.x = 0.639998686; + xy.r.y = 0.330010138; + xy.g.x = 0.300003784; + xy.g.y = 0.600003357; + xy.b.x = 0.150002046; + xy.b.y = 0.059997204; + return xy; + + case Primaries::k2100: + xy.r.x = 0.708; + xy.r.y = 0.292; + xy.g.x = 0.170; + xy.g.y = 0.797; + xy.b.x = 0.131; + xy.b.y = 0.046; + return xy; + + case Primaries::kP3: + xy.r.x = 0.680; + xy.r.y = 0.320; + xy.g.x = 0.265; + xy.g.y = 0.690; + xy.b.x = 0.150; + xy.b.y = 0.060; + return xy; + } + JXL_UNREACHABLE("Invalid Primaries %u", static_cast<uint32_t>(primaries)); + } + + Status SetPrimaries(const PrimariesCIExy& xy) { + JXL_DASSERT(have_fields); + JXL_ASSERT(HasPrimaries()); + if (xy.r.x == 0.0 || xy.r.y == 0.0 || xy.g.x == 0.0 || xy.g.y == 0.0 || + xy.b.x == 0.0 || xy.b.y == 0.0) { + return JXL_FAILURE("Invalid primaries %f %f %f %f %f %f", xy.r.x, xy.r.y, + xy.g.x, xy.g.y, xy.b.x, xy.b.y); + } + + if (ApproxEq(xy.r.x, 0.64) && ApproxEq(xy.r.y, 0.33) && + ApproxEq(xy.g.x, 0.30) && ApproxEq(xy.g.y, 0.60) && + ApproxEq(xy.b.x, 0.15) && ApproxEq(xy.b.y, 0.06)) { + primaries = Primaries::kSRGB; + return true; + } + + if (ApproxEq(xy.r.x, 0.708) && ApproxEq(xy.r.y, 0.292) && + ApproxEq(xy.g.x, 0.170) && ApproxEq(xy.g.y, 0.797) && + ApproxEq(xy.b.x, 0.131) && ApproxEq(xy.b.y, 0.046)) { + primaries = Primaries::k2100; + return true; + } + if (ApproxEq(xy.r.x, 0.680) && ApproxEq(xy.r.y, 0.320) && + ApproxEq(xy.g.x, 0.265) && ApproxEq(xy.g.y, 0.690) && + ApproxEq(xy.b.x, 0.150) && ApproxEq(xy.b.y, 0.060)) { + primaries = Primaries::kP3; + return true; + } + + primaries = Primaries::kCustom; + JXL_RETURN_IF_ERROR(red.SetValue(xy.r)); + JXL_RETURN_IF_ERROR(green.SetValue(xy.g)); + JXL_RETURN_IF_ERROR(blue.SetValue(xy.b)); + return true; + } + + CIExy GetWhitePoint() const { + JXL_DASSERT(have_fields); + CIExy xy; + switch (white_point) { + case WhitePoint::kCustom: + return white.GetValue(); + + case WhitePoint::kD65: + xy.x = 0.3127; + xy.y = 0.3290; + return xy; + + case WhitePoint::kDCI: + // From https://ieeexplore.ieee.org/document/7290729 C.2 page 11 + xy.x = 0.314; + xy.y = 0.351; + return xy; + + case WhitePoint::kE: + xy.x = xy.y = 1.0 / 3; + return xy; + } + JXL_UNREACHABLE("Invalid WhitePoint %u", + static_cast<uint32_t>(white_point)); + } + + Status SetWhitePoint(const CIExy& xy) { + JXL_DASSERT(have_fields); + if (xy.x == 0.0 || xy.y == 0.0) { + return JXL_FAILURE("Invalid white point %f %f", xy.x, xy.y); + } + if (ApproxEq(xy.x, 0.3127) && ApproxEq(xy.y, 0.3290)) { + white_point = WhitePoint::kD65; + return true; + } + if (ApproxEq(xy.x, 1.0 / 3) && ApproxEq(xy.y, 1.0 / 3)) { + white_point = WhitePoint::kE; + return true; + } + if (ApproxEq(xy.x, 0.314) && ApproxEq(xy.y, 0.351)) { + white_point = WhitePoint::kDCI; + return true; + } + white_point = WhitePoint::kCustom; + return white.SetValue(xy); + } + + // Checks if the color spaces (including white point / primaries) are the + // same, but ignores the transfer function, rendering intent and ICC bytes. + bool SameColorSpace(const ColorEncoding& other) const { + if (color_space != other.color_space) return false; + + if (white_point != other.white_point) return false; + if (white_point == WhitePoint::kCustom) { + if (!white.IsSame(other.white)) { + return false; + } + } + + if (HasPrimaries() != other.HasPrimaries()) return false; + if (HasPrimaries()) { + if (primaries != other.primaries) return false; + if (primaries == Primaries::kCustom) { + if (!red.IsSame(other.red)) return false; + if (!green.IsSame(other.green)) return false; + if (!blue.IsSame(other.blue)) return false; + } + } + return true; + } + + // Checks if the color space and transfer function are the same, ignoring + // rendering intent and ICC bytes + bool SameColorEncoding(const ColorEncoding& other) const { + return SameColorSpace(other) && tf.IsSame(other.tf); + } + + // Returns true if all fields have been initialized (possibly to kUnknown). + // Returns false if the ICC profile is invalid or decoding it fails. + Status SetFieldsFromICC(IccBytes&& new_icc, const JxlCmsInterface& cms) { + // In case parsing fails, mark the ColorEncoding as invalid. + JXL_ASSERT(!new_icc.empty()); + color_space = ColorSpace::kUnknown; + tf.transfer_function = TransferFunction::kUnknown; + icc.clear(); + + JxlColorEncoding external; + JXL_BOOL new_cmyk; + JXL_RETURN_IF_ERROR(cms.set_fields_from_icc(cms.set_fields_data, + new_icc.data(), new_icc.size(), + &external, &new_cmyk)); + cmyk = new_cmyk; + JXL_RETURN_IF_ERROR(FromExternal(external)); + icc = std::move(new_icc); + return true; + } + + JxlColorEncoding ToExternal() const { + JxlColorEncoding external = {}; + if (!have_fields) { + external.color_space = JXL_COLOR_SPACE_UNKNOWN; + external.primaries = JXL_PRIMARIES_CUSTOM; + external.rendering_intent = JXL_RENDERING_INTENT_PERCEPTUAL; //? + external.transfer_function = JXL_TRANSFER_FUNCTION_UNKNOWN; + external.white_point = JXL_WHITE_POINT_CUSTOM; + return external; + } + external.color_space = static_cast<JxlColorSpace>(color_space); + + external.white_point = static_cast<JxlWhitePoint>(white_point); + + CIExy wp = GetWhitePoint(); + external.white_point_xy[0] = wp.x; + external.white_point_xy[1] = wp.y; + + if (external.color_space == JXL_COLOR_SPACE_RGB || + external.color_space == JXL_COLOR_SPACE_UNKNOWN) { + external.primaries = static_cast<JxlPrimaries>(primaries); + PrimariesCIExy p = GetPrimaries(); + external.primaries_red_xy[0] = p.r.x; + external.primaries_red_xy[1] = p.r.y; + external.primaries_green_xy[0] = p.g.x; + external.primaries_green_xy[1] = p.g.y; + external.primaries_blue_xy[0] = p.b.x; + external.primaries_blue_xy[1] = p.b.y; + } + + if (tf.have_gamma) { + external.transfer_function = JXL_TRANSFER_FUNCTION_GAMMA; + external.gamma = tf.GetGamma(); + } else { + external.transfer_function = + static_cast<JxlTransferFunction>(tf.GetTransferFunction()); + external.gamma = 0; + } + + external.rendering_intent = + static_cast<JxlRenderingIntent>(rendering_intent); + return external; + } + + // NB: does not create ICC. + Status FromExternal(const JxlColorEncoding& external) { + // TODO(eustas): update non-serializable on call-site + color_space = static_cast<ColorSpace>(external.color_space); + + JXL_RETURN_IF_ERROR( + WhitePointFromExternal(external.white_point, &white_point)); + if (external.white_point == JXL_WHITE_POINT_CUSTOM) { + CIExy wp; + wp.x = external.white_point_xy[0]; + wp.y = external.white_point_xy[1]; + JXL_RETURN_IF_ERROR(SetWhitePoint(wp)); + } + + if (external.color_space == JXL_COLOR_SPACE_RGB || + external.color_space == JXL_COLOR_SPACE_UNKNOWN) { + JXL_RETURN_IF_ERROR( + PrimariesFromExternal(external.primaries, &primaries)); + if (external.primaries == JXL_PRIMARIES_CUSTOM) { + PrimariesCIExy primaries; + primaries.r.x = external.primaries_red_xy[0]; + primaries.r.y = external.primaries_red_xy[1]; + primaries.g.x = external.primaries_green_xy[0]; + primaries.g.y = external.primaries_green_xy[1]; + primaries.b.x = external.primaries_blue_xy[0]; + primaries.b.y = external.primaries_blue_xy[1]; + JXL_RETURN_IF_ERROR(SetPrimaries(primaries)); + } + } + CustomTransferFunction tf; + if (external.transfer_function == JXL_TRANSFER_FUNCTION_GAMMA) { + JXL_RETURN_IF_ERROR(tf.SetGamma(external.gamma)); + } else { + TransferFunction tf_enum; + // JXL_TRANSFER_FUNCTION_GAMMA is not handled by this function since + // there's no internal enum value for it. + JXL_RETURN_IF_ERROR(ConvertExternalToInternalTransferFunction( + external.transfer_function, &tf_enum)); + tf.SetTransferFunction(tf_enum); + } + this->tf = tf; + + JXL_RETURN_IF_ERROR(RenderingIntentFromExternal(external.rendering_intent, + &rendering_intent)); + + icc.clear(); + + return true; + } +}; + +} // namespace cms +} // namespace jxl + +#endif // LIB_JXL_CMS_COLOR_ENCODING_CMS_H_ |