From 9bd35fda56b32f7aa022b94c3d228942ad3dff9b Mon Sep 17 00:00:00 2001 From: Nico Weber Date: Mon, 1 May 2023 11:14:10 -0400 Subject: [PATCH] ICC: Implement TRC inversion in from_pcs for parametric curves --- Tests/LibGfx/TestICCProfile.cpp | 15 ++++++++- Userland/Libraries/LibGfx/ICC/Profile.cpp | 25 +++++++++++---- Userland/Libraries/LibGfx/ICC/TagTypes.h | 39 +++++++++++++++++++++++ 3 files changed, 71 insertions(+), 8 deletions(-) diff --git a/Tests/LibGfx/TestICCProfile.cpp b/Tests/LibGfx/TestICCProfile.cpp index d5d66e80b5..7707f0af46 100644 --- a/Tests/LibGfx/TestICCProfile.cpp +++ b/Tests/LibGfx/TestICCProfile.cpp @@ -158,6 +158,10 @@ TEST_CASE(from_pcs) { auto sRGB = MUST(Gfx::ICC::sRGB()); + auto sRGB_curve_pointer = MUST(Gfx::ICC::sRGB_curve()); + VERIFY(sRGB_curve_pointer->type() == Gfx::ICC::ParametricCurveTagData::Type); + auto const& sRGB_curve = static_cast(*sRGB_curve_pointer); + auto sRGB_from_xyz = [&sRGB](FloatVector3 const& XYZ) { u8 rgb[3]; MUST(sRGB->from_pcs(XYZ, rgb)); @@ -185,7 +189,16 @@ TEST_CASE(from_pcs) 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 the inverse curve transform. + float f64 = sRGB_curve.evaluate(64 / 255.f); + EXPECT_EQ(sRGB_from_xyz((r_xyz + g_xyz + b_xyz) * f64), Color(64, 64, 64)); + + float f128 = sRGB_curve.evaluate(128 / 255.f); + EXPECT_EQ(sRGB_from_xyz((r_xyz + g_xyz + b_xyz) * f128), Color(128, 128, 128)); + + // Test for curve and matrix combined. + float f192 = sRGB_curve.evaluate(192 / 255.f); + EXPECT_EQ(sRGB_from_xyz(r_xyz * f64 + g_xyz * f128 + b_xyz * f192), Color(64, 128, 192)); } TEST_CASE(to_lab) diff --git a/Userland/Libraries/LibGfx/ICC/Profile.cpp b/Userland/Libraries/LibGfx/ICC/Profile.cpp index 0db10a9c8d..d7708e61ec 100644 --- a/Userland/Libraries/LibGfx/ICC/Profile.cpp +++ b/Userland/Libraries/LibGfx/ICC/Profile.cpp @@ -1551,6 +1551,7 @@ ErrorOr Profile::from_pcs(FloatVector3 const& pcs, Bytes color) const // greenTRC^-1, and blueTRC^-1 function is undefined. If a one-dimensional curve is constant, the curve cannot be // inverted." + // Convert from XYZ to linear rgb. // 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(); @@ -1561,13 +1562,22 @@ ErrorOr Profile::from_pcs(FloatVector3 const& pcs, Bytes color) const 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; + auto evaluate_curve_inverse = [this](TagSignature curve_tag, float f) { + auto const& trc = *m_tag_table.get(curve_tag).value(); + VERIFY(trc.type() == CurveTagData::Type || trc.type() == ParametricCurveTagData::Type); + if (trc.type() == CurveTagData::Type) { + TODO(); + return 0.f; + } + return static_cast(trc).evaluate_inverse(f); + }; + + // Convert from linear rgb to device rgb. // 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 @@ -1576,12 +1586,13 @@ ErrorOr Profile::from_pcs(FloatVector3 const& pcs, Bytes color) const // (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); + float device_r = evaluate_curve_inverse(redTRCTag, linear_rgb[0]); + float device_g = evaluate_curve_inverse(greenTRCTag, linear_rgb[1]); + float device_b = evaluate_curve_inverse(blueTRCTag, linear_rgb[2]); - // 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]); + color[0] = round(255 * device_r); + color[1] = round(255 * device_g); + color[2] = round(255 * device_b); return {}; } diff --git a/Userland/Libraries/LibGfx/ICC/TagTypes.h b/Userland/Libraries/LibGfx/ICC/TagTypes.h index 5c320690e7..8c57f0075a 100644 --- a/Userland/Libraries/LibGfx/ICC/TagTypes.h +++ b/Userland/Libraries/LibGfx/ICC/TagTypes.h @@ -739,6 +739,45 @@ public: VERIFY_NOT_REACHED(); } + // y must be in [0..1]. + float evaluate_inverse(float y) const + { + VERIFY(0.f <= y && y <= 1.f); + + // See "Recommendations" section in https://www.color.org/whitepapers/ICC_White_Paper35-Use_of_the_parametricCurveType.pdf + // Requirements for the curve to be non-decreasing: + // * γ > 0 + // * a > 0 for types 1-4 + // * c ≥ 0 for types 3 and 4 + // + // Types 3 and 4 additionally require: + // To prevent negative discontinuities: + // * cd ≤ (ad + b) for type 3 + // * cd + f ≤ (ad + b)^γ + e for type 4 + // To prevent complex numbers: + // * ad + b ≥ 0 + // FIXME: Check these requirements somewhere. + + switch (function_type()) { + case FunctionType::Type0: + return powf(y, 1.f / (float)g()); + case FunctionType::Type1: + return (powf(y, 1.f / (float)g()) - (float)b()) / (float)a(); + case FunctionType::Type2: + // Only defined for Y >= c, so I suppose this requires c <= 0 in practice (?). + return (powf(y - (float)c(), 1.f / (float)g()) - (float)b()) / (float)a(); + case FunctionType::Type3: + if (y >= (float)c() * (float)d()) + return (powf(y, 1.f / (float)g()) - (float)b()) / (float)a(); + return y / (float)c(); + case FunctionType::Type4: + if (y >= (float)c() * (float)d()) + return (powf(y - (float)e(), 1.f / (float)g()) - (float)b()) / (float)a(); + return (y - (float)f()) / (float)c(); + } + VERIFY_NOT_REACHED(); + } + private: FunctionType m_function_type;