Commit Graph
115 Commits
Author SHA1 Message Date
7722172585 Add OpenRaster (.ora) import and export (#2257)
Open and save .ora with pixel layers (any offset), nested groups, pass-through
groups, names, visibility, opacity, edit/alpha locks and blend modes (svg:* plus
Krita's krita:* names), at 8 and 16 bit. Save applies pixel masks, renders
text/shape/fill/smart layers to pixels and reports what OpenRaster can't hold.
Wired into File > Open/Save, the CLI, automation/MCP, the web picker and the
desktop file associations. Checked against 34 Krita 5.2.9-authored files
(opt-in krita_corpus test) and Krita re-opening our exports.

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

Co-authored-by: Brandon Thomas <echelon@gmail.com>
2026-10-10 07:12:24 -04:00
e05deac124 fix(engine): reject invalid core-filter parameters (#2228)
* fix(engine): reject invalid core-filter parameters

* test: cover filter validation through CLI and automation

* fix(ui): preserve filter preview minima during downsampling

---------

Co-authored-by: Brandon Thomas <echelon@gmail.com>
2026-10-10 04:57:56 -04:00
dd2ce3da33 GPU startup: CPU mode on Linux falls back to Vulkan to present the window (#1337) (#2206)
CPU rendering mode limited the window's wgpu instance to GL on Linux.
With NVIDIA's EGL the GL adapter can't present to the window (wgpu:
"EGL says it can present to the window but not natively") and llvmpipe
isn't offered, so the adapter selector found nothing and every start
exited with "no graphics adapter found", on X11 and Wayland alike. The
user couldn't reach Preferences to undo it.

CPU mode now creates the instance with GL and Vulkan. GL adapters still
rank first (llvmpipe, then hardware GL), so nothing changes where GL can
present; Vulkan is used only to draw the window when no GL adapter can.
Image compositing stays on the CPU.

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Co-authored-by: Brandon Thomas <echelon@gmail.com>
2026-10-10 04:12:17 -04:00
265f10b91d Fix SVG selection and import detection (#2224)
* Fix SVG import detection and configurable group depth

* Fix malformed SVG error routing and formatting

* Refresh scorecard for SVG depth preference

---------

Co-authored-by: Brandon Thomas <echelon@gmail.com>
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-10 03:58:08 -04:00
a81eaa4187 Keyboard Shortcuts: import Photoshop .kys sets; first launch takes Photoshop's live set (#1163) (#1987)
* Edit › Keyboard Shortcuts: Import Photoshop Shortcuts… loads a .kys set (#1163)

Photoshop saves a shortcut set as .kys XML: <command name="Gaussian Blur...">
rows with one or more <shortcut>Control+Opt+Cmd+G</shortcut>, then the tool
keys. The dialog's new button (and File › Open or a drop of a .kys) parses it
with roxmltree, matches each row to our commands by menu label, and fills the
dialog's overrides, so OK applies the set the way hand-made changes are
(a key moved to a command is taken from its old owner) and Cancel discards it.

Rules, from Photoshop's own ambiguities: a label in several menus (Levels…
under Image › Adjustments and Layer › New Adjustment Layer; All under
Select, Edit › Purge and View › Show) goes to the command whose default is one
of the row's keys, else the one with a default, else the first in menu order.
A row's first key is kept unless one of its keys is already the command's
default (Zoom In is Cmd++ then Cmd+=). Scripts, Bridge, the frames commands
and the macOS application menu have no command here and are counted in the
message. Tool keys are not imported: the tool letters are fixed.

* First launch beside Photoshop imports its live shortcut set; the Open dialog lists .kys

Photoshop keeps the set in use as Keyboard Shortcuts.psp in its per-version
settings folder (macOS ~/Library/Preferences/Adobe Photoshop <v> Settings,
Windows %APPDATA%\Adobe\Adobe Photoshop <v>\Adobe Photoshop <v> Settings),
the same XML a saved .kys holds. The desktop app hands the newest version's
file to the UI (Services::photoshop_shortcuts); at the first launch that has
no shortcut overrides yet, kys_import::auto_import applies it through the
Keyboard Shortcuts dialog's own OK path and posts a notice naming the set and
the count. The preference dialogs.photoshopShortcuts records the source, so
the import happens once; someone who already changed a shortcut keeps theirs.
Reset All to Defaults undoes it.

The Open dialog's filter now lists .kys (it greyed them out), and dropping
Keyboard Shortcuts.psp imports it like a .kys.

* kys import: Ukrainian and Dutch strings; tests follow Cmd+K and Cmd+F as the defaults (#1900)

* kys import: user-facing strings name no other product (#1985)

The button is Import Shortcuts…, its tip names the .kys format, the notice
says "Keyboard shortcuts imported" and names the set, and a file that is not
a set is "not a keyboard shortcut set (.kys)". A set without a name is called
by its file name. Comments, identifiers and the XML root tag keep the format's
own name.

* rustfmt: format panels.rs and prefs_ui.rs

---------

Co-authored-by: Brandon Thomas <echelon@gmail.com>
2026-10-09 21:55:43 -04:00
8c3b6f8e5f Appearance modes: sync with system, separate dark and light themes (#1981)
* Appearance modes: sync with system, separate dark and light themes

* Appearance button cycles Auto, Light, Dark

* Appearance: default stays Dark, follow the portal signal instead of polling, split out the Preferences resize

* Preferences: size both appearance cards for the five dark themes

---------

Co-authored-by: Brandon Thomas <echelon@gmail.com>
2026-10-09 21:34:49 -04:00
db0fb07862 Add native layered Paint.NET (.pdn) import (#1998)
Squashed onto main, with the maintainer's asks applied:
- Photoshop's blend menus (layers, brush, pencil, gradient, layer style) stay as they are;
  the Layers panel and Blending Options list a Paint.NET mode only for a layer that uses one.
- The six Paint.NET mode labels are translated in every catalog, and the blend-mode i18n test
  iterates layer_modes().
- pdn::pixels rejects truncated uncompressed (version 1) data before allocating its buffer.

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-09 21:05:00 -04:00
Ahmed MoussaandGitHub 1cfc56db56 fix(Text): served fonts become script fallbacks on the web (#2109)
A host's font manifest can now list the scripts each font covers (`scripts` field). The first served family for a script the bundled fonts lack (Arabic, Hebrew, Devanagari, Thai, Japanese, Chinese, Korean) becomes that script's fallback. When a layout contains text in that script, it requests the family. The font database adds the family to the fallback stack once it arrives, and type layers drawn before then are re-rendered.

The web shell also fetches the fallbacks for the browser's languages (`navigator.languages`) at startup, so an Arabic reader gets the Arabic font before typing anything.

The manifest's SHA-256 is now turned into a Subresource Integrity value (`sha256-<base64>`) and passed with the font fetch, so the browser rejects any file whose bytes don't match.

Also adds a contributor entry for hamada147 to `contributors/people.toml`.

Signed-off-by: Ahmed Moussa <moussa.ahmed95@gmail.com>
2026-10-09 20:04:04 -04:00
1330899aca fix(web): persist imported brush libraries in IndexedDB (#479) (#2054)
* Persist browser brush presets through IndexedDB

* fix(web): clear resolved preset staging rejections

* web: translate the brush preset storage messages

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

---------

Co-authored-by: zenjabba <679864+zenjabba@users.noreply.github.com>
Co-authored-by: Brandon Thomas <echelon@gmail.com>
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-09 19:58:25 -04:00
Harrison O'Connor-HooverandGitHub 269d8fedc7 Keep startup responsive while recovering autosaved documents (#2115) 2026-10-09 19:50:53 -04:00
85572c61c3 Save in the background so large saves don't freeze the window (#2017) (#2072)
* Save in the background so large saves don't freeze the window (#2017)

File › Save and Save As encoded and wrote the whole document on the UI thread,
so a multi-GB layered TIFF or PSB save left the window unresponsive for the
whole write, with no progress.

Saves now run as a background job: a new `Services::save_file` encodes and
writes atomically on a worker (the desktop app provides it), the document is
locked meanwhile, and the existing progress dialog shows the stage with Cancel.
Cancelling while encoding leaves the file untouched. The save is recorded when
the job ends, the unsaved-changes prompt waits for it, and a window close
during a save is held until it ends. Without the service (web, tests) saves
stay synchronous.

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

* Background save: a save can't be cancelled once writing starts (#2072 review)

- `Services::save_file` now gets a `SaveCtl` and calls `commit()` right before
  replacing the file. Cancel and Esc are refused after that, so a "Cancelled"
  save can no longer replace the file while the document shows unsaved.
- A save's worker stays counted (`saving()`) until it returns, even when its job
  was cancelled: quitting waits for it, and another save to the same file is
  refused until it is gone.
- A failed or cancelled save no longer closes a window whose close was held.
- The pending reply carries the job id, and the job becomes `last_started`, so
  control and MCP clients can wait for the save.

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

---------

Co-authored-by: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-10-09 19:39:55 -04:00
0e011a69dc feat(canvas): Caps Lock forces the precise crosshair for painting tools (#1758) (#2111)
* feat(canvas): Caps Lock forces the precise crosshair for painting tools (#1758)

* Caps Lock poll: back off for 5 s after an X error instead of reconnecting and warning every frame

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-09 19:25:36 -04:00
Roaldo SulejmaniandGitHub 424a5d492f fix: remove Photoshop from user-facing strings (#1985) (#2008)
The app still showed the Adobe Photoshop name in several user-visible
strings. Per the product rule (user-facing text says PhotoCraft only),
none of these mention Photoshop now.

UI labels, tooltips and status messages:
- New Document preset "Default Photoshop Size" -> "Default PhotoCraft Size"
- "All shortcuts reset to Photoshop defaults." -> "All shortcuts reset to
  PhotoCraft defaults."
- "Import Photoshop brushes (.abr)" -> "Import brushes (.abr)"
- smart-filter fallback name "Photoshop filter" -> "Smart filter"
- File > Save As .psd/.psb filter "Photoshop" -> "PSD Document"

Errors and warnings:
- "this Photoshop filter isn't implemented in PhotoCraft" -> "this smart
  filter isn't implemented in PhotoCraft"
- smart_map: "has no PSD equivalent", "not a recognized filter", "its
  filter data is missing/unreadable", "this filter has no PSD equivalent"
- tiff_layers: "the PSD layer data...", "the PSD image resources..."
- multichannel: "32-bit Multichannel isn't supported"
- abr: "not a readable brush file (.abr)"
- gradient import: "not a readable gradient file (.grd)"

Translations: updated the changed tl! sources in all 16 catalogs and
dropped the stale "Load Photoshop brushes (.abr)" entries.

Installer / OS metadata: de-branded the nfpm description, the PSB MIME
comment, the AppStream summary/description, the macOS UTType
descriptions, the Windows ProgId Description and the desktop launcher's
Keywords. The Photocraft.PhotoshopDocument ProgId and the
image/vnd.adobe.photoshop MIME/UTType stay: machine identifiers,
changing them hurts file fidelity and Open-with registration.

Command params docs (agent-facing): dropped Photoshop from the
type_cmds kernPair text, the layer_menu_cmds Advanced Blending defaults
and the mask_view_cmds backslash-key text. The "photoshopManages" wire
value stays. Comments, format identifiers and the "photoshop" theme
alias are out of scope and unchanged.

The macOS native-menu shortcut resolution messages no longer say "as in
Photoshop" (Hide/Minimize).
2026-10-09 13:59:07 -07:00
bdbd43e63f Color Lookup: Load 3D LUT opens the native file picker (#1947)
The Color Lookup properties' Load 3D LUT… entry opens the platform file picker (filtered to .cube, .3dl and .look, on desktop and web) and embeds the chosen LUT into that adjustment layer, replacing the typed-path field. A cancelled or invalid pick leaves the layer unchanged (#1749).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-09 16:20:13 -04:00
be446f89d3 Transparent PNG, TIFF and other flat images open as Layer 0, not Background (#1913)
A flat image with transparency (alpha channel or tRNS) opens as a normal, unlocked Layer 0, as in
Photoshop; opaque images still open as the locked Background. A lone Layer 0 saves back to PNG natively.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-09 15:43:14 -04:00
66b022cddd Web: fonts served next to the app, loaded on demand (#1875)
A host lists fonts in fonts/manifest.txt beside index.html (craft-fonts' manifest format, paths
kept inside the site); the web shell lists those families in the font menus and fetches a family
when it is picked or a layout needs it, then re-renders the affected type layers without a history
step. No manifest changes nothing; package.sh copies one in with PHOTOCRAFT_WEB_FONTS_DIR. The
style menu also lists the weights of a variable font's wght axis.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-09 13:14:47 -04:00
941b84a748 Save As and export dialogs start beside the document (#1866)
Export dialogs suggested a bare file name, so the save dialog opened wherever the OS chose.
pick_save now joins a bare suggestion onto the active document's folder (an explicit directory
or an untitled document is left as is), and the desktop dialog starts in the suggestion's folder.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-09 12:28:42 -04:00
1bccee1fda Desktop app: exit with status 2 when --control setup fails (#1834)
A bad control token, token plus token file, or a missing automation root printed an
error and exited 0, so a launcher believed the control server was up. Now exits 2,
like a bad --control port; subprocess tests cover the four paths. Fixes #1816.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-09 12:26:02 -04:00
d4a5d974ac File > Open starts in the last-used folder (#1889)
Open dialogs (File > Open and the single-file pickers for Place, scripts, notes and presets) now carry the most recent file's folder as initial_dir, and the desktop shell hands it to rfd's set_directory. Flips UI-217-3 to done.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-09 12:03:31 -04:00
Gert Pellin - GPEandGitHub 310e46e6a0 fix(ui): one Cmd+V pastes one layer (#1638) (#1743)
On macOS AppKit runs Edit > Paste's key equivalent and takes the
key-down, but winit still delivers the key-up (it forwards key-ups
while Cmd is held). The native menu fed egui a press plus a made-up
release, and clipboard_keys treats a Cmd+V release without a paste
event as an image paste (egui-winit drops Cmd+V when the clipboard
holds no text). So one Cmd+V pasted three times: the menu's press,
its synthetic release, and winit's real release.

- native_menu: a key equivalent adds only its press, inserted ahead of
  its key-up when that is already queued; winit delivers the release.
- clipboard_keys: a Cmd+V key press that reached egui marks the paste
  as seen, so its release doesn't paste again; any V/Insert release
  clears the mark (releasing Ctrl before V left it stale and swallowed
  the next image paste on Linux/Windows).
2026-10-09 22:08:03 +07:00
9b4562f2a0 Open dialog: upper- and mixed-case extensions on Linux and the BSDs (#1439)
The portal and zenity match rfd's *.ext globs case-sensitively, so IMG_0001.JPG or DSC_0001.NEF didn't show. On Linux and the BSDs each letter of an Open filter extension becomes a [jJ] class; Windows and macOS keep plain extensions.
Duplicate #1682 by @switch87 took the same approach.

Co-authored-by: switch87 <switch87@users.noreply.github.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-09 09:10:04 -04:00
89922792c5 Clarify that muda is a native menu crate, not Tauri (#1786)
muda is maintained by the tauri-apps org but is standalone: AppKit menus,
no webview or JS, so it fits the no-Tauri rule. Reword the comments that
called it "Tauri's menu crate".


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

Co-authored-by: Claude <noreply@anthropic.com>
2026-10-09 18:53:59 +07:00
BFlatandGitHub 8e8e9f2c68 Merge pull request #1778 from storytold/community/1607-affinity
AF.3: native Affinity import (layers, artboards, vectors, text, images), carrying #1607 by @callmeBalloch
2026-10-09 18:00:53 +07:00
ClaudeandBFlat 2515fa7cee Linux display server: compile read_display_server on Linux (and tests) only
macOS and Windows clippy rejected it as dead code (-D warnings): its only caller in main.rs is
#[cfg(target_os = "linux")]. Also puts read_rendering_prefs' doc line back above it.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01UHgFEHEsefoQoBParH47JA
2026-10-09 16:06:12 +07:00
Gert PellinandBFlat a132e37ee1 Linux: display server preference (X11 / XWayland) so file drops work
On a Wayland session dropping a file on the window does nothing: winit
0.30 (eframe 0.36) has no Wayland drag and drop (#386). The only way
out was launching with `WAYLAND_DISPLAY= photocraft`.

Add Preferences › Performance › Linux display server (auto | x11,
applies at next launch), direction 2 in #386. With x11, main.rs asks
winit for its X11 backend through eframe's event_loop_builder hook
(EventLoopBuilderExtX11::with_x11), so the window runs through XWayland
and drops work. No environment is modified (no unsafe). It only applies
when an X server is reachable (DISPLAY set); otherwise winit's default
is kept, so a missing XWayland can't stop the app from starting. Other
platforms ignore it.

The Wayland drop notice now points at the preference instead of the
environment variable (all catalogs updated).

Tests: the startup reader (x11, auto, unknown value, wrong type, broken
JSON, missing file), the notice text, prefs_usage, label translations.
Verified on KDE Plasma 6 Wayland: with x11 the window is an X11 window
(XWayland) and dropping a NEF from Dolphin opens it.
2026-10-09 16:06:12 +07:00
22ba13b47b macOS: quit with terminate:, not by closing the window (#1575, #1458) (#1624)
Since the native menu bar (#894), Quit runs file.exit, which closes the window through eframe
while AppKit's run loop is still going. AppKit's Touch Bar finder then removes a KVO observer from
the already-deallocated view in the next display cycle (`-[_NSTouchBarFinderObservation
invalidate]`): an uncaught exception that aborts on macOS 27 (#1458) and leaves a windowless app
on macOS 26 (#1575). The window's close button took the same path before #894; ⌘Q used winit's
default menu, which calls terminate:.

Every way of leaving (Quit, ⌘Q, the close button, the OS quit event, app.quit) arrives at
guard_window_close as a close request. Once leaving is settled (nothing unsaved, or the prompt
answered) it now runs Services::quit when the platform has one, keeping the window open for it.
On macOS that is -[NSApplication terminate:], scheduled on the main queue so winit is out of its
event handler and eframe's exiting hook still saves the window layout.

Reproduced in a macOS 26.6.2 VM with XCUITest: on main, Quit, Quit with a document, ⌘Q and the
close button abort with the #1458 stack; with this change all of them exit, write no crash report
and save ui.ron.

Co-authored-by: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-10-09 16:05:58 +07:00
callmeBallochandClaude 2b279ba61b Merge storytold/main into AF.3 native Affinity import
No conflicts.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01GQPh8UDgMyARR5Hat9UchC
2026-10-09 06:38:46 +00:00
shmuelbiller1andGitHub 83d5c5436d fix(autosave): report background write failures and retry unchanged revisions (#1577)
* fix(format): expose asynchronous autosave completion and enqueue errors

* feat(ui): accept nonblocking background autosave outcomes

* fix(desktop): pass worker autosave results to shell

* fix(ui): confirm autosaves only after successful writes and retry failures

* test(desktop): retry failed background autosave with unchanged revision

* test(format): report async autosave write failure and successful retry

* style(ui): compact autosave iterator chains under project rustfmt width

* style(format): apply rustfmt for autosave completion iterator

* fix(format): simplify async autosave completion types for Clippy

* test(format): allow slow Windows autosave worker time to finish

* test(desktop): use bounded wall-clock deadlines for background autosave checks

* style(format): match rustfmt for autosave worker signature
2026-10-09 13:24:30 +07:00
1910b9f87c Add Greek (Ελληνικά) UI localization (#1562)
A complete Greek catalog (el.tsv) registered with the other complete-menu languages: menus,
dialogs, preferences, filters, tool names (Greek with the English name in parentheses) and
font weights, plus Greek Services / Hide Others / Show All in the macOS app menu.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-09 00:31:00 -04:00
0256eaa4e0 Type Color Picker opens the full Color Picker; screen eyedropper with a pixel loupe (#1232)
The Type options bar and Character panel swatches open the full Color Picker (Text Color),
applying to the selected characters or the whole layer via type.setStyle; Cancel is inert. A
shared "Pick screen color" action in the full dialog and compact popups samples the desktop:
X11/Windows/macOS snapshots with an 11x11 magnifying loupe (4x-24x), the XDG PickColor portal on
Wayland, the EyeDropper API on the web. Captures stay in memory, are bounded at 64 MP, and are
ICC-converted to sRGB where the display publishes a profile.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-09 00:30:50 -04:00
callmeBallochandClaude 57ae2fb1a3 Merge storytold/main into AF.3 native Affinity import
Conflicts: File › Open and batch extension lists (SVG, swatches and
Affinity together); the import dispatcher (upstream's type-layer
preparation with the new result fields); the pinned EXR and Affinity
corpora side by side; README, book and development docs.

Affinity type layers now draw through text_import::prepare, so they get
the same raster bound as other imported type, and SVG import fills the
two new ImportResult fields.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01GQPh8UDgMyARR5Hat9UchC
2026-10-09 04:26:18 +00:00
Harrison O'Connor-HooverandGitHub 672c0775de Reject control requests that expire before dispatch (#1525) 2026-10-09 11:11:47 +07:00
b58b8df36b Non-Unicode arguments: translate the desktop notice (follow-up to #1168) (#1359)
* Non-Unicode arguments: translate the notice; keep run's doc comment on run

The "Could not open" notice for a path that is not valid Unicode is
translated in every shipped language, and the CLI's "Run the CLI" doc
comment moves back onto run (run_os has its own).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01UHgFEHEsefoQoBParH47JA

* Non-Unicode arguments: Polish translations

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01UHgFEHEsefoQoBParH47JA

---------

Co-authored-by: Claude <noreply@anthropic.com>
2026-10-09 09:00:44 +07:00
ad857f1c93 Wayland: drop notice offers paste and the XWayland command for this install (#338, #386) (#1073)
* Wayland: drop guidance offers paste and this install's XWayland command (#338, #386)

winit 0.30 (eframe 0.36) delivers no file drops on Wayland. The notice told every
user to run `WAYLAND_DISPLAY= photocraft`, which does nothing for the AppImage
(no `photocraft` on PATH) or the Flatpak (fallback-x11 grants no X socket while
Wayland is available). It now names the command for this install (Flatpak flags,
the quoted $APPIMAGE path, or `photocraft`), omits it without an X server, and
offers pasting a copied image file with Edit › Paste's shortcut, which works on
Wayland since #384. The start screen hint on Wayland says the same.

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

* Wayland drop notice: Polish translations

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01UHgFEHEsefoQoBParH47JA

---------

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-09 08:57:26 +07:00
MarekSpodBiedryandGitHub c77e4122ce Control server: a bad JSON reply carries "id": null like every other unknown-id error (#1006) (#1304) 2026-10-09 08:36:44 +07:00
b97a72ac79 File dialogs no longer freeze or crash the app (#673, #574) (#1071)
* File dialogs no longer block the event loop (#673, #574)

Open, Save As, Export As, Place and every other file dialog ran
rfd's synchronous dialog inside winit's event handler. On macOS that
runs AppKit's modal loop there, and an event arriving meanwhile
aborted the app (SIGABRT, panic_cannot_unwind); on Linux the frozen
event loop made compositors offer to kill the app.

The platform shell now shows dialogs through one asynchronous
Services::file_dialog: the request is queued with what to do with the
answer, shown at the end of the frame with the window as its parent
(a sheet on macOS, an owned modal dialog elsewhere) and awaited on a
helper thread; the answer comes back through a channel that wakes the
app, which continues the action on a later frame. One dialog at a
time. The unsaved-changes prompt waits behind Save's dialog and closes
the document only once it saved. The web build uses the same service.

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

* brush picker: Import ignores the Open dialog's result, like the other callers

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01UHgFEHEsefoQoBParH47JA

---------

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-09 08:08:03 +07:00
5bef14dbdc Swatches: user swatch model with .aco/.ase import and export (FILE-215-14, part of #215) (#886)
- photocraft-psd: clean-room .aco (v1/v2, UTF-16 names, RGB/HSB/CMYK/Lab/Gray at 16 bits,
  other spaces kept verbatim) and .ase (groups, global/spot/process, RGB/CMYK/LAB/Gray floats,
  unknown models kept) readers and writers; byte-stable round trips, hostile input -> Err.
- engine: presets::swatches (colours in their own model, display/foreground conversion through
  photocraft-cms working spaces, persisted with the prefs, default 40 colours in 4 groups) and
  swatch_cmds (swatches.list/add/delete/rename/move/newGroup/renameGroup/deleteGroup/use/reset/
  import/export) with graceful-failure tests.
- ui: swatches_ui.rs replaces the const grid (groups, tooltips, click = foreground and selected
  type, Alt/Ctrl-click = background, rename/delete/move, panel menu Import/Replace/Export/Reset),
  Color Picker "Add to Swatches", .aco/.ase via File > Open and drag-and-drop; strings translated
  in every complete catalog.
- Scorecard FILE-215-14 -> done.

Co-authored-by: leon <claude.udder842@passmail.net>
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Co-authored-by: Brandon Thomas <echelon@gmail.com>
2026-10-08 20:43:01 -04:00
4477cf6bdf Fix #1380: the web build asks before leaving the page with unsaved work (#1424)
Closing or reloading the browser tab dropped unsaved documents without a
word, while the desktop app asks before closing its window. The web shell
now registers a beforeunload listener that has the browser show its own
"Leave site?" prompt while a document has unsaved changes (the new
PhotocraftApp::has_unsaved_work, refreshed every frame).


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

Co-authored-by: Claude <noreply@anthropic.com>
2026-10-09 07:38:12 +07:00
e2a760f68d Fix #1371: read large macOS display ids, keep ICC bytes out of the error (#1412)
NSScreenNumber was coerced with `as integer`, so a CGDirectDisplayID above
AppleScript's integer range (2^29) came back as a real, "2.077748985E+9".
parse_reply rejected it, dropped the only display, and the error quoted
the whole raw line, base64 ICC profile included, in the Display profile
notice and Color Settings.

The helper now asks NSNumber for stringValue; the parser also accepts an
exact scientific-notation id; and the error names only the start of the
first field, capped at 80 characters.


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

Co-authored-by: Claude <noreply@anthropic.com>
2026-10-09 07:38:00 +07:00
43b4742e3f GPU startup: a failed CPU start no longer pins later starts to CPU
A start that returned an error kept its gpu-starting.json marker so the
next start would try a safer backend. On the CPU path there is no safer
backend: the marker made every later start plan CPU again and fail the
same way (seen under XWayland, where the GL adapter can't present: "no
graphics adapter found"), until the file was deleted by hand.

Keep the marker after an error only when a safer backend exists.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-09 07:26:55 +07:00
652b9726be Pen tablets: Xwayland for a pen on Wayland, macOS monitor after NSApp (#639, #759) (#1070)
Wayland compositors give a pen only to clients that bind zwp_tablet_v2,
which winit 0.30 doesn't, so on Wayland the pen did nothing in the
window. With a pen attached (BTN_TOOL_PEN in /proc/bus/input/devices),
$DISPLAY set and the X11 libraries installed, the window now opens
through Xwayland, where the X11 reader gets pressure, tilt and the
eraser. PHOTOCRAFT_NATIVE_WAYLAND=1 keeps native Wayland.

macOS: the AppKit event monitor was installed before winit created the
shared NSApplication; it now goes in from eframe's app creation. The
pen's button-up (pressure 0) no longer zeroes the pressure of the
stroke's last moves, which reach the UI in the same frame.

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-09 05:08:44 +07:00
b885173da1 Embed PhotoCraft CLI Windows icon and version metadata (#889)
* Embed PhotoCraft CLI Windows icon and version metadata

The CLI crate now sets build to build.rs and takes winresource 0.1 as a
build dependency. On a Windows target the script embeds photocraft.ico
and sets ProductName to PhotoCraft, FileDescription to PhotoCraft
command-line interface, CompanyName to Learning Machines LLC, the shared
copyright line, OriginalFilename to photocraft-cli.exe, and InternalName
to photocraft-cli. Version strings keep the Cargo package version,
including a pre-release tag, and the numeric file and product versions
are major.minor.patch.0. A missing resource compiler prints a cargo
warning and the link continues, while PHOTOCRAFT_REQUIRE_WINRES makes
the script return an error that names rc.exe or windres. package.ps1
runs check-cli-resources.ps1 after the PE subsystem checks and before
signing, including SkipBuild artifacts, and that script checks those
VERSIONINFO fields against the package version and compares the icon
Windows extracts from the CLI with photocraft.ico drawn at 32x32,
without launching the binary. docs/releasing.md records the same CLI
resource contract beside the existing GUI one.

Refs #613

* Drop the untested check-cli-resources.ps1 release gate

It has never run on Windows, and a misfiring 200-line gate in package.ps1 would block
releases. The CLI build.rs icon/VERSIONINFO stays; PHOTOCRAFT_REQUIRE_WINRES=1 in
package.ps1 already makes a missing resource compiler a build error.

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-08 17:05:38 -04:00
f41eb4f70d SVG opens as shape layers and places as a vector smart object (FILE-215-9, part of #215) (#1305)
* SVG opens as shape layers and places as a vector smart object (FILE-215-9, part of #215)

crates/io/src/svg.rs: usvg parses (already in the tree through egui_extras); paths become
ShapeLayer knots with the transform chain baked in, quadratics raised to cubics and every
subpath after the first joining the component so the fill rule spans the path as SVG does;
groups keep opacity and blend mode (transform-only and id-only wrapper groups vanish);
text is shaped into outline shapes with the craft fonts plus the system's; gradients become
the layer's gradient fill (stops and angle, with a warning). Clip paths, masks, filters,
pattern paints and images are rasterised by resvg into pixel layers over their bounding box
rather than dropped. Limits: 16384 px canvas, 2000 elements, 24 M px per raster, bounded
recursion; malformed input is IoError::Svg.

smart_cmds::render rasterises an SVG source at the placement's axis scale (1/16 steps,
cached per scale) and divides the placement by it, affine and projective alike; place_bytes
renders that cache up front. svg/svgz join the openable lists; book page; scorecard row done.

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

* SVG import: never read local files from <image href>

usvg's default image resolver reads any absolute or relative path in an
<image href>, so opening an untrusted SVG could embed local files
(/etc/..., ~/.ssh/...) into the document. Parse with a resolver that
only accepts data: URLs (resources_dir None, string hrefs ignored).
Embedded sub-SVGs already get no <image> resolution from usvg. The font
database (and its system-font scan) is now only loaded when the drawing
may contain text. Regression test: a PNG on disk referenced by absolute
path, file:// URL and relative path is not loaded; a data: URL still is.

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-08 17:04:21 -04:00
5e13abd67c A non-Unicode command-line argument is a usage error, not a startup panic (#1108) (#1168)
* fix(apps): a non-Unicode argument is a usage error, not a startup panic (#1108)

Both binaries read `std::env::args()`, which panics on an argument that is
not valid Unicode: `photocraft-cli info $'caf\xe9.png'` died in
`library/std/src/env.rs` before printing anything, and a file manager
handing the desktop app a Latin-1 file name would do the same before any
window opened.

CLI: `photocraft_cli::run_os` takes the raw `OsString`s, and an argument
that does not convert is a usage error naming its position and a lossy
rendering (exit 2, with the usage text), like any other bad argument.
`main` calls it; `run(&[String])` is unchanged for in-process callers.

Desktop: the loop matches on `OsStr::to_str`. Flag values that are paths
(`--control-token-file`, `--automation-*-root`) become `PathBuf`s directly
and so keep working with any name; a non-Unicode `--control-token` is a
control-argument error like a bad `--control` port; non-Unicode file
arguments are collected and reported in a "Could not open" notice once the
app is up, so the launch still succeeds.

Test: `a_non_unicode_argument_is_a_usage_error_not_a_panic` (unix) runs
the binary with a `caf\xe9.png` argument for `info` and `convert` and
asserts exit 2, the message, and no panic output.

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

* photocraft-cli: put run()'s doc line back above run, not run_os

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

---------

Co-authored-by: wstock <1394687+wstock@users.noreply.github.com>
Co-authored-by: Claude Fable 5.1 <noreply@anthropic.com>
Co-authored-by: Brandon Thomas <echelon@gmail.com>
2026-10-08 16:37:56 -04:00
83936bdcee Fix #1271, fix #1316: Preferences › Interface › System Title Bar (#1344)
* Fix #1271, fix #1316: Preferences › Interface › System Title Bar

Since #986, Windows and Linux draw PhotoCraft's own one-row title bar
with its own window buttons. On tiling window managers those buttons do
nothing, and some Linux desktops look better with their own decorations.
The new System Title Bar preference (off by default) gives the window the
system's title bar and buttons back; it is read when the app starts.
macOS always uses the system's. Translated in every shipped language.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01UHgFEHEsefoQoBParH47JA

* Scorecard: count the System Title Bar preference

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01UHgFEHEsefoQoBParH47JA

---------

Co-authored-by: Claude <noreply@anthropic.com>
2026-10-09 03:36:21 +07:00
Gert Pellin - GPEandGitHub cbed56b7f1 Desktop: write log records to standard error and a rotating log file (#1143)
* Desktop: write log records to standard error and a rotating log file

The desktop app installed no logger, so every log::warn!/log::error! in
the workspace was discarded: GPU startup fallbacks, a lost wgpu device,
the crash guard's panic report. A launch from a desktop menu has no
terminal either, so there was nothing to attach to a bug report.

apps/photocraft/src/logging.rs (no new dependency):
- standard error plus <settings dir>/logs/photocraft.log; each launch
  rotates the previous log to photocraft.1.log and .2.log, so the log of
  a run that crashed survives the next start; the file is capped at 16 MiB
- records logged before the settings directory is known are buffered
  (bounded) and written once the file is attached
- default levels: info for photocraft*, warn for everything else;
  RUST_LOG replaces them with env_logger-style directives
- never panics: an unusable directory or a failed write leaves standard
  error as the only sink

The logger is installed first in main (so the panic hook is recorded) and
the file is opened after argument parsing (--version and usage errors
leave no file). Documented in docs/development.md (Logs).

* Contributors: credit switch87 as Gert Pellin

* Desktop: report an unusable log folder through the logger

eprintln! panics when standard error is a broken pipe (a GUI launch on
Windows); log::warn! reaches standard error the same way once the file
sink has given up, and never panics. Same fix as storytold/vectorcraft#465.
2026-10-08 16:09:32 -04:00
Nigel Ihza FirdausyandGitHub 05b405aab0 fix(cli): report missing font fallbacks in run, info and convert (#1299)
* fix(cli): report missing font substitutions in run, info, and convert (#1251)

* test(cli): verify missing fonts in run/info/convert and no duplicate or false warnings

* docs(cli): document missing-font fallback warnings in convert, info and run

* style(cli): apply exact rustfmt format for multi-command warning test
2026-10-08 16:08:50 -04:00
Jamie PatesandGitHub 3952fcef79 Resolve issue that makes Photocraft save various file formats incorectly as a .psd (#1178) 2026-10-08 15:42:17 -04:00
lionelandGitHub 69c01c1bff Startup: preserve the GPU backend after windowing errors (#1228)
* fix(startup): preserve GPU backend after non-graphics errors

* fix(startup): retain prior crash evidence on windowing errors
2026-10-08 15:32:54 -04:00