LibGfx/ICC: Implement forward transform for mft1 and mft2 tags

mft1 and mft2 tags are very similar. The only difference is that
mft1 uses an u8 lookup table, while mft2 uses a u16 lookup table.
This means their PCS lookup encodings are different, and mft2 uses a
PCSLAB encoding that's different from other places in the v4 spec.
This commit is contained in:
Nico Weber 2023-12-04 08:32:21 -05:00 committed by Andreas Kling
parent b138bc0004
commit 72f5461af4
2 changed files with 179 additions and 6 deletions

View file

@ -1213,12 +1213,14 @@ ErrorOr<FloatVector3> Profile::to_pcs_a_to_b(TagData const& tag_data, ReadonlyBy
VERIFY(number_of_components_in_color_space(connection_space()) == 3);
switch (tag_data.type()) {
case Lut16TagData::Type:
// FIXME
return Error::from_string_literal("ICC::Profile::to_pcs: AToB*Tag handling for mft2 tags not yet implemented");
case Lut8TagData::Type:
// FIXME
return Error::from_string_literal("ICC::Profile::to_pcs: AToB*Tag handling for mft1 tags not yet implemented");
case Lut16TagData::Type: {
auto const& a_to_b = static_cast<Lut16TagData const&>(tag_data);
return a_to_b.evaluate(data_color_space(), connection_space(), color);
}
case Lut8TagData::Type: {
auto const& a_to_b = static_cast<Lut8TagData const&>(tag_data);
return a_to_b.evaluate(data_color_space(), connection_space(), color);
}
case LutAToBTagData::Type: {
auto const& a_to_b = static_cast<LutAToBTagData const&>(tag_data);
if (a_to_b.number_of_input_channels() != number_of_components_in_color_space(data_color_space()))

View file

@ -319,6 +319,8 @@ public:
Vector<u16> const& clut_values() const { return m_clut_values; }
Vector<u16> const& output_tables() const { return m_output_tables; }
ErrorOr<FloatVector3> evaluate(ColorSpace input_space, ColorSpace connection_space, ReadonlyBytes) const;
private:
EMatrix3x3 m_e;
@ -376,6 +378,8 @@ public:
Vector<u8> const& clut_values() const { return m_clut_values; }
Vector<u8> const& output_tables() const { return m_output_tables; }
ErrorOr<FloatVector3> evaluate(ColorSpace input_space, ColorSpace connection_space, ReadonlyBytes) const;
private:
EMatrix3x3 m_e;
@ -1030,6 +1034,173 @@ private:
Vector<XYZ, 1> m_xyzs;
};
inline ErrorOr<FloatVector3> Lut16TagData::evaluate(ColorSpace input_space, ColorSpace connection_space, ReadonlyBytes color_u8) const
{
// See comment at start of LutAToBTagData::evaluate() for the clipping flow.
VERIFY(connection_space == ColorSpace::PCSXYZ || connection_space == ColorSpace::PCSLAB);
VERIFY(number_of_input_channels() == color_u8.size());
// FIXME: This will be wrong once Profile::from_pcs_b_to_a() calls this function too.
VERIFY(number_of_output_channels() == 3);
// ICC v4, 10.11 lut8Type
// "Data is processed using these elements via the following sequence:
// (matrix) ⇨ (1d input tables) ⇨ (multi-dimensional lookup table, CLUT) ⇨ (1d output tables)"
Vector<float, 4> color;
for (u8 c : color_u8)
color.append(c / 255.0f);
// "3 x 3 matrix (which shall be the identity matrix unless the input colour space is PCSXYZ)"
// In practice, it's usually RGB or CMYK.
if (input_space == ColorSpace::PCSXYZ) {
EMatrix3x3 const& e = m_e;
color = Vector<float, 4> {
(float)e[0] * color[0] + (float)e[1] * color[1] + (float)e[2] * color[2],
(float)e[3] * color[0] + (float)e[4] * color[1] + (float)e[5] * color[2],
(float)e[6] * color[0] + (float)e[7] * color[1] + (float)e[8] * color[2],
};
}
// "The input tables are arrays of 16-bit unsigned values. Each input table consists of a minimum of two and a maximum of 4096 uInt16Number integers.
// Each input table entry is appropriately normalized to the range 0 to 65535.
// The inputTable is of size (InputChannels x inputTableEntries x 2) bytes.
// When stored in this tag, the one-dimensional lookup tables are packed one after another"
for (size_t c = 0; c < color.size(); ++c)
color[c] = lerp_1d(m_input_tables.span().slice(c * m_number_of_input_table_entries, m_number_of_input_table_entries), color[c]) / 65535.0f;
// "The CLUT is organized as an i-dimensional array with a given number of grid points in each dimension,
// where i is the number of input channels (input tables) in the transform.
// The dimension corresponding to the first input channel varies least rapidly and
// the dimension corresponding to the last input channel varies most rapidly.
// Each grid point value is an o-byte array, where o is the number of output channels.
// The first sequential byte of the entry contains the function value for the first output function,
// the second sequential byte of the entry contains the function value for the second output function,
// and so on until all the output functions have been supplied."
auto sample = [this](Vector<unsigned> const& coordinates) {
size_t stride = 3;
size_t offset = 0;
for (int i = coordinates.size() - 1; i >= 0; --i) {
offset += coordinates[i] * stride;
stride *= m_number_of_clut_grid_points;
}
return FloatVector3 { (float)m_clut_values[offset], (float)m_clut_values[offset + 1], (float)m_clut_values[offset + 2] };
};
auto size = [this](size_t) { return m_number_of_clut_grid_points; };
FloatVector3 output_color = lerp_nd(move(size), move(sample), color) / 65535.0f;
// "The output tables are arrays of 16-bit unsigned values. Each output table consists of a minimum of two and a maximum of 4096 uInt16Number integers.
// Each output table entry is appropriately normalized to the range 0 to 65535.
// The outputTable is of size (OutputChannels x outputTableEntries x 2) bytes.
// When stored in this tag, the one-dimensional lookup tables are packed one after another"
for (u8 c = 0; c < output_color.length(); ++c)
output_color[c] = lerp_1d(m_output_tables.span().slice(c * m_number_of_output_table_entries, m_number_of_output_table_entries), output_color[c]) / 65535.0f;
if (connection_space == ColorSpace::PCSXYZ) {
// Table 11 - PCSXYZ X, Y or Z encoding
output_color *= 65535 / 32768.0f;
} else {
VERIFY(connection_space == ColorSpace::PCSLAB);
// ICC v4, 10.10 lut16Type
// Note: lut16Type does _not_ use the encoding in 6.3.4.2 General PCS encoding!
// "To convert colour values from this tag's legacy 16-bit PCSLAB encoding to the 16-bit PCSLAB encoding defined in 6.3.4.2 (Tables 12 and 13),
// multiply all values with 65 535/65 280 (i.e. FFFFh/FF00h).
// Any colour values that are in the value range of legacy 16-bit PCSLAB encoding, but not in the more recent 16-bit PCSLAB encoding,
// shall be clipped on a per-component basis."
output_color *= 65535.0f / 65280.0f;
// Table 42 — Legacy PCSLAB L* encoding
output_color[0] = clamp(output_color[0] * 100.0f, 0.0f, 100.0f);
// Table 43 — Legacy PCSLAB a* or PCSLAB b* encoding
output_color[1] = clamp(output_color[1] * 255.0f - 128.0f, -128.0f, 127.0f);
output_color[2] = clamp(output_color[2] * 255.0f - 128.0f, -128.0f, 127.0f);
}
return output_color;
}
inline ErrorOr<FloatVector3> Lut8TagData::evaluate(ColorSpace input_space, ColorSpace connection_space, ReadonlyBytes color_u8) const
{
// See comment at start of LutAToBTagData::evaluate() for the clipping flow.
VERIFY(connection_space == ColorSpace::PCSXYZ || connection_space == ColorSpace::PCSLAB);
VERIFY(number_of_input_channels() == color_u8.size());
// FIXME: This will be wrong once Profile::from_pcs_b_to_a() calls this function too.
VERIFY(number_of_output_channels() == 3);
// ICC v4, 10.11 lut8Type
// "Data is processed using these elements via the following sequence:
// (matrix) ⇨ (1d input tables) ⇨ (multi-dimensional lookup table, CLUT) ⇨ (1d output tables)"
Vector<float, 4> color;
for (u8 c : color_u8)
color.append(c / 255.0f);
// "3 x 3 matrix (which shall be the identity matrix unless the input colour space is PCSXYZ)"
// In practice, it's usually RGB or CMYK.
if (input_space == ColorSpace::PCSXYZ) {
EMatrix3x3 const& e = m_e;
color = Vector<float, 4> {
(float)e[0] * color[0] + (float)e[1] * color[1] + (float)e[2] * color[2],
(float)e[3] * color[0] + (float)e[4] * color[1] + (float)e[5] * color[2],
(float)e[6] * color[0] + (float)e[7] * color[1] + (float)e[8] * color[2],
};
}
// "The input tables are arrays of uInt8Number values. Each input table consists of 256 uInt8Number integers.
// Each input table entry is appropriately normalized to the range 0 to 255.
// The inputTable is of size (InputChannels x 256) bytes.
// When stored in this tag, the one-dimensional lookup tables are packed one after another"
for (size_t c = 0; c < color.size(); ++c)
color[c] = lerp_1d(m_input_tables.span().slice(c * 256, 256), color[c]) / 255.0f;
// "The CLUT is organized as an i-dimensional array with a given number of grid points in each dimension,
// where i is the number of input channels (input tables) in the transform.
// The dimension corresponding to the first input channel varies least rapidly and
// the dimension corresponding to the last input channel varies most rapidly.
// Each grid point value is an o-byte array, where o is the number of output channels.
// The first sequential byte of the entry contains the function value for the first output function,
// the second sequential byte of the entry contains the function value for the second output function,
// and so on until all the output functions have been supplied."
auto sample = [this](Vector<unsigned> const& coordinates) {
size_t stride = 3;
size_t offset = 0;
for (int i = coordinates.size() - 1; i >= 0; --i) {
offset += coordinates[i] * stride;
stride *= m_number_of_clut_grid_points;
}
return FloatVector3 { (float)m_clut_values[offset], (float)m_clut_values[offset + 1], (float)m_clut_values[offset + 2] };
};
auto size = [this](size_t) { return m_number_of_clut_grid_points; };
FloatVector3 output_color = lerp_nd(move(size), move(sample), color) / 255.0f;
// "The output tables are arrays of uInt8Number values. Each output table consists of 256 uInt8Number integers.
// Each output table entry is appropriately normalized to the range 0 to 255.
// The outputTable is of size (OutputChannels x 256) bytes.
// When stored in this tag, the one-dimensional lookup tables are packed one after another"
for (u8 c = 0; c < output_color.length(); ++c)
output_color[c] = lerp_1d(m_output_tables.span().slice(c * 256, 256), output_color[c]) / 255.0f;
if (connection_space == ColorSpace::PCSXYZ) {
// "An 8-bit PCSXYZ encoding has not been defined, so the interpretation of a lut8Type in a profile that uses PCSXYZ is implementation specific."
} else {
VERIFY(connection_space == ColorSpace::PCSLAB);
// ICC v4, 6.3.4.2 General PCS encoding
// 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;
}
inline ErrorOr<FloatVector3> LutAToBTagData::evaluate(ColorSpace connection_space, ReadonlyBytes color_u8) const
{
VERIFY(connection_space == ColorSpace::PCSXYZ || connection_space == ColorSpace::PCSLAB);