浏览代码

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.
Nico Weber 2 年之前
父节点
当前提交
4169c94ebe
共有 3 个文件被更改,包括 167 次插入0 次删除
  1. 34 0
      Tests/LibGfx/TestICCProfile.cpp
  2. 129 0
      Userland/Libraries/LibGfx/ICC/Profile.cpp
  3. 4 0
      Userland/Libraries/LibGfx/ICC/Profile.h

+ 34 - 0
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());

+ 129 - 0
Userland/Libraries/LibGfx/ICC/Profile.cpp

@@ -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));

+ 4 - 0
Userland/Libraries/LibGfx/ICC/Profile.h

@@ -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.