Commit Graph
471 Commits
Author SHA1 Message Date
Hector Villarreal 60e45bb926 Color Replacement: refuse RGB/32, Grayscale, Bitmap and Indexed documents (#11923)
paint.colorReplacement and its live stroke refuse the modes and depths
the reference application refuses, with its words: 32 bit per channel
RGB, Grayscale and Bitmap (only full color modes), and Indexed Color.
Nothing is painted and no history is added. RGB/8, RGB/16, CMYK and Lab
paint as before; cases that weren't measured keep today's behaviour.
2026-11-07 21:00:56 -06:00
yoriswind 99653a4626 fix(actions): preserve the Equalize dialog on recorded playback 2026-11-07 10:24:40 -08:00
Trevor Brown 5c49acf7c0 Merge remote-tracking branch 'upstream/main' into fix/magnetic-lasso-composite-11686 2026-11-07 09:39:38 -05:00
David Mann e18900e5fb Merge pull request #10443 from hectorviov/fix/replace-color-add-subtract
Replace Color: Add to Sample and Subtract from Sample (#6280)
2026-11-07 14:44:38 +01:00
yoriswind 2e210c0aae Scope Actions tool recording guard to the active take 2026-11-06 13:40:05 -08:00
Trevor BrownandClaude Fable 5.1 3014737225 Magnetic Lasso: tests for following the visible composite (#11686)
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2026-11-06 15:49:58 -05:00
yoriswind 5f480238d0 Reconcile five Classic Actions commands with current main 2026-11-06 09:29:48 -08:00
FredApps aee46a2fc6 Merge remote-tracking branch 'origin/main' into fix/10164-final-prune-u2 2026-11-06 17:28:48 +01:00
David Mann ec5eb1e2d0 Merge commit 'refs/fixprs/main' into HEAD 2026-11-06 10:21:59 +01:00
Khawar Zamman Wani 123b2032c4 Web build: Place runs a slice per frame, so a large PNG no longer freezes the page (#10164, rebased onto main)
The remaining commits of the stack, applied on top of the squash-merged #10147:

- PNG decodes in resumable steps of rows (PngDecodeRows, shared with decode_rows)
- PNG imports as a document in resumable steps (io PngImport, text metadata and
  document build in bounded steps)
- Jobs can run a slice at a time where there are no threads (StepWork,
  cooperative sessions, Session::poll_jobs)
- Place decodes, flattens and resizes in slices (PlaceSteps), reading a path
  and keeping the frame, memory gate and typed memory-budget errors main has
- The web build places dropped and picked files a slice per frame, with a
  bounded place inbox
- A measurement of the stepped place; docs for jobs without threads
2026-11-06 08:51:17 +05:30
Hector VillarrealandClaude Opus 5.5 be4a915161 Replace Color: Lab bounds refuse subtracted samples; panic_hunt covers them (tanish2k09 review)
Main's Lab bounds (#10765) refuse RGB samples, subtractPoints and order
included. A test pins that for subtractPoints and order, and panic_hunt
now sends malformed and far-off subtractPoints, points and click orders,
also with Lab bounds, to every command.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-05 21:15:00 -06:00
David MannandClaude Opus 5.5 99b93f82da Batch 18 (CodeRabbit nitpicks): crop test checks the active layer's covered pixels; Esc test goes through the shortcut handler
- #11458: the straight-crop rows also check that the active layer's pixels
  inside the old canvas are the plain crop's, so a fill that recolours them
  fails.
- #11295: the Esc case sends the key through shortcuts::handle instead of
  calling cancel directly.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-06 04:04:26 +01:00
yoriswind d58701ddff Merge current main into the Actions preservation candidate 2026-11-05 18:31:30 -08:00
David Mann 3af79c0cb7 Merge main 9b02eac2c4 into integration batch 18 2026-11-06 02:14:46 +01:00
yoriswind 84c41c5f10 Limit Actions panel changes to the five reviewed commands 2026-11-05 17:13:15 -08:00
David Mann d563b3100b Merge commit '9bef3b2dc8b84c1b6cefc8ce0dc2b01d089cd9e8' into HEAD
# Conflicts:
#	crates/engine/tests/it/svg_smart_object.rs
2026-11-06 01:46:48 +01:00
e59dcc9c44 GPU plan: cache adjustment LUT rows between plans (#11498) (#11539)
* GPU plan: cache adjustment LUT rows between plans (#11498)

adjustment_program rebuilt the Levels/Curves tone tables on every plan
(twice per refresh), about 1-2 ms per Levels layer each time. Keep the
rows of Levels, Curves, Color Balance and Hue/Saturation ranges in a
32-entry most-recently-used cache (photocraft_compose::lut_rows, at
most 32 x 3 x 16 KiB = 1.5 MiB), built outside the lock.

Entries are keyed by a u64 hash of exactly what each row builder reads,
fields destructured in full and floats by bits (-0.0 and 0.0 stay
apart). Hue/Saturation master sliders don't add entries; Hue/Sat ranges
and Color Balance don't depend on depth. Edit > Purge > All drops the
cache, and a filled cache enables it.

Release, per call: Levels 8/16-bit 1.10 -> 0.003-0.018 ms, Curves
0.16 -> 0.018, Color Balance 0.11 -> 0.001-0.015, Hue/Sat ranges
0.05 -> 0.008-0.018. The tables are identical, so rendering is unchanged.

Tests: cached rows match fresh tables bit for bit at 8/16/32-bit, hits
and LRU eviction on a local cache, key coverage, and Purge All
releasing the cache.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* compose: rustfmt the LUT row cache tests (#11498)

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* GPU plan: test that plans read cached LUT rows and that curve points key them (#11498)

Plant rows no builder makes under a fresh key and check the planner returns
them, and check that moving a Curves point changes its key (review on #11539).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* compose: prevent in-flight LUT builds from undoing purge

---------

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Co-authored-by: FredApps <FredApps@users.noreply.github.com>
2026-11-06 00:32:49 +01:00
David Mann 1d053f5c2c Merge PR #11495: Actions: play recorded Stack Modes and Video Frame events 2026-11-06 00:28:47 +01:00
David Mann 1d5c69dda4 Merge PR #11458: Crop: fill the active layer and Background with Content-Aware Fill 2026-11-06 00:28:41 +01:00
David Mann f96e77e794 Merge PR #11553: Actions: replay recorded Perspective Warp steps 2026-11-06 00:28:38 +01:00
e55e2f3577 Place decodes and fits in the background: dropping a large file no longer freezes the window (#10147)
* Place decodes, fits and renders in a background job

File > Place Embedded / Linked started with Session::start, and
Session::start_place_bytes for bytes with no path, now run the decode,
flatten, resize and SVG render on a worker (prepare_place) and insert the
layer when the job is applied (apply_place). Inline callers (execute, the
CLI, headless MCP, actions) behave and return exactly as before. Part of
#10031.

* Dropped and placed files no longer freeze the window on desktop

With background jobs on, File > Place and files dropped on the canvas start
the place as a job (JobsUi.place). Free Transform begins when the layer has
landed, a failed or cancelled place is reported and the drop queue goes on,
and a drop that arrives meanwhile waits for it. The place shows its progress
in the status bar instead of the modal dialog, so the view stays usable.
Part of #10031.

* Tests: compare place job results without the layer id

Layer ids come from a counter shared by every session, so a job and an inline place in two sessions never get the same one.

* Dropped files stay with the document they were dropped on

Each queued canvas drop now carries its document's id. A file is placed
only while that document is active: files dropped on a document in
another tab wait until it is back in view, and are discarded with a
message when it is closed. Web drops, read by the shell after the drop,
go to the document the drop landed on.

Before, the next queued file went into whichever document was active
when the previous place finished, failed or was cancelled, in the
background and inline paths alike.

* Remove the unused file_open import

* fix: address background placement review feedback

* fix: preserve drop targets and interrupt placement pixel stages

* fix: qualify placement inbox document identity

* test: wait for background placement in app drop regression

* fix: retain queued drops while external document jobs run

* test: clone rendered pixels from drop-managed buffers

* test: cover band rows channels and queued-drop resumption

* Place decodes and fits in the background: dropping a large file no longer freezes the window

Placing a file decoded, flattened and resized on the UI thread; a large PNG froze the
window for seconds (#10031). `place_bytes` is split into a session read, `prepare_place`
(decode, flatten, fit, translate, SVG render at placement scale) and `apply_place`, and
runs through `jobs::run`, so File › Place and canvas drops run on a worker with progress
and cancel. The window follows the job: status-bar progress, Free Transform when the layer
lands, and drops queued per target document.

Review fixes on the way:
- a path and byte drop queue entry records its target DocId, so files dropped on one
  document never land in another after a tab switch, and closed documents discard theirs;
- the closed-document notice stays up for a frame instead of being cleared by the next
  queued placement;
- the queue is bounded (32 files / 256 MiB) so drops waiting on another tab cannot grow it
  without limit;
- flatten and resize stop at the job's interrupt, and a place skips the modal progress
  dialog only when the status bar actually shows its progress;
- the placement's `doc` is kept only for SVG input, through the importer's `is_svg_input`;
- the interrupted-composite test uses an opaque fixture that varies by row and channel, and
  checks the composite against the layer's stored pixels.

* Place reads its file in the job, and refuses a huge canvas as before

file.placeEmbedded and file.placeLinked read the whole file in the
command before the job started, so a path dropped on the canvas or
picked in File > Place still blocked the window for the read. The place
now carries the path and reads it in prepare_place: on the job's worker
in the background, on the calling thread inline (wasm, background jobs
off), with the same read_file limit and error text. Whether the source
is a vector one is decided once it has been read.

The job's flatten check returned Other("the document canvas is too large
to flatten"), where the place used to give flattened_checked's
BadParams for file.place naming the canvas size. Both now share
check_flattenable, so the old error type and message are back.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* Path drops aren't capped, and a discarded drop wakes the queue

The 32-file cap on the drop queue counted path drops, which hold no
file contents while they wait, so dropping 40 files on the canvas
placed 32 and refused 8 where main placed them all. The count cap now
applies to browser drops only (each holds its whole file); the byte
budget still bounds those.

When place_next_dropped discarded a closed document's files it returned
without asking for another frame, so another document's queued files
waited for the next input event. It now requests a repaint when files
remain, keeping the one frame that shows the notice.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

---------

Co-authored-by: FredApps <153173854+FredApps@users.noreply.github.com>
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-05 22:49:52 +01:00
6326117c53 Web build: big operations that won't fit fail with an error instead of trapping, and a trap says so (#10129)
* Raster: ask the allocator whether a big operation fits; PSD save reserves its whole output

New photocraft_raster::headroom: ensure(need) tries to reserve the need plus a margin and frees it
at once (web only; native always passes), below(bytes) tells whether less than that can be
allocated, and simulate_available fakes the answer for tests and diagnostics.

PsdFile::to_bytes reserved only the pixel data, so embedded smart-object data and other tagged
blocks grew the output by doubling, which on the web is one unrecoverable huge realloc. It now
reserves the exact encoded length.

* Big operations that won't fit in memory fail with an error instead of trapping

Place, Image Size, a smart-object re-render and Save now ask the allocator whether their estimated
need can be allocated before they start (photocraft_engine::memory_gate, estimates per operation,
photocraft_io::estimate_export_bytes for saves). A refusal changes nothing and says what the
operation needed and what to do (save, Edit > Purge > All, close documents).

A standing notice warns when less than 512 MiB can be allocated, with hysteresis, so the user can
save before anything traps. Notices can now be sticky, which keeps them past the auto-hide delay.

* Web build: a trap says so instead of freezing the page, and ?free-mb simulates low memory

index.html holds a hidden, static 'PhotoCraft stopped' message with a Reload link. web.rs reveals
it when the module traps (the window error event, or an unhandled rejection from a spawned
future), tells running out of memory from an internal error by the panic message a chained panic
hook kept, and skips the Leave site prompt once stopped. ?free-mb=N makes the memory gates and the
low-memory warning see only N MiB, for checking them in a browser.

* Docs: web build memory handling in the gaps list and the parity assessment

* Engine: use is_multiple_of in the megabyte formatter

* Cargo.lock: the web shell depends on photocraft-raster

* Web boot page: carry the stopped message too, as the test that keeps the two pages equal requires

* Save check counts one copy of the document, and runs once per save

The encoded file's buffer is reserved fallibly, so a save it doesn't fit
in fails with an error instead of trapping; counting it as well refused
saves that would have fitted. On the background-save path the check ran
in write_document and again in start_save, which other callers reach
directly; it now runs only in start_save there.

* Review fixes: warning check allocates nothing, PSD merged image counted, automation saves checked

The low-memory warning's periodic check reads the size of linear memory
instead of reserving a block: wasm memory never shrinks, so a trial every
few seconds raised the high-water mark by up to 768 MiB for nothing. The
operation checks keep their trial reservation.

PSD and PSB saves always build a canvas-sized merged image (a placeholder
without maximum compatibility) and hold it raw and encoded; the save
estimate now counts it. The synchronous automation save checks memory
before encoding, as File > Save does, and its test asserts the memory
error.

The trap listeners go in before the app is built, so a trap while
building it shows the stopped message too, and a memory access out of
bounds reads as unexpected rather than as running out of memory.

* Cover layered PSD preflight limitation

* Fix layered PSD test iteration

* Docs: layered PSD preflight limitation in the gaps list

* Web OOM gate: gate direct exports, truthful advice for unbounded PSD sources (#10024)

* Web OOM gate: estimate layered PSDs per layer instead of refusing every real PSD

The PSD check admitted a file only when its color-mode data, image
resources and layer section were all empty. Every Photoshop file has
image resources (at least the resolution), so on the web nearly every
real PSD was refused for Place, PSD smart objects could not render,
and Image Size was refused for documents holding one, whatever the
free memory (u64::MAX never fits).

The color-mode data and image resources hold no pixels, so they are
now only skipped. A layered file reads its layer count from the layer
info (signed, absolute value) and adds a canvas-sized surface and mask
per layer to the flat estimate. Files that are not 8-bit RGB or
grayscale, keep their layers in a Layr tagged block, or whose sections
can't be read stay unbounded and refused, as before.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* Web stopped page: match trap messages ignoring case

Safari reports the allocation-failure abort as "Unreachable code
should not be executed" and Firefox says "out of memory", which the
case-sensitive matching missed: the trap was not recognised, or was
shown as an unexpected stop instead of running out of memory.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* Export estimate: count the merged image OpenRaster writes

The ORA writer keeps the layers but also flattens the document for
mergedimage.png, a 16 B/px composite of the canvas, which the
estimate left out.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

---------

Co-authored-by: FredApps <153173854+FredApps@users.noreply.github.com>
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-05 22:04:59 +01:00
yoriswind 8bc5399652 Keep unknown Stack Mode None descriptors inert 2026-11-05 12:57:30 -08:00
yoriswind 19ac2452a0 Merge Batch 17 while preserving Crop fill and Artboard sizing 2026-11-05 09:15:36 -08:00
yoriswind 8a126888f3 Merge upstream main after integration batch 17 2026-11-05 09:09:02 -08:00
yoriswind 9f393ebeb5 Require the null class for edited Stack Mode None provenance 2026-11-05 08:50:31 -08:00
yoriswind b6bb43bc19 Handle edited Stack Modes and cap video frame growth 2026-11-05 08:48:40 -08:00
yoriswind fbccaaeabb style(actions): apply Perspective Warp formatter 2026-11-05 08:40:04 -08:00
yoriswind 3feae0e0b6 Actions: map recorded Perspective Warp planes 2026-11-05 08:36:06 -08:00
yoriswind 05d3c0bf27 Support recorded Smart Object Stack Modes and video frame edits 2026-11-05 06:24:06 -08:00
yoriswind 04a3d0d295 fix(crop): bound the widened sampling read 2026-11-05 05:43:56 -08:00
yoriswind add3c72f91 fix(crop): keep Background fill for a donorless active layer 2026-11-05 05:37:55 -08:00
yoriswind 337794eadc test(crop): size turned controls for the minimum warp budget 2026-11-05 05:36:45 -08:00
David Mann f162c19e81 Merge remote-tracking branch 'origin/main' into integration-batch-17
# Conflicts:
#	crates/ui-egui/src/control/dispatch.rs
#	crates/ui-egui/src/control/params.rs
2026-11-05 12:44:02 +01:00
Khawar Zamman Wani e3ecd73569 feat(frame): rework the Frame tool's selection, handles and placement (#8920)
Thanks @wanikhawar — merging the Frame Tool rework.

Summary: Frame Tool selection/handles/placement now distinguish frame vs image (border / click / double-click and Layers thumbnails), with independent transforms, shape variants, Properties/options-bar stroke and fit commands, Convert to Frame, CLI/MCP `layer.frame.*` commands, native `.pcraft` persistence, and PSD export via editable groups + `pcFr` metadata (not Photoshop's native frame type). Tests cover selection, transforms, fitting, locks/hostile geometry, and native/PSD round-trips.
2026-11-05 12:39:02 +01:00
David MannandClaude Opus 5.5 16000b01ef Address CodeRabbit review: batch 17 findings
Engine: droplet dependency capture skips disabled steps; a nested Batch
that was cancelled or could not write its error log fails the caller;
runDroplet forwards overrideOpen; actions.rename follows named Batch
steps; Batch steps inside a script count against MAX_SCRIPT_STEPS;
output claims compare destination identity; Save and Close never
replaces a read-only source. ATN: Md must be a BlnM enumeration,
adjustment-layer names must round-trip, Calculations exports a missing
blending as Multiply, Apply Image refuses unnamed-layer transparency.
Filters: Shape Blur records its resolved geometry, reserves its kernel
memory first, and reports a preparation failure as an error, not a
cancel; Crop keeps an artboard preset when the size is unchanged and
fits the board on a turned crop.

Codecs/cms/algo: zero-axis FITS image HDUs are empty; a GIF walk that
breaks off before the trailer fails closed; two-value .3dl shaper rows
are applied; fragmented custom kernels are refused and their work polls
cancellation.

UI: Conditional branches play through panel playback; paused actions
refuse to resume after being replaced; Playback Options OK validates
its fields; transform double-click ignores a hidden pivot; path boxes
snap to a stationary selection; modal blocking uses the effective tool;
a picker-consumed release clears snap lines and ends a Move drag; the
Crop preset control shows Selection and checks only it; Crop box undo
restores frame_from_selection; Bosnian fill labels.

Tests: timing asserts moved to an ignored test, plus the review's
coverage nitpicks.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-05 11:37:38 +01:00
yoriswind 1de6c9e2c5 Find active layer donors beyond a turned crop corner window 2026-11-05 01:29:48 -08:00
yoriswind 224dcdc936 Fix content-aware crop targets and history labels 2026-11-05 01:29:48 -08:00
David Mann 5385ebd95b Merge PR #10788: Support recorded dialog filters, Calculations and Batch naming
# Conflicts:
#	crates/engine/src/cmd/actions_cmds.rs
#	crates/engine/src/cmd/automate_cmds.rs
#	crates/engine/src/cmd/file_cmds.rs
#	crates/engine/src/cmd/file_cmds/batch_run.rs
#	crates/engine/src/cmd/file_cmds/tests.rs
#	crates/engine/tests/it/main.rs
#	crates/ui-egui/src/panels/actions.rs
2026-11-05 05:19:44 +01:00
30b199f8df Port #8659: keep name, blend mode and clipping on adjustment-layer Make steps
An adjustment layer's Make step now loads its `Nm  `, `Md  ` and `Grup`
keys as the command's `name`, `mode` and `clipToPrevious` params and saves
them back. An unreadable value keeps the step as an unsupported kept step;
`layer.newAdjustmentLayer.*` validates the new params like New Layer and
clips through the clip-group rules. Fixes the remainder of #8384.

Co-Authored-By: bommerts <44821497+bommerts@users.noreply.github.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-05 04:35:32 +01:00
8d34370bcf Port #8111: the Crop tool fits a canvas-sized artboard to its frame
A straight Crop tool crop (image.crop with an explicit rectangle) of a
document whose only artboard covers the whole canvas now fits that board
to the new canvas and clears its size preset. Before, the board kept its
old size and hung past the cropped canvas.

Measured in the reference application: its Crop tool in an artboard
document edits the artboard ("Edit Artboard": the board takes the frame,
content stays where it is), so the visible result matches. Image > Crop
with a selection does nothing there, Image > Trim keeps the board's
size, and a turned crop is impossible with artboards.

Left out of the original PR: the fit on selection crops, Trim and turned
crops (they differ from the reference application), so crop_doc and
Export Layers to Files are unchanged; the printer-query commits
(superseded on main) and the shapes.rs hunk (already on main).

Co-Authored-By: durtyrobbin <76844303+durtyrobbin@users.noreply.github.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-05 04:00:14 +01:00
yoriswind 83861f6862 Preserve DDS subresources and recorded filter representations 2026-11-04 17:12:28 -08:00
yoriswind 067cef6543 fix: preserve DDS slices and recorded Shape Blur metadata 2026-11-04 16:54:35 -08:00
yoriswind a27de5e30d fix: replace stale Smart Filter shape selectors 2026-11-04 16:06:21 -08:00
yoriswind 6c831b7569 fix: retain LUT shapers and resolve edited Shape Blur geometry 2026-11-04 12:34:44 -08:00
yoriswind 1720af269d Merge current main while preserving recorded Batch safeguards 2026-11-04 10:47:01 -08:00
yoriswind 0769620468 Keep unsupported Shape Blur origin in preservation regression 2026-11-04 10:44:31 -08:00
David MannandClaude Opus 5.5 642bcf6229 Fix-up #10986 with #10965: a play that changes nothing adds no history entry
With #10965 an action play is one history entry, and a play that leaves the
document unchanged adds none (#10944). The Current Layer fill from the uniform
layer alone rewrites the same pixels, so the test expects an entry, and checks
undo and redo, only when the document changed.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-04 15:45:35 +01:00
David Mann cb323e9aea Merge PR #10965: Group action playback into one history state 2026-11-04 14:51:18 +01:00
David Mann aac944a315 Merge PR #11223: Actions: map supported classic Gradient recordings
# Conflicts:
#	crates/engine/tests/it/main.rs
2026-11-04 14:51:00 +01:00