Filter Options › Hide hides the row but keeps the filter applied, as in the
reference application (#10876, #10877). This PR now only draws the row in
every theme.
With a free ratio, Shift on a crop corner kept the proportions by letting
the larger relative extent win, so (-100, -50) on an 800 x 500 frame gave
720 x 450. Photoshop projects the pointer onto the frame's diagonal through
the dragged corner and gives 706 x 441. Do the same, with and without Auto
Center Preview and on a turned frame. A ratio preset, Alt+Shift and edge
handles keep today's rule.
* TIFF: open YCbCr images as RGB (#10316)
Scanners often write JPEG-compressed YCbCr TIFFs with 2x2 chroma
subsampling. The tiff crate decodes and upsamples them but returns raw
YCbCr samples, which the decoder rejected with "colour type YCbCr(8)".
The new tiff/ycbcr.rs converts them to RGB after decoding: JPEG data
uses the JFIF full-range BT.601 conversion (as libtiff does), other
compressions honour ReferenceBlackWhite and YCbCrCoefficients with the
TIFF defaults as fallback. 8 and 16-bit. YCbCr with extra samples, or
in other sample formats, fails with a clear unsupported error.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* Contributors: add Yuki-init's display name
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* TIFF YCbCr: test that YCbCrCoefficients set the luma weights
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* TIFF: YCbCr tags with the wrong value count use the defaults
tag_f64s now returns None unless the tag holds exactly n values, so a
YCbCrCoefficients or ReferenceBlackWhite tag with extra values falls
back to the TIFF defaults instead of being truncated.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Lrgj5pnShYtsSnrsZq5iRn
* TIFF YCbCr: tests for tiled JPEG with ICC, big-endian 16-bit and planar
Covers the layouts the review asked for: a tiled 4:2:0 JPEG file with an
embedded profile (checked against an independent JPEG decoder), a
big-endian 16-bit file, and planar data in both byte orders.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Lrgj5pnShYtsSnrsZq5iRn
* TIFF YCbCr: cite the formula sources and pin the green-channel choice
Documents where the conversion comes from: TIFF 6.0 Section 21
(YCbCrCoefficients, pp. 90-91) and libtiff's tif_color.c
(TIFFYCbCrToRGBInit, TIFFYCbCrtoRGB). Red and blue follow the spec;
green follows libtiff and adds Y unscaled, which matches the spec only
when the luma weights sum to 1. The module doc records this as a
deliberate compatibility choice, and a new test pins it: with weights
0.25/0.25/0.25, mid gray stays (128, 128, 128).
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Lrgj5pnShYtsSnrsZq5iRn
* Codecs README: YCbCr TIFFs with three 8- or 16-bit samples decode to RGB
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Lrgj5pnShYtsSnrsZq5iRn
* Format tiff_ycbcr tests with rustfmt
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* test(tiff): compare grouped YCbCr decoding and update support notes
* style(tiff): align grouped decode call with test formatting
* style(tiff): apply repository rustfmt to grouped YCbCr regression
---------
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Co-authored-by: FredApps <153173854+FredApps@users.noreply.github.com>
Co-authored-by: FredApps <FredApps@users.noreply.github.com>
A crop with Delete Cropped Pixels off now turns the Background into a
normal layer named "Layer 0", as in Photoshop, within the crop's one
history step: straight, extending and turned crops, and crops with a
target W x H x Resolution. The pixels past the canvas stay on every
layer, and the area an extending crop adds is transparent on every
layer instead of taking the background colour. A turned crop converts
the Background before the turn, so it turns like the other layers and
keeps its overhang.
Delete Cropped Pixels on is unchanged. Image > Reveal All keeps its
current result (the Background stays, its extension takes the
background colour) by calling the inner crop with a keep-Background
flag. Content-Aware crop fill with Delete off was not measured and is
left as it is.
Adds crates/engine/tests/it/crop_delete_off_background.rs. The #7086
test now locks the cropped layer back into a Background, the state
documents saved before this change still hold.
* Format: take the .pcraft envelope from craft-pcraft-format
The manifest envelope, TileRef, Hash, FORMAT_VERSION, EXTENSION, the
entry names, tile_path and blob_path, is_valid_hash, the manifest
nesting and item checks and the migration runner now come from
craft-libs' craft-pcraft-format. photocraft-format re-exports them, so
callers do not change. Manifest is craft_pcraft_format::Manifest<DocM>.
Saved bundles keep the same bytes. A save still checks only the nesting
and the item count of its manifest, not its size, and the manifest
nesting limit is still computed from photocraft_doc::MAX_GROUP_DEPTH.
Tests: a save, load and save again gives the same bytes in every color
mode and depth, with and without previews; MAX_GROUP_DEPTH equals
craft_pcraft_format::MAX_GROUP_DEPTH.
* Format: pin craft-pcraft-format from storytold/craft-libs
* Format: read manifests with craft-pcraft-format's read_manifest_with
Pins craft-pcraft-format to dca870a0, the head of craft-libs #407.
Loading reads the manifest with read_manifest_with, which runs the
limit check, the version check, the migration steps and the envelope
read in one path; the layers are still decoded one at a time after the
rest of the document. A save builds the manifest with Manifest::new and
tile references with TileRef::new, and the bundle's directory names and
the .zst suffix come from the crate's constants. Saved bytes and error
messages are unchanged.
* Format: pin craft-pcraft-format to craft-libs #407's head 04dd1bd
04dd1bd changes only the crate's docs and a test, so photocraft-format
builds the same code.
* Format: pin craft-pcraft-format to craft-libs main at b8f7d93
craft-libs #407 merged as b8f7d93. Its read_manifest also refuses a
thumbnail other than thumb.png and a composite other than
composite/preview.png, the only names photocraft-format writes.
A Cmd-click adding the Background to the selection loaded as a plain select that replaced
the selection; a Move to an ordinal of a channel loaded as a layer arrange. Found by
CodeRabbit on #10776.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The doc merge left five Actions rows that disagreed (version 12 unsupported vs read; 120 vs
127 events). Found by CodeRabbit on #10776.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
An ordinary paste now remembers the SVG next to the bitmap it took, and a clipboard that no
longer holds the remembered SVG forgets it, so Paste As neither replaces a newer internal copy
with the old SVG nor ignores the same SVG copied again later. Paste As Pixels reads the bitmap
when the SVG read fails. Found by CodeRabbit on #10776.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The remembered name was the Latin-1 reading of the bytes, so a GBK or Shift_JIS name was
written back garbled. Found by CodeRabbit on #10776.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The first value played and saved, the other was dropped. Same rule as the Group I and
Distort mappers' `setting`. Found by CodeRabbit on #10776.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Main's buffer reuse (#10514) gives Buffer a Drop impl, so moving .px out of it no longer compiles.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Supported table read from the batch's events.rs. The PRs' own sections (settings and command
options retained, Render defaults, steps kept disabled) are kept; the known-gaps tables that
#10702 and #10704 close are gone.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
With our fix-up 8a0f959d29 on top of the author's head 072a87e7b0.
Conflicts with the batch resolved: Image Size keeps #10702's point sizes and
writes `scaleStyles` only when true (no key and false play the same in the
reference application, so recorded steps save back unchanged); Convert to
Profile takes #10704's options; new tests carry #10743's `encoding` field;
#10702's absolute-intent test expects the new Convert to Profile flags.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
#10628 refuses an `_atnEvent` that is not a string; #10632 stores the effect's own event
there as an object, so every gallery step loaded from its own event failed Save Actions.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* perf: reuse large buffers so a pinned glibc trim threshold costs no speed (#10479)
With MALLOC_TRIM_THRESHOLD_ set (#8743), glibc maps every allocation over
128 KiB fresh, so tiles, compositor bands and filter scratch were page-faulted
in on every call: viewport compose 3.6x slower, Gaussian Blur up to 1.8x,
.pcraft save 2.4x.
- photocraft_raster::scratch: a process-wide, capped pool of large Vec
buffers (u8/u16/f32/[f32; 4]). A miss drops the oldest buffers worth its
size down to a 256 MiB floor, so peak memory stays near glibc's defaults.
The engine releases the pool after any command that closes a document,
Headless::close does too, and an idle thread releases it after 2 s unused.
- Tiles, compose Buffer/masks/bands, algo Image and the blur paths, engine
adjustment/filter scratch, GPU tile upload and io import buffers go through
the pool.
- .pcraft save reuses one zstd FrameCompressor per worker (byte-identical).
- interactive_bench reports minor page faults per run.
36 MP, trim threshold set vs glibc defaults: viewport 5.6 vs 6.1 ms (was
22.7), Blur r4 445 vs 437 ms (was 852), Curves CPU proxy 35.0 vs 35.7 ms
(was 50), flatten 146 vs 147 ms (was 237), save .pcraft 251 vs 230 ms (was
545). RSS after closing a 5400x7200 document: 21 MB with trim set.
* fix(linux): restart once with glibc's trim threshold set so closed documents return memory (#8743)
With glibc's default malloc, the memory of a closed document stays with the
process: freeing a large buffer raises glibc's dynamic mmap threshold, so
later tile buffers land in heap pages glibc keeps. A session that opened and
closed a 5400x7200 document held ~450 MiB afterwards.
photocraft_engine::malloc_tuning::apply(), called first in both binaries'
main, re-executes the binary once with MALLOC_TRIM_THRESHOLD_=131072 on Linux
with glibc: same PID, arguments, argv[0] and environment, in safe Rust
(CommandExt::exec). A malloc variable or glibc.malloc.* tunable the user or a
package already set is left alone, so setting MALLOC_TRIM_THRESHOLD_ opts
out. No-op on macOS, Windows, musl and wasm.
Depends on the scratch-buffer pool (#10479), without which the pinned
threshold slows filters, previews, flatten and .pcraft save by 26-209%.
* fix: corpus tests build again; return remaining pooled buffers (#10479 review)
- Corpus-gated io tests and the Photoshop-oracle test moved `.px` out of a
`Buffer`, which now implements `Drop` (E0509 with `--features corpus`):
use `into_px()`.
- io: masks, the merged image, extra channels, smart-filter masks,
smart-object pixels and patterns give their `interleave` buffers back to
the scratch pool after writing them.
- compose: recycle the mask values of a pass-through group, `effect_shape`
and both `split_parts` callers.
- Styled Free Transform preview copies each band out instead of moving it,
so the band returns to the pool.
- interactive_bench: the fault count covers each whole run, untimed setup
included; say so.
* fix: corpus tests build again; return remaining pooled buffers (#10479 review)
- Corpus-gated io tests and the Photoshop-oracle test moved `.px` out of a
`Buffer`, which now implements `Drop` (E0509 with `--features corpus`):
use `into_px()`.
- io: masks, the merged image, extra channels, smart-filter masks,
smart-object pixels and patterns give their `interleave` buffers back to
the scratch pool after writing them.
- compose: recycle the mask values of a pass-through group, `effect_shape`
and both `split_parts` callers.
- Styled Free Transform preview copies each band out instead of moving it,
so the band returns to the pool.
- interactive_bench: the fault count covers each whole run, untimed setup
included; say so.
* fix: recycle the masks of adjustment-atop and shape layers (#10479 review)
* fix: recycle the masks of adjustment-atop and shape layers (#10479 review)
* fix: recycle effect hiding masks (#10479 review)
* fix: recycle effect hiding masks (#10479 review)
* fix: recycle knockout, CMYK and GPU shape masks; release the pool when Split/Merge Channels close a document (#10479 review)
* fix: recycle knockout, CMYK and GPU shape masks; release the pool when Split/Merge Channels close a document (#10479 review)
* fix: release the pool when a closed document's cancelled workers exit; idle-release switch for tests; don't pool PSD planes (#10479 review)
* fix: release the pool when a closed document's cancelled workers exit; idle-release switch for tests; don't pool PSD planes (#10479 review)
* fix: release the pool for workers cancelled before their document closes; take the pool lock for budget and idle-release changes (#10479 review)
* fix: release the pool for workers cancelled before their document closes; take the pool lock for budget and idle-release changes (#10479 review)
* fix: idle release rechecks activity under the lock; try_take_exact releases the pool and retries before failing (#10479 review)
* fix: idle release rechecks activity under the lock; try_take_exact releases the pool and retries before failing (#10479 review)
* test: cover a successful try_take_exact miss; name the failed-reservation test for what it checks (#10479 review)
* test: cover a successful try_take_exact miss; name the failed-reservation test for what it checks (#10479 review)
* fix: pool the final GPU effect shape buffer and return it after use (#10479 review)
* fix: pool the final GPU effect shape buffer and return it after use (#10479 review)
* docs: describe scratch pool lifecycle regression invariants
* test: verify polling releases cancelled document scratch buffers
* perf: recycle remaining transform selection and PSD scratch buffers
* fix: free PSD gather bands before allocating channel planes
* test: assert exact scratch take reuses pooled storage
* perf: reuse large buffers so a pinned glibc trim threshold costs no speed (#10479)
With MALLOC_TRIM_THRESHOLD_ set (#8743), glibc maps every allocation over
128 KiB fresh, so tiles, compositor bands and filter scratch were page-faulted
in on every call: viewport compose 3.6x slower, Gaussian Blur up to 1.8x,
.pcraft save 2.4x.
- photocraft_raster::scratch: a process-wide, capped pool of large Vec
buffers (u8/u16/f32/[f32; 4]). A miss drops the oldest buffers worth its
size down to a 256 MiB floor, so peak memory stays near glibc's defaults.
The engine releases the pool after any command that closes a document,
Headless::close does too, and an idle thread releases it after 2 s unused.
- Tiles, compose Buffer/masks/bands, algo Image and the blur paths, engine
adjustment/filter scratch, GPU tile upload and io import buffers go through
the pool.
- .pcraft save reuses one zstd FrameCompressor per worker (byte-identical).
- interactive_bench reports minor page faults per run.
36 MP, trim threshold set vs glibc defaults: viewport 5.6 vs 6.1 ms (was
22.7), Blur r4 445 vs 437 ms (was 852), Curves CPU proxy 35.0 vs 35.7 ms
(was 50), flatten 146 vs 147 ms (was 237), save .pcraft 251 vs 230 ms (was
545). RSS after closing a 5400x7200 document: 21 MB with trim set.
* fix: corpus tests build again; return remaining pooled buffers (#10479 review)
- Corpus-gated io tests and the Photoshop-oracle test moved `.px` out of a
`Buffer`, which now implements `Drop` (E0509 with `--features corpus`):
use `into_px()`.
- io: masks, the merged image, extra channels, smart-filter masks,
smart-object pixels and patterns give their `interleave` buffers back to
the scratch pool after writing them.
- compose: recycle the mask values of a pass-through group, `effect_shape`
and both `split_parts` callers.
- Styled Free Transform preview copies each band out instead of moving it,
so the band returns to the pool.
- interactive_bench: the fault count covers each whole run, untimed setup
included; say so.
* fix: recycle the masks of adjustment-atop and shape layers (#10479 review)
* fix: recycle effect hiding masks (#10479 review)
* fix: recycle knockout, CMYK and GPU shape masks; release the pool when Split/Merge Channels close a document (#10479 review)
* fix: release the pool when a closed document's cancelled workers exit; idle-release switch for tests; don't pool PSD planes (#10479 review)
* fix: release the pool for workers cancelled before their document closes; take the pool lock for budget and idle-release changes (#10479 review)
* fix: idle release rechecks activity under the lock; try_take_exact releases the pool and retries before failing (#10479 review)
* test: cover a successful try_take_exact miss; name the failed-reservation test for what it checks (#10479 review)
* fix: pool the final GPU effect shape buffer and return it after use (#10479 review)
* docs: describe scratch pool lifecycle regression invariants
* test: verify polling releases cancelled document scratch buffers
* perf: recycle remaining transform selection and PSD scratch buffers
* fix: free PSD gather bands before allocating channel planes
* test: assert exact scratch take reuses pooled storage
WIP: gallery_effect_steps_save_back_as_the_event_they_were_recorded_as still fails (Accented Edges step refused on save); to resolve next.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
- Convert to Profile: the reference application records `sdwM` -1 for RGB
targets (`convertToProfile T="ProPhoto RGB" Inte=AClr Dthr=true Fltt=false
rasterizePlaced=false sdwM=-1`). The PR accepted only 2, so every recorded
step became a kept step that fails at playback. Accept and write -1.
- Paste: an ordinary Edit > Paste records `AntA Anno` + `As Pxel` (Paste in
Place adds `inPlace true`). `As Pxel` was routed through the explicit vector
mode (no layer recreation, no mask/channel targets) and Paste in Place became
a kept step. `As Pxel` now keeps the ordinary Paste / Paste in Place route.
- Dither: transparent layers stay sparse (only stored tiles are dithered) and
tiles run in parallel. 24 MP 8-bit, one layer: opaque 1576 -> 101 ms;
transparent with one stored tile 1670 -> 5 ms and 384 -> 1 stored tiles.
- Image Size: a recorded step without `scaleStyles` (Fit Image) gained
`scaleStyles false` on save; the key is now written only when styles scale.
Co-authored-by: yoriswind <164435053+yoriswind@users.noreply.github.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Three gaps in loading and saving Filter Gallery steps from .atn files:
- An effect recorded under its own event by string ID (event `cutout`
with `GEfk` = `Ct `, the form newer versions write) stayed a kept
Photoshop step: only 4-char codes were routed to the effect table.
The effects' string IDs are now matched too.
- A step recorded as the effect's own event (`AccE` "Accented Edges")
saved back as a `GEfc` "Filter Gallery" event, so load -> save changed
the event and its panel name. The step now remembers the event it was
loaded from (hidden `_atnEvent` param) and writes it back when that
event is the step's own effect; `GEfc` steps, steps made in PhotoCraft
and a stale or forged event naming another effect save as `GEfc`.
Playback strips the param (`playback_params`, like `_actionLayer`), so
the command, the journal (Last Filter), smart filters and the step's
dialog never carry it.
- A key that was present but had the wrong type or a non-finite value
took the default (Texturizer `InvT` as Integer 1 played with invert
off; `EdgW` NaN played as 2). Only missing keys take defaults now; an
unreadable present key keeps the step.
Co-authored-by: hectorviov <1501713+hectorviov@users.noreply.github.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
- A Trap step recorded without Wdth loaded as {} and saved back as Wdth 1 px,
so it reloaded as {"width":1}. It now loads as width 1 (what the command
plays), so load -> save -> load gives the same step.
- The Match Color dialog records Source "None" as "source": -1, which the
export refused because "source" wasn't an allowed key, so a Match Color
recorded in PhotoCraft could never be saved. A null or negative source
(and a sourceLayer with it) is now written as the source-less event; a
real source (>= 0) is still refused.
- Split Channels closes the original, but split and its enabled check never
checked the layer count, so a played SplC split and closed a layered
document with no undo. Like the reference application it now refuses
(Err, document untouched and open) a document with more than one layer
(a hidden one counts) or a lone group; a Background-only image or a lone
normal layer still splits.
Co-authored-by: hectorviov <1501713+hectorviov@users.noreply.github.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
- steps_layer.rs select_from/select_to: #10753's layer references by name, index and ordinal
(recorded_layer_target / recorded_layer_ref) together with #10702's property references and the
zero document offset it refuses; the line merge had interleaved the two functions.
- layer_params.rs explicit_layer: #10753's group-end arm had lost its closing brace next to
#10702's Background arm.
- events.rs event_for: the batch's lookup (by code, else by non-empty name, legacy codes) kept;
#10628 renamed IntW/Dstr to intersectWith/distribute, and the run-time names the reference
application reports (interfaceWhite, distort) still load through NAME_ALIASES (test extended).
- Vibrance: one event, saved as the string event `vibrance` (#10702, from #8659) with the
`vibrance` class main writes (#8534); `Vbrn` still loads. #10702's round-trip test expects that
class now (the top-level class does not change what the reference application plays).
- #10702's tests build AtnFile with the `encoding` field main added (#8327).
All engine tests pass.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Trace Contour read any i32 level, but a recorded step carries a whole
number 0..255 that the reference application clamps; import now clamps
(Lvl -1 -> 0, 300 -> 255) and export refuses levels outside 0..255.
Save Actions refused every Tiles/Pointillize step recorded through the
filter dialog, because the dialog adds foreground/background colours the
recorded event does not carry. Export now drops them (playback uses the
current colours, as the reference application does). Tiles count is
rounded on export like the native command rounds it (17.4 -> TlNm 17),
and null params export the defaults instead of being refused.
Co-authored-by: yoriswind <164435053+yoriswind@users.noreply.github.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
- A Shift-click channel select (`slct` Chnl.Chnl.Cyn with `Extd` true) imported as a plain
single-channel `channel.target`, so playback targeted Cyan alone and Save dropped `Extd`.
It now stays a kept Photoshop step (only `Extd` absent or false maps).
- `select Chnl.Chnl.Lmnc` went through `channel_of`'s legacy fallback, targeted Lightness and
saved back as `Lght `. The select importer now uses the exact CHANNELS lookup, so it stays a
kept step and round-trips unchanged.
- The Channels panel recorded a colour click as `{"channel":{"color":0}}`, which the new export
rejects, so Save Actions of a PhotoCraft recording failed. A colour row click now sends the
mode's lowercase colour name (`{"color":"cyan"}`), which targets the same channel and exports
as the component select; other rows keep their reference.
Co-authored-by: yoriswind <164435053+yoriswind@users.noreply.github.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The Lens Flare params doc said "centerX":number=0.5, which the filter
dialog reads as an integer field defaulting to 0: Filter > Render > Lens
Flare put the flare in the top-left corner and an imported 0.4 centre
rounded to 0. The doc is back to 0..1=0.5; the engine still plays
off-canvas centres unclamped (a recorded 1.4 centre now has a playback test).
A Clouds or Difference Clouds step recorded through PhotoCraft's dialog
carries {"seed":0}, and Save Actions refused it. The event has no seed
key; seed 0 is what the imported empty step plays as, so it is dropped on
export. Any other seed still plays differently and is still refused.
Co-authored-by: yoriswind <164435053+yoriswind@users.noreply.github.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
- FTcs reference points used a 3x3 grid (Qcs0..Qcs8). The reference application has
Qcsa centre, Qcs0..Qcs3 corners clockwise from top-left and Qcs4..Qcs7 the sides
top/right/bottom/left (measured: 50 % scale, Qcs6 keeps the bottom centre, Qcs1 the
top-right corner). Qcs8 and Qcsi stay kept steps; nothing writes Qcs8.
- file.close {"save": false} (used by ui-egui tests) became an error; it discards again
and saves as Svng N.
- Batch never performs the action's Save As, so the scratch document stayed dirty and the
recorded Close (no Svng -> "save":"ask") failed every file of "...; Save As; Close".
A Save / Save As save point now marks the document saved.
- Adjustment-layer opacity is quantised to 8 bits on load; a recorded 70 % saved back as
70.196 %. The whole percent that quantises to the same level is saved again.
- Cmd-clicking a group's thumbnail errored; it loads the whole canvas again as before.
A recorded Cmd-click on another layer's thumbnail kept the numeric id and failed strict
Save Actions; it records the layer by name (or as the Background).
Co-authored-by: yoriswind <164435053+yoriswind@users.noreply.github.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The Group F loader only mapped descriptors with an empty name and class
`null`, and nested shapes with an empty name. The reference application
names every step descriptor after the localized step (plus NUL), uses
the event code as its class and names nested shapes too, so every real
recorded Add To / Fade / Merge Layers ... step stayed a kept step and
never played (e.g. `addTo` with name "Add To\0", class `AddT` and a
nested `Rctn` named "\0"). The header is no longer checked; the shape
is matched by class only; key, order and enum checks stay strict.
IntW, Dstr and MrgL were registered with the string IDs
`interfaceWhite`, `distort` and none; the real ones are
`intersectWith`, `distribute` and `mergeLayers`, so steps written with
them stayed kept.
Co-authored-by: yoriswind <164435053+yoriswind@users.noreply.github.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
With our fix-up ea4112a4ce on top of the author's head 686f08c884. docs/atn.md kept from the batch (regenerated at the end); development.md and parity doc rows as unions.
- atn_cmds/tests.rs: the line unions of the merges split tests into each other (shared closing
lines kept once). Rebuilt from main plus every merged PR's added and changed tests, item by
item; the shared every_mapped_command table merged line by line.
- events.rs: Remove White Matte had lost its closing lines in the same way.
- #10627 and #10631 both added a `bare_from`/`bare_to` with different export rules (the Group E
one also takes a pixel `target`): #10627's is now `empty_from`/`empty_to`, so Focus Area and
Remove Background keep their own rule.
- The non-object params fix of #10631's fix-up (632a62b554) applied to the same `sets_only`
copies in steps_image.rs (#10627) and steps_distort.rs (#10629).
- Smart Sharpen export writes `presetKind` Custom first again, as recorded steps carry it
(#10627's fix-up 6ab24508d8 had dropped it, so load -> save lost the key).
All engine tests pass (lib 2591, integration suites green).
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>