Commit Graph
2873 Commits
Author SHA1 Message Date
David Mann b8bcc02cdb Merge #10905: Crop with Delete Cropped Pixels off turns the Background into Layer 0 (#10867) 2026-11-03 00:30:48 +01:00
David Mann 673bfb4d9c Merge remote-tracking branch 'origin/main' into integration-batch-12
# Conflicts:
#	docs/file-format-parity.md
2026-11-02 20:15:23 +01:00
FredApps 9236d27eb6 Merge current main into fix/gnome-window-decorations 2026-11-02 19:10:43 +01:00
ljkx 945cf2218d Crop with Delete Cropped Pixels off turns the Background into Layer 0 (#10867)
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.
2026-11-02 18:29:48 +01:00
David MannandClaude Opus 5.5 c01d82198a Integration: a Background selection keeps its modifier, and Move needs a layer destination (#10702, #10753)
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>
2026-11-02 16:47:11 +01:00
David MannandClaude Opus 5.5 50fd920b3b Integration: a gallery effect's own step keeps its panel name bytes on save (#10632)
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>
2026-11-02 16:33:29 +01:00
David MannandClaude Opus 5.5 c03c97222b Integration: Trap, Match Color, Custom and gallery steps that record a key twice stay kept steps (#10630, #10631, #10632)
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>
2026-11-02 16:30:02 +01:00
David MannandClaude Opus 5.5 e155cf85bf Integration: batch tests read composed pixels with Buffer::into_px after main's #10514 (#10704, #10618)
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>
2026-11-02 14:09:34 +01:00
David Mann 4c95a2d4bd Merge main 5d9c57e60d into integration batch 12 2026-11-02 13:54:27 +01:00
David MannandClaude Opus 5.5 027d191312 Integration: the dither test reads pixels with as_chunks (clippy chunks_exact_to_as_chunks) (#10704)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-02 13:34:57 +01:00
David MannandClaude Opus 5.5 218a84b801 Merge PR #10704: Actions: restore command options for ATN playback
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>
2026-11-02 12:57:10 +01:00
David MannandClaude Opus 5.5 568bf40943 Integration: a Filter Gallery effect's recorded event exports again next to #10628's mask and merge events (#10632)
#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>
2026-11-02 12:51:07 +01:00
Khawar Zamman Wani 5d9c57e60d Linux: return memory of closed documents to the system (#8743) (#10515)
* 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
2026-11-02 09:30:46 +01:00
Khawar Zamman Wani 86ddfb72f7 Reuse large buffers so returning freed memory to the OS doesn't slow filters (#10479) (#10514)
* 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
2026-11-02 09:29:59 +01:00
David MannandClaude Opus 5.5 c788695a59 Integration: event_for keeps the batch's lookup plus #10632's routing of effect events (#10632)
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>
2026-11-02 03:21:42 +01:00
8a0f959d29 Integration: real recorded Convert to Profile and Paste steps play, sparse parallel dither (#10704)
- 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>
2026-11-02 03:18:46 +01:00
David Mann 79a2a4e15f Merge PR #10632: Actions: map all 47 Filter Gallery effects (#10592)
With our fix-up 023e2662d7 on top of the author's head. Conflicts resolved item by item (Rust), docs as unions.
2026-11-02 03:18:45 +01:00
David Mann 520a996b30 Merge PR #10630: Actions: map Photoshop's Split Channels, Merge Spot Channel, Trap and Match Color steps (#10609)
With our fix-up a299bf9f6c on top of the author's head. Conflicts resolved item by item (Rust), docs as unions.
2026-11-02 03:15:28 +01:00
023e2662d7 Integration: Filter Gallery effect steps keep their recorded event (#10632)
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>
2026-11-02 03:15:07 +01:00
a299bf9f6c Integration: Trap round trip, Match Color source None, Split Channels only on single-layer images (#10630)
- 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>
2026-11-02 03:11:30 +01:00
David Mann 951b49e939 Merge PR #10616: Import color component selections from Photoshop actions
With our fix-up cf30adf2a2 on top of the author's head. Conflicts resolved item by item (Rust), docs as unions; docs/atn.md regenerated at the end.
2026-11-02 03:10:25 +01:00
David Mann 496d9f3623 Merge PR #10618: Map Stylize and Pixelate action events
With our fix-up 52cc86351b on top of the author's head. Conflicts resolved item by item (Rust), docs as unions; docs/atn.md regenerated at the end.
2026-11-02 03:10:21 +01:00
David Mann 72d3e49fc6 Merge PR #10620: Map Render filter steps in Photoshop actions
With our fix-up 563666d69a on top of the author's head. Conflicts resolved item by item (Rust), docs as unions; docs/atn.md regenerated at the end.
2026-11-02 03:10:18 +01:00
David MannandClaude Opus 5.5 d97a1c2efa Integration: resolve #10702 and #10628 against the batch (layer selection, event lookup, Vibrance)
- 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>
2026-11-02 03:10:15 +01:00
52cc86351b Integration: Trace levels clamp to 0..255, dialog Tiles/Pointillize save (#10618)
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>
2026-11-02 03:07:01 +01:00
cf30adf2a2 Integration: keep Shift-click and Lmnc channel selects, record panel colour clicks by name (#10616)
- 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>
2026-11-02 03:03:10 +01:00
563666d69a Integration: Lens Flare dialog centre and Clouds seed 0 on Save Actions (#10620)
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>
2026-11-02 02:55:32 +01:00
David Mann 5d6041c1cd Merge PR #10628: Map selection, clipboard and layer-operation ATN events
With our fix-up 0e41091687 on top of the author's head. Conflicts resolved item by item (Rust), docs as unions; docs/atn.md regenerated at the end.
2026-11-02 02:55:08 +01:00
David Mann 4f899196cb Merge PR #10702: Actions: preserve settings in mapped ATN events
With our fix-up 78cf0cf91f on top of the author's head. Conflicts resolved item by item (Rust), docs as unions; docs/atn.md regenerated at the end.
2026-11-02 02:55:02 +01:00
78cf0cf91f Integration: transform anchor codes, Close save:false, Batch Save As + Close, opacity and thumbnail-click regressions (#10702)
- 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>
2026-11-02 02:53:47 +01:00
0e41091687 Integration: Group F steps recorded by the reference application map (#10628)
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>
2026-11-02 02:50:33 +01:00
David Mann d570b77abb Merge PR #10753: Support recorded layer, document and layer comp references
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.
2026-11-02 02:40:55 +01:00
David MannandClaude Opus 5.5 e16e4beb12 Integration: repair the merged Actions test file and event table; keep each PR's export rules (#10627, #10629, #10631)
- 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>
2026-11-02 02:40:14 +01:00
ea4112a4ce Integration: group-end layer index fails, ordinal precondition resolves once (#10753)
A `{"layer":{"index":n}}` that lands on a group end made explicit_layer
return None, so layer.translate and the other callers acted on the
selected layers instead (e.g. layer.translate {"layer":{"index":3}} with
L1+L2 selected moved both). It now fails with "a group end is not a
layer"; layer.select still treats a group end as a no-op.

explicit_layer_enabled resolved a relative ordinal (Frwr/Bckw), made that
layer active, then ran the precondition with the unresolved ordinal, which
resolved again one layer further. With only Red targeted and A (active),
B, C (type) bottom to top, edit.clear {"layer":{"ordinal":"Frwr"}} was
refused with "Rasterize the layer" although it acts on raster B. The
precondition now sees the resolved layer id.

Co-authored-by: yoriswind <164435053+yoriswind@users.noreply.github.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-02 02:17:31 +01:00
David Mann 48b0e221cd Merge PR #10627: Actions: map Photoshop's newer-feature steps (Oil Paint, Smart Sharpen, Focus Area, Sky, Remove Background) (#10610)
With our fix-up 6ab24508d8 on top of the author's head 8988b6bfd0. Conflicts resolved: import lists as unions (bare_from/bare_to taken from steps_other, #10631), event table, tests and parity doc rows as unions.
2026-11-02 02:16:45 +01:00
David Mann 5bc852379a Merge PR #10743: Read Photoshop Actions version 12 descriptors
With our fix-up 1f524c378b on top of the author's head 713aa92ba5. Resolved with main: the version check sits in main's parse_layout (it merged cleanly); doc comments combined; atn_contract no longer expects version 12 to be refused and pins that an empty version 12 set parses; one ATTRIBUTION row for all ATN fixtures; the fixture README states which files were encoded how; development.md no longer counts the fixtures.
2026-11-02 02:13:56 +01:00
6ab24508d8 Integration: .atn Oil Paint/Smart Sharpen clamp on import, Smart Sharpen maps the Gaussian form (#10627)
Out-of-range Oil Paint and Smart Sharpen settings kept the step as a
Photoshop step; the reference application clamps them when the step
plays, so the import now clamps (stylization 10.5 -> 10, Amnt 500.5 % ->
500, Rds 0.05 px -> 0.1). Export still refuses out-of-range params.

Oil Paint's light direction (LghD) had no range: LghD 500 mapped and
saved back as 500. It now clamps to the command's -180..180 on import
(500 -> 180) and export refuses {"angle": 181}.

Smart Sharpen mapped the legacy Lens Blur form, which the command's
Gaussian sharpen does not play, and wrote every step as legacy Lens
Blur. It now follows the PSD smart-filter mapping (photocraft-io
smart_map): only useLegacy false + blur GsnB maps (noiseReduction ->
reduceNoise); the reference default (useLegacy true, lensBlur), Motion
Blur and legacy steps stay Photoshop steps; export writes useLegacy
false, Amnt, Rds, noiseReduction, blur GsnB.

Co-authored-by: hectorviov <1501713+hectorviov@users.noreply.github.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-02 02:13:01 +01:00
1f524c378b Integration: track the v12 .atn fixtures and sniff v12 in the CLI (#10743)
The three version 12 fixtures were tracked inside crates/psd/fuzz/corpus,
which crates/psd/fuzz/.gitignore ignores and cargo-fuzz rewrites (cmin) or
deletes; the psd and engine test binaries include_bytes! them, so cleaning
the corpus broke the build. They move to crates/psd/tests/fixtures/atn with
a README (sizes, purpose, sha256; the seeds note folded in) and an
ATTRIBUTION.md row; the fuzzer takes the folder as an extra corpus dir.

photocraft-cli recognised an extensionless Actions file only by version
word 16, so a version 12 set without .atn (classic Mac OS Photoshop 5)
went down the JSON path and failed with a UTF-8 error. It now accepts 12
and 16, the versions photocraft-psd reads. Input: v12-commands.atn copied
to "Legacy Actions" and passed to load_actions_file.

Co-authored-by: yoriswind <164435053+yoriswind@users.noreply.github.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-02 02:09:41 +01:00
David Mann cafaabcf04 Merge PR #10631: Actions: map Photoshop's De-Interlace, NTSC, HSB/HSL, Custom, Average and matting steps (#10607)
With our fix-up 632a62b554 on top of the author's head 4ed3f5b591. Conflicts resolved as unions (module list, event table, tests, parity doc rows).
2026-11-02 02:03:24 +01:00
David Mann 2d332d3ba5 Merge PR #10629: Actions: map Photoshop's Distort filter steps (Pinch, Polar Coordinates, Spherize, Twirl, Wave, ZigZag) (#10605)
With our fix-up 363e0f5e89 on top of the author's head 244082fb2c. Conflicts with main resolved as unions (new tests next to main's; parity doc revision rows).
2026-11-02 02:02:03 +01:00
632a62b554 Integration: Custom import takes whole numbers only, export refuses non-object params (#10631)
A Custom (Cstm) step with a fractional Scl/Ofst/cell, e.g. Scl Double 1.5,
was mapped to filter.other.custom with scale 1.5, but export writes integers
and refuses the fraction, so Save Actions failed on a step that used to stay
a kept Photoshop step and round-trip. `within` now requires a whole number;
an integral Double such as 2.0 still maps.

`sets_only` accepted any non-object params, so custom_to(json!([1])) wrote an
identity Custom and bare_to(json!(5)) an empty descriptor for params no
command can play. Only an object or null passes now.

Co-authored-by: hectorviov <1501713+hectorviov@users.noreply.github.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-02 01:59:05 +01:00
363e0f5e89 Integration: keep Wave action steps with more generators than the renderer draws (#10629)
A Wave step recorded with NmbG above 64 (photocraft_algo::MAX_WAVE_GENERATORS)
mapped to filter.distort.wave, but the renderer clamps to 64 generators, so the
step played differently from Photoshop. Add generators 1..MAX_WAVE_GENERATORS to
WAVE_COMMAND_LIMITS: such a step (e.g. NmbG 65, 999, 1000000) now stays a kept
Photoshop step on import, and {"generators": 65} is refused on export, as the
PSD smart-filter mapper does.

Co-authored-by: hectorviov <1501713+hectorviov@users.noreply.github.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-02 01:58:01 +01:00
David Mann 5a99af60d7 Merge PR #10620: Map Render filter steps in Photoshop actions 2026-11-02 01:54:44 +01:00
aiongg fab871dadb Merge pull request #10535 from davidmann4/integration-batch-11
Integration batch 11: 9 ATN PRs on main
2026-11-01 19:11:02 -05:00
Gabriel Heuser 24be8ea6e2 Add editable arrowheads to Line tool (#6408) (#9259)
* Add configurable arrowheads to line tool

* Add editable arrowheads to Line tool

* Fix arrow scaling validation and PSD import defaults

* Preserve arrow geometry under affine transforms

* Preserve scaled arrowhead dimensions and prevent non-finite geometry

* Localize arrowhead Start and End labels

* Fix arrow transform editability and short line handling

* Fix PSD arrow concavity serialization

* Fix arrow dimension fallback validation

* Refine arrowhead options UI to match Photoshop

* Add opt-in arrowhead defaults to Shape properties

* Fix tool reset syntax after upstream merge

* Validate arrowhead flags and clamp copied defaults

* Preserve editable headless lines under transforms

* Fix arrowhead UI imports and formatting after v0.17 merge

* Format arrowhead properties after v0.18 merge

* Fix Clippy lint errors in arrow geometry and tests

* Fix headless arrow geometry after nonuniform scaling

* Fix proportional arrow scaling in Properties panel

* Fix Rust formatting in Properties arrow scaling tests

* test: validate invalid arrowhead dimensions without mutation

* fix: preserve arrow compatibility with older pcraft versions
2026-11-01 18:46:05 -05:00
yoriswind 686f08c884 test(engine): record and export accepted comp index bounds 2026-11-01 14:08:40 -08:00
yoriswind 5321bd773d Reject comp references outside the ATN index format before recording 2026-11-01 13:52:11 -08:00
yoriswind c5ed8f9fcf Cover comp recording wire bounds and invalid move destinations 2026-11-01 13:44:29 -08:00
yoriswind 6e5f02c014 Check absolute profile intent against an independent pixel value 2026-11-01 13:17:55 -08:00
yoriswind 0ae7356064 Respect mask creation clearing the selection in the control test 2026-11-01 13:15:34 -08:00