PhotoCraft reads GIMP's .xcf (and .xcf.gz) files, read only, through a new
reader in photocraft-io written from the public XCF specification: versions
0 to 26, uncompressed, RLE and zlib tiles, 8/16/32-bit integer and
16/32/64-bit float precisions (linear precisions get a linear profile),
pixel layers at any position, groups by item path with pass-through,
layer masks applied or disabled, opacity, visibility, locks, colour tags,
the 2.10 and legacy blend modes mapped to Photoshop's (modes without an
equivalent open as Normal with a note), the ICC profile, resolution,
guides, the comment, extra channels and the saved selection; indexed
images open as RGB. Text, vector and link layers open as their pixels;
layer effects, paths and floating selections are reported.
Fixtures are written by GIMP 3.2 itself (scripts/xcf_fixtures.py), each
with GIMP's merged rendering as a PNG. In gamma-encoded blend and
composite spaces our composite matches GIMP within 0.5/255; GIMP's
linear-light default, its Clip to backdrop and its clamp-after-alpha
for unbounded modes are documented and measured in docs/xcf.md.
GetAsyncKeyState(VK_RBUTTON) reads the physical button system-wide: with swapped buttons a primary click read as right-down, and a right button held over another window counted too.
DDS textures can now be saved, not just opened: uncompressed (BGRA, BGRX, L8, A8L8; lossless) or BC1/BC2/BC3 (DXT1/3/5) from a clean-room block encoder, with an optional box-filtered mip chain, in the legacy header. Save As and Save a Copy ask with DDS Options; scripts pass `ddsCompression`/`ddsMipmaps` (file commands, control `app.save`, RPC `doc.save`, MCP `doc_save`/`doc_export`), and the CLI takes `--dds-compression`/`--dds-mipmaps`. Targa and DDS answers travel together as `FormatChoices` through the Save As flow.
Review follow-ups:
- File › Save behind DDS or Targa Options records a plain Save with its answer, and replaying `file.save` applies it (#3531).
- The block encoder finds the principal axis from every channel, so a channel that varies on its own can't hide the others' variation.
- DDS joins the shared round-trip suite; the header is built before the output buffer is allocated.
- Enter on a focused Cancel in DDS, Targa or TIFF Options cancels instead of saving.
On Windows with Windows Ink (WM_POINTER), winit drops penFlags (PEN_FLAG_BARREL) and reports stylus contact as WindowEvent::Touch, which egui-winit maps exclusively to PointerButton::Primary. Additionally, Windows Ink does not update GetAsyncKeyState(VK_RBUTTON) when pressing the pen barrel button. Consequently, Alt + Barrel Button drag was recognized as Alt + Left Drag (triggering the eyedropper and painting strokes instead of brush resize), and right-clicks with the stylus were treated as left-clicks.
This commit resolves the issue by:
1. Adding a native Windows message hook (WH_GETMESSAGE) in crates/tablet/src/windows.rs calling GetPointerPenInfo to intercept PEN_FLAG_BARREL (0x00000001) and POINTER_FLAG_SECONDBUTTON.
2. Exposing
ight_button_down in Services combining the pointer hook and GetAsyncKeyState(VK_RBUTTON).
3. Updating canvas.rs and paint_mouse.rs to prevent strokes_live and alt_eyedropper from hijacking the stylus gesture when the barrel button is held, and correctly routing resize events.
4. Adding tests asserting that Alt + stylus barrel drag resizes diameter horizontally and adjusts hardness vertically without triggering eyedropper or painting.
- A requested adapter that couldn't be used, followed by a software window
adapter, lost its System Info note to the software-graphics message. Append
instead of replacing.
- PHOTOCRAFT_GPU_ADAPTER only forces its own process when startup acts on it
(Linux, outside CPU mode); in CPU mode it is ignored, so the launch may
still go to the running window.
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
The Preferences row was Linux-only, but PHOTOCRAFT_GPU_ADAPTER and
performance.gpuAdapter were honoured on every platform, so Windows and macOS
could pick a non-default adapter without being tested for it. Ignore the
request off Linux (and in CPU mode, as before), and don't make a launch
standalone for a variable that is ignored there.
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
- cargo fmt on the adapter selection and its launch code.
- PHOTOCRAFT_GPU_ADAPTER is capped at 256 characters like the saved preference.
- Put the multi-GPU paragraph in docs/development.md on its own instead of
appending to the --safe-gpu paragraph, so the diff only adds lines.
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
In a Flatpak on Wayland, arboard can't open at all: the sandbox hides the
data-control protocol from it, and --socket=fallback-x11 gives no X server
while a compositor runs. Paste then reported "the clipboard holds no image"
even when it held one. Now the clipboard service reports that it had no
clipboard to read, and the paste shows a notice that explains it and gives
the XWayland command for this install.
Also adds my contributors/people.toml entry.
Claude-Session: https://claude.ai/code/session_01AbyXia7KGdFgbNjBeggY8J
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
- PHOTOCRAFT_GPU_ADAPTER now counts as a per-process launch option, so a
second launch with it set doesn't forward to the running window and drop it.
- Gate RenderState::available_adapters to native targets (it doesn't exist on
wasm, which broke the web build).
- Share the adapter name matcher (engine prefs) between startup and
Preferences, so a saved fragment like "nvidia" isn't flagged as missing when
it matches a listed adapter.
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
* fix(linux): keep crash-recovery autosaves in $XDG_STATE_HOME, out of the config folder (#6612)
* test(linux): cover the XDG state recovery folder (#6612)
With the backend on auto, Linux installed no adapter selector, so wgpu took
the first discrete GPU the driver listed. On a machine with an eGPU, a
laptop dGPU and an iGPU that was not the one the user wanted.
- performance.gpuAdapter: a case-insensitive piece of the adapter's name
(several words must all appear); empty is automatic.
- PHOTOCRAFT_GPU_ADAPTER overrides it for one launch.
- Preferences > Performance > Advanced > GPU adapter lists the adapters the
instance found; a saved adapter that is absent stays listed, marked.
- A missing adapter, or one that can't present to the window, falls back to
the automatic choice with a note in System Info. CPU mode ignores it.
- Shown on Linux only; tested on Linux only. Windows and macOS need their own
work (the startup code honours the setting there, but it is untested).
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
The rotation and instance-log tests in logging.rs, and two app_dirs migration-lock tests, drop a
flock and check straight away that it is free. A child that another test spawns at that moment
holds a copy of every open descriptor until its exec, the lock included, so the check sometimes
saw it still held (about 6 failures in 15 runs of the app's test binary). Tests that spawn
children (the portal mock's dbus-daemon, the display-profile helper test) now hold CHILD_SPAWNS
for reading while they spawn, and the lock tests hold it for writing.
The portal mock made its message stream inside its thread; a stream only gets messages that
arrive after it exists, so a SaveFile sent before the thread got going was lost and the test
waited forever (about 1 run in 25). The stream is now made before the thread starts.
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
apps/photocraft/src/main.rs: #6499's parse_launch_args is the one parser; it also takes main's --help/-h, -- and unknown-option errors (#6435); usage_exit_code is the one exit helper
The probe read the FileChooser `version` property and only counted a few D-Bus
error names as "absent". zbus wraps property errors in `Error::FDO`, and a portal
with no FileChooser backend doesn't export the interface at all, so the read fails
with `InvalidArgs` ("No such interface"). Neither matched, the probe said
"inconclusive", and Save As / Export did nothing without the "No file dialog can
open" notice.
Introspect the portal object and look for the interface instead, and treat
InvalidArgs, UnknownMethod and NameHasNoOwner (as `FDO` or `MethodError`) as absent.
Fixes#5739
Claude-Session: https://claude.ai/code/session_01AfRHB1wHUjrJx6agBYGnVu
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
With the `auto` backend on Windows, eframe created its wgpu instance with
Vulkan, and creating it, listing its adapters and asking each adapter
about the window's surface loaded and ran the Intel Vulkan driver
(igvk64.dll) before the adapter selector could pick Intel's DX12
adapter. When the driver crashed there, the first launch died, and only
the next one moved to DX12 through the startup marker.
`configure` now lists the DX12 adapters first, with a DX12-only
instance, when the plan is `auto` on Windows without WGPU_BACKEND. If
one of them is an Intel adapter, the instance is created with DX12 and
GL only (GL stays the last resort), so no Vulkan driver is loaded. On a
system with an Intel and a discrete GPU, the discrete GPU then runs on
DX12 instead of Vulkan. WGPU_BACKEND, --safe-gpu, a chosen backend and
the crash fallback are unchanged, and System Info still shows the Intel
DirectX 12 note.
The decision is `windows_auto_backends`, tested with vendor lists.
Main prints through craft-print, which runs `lp`; a Flatpak has no `lp`,
so File › Print fails there. This keeps craft-print as the route wherever
`lp` is on the path (and on Windows) and adds two more behind it:
- `photocraft-ipp`, a pure-Rust IPP client (CUPS-Get-Printers/Classes/
Default, Print-Job over HTTP/1.1) that talks to CUPS over its socket or
TCP; the Flatpak manifests grant --socket=cups for it. A `printServer`
named in the request always takes this route. `file.print.printers`
lists the queues.
- The XDG Print portal as the app's spooler (`Session::print_spooler`),
for a sandbox without the CUPS socket or a system without a printer:
the system print dialog, seeded with copies, paper and orientation.
PHOTOCRAFT_PRINT_PORTAL=1 prefers it, =0 removes it.
A route that exists but fails (unknown printer, refused job) is an error;
only a missing route moves on. Results gain `backend` (lp, windows, ipp,
portal). docs/printing.md describes the routes.
* Web: Place Embedded and canvas drops place images (#5749)
File > Place Embedded was disabled in the browser (the engine command reads a
path), though the menu path reads the picked file's bytes. Files dropped on
the canvas were always opened as new documents. The web shell now tracks the
drag position, and drops on the canvas are placed from their bytes.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* Format (cargo fmt)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* Narrow to the web canvas-drop half: Place Embedded enablement landed in #5585
Drop the picker test that covered the part #5585 now owns.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
logging::rotate renamed photocraft.log up the chain on every start without
knowing whether another instance still wrote to it. The running instance
kept its handle, so after a second launch its live log was .1.log, after a
third .2.log, and a fourth launch deleted it.
The instance that writes photocraft.log now holds an exclusive lock on
photocraft.lock until it exits (a separate file, since Windows locks are
mandatory and would keep the log itself from being opened). A start that
finds that lock held leaves the chain alone and writes
photocraft-<pid>.log, marked by photocraft-<pid>.lock. Those side-by-side
logs are pruned to the newest KEEP once their instances have exited; a
held one is never touched. Rotation and pruning run under rotate.lock so
two starts at once can't interleave. On a file system without locks every
start rotates as before.
* fix(desktop): `photocraft --help` prints usage; unknown options are usage errors (#6431)
The desktop binary's argument loop took every argument it didn't know as
a file, so `photocraft --help`, `-h` or a typo such as `--safegpu`
opened the window and logged "Couldn't open --help".
- `--help` / `-h` print the usage (every option and environment
variable) and exit 0.
- Any other argument starting with `-` (except macOS `-psn_`) is named
on stderr and the launch exits 2 before the log file or window, along
with any bad `--control` port (`control_args_exit_code` becomes
`usage_exit_code`).
- `--` ends the options, so a file whose name starts with `-` opens.
- `--help` and `--version` write without `println!`, so a closed pipe
(`photocraft --help | head -1`) can't panic.
Tests: apps/photocraft/tests/command_line_usage.rs runs the binary with
no display; 4 of its 6 tests fail on the old code.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* test(desktop): usage tests exit before the window opens; fmt smart_filter_authorization
`arguments_after_double_dash_are_files` and `macos_process_serial_number_is_still_ignored`
passed only valid arguments, so the app went on to open its window. Removing DISPLAY makes
that fail fast on Linux, but Windows runners have a desktop and both tests hung until
nextest's 600 s timeout. They now add a bad `--control` value so the launch always fails
with the usage errors, and assert stderr names only that error (no "unknown option").
Also `cargo fmt` crates/automation/tests/smart_filter_authorization.rs, left unformatted
on main by #5684, which failed the Format step of every PR's ubuntu test job.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
* Save: ask before overwriting a file another app changed (#6596) [WIP]
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* Save: ask before overwriting a file another app changed (#6596)
File › Save now notes the size and modification time of a document's
own file when it is opened or saved, and checks them again before
writing back. If another app changed the file in between, a prompt asks
whether to replace it (Cancel keeps both; Save As saves elsewhere)
instead of silently overwriting the other app's changes.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
* fix(windows): keep crash-recovery autosaves out of the roaming profile (#5648)
* test(windows): cover the local recovery folder and moving old autosaves (#5648)
With a document open in the web app, Place Embedded was disabled because its menu enablement inherited the engine's native-filesystem check, although the menu path reads the picker's bytes and never touches a path. Place Embedded is now enabled whenever a document and a file-dialog service exist; Place Linked keeps the engine's native guard, since the web could only link to a bare file name (caught in #5817 by Higgs32584). An enablement test runs with the native check forced off, and a file-dialog test covers the browser answer shape, cancel, invalid bytes and the Free Transform handoff.
Fixes#1209. Fixes#5749.
An auto-hidden taskbar reserves no work area, so the maximize guard left the
caption-less window covering the whole monitor and the shell treated it as a
full-screen app, never revealing the taskbar. On each monitor edge with an
auto-hidden taskbar (found via Shell_TrayWnd / Shell_SecondaryTrayWnd through
safe winsafe calls) the window now stops 1 px short of the monitor.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Batch strips Save/Save As/zoom steps from every action of a loaded set, called actions
included, and remaps their unchecked/dialog step indices.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>