Commit Graph
2887 Commits
Author SHA1 Message Date
NetterSilverandClaude Haiku 5.5 acc3420c6d Keep previous crop length when normalizing rounds to zero
Found while testing crates/ui-egui/src/crop_size.rs:86-107 (format_length / normalize_length)

Co-Authored-By: Claude Haiku 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-20 20:14:50 +02:00
Guillem Hermida 872748fb5b Codecs: save DDS textures, uncompressed or BC1–BC3, with optional mipmaps (#1327) (#5993)
DDS textures can now be saved, not just opened: uncompressed (BGRA, BGRX, L8, A8L8; lossless) or BC1/BC2/BC3 (DXT1/3/5) from a clean-room block encoder, with an optional box-filtered mip chain, in the legacy header. Save As and Save a Copy ask with DDS Options; scripts pass `ddsCompression`/`ddsMipmaps` (file commands, control `app.save`, RPC `doc.save`, MCP `doc_save`/`doc_export`), and the CLI takes `--dds-compression`/`--dds-mipmaps`. Targa and DDS answers travel together as `FormatChoices` through the Save As flow.

Review follow-ups:
- File › Save behind DDS or Targa Options records a plain Save with its answer, and replaying `file.save` applies it (#3531).
- The block encoder finds the principal axis from every channel, so a channel that varies on its own can't hide the others' variation.
- DDS joins the shared round-trip suite; the header is built before the output buffer is allocated.
- Enter on a focused Cancel in DDS, Targa or TIFF Options cancels instead of saving.
2026-10-20 11:02:39 -06:00
Brandon ThomasandClaude Opus 5.5 9b9643764f Skip CodeRabbit reviews on Dependabot PRs (#7991)
Adds .coderabbit.yaml so CodeRabbit no longer reviews or comments on PRs opened by dependabot[bot].

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-20 11:38:22 -04:00
Dylan M. Taylor 8e7841554e Merge pull request #7429 from dylanmtaylor/docs/add-sonar-coderabbit-badges
docs: add SonarCloud and CodeRabbit badges to README
2026-10-20 11:30:45 -04:00
Brandon ThomasandClaude Opus 5.5 46e538b3a2 Release 0.11.1
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
v0.11.1
2026-10-20 08:47:46 -04:00
Brandon ThomasandClaude Opus 5.5 8ede07b096 Format: build on FreeBSD (rustix has no xattr calls there)
The 0.11.0 release's FreeBSD job failed: copy_xattrs used rustix's
fsetxattr/flistxattr/fgetxattr, which rustix offers only on Linux,
Android and Apple systems. The real copy is now gated to those, and
other Unix systems get a no-op copy_xattrs, as Windows has. The xattr
tests are gated the same way; the Unix mode test now also checks that
no temporary file is left behind.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-20 08:47:42 -04:00
Brandon ThomasandClaude Opus 5.5 cd79a5a9e8 Release 0.11.0
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-20 08:23:58 -04:00
aiongg 03d0c7e3b3 Merge pull request #7823 from aiongg/integration-5
Integration batch: 8 PRs on main
2026-10-19 23:11:09 -04:00
aiongg 46f7e2422e Integration: drop #7690's benchmark report, as #7690 did before it merged 2026-10-19 18:41:15 +00:00
Brandon ThomasandClaude Opus 5.5 3a896a5efc docs: regenerate the scorecard (Show File Name In Title adds a preference) (#7829)
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-19 14:23:47 -04:00
d5c7edacdf Layered TIFF: write PSD-style layer data unless a length overflows (Photoshop opened only the composite) (#5514)
The layered TIFF writer let the PSD exporter pick PSB from its size estimate, which counts every
layer plane uncompressed. A document with many canvas-sized layers passes 2 GB there while its
real layer data is a small fraction of it (a 3000×3441 mockup with 180 layers: estimate ~2.57 GB,
real data ~111 MB), so tag 37724 got the PSB-style header ("Adobe Photoshop Document Data
V0002", 8-byte lengths). Photoshop does not read that in a TIFF: it opens only the composite, as
one locked Background.

PsdExportOptions gains `size_limit_psb` (default true, .psd saves unchanged). The layered TIFF
turns it off and writes PSD-style data ("Document Data Block"), falling back to PSB only past
30000 px, when forced, or when a length really overflows 32 bits.

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Co-authored-by: Brandon Thomas <echelon@gmail.com>
2026-10-19 14:21:01 -04:00
Marco Arnulfo 4f15642ee8 Toolbar: Edit Toolbar, Quick Mask and Screen Mode in every theme (part of #4653) (#4680) 2026-10-19 14:20:48 -04:00
David MannandClaude Opus 5.5 e306bcd685 Crop: a turned crop rotates only what the frame keeps (#5006) (#5007)
A turned crop rotated the whole canvas, then cropped to the frame. With
Delete Cropped Pixels on, every layer, mask and channel is now trimmed to
the frame's source (plus the interpolation kernel's reach) before the
turn, so the rotation costs the frame's size: a 512x442 frame turned 3°
on a 3859x2594 two-layer document goes from 541 ms to 42 ms.

Rotate Canvas also fills the Background with shared tiles instead of a
canvas-sized float buffer.

Co-authored-by: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-10-19 14:20:42 -04:00
MarekSpodBiedry 1a03735cc6 fix(engine): apply document-wide image and colour conversions to stored video frames (#4956) (#5078) 2026-10-19 14:20:35 -04:00
Nicolas FalesyandClaude Opus 5.5 8bed609ef0 Flatpak on Wayland: say why a paste finds nothing, and how to paste anyway (#5031) (#5279)
In a Flatpak on Wayland, arboard can't open at all: the sandbox hides the
data-control protocol from it, and --socket=fallback-x11 gives no X server
while a compositor runs. Paste then reported "the clipboard holds no image"
even when it held one. Now the clipboard service reports that it had no
clipboard to read, and the paste shows a notice that explains it and gives
the XWayland command for this install.

Also adds my contributors/people.toml entry.


Claude-Session: https://claude.ai/code/session_01AbyXia7KGdFgbNjBeggY8J

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-19 14:20:29 -04:00
Persi f5af2f3ef5 PSD export: check the Background's opacity from its tiles (#5998) 2026-10-19 14:20:22 -04:00
David MannandClaude Opus 5.5 c136d2dd3d Guides: Ctrl/Cmd drags a guide with every tool that moves guides, and the cursor shows it (#6288) (#6294)
With Ctrl (Cmd on the Mac) held over a guide, Photoshop's tools that turn Ctrl into
the Move tool show the guide cursor and drag the guide. Three gaps remained:

- The Frame and Artboard tools showed the guide cursor under Ctrl but took the
  press themselves (a new frame or board), so the guide never moved. Their pointer
  handlers checked the selected tool instead of the effective one.
- The Gradient tool's live mode took a Ctrl press sent with the event (automation,
  `ui.pointer` with `command: true`) instead of handing it to the Move tool.
- With Window > Modifier Keys' latched Ctrl, the press took the guide but the
  pointer kept the tool's own cursor: `command_guide_at`, the decision the cursor
  and the guide context menu use, knew only the selection tools.

`command_guide_at` now covers every `hold_keys::cmd_moves` tool, and not during a
stroke or type editing. Frame, Artboard and live Gradient step aside for a Ctrl
press when they have no drag of their own under way. Tools that keep Ctrl for
themselves (Pen, Hand, Zoom, Crop, type, shapes, slices) and Lock Guides are
unchanged.

Co-authored-by: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-10-19 14:20:16 -04:00
David MannandClaude Opus 5.5 5e1d90d86e Preferences: a wrongly typed value resets only that field (#6611) (#6641)
One value of the wrong type in prefs.json (say "recentFileCount": "ten")
made the whole file fail to deserialize, so every preference fell back
to its default for the session, and since the file was never rewritten
it happened again on every launch.

load_prefs_json now reads leniently when the file doesn't deserialize as
a whole: it starts from the defaults and lays the file's values over
them one at a time, descending into section objects and map entries,
keeping every value that still reads and leaving out only the ones that
don't. Colour settings get the same treatment instead of resetting as a
whole. It returns the left-out values (keys and original value); text
that isn't a JSON object still fails as before.

On launch the app shows a notice naming the reset paths and saves once:
the three-way merge base gets the unreadable values, so the merge
replaces (or removes) exactly those in the stored file and keeps
everything else, including keys this version doesn't know.

Co-authored-by: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-10-19 14:20:09 -04:00
David MannandClaude Opus 5.5 77b845e098 Actions: loading a set renames each Play target once (#6806) (#6807)
Load Actions applied each rename as its own pass over every action in the set, so chained
renames ("A" -> "A 2", "A 2" -> "A 2 2") retargeted a step twice, and with replace:false it
also rewrote steps of actions already in the set. Now one original->new map is applied once
to the newly loaded actions' Play and Conditional (then/else) steps and to function keys.

Co-authored-by: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-10-19 14:20:02 -04:00
MarekSpodBiedry 2cdc8cb73d Content-Aware Fill: preview at full resolution so it matches the result (#6530) (#6838) 2026-10-19 14:19:55 -04:00
FinnNkandClaude Opus 5.5 e3224000ca fix(psd): make packbits::decode_into crate-private and overflow-safe (#6950)
decode_into computed out.len() + expected unchecked. With a non-empty
buffer and an expected length near usize::MAX it panicked in debug builds
and, in release, wrapped and returned Ok having decoded nothing. Compute
the target with checked_add and report LimitExceeded, and test each packet
as len > target - out.len() so the bound checks cannot overflow either.

The only callers are decode and decode_rle in this module, and the crate
README documents packbits::{encode, decode} as the public API, so
decode_into becomes pub(crate).

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-19 14:19:48 -04:00
David MannandClaude Opus 5.5 bf638a7a22 Crop: Enter during a drag ends the drag (#7055) (#7056)
Pressing Enter while the frame is still being dragged commits the crop; commit_crop now ends the drag, so the release no longer restores a frame in the old document's coordinates as a pending crop.

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-19 14:19:42 -04:00
David MannandClaude Opus 5.5 b67502e20f Reveal All: one History step named "Reveal All" (#7145) (#7147)
image.revealAll ran image.crop, whose step is labelled "Crop". It now
runs it inside analysis_cmds::compound with the label "Reveal All".

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-19 14:19:35 -04:00
Michael HigginsandClaude Opus 5.5 73b79953e6 Print: Save as PDF gets a Browse… button (#5328) (#7586)
Save as PDF was only a free text field, so saving a PDF meant typing a
full path by hand. Browse… opens the standard Save dialog (pick_save),
suggesting the document's name as a PDF, and puts the chosen path in the
field. file_ui::body now gets the dialog id so the answer can be written
back into the Print dialog on a later frame.

Refs storytold/photocraft#5328

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-19 14:19:28 -04:00
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
David MannandClaude Opus 5.5 2cc107d05f Crop: a ui.pointer click on the frame leaves it alone (#7740) (#7741)
`to_press` and `control_point` read a point in the press-time view through
`remap`, which rounds it through f32 and the view even when the view hasn't
moved. A `ui.pointer` down + up at the same point then came back slightly
apart: the frame moved by a hair and a box Undo step was pushed that undid
nothing visible. Skip the remap while the view is still the press view.

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-19 14:19:15 -04:00
98d3ebc1f5 perf(codecs): Avoid redundant PNG filter passes (#7690)
* perf(codecs): Avoid redundant PNG filter passes

Keep the previous filter path as an exact byte oracle and measure the filter kernel independently of compression.

* docs(perf): measure PNG filtering on real images

* perf(codecs): stop PNG candidates at the winning cost

* Drop the local benchmark report

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-19 14:19:05 -04:00
209a384ef0 Home screen: fit the card and stack the buttons in a narrow window (#5774)
* Home screen: fit the card and stack the buttons in a narrow window

The Home screen's card was a fixed 460 points wide, centred on the canvas, with two
190-point buttons side by side. A tiling window manager can give PhotoCraft a canvas
narrower than that (Hyprland on Omarchy gave a 728-point window, below the 760-point
minimum), so the New document and Open buttons were clipped at the edges.

The card is now as wide as the canvas when the canvas is narrower than 460 points. The
buttons are sized from their labels (so longer translations still fit) and are stacked
when two of them don't fit side by side; the card grows by one row when they stack.

Adds start_screen_buttons_stay_inside_a_narrow_canvas, which fails before this change
(New document starts at x = -1 in a 390-point canvas).

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

* contributors: add TaylorBurke

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

---------

Co-authored-by: Claude Sonnet 5.5 <noreply@anthropic.com>
Co-authored-by: Brandon Thomas <echelon@gmail.com>
2026-10-19 14:18:33 -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
Vikram BhaskaranandBrandon Thomas 7200f64bf1 Explain the Memory Usage budget and GPU caps (#6546) (#6892)
* Explain the per-document memory limit and automatic GPU budget caps.

* Add catalog entries for the Memory Usage explanation (#6546).

The new preference strings need a row in every complete-menu language
for the i18n tests.

---------

Co-authored-by: Brandon Thomas <echelon@gmail.com>
2026-10-19 14:17:49 -04:00
Omar SaidahmedandBrandon Thomas cec0e843e6 refactor(algo): route the splitmix64 copies through one function (#7603)
* route the splitmix64 copies through one function

* document the splitmix64 generators

---------

Co-authored-by: Brandon Thomas <echelon@gmail.com>
2026-10-19 14:17:32 -04:00
aiongg 3f509f06ed Integration: regenerate docs/scorecard.md 2026-10-19 18:01:48 +00: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
Khawar Zamman WaniandClaude Opus 5.5 caa1539030 Tools: Sample All Layers is each tool's own, as in Photoshop (#5524) (#5570)
One ToolOptions::sample_all_layers was shared by twelve tools, so ticking
it for the Magic Wand or Paint Bucket also turned it on for Blur, Sharpen
and Smudge. A dab of those on an annotation layer then pasted an opaque
copy of the composite there, which sat above the picture and hid every
later blur of it (the sharp round patch in #5524).

ToolOptions::sample_all is now a SampleAllLayers with one flag per tool,
all off by default: Magic Wand, Quick and Object Selection (and Object
Finder), Paint Bucket, Magic Eraser, Spot Healing, Patch, Content-Aware
Move, Remove, Blur, Sharpen and Smudge. Each options bar and command
reads its own tool's flag, and Reset Tool resets only that tool's. The
new key means older preferences drop the shared flag, so every tool
starts with it off.

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-19 13:33:14 -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 3cafc63656 Free Transform: W and H take a typed length in percent view (#6284) (#6293)
W and H showing percent rejected a typed length ("40px", "2 in", "5cm"),
since the shared unit parser has no length to convert a percentage
against. The fields now carry the box's original side as the length 100 %
stands for, so a typed length becomes that share of it at the document's
resolution, and "50%" typed into a length view is half the original side.
The field keeps its chosen unit; the aspect link and the scale limits
apply as before. Other percent fields (opacity, fill) still reject lengths.

Co-authored-by: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-10-19 13:32:51 -04:00
Josephand00xJ0 80cd36846e GPU canvas: redraw mip levels of 128 px or less whole; fixes AMD Vulkan corruption after partial updates (#6418) (#6438)
* GPU canvas: redraw mip levels of 128 px or less whole; AMD Vulkan corrupted them after partial updates (#6418)

On an RX 6900 XT with Vulkan (driver 26.8.1), a regenerate_mips pass that drew only the damaged
part of a small mip level corrupted texels outside its scissor, at the level's top-left; DX12 on
the same card was fine. A sweep of 15 canvas sizes x 8-bit/float x 6 dabs found 66 of 180 cases
corrupt on Vulkan, every corrupted level at most 64 px; sampling the levels in a shader shows the
same pixels, so it is the texture contents, not the readback.

mip_damage (regenerate_mips's damage arithmetic) now redraws levels of at most WHOLE_MIP_SIDE
(128 px) whole on every adapter: no new passes or textures, only a larger scissor on the
smallest levels. With it the sweep is clean on both backends.

Tests: small_mip_levels_are_redrawn_whole (the rule, no GPU) and
partial_updates_match_a_full_rebuild_at_every_mip_level (fails on the Radeon over Vulkan without
the change, passes with it, like canvas_mips_preserve_opaque_pixels_and_partial_updates).

* contributors: add 00xJS (GitHub profile name)

* Mip sweep test: hold the GPU lock like the other GPU tests (#6438 review)

---------

Co-authored-by: 00xJ0 <56056764+00xJ0@users.noreply.github.com>
2026-10-19 13:32:42 -04:00
Marco Arnulfo a012894505 Layer Mask Properties: the Feather slider has fine steps near 0 px, as in Photoshop (#6545) (#6727) 2026-10-19 13:32:35 -04:00
MarekSpodBiedry daa9abfe43 Disable Save for Web while a Free Transform is pending (#6737) (#6832) 2026-10-19 13:32:29 -04:00
Vikram Bhaskaran e1c209eadb Hide the Hyphenate checkbox until layout hyphenates (#6710) (#6869)
* Hide the Hyphenate checkbox until layout actually hyphenates (#6710).

The flag still round-trips to PSD and ⌥⇧⌘H still toggles it, but the
Paragraph and Properties panels no longer advertise a no-op.

* Assert Hyphenate stays hidden until layout hyphenates (#6710).

The Paragraph properties and paragraph style options editor now have
tests that fail if the no-op control is shown again.

* Drop the duplicate #[test] on hyphenate_is_not_shown (#6710).

Clippy -D warnings failed on duplicate-macro-attributes and unused mut
on the harness that only queries the label.
2026-10-19 13:32:21 -04:00
Korg1ch 8c2cfdba6a fix(linux): keep crash-recovery autosaves in $XDG_STATE_HOME, out of the config folder (#6612) (#6941)
* fix(linux): keep crash-recovery autosaves in $XDG_STATE_HOME, out of the config folder (#6612)

* test(linux): cover the XDG state recovery folder (#6612)
2026-10-19 13:32:14 -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
zrsix1andClaude Opus 5.5 c78ec47f4b Type: a click inside the paragraph box off the glyphs moves the caret (#7606)
While editing paragraph text, a click inside the box that missed the glyphs
(left of a line's start, or below the last line) committed the edit. Like
Photoshop, it now places the caret at the nearest position; a click outside
the box still commits.

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-19 13:31:56 -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
acb3a66fd5 Crop: a preset picked in Tool Presets fits the frame to its ratio (#7143) (#7144)
select_tool_preset only merged the preset's tool options, so a crop
preset's ratio was set without refitting the frame; the options bar's
dropdown already refits. It now compares the ratio before and after and
calls crop_ui::apply_ratio when it changed, as the dropdown does.

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Co-authored-by: Brandon Thomas <echelon@gmail.com>
2026-10-19 13:31:10 -04:00