// 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.
// Define these only once. We can't use HWY_ONCE here because it is defined as // 1 only on the last pass. #ifndef LIB_JXL_JXL_CMS_CC #define LIB_JXL_JXL_CMS_CC
// These fields are used when the HLG OOTF or inverse OOTF must be applied. bool apply_hlg_ootf;
size_t hlg_ootf_num_channels; // Y component of the primaries.
std::array<float, 3> hlg_ootf_luminances;
#if JPEGXL_ENABLE_SKCMS if (t->channels_dst == 1 && !t->skip_lcms) { // Contract back from 3 to 1 channel, this time forward. float* grayscale_buf_dst = t->buf_dst[thread]; for (size_t x = 0; x < xsize; ++x) {
grayscale_buf_dst[x] = buf_dst[x * 3];
}
buf_dst = grayscale_buf_dst;
} #endif
Status CreateProfileXYZ(const cmsContext context,
Profile* JXL_RESTRICT profile) {
profile->reset(cmsCreateXYZProfileTHR(context)); if (profile->get() == nullptr) return JXL_FAILURE("Failed to create XYZ"); returntrue;
}
#endif// !JPEGXL_ENABLE_SKCMS
#if JPEGXL_ENABLE_SKCMS // IMPORTANT: icc must outlive profile.
Status DecodeProfile(const uint8_t* icc, size_t size,
skcms_ICCProfile* const profile) { if (!skcms_Parse(icc, size, profile)) { return JXL_FAILURE("Failed to parse ICC profile with %" PRIuS " bytes",
size);
} returntrue;
} #else// JPEGXL_ENABLE_SKCMS
Status DecodeProfile(const cmsContext context, Span<const uint8_t> icc,
Profile* profile) {
profile->reset(cmsOpenProfileFromMemTHR(context, icc.data(), icc.size())); if (profile->get() == nullptr) { return JXL_FAILURE("Failed to decode profile");
}
// WARNING: due to the LCMS MD5 issue mentioned above, many existing // profiles have incorrect MD5, so do not even bother checking them nor // generating warning clutter.
returntrue;
} #endif// JPEGXL_ENABLE_SKCMS
#if JPEGXL_ENABLE_SKCMS
ColorSpace ColorSpaceFromProfile(const skcms_ICCProfile& profile) { switch (profile.data_color_space) { case skcms_Signature_RGB: case skcms_Signature_CMYK: // spec says CMYK is encoded as RGB (the kBlack extra channel signals that // it is actually CMYK) return ColorSpace::kRGB; case skcms_Signature_Gray: return ColorSpace::kGray; default: return ColorSpace::kUnknown;
}
}
// Returns white point that was specified when creating the profile.
JXL_MUST_USE_RESULT Status UnadaptedWhitePoint(const skcms_ICCProfile& profile,
CIExy* out) {
Color media_white_point_XYZ; if (!skcms_GetWTPT(&profile, media_white_point_XYZ.data())) { return JXL_FAILURE("ICC profile does not contain WhitePoint tag");
}
skcms_Matrix3x3 CHAD; if (!skcms_GetCHAD(&profile, &CHAD)) { // If there is no chromatic adaptation matrix, it means that the white point // is already unadapted.
*out = CIExyFromXYZ(media_white_point_XYZ); returntrue;
} // Otherwise, it has been adapted to the PCS white point using said matrix, // and the adaptation needs to be undone.
skcms_Matrix3x3 inverse_CHAD; if (!skcms_Matrix3x3_invert(&CHAD, &inverse_CHAD)) { return JXL_FAILURE("Non-invertible ChromaticAdaptation matrix");
}
Color unadapted_white_point_XYZ;
MatrixProduct(inverse_CHAD, media_white_point_XYZ, unadapted_white_point_XYZ);
*out = CIExyFromXYZ(unadapted_white_point_XYZ); returntrue;
}
Status IdentifyPrimaries(const skcms_ICCProfile& profile, const CIExy& wp_unadapted, ColorEncoding* c) { if (!c->HasPrimaries()) returntrue;
Color XYZ;
PrimariesCIExy primaries;
CIExy* const chromaticities[] = {&primaries.r, &primaries.g, &primaries.b}; for (int i = 0; i < 3; ++i) { float RGB[3] = {};
RGB[i] = 1;
skcms_Transform(RGB, skcms_PixelFormat_RGB_fff, skcms_AlphaFormat_Opaque,
&profile, XYZ.data(), skcms_PixelFormat_RGB_fff,
skcms_AlphaFormat_Opaque, skcms_XYZD50_profile(), 1);
Color unadapted_XYZ;
MatrixProduct(inverse_CHAD, XYZ, unadapted_XYZ);
*chromaticities[i] = CIExyFromXYZ(unadapted_XYZ);
} return c->SetPrimaries(primaries);
}
bool IsApproximatelyEqual(const skcms_ICCProfile& profile, const ColorEncoding& JXL_RESTRICT c) {
IccBytes bytes; if (!MaybeCreateProfile(c.ToExternal(), &bytes)) { returnfalse;
}
skcms_ICCProfile profile_test; if (!DecodeProfile(bytes.data(), bytes.size(), &profile_test)) { returnfalse;
}
if (!skcms_ApproximatelyEqualProfiles(&profile_test, &profile)) { returnfalse;
}
returntrue;
}
Status DetectTransferFunction(const skcms_ICCProfile& profile,
ColorEncoding* JXL_RESTRICT c) {
JXL_ENSURE(c->color_space != ColorSpace::kXYB);
float gamma[3] = {}; if (profile.has_trc) { constauto IsGamma = [](const skcms_TransferFunction& tf) { return tf.a == 1 && tf.b == 0 && /* if b and d are zero, it is fine for c not to be */ tf.d == 0 &&
tf.e == 0 && tf.f == 0;
}; for (int i = 0; i < 3; ++i) { if (profile.trc[i].table_entries == 0 &&
IsGamma(profile.trc->parametric)) {
gamma[i] = 1.f / profile.trc->parametric.g;
} else {
skcms_TransferFunction approximate_tf; float max_error; if (skcms_ApproximateCurve(&profile.trc[i], &approximate_tf,
&max_error)) { if (IsGamma(approximate_tf)) {
gamma[i] = 1.f / approximate_tf.g;
}
}
}
}
} if (gamma[0] != 0 && std::abs(gamma[0] - gamma[1]) < 1e-4f &&
std::abs(gamma[1] - gamma[2]) < 1e-4f) { if (c->tf.SetGamma(gamma[0])) { if (IsApproximatelyEqual(profile, *c)) returntrue;
}
}
for (TransferFunction tf : Values<TransferFunction>()) { // Can only create profile from known transfer function. if (tf == TransferFunction::kUnknown) continue;
c->tf.SetTransferFunction(tf); if (IsApproximatelyEqual(profile, *c)) returntrue;
}
uint32_t Type32(const ColorEncoding& c, bool cmyk) { if (cmyk) return TYPE_CMYK_FLT; if (c.color_space == ColorSpace::kGray) return TYPE_GRAY_FLT; return TYPE_RGB_FLT;
}
uint32_t Type64(const ColorEncoding& c) { if (c.color_space == ColorSpace::kGray) return TYPE_GRAY_DBL; return TYPE_RGB_DBL;
}
ColorSpace ColorSpaceFromProfile(const Profile& profile) { switch (cmsGetColorSpace(profile.get())) { case cmsSigRgbData: case cmsSigCmykData: return ColorSpace::kRGB; case cmsSigGrayData: return ColorSpace::kGray; default: return ColorSpace::kUnknown;
}
}
// "profile1" is pre-decoded to save time in DetectTransferFunction.
Status ProfileEquivalentToICC(const cmsContext context, const Profile& profile1, const IccBytes& icc, const ColorEncoding& c) { const uint32_t type_src = Type64(c);
// Uniformly spaced samples from very dark to almost fully bright. constdouble init = 1E-3; constdouble step = 0.2;
if (c.color_space == ColorSpace::kGray) { // Finer sampling and replicate each component. for (in[0] = init; in[0] < 1.0; in[0] += step / 8) {
cmsDoTransform(xform1.get(), in, out1, 1);
cmsDoTransform(xform2.get(), in, out2, 1); if (!cms::ApproxEq(out1[0], out2[0], 2E-4)) { returnfalse;
}
}
} else { for (in[0] = init; in[0] < 1.0; in[0] += step) { for (in[1] = init; in[1] < 1.0; in[1] += step) { for (in[2] = init; in[2] < 1.0; in[2] += step) {
cmsDoTransform(xform1.get(), in, out1, 1);
cmsDoTransform(xform2.get(), in, out2, 1); for (size_t i = 0; i < 3; ++i) { if (!cms::ApproxEq(out1[i], out2[i], 2E-4)) { returnfalse;
}
}
}
}
}
}
returntrue;
}
// Returns white point that was specified when creating the profile. // NOTE: we can't just use cmsSigMediaWhitePointTag because its interpretation // differs between ICC versions.
JXL_MUST_USE_RESULT cmsCIEXYZ UnadaptedWhitePoint(const cmsContext context, const Profile& profile, const ColorEncoding& c) { const cmsCIEXYZ* white_point = static_cast<const cmsCIEXYZ*>(
cmsReadTag(profile.get(), cmsSigMediaWhitePointTag)); if (white_point != nullptr &&
cmsReadTag(profile.get(), cmsSigChromaticAdaptationTag) == nullptr) { // No chromatic adaptation matrix: the white point is already unadapted. return *white_point;
}
cmsCIEXYZ XYZ = {1.0, 1.0, 1.0};
Profile profile_xyz; if (!CreateProfileXYZ(context, &profile_xyz)) return XYZ; // Array arguments are one per profile.
cmsHPROFILE profiles[2] = {profile.get(), profile_xyz.get()}; // Leave white point unchanged - that is what we're trying to extract.
cmsUInt32Number intents[2] = {INTENT_ABSOLUTE_COLORIMETRIC,
INTENT_ABSOLUTE_COLORIMETRIC};
cmsBool black_compensation[2] = {0, 0};
cmsFloat64Number adaption[2] = {0.0, 0.0}; // Only transforming a single pixel, so skip expensive optimizations.
cmsUInt32Number flags = cmsFLAGS_NOOPTIMIZE | cmsFLAGS_HIGHRESPRECALC;
Transform xform(cmsCreateExtendedTransform(
context, 2, profiles, black_compensation, intents, adaption, nullptr, 0,
Type64(c), TYPE_XYZ_DBL, flags)); if (!xform) return XYZ; // TODO(lode): return error
// xy are relative, so magnitude does not matter if we ignore output Y. const cmsFloat64Number in[3] = {1.0, 1.0, 1.0};
cmsDoTransform(xform.get(), in, &XYZ.X, 1); return XYZ;
}
Status IdentifyPrimaries(const cmsContext context, const Profile& profile, const cmsCIEXYZ& wp_unadapted, ColorEncoding* c) { if (!c->HasPrimaries()) returntrue; if (ColorSpaceFromProfile(profile) == ColorSpace::kUnknown) returntrue;
// These were adapted to the profile illuminant before storing in the profile. const cmsCIEXYZ* adapted_r = static_cast<const cmsCIEXYZ*>(
cmsReadTag(profile.get(), cmsSigRedColorantTag)); const cmsCIEXYZ* adapted_g = static_cast<const cmsCIEXYZ*>(
cmsReadTag(profile.get(), cmsSigGreenColorantTag)); const cmsCIEXYZ* adapted_b = static_cast<const cmsCIEXYZ*>(
cmsReadTag(profile.get(), cmsSigBlueColorantTag));
cmsCIEXYZ converted_rgb[3]; if (adapted_r == nullptr || adapted_g == nullptr || adapted_b == nullptr) { // No colorant tag, determine the XYZ coordinates of the primaries by // converting from the colorspace.
Profile profile_xyz; if (!CreateProfileXYZ(context, &profile_xyz)) { return JXL_FAILURE("Failed to retrieve colorants");
} // Array arguments are one per profile.
cmsHPROFILE profiles[2] = {profile.get(), profile_xyz.get()};
cmsUInt32Number intents[2] = {INTENT_RELATIVE_COLORIMETRIC,
INTENT_RELATIVE_COLORIMETRIC};
cmsBool black_compensation[2] = {0, 0};
cmsFloat64Number adaption[2] = {0.0, 0.0}; // Only transforming three pixels, so skip expensive optimizations.
cmsUInt32Number flags = cmsFLAGS_NOOPTIMIZE | cmsFLAGS_HIGHRESPRECALC;
Transform xform(cmsCreateExtendedTransform(
context, 2, profiles, black_compensation, intents, adaption, nullptr, 0,
Type64(*c), TYPE_XYZ_DBL, flags)); if (!xform) return JXL_FAILURE("Failed to retrieve colorants");
for (TransferFunction tf : Values<TransferFunction>()) { // Can only create profile from known transfer function. if (tf == TransferFunction::kUnknown) continue;
// Returns a context for the current thread, creating it if necessary.
cmsContext GetContext() { static thread_local void* context_; if (context_ == nullptr) {
context_ = cmsCreateContext(nullptr, nullptr);
JXL_DASSERT(context_ != nullptr);
Status GetPrimariesLuminances(const ColorEncoding& encoding, float luminances[3]) { // Explanation: // We know that the three primaries must sum to white: // // [Xr, Xg, Xb; [1; [Xw; // Yr, Yg, Yb; × 1; = Yw; // Zr, Zg, Zb] 1] Zw] // // By noting that X = x·(X+Y+Z), Y = y·(X+Y+Z) and Z = z·(X+Y+Z) (note the // lower case indicating chromaticity), and factoring the totals (X+Y+Z) out // of the left matrix and into the all-ones vector, we get: // // [xr, xg, xb; [Xr + Yr + Zr; [Xw; // yr, yg, yb; × Xg + Yg + Zg; = Yw; // zr, zg, zb] Xb + Yb + Zb] Zw] // // Which makes it apparent that we can compute those totals as: // // [Xr + Yr + Zr; inv([xr, xg, xb; [Xw; // Xg + Yg + Zg; = yr, yg, yb; × Yw; // Xb + Yb + Zb] zr, zg, zb]) Zw] // // From there, by multiplying each total by its corresponding y, we get Y for // that primary.
Color white_XYZ;
CIExy wp = encoding.GetWhitePoint();
JXL_RETURN_IF_ERROR(CIEXYZFromWhiteCIExy(wp.x, wp.y, white_XYZ));
Status ApplyHlgOotf(JxlCms* t, float* JXL_RESTRICT buf, size_t xsize, bool forward) { if (295 <= t->intensity_target && t->intensity_target <= 305) { // The gamma is approximately 1 so this can essentially be skipped. returntrue;
} float gamma = 1.2f * std::pow(1.111f, std::log2(t->intensity_target * 1e-3f)); if (!forward) gamma = 1.f / gamma;
switch (t->hlg_ootf_num_channels) { case 1: for (size_t x = 0; x < xsize; ++x) {
buf[x] = std::pow(buf[x], gamma);
} break;
case 3: for (size_t x = 0; x < xsize; x += 3) { constfloat luminance = buf[x] * t->hlg_ootf_luminances[0] +
buf[x + 1] * t->hlg_ootf_luminances[1] +
buf[x + 2] * t->hlg_ootf_luminances[2]; constfloat ratio = std::pow(luminance, gamma - 1); if (std::isfinite(ratio)) {
buf[x] *= ratio;
buf[x + 1] *= ratio;
buf[x + 2] *= ratio; if (forward && gamma < 1) { // If gamma < 1, the ratio above will be > 1 which can push bright // saturated highlights out of gamut. There are several possible // ways to bring them back in-gamut; this one preserves hue and // saturation at the slight expense of luminance. If !forward, the // previously-applied forward OOTF with gamma > 1 already pushed // those highlights down and we are simply putting them back where // they were so this is not necessary. constfloat maximum =
std::max(buf[x], std::max(buf[x + 1], buf[x + 2])); if (maximum > 1) { constfloat normalizer = 1.f / maximum;
buf[x] *= normalizer;
buf[x + 1] *= normalizer;
buf[x + 2] *= normalizer;
}
}
}
} break;
default: return JXL_FAILURE("HLG OOTF not implemented for %" PRIuS " channels",
t->hlg_ootf_num_channels);
} returntrue;
}
bool IsKnownColorPrimaries(uint8_t color_primaries) { using P = jxl::cms::Primaries; // All but kCustom if (color_primaries == kColorPrimariesP3_D65) returntrue; constauto p = static_cast<Primaries>(color_primaries); return p == P::kSRGB || p == P::k2100 || p == P::kP3;
}
bool ApplyCICP(const uint8_t color_primaries, const uint8_t transfer_characteristics, const uint8_t matrix_coefficients, const uint8_t full_range,
ColorEncoding* JXL_RESTRICT c) { if (matrix_coefficients != 0) returnfalse; if (full_range != 1) returnfalse;
// In case parsing fails, mark the ColorEncoding as invalid.
c->color_space = JXL_COLOR_SPACE_UNKNOWN;
c->transfer_function = JXL_TRANSFER_FUNCTION_UNKNOWN;
if (icc_size == 0) return JXL_FAILURE("Empty ICC profile");
ColorEncoding c_enc;
#if JPEGXL_ENABLE_SKCMS if (icc_size < 128) { return JXL_FAILURE("ICC file too small");
}
// skcms does not return the rendering intent, so get it from the file. It // should be encoded as big-endian 32-bit integer in bytes 60..63.
uint32_t big_endian_rendering_intent = icc_data[67] + (icc_data[66] << 8) +
(icc_data[65] << 16) +
(icc_data[64] << 24); // Some files encode rendering intent as little endian, which is not spec // compliant. However we accept those with a warning.
uint32_t little_endian_rendering_intent = (icc_data[67] << 24) +
(icc_data[66] << 16) +
(icc_data[65] << 8) + icc_data[64];
uint32_t candidate_rendering_intent =
std::min(big_endian_rendering_intent, little_endian_rendering_intent); if (candidate_rendering_intent != big_endian_rendering_intent) {
JXL_WARNING( "Invalid rendering intent bytes: [0x%02X 0x%02X 0x%02X 0x%02X], " "assuming %u was meant",
icc_data[64], icc_data[65], icc_data[66], icc_data[67],
candidate_rendering_intent);
} if (candidate_rendering_intent > 3) { return JXL_FAILURE("Invalid rendering intent %u\n",
candidate_rendering_intent);
} // ICC and RenderingIntent have the same values (0..3).
c_enc.rendering_intent = static_cast<RenderingIntent>(candidate_rendering_intent);
// Relies on color_space.
JXL_RETURN_IF_ERROR(IdentifyPrimaries(profile, wp_unadapted, &c_enc));
// Relies on color_space/white point/primaries being set already.
JXL_RETURN_IF_ERROR(DetectTransferFunction(profile, &c_enc)); #else// JPEGXL_ENABLE_SKCMS
t->apply_hlg_ootf = c_src.tf.IsHLG() != c_dst.tf.IsHLG(); if (t->apply_hlg_ootf) { const ColorEncoding* c_hlg = c_src.tf.IsHLG() ? &c_src : &c_dst;
t->hlg_ootf_num_channels = c_hlg->Channels(); if (t->hlg_ootf_num_channels == 3 &&
!GetPrimariesLuminances(*c_hlg, t->hlg_ootf_luminances.data())) {
JXL_NOTIFY_ERROR( "JxlCmsInit: failed to compute the luminances of primaries"); return nullptr;
}
}
// Special-case SRGB <=> linear if the primaries / white point are the same, // or any conversion where PQ or HLG is involved: bool src_linear = c_src.tf.IsLinear(); constbool dst_linear = c_dst.tf.IsLinear();
if (c_src.tf.IsPQ() || c_src.tf.IsHLG() ||
(c_src.tf.IsSRGB() && dst_linear && c_src.SameColorSpace(c_dst))) { // Construct new profile as if the data were already/still linear.
ColorEncoding c_linear_src = c_src;
c_linear_src.tf.SetTransferFunction(TransferFunction::kLinear); #if JPEGXL_ENABLE_SKCMS
skcms_ICCProfile new_src; #else// JPEGXL_ENABLE_SKCMS
Profile new_src; #endif// JPEGXL_ENABLE_SKCMS // Only enable ExtraTF if profile creation succeeded. if (MaybeCreateProfile(c_linear_src.ToExternal(), &icc_src) && #if JPEGXL_ENABLE_SKCMS
DecodeProfile(icc_src.data(), icc_src.size(), &new_src)) { #else// JPEGXL_ENABLE_SKCMS
DecodeProfile(context, Bytes(icc_src), &new_src)) { #endif// JPEGXL_ENABLE_SKCMS #if JXL_CMS_VERBOSE
printf("Special HLG/PQ/sRGB -> linear\n"); #endif #if JPEGXL_ENABLE_SKCMS
t->icc_src = std::move(icc_src);
t->profile_src = new_src; #else// JPEGXL_ENABLE_SKCMS
profile_src.swap(new_src); #endif// JPEGXL_ENABLE_SKCMS
t->preprocess = c_src.tf.IsSRGB()
? ExtraTF::kSRGB
: (c_src.tf.IsPQ() ? ExtraTF::kPQ : ExtraTF::kHLG);
c_src = c_linear_src;
src_linear = true;
} else { if (t->apply_hlg_ootf) {
JXL_NOTIFY_ERROR( "Failed to create extra linear source profile, and HLG OOTF " "required"); return nullptr;
}
JXL_WARNING("Failed to create extra linear destination profile");
}
}
if (c_dst.tf.IsPQ() || c_dst.tf.IsHLG() ||
(c_dst.tf.IsSRGB() && src_linear && c_src.SameColorSpace(c_dst))) {
ColorEncoding c_linear_dst = c_dst;
c_linear_dst.tf.SetTransferFunction(TransferFunction::kLinear); #if JPEGXL_ENABLE_SKCMS
skcms_ICCProfile new_dst; #else// JPEGXL_ENABLE_SKCMS
Profile new_dst; #endif// JPEGXL_ENABLE_SKCMS // Only enable ExtraTF if profile creation succeeded. if (MaybeCreateProfile(c_linear_dst.ToExternal(), &icc_dst) && #if JPEGXL_ENABLE_SKCMS
DecodeProfile(icc_dst.data(), icc_dst.size(), &new_dst)) { #else// JPEGXL_ENABLE_SKCMS
DecodeProfile(context, Bytes(icc_dst), &new_dst)) { #endif// JPEGXL_ENABLE_SKCMS #if JXL_CMS_VERBOSE
printf("Special linear -> HLG/PQ/sRGB\n"); #endif #if JPEGXL_ENABLE_SKCMS
t->icc_dst = std::move(icc_dst);
t->profile_dst = new_dst; #else// JPEGXL_ENABLE_SKCMS
profile_dst.swap(new_dst); #endif// JPEGXL_ENABLE_SKCMS
t->postprocess = c_dst.tf.IsSRGB()
? ExtraTF::kSRGB
: (c_dst.tf.IsPQ() ? ExtraTF::kPQ : ExtraTF::kHLG);
c_dst = c_linear_dst;
} else { if (t->apply_hlg_ootf) {
JXL_NOTIFY_ERROR( "Failed to create extra linear destination profile, and inverse " "HLG OOTF required"); return nullptr;
}
JXL_WARNING("Failed to create extra linear destination profile");
}
}
if (c_src.SameColorEncoding(c_dst)) { #if JXL_CMS_VERBOSE
printf("Same intermediary linear profiles, skipping CMS\n"); #endif
t->skip_lcms = true;
}
#if JPEGXL_ENABLE_SKCMS if (!skcms_MakeUsableAsDestination(&t->profile_dst)) {
JXL_NOTIFY_ERROR( "Failed to make %s usable as a color transform destination",
ColorEncodingDescription(c_dst.ToExternal()).c_str()); return nullptr;
} #endif// JPEGXL_ENABLE_SKCMS
// Not including alpha channel (copied separately). const size_t channels_src = (c_src.cmyk ? 4 : c_src.Channels()); const size_t channels_dst = c_dst.Channels(); #if JXL_CMS_VERBOSE
printf("Channels: %" PRIuS "; Threads: %" PRIuS "\n", channels_src,
num_threads); #endif
#if !JPEGXL_ENABLE_SKCMS // Type includes color space (XYZ vs RGB), so can be different. const uint32_t type_src = Type32(c_src, channels_src == 4); const uint32_t type_dst = Type32(c_dst, false); const uint32_t intent = static_cast<uint32_t>(c_dst.rendering_intent); // Use cmsFLAGS_NOCACHE to disable the 1-pixel cache and make calling // cmsDoTransform() thread-safe. const uint32_t flags = cmsFLAGS_NOCACHE | cmsFLAGS_BLACKPOINTCOMPENSATION |
cmsFLAGS_HIGHRESPRECALC;
t->lcms_transform =
cmsCreateTransformTHR(context, profile_src.get(), type_src,
profile_dst.get(), type_dst, intent, flags); if (t->lcms_transform == nullptr) {
JXL_NOTIFY_ERROR("Failed to create transform"); return nullptr;
} #endif// !JPEGXL_ENABLE_SKCMS
// Ideally LCMS would convert directly from External to Image3. However, // cmsDoTransformLineStride only accepts 32-bit BytesPerPlaneIn, whereas our // planes can be more than 4 GiB apart. Hence, transform inputs/outputs must // be interleaved. Calling cmsDoTransform for each pixel is expensive // (indirect call). We therefore transform rows, which requires per-thread // buffers. To avoid separate allocations, we use the rows of an image. // Because LCMS apparently also cannot handle <= 16 bit inputs and 32-bit // outputs (or vice versa), we use floating point input/output.
t->channels_src = channels_src;
t->channels_dst = channels_dst; #if !JPEGXL_ENABLE_SKCMS
size_t actual_channels_src = channels_src;
size_t actual_channels_dst = channels_dst; #else // SkiaCMS doesn't support grayscale float buffers, so we create space for RGB // float buffers anyway.
size_t actual_channels_src = (channels_src == 4 ? 4 : 3);
size_t actual_channels_dst = 3; #endif
AllocateBuffer(xsize * actual_channels_src, num_threads, &t->src_storage,
&t->buf_src);
AllocateBuffer(xsize * actual_channels_dst, num_threads, &t->dst_storage,
&t->buf_dst);
t->intensity_target = intensity_target; return t.release();
}
Die Informationen auf dieser Webseite wurden
nach bestem Wissen sorgfältig zusammengestellt. Es wird jedoch weder Vollständigkeit, noch Richtigkeit,
noch Qualität der bereit gestellten Informationen zugesichert.
Bemerkung:
Die farbliche Syntaxdarstellung und die Messung sind noch experimentell.