From 4169c94ebe7a4cd3461f1288ea7f99c7a4eb570f Mon Sep 17 00:00:00 2001 From: Nico Weber Date: Mon, 1 May 2023 08:24:13 -0400 Subject: [PATCH] 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. --- Tests/LibGfx/TestICCProfile.cpp | 34 ++++++ Userland/Libraries/LibGfx/ICC/Profile.cpp | 129 ++++++++++++++++++++++ Userland/Libraries/LibGfx/ICC/Profile.h | 4 + 3 files changed, 167 insertions(+) diff --git a/Tests/LibGfx/TestICCProfile.cpp b/Tests/LibGfx/TestICCProfile.cpp index 5d70d04332..d5d66e80b5 100644 --- a/Tests/LibGfx/TestICCProfile.cpp +++ b/Tests/LibGfx/TestICCProfile.cpp @@ -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()); diff --git a/Userland/Libraries/LibGfx/ICC/Profile.cpp b/Userland/Libraries/LibGfx/ICC/Profile.cpp index 3099000d38..0db10a9c8d 100644 --- a/Userland/Libraries/LibGfx/ICC/Profile.cpp +++ b/Userland/Libraries/LibGfx/ICC/Profile.cpp @@ -9,6 +9,7 @@ #include #include #include +#include #include #include @@ -1471,6 +1472,134 @@ ErrorOr 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 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 = [&](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 Profile::to_lab(ReadonlyBytes color) const { auto pcs = TRY(to_pcs(color)); diff --git a/Userland/Libraries/LibGfx/ICC/Profile.h b/Userland/Libraries/LibGfx/ICC/Profile.h index 9566d7f342..de2ee1451a 100644 --- a/Userland/Libraries/LibGfx/ICC/Profile.h +++ b/Userland/Libraries/LibGfx/ICC/Profile.h @@ -267,6 +267,10 @@ public: // Call connection_space() to find out the space the result is in. ErrorOr 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 from_pcs(FloatVector3 const&, Bytes) const; + ErrorOr to_lab(ReadonlyBytes) const; // Only call these if you know that this is an RGB matrix-based profile.