Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 12 additions & 0 deletions Tests/LibGfx/TestICCProfile.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -300,6 +300,18 @@ TEST_CASE(roundtrip_lab_mft1)
test_roundtrip(*profile);
}

TEST_CASE(roundtrip_lab_mft2)
{
auto profile = TRY_OR_FAIL(Gfx::ICC::IdentityLAB_mft2());
test_roundtrip(*profile);
}

TEST_CASE(roundtrip_xyz_mft2)
{
auto profile = TRY_OR_FAIL(Gfx::ICC::IdentityXYZ_D50());
test_roundtrip(*profile);
}

TEST_CASE(roundtrip_sRGB_matrix_profile)
{
auto profile = TRY_OR_FAIL(Gfx::ICC::sRGB());
Expand Down
7 changes: 4 additions & 3 deletions Userland/Libraries/LibGfx/ICC/Profile.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -1491,9 +1491,10 @@ static TagSignature backward_transform_tag_for_rendering_intent(RenderingIntent
ErrorOr<void> Profile::from_pcs_b_to_a(TagData const& tag_data, FloatVector3 const& pcs, Bytes out_bytes) const
{
switch (tag_data.type()) {
case Lut16TagData::Type:
// FIXME
return Error::from_string_literal("ICC::Profile::to_pcs: BToA*Tag handling for mft2 tags not yet implemented");
case Lut16TagData::Type: {
auto const& a_to_b = static_cast<Lut16TagData const&>(tag_data);
return a_to_b.evaluate_from_pcs(connection_space(), data_color_space(), pcs, out_bytes);
}
case Lut8TagData::Type: {
auto const& a_to_b = static_cast<Lut8TagData const&>(tag_data);
return a_to_b.evaluate_from_pcs(connection_space(), data_color_space(), pcs, out_bytes);
Expand Down
97 changes: 94 additions & 3 deletions Userland/Libraries/LibGfx/ICC/TagTypes.h
Original file line number Diff line number Diff line change
Expand Up @@ -345,8 +345,9 @@ class Lut16TagData : public TagData {
Vector<u16> const& output_tables() const { return m_output_tables; }

// FIXME: If we add DeviceLink support, this can become an arbitrary nD -> nD transform.
// For now, we only have nD -> 3D.
// For now, 3D -> nD and nD -> 3D is sufficient.
ErrorOr<FloatVector3> evaluate_to_pcs(ColorSpace input_space, ColorSpace connection_space, ReadonlyBytes) const;
ErrorOr<void> evaluate_from_pcs(ColorSpace connection_space, ColorSpace output_space, FloatVector3, Bytes) const;

private:
EMatrix3x3 m_e;
Expand Down Expand Up @@ -1072,8 +1073,6 @@ inline ErrorOr<FloatVector3> Lut16TagData::evaluate_to_pcs(ColorSpace input_spac
// See comment at start of LutAToBTagData::evaluate() for the clipping flow.
VERIFY(connection_space == ColorSpace::PCSXYZ || connection_space == ColorSpace::PCSLAB);
VERIFY(number_of_input_channels() == color_u8.size());

// FIXME: This will be wrong once Profile::from_pcs_b_to_a() calls this function too.
VERIFY(number_of_output_channels() == 3);

// ICC v4, 10.10 lut16Type
Expand Down Expand Up @@ -1157,6 +1156,98 @@ inline ErrorOr<FloatVector3> Lut16TagData::evaluate_to_pcs(ColorSpace input_spac
return output_color;
}

inline ErrorOr<void> Lut16TagData::evaluate_from_pcs(ColorSpace connection_space, ColorSpace output_space, FloatVector3 pcs, Bytes color_u8) const
{
// This is very similar to Lut16TagData::evaluate_from_pcs(), but instead of converting from device space to PCS,
// it converts from PCS to device space.
VERIFY(connection_space == ColorSpace::PCSXYZ || connection_space == ColorSpace::PCSLAB);
VERIFY(number_of_input_channels() == 3);
VERIFY(number_of_output_channels() == color_u8.size());

// ICC v4, 10.10 lut16Type
// "Data is processed using these elements via the following sequence:
// (matrix) ⇨ (1D input tables) ⇨ (multi-dimensional lookup table, CLUT) ⇨ (1D output tables)"

if (connection_space == ColorSpace::PCSXYZ) {
// Table 11 - PCSXYZ X, Y or Z encoding
pcs *= 32768.0f / 65535;
} else {
VERIFY(connection_space == ColorSpace::PCSLAB);

// ICC v4, 6.3.4.2 General PCS encoding
// Table 12 — PCSLAB L* encoding
pcs[0] = clamp(pcs[0] / 100.0f, 0.0f, 1.0f);

// Table 13 — PCSLAB a* or PCSLAB b* encoding
pcs[1] = clamp((pcs[1] + 128.0f) / 255.0f, 0.0f, 1.0f);
pcs[2] = clamp((pcs[2] + 128.0f) / 255.0f, 0.0f, 1.0f);

pcs *= 65280.0f / 65535;
}

// "3 x 3 matrix (which shall be the identity matrix unless the input colour space is PCSXYZ)"
if (connection_space == ColorSpace::PCSXYZ) {
EMatrix3x3 const& e = m_e;
pcs = FloatVector3 {
(float)e[0] * pcs[0] + (float)e[1] * pcs[1] + (float)e[2] * pcs[2],
(float)e[3] * pcs[0] + (float)e[4] * pcs[1] + (float)e[5] * pcs[2],
(float)e[6] * pcs[0] + (float)e[7] * pcs[1] + (float)e[8] * pcs[2],
};
}

// "The input tables are arrays of 16-bit unsigned values. Each input table consists of a minimum of two and a maximum of 4096 uInt16Number integers.
// Each input table entry is appropriately normalized to the range 0 to 65535.
// The inputTable is of size (InputChannels x inputTableEntries x 2) bytes.
// When stored in this tag, the one-dimensional lookup tables are packed one after another"
for (size_t c = 0; c < 3; ++c)
pcs[c] = lerp_1d(m_input_tables.span().slice(c * m_number_of_input_table_entries, m_number_of_input_table_entries), pcs[c]) / 65535.0f;

// "The CLUT is organized as an i-dimensional array with a given number of grid points in each dimension,
// where i is the number of input channels (input tables) in the transform.
// The dimension corresponding to the first input channel varies least rapidly and
// the dimension corresponding to the last input channel varies most rapidly.
// Each grid point value is an o-byte array, where o is the number of output channels.
// The first sequential byte of the entry contains the function value for the first output function,
// the second sequential byte of the entry contains the function value for the second output function,
// and so on until all the output functions have been supplied."
auto sample = [this](IntVector3 const& coordinates, Span<float> out) {
size_t stride = out.size();
size_t offset = 0;
for (int i = 3 - 1; i >= 0; --i) {
offset += coordinates[i] * stride;
stride *= m_number_of_clut_grid_points;
}
for (size_t c = 0; c < out.size(); ++c)
out[c] = (float)m_clut_values[offset + c];
};

Vector<float, 4> scratch;
Vector<float, 4> color;
scratch.resize(number_of_output_channels());
color.resize(number_of_output_channels());

lerp_nd({ m_number_of_clut_grid_points, m_number_of_clut_grid_points, m_number_of_clut_grid_points }, move(sample), pcs, scratch.span(), color.span());

// "The output tables are arrays of 16-bit unsigned values. Each output table consists of a minimum of two and a maximum of 4096 uInt16Number integers.
// Each output table entry is appropriately normalized to the range 0 to 65535.
// The outputTable is of size (OutputChannels x outputTableEntries x 2) bytes.
// When stored in this tag, the one-dimensional lookup tables are packed one after another"
for (u8 c = 0; c < color.size(); ++c)
color[c] = lerp_1d(m_output_tables.span().slice(c * m_number_of_output_table_entries, m_number_of_output_table_entries), color[c] / 65535.0f) / 65535.0f;

// Since the LUTs assume that everything's in 0..1 and we assume that's mapped linearly to bytes,
// we don't need to look at output_space.
// 6.5 Device encoding
// "The specification of device value encoding is determined by the device. Normally, device values in the range of
// 0,0 to 1,0 are encoded using a 0 to 255 (FFh) range when using 8 bits"
(void)output_space;

for (u8 c = 0; c < color_u8.size(); ++c)
color_u8[c] = round_to<u8>(clamp(color[c] * 255.0f, 0.0f, 255.0f));

return {};
}

inline ErrorOr<FloatVector3> Lut8TagData::evaluate_to_pcs(ColorSpace input_space, ColorSpace connection_space, ReadonlyBytes color_u8) const
{
// See comment at start of LutAToBTagData::evaluate() for the clipping flow.
Expand Down
76 changes: 76 additions & 0 deletions Userland/Libraries/LibGfx/ICC/WellKnownProfiles.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -112,6 +112,82 @@ ErrorOr<NonnullRefPtr<Profile>> IdentityLAB()
return Profile::create(header, move(tag_table));
}

ErrorOr<NonnullRefPtr<Profile>> IdentityLAB_mft2()
{
// Identity mapping between CIELAB and PCSXYZ, using an unnecessarily large mft2.

auto header = profile_header(ColorSpace::CIELAB, ColorSpace::PCSLAB);
header.device_class = DeviceClass::ColorSpace;

OrderedHashMap<TagSignature, NonnullRefPtr<TagData>> tag_table;

TRY(tag_table.try_set(profileDescriptionTag, TRY(en_US("SerenityOS Identity LAB, mft2"sv))));
TRY(tag_table.try_set(copyrightTag, TRY(en_US("Public Domain"sv))));

// mft1 is plenty; this is just for testing mft2 LAB codepaths.
EMatrix3x3 e_matrix = identity_matrix();

Vector<u16> input_tables;
for (int c = 0; c < 3; ++c) {
// mft2 allows between 2 and 4096 entries. If there are just two values, it linearly maps between them.
input_tables.append(0);
input_tables.append(65535);
}

auto clut_values = make_2x2x2_cube<u16>();
Vector<u16> output_tables = input_tables;

auto mft2 = TRY(try_make_ref_counted<Lut16TagData>(0, 0, e_matrix,
3, 3, 2,
2, 2,
move(input_tables), move(clut_values), move(output_tables)));
TRY(tag_table.try_set(AToB0Tag, mft2));
TRY(tag_table.try_set(BToA0Tag, mft2));

// White point.
TRY(tag_table.try_set(mediaWhitePointTag, TRY(XYZ_data(header.pcs_illuminant))));

return Profile::create(header, move(tag_table));
}

ErrorOr<NonnullRefPtr<Profile>> IdentityXYZ_D50()
{
// Identity mapping between nCIEXYZ and PCSXYZ.

auto header = profile_header(ColorSpace::nCIEXYZ, ColorSpace::PCSXYZ);
header.device_class = DeviceClass::ColorSpace;

OrderedHashMap<TagSignature, NonnullRefPtr<TagData>> tag_table;

TRY(tag_table.try_set(profileDescriptionTag, TRY(en_US("SerenityOS Identity XYZ"sv))));
TRY(tag_table.try_set(copyrightTag, TRY(en_US("Public Domain"sv))));

// "An 8-bit PCSXYZ encoding has not been defined", so use an identity 16-bit mft2 lookup table.
EMatrix3x3 e_matrix = identity_matrix();

Vector<u16> input_tables;
for (int c = 0; c < 3; ++c) {
// mft2 allows between 2 and 4096 entries. If there are just two values, it linearly maps between them.
input_tables.append(0);
input_tables.append(65535);
}

auto clut_values = make_2x2x2_cube<u16>();
Vector<u16> output_tables = input_tables;

auto mft2 = TRY(try_make_ref_counted<Lut16TagData>(0, 0, e_matrix,
3, 3, 2,
2, 2,
move(input_tables), move(clut_values), move(output_tables)));
TRY(tag_table.try_set(AToB0Tag, mft2));
TRY(tag_table.try_set(BToA0Tag, mft2));

// White point.
TRY(tag_table.try_set(mediaWhitePointTag, TRY(XYZ_data(header.pcs_illuminant))));

return Profile::create(header, move(tag_table));
}

ErrorOr<NonnullRefPtr<ParametricCurveTagData>> sRGB_curve()
{
// Numbers from https://en.wikipedia.org/wiki/SRGB#From_sRGB_to_CIE_XYZ
Expand Down
2 changes: 2 additions & 0 deletions Userland/Libraries/LibGfx/ICC/WellKnownProfiles.h
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,8 @@ class Profile;
class ParametricCurveTagData;

ErrorOr<NonnullRefPtr<Profile>> IdentityLAB();
ErrorOr<NonnullRefPtr<Profile>> IdentityLAB_mft2();
ErrorOr<NonnullRefPtr<Profile>> IdentityXYZ_D50();

ErrorOr<NonnullRefPtr<Profile>> sRGB();

Expand Down
4 changes: 4 additions & 0 deletions Userland/Utilities/icc.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -381,8 +381,12 @@ ErrorOr<int> serenity_main(Main::Arguments arguments)
if (!name.is_empty()) {
if (name == "LAB")
return Gfx::ICC::IdentityLAB();
if (name == "LAB_mft2")
return Gfx::ICC::IdentityLAB_mft2();
if (name == "sRGB")
return Gfx::ICC::sRGB();
if (name == "XYZ")
return Gfx::ICC::IdentityXYZ_D50();
return Error::from_string_literal("unknown profile name");
}
auto file = TRY(Core::MappedFile::map(path));
Expand Down
Loading