ICC: Implement some of Profile::from_pcs()

This implements conversion from profile connection space to the
device-dependent color for matrix-based profiles.

It only does the inverse color transform but does not yet do the
inverse tone reproduction curve transform -- i.e. it doesn't
implement many cases (LUT transforms), and it does the one thing
it does implement incorrectly. But to vindicate the commit a bit,
it also does the incorrect thing very inefficiently.
This commit is contained in:
Nico Weber 2023-05-01 08:24:13 -04:00 committed by Sam Atkins
parent 2319b2ffb5
commit 4169c94ebe
Notes: sideshowbarker 2024-07-17 07:06:47 +09:00
3 changed files with 167 additions and 0 deletions

View file

@ -154,6 +154,40 @@ TEST_CASE(to_pcs)
EXPECT_APPROXIMATE_VECTOR3(xyz_from_sRGB(64, 128, 192), r_xyz * f64 + g_xyz * f128 + b_xyz * f192);
}
TEST_CASE(from_pcs)
{
auto sRGB = MUST(Gfx::ICC::sRGB());
auto sRGB_from_xyz = [&sRGB](FloatVector3 const& XYZ) {
u8 rgb[3];
MUST(sRGB->from_pcs(XYZ, rgb));
return Color(rgb[0], rgb[1], rgb[2]);
};
auto vec3_from_xyz = [](Gfx::ICC::XYZ const& xyz) {
return FloatVector3 { xyz.X, xyz.Y, xyz.Z };
};
// At 0 and 255, the gamma curve is (exactly) 0 and 1, so these just test the matrix part.
EXPECT_EQ(sRGB_from_xyz(FloatVector3 { 0, 0, 0 }), Color(0, 0, 0));
auto r_xyz = vec3_from_xyz(sRGB->red_matrix_column());
EXPECT_EQ(sRGB_from_xyz(r_xyz), Color(255, 0, 0));
auto g_xyz = vec3_from_xyz(sRGB->green_matrix_column());
EXPECT_EQ(sRGB_from_xyz(g_xyz), Color(0, 255, 0));
auto b_xyz = vec3_from_xyz(sRGB->blue_matrix_column());
EXPECT_EQ(sRGB_from_xyz(b_xyz), Color(0, 0, 255));
EXPECT_EQ(sRGB_from_xyz(r_xyz + g_xyz), Color(255, 255, 0));
EXPECT_EQ(sRGB_from_xyz(r_xyz + b_xyz), Color(255, 0, 255));
EXPECT_EQ(sRGB_from_xyz(g_xyz + b_xyz), Color(0, 255, 255));
EXPECT_EQ(sRGB_from_xyz(r_xyz + g_xyz + b_xyz), Color(255, 255, 255));
// FIXME: Implement and test the inverse curve transform.
}
TEST_CASE(to_lab)
{
auto sRGB = MUST(Gfx::ICC::sRGB());

View file

@ -9,6 +9,7 @@
#include <LibGfx/ICC/BinaryFormat.h>
#include <LibGfx/ICC/Profile.h>
#include <LibGfx/ICC/Tags.h>
#include <LibGfx/Matrix3x3.h>
#include <math.h>
#include <time.h>
@ -1471,6 +1472,134 @@ ErrorOr<FloatVector3> Profile::to_pcs(ReadonlyBytes color) const
VERIFY_NOT_REACHED();
}
static TagSignature backward_transform_tag_for_rendering_intent(RenderingIntent rendering_intent)
{
// ICCv4, Table 25 — Profile type/profile tag and defined rendering intents
// This function assumes a profile class of InputDevice, DisplayDevice, OutputDevice, or ColorSpace.
switch (rendering_intent) {
case RenderingIntent::Perceptual:
return BToA0Tag;
case RenderingIntent::MediaRelativeColorimetric:
case RenderingIntent::ICCAbsoluteColorimetric:
return BToA1Tag;
case RenderingIntent::Saturation:
return BToA2Tag;
}
VERIFY_NOT_REACHED();
}
ErrorOr<void> Profile::from_pcs(FloatVector3 const& pcs, Bytes color) const
{
// See `to_pcs()` for spec links.
// This function is very similar, but uses BToAn instead of AToBn for LUT profiles,
// and an inverse transform for matrix profiles.
if (color.size() != number_of_components_in_color_space(data_color_space()))
return Error::from_string_literal("ICC::Profile: output color doesn't match color space size");
auto has_tag = [&](auto tag) { return m_tag_table.contains(tag); };
auto has_all_tags = [&]<class T>(T tags) { return all_of(tags, has_tag); };
switch (device_class()) {
case DeviceClass::InputDevice:
case DeviceClass::DisplayDevice:
case DeviceClass::OutputDevice:
case DeviceClass::ColorSpace: {
// FIXME: Implement multiProcessElementsType one day.
if (has_tag(backward_transform_tag_for_rendering_intent(rendering_intent()))) {
// FIXME
return Error::from_string_literal("ICC::Profile::from_pcs: BToA*Tag handling not yet implemented");
}
if (has_tag(BToA0Tag)) {
// FIXME
return Error::from_string_literal("ICC::Profile::from_pcs: BToA0Tag handling not yet implemented");
}
if (data_color_space() == ColorSpace::Gray) {
// FIXME
return Error::from_string_literal("ICC::Profile::from_pcs: Gray handling not yet implemented");
}
// FIXME: Per ICC v4, A.1 General, this should also handle HLS, HSV, YCbCr.
if (data_color_space() == ColorSpace::RGB) {
if (!has_all_tags(Array { redMatrixColumnTag, greenMatrixColumnTag, blueMatrixColumnTag, redTRCTag, greenTRCTag, blueTRCTag }))
return Error::from_string_literal("ICC::Profile::from_pcs: RGB color space but neither LUT-based nor matrix-based tags present");
VERIFY(color.size() == 3); // True because of color.size() check further up.
// ICC v4, F.3 Three-component matrix-based profiles
// "The inverse model is given by the following equations:
// [linear_r] = [redMatrixColumn_X greenMatrixColumn_X blueMatrixColumn_X]^-1 [ connection_X ]
// [linear_g] = [redMatrixColumn_Y greenMatrixColumn_Y blueMatrixColumn_Y] * [ connection_Y ]
// [linear_b] = [redMatrixColumn_Z greenMatrixColumn_Z blueMatrixColumn_Z] [ connection_Z ]
//
// for linear_r < 0, device_r = redTRC^-1[0] (F.8)
// for 0 ≤ linear_r ≤ 1, device_r = redTRC^-1[linear_r] (F.9)
// for linear_r > 1, device_r = redTRC^-1[1] (F.10)
//
// for linear_g < 0, device_g = greenTRC^-1[0] (F.11)
// for 0 ≤ linear_g ≤ 1, device_g = greenTRC^-1[linear_g] (F.12)
// for linear_g > 1, device_g = greenTRC^-1[1] (F.13)
//
// for linear_b < 0, device_b = blueTRC^-1[0] (F.14)
// for 0 ≤ linear_b ≤ 1, device_b = blueTRC^-1[linear_b] (F.15)
// for linear_b > 1, device_b = blueTRC^-1[1] (F.16)
//
// where redTRC^-1, greenTRC^-1, and blueTRC^-1 indicate the inverse functions of the redTRC greenTRC and
// blueTRC functions respectively.
// If the redTRC, greenTRC, or blueTRC function is not invertible the behaviour of the corresponding redTRC^-1,
// greenTRC^-1, and blueTRC^-1 function is undefined. If a one-dimensional curve is constant, the curve cannot be
// inverted."
// FIXME: Inverting matrix and curve on every call to this function is very inefficient.
auto const& red_matrix_column = this->red_matrix_column();
auto const& green_matrix_column = this->green_matrix_column();
auto const& blue_matrix_column = this->blue_matrix_column();
FloatMatrix3x3 forward_matrix {
red_matrix_column.X, green_matrix_column.X, blue_matrix_column.X,
red_matrix_column.Y, green_matrix_column.Y, blue_matrix_column.Y,
red_matrix_column.Z, green_matrix_column.Z, blue_matrix_column.Z
};
if (!forward_matrix.is_invertible())
return Error::from_string_literal("ICC::Profile::from_pcs: matrix not invertible");
auto matrix = forward_matrix.inverse();
FloatVector3 linear_rgb = matrix * pcs;
// See equations (F.8) - (F.16) above.
// FIXME: The spec says to do this, but it loses information. Color.js returns unclamped
// values instead (...but how do those make it through the TRC?) and has a separate
// clipping step. Maybe that's better?
// Also, maybe doing actual gamut mapping would look better?
// (For LUT profiles, I think the gamut mapping is baked into the BToA* data in the profile (?).
// But for matrix profiles, it'd have to be done in code.)
linear_rgb.clamp(0.f, 1.f);
// FIXME: Implement curve inversion and apply inverse curve transform here.
color[0] = round(255 * linear_rgb[0]);
color[1] = round(255 * linear_rgb[1]);
color[2] = round(255 * linear_rgb[2]);
return {};
}
return Error::from_string_literal("ICC::Profile::from_pcs: What happened?!");
}
case DeviceClass::DeviceLink:
case DeviceClass::Abstract:
// ICC v4, 8.10.3 DeviceLink or Abstract profile types
// FIXME
return Error::from_string_literal("ICC::Profile::from_pcs: conversion for DeviceLink and Abstract not implemented");
case DeviceClass::NamedColor:
return Error::from_string_literal("ICC::Profile::from_pcs: from_pcs with NamedColor profile does not make sense");
}
VERIFY_NOT_REACHED();
}
ErrorOr<CIELAB> Profile::to_lab(ReadonlyBytes color) const
{
auto pcs = TRY(to_pcs(color));

View file

@ -267,6 +267,10 @@ public:
// Call connection_space() to find out the space the result is in.
ErrorOr<FloatVector3> to_pcs(ReadonlyBytes) const;
// Converts from the profile connection space to an 8-bits-per-channel color.
// The notes on `to_pcs()` apply to this too.
ErrorOr<void> from_pcs(FloatVector3 const&, Bytes) const;
ErrorOr<CIELAB> to_lab(ReadonlyBytes) const;
// Only call these if you know that this is an RGB matrix-based profile.