New engine command `layer.showOnly {"layer"}`: hides every other layer
(keeping the layer's enclosing groups visible); run again on the same
layer, it restores every layer's previous visibility, layers that were
already hidden included. ⌥-clicking another eye while one layer is shown
alone moves the solo and keeps the original snapshot. The snapshot is
per-document view state (`DocState::show_only`), not history.
The Layers panel sends ⌥-clicks on an eye to it; plain clicks still toggle.
Co-authored-by: Jason Short <3344856+jason4short@users.noreply.github.com>
Co-authored-by: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
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.
Selecting "Keyboard Shortcuts" in Edit > Search did nothing (#536): palette::show
and dialogs::show run in the same frame (lib.rs:959-960), so the dialog's OK footer
(dialogs.rs:183) read the very Enter that launched it as "confirm" and closed the
dialog before it was ever visible. Every palette-launched dialog hit this when
opened with Enter; the mouse path was unaffected.
The palette now consumes the Enter it acts on (input.consume_key, the same idiom
menu_nav already uses), so dialogs::show sees no press in that frame. A second Enter
still confirms and closes the dialog, which the regression test also asserts.
Test: palette_opens_the_keyboard_shortcuts_dialog drives the real UI harness
(search -> type -> Enter -> dialog open, with and without a document). Full ui-egui
suite 677 pass; clippy -D warnings; fmt; wasm32 check of ui-egui.
Fixes#536
* 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>
* 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>
* 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>
The blend ran against the colour stored in transparent pixels (black),
so Multiply and the other darkening modes filled an empty layer with
opaque black. The blend now acts only where the layer has pixels, and
the alpha grows by source-over, so an empty layer takes the source in
any mode. Opaque pixels blend exactly as before.
`apply_tiled_with` filters tiles with `chunk.par_iter()` and each worker
reports progress (`0.98 * n / total`) from its own thread. The values are
monotonic in the finish counter, but delivery order is not, and the test's
callback did a non-atomic load->assert->store of the last value: when a
worker holding a smaller value was preempted past another worker's store,
the assert fired. Observed once in a full `cargo test --workspace` run;
reproduced 1-in-300 under synthetic CPU load.
`Interrupt` never promised ordered delivery and its engine consumer
already clamps (`JobCtx::progress` keeps `fetch_max` of the f32 bits, see
crates/engine/src/jobs.rs). The test now does the same: fetch_max high-water
mark plus an in-range assert, and requires the mark to reach exactly 1.0.
Renamed to `..._progress_never_regresses` to say what it actually checks.
Stress after the fix: 0 failures in 300 runs.
The identical-looking asserts in content_aware.rs and io/tests/cancel.rs
stay strict: those paths report progress from a single thread.
* 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>
* 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>
* 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>
* 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>
* Add warp splits and grid presets
* Translate the warp grid labels
* Satisfy clippy on the warp section-edge check
* Address the warp review notes
* Format the warp handle arm
* i18n: French and Indonesian warp grid labels
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>
* 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
* "id" Indonesian
* extra space for fomat (ubuntu-latest)
* Update id.tsv
* Update mod.rs
* i18n(id): translate the labels added on main since this catalog; Mixer Brush terms
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>
* i18n: add French (fr) UI translation
A complete French catalog (menus, dialogs, panels, plurals), registered with its
plural rule (0 and 1 take the singular). Covers the latest commands, Stamp Visible
and Stamp Down included. A test checks the bundled UI fonts carry every French glyph.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
* i18n(fr): translate labels added on main since this PR (from #582 by neko233-com)
Co-Authored-By: neko233-com <242357072+neko233-com@users.noreply.github.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* i18n(fr): translate the labels added on main since this catalog
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* i18n(fr): Camera Raw scopes and the Patch tool
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>
Co-authored-by: neko233-com <242357072+neko233-com@users.noreply.github.com>
* 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>
* 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>
* 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>
* 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>
Every open path read the whole file with std::fs::read, which asks
Windows for up to 4 GiB in a single ReadFile. Some file systems (exFAT,
network redirectors) reject a read that large with
ERROR_INVALID_PARAMETER, "The parameter is incorrect" (os error 87),
as reported for a 4.75 GB PSB in #375.
Add photocraft_format::read_file and read_all beside atomic_write:
64 MiB reads into a buffer reserved up front, and an OutOfMemory error
that says how much the file needed. Route every document read through
them:
- the background open job (engine jobs.rs read_path);
- the engine's read_file;
- Open Recent and OS open events;
- desktop drag and drop, which now goes through open_path (and so
through the background job when jobs are on);
- the File > Open dialog;
- .pcraft loading;
- the automation workspace read and automation::files::open;
- smart object Replace Contents and linked files.
The dialog no longer fails silently: PickOpenFn returns the picked name
with the read result, and a failure is reported through open_failed and
the notices like any other open failure.
Speed is unchanged (1.11 s vs 1.13 s for a warm 4.75 GB file).
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
* PSD: read smart-filter masks (FEid) with one section per smart object
Photoshop writes the global Filter Effects block (`FEid`/`FXid`) as a
version followed by one length-prefixed section per filtered smart
object, each padded to 4 bytes outside its length. The reader took the
first section's length as the whole body, so any document with two or
more filtered smart objects failed with "filter effects: N unexpected
bytes after the data": the smart filter masks were not imported and the
open reported a warning. The 28 FEid files in the public corpora each
hold a single section, which is why the corpus never hit it.
The reader now walks every section (still accepting several items in one
section, as earlier PhotoCraft versions wrote), and the writer emits one
padded section per item, as Photoshop does.
Tests: reads_one_section_per_item (fails before: parse error) and
reads_several_items_in_one_section; malformed_data_is_an_error now cuts
only before the end of the section.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* PSD FEid: an empty item list still writes one section, so it reads back
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
---------
Co-authored-by: EthanSuquet <292885762+EthanSuquet@users.noreply.github.com>
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Co-authored-by: Brandon Thomas <echelon@gmail.com>
* Content-Aware Scale: 13x faster seam removal, bit-identical output (part of #211)
Every removed seam recomputed the whole energy map, ran the seam search with an allocation per seam and copied the whole image into a new buffer, all on one thread: about 320 ms per seam on 24 MP. Now the image, protection and energy buffers keep their row stride and a seam is removed by shifting the rest of each row in place (in parallel); only the pixels next to the removed seam get their energy recomputed, with the same per-pixel arithmetic; the seam search reuses its buffers and its interior loop has no edge tests, so it vectorizes. Ties are broken exactly as before.
A new test pins the output bits of eight carves (shrink, enlarge, both axes, protection, 1/3/4 channels, tie-heavy noise), taken from the previous implementation.
* Content-Aware Scale: oracle test against the previous carver; keep the 1 px enlarge fix
carver_matches_reference compares the new carver bit for bit with the
implementation it replaced (now kept in the tests) over 400 random carves and
200 seam searches and energy maps. The rebase keeps main's #387 fix: a 1 px
wide image's one seam is taken so it can be duplicated.
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>
* Paste: a copied image file pastes its pixels (part of #338)
Copying a file in GNOME Files, Finder or Explorer puts its path on the clipboard, not pixels, so Edit > Paste found no image and said the clipboard was empty. When the clipboard has no image, the native clipboard service now reads arboard's file list and pastes the first file that decodes (upright, as RGBA8), skipping non-images after reading only their header. TGA, which has no magic number, also needs its extension.
arboard splits text/uri-list on LF only, but the list is CRLF-terminated (RFC 2483) and GTK writes it that way, so paths copied in GNOME Files arrive with a trailing '\r'; it is trimmed before opening.
* Paste: copied files before the clipboard image; require arboard 3.6
Finder puts a copied file's icon on the clipboard as an image next to
the file URL, so reading the image first pasted the icon on macOS. The
file list is now read first and the plain image is the fallback.
file_list() needs arboard 3.5 or later, so the requirement says 3.6
(what the lockfile already has).
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>
* 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>
* Clarify integer and float bit-depth labels
Add type-specific labels to New Document and Image Mode with translations and focused UI/catalog tests.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Copilot-Session: f77709e9-6475-4953-ab57-b78347b4499d
* Fix bit-depth labels and tooltips
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Copilot-Session: e5e4c3e6-e406-4010-b05d-5042f9f4e4fc
* Keep the reference menu names for the depth commands and preference
The Image > Mode items keep their menu-catalogue labels, so the engine
labels (command palette, MCP, Keyboard Shortcuts) and the preference
choice stay "8/16/32 Bits/Channel" to match; the integer / floating point
hint comes from the new tooltips.
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
* Show active ICC profile in status bar (#321)
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Copilot-Session: f77709e9-6475-4953-ab57-b78347b4499d
* Fix active profile status review feedback
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Copilot-Session: e5e4c3e6-e406-4010-b05d-5042f9f4e4fc
---------
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Copilot-Session: f77709e9-6475-4953-ab57-b78347b4499d
Copilot-Session: e5e4c3e6-e406-4010-b05d-5042f9f4e4fc
* PSD: read/write the artboard lock at Photoshop's lspf bit 3 (#395)
Photoshop writes layer locks with the artboard bit at 0x08 (a Background
layer carries 0x0D). PhotoCraft read it at 0x10, so 0x0D came back as
0x05 and the lock bytes drifted on every re-save. Map artboard to bit 3
on both read and write; still accept 0x10 on read so older PhotoCraft
files keep their lock, rewriting it at 0x08.
Co-Authored-By: Qwen 3.8 Flash <noreply@qwen.ai>
* style(io): fix PSD artboard lock review comments
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Copilot-Session: e5e4c3e6-e406-4010-b05d-5042f9f4e4fc
---------
Co-authored-by: Qwen 3.8 Flash <noreply@qwen.ai>
Copilot-Session: e5e4c3e6-e406-4010-b05d-5042f9f4e4fc
* 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>
* 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
* Lasso inside liquify mode , with hotkeys L, alt , ctrl+i, ctrl+h for visibility
* Liquify Freeze Lasso: scanline fill, tests, one translated tooltip, theme colours
- apply_lasso fills node rows with an even-odd scanline pass (O(rows x edges) plus the nodes
written) instead of a point-in-polygon test per node; non-finite points are skipped and huge
or off-canvas polygons clamp to the field.
- LassoMask joins LiquifyTool::ALL, so the strip draws its button in the loop; its tooltip is
one translated string (all catalogs); the outline uses theme tokens.
- A closed lasso clears Redo (#435).
- filter.liquify params doc lists lassoMask.
- Tests: concave polygon vs. a reference even-odd test at several cell sizes, thaw, degenerate
and hostile polygons, replay; the UI lasso records a stroke, Alt thaws, undo/redo.
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>
* Smart objects keep their four-corner (Distort / Perspective) placement
A smart object's placement was an affine transform, so a perspective
placement lost its fourth corner: a mockup PSD's distorted screen looked
right on open (Photoshop's cached pixels) but re-rendered as a
parallelogram after Edit Contents → Save.
- SmartObject gains `perspective`, the full projective map (source px →
document px); `transform` keeps its affine approximation
- Re-renders go through it (place_source_projective), as do moves,
canvas rotations and edit.transform, so Distort / Perspective now work
on smart objects instead of being refused
- PSD import reads the quad (nonAffineTransform / Trnf) and export writes
it back; .pcraft stores the field (absent for affine placements)
- Test: a distorted smart object survives Edit Contents → Save
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
* Smart objects: warp maps through a distorted placement; perspective round-trip tests
Warp handles and stored warps on a Distort / Perspective smart object go through its full
projective placement (smart_cmds::placement) instead of the affine approximation, so they sit
on the distorted corners. Tests: SoLd perspective round trip and quad_moved (io), a perspective
smart layer in the .pcraft rich_doc, warp on a distorted smart object (engine). Comments no
longer name another product.
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>
* 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>
* fix(transform): undo inside Free Transform steps through the transform
Undo while transforming used to undo the document's last step under the
open box. Now, as in Photoshop, Undo and Redo step back and forth through
the session's own changes: each handle drag, nudge or option edit is one
step. Toggle Last State and the History panel's rows wait until the
transform is committed or cancelled. A press on a preset warp turns it into
a custom mesh only when it grabs a point, so a miss records no step.
* fix(transform): widen the Free Transform handle grab radius
Handles now grab within 12 pt instead of 8, so a corner is easier to catch
without rotating instead. The Move tool's transform controls and snapping
use the same radius. On boxes smaller than the grab areas the nearest
handle wins, so the reference point doesn't take presses on corners.
* fix(transform): route every Undo through the open transform
The interception moves from the menu path into PhotocraftApp::run, so edit.undo and edit.redo from the control channel and MCP step through the box like the menu and shortcuts do. edit.toggleLastState is refused while the box is open instead of changing the document under it.
* chore(transform): describe behaviour without naming other products
With characters selected while editing type, a new foreground colour from
the Color panel, the Swatches or the Color Picker now recolours them, as in
Photoshop, within the editing session's history step. The HSB sliders now
act only on an edit: their h/s/v round trip isn't exact, so comparing
values rewrote the foreground every frame.
* fix(ui): close the polygonal lasso on double-click
As in Photoshop, double-clicking closes the polygon at the last point. The
second click is handled in the click dispatch, so it never starts a new
polygon, including when the first click already closed it on the first
vertex. egui reports it as a triple click when a vertex went down shortly
before, so that counts too.
* fix(ui): a new polygonal lasso replaces the selection in one step
The first click fixes the selection mode. A new-selection polygon hides the old outline while it is drawn instead of deselecting, so closing it is one history step and Esc leaves the selection untouched. Enter closes the polygon in the mode it started in.