Commit Graph
133 Commits
Author SHA1 Message Date
0f16bce0c5 Add the Magnetic Lasso tool (#864)
* Add the Magnetic Lasso tool

A selection border that snaps to edges as the pointer moves, in the
Lasso (L) flyout after the Polygonal Lasso.

- photocraft-algo `magnetic`: live-wire edge tracing (Mortensen &
  Barrett 1995): Sobel strength, non-maximum suppression and gradient
  direction link costs, a shortest path inside a corridor of the
  detection width around the pointer's path, sub-pixel edge centres.
  Pixels are read lazily in 128 px blocks.
- Engine: `EdgeSource` (the active pixel layer, else the composite) and
  `select.magneticLasso`: rough points in, a border along the edges out;
  `trace: false` selects a given outline, which is how the tool commits.
- UI: click to start, move to trace (button up or down), points fasten
  by themselves (Frequency), click fastens, Alt-click straight, Alt-drag
  freehand, Backspace/Delete removes points, click the first point,
  double-click or Enter closes, Esc cancels, [ ] change the width.
  Options bar: Width, Contrast, Frequency, tablet pressure. `ui.inspect`
  reports the border in progress.
- The Lasso and Polygonal Lasso now apply the options bar's Feather;
  `select.lasso` rejects a feather outside 0..1000.
- Icon adapted from Noun Project's "Magnetic Lasso Tool" by Lil' Seal
  (CC BY 3.0, licence file and ATTRIBUTION.md row).

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

* it: translate the Magnetic Lasso strings (Italian landed after this PR was written)

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-07 19:42:45 -04:00
7a9b591189 Italian UI translation (it) (#853)
* Update mod.rs

* Update system.rs

* Update live_tests.rs

* Update localization.md

* Italian UI translation (it)

* Register Italian fully and bring the catalog up to date

- LANGUAGES grows to 13 entries (the array length was still 12), with the native
  name Italiano.
- The system-locale test expects the code `it`, not `it-IT`.
- Italian joins the i18n coverage report, ATTRIBUTION.md and LICENSE-translations.txt.
- Translate the strings main gained since the catalog was written, and fix two
  comments copied from the Spanish catalog.

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-07 19:13:01 -04:00
b436950ae3 Layer Style: complete multi-instance effects and dialog parity (#157) (#786)
* layer style: instance-aware engine apply (layer.layerStyle.replace) with param overlays, contour presets and factory defaults

* layer style: instance-based dialog state preserves every effect; Preview checkbox gates the live preview

* layer style: Styles swatches and New Style… save the dialog pending state (style.presets.new effects param)

* layer style: Contour picker and Noise slider for shadows and glows (CPU-rendered noise; GPU falls back to the CPU for noisy effects)

* layer style: Make Default / Reset to Default per effect kind (user defaults persist with the presets)

* layer style: double-stroke band regression (manga case) and control-protocol docs for the instance-based dialog

* layer style: make Styles a top-level tab displaying preset swatches in the right panel

* i18n: translate the Layer Style preset strings in de and pt-br

The German and Brazilian Portuguese catalogs landed on main after this
branch added "Style {name} saved", "Make Default" and "Reset to
Default"; every_tl_literal_is_translated needs them.

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

* presets: move the stray BTreeMap import to the top of the module

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-07 19:05:15 -04:00
8948511eeb Large PSBs open with their pixels: decode planes past 2 GiB, plus 4-5 GB PSB fixtures (#375) (#782)
* PSD: decode planes up to 8 GiB, so large PSBs keep their pixels (#375)

MAX_DECODED_BYTES capped a single decode at 2 GiB. Real PSBs go past
that in one plane set: the merged image of a 30000x30000 RGB file is
2.7 GB, a 33000x33000 16-bit layer channel 2.2 GB. Opening such a file
gave a document with no layers (flattened files) or layers whose
channels were "treated as empty" (layered files). Saving hit the same
cap on the encode side and silently fell back to uncompressed data.

The cap is now 8 GiB on 64-bit targets and 2 GiB elsewhere, the budget
photocraft-codecs already uses for Limits::max_alloc. It stays a
backstop: the decoders bound their output by their input on their own
(RLE expands at most 64x, Raw needs every byte present). The Raw and
RLE output buffers are reserved with try_reserve_exact, so a machine
without that much memory gets an error instead of an abort.

generated with Claude Code

* PSD import: a channel that can't be decoded leaves its layer empty (#375)

The warning says the channel is "treated as empty", but the importer
filled a missing transparency channel with opaque alpha, so a layer
whose channels all failed became an opaque black rectangle over the
document. A channel that is present but undecodable now leaves the
whole layer empty (transparent), as the warning says.

tests/large_psb.rs: the damaged-channel case (PSD and PSB), and an
opt-in release test that opens a flattened 29000x29000 PSB whose
merged image decodes to 2.52 GB (16 s, 11.6 GB peak here).

generated with Claude Code

* Large-file fixtures: generate and check 4-5 GB PSBs (#375, #216)

photocraft-psd example big_psb writes five PSBs straight from the spec,
independent of this crate's writer and its limits: flattened and
layered, raw (4.4 and 4.8 GB files, data past 4 GiB) and RLE (small
files whose planes decode past 2 GiB, up to a 33000x33000 16-bit
layer). Every sample follows one formula, so photocraft-io example
big_psb_check opens a file through photocraft_io::import and checks
pixels without a reference; with SAVE set it saves as PSB and checks
the re-opened file.

Release build, this machine: all five open with correct pixels and
survive a PSB save and re-open (5-14 s per import). Before the 8 GiB
cap, three of the five opened empty or black.

Scorecard FILE-216-2 missing -> partial: no 2 GB PSD, 150-layer or
20 MP fixture yet, and nothing runs in CI.

generated with Claude Code

* psd: ZIP channels reserve fallibly and read in bounded chunks

A small ZIP channel declaring a huge decoded size could make
zip_decompress try a multi-GiB with_capacity/read_to_end and abort on
allocation failure. Reserve with try_reserve_exact/try_reserve, read
through take(expected) in 1 MiB chunks, and report LimitExceeded
instead. Tests: absurd declared sizes, past MAX_DECODED_BYTES, garbage.

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-07 18:59:39 -04:00
f9479063be Rework Camera Raw filter preview and navigation (#781)
* Add Camera Raw source-resolution preview navigation

* Keep web preview responsive and discard stale refinement errors

* Preserve Alt wheel intent throughout smoothed Camera Raw zoom

* Specify refinement result type for the wasm fallback

* Bound sparse Camera Raw proxy domains and validate detail tests

* Document Camera Raw zoom controls with native screenshots

* Drop the Camera Raw zoom screenshots; keep the guide as docs/camera-raw-zoom.md

We don't commit sizable binary assets. Removes fit.png and actual-size.png,
their LICENSE.txt and ATTRIBUTION row, and moves the controls guide next to
docs/camera-raw-histogram.md.

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

* Translate the Camera Raw full-resolution notice in de and pt-br

Main added German and Brazilian Portuguese catalogues after this branch
forked; the i18n coverage tests need the new label in both.

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

---------

Co-authored-by: abc2cb <271337822+abc2cb@users.noreply.github.com>
Co-authored-by: Brandon Thomas <echelon@gmail.com>
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-07 18:40:24 -04:00
c887ed94cc Add the Content-Aware Move tool (#213) (#814)
* Content-Aware Move: move or extend the selected pixels, the gaps filled content-aware (#213)

New command paint.contentAwareMove {offset, mode: move|extend, structure 1..7,
color 0..10, sampleAllLayers}. The content is pasted at the offset, its colour
fitted to the new place by `color` (0 keeps it, 10 is a full seamless clone),
and an edge band whose width `structure` sets is re-synthesised from the
surroundings. In move mode the old place is then filled with
content_aware::fill, never sampling the moved content (no ghost). The
selection follows the content. One undo step; runs as a background job with
progress and cancel. Works on layer pixels, masks and channels, at any depth.

Timing (retouch_bench, release, 6016x6016, elliptical selections dragged
300 px): 250x250 move 240 ms / extend 129 ms, 500x500 0.93 s / 0.53 s,
1000x1000 2.6 s / 1.5 s.

Known limit: at low Structure the wide band comes out smeared (the PatchMatch
vote blurs a thin band between high-contrast regions); that is the shared
completion, not this command.

generated with Claude Code

* Content-Aware Move Tool in the J group (#213)

Tool::ContentAwareMove next to the Patch Tool: outside the selection a drag
draws a lasso, inside it drags the selection and runs paint.contentAwareMove
on release (as a background job). Options bar: Mode (Move/Extend),
Structure, Color, Sample All Layers. The drag shows the moving outline; a
live preview of the result is not part of this change. New strings are
translated in every catalog. Scorecard TOOL-213-4: missing -> partial
(Transform On Drop and the live preview are still open).

generated with Claude Code

---------

Co-authored-by: Brandon Thomas <echelon@gmail.com>
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-07 18:40:14 -04:00
684f85e2e5 ⌘⌥⌃-click on the canvas selects the topmost layer under the pointer (#813)
* ⌘⌥⌃-click on the canvas selects the topmost layer under the pointer

With any tool, ⌘⌥⌃-click runs `layer.pickAt` at the pointer (the topmost
visible layer with pixels there becomes the selected layer) without
switching to the Move tool; the press, drag and release are swallowed so the
active tool never paints or draws. It runs before the ⌃⌥ brush-resize
gesture, which the same keys would otherwise trigger.

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

* quick_pick: document the macOS-only gesture, report a failed pick as an error

Windows and Linux never report mac_cmd, so the gesture is a documented no-op there and
Ctrl+Alt keeps resizing the brush. Tests cover both, plus a failed pick.

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

---------

Co-authored-by: Jason Short <3344856+jason4short@users.noreply.github.com>
Co-authored-by: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Co-authored-by: Brandon Thomas <echelon@gmail.com>
2026-10-07 18:02:23 -04:00
TiredIsaandClaude Opus 5.5 4b3da49486 Camera Raw: read and write settings as a live PSD Smart Filter (part of #215) (#459)
* Camera Raw: read and write settings as a live PSD Smart Filter (part of #215)

- Map Camera Raw 18.4 (process 5/6) filter settings to and from PSD: custom white
  balance, Light, Presence, parametric and four point curves, HSL, Color Grading,
  sharpening, noise, grain and numeric post-crop vignette controls.
- Keep the original Photoshop descriptor; edits patch only changed fields. Unknown
  settings or versions stay an opaque filter, preserved byte for byte.
- PSD stores integer slider levels: export rounds continuous PhotoCraft values
  (hue 360 -> 0) instead of dropping the filter.
- Tests: field mapping, patching, opaque fallback, malformed input, rounding, and
  8/16/32-bit PSD -> edit -> .pcraft -> PSD with mask, filter order and undo/redo.

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

* Camera Raw PSD: use as_chunks for curve pairs (clippy 1.99)

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

* Camera Raw PSD: no product names in Rust comments, strings or test names

Only the descriptor class key itself keeps its literal name.

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

---------

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-07 17:59:16 -04:00
BFlatandClaude Opus 5.5 6b926cdecd DX12: load the shader compiler from System32 or beside the exe, never from PATH (#712) (#817)
wgpu's default DX12 compiler (`Auto`) loads `dxcompiler.dll` by name, and the Windows DLL search
falls through to the current directory and PATH, so another program's DXC build (a Chrome or SDK
folder) was loaded and crashed shader compilation or failed device creation. The app now uses FXC
(d3dcompiler_47.dll, part of Windows) unless a dxcompiler.dll ships beside photocraft.exe, which is
loaded by its full path. WGPU_DX12_COMPILER still overrides.

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-07 14:48:27 -07:00
9ef9992878 fix(automation): close ambient filesystem authorization gaps (#763)
* fix(automation): block ambient filesystem bypasses

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

Copilot-Session: e77cd34a-ce3e-4981-996f-4d37d49df90d

* test(automation): cover ambient path denial

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

Copilot-Session: e77cd34a-ce3e-4981-996f-4d37d49df90d

* fix(automation): guard filesystem path authorization

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

Copilot-Session: e85c024b-ace5-4415-9208-b8d4e4684505

* automation: judge the first non-empty prefs segment; refuse any path without data

prefs.set skips empty path segments, so `.scriptEvents.enabled` reached
script events past the section check. Preset import / plugin.install
with a whitespace path also read the filesystem.

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>
Copilot-Session: e77cd34a-ce3e-4981-996f-4d37d49df90d
Copilot-Session: e85c024b-ace5-4415-9208-b8d4e4684505
2026-10-07 17:14:03 -04:00
Alex GorevskiandCopilot 1b24b5212b Repair corrupt directory-save objects (#752)
* Fix directory save reuse of corrupt objects

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

Copilot-Session: e77cd34a-ce3e-4981-996f-4d37d49df90d

* Avoid revalidating unchanged directory-save objects

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

Copilot-Session: e85c024b-ace5-4415-9208-b8d4e4684505

---------

Copilot-Session: e77cd34a-ce3e-4981-996f-4d37d49df90d
Copilot-Session: e85c024b-ace5-4415-9208-b8d4e4684505
2026-10-07 17:13:09 -04:00
4a0a8d0794 Actions: actions.record/stop/play/list commands for agents and the CLI (part of #219) (#622)
* Actions: actions.record/stop/play/list commands for agents and the CLI (part of #219)

The Actions list moves from UiState to the engine Session, so the panel, photocraft-cli, the control channel and MCP share one list. actions.play checks every nested step with the same authorization a top-level command gets in untrusted sessions.

* Regenerate docs/scorecard.md after rebase

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

---------

Co-authored-by: Matt Van Horn <455140+mvanhorn@users.noreply.github.com>
Co-authored-by: Brandon Thomas <echelon@gmail.com>
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-07 17:12:52 -04:00
bd632e7eb3 Layered TIFF: open and save Photoshop layer data in TIFF files, in either byte order (part of #215) (#672)
* Layered TIFF: open and save Photoshop layer data in TIFF files, in either byte order (part of #215)

Photoshop keeps a layered TIFF's layers in two private tags: 37724 (ImageSourceData, the same
layer section a PSD has) and 34377 (the image resources). PhotoCraft now reads and writes both,
so a TIFF opens with its groups, masks, layer styles, type, shapes, smart objects and adjustment
layers exactly as the PSD path would, and saves them back.

* photocraft-psd: a `tiff` module with `ImageSourceData` (parse/write, PSD- and PSB-sized),
  the 34377 resource helpers, and a field-by-field byte-order transcoder. In Intel-order TIFFs
  Photoshop byte-swaps the whole block structure (reversed keys, little-endian integers and
  doubles) while pixel samples stay big-endian; the transcoder knows the layer-info structure,
  masks, blending ranges and every block photocraft-io maps (descriptors, TySh, lrFX, the
  binary adjustments, paths, patterns, filter effects, metadata settings, linked layers,
  annotations...), recognizes descriptor-shaped unknown blocks, and drops the rest with a
  warning instead of passing byte-swapped data to other readers. The reader gained an optional
  little-endian mode. 23 unit tests; a fuzz target in the CI matrix.
* photocraft-codecs: the two tags travel as opaque bytes on `Metadata`, read by a
  bounds-checked walk of IFD0 (classic and BigTIFF, both byte orders) and written back as
  BYTE / UNDEFINED tags. `tiff_writes_little_endian()` tells which order the encoder produces.
* photocraft-io: `tiff_layers` rebuilds a PSD model from the TIFF's pixels plus the tags on
  import, and on export runs the PSD exporter (new `merged_matte` / `merged_compression`
  options), interleaves its merged planes for the TIFF image and stores the layer section in
  the encoder's byte order. `ExportOptions.tiff_layers` (default on). Lab and Multichannel
  documents save flat; extra alpha channels are not written (one transparency sample).
* UI: the TIFF Options prompt ("Including layers will increase file size." / "Discard Layers
  and Save a Copy" / "Don't show again") behind Preferences > File Handling > Ask Before
  Saving Layered TIFF Files; `ExportSettings.tiff_layers`; catalogs updated.
* Tests: io round trips at 8/16/32 bits in Gray/RGB/CMYK, equivalence with the PSD path, every
  TIFF compression, the flat fall-backs, malformed input, and files written by psdtags in both
  byte orders (fixtures from scripts/layered_tiff_fixtures.py); a corpus test (feature
  `corpus`) writes every corpus PSD as a layered TIFF and compares the result with the PSD
  round trip (700 files, 0 differences, 0 crashes). Codecs tests cover the tag walk on
  hand-made files.
* Scorecard FILE-215-7 done, BUG-204-3 partial; README and book updated.

Co-Authored-By: Claude Mythos 5.1 <noreply@anthropic.com>

* Layered TIFF: de and pt-br strings; neutral wording in the psd tiff docs

The four psdtags fixtures stay out of the repository (no binary test data here): the
generator now writes them to the gitignored corpus/layered-tiff/, and the tests that read
them skip with a message when absent.

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

* Regenerate docs/scorecard.md

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

---------

Co-authored-by: leon <claude.udder842@passmail.net>
Co-authored-by: Claude Mythos 5.1 <noreply@anthropic.com>
Co-authored-by: Brandon Thomas <echelon@gmail.com>
2026-10-07 17:10:35 -04:00
2baa95d684 Type: hitTest, caret and navigate commands so agents can edit text by position (part of #219) (#623)
* Type: hitTest, caret and navigate commands so agents can edit text by position (part of #219)

Stateless queries in document pixels. The word, line and hit-test helpers move from type_tool.rs into photocraft-text, and the Type tool calls the same code.

* Regenerate docs/scorecard.md after rebase

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

---------

Co-authored-by: Matt Van Horn <455140+mvanhorn@users.noreply.github.com>
Co-authored-by: Brandon Thomas <echelon@gmail.com>
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-07 17:02:09 -04:00
2cbb13a79d fix(i18n): follow native UI languages on first launch (#661)
* fix(i18n): follow native UI languages on first launch

* i18n system tests: German and Portuguese are supported now; use Swedish as the unsupported tag

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-07 16:52:55 -04:00
3f637410cb Apply the Interface UI Font Size preference (#616)
* Apply the Interface UI Font Size preference

* Regenerate the scorecard after rebase (58 unread settings); drop the before/after screenshots

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-07 16:43:03 -04:00
06ec4044ea Improve Korean terminology and dynamic UI translation coverage (#604)
* Improve Korean Photoshop terminology and dynamic UI translations

* Keep untranslated form labels in sentence case; add the new strings to de and pt-br

label_of fell back to the Title Case filter label whenever the sentence-case label had no
translation, which changed English form labels ("Use antialias" -> "Use Antialias"). It now
uses the Title Case translation only when one exists. German and Brazilian Portuguese (merged
since this branch was opened) get "No patterns" and "Enable {name}".

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

---------

Co-authored-by: kijoong <gijoong.gim@gmail.com>
Co-authored-by: Brandon Thomas <echelon@gmail.com>
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-07 16:32:47 -04:00
a62b6473f3 Transform modes: Skew / Distort / Perspective as real modes; legacy corners as a preference (#671)
- Edit › Transform › Skew, Distort and Perspective now set a mode on the box
  instead of just starting Free Transform: in Distort a corner moves alone
  with no keys held (Skew: edges skew; Perspective: corners move in mirrored
  pairs). While a box is up they switch its mode, as does a right-click on
  the canvas (Free Transform, Scale, Rotate, Skew, Distort, Perspective),
  with the current mode checked.
- Distort moves only corners (or the whole box from inside): dragging
  outside doesn't rotate, edge handles don't move, nothing snaps; the cursor
  shows when a drag would do nothing.
- Corner drags stay proportional by default (⇧ frees them). Preferences ›
  General › Use Legacy Free Transform (already a setting, hidden until now)
  swaps that: free by default, ⇧ keeps proportions.
- The handle under a press is hit-tested where the press was, before
  snapping, which could push it off a corner and move the whole box.

Co-authored-by: Jason Short <3344856+jason4short@users.noreply.github.com>
Co-authored-by: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-10-07 16:28:17 -04:00
reese 2cdff3afb9 feat(ui): numeric fields evaluate typed arithmetic (#726)
Typing 1280*2 or 20*2+5-2 into a numeric field applies the result on
Enter, Tab or click-away (+ - * /, * and / first). Plain numbers still
apply as they're typed. Arithmetic is held until it's committed, so a
caller that rounds a partial result can't cut it short, and changed()
now means a new value rather than a keystroke.

The floating Properties card scopes its fields per layer, so text still
being typed for one layer can't commit to the next.

Fixes #601
2026-10-07 16:28:08 -04:00
reese 6c4010cfc4 Color panel: click a chip to edit that color, double-click to open the Color Picker (#655) (#664)
* fix: Color panel chips pick the colour to edit and open the Color Picker on double-click

A click on the foreground or background chip makes the Color panel's field and hue
strip edit that colour; a double-click opens the Color Picker on it, with its RGB,
HSB, Lab, CMYK and hex fields. Each chip remembers its own hue for greys.

Fixes #655

* refactor(ui): keep the Color panel's edited chip in UiState

The chip the Color panel edits is UiState::color_panel.background, so the
control channel and MCP can read it and set it with
ui.set {colorPanel: {background}}. The double-click test now clicks where
the chips overlap, where the foreground chip takes the click.
2026-10-07 16:20:52 -04:00
782ea69f35 fix(linux): AppImage registers its desktop entry and icon on launch (#617)
* fix(linux): AppImage registers its desktop entry and icon on launch

On Wayland the compositor resolves the dock icon from the window's app_id
(ai.storyteller.photocraft, already set) against a host-installed .desktop
file. An AppImage installs nothing, so every run showed the generic Wayland
logo (#593). The app cannot set the icon itself: winit 0.30.13 (egui 0.36.2's
pin) has an empty set_window_icon on Wayland; xdg-toplevel-icon-v1 landed on
winit's 0.31 line only.

- packaging/linux/AppRun is a script instead of a symlink: best-effort
  integration into $XDG_DATA_HOME before launch (desktop entry with Exec
  rewritten to the AppImage's path and TryExec dropped, hicolor icons
  copied), content-compared so an unchanged install costs a few cmp calls,
  refreshed when the AppImage moves, PHOTOCRAFT_NO_DESKTOP_INTEGRATION=1
  opts out, and no failure can block the launch (best-effort steps silent).
- packaging/linux/apprun-test.sh: 8-case smoke (passthrough, Exec rewrite
  with spaces, metadata preserved, idempotence, move refresh, opt-out,
  unwritable data dir, extracted AppDir). packaging-lint runs it and now
  shellchecks AppRun.
- Docs: releasing.md Linux section; README install paragraph.

Fixes #593

* AppRun: skip desktop integration when the AppImage path needs Exec escaping

A quote, backquote, dollar, backslash, percent or line break in the path would need
desktop-entry escaping (a line break could even add keys), so integration is skipped
and the app still launches.

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-07 16:12:00 -04:00
8d8ea584b8 Brazilian Portuguese UI translation (pt-br) (#700)
* Brazilian Portuguese UI translation (pt-br)

Add a pt-br catalog covering every key in the existing catalogs (2,492
entries), register it as "Português (Brasil)" with Portuguese plural
rules, and resolve pt, pt-BR and pt-PT locales to it. i18n coverage
1404/1404.

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

* Keyboard Shortcuts dialog: show menus and commands in the UI language

The command list showed the raw English menu paths and labels while the
rest of the dialog was translated. Translate them the way the menus do
(tr / tr_id); search matches the translated and the English names, like
the command palette. Also rustfmt the pt-br registry row.

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

* pt-br: gate it in xtask i18n-coverage (from #637, thanks @RicSchonfelder); licence note covers German too

Co-Authored-By: RicSchonfelder <RicSchonfelder@users.noreply.github.com>
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>
Co-authored-by: RicSchonfelder <RicSchonfelder@users.noreply.github.com>
2026-10-07 16:11:38 -04:00
Elvis33LE fc00f1e5bc scorecard: BUG-203 (atomic saves) is done - flip the stale rows (#203) (#687)
The BUG-203-1..6 and REL-4 rows still say missing, but the atomic-save
work landed in #230 (8227071, 2026-10-05) - apparently after the 4b53f62
snapshot these statuses were verified against. Re-verified against the
current tree:

- one shared photocraft_format::atomic_write (same-dir temp, create_new +
  write_all + sync_all, rename, Unix directory fsync, Windows
  PermissionDenied rename retry with backoff), and every document write
  site routes through it (engine file_cmds/preset_store, automation
  files/headless/workspace, app services, format store/autosave);
- no in-place fallback exists; read-only dir, disk-full and cross-device
  errors name the cause and state the original was not changed;
- format/src/atomic/tests.rs failure-injects mid-write, rename and
  read-only-dir errors (11 tests; re-run for this change: green).

The regenerated docs/scorecard.md moves Reliability to 4/1/2 and Open
bugs to 12/3/17. No code changes.
2026-10-07 15:49:13 -04:00
e79dcb6f7f i18n: add German (de) UI translation (#309) (#663)
* i18n: add German (de) UI translation (#309)

A complete German catalog plus the registry row:

- de.tsv: the full key set (menus, tl! literals, blend-mode names, Camera
  Raw labels, generated preference labels), plural rule one|other, header
  documenting the format and the clean-room policy like ja.tsv.
- Registered with complete_menus: true; the locale resolver and the
  language-list tests gain de (de-DE/de-AT/de-CH resolve to it).
- ATTRIBUTION.md row and the docs/localization.md language table entry.

Translations are original work from the plain meaning of the English
labels in ordinary German image-editing vocabulary (Ebene, Maske,
Auswahl, Weichzeichnen, Tonwertkorrektur, Smart-Objekt ...); no other
product's localisation resources were consulted. Product and technology
names (PhotoCraft, ArtCraft, OpenType, CMYK, Lab, Camera Raw) stay in
Latin letters; the Camera Raw curve bands are distinct (Schwarz, Tiefen,
Dunkle, Helle, Lichter, Weiss).

Verified: catalog tests and preference-label tests green,
cargo xtask i18n-coverage reports de 1404/1404 (100%), the full
ui-egui suite passes (except the known machine-local wgpu driver
failure that also fails on clean main), clippy and wasm32 clean, and
an offscreen snapshot sequence switches de -> en -> de in one process
and ends fully German.

* de: translate Metadata (Export As gains the label in the #647 fix)

* Revert "de: translate Metadata (Export As gains the label in the #647 fix)"

This reverts commit 948a8d01dd83579a899d938611101daa50a9e1e1.

* de: apply the native-speaker review (#663)

All meaning errors from the review: Auto Tone is the tonal range
(Automatische Tonwertkorrektur), the Divide blend mode is arithmetic
(Dividieren), Note Paper is Notizpapier, Legal is the US paper format,
Fuzziness widens the colour tolerance (Toleranz:), Perspective Warp
planes are Flaechen (not Ebenen, which reads as layers), Revert is
Zuruecksetzen (matching its confirm text), Fade is Verblassen.
Typos: Tonwerthistogramm, Magischer-Radiergummi-Werkzeug,
Einfrier-Lasso. Consistency: Kanalmixer everywhere, Zoom In/Out as
Vergrössern/Verkleinern, a space before % in the scale labels.

* xtask i18n-coverage: gate the German catalog too

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-07 15:48:02 -04:00
47f9306fd0 Open HEIC/HEIF photos (#355) (#372)
* Open HEIC/HEIF photos (#355)

iPhones save HEIC by default and Photoshop opens it; PhotoCraft had no
HEIF code, so .heic was missing from File > Open and failed to decode.

Add Format::Heif to photocraft-codecs, decoded by heic-rs (pure Rust,
forbid(unsafe), MIT OR Apache-2.0, wasm-ready): single pictures and
grid-tiled photos, 8-bit to U8 and 10/12-bit to U16, auxiliary alpha,
ICC, EXIF and XMP. The container's irot/imir/clap are applied and EXIF
Orientation is rewritten to 1, so photos are never turned twice. Limits
are checked against the declared size before decoding. Read-only (no
pure-Rust HEVC encoder), listed in ASYMMETRIC_EXCEPTIONS.

File > Open (desktop, web) and the batch list accept heic/heif/hif.
Tests compare against Apple's own decode and a 16-bit source, cover
orientation, metadata, limits and every truncation; fixtures from
heic-rs and pillow-heif are attributed in ATTRIBUTION.md.

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

* HEIF: pin heic-rs exactly and add a decode_heif fuzz target

Review follow-up: heic-rs is young and parses untrusted files, so pin it
(=0.1.1) and fuzz the HEIF path. The target covers both orientation
paths and is meant to run with -timeout so a hang is reported, not
only a panic. It is added to the CI fuzz matrix.

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

* HEIF: a heic-rs panic is a malformed-file error, not a crash

Fuzzing decode_heif found two out-of-range slices in heic-rs 0.1.1 on
malformed files:
- in its box parser (boxes.rs:130), reachable from a 72-byte file, even
  through the metadata-only probe;
- during HEVC reconstruction (hevc/decode/recon.rs:70).

The background job path catches panics, but the synchronous ones (jobs
off, the CLI) did not. The HEIF codec now runs the decoder under
catch_unwind and reports a panic as CodecError::Malformed, as
gpu/health.rs already does around wgpu.

The fuzz target also calls heic-rs directly, so its panics still reach
the fuzzer. The 72-byte input is a regression test, which fails
without the guard.

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

* HEIF: move the decoder into an optional photocraft-heif crate

The heic-rs decoding from #372 now lives in `crates/heif` (photocraft-heif, standalone, depends
only on heic-rs =0.1.1): probe, decode with a pixel limit, container transforms, alpha, 8/10-bit,
ICC/EXIF/XMP and the catch_unwind guard, behind a small API of its own (Info, Options, Decoded,
Error). photocraft-codecs uses it behind a new non-default `heif` feature; the apps and
photocraft-io forward it. HEIF detection, extensions and mime stay compiled in, so without the
feature a HEIC file is still recognised and opening it is a clear "HEIC/HEIF support isn't
included in this build of PhotoCraft" error. `cargo xtask layers` allows exactly codecs -> heif
between standalone crates.

The fixtures are no longer committed: `cargo xtask corpus --heif` fetches them from heic-rs
(tests/fixtures, v0.1.1) and pillow-heif (tests/images) at pinned commits, verified against
xtask/heif-corpus.sha256 (same bytes as the PR's copies). The fixture tests run with
`--features corpus,heif` (`cargo xtask test-corpus` enables both).

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

* Build CI and official binaries with HEIC support (--features heif)

CI test and clippy, the FreeBSD job, and the macOS, Linux (and Flatpak), FreeBSD, Windows and
web packaging all build with the `heif` feature; CI also tests codecs/io without it on Linux so
the "not in this build" path stays covered. `cargo xtask wasm` also checks photocraft-codecs with
`--features heif`.

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

* Docs: HEIC open is an optional build feature, on in official builds

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-07 10:20:40 -04:00
b07a2e5b43 Remember desktop window and panel sizes across restarts (#587)
* Remember desktop window and panel sizes across restarts

* Window layout: discard a saved ui.ron that would crash startup

eframe restores the window geometry and egui memory before the app runs, and a
zoom factor of 0, NaN or 1e30, or a NaN/huge window size aborted every launch
(dpi/wgpu assertions). Check the file first and remove it if it holds such values.

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-07 09:12:44 -04:00
e77325bd4a Add fixed UI scale steps from 75% to 300% (#611)
* add 75-300% fixed ui scale steps

* UI scale: French and Indonesian labels for the new steps

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

* i18n(ko): UI scale steps

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-07 09:11:35 -04:00
d4086ba11a feat(i18n): add Korean and live language switching (#582)
* feat(i18n): add Korean and live language switching

* i18n(ko): Edit Type (#598)

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-07 09:06:33 -04:00
45bedb0962 Monitor profile auto follows each window's display; what is applied is visible (#569) (#591)
* Monitor profile: say which profile is in use and why; apply a late display reading (#569)

Color Settings › Monitor Profile used to fall back to sRGB without a
word when the display's profile was missing, unreadable, not RGB or
unusable as a destination, and dropped a macOS reading that took longer
than 2 s. `ColorState::monitor_status` now reports the profile actually
applied, its fingerprint, the source (auto / manual / fallback) and the
reason for a fallback. `edit.colorSettings` (`monitorStatus`),
Help › System Info and the Color Settings dialog show it, and a fallback
in auto posts a notice.

The macOS reader also names the display it read. On a dual-display Mac
it reports the menu-bar display ("Built-in Retina Display") while the
canvas is on the external monitor, which is the display-selection bug
in #569; following the window's display is the next step.

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

* Monitor profile auto: each window uses the profile of the display it is on (#569)

The macOS reader now lists every display (id, name, frame, ICC profile
and its System Settings name) instead of asking for the "main screen",
which from a windowless helper is the menu-bar display. Each window
picks the display it overlaps most, as NSWindow.screen does, and its
canvas is shown in that display's profile: moving a window to another
display switches the profile without a restart, and two windows of one
document on different displays each keep their own. The GPU canvas
shares the document texture and keeps a display LUT per (document,
display); CPU textures are per (document, display).

Displays are read again when PhotoCraft comes back to the front (a
profile changed in System Settings or by a calibration tool), and when
a window is on an unknown display or one whose size changed (connected,
removed, rearranged), at most every 5 s. A failed re-read keeps the
previous reading. Help › System Info lists each display's profile.

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

* Monitor profile auto: act on the r1 review; no periodic re-read (#569)

Following the owner's decision on echo round r1:

- No periodic discovery: a read costs about 0.2 s of CPU, and a profile
  reassigned while PhotoCraft stays in front with no window moving
  practically doesn't happen. It is picked up at the next return to the
  front, or when Edit › Color Settings opens, which now reads at once.
- Every document window, not only the main one, asks for a reading when
  it is on an unknown display or one whose size changed.
- The focus re-read is throttled to one per 30 s; a trigger inside that
  window is deferred, not dropped.
- Starting a reading schedules the frame that applies its result, so an
  idle window doesn't wait for unrelated input.
- The osascript helper is stopped after 10 s (killed and reaped, both
  pipes drained meanwhile), so a hung reading can't block later ones.
- The Color Settings dialog recomputes "Monitor profile in use" as it
  draws, and the status says when an earlier reading is kept after a
  failed re-read (detection `retained`).

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

* Monitor profile auto: document windows count for focus and wake the main window (#569)

Echo round r2 on #591:

- Coming back to PhotoCraft through a document window (macOS reports
  focus per window, so the main window stays unfocused) now counts as a
  return to the front and reads the displays again, within the 30 s
  throttle.
- A document window that is on an unknown or resized display, or that
  brings PhotoCraft back to the front, asks the main window for a frame:
  the reading starts there, so it no longer waits for unrelated input.
  One frame per trigger, no continuous repaints.

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

* Monitor profile: pass the notice dismiss preference added on main

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-07 09:02:58 -04:00
3d23a708c2 Edit type layers by double-clicking their thumbnails (#598)
* Edit type layers by double-clicking their thumbnails

* Translate Edit Type for French and Indonesian

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-07 09:01:39 -04:00
90df9cfc15 Report per-language UI translation coverage (#220) (#427)
* feat(xtask): report UI translation coverage

Add a deterministic per-language coverage report backed by a checked English key inventory. Reject malformed, duplicate, and unknown catalog keys, and document the invocation.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

Copilot-Session: f77709e9-6475-4953-ab57-b78347b4499d

* fix(xtask): derive i18n coverage keys from source

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

Copilot-Session: e5e4c3e6-e406-4010-b05d-5042f9f4e4fc

* fix(xtask): handle Windows line endings in i18n scan

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Copilot-Session: e5e4c3e6-e406-4010-b05d-5042f9f4e4fc

* Scorecard: DIST-220-4 is partial now that xtask reports translation coverage

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

---------

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Co-authored-by: Brandon Thomas <echelon@gmail.com>
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Copilot-Session: f77709e9-6475-4953-ab57-b78347b4499d
Copilot-Session: e5e4c3e6-e406-4010-b05d-5042f9f4e4fc
2026-10-07 07:54:05 -04:00
b45dfde498 i18n: complete Simplified Chinese catalog and localize brush sections (#359)
* i18n: complete Simplified Chinese UI catalog and brush labels

* i18n(zh-hans): translate labels added on main since this PR (opening/cancel, layer navigation)

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

* i18n(zh-hans): keep main's Mixer Brush entries, translate the rendering-mode strings added since (#434)

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-07 07:46:52 -04:00
b5eed81a25 Start context-menu parity across canvas, paths, and document tabs (#556)
* Add blend modes to Gradient tool

* Establish context menu parity contract and first workflows

* Align Select menu actions and style selection context rows

* Add Pen canvas Make Selection context action

* Complete Pen path context menu and symmetry actions

* Select menu: no extra top-level Feather (keep Select > Modify > Feather only, for 1:1 menu parity)

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

* Regenerate docs/scorecard.md and docs/parity.md

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

* Regenerate docs/scorecard.md and docs/parity.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-07 07:45:26 -04:00
Sean Lavery 8aecbffb5c Add blend modes to Gradient tool (#543) 2026-10-07 07:43:36 -04:00
b4f9d56c93 Add the Patch tool with a live preview (#213) (#381)
* Add the Patch tool (#213)

paint.patch heals the selected area with texture from where the
selection is dragged (Source) or copies the selection onto the
dragged-to area (Destination), seamless-cloned like the Healing Brush.
The solve is clipped to the canvas, so patches at the edge stay opaque.

Tool::Patch joins the J group: outside the selection it draws a lasso
(Shift/Alt add/subtract), inside it drags the patch, the outline follows
the pointer and the offset is clamped to the canvas. Options bar:
Source / Destination. Strings translated in all six catalogs.

Tests at 8/16/32 bit, a UI gesture test, panic_hunt green. Scorecard
TOOL-213-3 is partial: the drag previews the outline, not the pixels.
retouch_bench: patch 250x250 22 ms, 1000x1000 362 ms (release).

generated with Claude Code

* Patch tool: live preview of the heal while dragging (#213)

The canvas shows the document healed at the pointer's offset
(patch_preview.rs, modelled on the Move tool's preview) and
recomposites only the healed area; release commits the exact
paint.patch and its damage rect refreshes the same area.

The preview (retouch_cmds::patch_preview) solves the colour fit on a
coarse grid (at most 128x128 cells) and interpolates it back: the
Poisson result is source + a harmonic correction, so the texture stays
exact and only the smooth correction is approximated (worst pixel 0.022
at 8x coarser in the test). Selection coverage is read tile-wise and
the interpolation runs per row in parallel.

retouch_bench (release, 6016x6016): preview 250x250 7 ms, 500x500
17 ms, 1000x1000 52 ms (exact patch: 21 / 90 / 354 ms). Scorecard
TOOL-213-3 done. Also applies rustfmt to the first commit's code.

generated with Claude Code

* Patch tool: cap patches at 16M pixels, drop product names from comments (#213)

The solve needs roughly 150 bytes per pixel, so the old 64M cap allowed
about 10 GB and risked an allocation abort instead of an error.

generated with Claude Code

* Regenerate docs/scorecard.md

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

* Tool::ALL holds both the Patch tool and the Mixer Brush (rebase onto #433)

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-07 07:42:30 -04:00
cc5de414dc Camera Raw: interactive histogram, clipping warnings and scopes (part of #215) (#407)
* Camera Raw: interactive histogram, clipping warnings and scopes (part of #215)

- RGB histogram of the preview proxy (algo::histogram), drawn as RGB ribbons;
  Before/After, five draggable tonal zones, shadow/highlight clipping warnings
  (U/O) and Alt-drag channel diagnostics.
- Pixel hover readout, up to nine RGB/Lab colour samplers (S), an ICC-managed
  vectorscope (algo::vectorscope) with skin-tone line, and a floating scope panel.
- Settings regrouped as Light, Color, Effects, Curve, Color Mixer, Color Grading
  and Detail; Camera Raw labels translated through the cameraRaw context.
- One point-curve editor (point_curve.rs) shared by Camera Raw, the Curves dialog
  and adjustment-layer Properties; fixes Camera Raw curve points not dragging.
- Double-clicking a Camera Raw smart filter reopens the Camera Raw dialog with its
  settings and updates that filter, instead of the generic parameter form.
- New point curves are validated (2-16 points, increasing inputs); stored Smart
  Filters keep loading.
- camera_raw_bench for P31/P36 (not enforced until a baseline includes them).

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

* Histogram tests: as_chunks for triangle indices (clippy 1.99)

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

* Regenerate docs/scorecard.md after rebasing onto main

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

* i18n tests: close the Mixer Brush test before the Camera Raw one (rebase)

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-07 07:40:55 -04:00
08e3e610c0 Preferences › Interface: Show bounding box when dragging layer (#484)
* Preferences › Interface: Show bounding box when dragging layer

New pref `interface.showBoundingBoxWhenDraggingLayer` (default off).
On: a Move drag shows the layer's outline and an arrow; the pixels stay
put until release. Off: the layer follows the pointer live with no
outline (Photoshop's behaviour). Label translated in all six catalogs.

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

* Keep the drag outline by default; the preference only turns off the live pixels

Off (the default) keeps today's Move drag: pixels follow the pointer and
the moved layers' outline is drawn. On, only the outline and arrow move.

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

* Regenerate docs/scorecard.md

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-07 07:12:58 -04:00
45d0f4ad93 docs: sync scorecard with implemented behavior (#348)
* docs: sync scorecard with implemented behavior

* Scorecard: cite symbols instead of line numbers in the updated bug notes

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

* 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-07 07:06:21 -04:00
floflo777 6422219226 Control protocol docs: file.open and file.save are refused over --control (#391)
Two passages said they worked like app.open/app.save. Say what the policy
really refuses, and pin those cases in its test.
2026-10-07 07:05:38 -04:00
d91ab25772 Expose the Mixer Brush as a UI tool (#213) (#433)
* feat(ui): expose Mixer Brush tool (#213)

Wire the existing Mixer Brush command into the selectable UI tool model, options bar, and control-driven painting path. Cover shortcut cycling, options persistence, and selection clipping.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

Copilot-Session: f77709e9-6475-4953-ab57-b78347b4499d

* docs(scorecard): mark Mixer Brush slice complete (#213)

Reflect the new Mixer Brush toolbar, options, control dispatch, and selection tests in TOOL-213-2 and regenerate the scorecard.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

Copilot-Session: f77709e9-6475-4953-ab57-b78347b4499d

* fix(i18n): translate Mixer Brush labels in zh-Hans

Add Simplified Chinese entries for the Mixer Brush tool, its options, and the brush-settings heading. Test all Mixer Brush UI strings against every registered language catalog.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

Copilot-Session: f77709e9-6475-4953-ab57-b78347b4499d

* Polish Mixer Brush UI controls

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

Copilot-Session: e5e4c3e6-e406-4010-b05d-5042f9f4e4fc

* 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>
Copilot-Session: f77709e9-6475-4953-ab57-b78347b4499d
Copilot-Session: e5e4c3e6-e406-4010-b05d-5042f9f4e4fc
2026-10-07 07:04:31 -04:00
48bd08150e feat(ui): add bounded Liquify redo history (#435)
* feat(ui): add bounded Liquify redo history

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

Copilot-Session: f77709e9-6475-4953-ab57-b78347b4499d

* 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>
Copilot-Session: f77709e9-6475-4953-ab57-b78347b4499d
2026-10-07 06:59:03 -04:00
7313e08141 32-bit documents: Levels don't clip the input and output ranges (#403)
* 32-bit documents: Levels don't clip the input and output ranges

Photoshop's float Levels keep the curve going past the black and white points
and use a plain power for the gamma. rgb32 and gray32 levels.psd now match
within 0.5/255 (was 10/255).

* Raise the Photoshop oracle floor to 132

* Levels: destructive Image › Adjustments › Levels doesn't clip in 32-bit either; neutral comment wording

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-07 06:53:56 -04:00
b41442c466 Color Picker: sample colours from the image with a pipette (#508)
* Color Picker: sample colours from the image with a pipette

With the Color Picker open, the pointer over the image turns into a
pipette, as in Photoshop. A click or drag there samples the composite
colour into the picker's new colour, and OK applies it. This works
whatever tool is selected; Space and middle-drag still pan. Over the
dialog the pointer is the normal arrow.

Agents get the same: `ui.pointer` (MCP `ui_pointer`) samples into an
open picker instead of driving the tool.

* Color Picker tests: rename the eyedropper test's colour helper after the rebase

#485 added a `color` test helper in the same module.

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-07 06:46:25 -04:00
eb2fb2634f Add rendering modes and CPU fallback recovery (#434)
* Add rendering policy with legacy preference migration and safe adapter selection

* Add rendering mode preferences and GPU fallback recovery warning

* Complete CPU fallback recovery and validate rendering mode UI

* Simplify GPU recovery and reuse preference persistence

* Rendering modes: rebase onto the preferences merge/retry save path; drop docs screenshots

save_preferences now writes through write_prefs (merging other instances' changes) and
only marks the revision saved when the write succeeds, so a failed recovery choice is
still retried. Removes a leftover diff3 marker in es.tsv, the docs/images/rendering-*.png
screenshots and their ATTRIBUTION row (no binary assets in the repo), and regenerates
docs/scorecard.md for the new performance.renderingMode preference.

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-07 06:11:06 -04:00
51d85c1170 Linux: AppImage update information and a .zsync beside it (#349) (#399)
* Linux: AppImage update information and a .zsync beside it (#349)

appimagetool now embeds
gh-releases-zsync|<owner>|<repo>|latest|photocraft-*-linux-<arch>.AppImage.zsync
(owner/repo from GITHUB_REPOSITORY, default storytold/photocraft), so
AppImageUpdate, AppImageLauncher and the like can find the next release and
fetch only the blocks that changed. With zsyncmake on the host it also writes
the .zsync, into its working directory, so the script runs it in $DIST and
warns when the file is missing.

The release job installs zsync and checks the embedded string and the .zsync
before uploading; docs/releasing.md lists the new asset.

Verified locally with packaging/linux/package.sh --skip-build --formats
appimage and zsyncmake from Ubuntu 22.04: --appimage-updateinformation prints
the string, and the .zsync's URL is the AppImage's basename.

* package.sh: absolute AppDir and appimagetool paths before running in $DIST

appimagetool now runs inside $DIST so zsyncmake writes the .zsync there,
which broke relative CARGO_TARGET_DIR / APPIMAGETOOL paths (AGENTS.md
suggests target/agent-<name>). Resolve both first, remove a stale .zsync
so a missing zsyncmake is reported, and list the .zsync and zsyncmake in
the header.

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

---------

Co-authored-by: bflatastic <bombayapirat@gmail.com>
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-07 05:26:01 -04:00
BFlatandClaude Opus 5.5 2628b0dd3d Preferences merge with other tabs/windows and retry failed saves; web startup shows a Reload hint (#475, #476, #477) (#561)
* Preferences: saves merge with what another tab or window stored, and failed writes are retried (#475, #476)

A save now writes only the values this instance changed since it last
loaded or saved them over what storage holds (a three-way merge over the
JSON tree; arrays are leaves), so a second browser tab no longer reverts
the other's changes. The saved revision moves only when the write
succeeds: a failure is retried after 2 s, doubling to 30 s (sooner after a
newer change), with a status message on the first failure and a notice
while it keeps failing. Shared by the desktop and web builds.

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

* Web: a startup that never gets going shows what may have failed and a Reload link (#477)

When the .wasm fails to download or doesn't match the page, no Rust runs,
so the loading page itself (CSS only, no script) adds the hint after 20 s.

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

---------

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-07 15:07:19 +07:00
BFlatandClaude Opus 5.5 7eb3b2e072 Automation: non-object params rejected; headless sees finished jobs; serve stdio survives a bad line; MCP batch keeps results (#503, #504, #505, #506, #529) (#546)
* Command params that are not a JSON object are an error (#506)

`Session::dispatch` now rejects array, string, number and boolean params
with "invalid parameters for `<id>`: params must be a JSON object" instead
of running the command with every default; `null` still means no params.
The one check covers the control channel, `serve`, MCP (headless and
bridge) and the CLI, and batch steps fail per step. `photocraft-cli run
--new` prefixes its error with `--new`.

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

* Headless requests apply finished background jobs first (#503)

A `wait: false` job was applied to its document only by a later command
or `jobs.list`, so `doc.save`, export, `doc.inspect`, `doc.render` and
`session.list` (and their MCP tools) still read and wrote the document
without it. `Headless::sync_jobs` now runs at the top of every JSON-lines
request and before every headless MCP tool call.

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

* `serve` stdio keeps the session after a rejected request line (#505)

An over-long request line (or one that is not UTF-8) ended the stdio
session, losing every open document. It now gets one error reply, the
rest of that line is skipped without being dispatched, and the next
request is served. `read_bounded_line` reads bytes, so a cut inside a
multi-byte character is reported as too long instead of an I/O error.
TCP connections still close after an oversized line.

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

* MCP `command_batch` keeps its step results when the reply budget stops it (#504)

The batch budget charged each step's plain JSON size, but MCP sends the
batch reply as escaped text content, so a batch the budget had already
stopped could still exceed the tool-result ceiling and come back as a
bare "response exceeds" error. MCP (headless and bridge) now uses
`BatchReplyBudget::escaped`, which charges the escaped size, through the
new `Headless::batch_with_budget`; `serve` keeps the plain budget.

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

* docs: the 1 MiB request-line limit does not apply to MCP stdio (#529)

The book stated the 1 MiB request-line limit without naming a transport.
It applies to desktop and headless TCP and to the headless JSON-lines
stdio server; MCP over stdio reads its own lines and only its tool
results are size-checked. Say so in the automation security pages and in
the control protocol's transport limits.

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

---------

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-07 14:00:36 +07:00
BFlatandClaude Opus 5.5 8f0f95959d Control channel: menu/dialog requests wait for their job; right-click menus at the point; MCP ui_pointer button (#510, #511, #512, #514) (#548)
* ui.inspect descriptions point to ui.menu.list for the menu tree (#510)

The control-protocol doc, the control.rs method list and the MCP
`ui_inspect` description said `ui.inspect` returns the menu tree; it
never did. They now say the menu tree comes from `ui.menu.list` (over
MCP, through `control_call`).

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

* ui.menu.invoke and ui.dialog.confirm wait for the job they start (#511)

Since filters became background jobs, invoking a no-dialog filter from
the menu or confirming a filter dialog over the control channel replied
`{job, pending: true}` before the filter was applied, unlike v0.2.0 and
unlike `engine.execute`. All three now share one helper that runs the
command with script events off and, when it started a job, defers the
reply until the job's result lands; `"wait": false` keeps the immediate
`{job, pending}` reply. The now-unused `run_automation` is gone.

The new test drives Blur More through `ui.menu.invoke` and Gaussian Blur
through its dialog's OK, with and without `wait` (it failed before).

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

* ui.pointer right-click opens its menu at the pressed point (#512)

A `ui.pointer` right-click listed the layers at the requested document
point but drew the layer menu at the canvas centre, and the Brush Preset
picker opened there too. Both now open at the point's screen position,
where a mouse right-click on it opens them. `ViewXform::active` gives the
active canvas's document-to-screen mapping as last laid out (the
construction several tests repeat); the centre stays the fallback when
the point can't be mapped.

The ui.pointer doc now lists `button`, both menus, and that top-level
modifier flags are ignored when `modifiers` is given.

The new test checks two points against the mouse path and the picker
position (it failed before).

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

* MCP ui_pointer forwards button and rejects unknown arguments (#514)

Bridge clients couldn't right-click through `ui_pointer`: it forwarded
only `events` and `modifiers` and silently dropped anything else
(`button`, a typo, a top-level `space`). It now forwards an optional
`button`, and `PointerParams` denies unknown fields, so the schema says
`additionalProperties: false` and a stray argument is an error.

The new bridge test checks the forwarded params, the rejection of
`buton` and top-level `space`, and the tool schema (it failed before).

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

---------

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-07 13:55:01 +07:00
BFlatandClaude Opus 5.5 e30b121969 History: tool and preset names, Frame from Layers as one step, undo/redo restore the targeted layers (#513, #528, #446, #496, #495) (#551)
* Elliptical Marquee selections record "Elliptical Marquee" in History (#513)

Both marquee tools commit through `select.rect`, which named every step
"Rectangular Marquee" whatever its `ellipse` param. The step is now named
after the tool that made it; the rectangular marquee keeps its name.

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

* Preset filters record their own names in History (#528)

Blur, Blur More, Sharpen, Sharpen More, Sharpen Edges and Despeckle run
a generic algorithm with fixed parameters and recorded its name
("Gaussian Blur", "Unsharp Mask", "Surface Blur"). The presets now live
in one table with their names, used for the history step and the job
progress text; Gaussian Blur, Unsharp Mask and Surface Blur keep theirs.

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

* Vertical type command no longer claims it renders horizontally (#446)

Vertical type has rendered in columns since #199, but the params text of
`type.orientation.vertical` still said it was "rendered horizontally for
now". It now documents the same `{"layer":id?}` as Horizontal.

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

* Frame from Layers is one history step (#496)

It grouped the layers through `layer.new.groupFromLayers` (its own
"Group Layers" step) and then recorded "Frame from Layers", so one undo
left the group behind. The grouping step is now folded into the frame
step with `purge_last`, as Transform Again on a copy does; if the group
can't be framed, the grouping is undone with nothing left to redo.

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

* Undo and redo restore the layers each history state targeted (#495)

History states held only the document, so after undo or redo the active
layer stayed whatever it was: redoing New Layer left the old layer
active (a Fill then painted it), and undoing Delete Layer left the
restored layer unselected. Each state now also keeps the active and
selected layers it had when it was created (when the document was
opened, or right after the step that made it), and undo and redo target
them again where they still exist. Selecting a layer is still not a
step and doesn't change any state's target, so selecting another layer
and then undoing or redoing goes back to that state's target, as the
reference app's Step Backward does.

The History panel lists redo labels from a new `redo_labels` accessor
instead of cloning and replaying the history every frame.

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

---------

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-07 13:54:10 +07:00
BFlatandClaude Opus 5.5 bd1322a5e4 Recovered documents keep their recovery data until saved or closed (#444) (#549)
Launch no longer deletes recovery entries right after loading them. A
recovered document adopts the entry it came from: its next autosave
replaces it in place (atomically) and saving or closing the document
removes it, so a second crash before the next autosave loses nothing.
New documents autosave under per-launch keys, so a document id reused
from an earlier launch never overwrites a surviving entry, and sidecars
whose key doesn't match their file name are ignored.

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-07 13:53:48 +07:00