Commit Graph
633 Commits
Author SHA1 Message Date
aiongg 9ac1d5adc6 Integration: regenerate docs/scorecard.md for the batch
#7813's performance.gpu_adapter is read from the prefs file before the window opens (gpu_startup::read_gpu_adapter), which the prefs audit's heuristic doesn't see, so it counts as unread.
2026-10-20 22:07:10 +00:00
aiongg db56aeb10a Merge PR #7813: Choose the graphics adapter on multi-GPU Linux machines (#1885)
# Conflicts:
#	apps/photocraft/src/gpu_startup.rs
#	crates/ui-egui/src/i18n/cs.tsv
#	crates/ui-egui/src/i18n/de.tsv
#	crates/ui-egui/src/i18n/el.tsv
#	crates/ui-egui/src/i18n/es.tsv
#	crates/ui-egui/src/i18n/fr.tsv
#	crates/ui-egui/src/i18n/hu.tsv
#	crates/ui-egui/src/i18n/id.tsv
#	crates/ui-egui/src/i18n/it.tsv
#	crates/ui-egui/src/i18n/ja.tsv
#	crates/ui-egui/src/i18n/ko.tsv
#	crates/ui-egui/src/i18n/nl.tsv
#	crates/ui-egui/src/i18n/pl.tsv
#	crates/ui-egui/src/i18n/pt-br.tsv
#	crates/ui-egui/src/i18n/ru.tsv
#	crates/ui-egui/src/i18n/sr.tsv
#	crates/ui-egui/src/i18n/tr.tsv
#	crates/ui-egui/src/i18n/uk.tsv
#	crates/ui-egui/src/i18n/vi.tsv
#	crates/ui-egui/src/i18n/zh-hans.tsv
#	crates/ui-egui/src/i18n/zh-hant.tsv
#	crates/ui-egui/src/prefs_ui.rs
2026-10-20 18:41:25 +00:00
aiongg 1ca8a8aad0 Merge PR #7964: Preserve near-black ICC colors on the GPU canvas
# Conflicts:
#	contributors/people.toml
2026-10-20 18:39:43 +00:00
aiongg 60764172aa Merge PR #7217: GPU startup: Auto on Windows leaves Vulkan out when an Intel adapter is present (#4) 2026-10-20 18:39:29 +00:00
aiongg e2169d0dfd Merge PR #7847: Print: Portrait and Landscape in the Print dialog on Windows 2026-10-20 18:39:29 +00:00
aiongg ea7553f031 Merge PR #5111: Large documents: a gigapixel New Document draws without freezing (#4017) 2026-10-20 18:39:28 +00:00
aiongg 0ae688b59a Merge PR #6939: Mixer Brush: Clean Brush empties the brush, and the automatic Load and Clean can be switched off 2026-10-20 18:39:27 +00:00
Guillem Hermida 872748fb5b Codecs: save DDS textures, uncompressed or BC1–BC3, with optional mipmaps (#1327) (#5993)
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.
2026-10-20 11:02:39 -06:00
ha1des 472a8d6f29 Preserve near-black ICC colors on the GPU canvas 2026-10-20 14:18:00 +02:00
aiongg 03d0c7e3b3 Merge pull request #7823 from aiongg/integration-5
Integration batch: 8 PRs on main
2026-10-19 23:11:09 -04:00
Ernie SmithandClaude Sonnet 5.5 0e97ac4ca8 Make adapter choice Linux-only everywhere, not just the dropdown
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>
2026-10-19 16:19:51 -04:00
Ernie SmithandClaude Sonnet 5.5 2644d06081 Review: rustfmt, cap the env adapter name, docs in their own paragraph
- 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>
2026-10-19 16:07:04 -04:00
WolfandClaude Opus 5.5 2f08860383 Print: Portrait and Landscape in the Print dialog on Windows
On Windows the Print dialog takes the paper and its orientation from the
printer driver, and had no way to change the orientation short of the
driver's own Print Settings… dialog. Photoshop has Layout buttons for it.
A print laid out landscape on portrait driver settings (from a script,
the CLI or another app) stopped with "the driver paper or orientation
differs from the print layout".

- The dialog has an Orientation row (Portrait, Landscape) under Print
  Settings…. It turns the driver settings through file.print.page and
  takes the paper from the driver again, so the paper and preview turn.
- file.print.page takes "orientation": "portrait|landscape" and returns
  the turned settings as printerSettings, with the page they give.
- print_windows turns the settings (the given ones or the printer's
  defaults) to the layout's orientation when the driver has the layout's
  paper the other way round; another paper is still refused.

Turning sets dmOrientation and its dmFields bit in the DEVMODE and keeps
every other byte, the driver's private settings included; the driver
validates the result as it does settings from Print Settings…. No new
UI strings: "Orientation", "Portrait" and "Landscape" are in every
catalog.

Tests: the turned settings (orientation set, all else kept, with and
without private driver bytes), when a paper counts as turned, an unknown
orientation refused before any printer is asked, the dialog's reading of
the driver paper and its error path, and, run by hand on Windows, a
real "Microsoft Print to PDF" driver turning for a landscape layout.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-19 21:46:41 +02:00
aiongg 46f7e2422e Integration: drop #7690's benchmark report, as #7690 did before it merged 2026-10-19 18:41:15 +00:00
Brandon ThomasandClaude Opus 5.5 3a896a5efc docs: regenerate the scorecard (Show File Name In Title adds a preference) (#7829)
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-19 14:23:47 -04:00
Marco Arnulfo 4f15642ee8 Toolbar: Edit Toolbar, Quick Mask and Screen Mode in every theme (part of #4653) (#4680) 2026-10-19 14:20:48 -04:00
aiongg 3f509f06ed Integration: regenerate docs/scorecard.md 2026-10-19 18:01:48 +00:00
Korg1ch 8c2cfdba6a fix(linux): keep crash-recovery autosaves in $XDG_STATE_HOME, out of the config folder (#6612) (#6941)
* 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)
2026-10-19 13:32:14 -04:00
aiongg acc853b517 Merge upstream main into integration-5
docs/file-format-parity.md: keeps both 2026-10-15 rows, main's #6063 PNG sTER row first, then #5976's PSD guides row
2026-10-19 17:20:29 +00:00
aiongg b8e856dffe Integration: #7022 write_region stays serial; the text rasterizer uses the parallel variant 2026-10-19 17:18:46 +00:00
Ernie SmithandClaude Sonnet 5.5 b892c0412e Choose the graphics adapter on multi-GPU Linux machines (#1885)
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>
2026-10-19 13:16:53 -04:00
KnottyDyes c9d7f1d984 Docs: install the CLI and register it with MCP clients (#7663) 2026-10-19 13:03:40 -04:00
640873de7b PNG: keep a stereo file's sTER pairing on save (#6063) (#6285)
* PNG: keep a stereo file's sTER pairing on save (#6063)

Read the sTER chunk (PNG Extensions 1.4.0 §3.6) when a PNG opens, store it
on the document's FileEncoding, and write it back when saving, so a stereo
PNG still says it is a stereo pair. Covers the ordinary and the palette
PNG encoders, including interlaced output.

* io tests: pass the new sTER argument to encode_png_indexed

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

---------

Co-authored-by: highperfocused <highperfocused@pm.me>
Co-authored-by: Brandon Thomas <echelon@gmail.com>
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-19 13:02:18 -04:00
David Mann c30f9180e8 Merge PR #7692: perf(raster): Avoid redundant default pixel work 2026-10-19 16:51:48 +02:00
David Mann c0072ca2da Merge PR #7691: perf(io): Interleave PSD pixels in the final buffer 2026-10-19 16:51:45 +02:00
David Mann b2269e3bdb Merge PR #7690: perf(codecs): Avoid redundant PNG filter passes 2026-10-19 16:51:43 +02:00
David Mann 8e54897d43 Merge PR #7022: perf(text): rasterize a type layer's pixels on all cores, bit for bit the same 2026-10-19 16:51:41 +02:00
David Mann 284ee60705 Merge PR #5976: PSD: keep guide resources 1092/1097 only while they match the guides (#5966) 2026-10-19 16:51:27 +02:00
Rene Leonhardt 4d50e31efd perf(codecs): stop PNG candidates at the winning cost 2026-10-19 13:43:19 +02:00
Rene Leonhardt b5b5776a30 perf(io): remove the PSD export inlining hint 2026-10-19 13:15:06 +02:00
Rene Leonhardt 78a9982eec perf(raster): share contiguous region reads 2026-10-19 13:15:06 +02:00
Rene Leonhardt 6b1aa81561 docs(perf): measure PNG filtering on real images 2026-10-19 13:15:05 +02:00
Rene Leonhardt 76a6405f9b perf(io): Interleave PSD pixels in the final buffer
Remove intermediate band buffers while retaining byte-for-byte equivalence with the previous path at every sample depth.
2026-10-19 11:31:59 +02:00
Rene Leonhardt f6e7fc3c2a perf(raster): Avoid redundant default pixel work
Preserve default sample bits while skipping absent zero tiles and redundant default-pixel copies. Add the previous read path as the oracle.
2026-10-19 11:31:59 +02:00
Rene Leonhardt 850bf9fdaa perf(codecs): Avoid redundant PNG filter passes
Keep the previous filter path as an exact byte oracle and measure the filter kernel independently of compression.
2026-10-19 11:31:59 +02:00
skylar taylor-barrick 4d4de45305 Merge pull request #7206 from skylartaylor/save-replace-metadata
Save: keep the replaced file's metadata
2026-10-18 16:48:40 -06:00
c6fd71241d docs: add user docs in docs/user (#3951)
* docs: add user docs for PhotoCraft

Adds docs/user/ (published at getartcraft.com/docs/photocraft), the docs
check and publish workflows (copied from craft-launcher), and a User docs
rule in AGENTS.md. Checker: 0 error(s), 0 warning(s).

Source notes per page:
- index.md: README (intro, status, Crafting Apps table), website
  crafting-apps.ts photocraft entry, links.rs (Discord, issues), menus.rs
  (Help > Report an Issue).
- getting-started/index.md: menus.rs and menu_catalog.rs labels, panels.rs
  (Layers, Properties, History), dialogs.rs (Create button).
- getting-started/install.md: README Downloads and Get started sections,
  gh release view v0.7.0 asset list, craft-launcher catalog.rs (lists
  PhotoCraft).
- guides/open-and-save-files.md: apps/photocraft/src/services.rs (open and
  save filters), README Formats and PSD sections, docs/file-format-parity.md.
- guides/adjust-colors-with-adjustment-layers.md: menu_catalog.rs, panels.rs
  (footer button, adjustment list), lib.rs (Properties shows new layer).
- guides/select-and-mask.md: menu_catalog.rs Select and Layer Mask menus,
  state.rs tool names, retouch_ui.rs (Select Subject button and message).
- guides/add-and-style-text.md: state.rs (Horizontal Type Tool, T),
  type_tool.rs, menu_catalog.rs Layer Style and Type menus, dialogs.rs.
- guides/export-an-image.md: export_dialog.rs (labels, formats, options).
- troubleshooting/taskbar-turns-black.md: issues 3550, 3864, 3430, 3144,
  PR 3308 (git: not an ancestor of v0.7.0), prefs_ui.rs (System title bar).
- troubleshooting/text-moves-when-editing.md: issues 3795, 1317, PR 3819
  (open).
- troubleshooting/get-help.md: menus.rs, links.rs, gpu_status.rs (System
  Info content).
- faq/*: README (status, license badge, offline, formats), LICENSE-MIT and
  LICENSE-APACHE, docs/file-format-parity.md.

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

* docs(user): apply final review fixes

Part 1: verified = latest release tag policy in docs/user/AGENTS.md; released-pages sentence in root AGENTS.md. Part 4 items 1-9: remove x86 row and TGA resolution, formats, new photocraft-does-not-start page, mask label, taskbar workaround, wording, Affinity read-only, index.

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

* docs(user): use the standard section title

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

* docs(user): describe PhotoCraft 0.7.0 startup behaviour

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

* docs(user): tighten PhotoCraft startup wording

Drop the unsourced "slower" claim for CPU mode and give the exact
Preferences path on Windows and macOS.

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

* docs: pin the docs checker to a commit and its SHA-256

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

* docs: drop the docs workflows until the checker is on artcraft-services main

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

* docs: re-verify against 0.8.2

- taskbar and PSD text pages: both fixes ship in 0.8.2; tell users to
  update and drop the "not in a release yet" wording and workarounds
- does not start: 0.8.2 fixes the Intel Iris Pro/Monterey Device(Lost)
  case; OpenCore Legacy Patcher Macs still have no fix
- install: Windows 10 or later, 32-bit Windows builds, RISC-V deb/rpm,
  Wayland paste and XWayland notice
- open and save: Krita and Paint.NET filters, Krita/PCX/FITS formats,
  save progress, JPEG/WebP encoding kept on Save
- export: TGA Resolution, multi-layer Export As and Quick Export
- text: Shift-click starts new text over existing type
- CLAUDE.md is now a regular file pointing to AGENTS.md
- verified: 0.8.2 on every page

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

* Drop the root AGENTS.md docs rule; keep the PR to docs/user/

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

* docs(user): drop the stale missing-tools count from the Photoshop FAQ

Re-verified the page against v0.9.0.

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

---------

Co-authored-by: Fabio Lussi <198260595+itrycodinganditsbad@users.noreply.github.com>
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-18 16:15:53 -06:00
Brian TandClaude Opus 5.5 ca69059dd0 Edit: Copy with no selection keeps fill and adjustment layers (#2344) (#7497)
Copy refused fill and adjustment layers ("the active layer has no
pixels"). With no pixel selection they are now taken whole, like smart
objects, type, shape and group layers, and Paste recreates them in this
or another document. An adjustment layer's clip spans the canvas, so
paste placement and the OS-clipboard mirror stay consistent.

Scorecard UI-217-23 note, gaps.md G9 and target-app-parity.md updated;
the row stays partial (a single pixel layer still copies pixels only).

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-18 17:23:26 -04:00
Nguyễn Minh Long 72a3423a7e Preferences: Enable Gestures off ignores touch-screen pinch and twist (#204) (#7478)
Edit > Preferences > Enhanced Controls > Enable Gestures (enhancedControls.touchGestures)
was saved but read by nothing, so it was hidden from the dialog. Now, with it off, two
fingers on a touch screen no longer zoom the canvas or the Navigator, nor turn the view.
The mouse and trackpad behave as before, including a trackpad pinch or Rotate event that
arrives while fingers rest on the screen.

- wheel_nav: pinch_factor() drops egui's multi-touch zoom when gestures are off and keeps
  the trackpad's own Event::Zoom (Windows' folded Ctrl + wheel pinch included);
  read_with() takes the preference.
- rotate_view: trackpad_delta() only falls back to the multi-touch rotation while gestures
  are on; Rotate events always count.
- canvas, Navigator: pass the preference through.
- prefs: touchGestures leaves HIDDEN_UNTIL_IMPLEMENTED, so the dialog shows it.
- Tests: synthetic two-finger pinch and twist, on and off, and the trackpad events with
  gestures off.
- docs/scorecard.md regenerated: settings that do nothing 26 -> 25 of 168.
2026-10-18 17:23:21 -04:00
Marco Arnulfo 8736098d38 Curves: Cmd/Ctrl-click on the image adds a point at the pixel's tone (#7473)
* Curves: Cmd/Ctrl-click on the image adds a point at the pixel's tone (part of #7325)

With Image > Adjustments > Curves open, a Cmd-click (Ctrl-click off the Mac) on
the image adds a point to the channel shown in the dialog, at the pixel's tone
and at the curve's current output there; Shift adds one point on each of Red,
Green and Blue. Only the press adds, an armed eyedropper still wins, and the
control channel's ui.pointer does the same with command: true.

* Curves: Cmd/Ctrl-click adds the point with the Hand tool too; Space and middle button still pan

The Hand tool took the Cmd-click press for a pan, so no point was added; the
press now counts before the Hand. On the control channel, ui.pointer with
space or the middle button pans as the mouse does instead of adding a point.

* Curves: the Cmd/Ctrl-click point's comment says where the curve can still change
2026-10-18 17:23:16 -04:00
JulienandClaude Opus 5.5 bbfeae98e4 Type: right-click on committed text opens the type layer menu (#7324) (#7430)
* Type: right-click on committed text opens the type layer menu (#7324)

With the Type tool, a right-click only opened a menu while text was being
edited; over committed type nothing opened. The menu was keyed on the edit
session alone (`editing_text`).

Over committed type, a Type tool (not a Type Mask tool) now opens
TYPE_LAYER_MENU: the type edit menu's layer rows, without the edit rows
(Undo, Cut, Copy, Paste, Select All), which would act on the document, and
without Faux Bold/Italic, which act on the characters being edited. Type
outside the selected layers becomes the target first, as a click with the
Type tool makes it. The control channel's right button takes the same path.

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

* Type layer menu: the clicked type becomes the active layer in a multiselection (#7324)

The structural rows (Rasterize Type, Convert to Shape, Create Work Path, the
paragraph/point conversions) act on the active layer. When the right-clicked
type was already selected but another selected type layer was active, they
acted on that other layer. Make the clicked type the active layer while
keeping the selection (layer_multi_cmds::set_selection), so the formatting
rows still act on every selected type layer. Found by CodeRabbit on #7430.

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

---------

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-18 17:23:05 -04:00
skylar taylor-barrick 915779b948 Merge storytold/main into save-replace-metadata
No textual conflicts, but the merge did not compile. main's 589941a5 added a
second AtomicFs test impl using copy_permissions, while this branch renames that
trait method to copy_metadata. Git merged both cleanly because they sit in
different parts of the file, giving a tree whose trait and impl disagree:
E0407, E0046 and E0599 in photocraft-format's lib test. That one error is what
failed all four CI jobs.

main's new impl is renamed to copy_metadata to match.

Verified with --features heif, which is what CI builds with and what I had
previously failed to pass locally: clippy --workspace --all-targets --locked
--features heif -- -D warnings is clean, and the workspace tests pass.
2026-10-18 15:12:41 -06:00
aiongg e7d4ce5e18 Merge PR #6364: Path Selection: the canvas right-click opens the path menu, as the Pen's does (#1056) 2026-10-18 14:19:00 +00:00
wwew 04967b12f9 docs: write product name as PhotoCraft in engine module doc, LUT export header and image sources (#5454) 2026-10-18 03:35:28 -04:00
dotCooCoo c2cf0e0610 Merge main into fix/mixer-brush-clean-and-load 2026-10-17 16:28:35 -07:00
dotCooCoo 3c9c93ed8c GPU startup: Auto on Windows leaves Vulkan out when an Intel adapter is present (#4)
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.
2026-10-17 15:28:20 -07:00
Diego Chagas 235e7578f3 Print through CUPS over IPP and the print portal, so File › Print works in the Flatpak (#2728) (#3017)
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.
2026-10-17 15:55:54 -06:00
Gert Pellin - GPE ba40454b18 Logging: a second launch leaves the running instance's log alone (#6432) (#6478)
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.
2026-10-17 11:57:39 -06:00
skylar taylor-barrick 8c12449819 Save: keep the replaced file's metadata (#6603, #7073, #7094, #7095, #7096)
Saving writes a temporary file and renames it over the target, so everything
attached to the old file went with the file that was unlinked. Several of those
are security decisions, so a save quietly undid them: a download lost its Mark
of the Web, an explicit ACL fell back to whatever the folder hands down.

New crates/format/src/atomic/meta.rs, reached from atomic_write through two
AtomicFs methods (copy_permissions became copy_metadata; replace_existing is new
and defaults to rename, so the failure-injecting test file systems are
unchanged):

- Extended attributes (#6603): flistxattr/fgetxattr/fsetxattr through safe
  rustix, per attribute, best effort. macOS Finder tags, com.apple.quarantine
  (the Mark of the Web's macOS counterpart) and Linux user.* all ride along.
  Attributes the system owns refuse to be written and are skipped;
  com.apple.decmpfs is skipped deliberately, since it describes storage the new
  bytes do not have.
- macOS ACL (#7073): a new macOS-only exacl dependency (MIT).
  fcopyfile(COPYFILE_ACL) is an unsafe fn and fclonefileat(CLONE_ACL) is
  APFS-only and would mean writing every save as clone-then-truncate; the
  reasoning is in docs/decisions.md. A file with no ACL is left alone.
- Windows (#7094, #7096): ReplaceFileW through winsafe's kernel feature (MIT,
  no dependencies, a safe wrapper; the workspace forbids writing the FFI call
  here). One call keeps the destination's DACL, its named streams including
  Zone.Identifier, and its creation time.
- Automation write path (#7095): AuthorizedWorkspace::write copies the replaced
  file's attributes through the two cap-std descriptors, never a path, so a
  request path still cannot escape the write root. Permissions there remain
  #4963; ReplaceFileW is path-based and so is not used on that path.

The order inside copy_metadata matters: attributes and ACL first, permission
bits last. Writing an attribute needs the destination open for writing, so
copying a 0444 mode onto the temporary file first locks you out of your own
temporary file. Covered by a regression test.

Atomicity is not weakened. The metadata step runs on the temporary file before
the swap, never touches bytes, and never fails a save. Windows is the one place
the swap itself changed, and a backup file name is passed because it is a
correctness requirement, not an option: ReplaceFileW without one documents
ERROR_UNABLE_TO_MOVE_REPLACEMENT as leaving the replaced file deleted and the
replacement under its temporary name, which is a failed save that loses the
user's document. With a backup name the same failure leaves both files alone.
ERROR_UNABLE_TO_MOVE_REPLACEMENT_2, the one error that does move things, is
unwound by hand; every other error falls back to the plain rename, i.e. to the
previous behaviour.

THE WINDOWS PATHS ARE UNVERIFIED ON WINDOWS. They were written and reviewed on
a Mac. The #[cfg(windows)] tests -- Zone.Identifier, a second named stream,
icacls output, creation time -- have never executed. They do compile and pass
clippy -D warnings against x86_64-pc-windows-msvc, which is as far as a Mac
gets. CI's Windows job is the first real run, and #7094 in particular (Mark of
the Web surviving a replace) is the whole point of the change and is the part
no one has observed working.

Tested on macOS: a real chmod +a entry and a real Finder tag both survive a
save (checked with ls -le and xattr, and as unit tests), plus a 64 KiB
attribute value, a read-only file, the mode, and the automation path.

Still open: Linux POSIX ACLs are not carried over (system.posix_acl_access is
not reported by listxattr, so it needs its own step; not part of these issues),
and macOS creation time is still reset by a save (that half of #6603 needs
setattrlist, which has no safe wrapper here).
2026-10-17 10:43:09 -06:00
ebosido444-ai fcd337e1fc perf(text): rasterize a type layer's pixels on all cores, bit for bit the same
A type render spends most of its time after the glyphs are drawn: composing each
colour's coverage over the premultiplied accumulator, dividing it back to straight
colour, and converting the tile bytes. All three are per pixel - no pixel reads a
slot another wrote - so they ran one after another on one core for no reason.

Measured on a 3.75 MP paragraph (4000 characters, 14 pt at 300 dpi, 1851 glyphs),
release, Windows 11, i5-10300H: rasterize 98.3-103.9 ms before, 60.3-61.8 ms after
(1.6-1.7x). Whole-render stage profile before: ink rect 4.1, draw 18.6-22.9, prefix
sum 7.0-8.1, composite 22.6-33.4, unpremultiply 11.2-14.0, write_region 23.8, prune
3.1 ms.

The glyph drawing, the winding prefix sum and the ink-rect pass stay on one thread:
the first two carry a running accumulator across pixels, and the band split that
would keep them bit-identical costs a second glyph traversal per band.

Native only (rayon, as the other crates): the web build keeps the serial loops.
2026-10-17 17:42:50 +03:00