Commit Graph
49 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
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 Mann 4c95a2d4bd Merge main 5d9c57e60d into integration batch 12 2026-11-02 13:54:27 +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 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
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
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
yoriswind 686f08c884 test(engine): record and export accepted comp index bounds 2026-11-01 14:08:40 -08:00
yoriswind c5ed8f9fcf Cover comp recording wire bounds and invalid move destinations 2026-11-01 13:44:29 -08:00
yoriswind cebea834c1 Prove recorded comp ordinals restore the current layout 2026-11-01 13:06:05 -08:00
yoriswind 21b6546907 Resolve recorded Action reference forms at playback 2026-11-01 12:55:02 -08:00
yoriswind 713aa92ba5 Exercise legacy action imports through the integration suite 2026-11-01 11:50:10 -08:00
David Mann b2db24400e Merge main at b1cfff9d3 into integration batch 10
Conflicts resolved as unions of both sides: codecs re-exports (PngRows + TiffBands), smart_cmds source imports
(main's source_surface_from_buffer and cache API with #8571's svg_source_image_in/convert_svg_source; main's
duplicate rendering import dropped), io flat.rs test modules (#8536's banded TIFF tests and main's PNG ones),
menus/invoke.rs (#8571's smart SVG intercept and main's layer.selectUnderCursor), state.rs (#8311's
type_box_overflow and main's type_insertion_style), the canvas_geom test keeps the is_finite guard, docs
status lines keep both change lists, i18n catalogs keep both sides' rows (hi.tsv: main's wording where both
added the same string).
2026-10-30 14:28:02 +01:00
FredApps 0f120bbda1 Merge current main, reconcile streamed PNG profiles and workflow inputs 2026-10-30 06:52:05 +01:00
David Mann fea300dd4c Merge origin/main into integration batch 10 2026-10-30 01:47:01 +01:00
FredApps c93c0bb36d Merge main into trimmed integration; reconcile blur, pattern and Liquify behavior 2026-10-29 23:47:53 +01:00
FredApps 29bf828ec2 Merge upstream main into reviewed performance integration
Preserve both UI parity status entries; retain all reviewed performance changes.
2026-10-29 23:35:19 +01:00
David Mann 701315b9ea Merge PR #8371: Harden Photoshop action export targets and corpus assertions
Conflict: crates/engine/src/atn_cmds.rs was split into cmd/atn_cmds/ on main.
Kept main's tree there: the PR's set_to guard for a `layers` list is dead on
main (the MAPPED path already refuses it), and its new test in
layer_props_tests.rs repeats main's own refusal test, so both stay out. The
stricter corpus round-trip in tests/it/corpus_atn.rs comes in as written.
2026-10-29 20:21:24 +01:00
FredApps 53dbfe280c Integrate 16 reviewed performance and usability PRs
Includes #8472 #8488 #9461 #9793 #9809 #9813 #9814 #9822 #9826 #9847 #9850 #9852 #9921 #9924 #9961 #10080 at reviewed heads.

Preserves each PR head as a parent. Reconciles CI workflow inputs and benchmark jobs without dropping checks. Excludes #8536 #9825 #9969 #10129. Combined CI pending; no manual validation claimed.
2026-10-29 14:31:15 +01:00
FredApps bf33e45694 Merge approved PhotoCraft PR #9342: fix(io, engine): Embed Color Profile writes sRGB for an untagged document (#9325)
Preserve current module structure and port the approved source/test changes to unique current locations.
2026-10-29 03:51:21 +01:00
FredApps 7e061febf4 Merge approved PhotoCraft PR #9472: fix(io): import a Displace smart filter that names its map file without embedding it (#9162) 2026-10-29 03:29:54 +01:00
Federico Olivares 50f4602c33 Merge main into the integration batch 2026-10-28 18:20:58 -03:00
Federico Olivares 5a6270921c Merge main into the integration batch
Main now carries #9031 (Halftone) and #8923; this branch keeps only what it adds on top: the guard for a setting beside a Gallery stack's effect list, its test, and the PSD open warning for patterns that paint nothing.
2026-10-28 11:17:59 -03:00
Brennan StehlingandClaude Opus 5.5 2119158a5e perf: a pixel change damages only the tiles that changed; command rects become checked hints (#9329)
step_damage narrows a layer whose only change is in its surfaces (pixels, mask, type,
smart-object and fill caches) to the bounding box of the tiles that differ: a merge walk of
the two tile maps, pointer compares only. A mask's tiles grow by its feather, all of them by
the layer's effects and the effects of the groups around it, never past the layer's whole
extents. A frame-spanning effect whose frame moved keeps the whole extents (#4053). Undo,
redo and History jumps get it for free: undoing a 60 px dab on a full-canvas layer redraws
its tiles, not the canvas, and so does a fill or filter on a small selection.

The painting commands' own damage rects go through one door, Session::hint_damage, which
intersects them with the edit's damage. Debug and test builds count a hint that misses a
changed tile in Session::damage_misses and log it; nothing panics. refresh_type_layers reports
step_damage instead of None.

Deleted: note_damage and layers_damage (identical to step_damage after layer.setProps and
layer.translate), note_inert_insert (step_damage already steps over inert layers, #1771),
and the engine's siblings_of they used.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-10-27 23:09:58 -07:00
David MannandClaude Opus 5.5 6fcbedd229 Merge origin/main into pr-4329-fixup
Main moved the engine commands into crates/engine/src/cmd/ and the
dialogs and panels into submodules. The PR's placement calls follow
them, and main's new insert sites (new_adjustment in adjust_params.rs,
Place in file_cmds/open.rs, shapes in vector_cmds/shape.rs) use
layer_place::insert_new like the PR's originals. The New Group dialog
moves to dialogs/new_group_ui.rs; the Layers panel flyout's New Group…
opens it.

Main's Session::precheck now also checks layer.new.group's and
layer.new.groupFromLayers' new params (name, color, mode, opacity,
position), so precheck_agrees_with_execute holds. Main had since added
"New Group…" to every catalog: the PR's duplicate rows are dropped.
Main's newer tests that made a layer with an open group active now
open the group last, as the PR did for the older ones.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-27 15:22:03 +01:00
Federico Olivares e173a9be17 Merge main into the integration batch
Main reorganised the source tree (command modules under engine/src/cmd, tools and dialogs under ui-egui/src, large files split into modules, inline test modules moved to their own files). This branch's changes to the moved and split files are ported to their new locations.
2026-10-27 10:44:16 -03:00
Khawar Zamman Wani 761c46c56e perf(selection): combine marquee additions and subtractions sparsely 2026-10-27 12:15:33 +05:30
aiongg 065c2c2beb test: keep split modules visible to the source scans
The i18n coverage scan and prefs_usage stopped reading a file at its first `#[cfg(test)] mod`. A split parent declares `#[cfg(test)] mod tests;` before the rest of its code, so the scans skipped that code, and on main an inline test module in the middle of panels.rs hid the code after it. Both now drop only the body of an inline test module.

The prefs audits in scorecard and prefs_usage count a read when the section and the field appear in the same file. Both now also treat a split module (`foo.rs` with `foo/`) as one file, and prefs_usage leaves out `engine/src/prefs/` as scorecard already does, since that directory now holds the rest of the prefs model.

On main and on this branch: the i18n coverage report is identical (2,142 source keys; the old scan found 2,049 on main), `cargo xtask scorecard --check` is up to date, and prefs_usage passes.
2026-10-26 22:54:56 +00:00
Federico Olivares 8aaa03e2db Merge main into the integration batch 2026-10-26 11:52:45 -03:00
Hasan-ZayniandClaude Opus 5.5 0f48dfacc8 fix(io): import a Displace smart filter that names its map file without embedding it (#9162)
Photoshop can save a Displace smart filter with only DspF, a path to the
map file, and no embedded DspD map. Import required the embedded map, so
such a filter became psd.unsupportedFilter and Replace Contents skipped it.
It now imports as filter.distort.displace with the path as mapPath (read on
re-render through the document path policy) and writes back unchanged.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-25 18:09:08 +03:00
4a17c64139 Integration: a straight Content-Aware crop fills over hidden pixels (#8179)
Photoshop keeps the pixels an earlier Delete Cropped Pixels off crop hid
only under a turned frame. A straight crop pulled back out over them fills
the whole area past the old canvas from the canvas, with Delete Cropped
Pixels off or on (checked: 64x48 green with a red left strip, crop to x=16
keeping pixels, then a Content-Aware crop to x=-24 w=72: all green). The
PR took the snapshot for straight crops too, so the hidden red came back.
The straight path is main's again; the turned path keeps the snapshot.

extending_crop_keeps_the_hidden_pixels becomes
extending_crop_fills_over_the_hidden_pixels (transparent and white
Background, Delete off and on); it fails with the straight snapshot back.

Co-authored-by: Michael Higgins <55243596+Higgs32584@users.noreply.github.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-25 14:35:33 +01:00
David Mann 14946918cd Integration: merge main (#8179) 2026-10-25 14:33:03 +01:00
Hector VillarrealandClaude Opus 5.5 6f1164e034 fix(io, engine): Embed Color Profile writes sRGB for an untagged document (#9325)
An untagged document (a new one in the default working space, or a file
opened without a profile) is treated as sRGB (sGray for grayscale), but
Export As, Save As and Save for Web wrote no profile for it, so viewers
had to guess. Photoshop embeds sRGB IEC61966-2.1 for both a new and a
"Don't Color Manage" document when the profile is embedded.

A flat export to a format that carries profiles now tags an untagged
image with the profile assumed for its mode; the codec still leaves it
out when embedding is off, and formats without profiles are unchanged.
Save for Web does the same when Embed Color Profile is on.

Tests that relied on untagged exports carrying no profile (an EXIF
ColorSpace round trip, the Export As proxy-estimate premise, an Affinity
preview fixture, a WebP lossy check) now say so explicitly.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-24 22:40:40 -06:00
Federico Olivares e386cc2ae9 Format the acceptance tests 2026-10-24 02:33:02 -03:00
Federico Olivares aad49d3213 Acceptance tests: drop the BMP header check, its PR left the batch 2026-10-24 00:55:36 -03:00
Federico Olivares 7e4ff3222d Acceptance test: Gaussian Blur radius starts at 0.1 2026-10-24 00:51:31 -03:00
Federico Olivares 35c6c82b2d Add cross-PR acceptance tests for the parity batch 2026-10-24 00:51:31 -03:00
Federico Olivares f863d9bd21 Merge PR #8811 into integration batch 2026-10-24 00:51:27 -03:00
David Mann 2b0deddec2 Merge remote-tracking branch 'origin/main' into integration-batch-7
# Conflicts:
#	crates/ui-egui/src/panels.rs
2026-10-24 00:51:37 +02:00
Perchenko AntonandBrandon Thomas f3941972df fix(actions): preserve scalar Selective Color settings in ATN export (#7681)
Co-authored-by: Brandon Thomas <echelon@gmail.com>
2026-10-23 18:34:54 -04:00
David Mann 9c5a7b907a Merge remote-tracking branch 'origin/main' into integration-batch-7
# Conflicts:
#	crates/ui-egui/src/i18n/tr.tsv
#	docs/ui-parity.md
2026-10-24 00:34:11 +02:00
5750ed40f9 Fix pasted shape group bounds and effects across documents (#8992)
* fix(clipboard): render copied shapes in destination canvas space

When a copied shape group is placed beyond its source canvas, rebuild its vector caches against the destination canvas and pixel format. Previously a centered paste into a larger document could clip the caches to an empty source-sized frame, leaving no transform bounds and incorrect effects until a point edit refreshed each shape.

Restore missing shape caches and refresh source-clipped caches when canvas dimensions change. Cover Paste and Copy to Document, nested group contents, 8/16-bit destinations, rendered cache equality, transform bounds and movement Undo.

* fix(clipboard): measure shape placement before destination clipping

Restore missing source-space shape caches before measuring copy bounds, then adapt and translate for the destination. Re-render empty caches when shapes move back onto the canvas instead of shifting an empty raster.

Add focused integration coverage for 32 combinations of partial and full destination clipping, grouped shapes, missing caches, 8/16-bit output, centered and point placement, and Undo/Redo. Clippy and WASM checks pass.

---------

Co-authored-by: kotulsky <7805437+kotulsky@users.noreply.github.com>
Co-authored-by: Brandon Thomas <echelon@gmail.com>
2026-10-23 17:54:26 -04:00
aiongg 7aaa59712a Merge main into integration-batch-7 2026-10-23 20:37:05 +00:00
aiongg 18f4b5348c Tests: spread three format-by-geometry matrices over the days, and build one precheck state per case
The tilt-shift reference tests and tiled_clear_and_fill_match_one_pass_over_the_area cross pixel
formats or channel counts with independent geometry and fill settings. Each run takes a quarter of
the combinations through Sweep, a different quarter each UTC day.

precheck_agrees_with_execute built all five document states for every call and kept one; it now
builds only the state the call needs.
2026-10-23 08:30:48 +00:00
Gert Pellin 7c180529d6 Layers: a new fill or adjustment layer drops the selection it took as mask (#8801)
A new fill or adjustment layer made while something is selected takes the
selection as its layer mask (#1250). In Photoshop the marching ants are then
gone, as after Reveal Selection, which drops the selection since #5251.
PhotoCraft kept it, so the next pixel edit on another layer (Duplicate,
Desaturate, Levels…) only touched the area inside the old selection.

mask_new_layer now clears the selection when it became the layer mask, in
the same history step: one undo removes the layer and brings the selection
back. A path used as vector mask leaves the selection alone, and so does the
Gradient tool's fill layer.

Tests: new_adjustment_and_fill_layers_are_masked_by_the_selection asserts the
selection is gone and comes back on undo, and the new
an_edit_after_a_new_fill_layer_on_a_selection_reaches_the_whole_layer covers
the duplicate-and-desaturate case (both fail on main). Tests that deselected
right after a new fill layer no longer need to; tests that need a selection
after one select again.
2026-10-23 10:05:10 +02:00
edea110601 Integration: refuse Paste Into a Lock-All group before pasting (#6352)
With the mask commands now honouring Lock All, Paste Into / Paste Outside with a
child of a Lock-All group active committed edit.paste and then failed on the
refused layer.layerMask.revealAll, leaving an unmasked pasted layer behind an
Err. paste_into now checks the destination group's locks first and refuses
cleanly. Test: mask_lock_all::paste_into_a_lock_all_group_refuses_before_pasting
(layer count and history unchanged). Also moves mask_lock_all.rs into main's
one-binary tests/it layout.

Co-authored-by: osaidahmed <170216456+osaidahmed@users.noreply.github.com>
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-10-22 20:52:59 +02:00
bommerts 7f6aa6f852 Merge upstream consolidated integration test layout 2026-10-22 14:41:05 -04:00
aiongg ffa400c754 Keep tests correct and docs current with one test binary per crate
- 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.
2026-10-22 16:55:34 +00:00
aiongg a6428d3dfb Build each crate's integration tests as one binary
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.
2026-10-22 16:55:34 +00:00