* photocraft-plugins: sandboxed WebAssembly filter plug-ins (wasmi host, ABI v1)
A new L4 crate runs plug-ins in wasmi (pure Rust, MIT/Apache) with no host
imports, a fuel budget metered in slices with a wall-clock deadline, a memory
cap, a recursion cap and wasmi's strict module limits. Every band gets a fresh
instance; traps, budgets, bad manifests and bad pointers are all Errors.
Pixels go over as interleaved normalised f32 in tile-aligned bands, in
parallel on native, blended by the selection. Includes the example Invert
plug-in (no_std Rust, examples/plugins/invert-rs) built into a test fixture,
hostile-module tests, and docs/plugins.md.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* Engine: plugin.* commands (install, list, run, remove, reload)
plugin.run filters the active pixel layer, a targeted channel or a smart
object (recorded as a smart filter) as one undo step; Preferences >
Plug-ins gains Use Additional Plug-ins Folder, loaded when prefs apply.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* UI: Filter > Plug-ins lists installed plug-ins with generated dialogs
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* Plug-ins: bands never outgrow the memory cap; regenerate parity
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* Bench: example plug-in Invert vs built-in Invert on 24 MP
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
* CMYK documents display and export through their embedded CMYK profile
A thread-local CmykSpace (photocraft_color::convert::with_cmyk_space) replaces the built-in
coated CMYK while the CPU compositor, the GPU compositor's uploads and the PSD merged
composite convert a document's CMYK pixels; GPU residents track the profile they were
converted with.
Refs #46
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* EXR/HDR are linear: tag imports linear sRGB, linearise exports
Refs #46
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* Colour-managed canvas: document profile -> monitor profile on GPU and CPU paths
* engine display_color: CanvasDisplay per (document profile, mode, monitor), identity fast
path when they match (sRGB on sRGB costs nothing); linear composites are stored
sRGB-encoded in the 8-bit texture. canvas_lut now always carries the CM transform, with
Proof Colors / Gamut Warning / 32-bit preview on top.
* GPU canvas applies the display LUT whenever the transform isn't the identity (also for
filter/proxy previews); the CPU canvas and Navigator transform the 8-bit composite.
* Edit > Color Settings > Monitor Profile (auto|built-in|.icc). macOS 'auto' reads the main
display's ICC profile at launch via osascript/AppKit (no unsafe); elsewhere sRGB.
* Tests: P3 (hand-computed), Adobe RGB, ProPhoto, Gray Gamma 2.2, synthetic CMYK, linear,
monitor setting; GPU+CPU canvas test via kittest (skips without Rgba32Float).
Fixes#46
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* io: tag EXR/HDR linear in import_flat (adapt to the shared image_to_document)
#65 moved flat decoding into image_to_document(name, img) so camera raws can reuse it; the
linear-EXR tagging needs the file bytes for format detection, so it now runs in import_flat.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
* File open/save: set the path everywhere, Open Recent, show errors and import/export warnings
File › Open, drag-and-drop onto the window and launch paths now share
PhotocraftApp::open_file: the document is named after the file (not the full
path), keeps its path so File › Save writes back, and goes to Open Recent.
Failures are shown as errors (status bar + a non-blocking notice).
Import/export warnings from photocraft-io are no longer thrown away: they show
in the status bar and in a dismissable notice (native and web), and the
control protocol returns them from app.open/app.save/file.open/file.save/
file.saveAs/file.saveACopy. .pcraft is in the Open and Save filters, and
File › Save writes .pcraft documents in place.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* macOS: open files from Finder, Open With and the Dock (Apple events)
Finder double-click, Open With, drops on the Dock icon and open -a deliver
files as kAEOpenDocuments Apple events, not argv, and winit 0.30 ignores them.
Register an NSAppleEventManager handler through the small fmv-macos-events
crate (MIT OR Apache-2.0, 168 lines, audited, pinned =0.1.0; it keeps the
Objective-C unsafe out of our forbid(unsafe_code) workspace) before the event
loop starts, so both the launching event and later ones reach the UI as
OsEvent::Open; the window is focused. The quit Apple event goes through the
normal close path.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
* Shortcuts match like Photoshop: Ctrl/Cmd + '+' zooms, exact modifiers, clipboard keys
- Cmd/Ctrl+'=' also matches '+' (Shift+= on US layouts, numpad +, the '+' key on
Nordic/German layouts); Delete also matches Backspace (the Mac delete key).
- Modifiers match exactly (Cmd+Alt+I no longer runs Cmd+I); Shift shortcuts on
punctuation match the shifted character (Cmd+Shift+; arrives as Cmd+':').
- Zoom In/Out, Fit on Screen and 100% stay live while a dialog or a non-text
widget has focus.
- clipboard_keys: egui-winit turns Cmd+C/X/V into Copy/Cut/Paste events (and
drops Cmd+V when the clipboard holds an image); outside text fields they
become key presses again so Edit > Copy/Cut/Paste/Copy Merged run.
- Snapping: Ctrl (Windows) / Control (Mac) disables snapping only while held
during the drag, not at the press (Ctrl-click belongs to the tool).
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* Dialogs: no cancel on outside click; canvas pans and zooms under them
- Disable egui's keyboard UI zoom: Cmd/Ctrl +/-/0 never scale the interface.
- A click outside a dialog no longer silently cancels it (Esc still does).
- With a dialog open the canvas still scrolls, pinch/Cmd-scroll zooms and
Space/middle/Hand-drag pans, like Photoshop; everything else stays modal.
- Clipboard key presses restored in raw_input_hook.
- Studio Properties card: Order::Middle (menus, popups, dialogs stay above it)
and draggable by its title off the area being worked on.
- Magic Wand options (tolerance, anti-alias, contiguous, sample all layers) in
the Studio themes too.
- egui_kittest tests: Ctrl+Plus zooms the canvas and leaves zoom_factor at 1,
zoom works with a dialog open, outside clicks keep the dialog, scroll and
space-drag pan under it.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* Menu bar: hovering another title switches the open menu
Like Windows and macOS menu bars: with a menu open, moving to another top-level
title opens that menu. Tests for the switch and dropdown row spacing.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
- canvas: partial redraws read last_damage with unwrap_or(EMPTY), and the
GPU cache is looked up with let-else after the upload.
- icons: a missing icon (and missing fallback) yields empty bytes, which
egui shows as a broken image, instead of expect.
- type tool: the caret update is an if-let instead of expect("editing").
- Variables dialog: pages return early if their JSON state is malformed
instead of unwrap.
- parity: the same hunk as #40.
- ui-egui denies unwrap/expect/panic/unimplemented/todo/unreachable
(a one-line attribute in lib.rs).
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Three remaining expect() sites in files no open PR touches:
algo::inpaint::complete and algo::pyramid::gaussian loop with while-let
instead of levels.last().expect(), and ui-egui parity::compute indexes
the menu it just pushed instead of last_mut().expect().
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Close, Close All, Close Others, Revert, Exit and the window close button
now ask Save / Don't Save / Cancel when a document has unsaved changes.
The action is parked until each dirty document has been answered, then
re-run. Window close requests are cancelled while the prompt is open.
Closes#9
On D3D12 without DXC, wgpu compiles HLSL with FXC, which aborted on
fs_adjust ("internal error: compilation aborted unexpectedly"). wgpu turns
that into a panic, so every GPU test failed on the Windows runner, and the
app would crash where the same path is taken. A bisect on the Windows
runner pinned it to two cases of adjust()'s switch, Exposure and Selective
Color: both looped over dynamically indexed vector components. They are
now written per channel / per range with the same math (GPU-vs-CPU parity
is unchanged and passes on Metal and on the Windows runner).
Compositor::try_new_with_format catches validation and internal errors
while building the pipelines and returns Err; the canvas then uses the CPU
compositor (once, remembered) instead of crashing.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Rust 1.99's clippy adds chunks_exact_to_as_chunks and some_filter, which
broke the Clippy step on every CI runner (it uses the latest stable). All
sites are mechanical: as_chunks drops the remainder exactly like
chunks_exact.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
CI's `cargo fmt --all -- --check` has failed on every run because the
repo had no rustfmt config and was never formatted. Use the same settings
as EffectCraft/FilmCraft (max_width 160, use_small_heuristics Max), which
also keeps the churn small. Formatting only; no code changes.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The canvas only showed effects after OK. While the dialog is open, the
canvas now renders the document with the dialog's style applied: the same
commands OK runs, replayed on a scratch session (no history), cached by
document revision + style fields so switching pages doesn't re-render.
Cancel drops the preview. Works on both GPU and CPU canvas paths
(display_doc). Tests cover the preview, re-render on edit, and cancel.
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
All dialogs (Layer Style, New Document, filters, ...) were pinned to the
centre. Dragging the title row now moves them, clamped so the dialog stays
on screen. The title text is no longer selectable, so dragging it moves
the dialog instead of selecting text.
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Tracks opened file paths (de-duplicated, newest first, capped at 10) in UI
state, and shows them as a dynamic File > Open Recent submenu after Open As…,
plus Clear Recent Files. Clicking an entry reopens that path; a stale index
errors gracefully. Recorded on both dialog opens and path opens (file
associations / control channel). Tests cover tracking/dedupe/cap/listing/clear
and a real open → recent → reopen round-trip.
Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
Some drivers (e.g. Intel Vulkan on Linux/Windows) can't render to Rgba32Float,
so creating the 'fs_content' pipeline panicked with 'Format Rgba32Float is not
renderable' as soon as a document/image was opened. The accumulation format is
now chosen per adapter: Compositor::preferred_acc_format() keeps Rgba32Float
where it's a valid render target and falls back to Rgba16Float otherwise. The
compositor carries the format (pipelines, accumulation texture, byte size and
the readback half-decode all follow it); the app picks it from the eframe
adapter. Capable GPUs are unchanged (parity still exact, 18/18). A new test
forces the Rgba16Float path on any GPU and confirms it renders within display
tolerance (blend + adjustment). wasm green.
Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
ctx.open_url is unreliable on native (notably Windows), so the Help menu,
the Discord buttons, and the start-page links did nothing. Added an open_url
Service hook wired to the `open` crate on native (ShellExecute on Windows);
links::open prefers it and falls back to ctx.open_url (web). link_row now
routes clicks through it instead of ui.hyperlink_to. Test drives every Help
link id + the open() helper through a mock service.
Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
Preserve menu label spacing after egui installs its default menu-bar style.
- Apply six pixels of horizontal padding per side.
- Cover label padding, overlap, and File-menu interaction across themes.
Like Photoshop, the Add a mask button in the Layers panel and the
layer context menu's Add Layer Mask now turn an active selection into
the mask (Reveal Selection) and fall back to Reveal All without one.
Option-click on the button inverts it (Hide Selection / Hide All).
The choice lives in layer_menu_ui::add_mask_command so the button and
the menu agree; it only picks between the existing layer.layerMask.*
commands. Tests cover the mapping, the menu entries with and without a
selection, and that the new mask matches a rectangular selection.
Co-authored-by: Multipad-cyber <246280291+Multipad-cyber@users.noreply.github.com>
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
paint_gradient gains a dither flag: one quantisation step of monochromatic
position-hash noise, breaking banding without colour speckle. paint.gradient
takes "dither":bool (default true, like Photoshop); the options-bar Dither
checkbox now persists (gradient_dither) and is sent. New gradient_dither_
breaks_banding test; clippy + wasm green.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
paint.stroke/paint.pencil now honour a "mode" param (normal|multiply|screen|…),
and the options-bar Mode dropdown sets the session brush's blend mode (it was
decorative). The Eraser is forced to Normal (no blend mode in Photoshop). New
brush_blend_mode_multiply test; clippy + wasm green.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The options-bar Fill dropdown was decorative. It now persists in tool_options
(bucket_fill_pattern) and the click sends contents=foreground|pattern, so
Pattern actually flood-fills with the current pattern. ui tests + wasm green.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Play loops the playhead via timeline.setFrame at the timeline fps, using
egui time (wasm-safe, no Instant) and requesting repaints; pausing/closing
stops it. New pause icon. ui-egui tests + wasm green.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Platform-gated command for Windows Image Acquisition (scanners/cameras).
Reports device availability; real WIA COM device I/O is Windows + hardware
only (Photoshop gates this item to Windows too), so off-Windows it returns
available:false with zero devices. 1 test. Parity 624 -> 625 (100.0%);
FLOOR 620. Every Photoshop menu item now has a live command.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
layer.videoLayers.{newVideoLayerFromFile,replaceFootage,interpretFootage,
reloadFrame} load frames from a single image or an image-sequence folder;
file.import.videoFramesToLayers adds each frame as its own layer;
file.export.renderVideo composites the whole document at every timeline
frame and writes an image sequence (clean-room, no proprietary codec).
6 tests. Only Windows-only WIA Support remains. Parity 618 -> 624 (99.8%);
FLOOR 619.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Layer.video (VideoData: frame surfaces + fps + source); the layer's
displayed content is the frame at the timeline playhead, re-synced after
any edit/navigation. Commands: New Blank Video Layer, Insert Blank /
Duplicate / Delete / Restore Frame, Restore All Frames, Show Altered
Video, Rasterize (Layer > Video Layers > Rasterize and Rasterize > Video).
Timeline duration grows to fit. 4 tests; engine green, clippy clean, wasm
OK. Parity 609 -> 618 (98.9%); FLOOR 613. (File-based footage + Render
Video + Video Frames to Layers + .pcraft frame persistence are follow-ups.)
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
timeline_ui::handles() wired into menus::is_live so the Timeline panel
counts toward parity. 608 -> 609 (97.4%).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Clean-room trap: per-ink grayscale dilation (separable max filter,
par_rows/wasm-safe) applied only under already-inked pixels, so internal
colour edges get a trap band while object-vs-paper edges stay clean.
Engine image.trap (CMYK-only, active pixel layer, one history step).
algo 3 tests + engine 2 tests. Parity 607 -> 608; FLOOR 603.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Merge a presets/preferences file (another install, or exported library)
into the current preset library, appending gradient/style/shape/pattern/
tool-preset groups the library lacks (by name); idempotent. Headless,
2 tests. Parity 606 -> 607; FLOOR 602.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Groups the selected layers and clips them to a frame rect (their bounds)
via a rectangular layer mask — the visual result of Photoshop's Frame for
this command, built from existing group + mask primitives (no doc-model
change). Headless/scriptable; 2 tests. Parity 605 -> 606; FLOOR 601.
(A distinct Frame layer type + the interactive Frame tool K are a later arc.)
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Photoshop's data-driven graphics dialog in the ui-egui crate, driving the
headless engine commands: Define page (per-variable name + layer + type,
add/remove), Data Sets page (navigator, per-variable value editors, Apply).
State lives in the dialog's __variables field (command-shaped JSON), so the
control channel drives it via ui.dialog.set / confirm like a human. 2 tests;
offscreen snapshot verified. UI stays in ui-egui; all behaviour is engine
commands.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Clean-room implementation of Photoshop's data-driven graphics, headless
and scriptable for CLI/MCP batch generation:
- doc: Variables model (visibility/text/pixel-replacement bindings + named
data sets), threaded through .pcraft (manifest + convert).
- engine variables_cmds.rs: image.variables.define, image.variables.dataSets,
image.applyDataSet (visibility/text/pixel replace w/ fit/fill/conform/as-is
+ 9 alignments, one history step), file.import.variableDataSets (CSV),
file.export.dataSetsAsFiles (apply each set and export), variables.list.
- Pixel replacement loads+scales+aligns an image into the layer bounds via
the existing resample helpers.
- 7 tests (doc round-trip + lookups; engine define/apply, CSV import, export,
pixel replace, bad params). Parity 600 -> 605; FLOOR 600.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Harris+BRIEF/RANSAC panorama with bundle adjustment, seams and multi-band
blending (Auto-Align/Auto-Blend share it); Debevec-Malik HDR merge with
ghost removal and tone mapping; Brown-Conrady lens correction (filter,
batch, smart filter); wide-angle constraint mesh warp; Crop and
Straighten Photos; Camera Raw Filter (Basic, curves, mixer, grading,
detail, effects) with dialog; Vanishing Point planes (engine only).
Fixes warp_triangles tile binning.
Checkpoint also includes compiling in-progress work of the remaining agents.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Gradients/Patterns/Styles/Shapes/Tool Presets/Clone Source panels with
headless preset commands and a Custom Shape tool; Color card tabs match
Essentials. Character and Paragraph Styles (doc model, .pcraft, PSD style
sheets), Glyphs panel + type.insertText, Check Spelling (SCOWL-50 list).
Checkpoint also includes compiling in-progress work of the remaining agents.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
- Help menu: Join the ArtCraft Discord, PhotoCraft Website
(getartcraft.com/apps/photocraft), ArtCraft Website, PhotoCraft on
GitHub, Report an Issue; separator before About. All also in the ⌘K
palette and drivable via the control channel (returns {"url"}).
- "Join us on Discord" button + website/GitHub/ArtCraft links on the
start screen and in About; a Discord button in the title bar.
- Linux metainfo: drop url type vcs-browser (unknown to appstream 0.15 on
ubuntu-22.04, which failed both Linux release jobs); homepage is the app
page, contact is Discord. nfpm homepage and Windows ARP links updated.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
- Lowercase reverse-DNS id ai.storyteller.photocraft across icons, Linux
desktop/metainfo/mime/flatpak, Windows WiX, macOS Info.plist and workflows.
- PhotoCraft display name in the title bar, start screen, web loader,
Windows exe metadata, README and docs.
- .gitignore target/, roadmap refresh.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>