Commit Graph
3232 Commits
Author SHA1 Message Date
David Mann eaddba69bd Merge PR #11249: Actions: load and save Blend If and Gradient Fill layers (#10771) 2026-11-06 15:36:23 +01:00
92de311b2b Actions: keep more Blend If and Gradient Fill forms exact (#11249)
Review fix-ups on #11249 after merging main:
- A midpoint outside 5..95 %, transparency stops out of range or out of
  order, and a Gradient Fill Set without transparency stops stay
  reference-application steps instead of playing clamped or keeping the
  layer's own transparency.
- An inverted Blend If range stays a reference-application step; the
  command rejects it at play.
- The Layer Style dialog's OK records `Lefx` = {Scl} beside Blend If on a
  layer without effects (measured). That Set is still the dialog's one
  step and saves back with the `Lefx` it was recorded with.

Co-Authored-By: NaufalDzz <90562913+NaufalDzz@users.noreply.github.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-06 15:07:32 +01:00
David Mann 72645904c7 Merge remote-tracking branch 'origin/main' into pr-11249
# Conflicts:
#	crates/engine/src/cmd/atn_cmds/steps_make.rs
#	crates/engine/src/cmd/atn_cmds/tests.rs
#	docs/atn.md
2026-11-06 14:55:00 +01:00
David Mann 0b765b2fbe Merge PR #11005: Pen: Make Shape turns the path into a shape layer (#10934) 2026-11-06 14:53:39 +01:00
David Mann ec5eb1e2d0 Merge commit 'refs/fixprs/main' into HEAD 2026-11-06 10:21:59 +01:00
David Mann 7271c8cca7 Merge pull request #3634 from davidmann4/feat/history-snapshot-options
History Options: Automatically Create First Snapshot, New Snapshot When Saving, Show New Snapshot Dialog by Default (#2966)
2026-11-06 10:07:19 +01:00
David Mann 5d10956221 Merge pull request #9261 from davidmann4/fix/image-size-resolution-range
Image Size: refuse a resolution outside 1..30000 ppi instead of clamping it (#9260)
2026-11-06 03:16:20 +01:00
David Mann 03f9f53b13 Merge pull request #9213 from davidmann4/fix/crop-hidden-resample-budget
Crop W x H budgets the hidden pixels it resamples (#9212)
2026-11-06 03:15:14 +01:00
David MannandClaude Opus 5.5 8f7815adc5 Batch 18 (#11295): the shape box names its own step when the transform lands as a job
In the app (background jobs on) edit.transform runs as a job and lands
after app.run returns, so renaming the top History step to "Transform
Path" right away renamed the step before it (the second shape's "New
Shape Layer") and left the scale as "Free Transform". Found in the batch
smoke run; the PR's tests run with background jobs off.

edit.transform now takes stepName "Transform Path" (any other name is
refused, as select.transformSelection's stepName from #11444), and the box
passes it instead of renaming afterwards. New tests: the box with
background jobs on (fails without the fix), and the parameter.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-06 03:01:30 +01:00
David Mann 4c6d5e259a Merge remote-tracking branch 'origin/main' into HEAD
# Conflicts:
#	docs/ui-parity.md
2026-11-06 02:32:59 +01:00
Zinadin ZidanandClaude Opus 5.5 3d60ad01f9 Channels: the panel menu has the channel commands, New Spot Channel among them (#11451) (#11557)
* Channels: the panel menu has the channel commands, New Spot Channel among them (#11451)

The Channels panel's menu had no channel items, so New Spot Channel... and
the rest could only be reached from a row's right-click menu. It now lists
New Channel..., Duplicate Channel, Delete Channel, New Spot Channel...,
Merge Spot Channel, Channel Options..., Split Channels and Merge
Channels..., each acting on the targeted channel and greyed where it can't:
Duplicate takes a single colour, alpha or spot channel, the composite of a
one-channel image, the Quick Mask or a targeted layer mask. Channel
Options... in Quick Mask mode now opens the targeted alpha or spot
channel's options, not Quick Mask Options. The panel menus run their
commands through the menu bar's enabled rules, so the Layers panel menu
greys its dialog items the same way too. The labels are in every catalog.

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

* Channels: New Channel... and Merge Channels... ask first, from the panel and row menus (#11451)

The panel menu's Merge Channels... ran channel.merge with no parameters, and
the engine merged the first grayscale documents into RGB and closed them with
no choice; New Channel... added a channel straight away. Both now open a
dialog when called without parameters, as New Spot Channel... and Channel
Options... already did: New Channel... reuses the Channel Options dialog
(name, colour indicates, colour and opacity), and a new Merge Channels
dialog picks the mode and the open grayscale document for each channel. The
channel rows' menu had the same two items and goes through the same path.
With parameters (scripts, the New button) both still run at once.

The Merge Spot Channel test now checks pixels, so a delete couldn't pass it.

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

* Channels: Merge Channels waits for three grayscale documents and the dialog offers only modes they fill (#11451)

With two grayscale documents open, Merge Channels... was enabled and its
dialog repeated a document for the third channel; OK then failed in the
engine and the dialog closed. The engine's rule now needs three documents
of one size (no mode merges fewer), so the menus grey the item below that.
The dialog offers only the modes the open documents can fill (RGB and Lab
from three, CMYK from four), greys OK with the reason while the picks
repeat, keeps itself open when OK is refused, and comes back with the
engine's message if the merge itself fails.

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

* Channels: the Merge Channels dialog keeps its picks by document id (#11451)

The dialog kept the picked source documents by tab index, and background
work can still run while it is open, so a document moved or closed in the
meantime could leave another one at a picked index; OK would then merge,
and close, the wrong document. The picks are now document ids, resolved to
the current tabs when OK is pressed: a closed pick, or one that is no
longer a grayscale document of the same size, is refused with the reason
and the dialog stays open.

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

---------

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-06 02:20:08 +01:00
David Mann cc361c306d Merge commit '9bef3b2dc8b84c1b6cefc8ce0dc2b01d089cd9e8' into HEAD
# Conflicts:
#	crates/engine/Cargo.toml
#	crates/engine/src/prefs.rs
#	docs/ui-parity.md
2026-11-06 02:16:44 +01:00
David Mann 3af79c0cb7 Merge main 9b02eac2c4 into integration batch 18 2026-11-06 02:14:46 +01:00
David MannandClaude Opus 5.5 88704d2350 Merge fix-up: the turned crop's hidden pixels after Layer 0 replaces the Background
Since #10867 the non-deleting crop turns a 2000 px layer to 2001 px bounds, so
the budget refusal names 200100 x 200100 px.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-06 02:06:41 +01:00
David Mann 4b61c8ef34 Merge commit '9bef3b2dc8b84c1b6cefc8ce0dc2b01d089cd9e8' into HEAD
# Conflicts:
#	crates/algo/src/resample/tests.rs
2026-11-06 01:58:38 +01:00
David Mann 9bef3b2dc8 Merge pull request #6088 from davidmann4/fix/snapshot-followups
History snapshots: follow-ups to #3567 (#6087)
2026-11-06 01:32:54 +01:00
David MannandClaude Opus 5.5 6e42bc901e Batch 18 (#11458): a guarded label lookup in the Content-Aware crop donor check
The donor check indexed the label buffer directly; the bounds are inside
the canvas today, but a lookup that is out of range now counts as no donor
instead of panicking.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-06 00:53:47 +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 09e44e022c Merge PR #11497: Menus: View › Guides submenu in the reference's order, with Edit Selected Guides and Clear Selected Guides (#11400)
# Conflicts:
#	crates/ui-egui/src/lib.rs
#	docs/parity-checklist.md
#	docs/scorecard.md
2026-11-06 00:29:28 +01:00
David Mann d43d1c6d0a Merge PR #11443: Move tool: History names its steps Move, Nudge and Duplicate as the reference does (#11383)
# Conflicts:
#	crates/ui-egui/src/lib.rs
2026-11-06 00:29:07 +01:00
David Mann e932db7c69 Merge PR #11494: Menus: Edit › Preferences says Plugins… and Camera Raw… and has the reference's separators (#11398)
# Conflicts:
#	crates/ui-egui/src/lib.rs
2026-11-06 00:29:01 +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
David Mann a77d7d6b53 Merge PR #11503: Layer > Smart Objects says Reveal in Explorer, Embed Linked and Embed All Linked, in the reference order, without the Warp rows (#11401) 2026-11-06 00:28:25 +01:00
David Mann c581cd57c2 Merge PR #11455: Menus: Combine Shapes, Open Recent, Arrange and Proof Setup items carry the reference's names (#11399) 2026-11-06 00:28:19 +01:00
David Mann be08f07614 Merge PR #11453: Polygonal Lasso: History step "Polygonal Lasso" and Shift diagonals snap to the shorter offset (#11393) 2026-11-06 00:28:16 +01:00
David Mann d6d14dea8f Merge PR #11496: Artboard tool: the first board drawn in a document takes the Background in as "Layer 0" (#11387) 2026-11-06 00:28:10 +01:00
David Mann 45c797b85b Merge PR #11521: Shift+letter cycles a tool group in toolbar order and the anchor point tools no longer take the letter P (#11376) 2026-11-06 00:28:07 +01:00
David Mann fd69404ea5 Merge PR #11535: The healing slot starts on Spot Healing Brush and the brush slot lists Color Replacement before Mixer Brush (#11375) 2026-11-06 00:28:04 +01:00
David Mann 6ff31fa4f8 Merge PR #11437: Shortcuts: Ctrl+/ opens Layer › Lock Layers…, shown in the Layer menu and the Layers panel menu (#11397) 2026-11-06 00:27:52 +01:00
David Mann 9beb5a0cba Merge PR #11444: Selection tools: dragging the outline records Move Selection, as the reference does (#11390) 2026-11-06 00:27:46 +01:00
David Mann 9f88425d83 Merge PR #11456: Shortcuts: Ctrl+Alt+0 also zooms to 100% and F9 also opens Actions, the reference's second keys (#11396) 2026-11-06 00:27:40 +01:00
e4e2607c85 Preview spot inks over the RGB/CMYK composite as an overprint (#10276)
* PSD: a flat file's named extra channels are not its transparency

A PSD without layer records had its first extra channel read as the merged
transparency, so the first spot or alpha channel of a flattened document
vanished and its ink punched a transparent hole in the image (psd-tools
cmyk-alpha-spot.psd, cmyk-spot.psd). When the alpha channel names (1045, or
1006 when 1045 is missing) name every extra channel and the first is not
"Transparency", there is none. The import also reads channel names from
1006 when 1045 is missing, and a spot whose DisplayInfo colour space is
unknown takes its ink from the Alternate Spot Colors (1067).

* channel_map: read 1067 records with as_chunks

* Preview spot inks over the RGB/CMYK composite as an overprint

Visible spot channels of RGB and CMYK documents print over the canvas
composite with the Multichannel ink formula, in Channels panel order,
instead of a tint; alpha channels and Quick Mask still tint over them.
Spot channels open visible, and the PSD thumbnail (1036) prints them.
flatten, export and the merged image stay free of spot inks. Ink
colours go through the document's CMYK profile, and into an RGB
document's own profile.

Refs #10249

* channel_view: test solidity over a gray picture

* Thumbnail: matte only inked pixels; test the merged planes directly

Review feedback on #10276: pixels without spot ink keep their
transparency in the thumbnail buffer, and the thumbnail test checks the
saved merged process planes instead of a re-imported flatten.

* Thumbnail spot inks: sample big boxes on a 4x4 grid

Review feedback on #10276: reducing each spot channel no longer scans the
whole document; a thumbnail pixel averages at most 4x4 samples, so a huge
document costs about h x 4 row reads per spot.

* Spot overlay: composite a proxy for the first draw of big documents

The overprint needs the composite under the inks. Rendering the full-resolution
composite to sample every factor-th pixel cost ~1 s for a 24 MP CMYK document.
A proxy document picks the same pixels (nearest-neighbour downsample) for about
1/factor² of the work. Documents with layer effects, vector masks or feathered
masks keep the full-resolution path.

* Proxy: downsample rows in parallel; the overlay proxy leaves out unused channels

The spot overlay's proxy shrank the spot channels, selection and Quick Mask that an
RGB or CMYK composite never reads: about two thirds of its cost. downsample now
reads its rows in parallel, 64 at a time.

* Spot overlay proxy: keep the full-resolution composite under uncached pattern fills

A pattern fill renders its tiles at their own size on a proxy, so the proxy would
sample other pattern pixels. Unless the fill's matching cache is shrunk with the
proxy, use the full-resolution composite.

* Spot overlay proxy: hidden layers don't decide the composite path

Hidden layers and the contents of hidden groups are not composited, so a hidden
feathered mask or pattern fill no longer forces the full-resolution composite
(the same rule as proxy_faithful).

* Update crates/compose/src/multichannel.rs

Co-authored-by: aiongg <67543387+aiongg@users.noreply.github.com>

* Spot overlay: only inked pixels replace the canvas; inks through the display transform

Where every visible spot is 0 the overlay stays transparent, so the normal
canvas (display LUT, proofing, 32-bit preview, full resolution) shows; an
empty spot renders no composite. Printed pixels go through the canvas
display transform (sRGB encoding of linear composites, monitor profile,
Proof Colors), keyed into the texture. Ink colours come from one InkSpace
per render: the document's profiles are parsed once, not per spot per
damage rect.

* test(compose): cover hidden spot preview and document approximation

* fix(ui-egui): preserve solo spot channel display when process colours are hidden

Clicking a spot channel row in the Channels panel targets the channel alone
and hides all process colour channels (`vis == 0`). Overprint inks were
previously extracted from `overlays` before checking `vis == 0`, leaving
`overlays` empty and causing the solo channel view to fall back to an active
layer mask (or return `None` if the layer had no mask).

Keep the spot channel in `overlays` so `vis == 0` can select it as the solo
grayscale picture, separating overprint inks only when process colours are
visible (`vis > 0`).

Adds a regression test targeting an existing spot channel in RGB and CMYK on
an unmasked layer and editing it.

* channel_view: document the spot overprint preview as an approximation

* test(ui-egui): solo spot channel wins over an unrelated layer mask

* channel_view: sRGB spot inks, lazy display transform, inked-area composite

Review fixes for the spot overprint preview:

- RGB spot colours are sRGB, like the CMYK, Lab and gray ones: InkSpace now
  converts them into an RGB document's own profile too. In a linear (EXR/HDR),
  Adobe RGB or ProPhoto document an RGB ink showed the wrong colour (brightened
  in a linear one once the canvas sRGB-encoded it).
- The display transform is looked up only when the view is redrawn and inks
  are shown, not every frame before the cache check; display_transform_for
  borrows the proof state (proof_ref) instead of cloning a default CMYK profile.
- Visible spot channels without ink, with nothing else shown, draw no texture:
  render returns None and ensure remembers it, instead of uploading an
  all-transparent document-sized texture. A damage re-render that finds nothing
  clears its rect, so erased ink never lingers.
- Printing a few inked pixels composites only their bounding box when that is
  cheaper than the whole (proxy) composite, and a cached composite is borrowed
  instead of copied.

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

---------

Co-authored-by: aiongg <67543387+aiongg@users.noreply.github.com>
Co-authored-by: FredApps <153173854+FredApps@users.noreply.github.com>
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-05 23:34:12 +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
yoriswind 3ba306e132 docs(crop): clarify content-aware fill targets 2026-11-05 13:32:51 -08: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
a3c0b7258c Select Subject: keep parts that run off the image edge and detached parts near the subject (#10160)
* Select Subject: keep parts that run off the image edge and detached parts near the subject

The backdrop pass trained its colour model on the whole frame outside the first
result's box and fixed it as background, so a subject cut off by the edge (a
portrait's torso) taught the backdrop its own colour and was dropped. Frame
pixels of a side that the rest of the frame cannot explain are now left out of
the model and left to the cut; a floor or table running up the sides stays
backdrop.

The first pass also dropped every part under a fifth of the largest, losing a
head behind a thin neck or a tail. Non-speck parts next to the main one are kept.

* Select Subject: stray edge pixels stay backdrop; cache test checks the recompute

A lone odd pixel in the frame (noise, a dead pixel) is not the subject running
out of the picture: only patches of at least half a frame square are released.

The #2865 cache test asserted a bit-identical selection after painting a corner
pixel. One changed pixel can move the cut's edge slightly (the original
algorithm already did on other noise), so it now checks that the mask was
recomputed and still selects the subject.

* Test subject selection against edge and nearby clutter

* Keep narrow edge clutter out of subject selection

* Preserve cropped subject components at image edges

* Keep edge poles and diagonal streaks out of subject selection

A detached part counted as near the subject when its bounding box
overlapped the main part's margin box. A thin diagonal streak from a
corner (a cable, a rail) has a box that reaches the margin without any
of its pixels doing so, and was kept as part of the subject. A part is
now near only when some of its pixels lie inside the margin box.

A patch that runs along an image edge for two frame widths was released
from the backdrop model whatever its colour, so a pole or door frame
along a side edge next to the subject's shoulder was left to the cut,
which could grow the selection along it. A wide patch is now released
only where its extent along the edge overlaps the first-pass blob's
margin box. Beside the subject that box is cut out of the frame, so the
pole only remains above or below it and is not released, while a torso
cut off below a head (the case the release exists for) still lies under
the head's box. Requiring the subject's colour for wide patches too, or
requiring them to touch the box, would drop exactly that torso: its
colour is not in the head-only blob and it is far from the head's box.

The margin box computation moves into margin_box so subject_at_edge
and its tests share it. Tests pin the diagonal streak, the pole beside
the shoulder (on all four edges, with the cropped torso still
released) and clutter that is too far or too small.

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:32 +01:00
fcb4f4e494 Grow and Similar: budget the colour index and report allocation failures (#9825)
* Select: Grow and Similar index colours under a byte budget and fail instead of aborting

ColorMatcher::new returns a Result, the colour tree is a flat node array reserved at its exact size, and Grow/Similar's mask and frontier stack use fallible allocation so a failure keeps the selection.

* Select: a 1 GiB colour index on 32-bit, and Sample All Layers reports a failed composite

The web build's 256 MiB index budget (about 4.2M colours) refused Grow and
Similar on a noisy 24 MP photo whose index fits easily; 1 GiB admits every
distinct colour of such a photo. The 64-byte charge per colour is an average,
not a worst case, so the comment now says so, and the point list the tree keeps
gives back its doubling slack once the dedupe set is gone.

Sample All Layers renders the composite inside compose, so the selection tools
now pass an allocation checkpoint before it, like the other commands that hand
big work to another crate, and a failure there keeps the selection.

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:03:48 +01:00
yoriswind 8bc5399652 Keep unknown Stack Mode None descriptors inert 2026-11-05 12:57:30 -08:00
Trevor BrownandClaude Fable 5.1 87062ef0c0 The healing slot starts on Spot Healing Brush and the brush slot lists Color Replacement before Mixer Brush (#11375)
TOOL_SECTIONS and Tool::ALL now order the healing slot as Spot Healing, Remove, Healing, Patch, Content-Aware Move, Red Eye and the brush slot as Brush, Pencil, Color Replacement, Mixer Brush. A fresh healing slot shows Spot Healing Brush and J selects it.

Existing tests that expected the old orders (B and J cycles, the brush flyout click, the shortcut override test) are updated. The engine's shortcut table keeps the same order as Tool::ALL, so its two pairs are swapped to match.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2026-11-05 14:12:50 -05:00
Trevor BrownandClaude Fable 5.1 b9cf495f57 Embed All Linked: tests check each object keeps its own linked file; regenerate the scorecard (#11401)
The Embed All Linked test now gives every fixture a different fill and asserts each embedded source carries its own linked file name and bytes, and leaves the already embedded object alone. docs/scorecard.md is regenerated with cargo xtask scorecard after the parity checklist.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2026-11-05 14:00:46 -05:00
Trevor BrownandClaude Fable 5.1 4083cb6cf4 Artboard first-board test: the Background's pixels travel with it and come back on undo (#11387)
The first-board test now samples pixels as well as names and locks: Layer 0 holds the Background's white after the move, Red keeps its red, and the Background restored by undo holds the same white. This catches a regression that swaps in an empty unlocked layer named "Layer 0".

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2026-11-05 13:06:47 -05:00
Trevor BrownandClaude Fable 5.1 8d33de79b0 Shift+letter cycles a tool group in toolbar order and the anchor point tools no longer take the letter P (#11376)
The group Shift+letter steps through is now sorted by position in TOOL_SECTIONS, so it follows the flyout instead of Tool::ALL. Add Anchor Point, Delete Anchor Point and Convert Point have no default letter, so P cycles only Pen, Freeform Pen and Curvature Pen.

The engine's shortcut table lists the three anchor point tools without a key to match, and two tests that expected the letter P for them are updated.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2026-11-05 12:53:30 -05:00
Trevor BrownandClaude Fable 5.1 14d9de4f86 Merge upstream/main into fix/smart-objects-menu-11401
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2026-11-05 12:42:11 -05:00
Trevor BrownandClaude Fable 5.1 712da4f05e Layer > Smart Objects says Reveal in Explorer, Embed Linked and Embed All Linked, in the reference order, without the Warp rows (#11401)
Windows now reads "Reveal in Explorer", Convert to Embedded is "Embed Linked", and a new Embed All Linked command (layer.smartObjects.embedAllLinked) embeds every linked smart object of the document in one history step, refusing the whole step with an error that names a missing file. The three Warp rows leave the submenu; their commands stay registered for scripts and the Edit menu.

The menu parity floor drops from 660 to 658 because three live catalog rows were removed and one added.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2026-11-05 12:20:09 -05: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
David Mann 405b56c3c2 Merge pull request #11320 from davidmann4/integration-batch-17
Integration batch 17: 14 epic PRs
2026-11-05 18:02:17 +01:00
yoriswind 1a68a20c1c fix(actions): match the fixed-width pixel unit array 2026-11-05 08:50:56 -08:00