Commit Graph
3400 Commits
Author SHA1 Message Date
f23d173933 Add an opt-in WinTab backend for pen pressure on Windows (#11331)
* Add an opt-in WinTab backend for pen pressure on Windows

Windows Ink stays the default. Preferences > Tools > Pen Tablet has a Tablet
input dropdown (Windows Ink (default) / WinTab). WinTab opens a WinTab 1.4
system context on PhotoCraft's window and polls pressure and the eraser end
(tilt on devices that report orientation axes; rotation only with a pen and
driver that report twist) into the stylus feed. The row says to turn Windows
Ink off in the tablet's software utility; the choice applies at next launch.

- crates/tablet: Windows-only wintab/wintab-sys backend. Devices with
  orientation axes use the orientation packet; pressure-only devices open a
  packet without it and report neutral tilt. Leaving is the driver's hardware
  proximity message (WT_PROXIMITY) and the packet's out-of-context status, with
  a long fallback for drivers that never report either. Overlap priority
  follows window focus, and the message hook requests a frame for each WinTab
  message so pressure-only strokes update without cursor movement.
- apps/photocraft: install the backend when the preference is on; poll it at
  the start of each frame; the installed wake repaints the UI.
- ui-egui: TabletPollFn service; the Windows-only Pen Tablet row with the
  Tablet input dropdown and the note.
- engine: tools.tabletInput (windowsInk/winTab), default windowsInk.
- docs: hardware-parity WinTab gap entry, qualifying tilt to orientation axes
  and rotation to twist-reporting pens.

Tested locally on a Wacom tablet with Windows Ink off: pressure, tilt and the
eraser end reach the brush (rotation where the pen reports twist); the default
path is unchanged.

* WinTab: a message that finds the state borrowed is skipped, not a panic in the hook

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

* WinTab: rustfmt

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

* Scorecard: count the Tablet input setting

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

---------

Co-authored-by: FredApps <153173854+FredApps@users.noreply.github.com>
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-06 18:39:40 +01:00
FredApps d2ce418bb3 Merge pull request #11641 from wanikhawar/claude/agitated-jones-8e1a8e
A little free memory no longer refuses every PSD; the refusal says what to do
2026-11-06 18:36:17 +01:00
FredApps ce97bde34b Merge pull request #11628 from wanikhawar/claude/brave-wu-261567
Network scratch folders report their own free space, or say they can't
2026-11-06 18:15:40 +01:00
FredApps 71aa3eef9a Merge pull request #10164 from wanikhawar/fix/10031-web-place-steps
Web build: Place runs a slice per frame, so a large PNG no longer freezes the page
2026-11-06 18:12:11 +01:00
FredApps a81d9f9220 Merge pull request #11609 from wanikhawar/fix/smart-filter-stale-adjust-params
Stop stale settings overriding edits to adjustment smart filters
2026-11-06 18:09:27 +01:00
FredApps c49c1fd6fc Describe scratch settings without promising memory admission
Present the live scratch state as current configuration rather than the cause of a file's refusal. Explain that configured scratch capacity cannot override caller/build limits or replace non-spillable memory. Keep the typed error and admission policy unchanged, and add wording regressions.

Formatting and whitespace checks passed. No local build or test execution; validation awaits hosted CI.
2026-11-06 17:51:45 +01:00
FredApps 8d2ee0787f Merge scratch query updates into memory admission policy 2026-11-06 17:29:23 +01:00
FredApps aee46a2fc6 Merge remote-tracking branch 'origin/main' into fix/10164-final-prune-u2 2026-11-06 17:28:48 +01:00
David Mann af99f69602 Merge pull request #7719 from davidmann4/fix/cas-background-selection
Content-Aware Scale works on the Background with a selection (#7717)
2026-11-06 17:28:41 +01:00
FredApps 898979da30 Merge main into scratch folder capacity queries 2026-11-06 17:27:38 +01:00
FredApps 2ce36bd66c Merge main and use replacement for adjustment smart filter edits 2026-11-06 17:27:16 +01:00
David Mann 1e82972ee1 Merge pull request #9667 from davidmann4/fix/system-tools-full-path
Windows: run explorer.exe and reg.exe by full path (#9666)
2026-11-06 17:26:45 +01:00
FredApps c8e4376ec6 Merge pull request #11513 from wanikhawar/claude/investigate-issue-e27e90
Windows: count scratch room when opening large files; refusal names the build's limit
2026-11-06 17:26:22 +01:00
David Mann 8b70147d06 Merge pull request #9244 from davidmann4/fix/flip-angle-gradient-sweep
Image Rotation: a flip reverses an Angle gradient fill's sweep (#9243)
2026-11-06 17:26:13 +01:00
David Mann 3768e84b67 Merge pull request #9480 from davidmann4/fix/remove-enable-animations
Interface: remove the Enable animations preference (#9479)
2026-11-06 17:25:38 +01:00
David Mann 89235b39ac Merge pull request #10501 from davidmann4/fix/10328-review-followup
Review follow-up to #10328: Liquify saved twirl, copied type brings its styles, restored tests (#10500)
2026-11-06 17:25:11 +01:00
David Mann 90f6d9aab6 Merge pull request #9300 from davidmann4/fix/trim-subquantum-alpha
Image: Trim (Transparent Pixels) ignores alpha that rounds to 0 at the document's depth (#9299)
2026-11-06 17:23:32 +01:00
David MannandClaude Opus 5.5 4f124949b5 Address CodeRabbit review: convert type style colours with the type; keep Basic Paragraph on Duplicate to New
Converting a document (layer copy to another mode/profile, Image > Mode,
Convert to Profile with or without dither, Duotone inks) converted run colours
but not the colours set by character/paragraph styles and Basic Paragraph, so
converted runs showed a false local override and stopped following later
style edits. Duplicate Layer > New Document dropped the source's Basic
Paragraph, giving inheriting type false overrides.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-06 14:20:31 +01:00
David Mann a9de7ebee8 Merge remote-tracking branch 'origin/main' into HEAD 2026-11-06 14:03:38 +01:00
Khawar Zamman Wani 1ce1bced95 PSD admission: a little free memory no longer refuses every file
With 1 GiB or less of free RAM, or 512 MiB or less of commit headroom, the
hard limit was exactly 0, so even a small file was refused with a message that
didn't say memory was low.

The reserve now shrinks with the measurement: up to a quarter of free RAM opens
automatically, the rest goes to approval, and an approved import always leaves
at least 256 MiB free. Limits are unchanged for automatic opening above 4/3 GiB
free and for approval from 4 GiB free. A refusal set by measured memory now
gives the free amount and says to close other apps and try again, and the
approval prompt warns when little memory would be left.

An unreadable cgroup still measures Some(0) and stays refused.
2026-11-06 17:03:50 +05:30
Khawar Zamman WaniandClaude ed49cd9401 Scratch free space is asked of the folder itself, so network folders report their own numbers
Windows network folders reported unknown because sysinfo doesn't list them, and an unlisted Linux
NFS or CIFS mount used the numbers of its listed parent. GetDiskFreeSpaceEx (Windows) and statvfs
(Unix) on the directory fix both. macOS keeps sysinfo's figure, which counts purgeable space, when
the listed disk is this volume. A mount that reports no space is flagged in Preferences, in the
hard-limit refusal and in spill::stats, with a hint to set a Scratch Size Limit or use a local disk.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
2026-11-06 14:58:27 +05:30
Khawar Zamman Wani 02c5ea8e68 Merge commit '349a03b6b18b0bcee64090f6faa9fc321960fc34' into claude/brave-wu-261567 2026-11-06 14:54:08 +05:30
David Mann ec5eb1e2d0 Merge commit 'refs/fixprs/main' into HEAD 2026-11-06 10:21:59 +01:00
David Mann 519d29c536 Merge remote-tracking branch 'origin/main' into HEAD
# Conflicts:
#	docs/scorecard.md
2026-11-06 10:10:32 +01:00
David Mann 7271c8cca7 Merge pull request #3634 from davidmann4/feat/history-snapshot-options
History Options: Automatically Create First Snapshot, New Snapshot When Saving, Show New Snapshot Dialog by Default (#2966)
2026-11-06 10:07:19 +01:00
Khawar Zamman Wani 349a03b6b1 Refusal text states the lower limit, links the download and shows readable sizes
The 32-bit message ended mid-phrase. It now says the cap applies or less if
less memory is free, and separates the cause from the next step. Windows
scratch tests check the returned numbers belong to the temp directory's volume.
2026-11-06 14:01:03 +05:30
Khawar Zamman Wani 1eb3218a4b Merge remote-tracking branch 'origin/main' into claude/investigate-issue-e27e90 2026-11-06 12:37:29 +05:30
Khawar Zamman Wani 390b3b388a Replace params for every adjustment smart filter dialog and Shape Blur
Color Lookup uses the schema dialog, which removes an emptied text key, so
a stale data or file was read over the chosen lut. Key the replacement on
image.adjustments.* rather than the dedicated editors. setParams' replace
now also holds for Shape Blur, which merged before resolving its shape.
2026-11-06 12:01:12 +05:30
Khawar Zamman Wani 2e13ec280b Replace an adjustment smart filter's params when its dialog edits them
layer.smartFilter.setParams merges, so a key the dialog removed stayed
stored. A Channel Mixer filter with Monochrome turned off kept its gray
row, which is read over red: the Red sliders had no effect. The dialog
holds all of an adjustment's settings, so it now passes replace: true;
other callers keep merging.
2026-11-06 11:52:38 +05:30
Khawar Zamman Wani 7c2127b6bc PNG import in steps: stream the rows into tiles, turn upright in bands
PngImport decoded the whole image and then copied it into a second, tile surface (about
+400 MB for a 100 MP RGBA8 file, enough to kill a browser tab). A PNG whose rows come in
final order now streams: PngDecodeRows::step_stream hands each group of rows on, they
become tiles at once (the PngStream the one-shot streamed import also uses) and are
dropped, then the prune scan runs in batches. decode_rows is the same step_stream driven
to the end, so there is one path. Interlaced, animated and early-eXIf-oriented files are
decoded whole as before; a late eXIf sends a streamed import back to a whole decode.

The turn upright of an EXIF orientation 2-8 was one whole-buffer pass in the poll of the
last rows; OrientSteps (photocraft-codecs) does it in bands of 64 rows through the same
band writer as Image::oriented, and PngImport runs it as a stage of its own.

Memory-budget errors keep their type and text: the stream is checked as the one-shot
streamed import is, and, like the whole-image path, a layer under 32 MiB is not.
Docs for the stepped place updated.
2026-11-06 09:13:41 +05:30
Khawar Zamman Wani 6ebc4c96f8 Place: move the pixels a few tiles at a time, name the real place command in the size error
A place at an offset off the tile grid rebuilt nearly every tile the image touches in
a single poll, which can overrun the ~8 ms slice on a big image. TranslateSteps
(photocraft-raster) builds the target tiles of Surface::translated a tile at a time
with the same code (a shared ShiftBuilder), so the result is bit-identical; the place
runs it as a Placing stage of its own in a cooperative session.

The flatten size refusal named "file.place", which is no command: it now names
file.placeEmbedded or file.placeLinked.
2026-11-06 09:13:41 +05:30
Khawar Zamman Wani 123b2032c4 Web build: Place runs a slice per frame, so a large PNG no longer freezes the page (#10164, rebased onto main)
The remaining commits of the stack, applied on top of the squash-merged #10147:

- PNG decodes in resumable steps of rows (PngDecodeRows, shared with decode_rows)
- PNG imports as a document in resumable steps (io PngImport, text metadata and
  document build in bounded steps)
- Jobs can run a slice at a time where there are no threads (StepWork,
  cooperative sessions, Session::poll_jobs)
- Place decodes, flattens and resizes in slices (PlaceSteps), reading a path
  and keeping the frame, memory gate and typed memory-budget errors main has
- The web build places dropped and picked files a slice per frame, with a
  bounded place inbox
- A measurement of the stepped place; docs for jobs without threads
2026-11-06 08:51:17 +05:30
David MannandClaude Opus 5.5 99b93f82da Batch 18 (CodeRabbit nitpicks): crop test checks the active layer's covered pixels; Esc test goes through the shortcut handler
- #11458: the straight-crop rows also check that the active layer's pixels
  inside the old canvas are the plain crop's, so a fill that recolours them
  fails.
- #11295: the Esc case sends the key through shortcuts::handle instead of
  calling cancel directly.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-06 04:04:26 +01:00
David Mann 5d10956221 Merge pull request #9261 from davidmann4/fix/image-size-resolution-range
Image Size: refuse a resolution outside 1..30000 ppi instead of clamping it (#9260)
2026-11-06 03:16:20 +01:00
David Mann 7122a21fec Merge pull request #9670 from davidmann4/fix/svg-markup-check
SVG: a DTD entity can't hide nesting from the depth check (#9669)
2026-11-06 03:15:57 +01:00
David Mann 03f9f53b13 Merge pull request #9213 from davidmann4/fix/crop-hidden-resample-budget
Crop W x H budgets the hidden pixels it resamples (#9212)
2026-11-06 03:15:14 +01:00
David MannandClaude Opus 5.5 8f7815adc5 Batch 18 (#11295): the shape box names its own step when the transform lands as a job
In the app (background jobs on) edit.transform runs as a job and lands
after app.run returns, so renaming the top History step to "Transform
Path" right away renamed the step before it (the second shape's "New
Shape Layer") and left the scale as "Free Transform". Found in the batch
smoke run; the PR's tests run with background jobs off.

edit.transform now takes stepName "Transform Path" (any other name is
refused, as select.transformSelection's stepName from #11444), and the box
passes it instead of renaming afterwards. New tests: the box with
background jobs on (fails without the fix), and the parameter.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-06 03:01:30 +01:00
David Mann 4c6d5e259a Merge remote-tracking branch 'origin/main' into HEAD
# Conflicts:
#	docs/ui-parity.md
2026-11-06 02:32:59 +01:00
Zinadin ZidanandClaude Opus 5.5 3d60ad01f9 Channels: the panel menu has the channel commands, New Spot Channel among them (#11451) (#11557)
* Channels: the panel menu has the channel commands, New Spot Channel among them (#11451)

The Channels panel's menu had no channel items, so New Spot Channel... and
the rest could only be reached from a row's right-click menu. It now lists
New Channel..., Duplicate Channel, Delete Channel, New Spot Channel...,
Merge Spot Channel, Channel Options..., Split Channels and Merge
Channels..., each acting on the targeted channel and greyed where it can't:
Duplicate takes a single colour, alpha or spot channel, the composite of a
one-channel image, the Quick Mask or a targeted layer mask. Channel
Options... in Quick Mask mode now opens the targeted alpha or spot
channel's options, not Quick Mask Options. The panel menus run their
commands through the menu bar's enabled rules, so the Layers panel menu
greys its dialog items the same way too. The labels are in every catalog.

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

* Channels: New Channel... and Merge Channels... ask first, from the panel and row menus (#11451)

The panel menu's Merge Channels... ran channel.merge with no parameters, and
the engine merged the first grayscale documents into RGB and closed them with
no choice; New Channel... added a channel straight away. Both now open a
dialog when called without parameters, as New Spot Channel... and Channel
Options... already did: New Channel... reuses the Channel Options dialog
(name, colour indicates, colour and opacity), and a new Merge Channels
dialog picks the mode and the open grayscale document for each channel. The
channel rows' menu had the same two items and goes through the same path.
With parameters (scripts, the New button) both still run at once.

The Merge Spot Channel test now checks pixels, so a delete couldn't pass it.

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

* Channels: Merge Channels waits for three grayscale documents and the dialog offers only modes they fill (#11451)

With two grayscale documents open, Merge Channels... was enabled and its
dialog repeated a document for the third channel; OK then failed in the
engine and the dialog closed. The engine's rule now needs three documents
of one size (no mode merges fewer), so the menus grey the item below that.
The dialog offers only the modes the open documents can fill (RGB and Lab
from three, CMYK from four), greys OK with the reason while the picks
repeat, keeps itself open when OK is refused, and comes back with the
engine's message if the merge itself fails.

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

* Channels: the Merge Channels dialog keeps its picks by document id (#11451)

The dialog kept the picked source documents by tab index, and background
work can still run while it is open, so a document moved or closed in the
meantime could leave another one at a picked index; OK would then merge,
and close, the wrong document. The picks are now document ids, resolved to
the current tabs when OK is pressed: a closed pick, or one that is no
longer a grayscale document of the same size, is refused with the reason
and the dialog stays open.

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

---------

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-06 02:20:08 +01:00
David Mann cc361c306d Merge commit '9bef3b2dc8b84c1b6cefc8ce0dc2b01d089cd9e8' into HEAD
# Conflicts:
#	crates/engine/Cargo.toml
#	crates/engine/src/prefs.rs
#	docs/ui-parity.md
2026-11-06 02:16:44 +01:00
David Mann 3af79c0cb7 Merge main 9b02eac2c4 into integration batch 18 2026-11-06 02:14:46 +01:00
David MannandClaude Opus 5.5 88704d2350 Merge fix-up: the turned crop's hidden pixels after Layer 0 replaces the Background
Since #10867 the non-deleting crop turns a 2000 px layer to 2001 px bounds, so
the budget refusal names 200100 x 200100 px.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-06 02:06:41 +01:00
David Mann 4b61c8ef34 Merge commit '9bef3b2dc8b84c1b6cefc8ce0dc2b01d089cd9e8' into HEAD
# Conflicts:
#	crates/algo/src/resample/tests.rs
2026-11-06 01:58:38 +01:00
David Mann dabf3bab97 Merge commit '9bef3b2dc8b84c1b6cefc8ce0dc2b01d089cd9e8' into HEAD
# Conflicts:
#	docs/scorecard.md
2026-11-06 01:54:36 +01:00
David Mann d563b3100b Merge commit '9bef3b2dc8b84c1b6cefc8ce0dc2b01d089cd9e8' into HEAD
# Conflicts:
#	crates/engine/tests/it/svg_smart_object.rs
2026-11-06 01:46:48 +01:00
David Mann b1bd2da9d7 Merge remote-tracking branch 'origin/main' into HEAD
# Conflicts:
#	crates/engine/src/cmd/layer_copy_cmds.rs
2026-11-06 01:33:39 +01:00
David Mann 9bef3b2dc8 Merge pull request #6088 from davidmann4/fix/snapshot-followups
History snapshots: follow-ups to #3567 (#6087)
2026-11-06 01:32:54 +01:00
David MannandClaude Opus 5.5 6e42bc901e Batch 18 (#11458): a guarded label lookup in the Content-Aware crop donor check
The donor check indexed the label buffer directly; the bounds are inside
the canvas today, but a lookup that is out of range now counts as no donor
instead of panicking.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-06 00:53:47 +01:00
e59dcc9c44 GPU plan: cache adjustment LUT rows between plans (#11498) (#11539)
* GPU plan: cache adjustment LUT rows between plans (#11498)

adjustment_program rebuilt the Levels/Curves tone tables on every plan
(twice per refresh), about 1-2 ms per Levels layer each time. Keep the
rows of Levels, Curves, Color Balance and Hue/Saturation ranges in a
32-entry most-recently-used cache (photocraft_compose::lut_rows, at
most 32 x 3 x 16 KiB = 1.5 MiB), built outside the lock.

Entries are keyed by a u64 hash of exactly what each row builder reads,
fields destructured in full and floats by bits (-0.0 and 0.0 stay
apart). Hue/Saturation master sliders don't add entries; Hue/Sat ranges
and Color Balance don't depend on depth. Edit > Purge > All drops the
cache, and a filled cache enables it.

Release, per call: Levels 8/16-bit 1.10 -> 0.003-0.018 ms, Curves
0.16 -> 0.018, Color Balance 0.11 -> 0.001-0.015, Hue/Sat ranges
0.05 -> 0.008-0.018. The tables are identical, so rendering is unchanged.

Tests: cached rows match fresh tables bit for bit at 8/16/32-bit, hits
and LRU eviction on a local cache, key coverage, and Purge All
releasing the cache.

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

* compose: rustfmt the LUT row cache tests (#11498)

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

* GPU plan: test that plans read cached LUT rows and that curve points key them (#11498)

Plant rows no builder makes under a fresh key and check the planner returns
them, and check that moving a Curves point changes its key (review on #11539).

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

* compose: prevent in-flight LUT builds from undoing purge

---------

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Co-authored-by: FredApps <FredApps@users.noreply.github.com>
2026-11-06 00:32:49 +01:00
David Mann 09e44e022c Merge PR #11497: Menus: View › Guides submenu in the reference's order, with Edit Selected Guides and Clear Selected Guides (#11400)
# Conflicts:
#	crates/ui-egui/src/lib.rs
#	docs/parity-checklist.md
#	docs/scorecard.md
2026-11-06 00:29:28 +01:00