1
Fork 0
mirror of https://github.com/RGBCube/serenity synced 2025-07-27 06:37:44 +00:00

LibGfx/ICC: Make mAB tags work for profiles that use PCSLAB

ICC profiles work by transforming from the input color space
(one of many: RGB, CMYK, YUV, etc) to a "profile connection space" (PCS)
and then from there to the output color space.

However, there's not one but two possible profile connection spaces,
PCSXYZ and PCSLAB. The matrix/curve tags can only be used with PCSXYZ,
but the mAB, mBA, mft1, mft2 tags can be used with PCSLAB as well.

The PCSLAB encoding has L going from 0 to 100 and ab from -128 to 127,
instead of from 0 to 1. So they need to be scaled up at the end.

That's also the reason for the "mystery conversion factor": PCSXYZ
doesn't go from 0 to 1 either, but from 0 to 65535/32768, per ICC v4
6.3.4.2 General PCS encoding, Table 11 - PCSXYZ X, Y or Z encoding.

Between input and output are various curves (and the CLUT) that
have domain and range of 0..1. For these, the color has to be linearly
scaled to 0..1 before the curve and back to the actual range after
the curve. Doing that back-to-back is a no-op, so scaling back at
the very end is sufficient.
This commit is contained in:
Nico Weber 2023-12-03 21:03:56 -05:00 committed by Sam Atkins
parent 64ffae9c55
commit fda9840f48
2 changed files with 29 additions and 17 deletions

View file

@ -1227,7 +1227,7 @@ ErrorOr<FloatVector3> Profile::to_pcs_a_to_b(TagData const& tag_data, ReadonlyBy
if (a_to_b.number_of_output_channels() != number_of_components_in_color_space(connection_space()))
return Error::from_string_literal("ICC::Profile::to_pcs_a_to_b: mAB output channel count does not match profile connection space size");
return a_to_b.evaluate(color);
return a_to_b.evaluate(connection_space(), color);
}
}
VERIFY_NOT_REACHED();

View file

@ -448,7 +448,7 @@ public:
Vector<LutCurveType> const& b_curves() const { return m_b_curves; }
// Returns the result of the LUT pipeline for u8 inputs.
ErrorOr<FloatVector3> evaluate(ReadonlyBytes) const;
ErrorOr<FloatVector3> evaluate(ColorSpace connection_space, ReadonlyBytes) const;
private:
u8 m_number_of_input_channels;
@ -1023,8 +1023,9 @@ private:
Vector<XYZ, 1> m_xyzs;
};
inline ErrorOr<FloatVector3> LutAToBTagData::evaluate(ReadonlyBytes color_u8) const
inline ErrorOr<FloatVector3> LutAToBTagData::evaluate(ColorSpace connection_space, ReadonlyBytes color_u8) const
{
VERIFY(connection_space == ColorSpace::PCSXYZ || connection_space == ColorSpace::PCSLAB);
VERIFY(number_of_input_channels() == color_u8.size());
VERIFY(number_of_output_channels() == 3);
@ -1032,6 +1033,15 @@ inline ErrorOr<FloatVector3> LutAToBTagData::evaluate(ReadonlyBytes color_u8) co
// "Data are processed using these elements via the following sequence:
// (“A” curves) ⇨ (multi-dimensional lookup table, CLUT) ⇨ (“M” curves) ⇨ (matrix) ⇨ (“B” curves).
// "The domain and range of the A and B curves and CLUT are defined to consist of all real numbers between 0,0 and 1,0 inclusive.
// The first entry is located at 0,0, the last entry at 1,0, and intermediate entries are uniformly spaced using an increment of 1,0/(m-1).
// For the A and B curves, m is the number of entries in the table. For the CLUT, m is the number of grid points along each dimension.
// Since the domain and range of the tables are 0,0 to 1,0 it is necessary to convert all device values and PCSLAB values to this numeric range.
// It shall be assumed that the maximum value in each case is set to 1,0 and the minimum value to 0,0 and all intermediate values are
// linearly scaled accordingly."
// Scaling from the full range to 0..1 before a curve and then back after the curve only to scale to 0..1 again before the next curve is a no-op,
// so we only scale back to the full range at the very end of this function.
auto evaluate_curve = [](LutCurveType const& curve, float f) {
VERIFY(curve->type() == CurveTagData::Type || curve->type() == ParametricCurveTagData::Type);
if (curve->type() == CurveTagData::Type)
@ -1087,27 +1097,29 @@ inline ErrorOr<FloatVector3> LutAToBTagData::evaluate(ReadonlyBytes color_u8) co
(float)e[3] * color[0] + (float)e[4] * color[1] + (float)e[5] * color[2] + (float)e[10],
(float)e[6] * color[0] + (float)e[7] * color[1] + (float)e[8] * color[2] + (float)e[11]
};
// Mystery conversion factor!
// skcms, littlecms, and argyll all do this somewhere, but I don't understand why!
// skcms has a "TODO: understand" comment as well.
// littlecms and argyll have both comments which don't make sense to me.
// littlecms does this to the matrix profile matrices (i.e. it considers the lut pcs scale canonical).
// skcms does it to the mAB matrix (...which means it'll do something different if the matrix is missing,
// and it'll also do something different if the b curve isn't the identity).
// argyll does it in Lut_Lut2XYZ(), but I'm not clear on when that's called.
// SampleICC does it in IccCmm.cpp, XYZScale() and in IccUtil.cpp, icXyzFromPcs().
// Without this, colors are too bright. So let's do it too, and maybe I'll understand it one day.
new_color *= 65535 / 32768.f; // ???
color = new_color.clamped(0.f, 1.f);
}
return FloatVector3 {
FloatVector3 output_color {
evaluate_curve(m_b_curves[0], color[0]),
evaluate_curve(m_b_curves[1], color[1]),
evaluate_curve(m_b_curves[2], color[2])
};
// ICC v4, 6.3.4.2 General PCS encoding
if (connection_space == ColorSpace::PCSXYZ) {
// Table 11 - PCSXYZ X, Y or Z encoding
output_color *= 65535 / 32768.0f;
} else {
VERIFY(connection_space == ColorSpace::PCSLAB);
// Table 12 — PCSLAB L* encoding
output_color[0] *= 100.0f;
// Table 13 — PCSLAB a* or PCSLAB b* encoding
output_color[1] = output_color[1] * 255.0f - 128.0f;
output_color[2] = output_color[2] * 255.0f - 128.0f;
}
return output_color;
}
}