mirror of
https://github.com/LadybirdBrowser/ladybird.git
synced 2024-12-01 20:10:28 +00:00
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:
parent
2319b2ffb5
commit
4169c94ebe
Notes:
sideshowbarker
2024-07-17 07:06:47 +09:00
Author: https://github.com/nico Commit: https://github.com/SerenityOS/serenity/commit/4169c94ebe Pull-request: https://github.com/SerenityOS/serenity/pull/18596 Reviewed-by: https://github.com/AtkinsSJ Reviewed-by: https://github.com/gmta
3 changed files with 167 additions and 0 deletions
|
@ -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());
|
||||
|
|
|
@ -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));
|
||||
|
|
|
@ -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.
|
||||
|
|
Loading…
Reference in a new issue