Commit Graph
979 Commits
Author SHA1 Message Date
NetterSilverandClaude Haiku 5.5 5a649f8e4e Apply background jobs to the layers they started on (#7620)
Fixes #7366

Co-authored-by: Claude Haiku 5.5 <noreply@anthropic.com>
2026-10-19 14:19:22 -04:00
dfe94d6ab9 Actions: export Box Blur, Surface Blur, Flatten Image and Black & White under ids Photoshop runs (#6330) (#6331)
Save Actions wrote these four steps under the four-char codes BoxB, SrfB,
Fltn and BlnW, which Photoshop does not run: executeAction fails with "the
command is not currently available" (measured in Photoshop on a document
with a filled pixel layer; Fltn resolves to the flatness property, not the
flatten event). The names boxblur, surfaceBlur and blackAndWhite and the
code FltI run.

- Box Blur, Surface Blur and Black & White now export by name
  (boxblur, surfaceBlur, blackAndWhite); Flatten Image exports as FltI.
  The descriptors are unchanged.
- Import accepts the names, FltI and flattenImage, and still accepts the
  old codes (a LEGACY_CODES table consulted only for four-char codes), so
  actions PhotoCraft saved before keep loading.
- Tests: each step exports the new id and imports back to the same command
  and params under the new id, the name and the legacy code; a set mixing
  Photoshop-shaped and legacy steps loads and re-saves with the new ids;
  near-miss ids (case, padding, empty, NULs) stay Photoshop steps; broken
  descriptors don't panic.

Co-authored-by: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Co-authored-by: Brandon Thomas <echelon@gmail.com>
2026-10-19 14:18:18 -04:00
2ef8a9d3f5 Reject exporting a partial Play Action call to .atn (#7602)
Fixes #7186

Co-authored-by: Claude Haiku 5.5 <noreply@anthropic.com>
Co-authored-by: Brandon Thomas <echelon@gmail.com>
2026-10-19 13:45:07 -04:00
Korg1chandBrandon Thomas 990483a95e Actions: Apply Layer Comp is recorded by the comp's name, so it plays in any document (#7181) (#7790)
* fix(engine): record Apply Layer Comp by the comp's name, not its document-local id (#7181)

* test(engine): a recorded Apply Layer Comp applies the comp of that name in another document (#7181)

---------

Co-authored-by: Brandon Thomas <echelon@gmail.com>
2026-10-19 13:44:51 -04:00
FinnNkandClaude Opus 5.5 8c3c3237f4 refactor(engine): drop the second damage pass after single-edit duplicates and pastes (#5880)
Session::edit has reported a new layer's bounds as damage since #3847, so
Duplicate Layer, Duplicate Layers and Paste no longer need to keep the
document from before the edit and work the damage out again with
note_insert. Paste Into still does: it is three edits.

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-19 13:33:06 -04:00
Omar Saidahmed 1c74da7baa keep layer-based slices when duplicating a document (#6133) 2026-10-19 13:32:59 -04:00
David MannandClaude Opus 5.5 4252fb7743 Presets: Migrate Presets reads tool presets like the launch loader (#7375) (#7376)
Migrate Presets deserialized the incoming tool presets all or nothing, so
one damaged entry dropped every good one, and it skipped the checks the
launch loader applies: blank or overlong names were imported and then lost
on restart, and a large file pushed the library past MAX_PRESETS. Read the
list with `presets::tools::load` and stop at MAX_PRESETS.

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-19 13:32:07 -04:00
NetterSilverandClaude Haiku 5.5 fe64e5a2bd Keep arbitrary canvas rotation interpolation in ATN export (#7630)
Fixes #7504

Co-authored-by: Claude Haiku 5.5 <noreply@anthropic.com>
2026-10-19 13:31:49 -04:00
David MannandClaude Opus 5.5 cbe6c4e131 Guides: colours follow Crop, Canvas Size and rotations (#7724) (#7725)
transform_doc_marks moved guide positions but not their colour entries,
which are keyed by exact position: after a crop the coloured guide fell
back to the preference colour and a guide that landed on its old place
took the colour. Map the entries with the same expressions as the
guides (swap orientation on quarter turns, clear on arbitrary angles).

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-19 13:31:41 -04:00
Khawar Zamman WaniandClaude Opus 5.5 81d5c514e2 perf(photomerge): build multi-band bands in bounded batches (#7758) (#7763)
* perf(photomerge): build multi-band bands in bounded batches (#7758)

panorama::multiband built every source's Laplacian and weight pyramids
at once and kept them all until accumulation, so Photomerge's peak
memory grew with the image count (~5 GB for four 24 MP sources). Build
the bands in batches under a 2 GiB estimate (at least one image) and
add each batch before building the next. Each pixel still adds the
bands in image order, so the output is bit-identical.

Planes now move into the pyramids instead of being cloned, and the
coverage and push-pull masks are freed once used: with one image built
at a time those clones were serial passes. seam_blend shifts its ROIs
in place instead of cloning every source for the blend.

Peak during the blend: 4x24 MP 5.04 -> 2.81 GB, 8x12 MP 5.50 -> 2.61 GB,
at +5% time (2x24 MP unchanged). New example bench_multiband measures it.

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

* perf(photomerge): let up to four 24 MP sources build their bands together

A 2 GiB band budget built every 24 MP source alone, which made the usual
2-4 image Photomerge 12-36% slower on a 32-thread desktop. The native
budget is now 5 GiB, so those still build side by side and only larger
stacks are batched. wasm32 has no threads, so it adds each band as soon
as it is built.

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

---------

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-19 13:31:33 -04:00
5b1f937287 Actions: a function key can take Shift and Command (#7646) (#7674)
Photoshop's Action Options pair the function key with Shift and Command
checkboxes. The Actions row menu offered plain F-keys only, and .atn import
dropped any key with a modifier. The row menu gets Shift and Command (Ctrl off
the Mac) toggles, and .atn load and save keep the modifiers.

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Co-authored-by: Brandon Thomas <echelon@gmail.com>
2026-10-19 13:31:26 -04:00
125d15961e Actions: Smart Blur quality uses Photoshop's SBQL/SBQM/SBQH codes (#6312) (#6313)
A Smart Blur step saved to .atn wrote its quality as `Hgh `/`Mdm `/`Low `,
codes Photoshop ignores (it plays them back with its fallback, low
quality), and Photoshop's own `SmBQ` values did not import as a quality.

The quality table now writes `SBQL`/`SBQM`/`SBQH` (enum type `SmBQ`) and
reads both those and the old `Hgh `/`Mdm `/`Low `, so actions PhotoCraft
wrote before still load. Unknown codes stay kept-verbatim Photoshop steps.

Measured in Photoshop by rendering: Smart Blur applied through Action
Manager with each code; SBQH and SBQM each give a distinct image, while
`Hgh `, `Mdm `, `Low `, a nonsense code and SBQL give the identical
(fallback) image. The smart filter Photoshop saves uses the same SmBQ
values (applied through Action Manager to a smart object, saved, filterFX
read back).

Tests: every quality round-trips through .atn as SBQ*; Photoshop-shaped
SmrB steps and the legacy codes import as the right quality; unknown,
wrong-type, NaN and missing values never panic; Smart Blur joins the
export/import round-trip table.

Co-authored-by: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Co-authored-by: Brandon Thomas <echelon@gmail.com>
2026-10-19 13:08:50 -04:00
David MannandClaude Opus 5.5 5c1b9425d1 Gradient Overlay: repaint the whole layer when a stroke moves its frame (#4053) (#5340)
A Gradient Overlay (and gradient strokes, linked patterns, linked bevel
textures) is laid out over the layer's bounds. Painting grows those
bounds, but the stroke commands reported only the stroke as damage, and
the canvas's committed-stroke path repainted only what the live stroke
had not shown. The canvas therefore kept the old gradient around earlier
strokes: one gradient per stroke instead of one over the whole layer.

Session dispatch now grows a command's reported damage by
photocraft_compose::relayout_damage(before, after): the whole composite
of any visible layer whose effects span its frame, when that frame moved.
The canvas adds the same area to a committed live stroke. Strokes inside
the existing bounds and frame-independent effects keep tight damage.

Co-authored-by: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-10-19 13:04:52 -04:00
David MannandClaude Opus 5.5 bc4f2a783d Scripts: Flatten All Layer Effects bakes with the document's Global Light and patterns (#5719) (#5720)
`bake` rendered each layer through `render_layer`, which has no document context (default Global
Light, no patterns, the render rect as canvas). It now uses the new doc-aware `render_layer_in`.

Co-authored-by: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-10-19 13:04:46 -04:00
codexistence 7efaa50455 fix: match Photoshop Gradient Fill scale limits (#6797) 2026-10-19 13:04:20 -04:00
Vikram Bhaskaran 4bf0625b04 Export and import saved tool presets (#6560) (#6889)
* Include tool presets in export and import so crop presets are portable.

* Pass presets_io_body to the harness without a redundant closure (#6560).

CI clippy -D warnings failed on clippy::redundant_closure in the
presets export/import regression.
2026-10-19 13:04:14 -04:00
Omar Saidahmed 6385fced3d refactor: use photocraft-color's srgb transfer functions in algo and proof simulation (#7604)
* share the srgb transfer functions from photocraft-color

* document the touched srgb helpers
2026-10-19 13:03:48 -04:00
David MannandClaude Opus 5.5 baa005d476 Type: formatting commands respect Lock All (#7718) (#7720)
Character (type.setStyle), Warp Text, Anti-Alias, Orientation, OpenType
and Load Default Type Styles all run through with_text_targets, which
never checked Lock All, so they changed a locked type layer and added a
history step. Photoshop refuses all of them ("the layer is locked").
Check every target with check_text_unlocked before the edit, as
type.edit already does.

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-19 13:03:32 -04:00
adbd40ef55 Actions: Photoshop Shadows/Highlights steps play (#5830) (#5854)
Imported actions stopped at Shadows/Highlights ("adaptCorrect has no
PhotoCraft equivalent yet"), although Image > Adjustments > Shadows/Highlights
exists. The step now maps to it both ways: shadowMode/highlightMode (amount,
width, radius), black/white clip, midtone contrast and colour correction,
each read as its char ID or string ID.

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Co-authored-by: Brandon Thomas <echelon@gmail.com>
2026-10-19 13:03:20 -04:00
96c151525a feat(prefs): Show File Name In Title keeps the title bar to the app name (#5037) (#5129)
Co-authored-by: highperfocused <highperfocused@pm.me>
Co-authored-by: Brandon Thomas <echelon@gmail.com>
2026-10-19 13:02:53 -04:00
Tarek MohamedandBrandon Thomas 5c513b8c88 fix(actions): flush pending recorded steps on save (#7193) (#7244)
When actions.save was called while recording was active, it exported only
already-committed action steps, silently omitting any pending steps shown
in the Actions panel.

Call flush_recording at the beginning of actions.save to commit pending
journal entries to the action and advance the recording pointer, ensuring
all displayed recorded steps are saved while allowing ongoing recording to
continue uninterrupted.

Fixes #7193

Co-authored-by: Brandon Thomas <echelon@gmail.com>
2026-10-19 13:02:37 -04:00
640873de7b PNG: keep a stereo file's sTER pairing on save (#6063) (#6285)
* PNG: keep a stereo file's sTER pairing on save (#6063)

Read the sTER chunk (PNG Extensions 1.4.0 §3.6) when a PNG opens, store it
on the document's FileEncoding, and write it back when saving, so a stereo
PNG still says it is a stereo pair. Covers the ordinary and the palette
PNG encoders, including interlaced output.

* io tests: pass the new sTER argument to encode_png_indexed

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

---------

Co-authored-by: highperfocused <highperfocused@pm.me>
Co-authored-by: Brandon Thomas <echelon@gmail.com>
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-19 13:02:18 -04:00
aiongg d95fdf3181 Merge pull request #7569 from aiongg/integration-4
Integration batch: 12 PRs on main
2026-10-18 23:29:05 -04:00
Korg1ch f3fb5781ef Lens Blur: an alpha channel as the depth map source, as in Photoshop (#7400) (#7508)
* fix(filters): Lens Blur reads its depth from an alpha channel, listed in Depth Map › Source as in Photoshop (#7400)

* test(filters): Lens Blur depth from an alpha channel by name or index, its errors, and the Source menu (#7400)
2026-10-18 17:23:32 -04:00
Brian TandClaude Opus 5.5 ca69059dd0 Edit: Copy with no selection keeps fill and adjustment layers (#2344) (#7497)
Copy refused fill and adjustment layers ("the active layer has no
pixels"). With no pixel selection they are now taken whole, like smart
objects, type, shape and group layers, and Paste recreates them in this
or another document. An adjustment layer's clip spans the canvas, so
paste placement and the OS-clipboard mirror stay consistent.

Scorecard UI-217-23 note, gaps.md G9 and target-app-parity.md updated;
the row stays partial (a single pixel layer still copies pixels only).

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-18 17:23:26 -04:00
Nguyễn Minh Long 72a3423a7e Preferences: Enable Gestures off ignores touch-screen pinch and twist (#204) (#7478)
Edit > Preferences > Enhanced Controls > Enable Gestures (enhancedControls.touchGestures)
was saved but read by nothing, so it was hidden from the dialog. Now, with it off, two
fingers on a touch screen no longer zoom the canvas or the Navigator, nor turn the view.
The mouse and trackpad behave as before, including a trackpad pinch or Rotate event that
arrives while fingers rest on the screen.

- wheel_nav: pinch_factor() drops egui's multi-touch zoom when gestures are off and keeps
  the trackpad's own Event::Zoom (Windows' folded Ctrl + wheel pinch included);
  read_with() takes the preference.
- rotate_view: trackpad_delta() only falls back to the multi-touch rotation while gestures
  are on; Rotate events always count.
- canvas, Navigator: pass the preference through.
- prefs: touchGestures leaves HIDDEN_UNTIL_IMPLEMENTED, so the dialog shows it.
- Tests: synthetic two-finger pinch and twist, on and off, and the trackpad events with
  gestures off.
- docs/scorecard.md regenerated: settings that do nothing 26 -> 25 of 168.
2026-10-18 17:23:21 -04:00
Marco Arnulfo 0eb0387d53 Save for Web and Quick Export: "All" keeps the photo's EXIF (fixes #7436) (#7471) 2026-10-18 17:23:10 -04:00
Korg1ch f8e4a1344f Print: the last print's settings survive a relaunch, as in Photoshop (#7410) (#7526)
* fix(print): the last print's settings are kept in the preferences, so Print opens with them after a relaunch (#7410)

* test(print): the last print's settings survive a relaunch for Print and Print One Copy (#7410)
2026-10-18 16:57:24 -04:00
NetterSilverandClaude Haiku 5.5 92721dd8c4 Refuse strict .atn export of plain events whose target is not the layer pixels (#7564)
Fixes #7272

Co-authored-by: Claude Haiku 5.5 <noreply@anthropic.com>
2026-10-18 16:57:19 -04:00
David MannandClaude Opus 5.5 5225b592fe Perspective Crop: draw the crop quad's side of the horizon (#7557) (#7558)
When the crop quad's horizon crossed the layer and most of the layer
(or its centre) lay beyond it, warp picked the source's majority side
and skipped every output pixel, so the crop came out blank. A clip
wholly on one side of the inverse horizon only ever samples that side,
so draw it; a straddling clip keeps the majority rule (#4863).

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-18 16:57:14 -04:00
David MannandClaude Opus 5.5 d77a989ef4 image.crop: accept whole-number float targetWidth/targetHeight (#7551) (#7552)
The target size was read with as_u64 only, so 100.0 (json.dumps of a
float, many agent clients) was refused as not a whole number, while the
crop rectangle and image.imageSize accept it. Fractional, negative,
zero and out-of-range values are still refused.

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-18 16:57:03 -04:00
8ea77bdfd6 Fade: restore uniformly inverted layer masks (#7383) (#7463)
* fix(fade): detect uniform mask changes and blend full canvas

* test(fade): cover uniform mask inversion and Fade across bit depths

* style(fade): keep default-pixel comparison readable

* Format edit_menu_cmds.rs with rustfmt

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

---------

Co-authored-by: Brandon Thomas <echelon@gmail.com>
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-18 16:56:33 -04:00
Hodd 140230b141 Replacing selected type keeps its size, as in Photoshop (#7457)
Typing or pasting over a selection now takes the style of the first selected character instead of the one before it, so retyping a smaller second line no longer takes the first line's size from the line break. Fixes #7413.
2026-10-18 16:56:26 -04:00
Khawar Zamman WaniandClaude Opus 5.5 502d055c6c file.saveACopy refuses the document's own file without "overwrite" (#7437) (#7456)
* file.saveACopy refuses the document's own file without "overwrite" (#7437)

Save a Copy through the engine (control channel, MCP, serve, headless
action playback) silently replaced the open document's own file when
given its path, so a lossy re-encode could not be undone. It now returns
BadParams when `path` is the active document's file (same text, ./ or ..
spelling, hard link, case on Windows via same_file) unless the params
pass "overwrite": true, and nothing is written. The desktop dialog writes
through the shell after the OS confirms a replace, so it is unchanged.

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

* same_file: compare file identity on Windows, so a hard link is the same file

On Windows `same_file` compared canonical path strings, so a hard link to
the input (another name for the same file) passed the new convert guard
without --in-place. It now compares the volume serial number and file index
(the `same-file` crate, already in the dependency tree; std's file_index is
unstable), as unix compares dev and inode, falling back to the
case-insensitive canonical path when a file won't open. The engine's
same_file test now checks hard links on every platform. (Review on #7454.)

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

* saveACopy own-file test: hard link on every platform, doc state unchanged after a refusal

Each refused copy (same path, ./ spelling, hard link) now also checks the
document keeps its path, revision and unsaved state. The hard link is no
longer unix-only: same_file compares file identity on Windows too. (Review
on #7456.)

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

---------

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-18 16:56:21 -04:00
aiongg 1c2193f562 Merge PR #7011: Artboards to Files: paths, notes, slices and marks move with the board (#7010) 2026-10-18 20:33:44 +00:00
aiongg b3b8c7b082 Merge PR #6809: Transform: drop a fill layer's cached render when its frame changes (#6808) 2026-10-18 20:33:44 +00:00
aiongg 4e517c054e Merge PR #6779: Image Size: a gradient fill's centre offset stays a fraction of the frame (#6778) 2026-10-18 20:33:44 +00:00
aiongg 978404424e Merge PR #6769: Presets: refuse renaming a group to a name another group has (#6768) 2026-10-18 20:33:43 +00:00
aiongg 5caeea8c1b Merge PR #6670: fix(engine): refuse a Content-Aware Fill with nothing to sample instead of filling black (#6532) 2026-10-18 20:33:43 +00:00
aiongg 34e5b4b42f Merge PR #5658: Video: Interpret Footage and New Video Layer from File reject an invalid frame rate (#5657) 2026-10-18 20:33:43 +00:00
aiongg baee303af1 Merge PR #5656: Image > Adjustments: locked smart objects refuse new smart filters (#5655) 2026-10-18 20:33:43 +00:00
aiongg fd44b1c873 Merge PR #5620: Smart Filters: deleting the last filter removes its filter mask (#5619) 2026-10-18 20:33:43 +00:00
aiongg a07b87a1af Merge upstream main into integration-2
crates/engine/src/mode_cmds/tests.rs: keeps #7139's opaque-background test and main's full-precision angle test
2026-10-18 15:11:28 +00:00
aiongg 4df773e966 Merge pull request #7405 from aiongg/next
Integration batch: 19 PRs on main
2026-10-18 11:09:40 -04:00
7066497886 Keep fractional translation-only Free Transform on .atn import (#7401)
Fixes #7298

Co-authored-by: Claude Haiku 5.5 <noreply@anthropic.com>
Co-authored-by: Brandon Thomas <echelon@gmail.com>
2026-10-18 10:51:21 -04:00
NetterSilverandClaude Haiku 5.5 713dbbed2f Save Actions ATN files atomically so a failed write keeps the old file (#7403)
Fixes #7290

Co-authored-by: Claude Haiku 5.5 <noreply@anthropic.com>
2026-10-18 10:50:37 -04:00
NetterSilverandClaude Haiku 5.5 80553ecd93 Refuse strict ATN export of Close with an explicit document target (#7393)
Fixes #7299

Co-authored-by: Claude Haiku 5.5 <noreply@anthropic.com>
2026-10-18 10:50:32 -04:00
David MannandClaude Opus 5.5 d459ca7939 Image Rotation: Arbitrary keeps Color Sampler points on their pixel (#7372) (#7373)
Sampler points are pixel indices, but rotate_arbitrary mapped them as the
pixel's top-left corner while the warp moves pixels by their centres, so
after a turn a point sat one pixel off its pixel, or fell off the canvas
(a corner point at 90° or 180°). Map the pixel centre, as #6720 does for
flips and right-angle turns; the turned Crop shares this map.

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-18 10:39:34 -04:00
David MannandClaude Opus 5.5 a4c7b5ef89 Rotate Canvas Arbitrary: keep the angle at full precision, so rotated crops land on the pixel (#7369) (#7370)
rotate_arbitrary read `angle` as f32. rotated_crop sizes its canvas with
rotated_size at f64 and passes the angle through JSON; near a whole-pixel
ceil boundary the f32 angle grew the canvas 1 px more (1000x1000 at
0.11470651449337071: 1003 instead of 1002), shifting the crop by 1 px.

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-18 10:39:29 -04:00
David MannandClaude Opus 5.5 fb6f8b4fa6 Canvas Size: growing the canvas re-renders PSD fill layers (#7362) (#7363)
A fill layer from a PSD keeps its stored pixels as a fill cache that covers
only the old canvas. Canvas Size, an extending Crop and Reveal All translated
that cache but kept it, so the added area of the fill layer stayed
transparent. Drop the fill caches whenever the canvas grows, so the fill
re-renders from its model over the whole canvas, as Photoshop does (Free
Transform and Perspective Crop already drop it).

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-18 10:39:24 -04:00