-
Notifications
You must be signed in to change notification settings - Fork 3.3k
LibGfx/ICC+icc: Implement converting from PCS to Lut16TagData, add built-in XYZ, LAB_mft2 profiles #26462
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Conversation
|
Thanks to #26459, this also means /Library/Application Support/Adobe/Color/Profiles/Photoshop5DefaultCMYK.icc The "PNG image with embedded color lookup table profile (has fallback matrix tags)" image on https://displaycal.net/icc-color-management-test/ also embeds a profile with mft2 BToA*Tags. Trying something similar to #26459 (comment) with Photoshop5DefaultCMYK.icc: Huh, this looks pretty off, actually! And visually looking at the output, burst-out-serenity-rgb-mft2.webp has quite a few black pixels that shouldn't be there. So more to do here. |
diff --git a/Userland/Libraries/LibGfx/ICC/TagTypes.h b/Userland/Libraries/LibGfx/ICC/TagTypes.h
index 00bb11e5bcd..22fcf4fff62 100644
--- a/Userland/Libraries/LibGfx/ICC/TagTypes.h
+++ b/Userland/Libraries/LibGfx/ICC/TagTypes.h
@@ -1232,7 +1232,7 @@ inline ErrorOr<void> Lut16TagData::evaluate_from_pcs(ColorSpace connection_space
// 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 < 3; ++c)
+ 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,Good enough for now! |
c25bb83 to
a70bf71
Compare
This profile identity-maps PCSXYZ to nCIEXYZ.
This allows converting byte-encoded LAB values to XYZ:
% echo '255, 0, 128' | \
icc -n XYZ --stdin-u8-to-pcs
pcsxyz(1.9999695, 0, 1.0039062)
It can also be written to a file, for other tools to use it:
icc -n XYZ --reencode-to serenity-XYZ.icc
(...or you can then run `icc serenity-XYZ.icc` instead of
`icc -n XYZ` if you want to dump it from a file instead of from
memory, for some reason.)
This profile uses mft2 (16-bit lut) because no mft1 (8-bit lut) PCSXYZ
encoding exists. That's probably because XYZ is a linear space, and for
linear spaces 8 bits isn't enough resolution.
This profile is a bit weird!
- *We* map the result to 8-bit values for display in `icc`.
We store PCS values as floats, so when converting to a space with
gamma, getting 8 bits out is fine, since we convert to 8 bits after
all the other conversion math. But here the final data color
space is also XYZ, and now we invented our own 8-bit XYZ encoding
by just linearly mapping the ICC 16-bit XYZ encoding to it.
- Since this is an identity mapping, it leaks the spec's internal PCSXYZ
encoding into the output data:
0x0000 maps to 0.0f per spec, which for us becomes 0x00
0x8000 maps to 1.0f per spec, which for us is not representable
(0x80 corresponds to 0x8080)
0xFFFF maps to 1.0 + (32767.0 / 32768.0) == 1.999969f (for us, 0xFF)
To repeat, 0xFF maps to 1.999969f.
We could use a linear ramp to map 0x00..0xff to 0.0f..1.0f, but
then the table wouldn't be its own inverse, and directly exposing
the actual PCSXYZ encoding is nice for testing.
In other words, ICC profiles are data-encoding-dependent. One could also
make a profile that maps 0x00..0xff to red 0.25..0.5 instead of to
0.0..1.0 (if one only needed red values in that range and wanted more
resolution in that range).
Anyways, this profile is probably mostly useful for testing.
Makes this work:
% echo 'pcsxyz(1.0, 0.0, 0.5)' | \
Build/lagom/bin/icc -n XYZ --stdin-u8-from-pcs
128, 0, 64
(Note that 1.0 maps to 128, as mentioned in the previous commit
message.)
Also makes it possible to write a roundtrip test for the
IdentityXYZ_D50() profile, so do so.
This also allows doing roundabout lab->xyz conversion (albeit filtered
through u8 resolution):
% echo 'pcslab(100, -128, 127)' | \
icc -n XYZ --stdin-u8-from-pcs | \
icc -n XYZ --stdin-u8-to-pcs
pcsxyz(0.3999939, 1.0039062, 0.039215088)
This is even weirder than the XYZ mft2 profile added in an
earlier commit in this PR:
mft2 uses a "legacy 16-bit PCSLAB encoding" instead of the "normal"
16-bit ICC PCSLAB encoding, and this being an identity mapping leaks
this into the outputs. We again linearly map this internal 16-bit
encoding to 8 bits.
This legacy encoding maps 0 to 0.0 and FF00 to 1.0 for L
(and FFFF to a bit more than 1.0), and 0 to -128.0 and FF00 to
127.0 for a* and b* (and FFFF to a bit more than 127.0).
This means this produces different values than our built-in LAB mft1
profile:
% echo 'pcslab(100, 127, -128)' | \
icc -n LAB_mft2 --stdin-u8-from-pcs | \
icc -n LAB --stdin-u8-to-pcs
pcslab(99.60784, 126, -128)
Staying in LAB for both steps is exact, of course:
% echo 'pcslab(100, 127, -128)' | \
icc -n LAB --stdin-u8-from-pcs | \
icc -n LAB --stdin-u8-to-pcs
pcslab(100, 127, -128)
Staying in LAB_mft2 for both steps is also exact, up to u8
resolution (100 and 127 don't exactly map to an u8 value with
our "16-bit legacy PCSLAB encoding mapped to u8" encoding):
% echo 'pcslab(100, 127, -128)' | \
icc -n LAB_mft2 --stdin-u8-from-pcs | \
icc -n LAB_mft2 --stdin-u8-to-pcs
pcslab(99.99693, 126.99219, -128)
This is just because the same PCSLAB value is encoded slightly
differently in the mft1 and mft2 LAB encodings:
% echo 'pcslab(100, 127, -128)' | \
icc -n LAB --stdin-u8-from-pcs
255, 255, 0
% echo 'pcslab(100, 127, -128)' | \
icc -n LAB_mft2 --stdin-u8-from-pcs
254, 254, 0
(There's no XYZ mft1 encoding, so at least we don't have mft1 and mft2
profiles that have different output.)
Use this profile to add a roundtrip test for Lut16TagData when PCS
is PCSLAB.
a70bf71 to
a07915d
Compare
These are really only useful for testing, so don't mention them in `icc --help` output for `-n`. They're supposed to behave identically to the built-in XYZ and LAB profiles (but not the LAB_mft2 profile, which uses a slightly different internal encoding, see SerenityOS#26462. The new XYZ profiles added here behave like the XYZ profile described there, but the new LAB profiles do not behave like the LAB_mft2 profile described there, but like the LAB (no _mft2) profile: % echo 'pcslab(100, 127, -128)' | \ icc -n LAB_mABmBA_u8_clut --stdin-u8-from-pcs | \ icc -n LAB --stdin-u8-to-pcs pcslab(100, 127, -128) % echo 'pcslab(100, 127, -128)' | \ icc -n LAB_mABmBA_u16_clut --stdin-u8-from-pcs | \ icc -n LAB --stdin-u8-to-pcs pcslab(100, 127, -128) % echo 'pcslab(100, 127, -128)' | \ icc -n LAB_mABmBA_no_clut --stdin-u8-from-pcs | \ icc -n LAB --stdin-u8-to-pcs pcslab(100, 127, -128) The profiles without a CLUT already round-trip fine, so add tests for those.
Similar to what SerenityOS#26452 did for Lut8TagData and SerenityOS#26462 for Lut16TagData. With this, we can convert from PCS to all tag types :^) Which means our ICC implementation is now fairly complete! We don't support DeviceLink or Abstract profile types (both don't appear in images; both are arguably useful in some not-embedded-in-images scenarios), nor the optiona BToD / DToB tags, and are missing some other minor things (like grayscale profiles -- but not much is missing here). But overall, this is now a fairly complete basic ICC implementation :^)
Similar to what SerenityOS#26452 did for Lut8TagData and SerenityOS#26462 for Lut16TagData. With this, we can convert from PCS to all tag types :^) Which means our ICC implementation is now fairly complete! We don't support DeviceLink or Abstract profile types (both don't appear in images; both are arguably useful in some not-embedded-in-images scenarios), nor the optiona BToD / DToB tags, and are missing some other minor things (like grayscale profiles -- but not much is missing here). But overall, this is now a fairly complete (if slow) basic ICC implementation :^)
This is similar to #26452, but for Lut16TagData instead of for Lut8TagData. Lut16TagData supports both PCSXYZ and PCSLAB, so this adds one built-in profile each.
The XYZ profile identity-maps PCSXYZ to nCIEXYZ.
This allows converting byte-encoded LAB values to XYZ:
It can also be written to a file, for other tools to use it:
(...or you can then run
icc serenity-XYZ.iccinstead oficc -n XYZif you want to dump it from a file instead of frommemory, for some reason.)
This profile uses mft2 (16-bit lut) because no mft1 (8-bit lut) PCSXYZ
encoding exists. That's probably because XYZ is a linear space, and for
linear spaces 8 bits isn't enough resolution.
This profile is a bit weird!
We map the result to 8-bit values for display in
icc.We store PCS values as floats, so when converting to a space with
gamma, getting 8 bits out is fine, since we convert to 8 bits after
all the other conversion math. But here the final data color
space is also XYZ, and now we invented our own 8-bit XYZ encoding
by just linearly mapping the ICC 16-bit XYZ encoding to it.
Since this is an identity mapping, it leaks the spec's internal PCSXYZ
encoding into the output data:
0x0000 maps to 0.0f per spec, which for us becomes 0x00
0x8000 maps to 1.0f per spec, which for us is not representable
(0x80 corresponds to 0x8080)
0xFFFF maps to 1.0 + (32767.0 / 32768.0) == 1.999969f (for us, 0xFF)
To repeat, 0xFF maps to 1.999969f.
We could use a linear ramp to map 0x00..0xff to 0.0f..1.0f, but
then the table wouldn't be its own inverse, and directly exposing
the actual PCSXYZ encoding is nice for testing.
In other words, ICC profiles are data-encoding-dependent. One could also
make a profile that maps 0x00..0xff to red 0.25..0.5 instead of to
0.0..1.0 (if one only needed red values in that range and wanted more
resolution in that range).
Anyways, this profile is probably mostly useful for testing.
The LAB_mft2 profile is even weirder, mostly because it's different from the the existing (mft1) LAB profile.
mft2 uses a "legacy 16-bit PCSLAB encoding" instead of the "normal"
16-bit ICC PCSLAB encoding, and this being an identity mapping leaks
this into the outputs. We again linearly map this internal 16-bit
encoding to 8 bits.
This legacy encoding maps 0 to 0.0 and FF00 to 1.0 for L
(and FFFF to a bit more than 1.0), and 0 to -128.0 and FF00 to
127.0 for a* and b* (and FFFF to a bit more than 127.0).
This means this produces different values than our built-in LAB mft1
profile:
Staying in LAB for both steps is exact, of course:
Staying in LAB_mft2 for both steps is also exact, up to u8
resolution (100 and 127 don't exactly map to an u8 value with
our "16-bit legacy PCSLAB encoding mapped to u8" encoding):
This is just because the same PCSLAB value is encoded slightly
differently in the mft1 and mft2 LAB encodings:
(There's no XYZ mft1 encoding, so at least we don't have mft1 and mft2
profiles that have different output.))
Use this profile to add a roundtrip test for Lut16TagData when PCS
is PCSLAB.