Engine: droplet dependency capture skips disabled steps; a nested Batch
that was cancelled or could not write its error log fails the caller;
runDroplet forwards overrideOpen; actions.rename follows named Batch
steps; Batch steps inside a script count against MAX_SCRIPT_STEPS;
output claims compare destination identity; Save and Close never
replaces a read-only source. ATN: Md must be a BlnM enumeration,
adjustment-layer names must round-trip, Calculations exports a missing
blending as Multiply, Apply Image refuses unnamed-layer transparency.
Filters: Shape Blur records its resolved geometry, reserves its kernel
memory first, and reports a preparation failure as an error, not a
cancel; Crop keeps an artboard preset when the size is unchanged and
fits the board on a turned crop.
Codecs/cms/algo: zero-axis FITS image HDUs are empty; a GIF walk that
breaks off before the trailer fails closed; two-value .3dl shaper rows
are applied; fragmented custom kernels are refused and their work polls
cancellation.
UI: Conditional branches play through panel playback; paused actions
refuse to resume after being replaced; Playback Options OK validates
its fields; transform double-click ignores a hidden pivot; path boxes
snap to a stationary selection; modal blocking uses the effective tool;
a picker-consumed release clears snap lines and ends a Move drag; the
Crop preset control shows Selection and checks only it; Crop box undo
restores frame_from_selection; Bosnian fill labels.
Tests: timing asserts moved to an ignored test, plus the review's
coverage nitpicks.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Main now reads `Mesh 4 12` as a declared 17³ cube (#9073, #9078), so the
preamble tests put a non-declaring header line in front of their 4-node
tables. They still check that a header is skipped and the rows after it are
read strictly: three_dl_mesh_preamble_is_accepted,
three_dl_malformed_first_token_is_refused.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
CodeRabbit (discussion_r4368798643): a shaper such as `0oops 114 455 1023`
failed to parse as a number, so it was skipped as a header and the table
loaded without its shaper. A header is now `3DMESH` or a line whose first
token starts with a letter and isn't a number; anything else is read
strictly and a bad token is refused.
Test: three_dl_malformed_first_token_is_refused.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
CodeRabbit finding on #9170 ("Refuse non-even shapers when the aligned bake
grid exceeds MAX_SIZE"), as @bommerts asked too ("clear refusal if the
requested precision cannot be retained"): past MAX_SIZE the bake fell back
to a coarser grid, which can land only on a bent shaper's unbent knots and
render the table alone. Such a shaper is refused. New test:
three_dl_shaper_too_fine_to_keep_is_refused (a 257-point shaper bent at odd
knots loaded unshaped without the fix; a 17-point shaper on a 17³ table
still loads).
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
CodeRabbit finding on #9170 ("Reject shaper rows that begin with NaN or
inf"): lines starting with a letter were skipped as headers, so a shaper
beginning with NaN or inf was dropped before the non-finite check and the
table loaded alone. A header is now a line whose first word isn't a number
(3DMESH, Mesh 4 12); NaN and inf parse as numbers, so such a shaper is
read and refused. New test: three_dl_shaper_starting_non_finite_is_refused
(the files loaded without the fix).
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
CodeRabbit finding on #9170 ("Reject non-finite shapers instead of silently
dropping them"), which @bommerts' refusal control agrees with: a NaN or inf
shaper was dropped and the table rendered alone, breaking apply-or-refuse.
apply_shaper refuses it. New test: three_dl_non_finite_shaper_is_refused
(NaN, inf and -inf shapers loaded without the fix); the PR's own NaN case
now expects refusal.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Review by @bommerts on #9170: strict numeric parsing (999a2c7be) refused the
standard 3DMESH / Mesh 4 12 preamble, because 3DMESH starts with a digit, so
every headered .3dl failed with "bad line 3DMESH". The 3DMESH line is
skipped as a header; rows after it stay strict. New test:
three_dl_mesh_preamble_is_accepted (bad line 3DMESH without the fix).
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Review by @bommerts on #9170: a shaper row with a token that isn't a number
(0 341 junk 682 1023) had the token dropped, which can turn a bent shaper
into an even ramp and lose it, and a second shaper row was ignored. Every
token of a numeric line must parse, and only one shaper row ahead of the
table is read. New test: three_dl_malformed_shaper_rows_are_refused (both
files loaded without the fix).
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Review by @bommerts on #9170: baking at max(table nodes, shaper points) only
kept the shaper's breakpoints when the two grids lined up. Shaper
0 0 512 1023 over a 5³ table read 0.0834 at 1/3, where the shaper is 0
(about 21 8-bit levels off). The bake grid now holds both the table's nodes
and the shaper's knots (lcm of their intervals, up to MAX_SIZE), so an
identity table carries the curve exactly. New test:
three_dl_shaper_knots_survive_a_nonaligned_table (0.0834 without the fix).
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
CodeRabbit finding on #9170 ("Check the baked table for non-finite values"):
a finite but extreme shaper such as 0 1e308 -1e308 0 overflowed to infinity
when interpolated over the resampled grid, and infinity times 0 put NaN in the
table. The shaper is scaled to 0..1 before interpolating, and the baked table
is checked like any parsed one. New test:
three_dl_extreme_shaper_bakes_only_finite_values (NaN without the fix).
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
CodeRabbit finding on #9170 ("Refuse a non-even shaper that has more points
than the 3D table"): the shaper was baked by reading it only at the table's
own nodes, so a 2x2x2 table kept just its two ends and lost the midtones.
Instead of refusing the file (Photoshop loads it), the table is now baked
with as many nodes as the shaper has points, up to MAX_SIZE. New test:
three_dl_shaper_finer_than_the_table_keeps_its_curve (grey 128 stays 128
without the fix).
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
CodeRabbit finding on #9170 ("Fix the even-ramp tolerance for shapers with
scale == 1.0"): the even-ramp check allowed one integer code (1.0) even for a
float shaper in 0..1, so every float shaper passed as even and was dropped.
A float shaper is now held to one 10-bit step. New test:
three_dl_float_shaper_is_applied (grey 128 stays 128 without the fix).
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
A .3dl's first row is a shaper: Photoshop applies it as a 1-D curve to
each input channel before the 3-D table. PhotoCraft skipped the row and
treated every shaper as the even ramp, so a file with a bent shaper
rendered as if it had none (grey 128 stayed 128 where Photoshop gives 71
for the shaper 0 114 455 1023, and 64 for a 17-point squared one).
parse_3dl now keeps the shaper and, when it isn't the even ramp, bakes it
into the table: each node becomes the table's trilinear value at the
shaped input. Even ramps (the usual case) are left untouched.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The exact-core test only used sRGB, whose curves are parametric. Run it over
a profile with distinct per-channel curv table TRCs as well, and check
convert_u16 against the untabulated evaluation for both.
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
run_u8 on a core without a lookup table (for example sRGB to Lab) evaluated the
first stage's curves with powf on every channel of every pixel. An 8-bit channel
has 256 values, so the curves are tabulated once per transform with the same
Curve::eval and the remaining stages run from there. Output is bit-identical.
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
run_u8 and run_u16 pushed every pixel through Vec/Option indirection
(apply_matrix, Post::eval, eval_fast). For a three-channel matrix/TRC transform
the matrix and the output-table index maths now run over blocks of 64 pixels on
fixed-size arrays. Same float operations in the same order per pixel, so the
output is bit-identical.
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
* chore: resolve SonarCloud maintainability issues
* fix: format imports and fix effect_entry argument type
* fix: restore test closure and wildcard import that broke compilation
* refactor: split composite_to_surface to reduce cognitive complexity
Keep the Mesh declaration's cube edge and output range (#9078) alongside
main's header-aware inference (#9121): a declared range wins for in-range
node codes, and anything else falls back to the depth inference, where a
header (the 3DMESH magic included) allows a 10-bit reading.
main's three_dl_without_a_header test used 'Mesh 1 10' as an arbitrary
header line on a 2-cube; that is now a 3-cube declaration, so it uses the
bare 3DMESH magic as its header.
A UTF-16 .look that Photoshop loads was read as UTF-8 and failed with "no <size> in .look", in Color Lookup and in PSD import. Decode FF FE / FE FF as UTF-16 before picking the format.
Fixes#9118
Co-authored-by: Brandon Thomas <echelon@gmail.com>
A table with no header peaking at 255 or 1023 was scaled to full range,
so white came out 255 where Photoshop gives 16 or 64. Without a header,
the inferred output depth now starts at 12 bits.
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
A comment holding <size>/<data> text was read as the LUT, so a commented
inverse .look rendered as identity. Drop comments before finding tags.
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
A `Mesh` line is now a declaration only when it carries exactly two integer
fields after the keyword; tokens like `Mesh invalid 1 12` no longer recover a
declaration from numbers in the wrong positions and fall back to headerless
inference. Under a valid declaration the node codes are integers in the declared
output range, so the declared range is used for every in-range value — including
a small max, where code 1 is 1/declared of full output, not full output itself
(#9073). Only a node beyond the declared range still falls back to max-value
inference.
The Lustre `3DMESH` / `Mesh <inBits> <outBits>` header declares the cube edge
(2^inBits + 1) and the output code range (up to 2^outBits - 1); the parser
skipped every text line, so it inferred both from the data. That rejected a
3-squared cube whose three-value shaper row counted as a 28th node, and
divided 10-bit codes under a 12-bit declaration by 1023 instead of 4095.
Photoshop 27.11 accepts and renders both (measured by the reporters).
Profile::content_hash is a fixed-key 64-bit SipHash of the ICC bytes, so a
crafted profile can share another profile's hash (about 2^32 work). Two
places took an equal hash as proof of an identical profile:
- transform::cached keyed its process-wide cache on the two hashes and the
options, so a colliding profile was handed the other profile's cached
transform and converted with the wrong colours.
- Profile::same_colors returned true on equal hashes, so a colliding
profile was taken for the other one and a needed conversion was skipped.
Add Profile::same_bytes, which compares the ICC bytes (an Arc pointer check
first, then the hash, then the bytes). same_colors uses it instead of the
hash. Each transform cache entry now keeps the ICC bytes of its two
profiles; a hit counts only when they match, and a colliding pair gets its
own transform without replacing the cached entry. The transform is built
after the cache lock is released, as before.
The regression tests give Display P3 the content hash of sRGB (a stand-in
for a found collision): before the fix, same_colors called them the same
and the cache returned sRGB's transform for P3; now both see P3's colours.
Fixes#8613
Co-authored-by: John Turnbach <jturnbach@users.noreply.github.com>
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
- gpu: the test modules share one GPU_LOCK, and timing and vector_mask
take it too, so one process drives one wgpu device at a time.
- ui-egui: the wgpu tests in pattern_preview_canvas, adjust_preview_gpu,
marquee_autoscroll and layout_perf take gpu_lock().
- affinity compound_limits: the child process selects its test by the
module path (compound_limits::<name>); with --exact, the bare name
matches no test and the child passes without running the check.
- xtask scorecard reads crates/io/tests/it/corpus.rs.
- Docs and comments name the new test paths and the --test it filters.
Every tests/*.rs file is its own executable that links the whole crate
graph. Move each crate's test files into tests/it/ as modules of one
tests/it/main.rs; helper modules under tests/common move with them.
These keep their own binary (own process):
- engine panic_hunt: a process-wide panic hook counts every panic.
- engine text_gamma: changes the process-wide text gamma.
- engine smart_filter_stages, mask_cache_purge: assert on global caches.
- codecs pnm_peak_memory: reads the process's peak RSS.
- photocraft mac_menu_appkit: harness = false.
crates/engine/src/file_cmds.rs: batch keeps the save-point closure (takes the output path) and also copies the session's authorize hook into each scratch session, as main now does.
crates/raw/src/sensor.rs: both sides cap the memory of retained tiles; kept the existing retained_segment_bytes check (checked multiplication, same limit) and dropped main's duplicate inline loop.
Two per-pixel/per-sample amplifiers driven by a crafted ICC profile:
- table_inverse recomputed a sampled curve's flat-run edge indices by
scanning the whole table on every call, and transform construction
inverts each destination curve OUT_TABLE times. Compute the edges once
per curve via a new Curve::inverse_table batch method.
- eval_multilinear enumerated 2^inputs cube corners over every declared
input, including single-grid-point (degenerate) dimensions that
contribute nothing. Interpolate over the effective dimensions only, so
a 15-channel CLUT of one-point dims no longer does 2^15 iterations per
pixel.
Findings:
- acfbe3a3-ee58-4b7e-b72a-5a4877dd15c9 (CWE-407, repeated flat-run scans)
- 9fcc72e7-46d6-4f75-9333-cb14a65eeafd (CWE-407, degenerate-dim corner blowup)
Found while testing crates/cms/src/lutfile.rs:227 (write_cube)
Co-Authored-By: Claude Haiku 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Found while testing crates/cms/src/lutfile.rs:111 (parse_cube) and :227 (write_cube)
Co-Authored-By: Claude Haiku 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
The colr nclx primaries and transfer were dropped, so a 10-bit BT.2020 HLG
HEIC opened as if its samples were sRGB. photocraft-heif now returns the
nclx (inherited from grid tiles like the ICC profile), codecs decodes PQ and
HLG to linear light in 32-bit float (BT.2100 inverse OETF + OOTF at 1000
cd/m2, 203 cd/m2 diffuse white = 1.0), and io tags the document with the
matching profile from the new photocraft_cms::builtin::cicp_rgb. BT.709/sRGB
SDR files are unchanged.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
A Color Lookup layer embeds its LUT file in the PSD (LUT3DFileData,
LUTFormat, LUT3DFileName). Photoshop stores the file as loaded and tags
its format; it never converts it. PhotoCraft kept only the parsed table,
so every save re-encoded it as .cube with LUTFormatCUBE while keeping the
original name: a layer loaded from "Film.3dl" was saved as .cube data
under a .3dl name.
The layer now keeps the file it was loaded from (its format and bytes,
`ColorLookup::source`), set when a PSD is read and when the colorLookup
command loads `data`/`file`, and cleared when a built-in look or "none"
replaces the table. A PSD save writes those bytes and format back
unchanged, but only while they still parse to the layer's table, so the
stored file can never go stale. Otherwise (a table made in PhotoCraft,
or one that no longer matches) it falls back to .cube as before and
swaps a .3dl/.look extension in the name for .cube.
The new field is `#[serde(default)]` and skipped when empty, so older
.pcraft files load unchanged. lutfile gains Format::detect/parse_as so
the recorded format is exactly the one the parser used.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>