Commit Graph
773 Commits
Author SHA1 Message Date
Brandon ThomasandClaude Opus 5.5 7926161595 Release prep 0.7.0: Linux riscv64 tarball + version bump (#2965)
* Release prep 0.7.0: add Linux riscv64 tar.gz to the release; bump to 0.7.0

- release.yml: new linux-riscv64 job, ported from GridCraft: cross-compiled on ubuntu-24.04
  against ports.ubuntu.com riscv64 libs, tar.gz only, photocraft-cli --version smoke-tested
  under QEMU. The draft-release job now needs it, so SHA256SUMS.txt covers it.
- packaging/linux/package.sh: CROSS_ARCH / CROSS_TARGET / CROSS_COMPILE / EMULATOR support.
- linux_libs: search /usr/lib/riscv64-linux-gnu on riscv64 (was the x86_64 triplet).
- README and docs/releasing.md list the riscv64 tarball.
- Version 0.6.0 -> 0.7.0 (cargo xtask version set), Cargo.lock refreshed; contributors refreshed.

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

* canvas: crisp elliptical marquee ants again in an unrotated view

#2732 (a27d7175) built the elliptical marquee's live ants in document space and snapped every
vertex to the half-pixel grid on its own. Near the edge midpoints the nearly flat arcs became
1 px stairs, whose antialiased steps are grey, so the ants no longer contrast on white or black:
drag_preview_canvas's marquee test fails on main (ubuntu and windows). In an axis-aligned view
(rotation a multiple of 90 degrees) draw the ellipse inscribed in the snapped screen box again,
as before #2732; rotated views keep the mapped ellipse.

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

---------

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-10 21:36:30 -04:00
skylar taylor-barrick 77f50330c6 UI.9: refresh shared panel integration against current main 2026-10-10 19:30:56 -06:00
3c70d3b34f Layer > Rasterize > Layer Style bakes effects; Rasterize Layer converts vector masks (#2245) (#2669)
* Add Layer > Rasterize > Layer Style; Rasterize Layer converts vector masks (#2245)

Nothing could bake layer effects into a layer's pixels, and Rasterize >
Layer refused a raster layer that has a vector mask.

New command layer.rasterize.layerStyle (Layer > Rasterize > Layer Style,
and in the Layers context menu). It renders the layer by itself through
the compositor at 100% opacity in Normal mode, so the effects, fill
opacity and masks end up in the pixels, including those past the canvas.
The layer's own opacity, blend mode, knockout and Blend If stay layer
properties. Type, shape, fill and Smart Object content is rasterized
first, all in one history step. With a Normal layer and Normal-mode
effects the composite is unchanged at 8, 16 and 32 bit. Effects in
another mode are evaluated against the isolated layer's transparent
backdrop, because one layer of pixels cannot hold several blend modes.

Rasterize > Layer and All Layers now also turn a vector mask into a
pixel mask ("all vector data") and leave effects live, as Photoshop
does. The menu enablement follows both changes.

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

* Regenerate docs/scorecard.md after rebase (#2245)

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

---------

Co-authored-by: Fabio Lussi <198260595+itrycodinganditsbad@users.noreply.github.com>
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Co-authored-by: Brandon Thomas <echelon@gmail.com>
2026-10-10 19:20:33 -04:00
325c5b3194 Tools: Single Row Marquee and Single Column Marquee (#2837) (#2848)
Both tools join the Marquee flyout (no key, outside the M / Shift+M cycle,
as in Photoshop). A click selects the one-pixel row (column) under the
pointer across the whole canvas; a press-drag moves the line until the
release. The selection goes through select.rect, so Shift adds, Alt
subtracts, Shift+Alt intersects and the options-bar modes apply; Feather
applies, Anti-alias and Style/Width/Height are greyed. Selection-mode
cursor badges, the selection right-click menu and snapping cover them.
Original row/column glyphs with an ATTRIBUTION.md row; tool names in all
catalogs; README tool list and the G6 / ui-parity rows updated.

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Co-authored-by: Brandon Thomas <echelon@gmail.com>
2026-10-10 19:10:34 -04:00
9d3ba499ad Layer > New > Background from Layer; the Background stays at the bottom (#2626) (#2663)
* Add Background from Layer and keep the Background at the bottom (#2626)

Layer > New > Background from Layer was missing, and the Background was a
locked layer that Layer > Arrange (with an explicit layer), Reverse and
dragging in the Layers panel could move, or put other layers below.

The new layer.new.backgroundFromLayer command renders the active layer as
it looks over the background colour, drops it to the bottom and locks it
as the Background, in one undo step. It is unavailable when the document
already has a Background, and for groups and adjustment layers.

Arrange refuses to move the Background and stops other layers above it;
layer.moveTo and Reverse refuse edits that would move it off the bottom.
The Layers panel shows no drop indicator for those drops.

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

* Parity floor 629; the empty-space drop test uses a document without a Background

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

---------

Co-authored-by: Fabio Lussi <198260595+itrycodinganditsbad@users.noreply.github.com>
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Co-authored-by: Brandon Thomas <echelon@gmail.com>
2026-10-10 19:09:26 -04:00
dhiaaddin 1e0f8844eb Font menus: add an Arabic sample for families that cover Arabic (#2535) 2026-10-10 19:06:05 -04:00
5b778b49f2 Move tool: highlight the layer Auto-Select would pick on rollover (#2834) (#2843)
* Move tool: highlight the layer Auto-Select would pick on rollover (#2834)

With the Move tool and Auto-Select on (or ⌘ held while it is off), hovering
the canvas outlines, in the theme accent, the layer (or group, per the
Auto-Select target) that a click there would pick. Preferences › Tools ›
Show Highlight on Rollover turns it off (on by default).

The hover asks the click's own hit test (pick_cmds::auto_select_target, now
shared with layer.pickAt), so a type layer is outlined anywhere inside its
text and fully locked layers are clicked through, as on a click. The answer
is cached per document revision and pointer pixel. Nothing is drawn while a
button is down, a drag or transform runs, or the selected pixels would move.

Part of #214 (scorecard TOOL-214-4, now partial).

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

* Move hover: follow main's moves_selected_pixels_with rename

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

---------

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Co-authored-by: Brandon Thomas <echelon@gmail.com>
2026-10-10 19:04:19 -04:00
Sir Starch 67e34a1d4c Themes: add Breeze Light and Breeze Dark (#2276) (#2335)
Two Studio-layout themes for KDE Plasma desktops: original colour values
chosen to sit close to Breeze's cool blue-grey surfaces and blue accent
(and its 5 px radius) so the app blends into a Plasma desktop, without
copying Breeze's colour schemes. Text readability comes first.

- theme.rs: ThemeKind::BreezeLight / BreezeDark. Every text colour (text,
  secondary, faint, accent, danger, warning) holds 4.5:1 on every surface
  and on hovered, pressed and selected states; the primary button is a
  deeper blue that takes white text and stays above 4.5:1 while it
  darkens on hover and press. Breeze's own inactive text and link blue
  are only about 2.8:1 on its light surfaces, so ours are darker. The
  close button goes red on hover.
- prefs: Theme/LightTheme::BreezeLight ("breezeLight") and
  Theme/DarkTheme::BreezeDark ("breezeDark"): Preferences > Interface,
  `prefs.set`.
- Window > Theme > Breeze Light/Dark Theme (`window.theme.breezeLight`,
  `window.theme.breezeDark`) and `ui.set {"theme": ...}` (aliases:
  breeze, kde, plasma for the light one; ...dark for the dark one).
- i18n: the four new labels in all 16 catalogs.
- docs/ui-design.md and ATTRIBUTION.md (original work, credits Breeze as
  the inspiration; Breeze's schemes are LGPL-2.0-or-later and none of
  their values, files or code are copied).
- Tests: names/aliases, surface order and blue-grey character, the
  contrast matrix for both themes, and the preference/mode selection.
2026-10-10 19:01:53 -04:00
FinnNkandClaude Opus 5.5 0c4016a4db feat(ui): hold Alt+Space to zoom out, as in Photoshop (#2877)
* feat(ui): Alt turns a held temporary Zoom In into Zoom Out

Alt reverses the Zoom tool's direction, but a held Zoom In (Cmd+Space)
ignored it: with Zoom Out unbound or rebound, Cmd+Alt+Space zoomed in.
held_tool now reports Zoom Out whenever Zoom In is the held binding and
Alt is down, unless Alt is part of the Zoom In key itself (a rebound
Alt+Z still zooms in). The default keys don't change here.

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

* feat(ui): hold Alt+Space to zoom out, as in Photoshop

Photoshop zooms out with Alt+Space (Option+Space on the Mac), usually
pressed as Space first, then Alt, then click. PhotoCraft's Zoom Out
(hold) default was Cmd+Alt+Space only, so Space then Alt still panned.

Zoom Out (hold) now defaults to Alt+Space. Holding Cmd+Alt+Space
matches both Cmd+Space and Alt+Space equally; the previous commit's
Alt rule resolves that to Zoom Out, so it keeps zooming out. Saved
overrides are kept. Holding Space first also avoids the Mac's Spotlight
shortcut and the Alt+Space window menu on many Linux desktops.

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

* chore(contributors): credit FinnNk by GitHub profile name

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

---------

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-10 18:56:04 -04:00
37cb99eb1f Let dock tabs be dragged to reorder within their group (#2272) (#2774)
Dock tabs only sensed clicks, so a drag on a tab fell through to the
strip behind it and moved the whole group; there was no per-group tab
order to change.

DockLayout gains tabOrder (tab names per group, like hidden_tabs, so the
Pro/Studio Color/Swatches swap is safe). It is read leniently: unknown
groups, names and wrong types fall back to the default order instead of
dropping the saved layout. It is drawn through visible_tabs, while
dockTabs and the panel bodies keep using the original indices.

Tabs on the dock strips now sense drags: dragging one along its strip
shows an insertion line and drops it there, a plain click still selects,
and a tab dragged off the strip moves the group as the strip does.
Lock Workspace stops both. The order is part of ui.set {dock}, the
remembered panelLayout and saved workspaces.

Co-authored-by: Fabio Lussi <198260595+itrycodinganditsbad@users.noreply.github.com>
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-10 18:47:36 -04:00
308c3369c0 Control: timeout errors name the likely causes (#2489) (#2497)
* Control: timeout text derives from the reply deadline (#2489 review)

The server deadline and both timeout replies now share one constant:
REPLY_DEADLINE drives the serve thread's clock and the text is built from
it, so the message can never claim a number the server no longer uses.

* Tests: drop the duplicate joined() helper left by #2782 and #2853 both landing

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-10 18:27:03 -04:00
46af2e6e30 Workspaces: preserve the embedding host menu and rail flags (#2620)
Co-authored-by: Brandon Thomas <echelon@gmail.com>
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-10 18:22:30 -04:00
Arthur Wagner 2194c7c58f Status bar: show document dimensions in the ruler unit (#2618) 2026-10-10 18:18:19 -04:00
Elvis33LE 03f4cc0de1 Correct Photomerge roadmap status (#2831) 2026-10-10 17:57:51 -04:00
a3d2a6d486 Place: wire Resize Image During Place into the implicit fit (#204) (#2538)
* feat(engine): Resize Image During Place drives the implicit Place fit (#204)

* Merge origin/main; regenerate docs/scorecard.md

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-10 17:53:25 -04:00
6bbfa77230 Interface: wire Dynamic Color Sliders for the colour tracks (#204) (#2531)
* feat(ui): the Dynamic Color Sliders preference controls the colour slider tracks (#204)

* Regenerate docs/scorecard.md

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-10 17:31:24 -04:00
Valentin KleinandClaude Opus 5.5 14dfa38581 Actions: double-click an action name to rename it (#2032) (#2769)
Photoshop renames an action in place when its name is double-clicked in the
Actions panel. PhotoCraft had no way to rename an action at all, so every
action kept its "Action N" default name.

- New engine command `actions.rename {"action": name|index, "name": str}`.
  Empty, whitespace-only and duplicate names are rejected (the old name
  stays). Nested `actions.play` steps that call the action by name, and
  its function-key binding, follow the rename. Refused during playback.
- Actions panel: double-clicking the name opens an in-place field with the
  name selected; Enter, Tab or clicking elsewhere commits through the
  command, Esc cancels. Double-clicking elsewhere on the row still plays.

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-10 16:54:36 -04:00
Brandon ThomasandClaude Opus 5.5 939c8f2c70 Progress docs: target-app parity, gaps, alpha gate, readiness by audience (#2696)
Progress docs per craftrules standards/progress-docs.md: target-app parity (merges parity-estimate.md), gaps, roadmap with alpha gate, readiness by audience (full / mainstream / essentials) with Opus 5.5 hours, and UI, brush, format, hardware and localization parity docs. docs/parity.md is now docs/parity-checklist.md.

The Windows test failure (file_dialog::tests::save_dialogs_remember_where_each_document_was_last_saved) is pre-existing on main: the test hard-codes POSIX paths.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-10 15:09:04 -04:00
Reynold HuandBrandon Thomas 5ab78e8ce1 feat(ui): the Use Smart Quotes preference turns typed quotes typographic (#204) (#2522)
Co-authored-by: Brandon Thomas <echelon@gmail.com>
2026-10-10 14:55:53 -04:00
skylar taylor-barrick 99a3970399 UI.9: preserve PhotoCraft controls in shared UI migration 2026-10-10 12:51:56 -06:00
Erica FrancoandBrandon Thomas 31def413cc fix(docs): Linux build prerequisites only name Debian packages (#2437)
The prerequisites listed `libxkbcommon-dev ... libgtk-3-dev`, which exist only
on Debian and Ubuntu: on Arch none of those names resolve and `pacman -S` fails
on the whole line, and on Fedora and openSUSE they all need `-devel`. Nothing
else in the repository gives the equivalents.

The app already speaks four distros at runtime: linux_libs.rs carries a
{soname, debian, fedora, arch, suse} table, reads ID and ID_LIKE from
/etc/os-release and prints the matching apt, dnf, pacman or zypper command when
a runtime library is missing. This brings the build instructions up to that.

Also adds the prerequisites to the book's Building page, which said only to
install the Rust toolchain.

Co-authored-by: Brandon Thomas <echelon@gmail.com>
2026-10-10 14:35:12 -04:00
Ayan 76fa217669 Quick Selection: 3.5x faster CPU strokes with hybrid graph cuts (#2436)
* Speed up Quick Selection with hybrid graph cuts

* Record Quick Selection stroke benchmarks and regression coverage
2026-10-10 14:17:48 -04:00
225ccc0db1 Crop tool: Photoshop's default mode turns the image behind an upright box; Use Classic Mode (P), Auto Center Preview (#1919) (#2545)
* Crop tool: Photoshop's default mode turns the image behind an upright box; Use Classic Mode (P), Auto Center Preview (#1919)

The Crop tool's gear menu gets Use Classic Mode (P toggles it while the tool shows a box; off by
default, as in Photoshop) and Auto Center Preview (on by default). In the default mode the box
stays upright on screen: a drag outside it turns the image about the box's centre (Shift 15 deg
steps, angle readout), a drag inside moves the image under the box, the arrow keys nudge the
image, Straighten turns the image so the line is level, and with Auto Center Preview a drawn or
resized box is re-centred in the canvas. Classic Mode is the previous behaviour.

The crop model and the commit are shared: crop_rect + crop_angle and one image.crop {angle}.
crop_mode.rs only drives the view: while a frame is edited the view's rotation is -crop_angle,
pivoting so the box (or the pressed point) stays put, and the user's view (a Rotate View turn,
its centre) is put back on commit, cancel, leaving the tool or switching to Classic Mode. The
untouched default frame leaves an R turn alone. The options live in ToolOptions.crop_shield
(classic_mode, auto_center_preview; ui.set cropShield). ui.pointer reads a default-mode drag's
points in the view at its press, so an up where the last move was is idempotent.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>

* Crop default mode: match Photoshop (view upright on tool pick, live Auto Center, image-turn readout, gear menu order) (#1919)

Checked by hand against Photoshop in its default (non-classic) mode:

- Picking the Crop tool turns a Rotate View (R) turn upright at once (about the view centre),
  and the view stays upright after Enter or Esc: the save/restore of the user's view is gone.
  Classic Mode still leaves the view alone (not checked against Photoshop).
- The angle beside the pointer while the image turns is the image's turn, clockwise positive.
- Auto Center Preview keeps a box being resized centred live: the handle follows the pointer,
  the box grows on both sides, and on the image the opposite edge (with Alt the centre) stays
  put. Computed from the press, so the view following the frame can't feed back into the drag.
- Gear menu order as Photoshop's: Use Classic Mode (P), Show Cropped Area (H), Auto Center
  Preview, no separator; Classic Mode greys out the other two. Show Cropped Area then acts as on
  and H does nothing (the stored choice is kept). German: Classic-Modus verwenden, Automatische
  zentrierte Vorschau.
- Crop preview past the canvas in a turned view: each band is drawn as its turned quad (the
  screen bounding box stretched band textures and spread the checkerboard). The white corners in
  the earlier offscreen snapshot were egui_kittest clamping the repeating checker texture; the
  real app shows the checkerboard in both modes.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>

* docs: regenerate scorecard for the crop default-mode row (#1919)

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>

* Crop default mode: the canvas keeps the view the Crop tool moves; a move starts from a centred box (#1919)

Tried by hand in the app: with Auto Center Preview on, a drag inside the box made the box wander
instead of moving the image, and a handle drag didn't keep the box's centre in place (after a
Space pan, too). Root cause: canvas_view works on a copy of the view for the frame and writes it
back at the end, so every camera change the Crop tool made during the frame (the image moving
with the pointer, the live-centred resize, Auto Center on release, the pivot while turning) was
undone, while the frame itself moved in the document. Tests driving tool_event never saw it.

- canvas_view notes the camera before the frame's input and, before writing its copy back,
  keeps a centre or rotation a tool changed meanwhile.
- With Auto Center Preview a move inside an off-centre box (after a pan) centres the box first,
  then grabs the image point now under the pointer.
- New tests/crop_default_mode.rs drives the full app UI with egui pointer and key events over
  several frames (offscreen wgpu): the box stays put while the image follows the pointer, and a
  resize after a Space pan keeps the box's centre fixed while the handle follows the pointer. Both
  failed before the fix.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>

* Crop default mode: a moved or resized box stays where it is on screen (#1919)

Checked in Photoshop: after a Space (Hand) pan the box isn't snapped back to the middle of
the canvas. A drag inside moves only the image, the box staying wherever it is; a resize keeps the
box's centre where it was at the press (also after a pan), and its release no longer pans the box
to the middle. Auto Center Preview still centres a newly drawn box on release and X still
re-centres. Classic Mode is unchanged.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>

* Crop tool: Classic Mode turns a Rotate View turn upright too (#1919)

Checked in Photoshop: with Classic Mode on and the view turned with Rotate View (R), picking
the Crop tool also turns the view upright, and the crop frame stays on the same image area. So the
Crop tool now drives the camera in both modes: default mode keeps it at -crop_angle (the box
upright), Classic Mode at 0 with the frame drawn with its own turn. Picking the tool pivots about
the view centre; commit and cancel leave the view upright in both modes, and P switching modes
never brings the R turn back. Auto Center Preview stays default-mode only.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>

* Regenerate the scorecard; update the crop test's header

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

* Crop: snap a new box's corner inside the untouched default frame in both modes (#2549 with #2545)

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

---------

Co-authored-by: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Co-authored-by: Brandon Thomas <echelon@gmail.com>
2026-10-10 14:14:19 -04:00
3092d3b6f9 Tests: line-tables-only dev debuginfo, nextest GPU group, faster loop docs (#2204) (#2414)
- [profile.dev] debug = "line-tables-only", matching CI's CARGO_PROFILE_DEV_DEBUG; halves link
  time and PDB size for test binaries.
- .config/nextest.toml: optional cargo-nextest runs with the GPU tests capped at 4 concurrent
  processes (unlimited ones time out in wgpu).
- AGENTS.md / docs/development.md: iteration commands (--lib --tests, --test <file>, nextest,
  doctests separately). CI is unchanged.

Co-authored-by: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Co-authored-by: Brandon Thomas <echelon@gmail.com>
2026-10-10 14:03:30 -04:00
AyanandBrandon Thomas 0a102eb920 Pixelate: CMYK Facet 13.7x faster, Lab halftone 5.0x faster (#2588)
* Reuse Facet quadrants and halftone dots without changing pixels

* Record full-image Pixelate timings and exact-output checks

---------

Co-authored-by: Brandon Thomas <echelon@gmail.com>
2026-10-10 14:03:03 -04:00
mroxsoandhighperfocused d59d2b509f Channels: tint single colour channel views on the canvas with Show Channels in Color (#2385) (#2574)
Interface > Show Channels in Color only reached the Channels panel
thumbnails (#1986); the canvas overlay hard-coded in_color = false, so an
isolated RGB/CMYK channel stayed gray while its thumbnail changed color.
channel_view::ensure now reads the preference (the view cache key already
carries the flag, so toggling rebuilds the texture), and a hidden Lab
Lightness goes neutral instead of black under the preference, so an
isolated a/b channel tints over mid gray like Photoshop. Lightness itself
stays grayscale (Adobe's exception).

Verified with the offscreen snapshot example: RGB green canvas goes
(128,128,128) -> (0,128,0) and CMYK cyan -> (0,176,232) when the
preference is on; unit tests cover render(), the Lab exception and the
cache key.

Co-authored-by: highperfocused <highperfocused@pm.me>
2026-10-10 14:02:38 -04:00
fa33712dd3 Speed up Iris Blur without changing output (#2578)
Co-authored-by: optimalbytee <optimalbytee@users.noreply.github.com>
Co-authored-by: Brandon Thomas <echelon@gmail.com>
2026-10-10 14:00:53 -04:00
Elvis33LEandBrandon Thomas ff9663bc9a EXR parts: import parts, channel groups and an AOV precomp as layers (#2338)
* EXR parts: import parts, channel groups and an AOV precomp as layers

file.exrParts lists what an EXR holds for a chooser - flat parts, prefixed
channel groups (diffuse.R/G/B), Cryptomatte layers as information;
file.openExrParts imports the chosen ones as the layers of one document
(F16 lossless as F32, linear sRGB tagged), with an optional additive
beauty precomp summed from the light-path AOV set (direct/indirect
diffuse+specular, emission; transmission/sss/volume when present) that
fails naming the missing AOVs. The codec gains decode_exr_channels for
exact named channels of one part; io shares the image_surface band writer.

File › Open EXR Parts… picks an EXR and opens a plain command form: a
checkbox per part and channel group (labels are the data's names with
size and channel count), a both/left/right view dropdown, and the
precomp checkbox. Cryptomatte layers ride along as non-rendered data for
the control channel. Form fields gain __labels for verbatim data-driven
labels; the new strings are catalogued in all sixteen complete languages.

* Regenerate parity and scorecard docs

---------

Co-authored-by: Brandon Thomas <echelon@gmail.com>
2026-10-10 13:51:06 -04:00
David MannandClaude Opus 5.5 7c18c68e24 Crop tool: W x H x Resolution mode resamples the crop to a size and resolution (#2443) (#2483)
image.crop takes optional targetWidth/targetHeight (px), resolution (ppi)
and resample: the cropped area is resampled to exactly that size (the
Image Size code path, now shared) and the resolution set, in the same
undo step, also after a turned crop. The Crop options bar gets
W x H x Resolution with unit-aware W/H fields, a px/in or px/cm
resolution, Photoshop's built-in size presets, Front Image (and I) and
Clear; the frame holds W:H while drawn or resized, X and the swap button
swap W and H.

Co-authored-by: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-10-10 13:46:31 -04:00
Simon f5de46e60a Menus: Reveal in Finder names the platform file manager off macOS (#217) (#2586)
* Menus: name the platform file manager in Reveal in Finder off macOS

The command opens Explorer on Windows and the desktop's file manager
(xdg-open) on Linux, but the Smart Objects menu item and the document
tab menu still said "Reveal in Finder". menus::reveal_label picks
"Show in Explorer" on Windows and "Show in Folder" on Linux and the web;
macOS keeps "Reveal in Finder". The menu catalogue (docs/parity.md),
the engine command labels and the command ids stay unchanged.

The help search, command palette and keyboard shortcut list read the
relabelled menu item, so they follow. Every complete language gets both
new labels, with the localized Windows name for Explorer.

* scorecard: platform shortcut hints and Reveal label are done (UI-217-19)
2026-10-10 13:40:30 -04:00
28ed1c1147 Handle allocation failures in large pixel operations (#2530)
* Make large pixel operations allocation-fallible

* Warp: drop the fixed 400 MP cap; fallible reservations already cover large transforms

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

* Adapt #2530 to main: fallible steep-Distort warp, Result from mask paste in float moves; regenerate scorecard

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-10 13:26:07 -04:00
B?! d1a062e044 AF.3: recognize current Affinity artboard properties (#2355) 2026-10-10 13:14:59 -04:00
MarekSpodBiedry 93629daedc Float the layer mask, not the layer, when the mask is targeted (#2468) (#2486) 2026-10-10 13:12:20 -04:00
Rene Leonhardt 5290930e4a fix: Preserve CMYK solid and gradient fills through PSD round trips (#2628)
* fix(compose): Preserve native CMYK fills through PSD round trips

Share depth-quantized ink samples between rendering and PSD caches.
Use the document profile for RGB fill colours and preserve valid imported
caches. Uncached CMYK fills use CPU rendering.

Cover all sample depths, gradient styles and tiled renders. Fix the real
16-bit gradient round trip and raise the psd-tools floor from 307 to 308.

* perf(raster): Reuse exact 8-bit CMYK conversions within rows

Cache the previous ink bytes and ICC result per row, with alpha decoded
independently. Keep the 16/32-bit conversion loops unchanged.

Matched 1 MP reads improve by 77% for smooth ramps, 33% with dithering
and 10% for random colours. Bitwise regression coverage includes all
depths, alpha and tile boundaries.
2026-10-10 13:11:54 -04:00
7722172585 Add OpenRaster (.ora) import and export (#2257)
Open and save .ora with pixel layers (any offset), nested groups, pass-through
groups, names, visibility, opacity, edit/alpha locks and blend modes (svg:* plus
Krita's krita:* names), at 8 and 16 bit. Save applies pixel masks, renders
text/shape/fill/smart layers to pixels and reports what OpenRaster can't hold.
Wired into File > Open/Save, the CLI, automation/MCP, the web picker and the
desktop file associations. Checked against 34 Krita 5.2.9-authored files
(opt-in krita_corpus test) and Krita re-opening our exports.

Co-authored-by: Scott Campbell <251750033+scottcampbelldata@users.noreply.github.com>
Co-authored-by: Brandon Thomas <echelon@gmail.com>
2026-10-10 08:40:35 -04:00
texjerandBrandon Thomas 05e0b1f8a5 kys import: first launch asks before importing; edit.keyboardShortcuts importKys (#1987 review) (#2251)
- The live set found beside Photoshop is offered, never applied unasked: an
  "Import keyboard shortcuts?" prompt names the set and how many keys it
  changes, with Import / Don't Import. Either answer is recorded in
  dialogs.photoshopShortcuts, so it is asked once; ui.inspect shows the
  pending offer as kysOffer.
- Parsing and label matching moved to the engine (photocraft_engine::kys), so
  edit.keyboardShortcuts {"importKys": <xml>} imports a set from agents, the
  CLI and MCP; the app's dialog uses the same plan with its menus' labels.
- "The keyboard shortcut set was not imported: {error}" is translated.
- .kys input over 4 MiB is refused before it is read as text.

Co-authored-by: Brandon Thomas <echelon@gmail.com>
2026-10-10 07:12:24 -04:00
4e8aaec643 Addition : Brush popup better UI (#2217)
* Brush name/tip/stroke cards

+ locale

* brush popup scale ui

scale brushes

* align fix + tests

* test fix

* ci fix

* ci fix 2

* don't scale text

* smaller width for tip+name

* scale + spacing + better UX

* Regenerate docs/scorecard.md

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

---------

Co-authored-by: JJ <jj@dot.com>
Co-authored-by: Brandon Thomas <echelon@gmail.com>
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-10 07:04:52 -04:00
AyanandBrandon Thomas d99562f038 Compute exact octagonal mask shifts with bounded workspace (#2154)
Co-authored-by: Brandon Thomas <echelon@gmail.com>
2026-10-10 06:46:06 -04:00
ae96037672 Expose native shape stroke controls with staged previews (#2218)
* Expose native shape stroke controls with staged previews

* Document shape stroke defaults in MCP control schema

* Regenerate docs/scorecard.md

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

---------

Co-authored-by: optimalbytee <optimalbytee@users.noreply.github.com>
Co-authored-by: Brandon Thomas <echelon@gmail.com>
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-10 06:42:52 -04:00
Sir StarchandBrandon Thomas a737281fad Distance fields on threads: bevel size changes 2-3x cheaper (#2119) (#2162)
* Distance fields: the Euclidean passes run on threads, results unchanged (#2119)

The chiselled bevel fields (#2006) and the precise glow fields are built
from an exact Euclidean distance transform, a few sweeps over the whole
map that all ran on one thread. They now run on scoped OS threads, in
groups of rows or columns handed out from a queue: edt_nearest (the
column pass, then the row pass), sharp_edges (rows), refine_edge_normals
(one edge pixel each) and the final per-pixel maps of the bevel and plain
Euclidean fields. Not rayon, as for the blurs (rows_par): maps are built
from inside rayon tiles, where a waiting worker steals other tiles (#276).
Maps under 65536 pixels stay on the calling thread, wasm always does.

Every unit is computed from its own inputs, in the same arithmetic as
before, so the fields are bit for bit the old ones whatever the thread
count: 216 fingerprints (bevel in and out, plain in and out, bevel_maps on
bars at 0 to 150 degrees, thin bars, discs, rounded rectangles, soft,
patchy and half-opaque masks, 40 stars, tiny and 1 pixel maps) are equal
at 1, 7, 16 and the default threads, and equal to the code before.
edt_nearest returns the nearest pixel as u32 (it was usize), which takes its
working memory from 20 to 16 bytes per pixel; a map past 2^32 pixels or a slice that does
not match its size gets "far" distances instead of a panic.

CPU compositor, 12 MP, bevel size change (ms, bevel_bench), main -> this:
ellipse 474 -> 217 (size 8), 990 -> 318 (250); text 747 -> 214 (8),
1110 -> 267 (250). A distance_field call on a 256x256 map goes from 2.2 to
1.8 ms, on 512x512 from 8.0 to 2.8, on 2048x2048 from 99 to 15.

Tests: par_units hands every unit to the closure once with its own
slices (short last unit, no second slice, units of different sizes, 1 to
64 threads); the nearest-pixel transform equals itself on 1, 2, 3 and 8
threads on awkward sizes and returns a seed at the distance it reports;
degenerate sizes; sharp edges and the four Euclidean fields are bit-equal
on 1, 2, 5 and 16 threads on discs, rounded rectangles, bars at several
angles, patchy opacity and an edge on the border. The nested-rayon
deadlock test (#276) now has a chiselled bevel layer in its document.

* GPU effect maps: the Euclidean fields are one window, not bands (#2119)

A bevel size change rebuilds the distance fields over the whole effect
region (the region grows with the size). The GPU path cut that into bands
of max(128, halo) rows, each computed from a window grown by the halo,
which for the bevel fields is reach + 10: at size 250, 262-row bands from
windows of about 786 rows. Every pixel went through the transform and the
edge refinement about three times, a 12 MP region gave about 12 tasks for
32 threads, and the two bevel fields ran one after the other.

The Euclidean fields (chiselled bevels, precise glows) now come from one
window, and distance_field spreads its own passes over the threads. A
rebuild is the whole region, which is also what the CPU compositor
computes, so the GPU canvas gets exactly the CPU's field: bands resolved
ties between equidistant edge pixels per window, and on a rebuild they
differed from the whole region by up to 0.07 px (ellipse) and 1.3 px (600
discs) at size 250, on a few dozen pixels in 12 MP. A partial update (a
brush dab) still computes the damage grown by the halo, from one window.
The chamfer fields (shadows, strokes, spread) are a sequential sweep and
keep the bands, and so does wasm, where there are no threads to spread
over and bands keep the memory small.

RTX 4080 SUPER, release, 12 MP, ms per size change on the GPU canvas
(bevel_bench from #2118, p50, median of 3 runs, before #2006 / main /
this), ellipse: 90 / 103 / 98 (8), 162 / 188 / 119 (50), 286 / 446 / 219
(250); text: 83 / 139 / 113, 140 / 215 / 130, 222 / 489 / 165. Alone in the
process (--no-cpu) ellipse at 250 is 407 -> 157 and text 468 -> 169.
36 MP ellipse at 250: 779 -> 382. Peak RSS: 12 MP at 250 1604 -> 862 MiB,
36 MP at 250 2987 -> 1726 MiB; at size 8 on 36 MP it is 163 MiB more
(1380 -> 1543). Precise glow fields on 12 MP: 46-84 ms -> 26 ms at reach
52-252 (32 ms at 12).

The window test now checks that the whole region is bit for bit the CPU's
field, and that a too small halo is noticed on a part of the region.

---------

Co-authored-by: Brandon Thomas <echelon@gmail.com>
2026-10-10 06:38:33 -04:00
Books 7198a94164 automation: accept absolute paths beneath the automation roots (#2176) (#2216)
doc_open, doc_save and the other automation file paths only took
forward-slash relative paths, so an agent passing C:/Users/me/a.png with
a C:/ root was refused. An absolute path is now compared with the
root's absolute path as text: if it lies beneath the root, the root's
components are stripped and the rest goes through the same checks as a
relative path and is opened through the same directory capability.
Nothing is resolved on the filesystem, so traversal, prefixes, device
names and link escapes are refused as before; absolute paths outside
the root, or with no root granted, are still refused.

Fixes #2176.
2026-10-10 05:53:23 -04:00
Sean LaveryandBrandon Thomas 478acf70cc Expand arithmetic expressions across numeric inputs (#2252)
* Expand arithmetic expressions across numeric inputs

* Test arithmetic and scientific notation in indexed color counts

* Drop the numeric_fields example (it wrote PNGs to /tmp; the widget tests cover the behaviour)

---------

Co-authored-by: Brandon Thomas <echelon@gmail.com>
2026-10-10 05:50:58 -04:00
kubuntugamerandBrandon Thomas 4c06b9fe4d fix(engine): adjustments on a smart object record smart filters (FILE-215-4); strokes honour doc CMYK (#2229)
On a smart-object layer, Image > Adjustments now records an editable
smart filter instead of committing disaster.

  - new engine helpers:
    - adjust_cmds::has_adjustable / adjust_as_smart_filter route
      image.adjustments.<kind> onto the source, with the selection as the
      filter mask, through smart_cmds::add_smart_filter
    - brush/eraser/pencil/live preview strokes now render inside
      with_cmyk_space(doc_cmyk_space(session)), so painted colours follow the
      document profile the same way the compositor does
  - docs/scorecard.md: FILE-215-4 partial -> done
  - tests: adjust_cmds/tests + brush_cmds/tests, plus panic_hunt and
    the lib suite all green on the committed tree

Known follow-up, already filed:
  - FILE-215-5 rows: double-clicking an adjustment smart-filter row is still
    a no-op. smart_ui::open_editor only proceeds when
    filter_dialog::has_dialog returns true, and has_dialog accepts
    filter.*, a fixed non-adjustment set and PREVIEWED - but not
    image.adjustments.* (those open through adjust_dialog::open, which isn't
    wired into the smart-filter row).

This is one commit on top of the latest upstream/main (rebased to fix the
stale fork ancestry that was blocking PR creation).

Co-authored-by: Brandon Thomas <echelon@gmail.com>
2026-10-10 05:49:07 -04:00
Brandon ThomasandClaude Opus 5.5 29d5f791d5 Regenerate docs/scorecard.md (TOOL-213-19 from #2193) (#2404)
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-10 05:20:57 -04:00
JohnnYoruandBrandon Thomas 897f9f37ad Add the Remove tool with non-local patch completion (#2193)
* Add the Remove tool with non-local patch completion

Tool::Remove (J flyout) paints a hole that paint.remove fills from its
surroundings as a background job, in one undo step. A stroke around an
object also removes what it encloses; the selection limits the hole.

The fill (photocraft_algo::nonlocal) follows Newson, Almansa, Gousseau and
Pérez (IPOL 2017): texture features in the patch distance, onion-peel
initialisation, match fields upsampled between levels and a best-patch
final copy, plus gradient-domain hiding of edge and interior seams.
Sparse holes are filled in tiles. inpaint::complete (Content-Aware Fill,
Spot Healing, Content-Aware Move) is unchanged.

On 20 holes in public-domain photos (remove_quality) the fill keeps 0.89
of the texture it replaces, against 0.54 with the Wexler/PatchMatch
completion. 24 MP (remove_bench): 100 px ring 120 ms, 1000 px ring 2.5 s.

* Translate the Remove tool's label and tighten comments

Adds "Remove Tool" to every UI catalog, following each one's pattern for
tool names and its existing word for Remove. Corrects comments that no
longer matched the code: the benchmark's description, the tile and margin
minimums, and the reasons given for two parameters.

* Put the Remove tool first in the J group

The J flyout's slot now shows Remove until another J tool is picked, and
J from another group selects it; repeated J goes on to Spot Healing Brush,
Healing Brush, Patch, Content-Aware Move and Red Eye.

---------

Co-authored-by: Brandon Thomas <echelon@gmail.com>
2026-10-10 05:19:11 -04:00
414b7b9e4b fix(ui): complete system appearance selection and styling (#2168)
* feat(ui): follow system appearance with a System theme

Follow the OS appearance through the existing egui input while retaining System as the saved theme choice. Apply the resolved palette to both egui styles and custom surfaces, and keep document surround settings independent.

Cover launch and live transitions, persistence, manual themes, menu checks, the Interface picker and Home contrast. Validate engine/UI suites, adversarial commands, wasm, layering, parity and the AutoReview gate. Strict Clippy reports pre-existing findings documented in the PR.

Refs: storytold/photocraft#2074

* Keep the document surround on the displayed theme in Sync with system (drop the dark-surround pinning)

---------

Co-authored-by: coygeek <coygeek@users.noreply.github.com>
Co-authored-by: Brandon Thomas <echelon@gmail.com>
2026-10-10 05:14:41 -04:00
Harrison O'Connor-HooverandBrandon Thomas 607a8542f5 Keep duplicated Smart Objects linked to shared contents (#2222)
* Keep duplicated Smart Objects linked to shared contents

* Share Smart Object source update code

---------

Co-authored-by: Brandon Thomas <echelon@gmail.com>
2026-10-10 05:05:56 -04:00
8d23c62272 Pen pressure curve: Preferences › Tools shapes how pen pressure reaches every brush (#2195)
* Pen pressure curve: Preferences › Tools shapes how pen pressure reaches every brush

Brush phase 6a (TOOL-213-11, #610). Every pen paints its pressure straight through, so artists
with a heavy or a light hand, or a tablet whose response feels off, had no way to adjust it.

- `tools.pressureCurve`: [input, output] points, linear by default, settable with `prefs.set`
  (validated: 2..=16 pairs within 0..1).
- `prefs::PressureCurve` reads it as a monotone cubic (Fritsch–Carlson), so a firmer press never
  gives less pressure. It sanitises the points itself (finite, clamped, sorted, one per input,
  capped, outputs made non-decreasing; flat beyond the ends), because a whole-section update or a
  hand-edited file can store anything.
- The stylus maps every pen sample's pressure through it before any brush sees it, so the live
  stroke, the commit and the journal agree. A mouse stays at full pressure.
- Preferences › Tools draws a curve editor under Use Tablet Pressure: drag a point, click to add
  one, right-click to remove one, with Linear, Soft and Hard presets. Translated in all 16 catalogs.

* Regenerate docs/scorecard.md

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-10 04:00:10 -04:00
265f10b91d Fix SVG selection and import detection (#2224)
* Fix SVG import detection and configurable group depth

* Fix malformed SVG error routing and formatting

* Refresh scorecard for SVG depth preference

---------

Co-authored-by: Brandon Thomas <echelon@gmail.com>
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-10 03:58:08 -04:00
texjerandBrandon Thomas b37bff987c Cmd held is the Move tool with the painting tools (Cmd+Alt duplicates) (#2248)
Photoshop switches to the Move tool while Cmd (Ctrl off the Mac) is held
with every tool but the Hand, Rotate View, Zoom, Crop, Slice, Path Selection,
shape, Pen and Type tools. #896 did this for the selection tools per press; this
does it for the rest (Brush, Eraser, Gradient, Clone Stamp, Eyedropper, ...).

hold_keys gains Temporary::Move (a modifier, not a rebindable key); the
canvas sets PhotocraftApp::tool_override for the frame and the tool state
machine, the Move tool's selection check (move_ui::moves_selected_pixels)
and the tool modules that gate on the tool read active_tool(). The held-tool
latch now lasts through the release frame, otherwise the Move's Up went to
the selected tool. So Cmd-drag with the Brush moves the layer, Cmd+Alt-drag
duplicates it first (move_mods), Cmd-drag with a selection floats the
selected pixels, and Cmd-click picks the layer under the pointer.

Off the Mac, Cmd+Alt is Ctrl+Alt, which the brush tools keep for the
on-canvas resize (brush_resize, also Alt+right-drag there): with a brush
tool Ctrl+Alt-drag resizes, with the other tools (Gradient, Eyedropper...) it
duplicates. Tests pin both layouts on any host. docs/control-protocol.md
describes `command` on ui.pointer events.

Co-authored-by: Brandon Thomas <echelon@gmail.com>
2026-10-10 02:44:27 -04:00