Fix ICC profile tagging across export formats#599
Merged
Conversation
Four related color-profile defects found by auditing every export
format x color space combination:
- Greyscale JPEG/WebP shipped untagged, untransformed pixels: the L-mode
image was fed a CMS transform whose source is the RGB working-space
profile, which lcms rejects ("cannot build transform"); the swallowed
error left working-TRC luma with no embedded profile, inconsistent
with the greyscale TIFF/PNG of the same edit. Both formats now run the
16-bit grey re-encode (new _greyscale_to_pil_u8) and embed the grey
profile.
- The greyscale re-encode used a pure 1/2.2 power, but the bundled
GrayGamma2.2.icc actually carries the sRGB TRC (as the _JXL_COLOR note
documents, and verified by probing the profile) — shadows didn't match
the tag. Encode with the sRGB OETF; this also makes JXL's GRAY/SRGB
tagging consistent.
- ACES/XYZ targets have no ICC profile, so export skipped CMS and wrote
untagged working-space pixels. The encoder now falls back to the
working space with its profile embedded (warning logged), and the
export colour-space combo no longer offers unmappable spaces
(EXPORT_COLOR_SPACES).
- Contact-sheet JPEGs (sRGB display-space tiles) now embed the sRGB
profile instead of shipping untagged.
Tests cover each defect and were verified to fail against the unfixed
encoder.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
An audit of every export format × colour space combination (encoding synthetic buffers through
_encode_exportand inspecting the resulting bytes) found four colour-profile defects. The mainstream paths — JPEG/PNG/TIFF in sRGB / Adobe RGB / ProPhoto / P3 D65 / Rec 2020, plus custom output ICC overrides — were already correct: right profile embedded, pixels converted. These were the broken cases:1. Greyscale JPEG/WebP exported untagged, untransformed pixels. The L-mode image was handed to a CMS transform whose source profile is the RGB working-space profile; lcms refuses to build an RGB→gray transform ("cannot build transform" on every greyscale JPEG/WebP export), the exception was swallowed, and the file shipped working-TRC luma with no embedded profile — visibly inconsistent with the greyscale TIFF/PNG of the same edit. Both formats now run the same 16-bit grey re-encode as TIFF/PNG (new
_greyscale_to_pil_u8helper) and embed the grey profile.2. Greyscale encode used a pure 1/2.2 power, but the tag carries the sRGB TRC. The bundled
GrayGamma2.2.iccactually holds the sRGB TRC despite its name — the existing_JXL_COLORcomment documents this, and I verified it by probing the profile through lcms (L* at input 8/255 is 2.35, matching sRGB's 2.19, not pure-2.2's 0.44). Pixels encoded 2.2 but tagged sRGB-TRC read wrong in the shadows. The re-encode now uses the sRGB OETF, which also makes JXL'sGRAY/SRGBenumerated tagging consistent with its pixels.3. ACES/XYZ export targets silently produced untagged Adobe RGB. Neither space has a mapped/bundled ICC profile, so CMS logged "ICC profile not found", skipped the conversion, and wrote untagged working-space pixels. Two-part fix: the encoder now falls back to exporting the working space with its profile embedded (warning logged) so stale presets/DB configs can never produce a mistagged file, and the export colour-space combo no longer offers unmappable spaces (new
EXPORT_COLOR_SPACES; ACES/XYZ remain available where they belong, as rawpy decode spaces).4. Contact-sheet JPEGs were untagged. Tiles are converted to sRGB display space before compositing; the sheet JPEG now embeds the bundled sRGB profile.
Test plan
tests/test_export_icc.py(6 tests): greyscale JPEG tagged + matches greyscale TIFF tonally, greyscale WebP tagged, shadow-probe proving the sRGB TRC (and rejecting the old 2.2 curve), ACES fallback produces working-space pixels with the exact working-space profile bytes,EXPORT_COLOR_SPACESexcludes unmappable spaces, contact-sheet sRGB bytes resolve. All four behavioral tests verified to fail against the unfixed encoder.test_export_color_management,test_export_jxl,test_export_webp,test_export_settings_form,test_contact_sheet,test_icc_lut, …) — 122 passed; the only failure is the known pre-existing Windows path-separator issue intest_output_dir_subfolder_of_source, unrelated.ruff check+ruff formatclean.