* ACEScg: File Handling option opens colour-less EXR renders in the ACES working space
Our render pipelines write ACEScg (AP1 primaries, ACES white ~D60,
linear) and — like most — state no colour space in the EXR header, so
io's linear-sRGB default misrepresented them (saturated hues shifted).
- cms: AcesCg joins the built-in profiles, generated from the published
AP1 primaries and ACES white point with a linear TRC (id "aces-cg",
aliases acescg).
- engine: File Handling gets "Open color-less EXR as ACEScg". When on,
an EXR that io tagged with the linear-sRGB default opens as ACEScg
instead; embedded or EXIF-declared profiles stay untouched.
Translated in every catalog. ACES view transforms (OCIO-based display)
remain future work; this covers the document's source colours.
* scorecard: 26 of 158 (main's #3472 preference added one too)
* Regenerate docs/scorecard.md after merging main
---------
Co-authored-by: Brandon Thomas <echelon@gmail.com>
Offsets and counts stored in a profile reach u32::MAX, which is
usize::MAX on 32-bit targets such as wasm32. Several places added to or
multiplied them before any bounds check, so a malformed profile could
panic (overflow checks on) or read from a wrapped offset (checks off).
- curv: reject an over-long count before computing the element length.
- mAB/mBA: the matrix and CLUT read through one bounds-checked slice at
the stored offset instead of adding to the offset.
- mluc: the record offset is computed with checked arithmetic.
- XYZ: one bounds-checked slice instead of off + 4 and off + 8.
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
* feat(color): Dot Gain 20%-compatible default working gray, as in Photoshop
Photoshop's default working gray is Dot Gain 20%, and its RGB -> Grayscale
measured in Photoshop 27.11 (default Color Settings) is D50 luminance
encoded with gamma 1.732: sRGB 64 / 128 / 192 grey and pure red become
46 / 105 / 177 / 107. PhotoCraft defaulted to sGray, which kept the sRGB
value (64 / 128 / 192 / 130), up to 24 levels off.
Adds the built-in "Dot Gain 20%-compatible (gamma 1.732, Photocraft)"
gray profile (id dot-gain-20-compat; our own fitted curve, not Adobe's
profile, the same gamma PhotoCraft already uses for gray adjustments) and
makes it the default working gray and the fallback for untagged gray
documents. sGray and Gray Gamma 2.2 stay selectable.
This changes a default: untagged gray documents now convert and display
as Photoshop shows them (midtones lighter than under sGray); documents with
an embedded profile are unchanged.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* Status bar test: the default working gray is now Dot Gain 20%-compatible
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* .atn test: #202020 in the Dot Gain 20%-compatible working gray
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Co-authored-by: Brandon Thomas <echelon@gmail.com>
An iPhone photo embeds Apple's Wide Color Sharing Profile, an input-class
profile with only AToB tables. Preserve kept it as the document's space, so
every composite into the document failed with "RGB profile without
matrix/TRC or BToA tags". Such a profile can't be a destination, so open
converts from it to the working space whatever the policy (Off still
discards it) and the report says inputOnly.
Co-authored-by: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
PAR_MIN_PIXELS / PAR_CHUNK_PIXELS (photocraft-cms) and OPENABLE
(photocraft-engine) are only used by native-only code (the rayon paths
and list_images). Give them the same cfg so wasm32 check and clippy
-D warnings are clean. No behaviour change.
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
A DeviceLink's header PCS field holds its output colour space
(ICC.1:2010 §7.2.7), so a CMYK→CMYK link has a 4→4 AToB0. The LUT
channel check hard-coded 3 outputs and rejected such files as invalid,
so users were told a well-formed profile was corrupt and never reached
the "DeviceLink profiles cannot be used" error.
Check DeviceLink LUTs against the header's output space instead, and
only let a Lab DeviceLink through link_stages when its tables end in
3 channels, so a Lab→CMYK link gets the unsupported error rather than
building a mismatched transform.
Fixes#2337
Claude-Session: https://claude.ai/code/session_01X5k8jSoLtGswanmHBipHER
Co-authored-by: Leel Wasanatha <260480244+kavishkashehan726-sudo@users.noreply.github.com>
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
* OpenEXR/HDR export: linearize grayscale documents too
Fixes#2386. The EXR/HDR branch of export_flat converted RGB images to
linear sRGB but left gray ones as stored, so an sGray #808080 was written
as 0.502 instead of 0.216. Gray images are now converted from their
profile (sGray when untagged) to linear gray. An untagged grayscale
OpenEXR opens tagged linear gray (an EXR Y channel is linear luminance),
and the open policy keeps that profile without a mismatch prompt, as it
already does for linear sRGB.
* contributors: add itsF4LCON
---------
Co-authored-by: Brandon Thomas <echelon@gmail.com>
* Color Lookup: keep the .cube input domain and reject the ones we cannot apply
DOMAIN_MIN/DOMAIN_MAX (and LUT_3D_INPUT_RANGE) were parsed and thrown
away, so a log-domain or extended-range look was applied to plain 0..1
inputs: silently wrong colours, exactly what the in-code comment
admitted. The domain now travels with the parsed LutFile (validated:
every component finite and min < max), and colorLookup refuses a
non-default domain with the reason instead of shifting the look -
the same unsupported-over-wrong-output call the codecs make. Honoring
a custom domain end to end means carrying it through ColorLookup
(doc, compose, gpu, PSD round trip) and stays open as the follow-up.
* tests: the colorLookup domain rejection needs a pixel layer active
The outer session of the test still had the adjustment layer active, so
the command was disabled before reaching the domain rejection - the
assertion only passed when the active layer happened to differ. Make it
deterministic with a fresh pixel layer.
* Color Lookup: bake a 1D .cube's input domain into the resample
The 1D branch of parse_cube still ignored DOMAIN_MIN/MAX and
LUT_1D_INPUT_RANGE. Since the 1D table is resampled onto a 0..1 cube anyway,
the domain is now applied there (saturating outside), and a degenerate
domain is rejected like the 3D one.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* cargo fmt after rebasing onto main
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
---------
Co-authored-by: Brandon Thomas <echelon@gmail.com>
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
connection() scaled the PCS by src.media_white() / dst.media_white() with
no check on the divisor, so a v4 profile whose wtpt tag holds zeros (parsed
and stored verbatim) gave a NaN scale, and every colour through a direct
absolute transform or a paper-simulating proof came out NaN. The transform
is now refused with CmsError::Invalid when either white has a zero,
negative or non-finite component; the other intents never divide by it and
are unchanged.
Co-authored-by: wstock <1394687+wstock@users.noreply.github.com>
Co-authored-by: Claude Fable 5.1 <noreply@anthropic.com>
Opening any file Photoshop tagged with sRGB IEC61966-2.1 asked
"Embedded Profile Mismatch: sRGB IEC61966-2.1 vs sRGB IEC61966-2.1":
open_policy compared a hash of the ICC bytes, and Photoshop's v2 profile
(1024-entry tables) and the built-in v4 sRGB (parametric curves) are the
same colours in different bytes. 32-bit documents in Photoshop's linear
sRGB missed the stay-linear exception for the same reason.
Add Profile::same_colors: same colour space and every sample of a device
grid within dE76 0.5 through both (re-encoding costs 0.025; moving the
red primary 2% costs 1.0). Colours are compared rather than round-tripped
because CMYK -> PCS -> CMYK is not an identity even for one profile.
Of the 75 profile-tagged PSDs in the test corpora, all 75 prompted on
open; now 6 do (Adobe RGB, Gray Gamma 2.2, SWOP), each a real mismatch.
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
- Real crash: a gray ICC profile whose A2B0 tag is stored as "mBA "
parsed fine, but re-encoding its RGB view (gray_as_rgb, used for gray
documents' composites) hit panic!("unsupported lutAtoB stage layout").
The ICC writer now leaves out a LUT its tag type can't store, and
gray_as_rgb drops the gray LUTs (one input channel) so the RGB view is
the matrix/TRC model it builds. Regression test
gray_profile_with_reversed_lut_type_reencodes.
- math::rgb_to_xyz_matrix returns None for dependent primaries.
- Builtin::profile indexes its cache by discriminant (tested); the shipped
CMYK profile falls back to synth::coated_cmyk() instead of expect.
- Two constant-only expects (Bradford inverse, built-in primaries) use the
documented narrow allow.
- cms denies unwrap/expect/panic/unimplemented/todo/unreachable.
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Rust 1.99's clippy adds chunks_exact_to_as_chunks and some_filter, which
broke the Clippy step on every CI runner (it uses the latest stable). All
sites are mechanical: as_chunks drops the remainder exactly like
chunks_exact.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
CI's `cargo fmt --all -- --check` has failed on every run because the
repo had no rustfmt config and was never formatted. Use the same settings
as EffectCraft/FilmCraft (max_width 160, use_small_heuristics Max), which
also keeps the churn small. Formatting only; no code changes.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>