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.
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>
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>
* 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
- 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>
- 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>
- 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>
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.
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>
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>
Retain bounded original SVG alongside its raster mirror. Explicit Pixels,
Path and Shape modes use real source data, validate destination and workload
limits before editing, and preserve legacy Paste when the mode is omitted.
Round-trip typed Photoshop Paste classes and keep unsupported descriptors.
Adapted-from: Skyfuller1303's PR #3409
Original-commit: 897f200727
Original-source: https://github.com/storytold/photocraft/pull/3409
Validation: engine check --tests; strict lib and vector target Clippy;
7 focused native/ATN tests; owned-file formatting and diff checks.
- Channel targeting routes an allow-list of pixel-editing paint commands, not every
`paint.*` id: the Mixer Brush's Alt-click load and Symmetry no longer follow a colour
set or get refused with "Rasterize the layer" on a fill layer. Patch and Content-Aware
Move stay routed (#9274).
- Native vector save-back checks the preview SVG as strictly as stored previews are
checked on load and PSD export, so a preview with an unreadable embedded image is
refused instead of stored and later dropped (#8571).
- .pcraft blobs read their zstd frame with framing headroom, so an incompressible
preview right at its 16 MiB limit still loads; the limit still bounds the data.
- Levels/Curves recall (Alt+shortcut) decides the combined Lab record from the target
the dialog captured, as remembering does, so a viewed layer mask keeps its settings
beside a Lab colour set (#9274).
Each comes with a test that fails without it.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>