Commit Graph
3466 Commits
Author SHA1 Message Date
Dan HiornsandClaude Opus 5.5 ad62f350e5 Symmetry: Transform Symmetry gets commit/cancel buttons and its own cursors (#4495) (#4508)
Transforming the painting symmetry could only be applied with Enter or
dropped with Esc, which a pen or touch user may have no key for, and the
canvas kept the brush outline although a press there doesn't paint.

While the transform is open, the options bar now shows "Transform
Symmetry" with commit and cancel buttons, like Free Transform's bar. The
canvas shows the move cursor over the centre, the rotate cursor over the
rotation handle and the arrow elsewhere.

Test: transform_has_commit_and_cancel_buttons_and_its_own_cursors.

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-13 12:18:21 -04:00
Michael HigginsandClaude Opus 5.5 c72dda1914 Assign Profile: list only profiles of the document's colour mode (#4951) (#5020)
The profile menu offered every built-in and installed profile, and picking
one of another colour space only produced "profile is Rgb; the document is
Cmyk" in the status bar. The menu now lists the profiles the command accepts
for the document's mode, and a remembered choice that doesn't fit falls back
to the working space.

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-13 12:11:56 -04:00
imSlicedBread a0627311ab Fix window dragging with modal dialogs (#4913) 2026-10-13 12:11:50 -04:00
f43d019a72 Brush Settings: use an amount slider for Hue Jitter (#4874)
Use the shared percentage row so the jitter amount does not look like a hue selection. Cover the rendered themed track, numeric and pointer edits, neighboring settings and engine journal replay.

Addresses the Hue Jitter appearance item in #3825; other assessment items remain open.

Co-authored-by: augrclk <217665348+augrclk@users.noreply.github.com>
Co-authored-by: GPT-6 <noreply@openai.com>
2026-10-13 12:11:44 -04:00
Dan HiornsandClaude Opus 5.5 05d361a367 Shortcuts: ⌥⇧⌘N makes a new layer without the dialog (#4794) (#4807)
⇧⌘N opens the New Layer dialog, as Layer › New › Layer… does in Photoshop.
Photoshop's companion shortcut, the same keys with ⌥ (Ctrl+Alt+Shift+N on
Windows and Linux), makes the layer at once; PhotoCraft didn't handle it,
so there was no way to get 0.7.0's dialog-free behaviour from the keyboard.

New Layer's shortcut with ⌥ added now runs layer.new.layer directly. It
follows a rebound New Layer shortcut, and a command bound to that key
itself keeps it.

Fixes #4794

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-13 12:11:37 -04:00
Dan HiornsandClaude Opus 5.5 e4d281eb20 Brushes: preset card size numbers stay clear of the tips (#4454) (#4485)
In the Brush Preset picker's cards the tip thumbnail grew to 44.8 pt in a
42 pt cell while the size number was anchored to the cell's bottom, so
the number of any medium or large brush (Hard Round's 30, Soft Round's
45) was drawn under the tip and couldn't be read.

card_tip_rect now keeps a band for the number at the bottom of the cell
and fits the tip above it, still scaled by the preset's thumb_scale so
big brushes read bigger than small ones. Compact cards (no number) are
unchanged.

Test: card_tips_never_cover_their_size_number checks every footer scale
that shows numbers, in all three card kinds.

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-13 12:11:31 -04:00
Marco Arnulfo 2ca12e17b5 Direct Selection: arrow keys nudge the selected anchors (fixes #4486) (#4500) 2026-10-13 12:11:24 -04:00
David MannandClaude Opus 5.5 afb7de2d3a Performance: brush dab on a CMYK document within budget (#5108)
A brush dab on an A4 300 ppi CMYK page spent ~5 ms per touched tile in the
GPU canvas's tile upload: every pixel of the 256x256 tile went through the
CMYK -> sRGB transform on one thread. CMYK and Lab tiles now convert in
16-row bands in parallel, and a run of identical pixels (the transparent or
flat area around a dab) is converted once. Texels are bit-identical to the
per-pixel conversion.

The band workers enter the document's CMYK profile scope; before, the rayon
workers converting tiles did not, so a CMYK document tagged with its own
profile uploaded tiles through the built-in coated CMYK instead.

P33 (Windows 11, 32 threads, RTX 5070 Ti, 15 reps): p95 13.7-16.8 ms -> 2.6-3.3 ms.
enforce stays false: the committed baseline machine measures every row
over budget, so it is left for the next baseline.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-10-13 18:10:21 +02:00
FinnNkandClaude Opus 5.5 33728243fb perf(gpu): closing a document frees what the compositor holds for it (#4529) (#4772)
GpuCanvas::retain called forget_doc only for documents still in its
texture map. A document whose entry had been dropped earlier (texture
creation failed, a fresh render failed after uploading, or the canvas
outgrew its budget after Canvas Size) kept its resident pages, effect
maps and lower-stack composite after it was closed, until another
document pushed the budgets over; with no other GPU document, for good.

Compositor::retain_docs forgets every document the caller doesn't list
as open, whatever the texture map says, and retain calls it each time.
The open documents' ids, their preview ids and the adjustment proxies
are already in that list, so nothing in use is dropped.

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-13 12:07:27 -04:00
deadmeuandClaude Opus 5.5 533b9a1c62 perf(raster): moving a layer no longer stores tiles of only the default pixel (#4844)
Surface::translated, under every layer move and the Move drag preview, allocated
every target tile that a moved source tile reached. For a move that isn't a whole
number of tiles, the transparent margins of a layer's edge tiles landed on target
tiles of their own, and those were stored though they hold only the default pixel
(256 KB each at 8-bit RGBA). #4414's shared uniform tiles were also stored when
their value was the default. Moving two 300 px blocks by (100, 56) left 10 of 18
stored tiles empty; nudging back and forth kept adding more.

A target that ends up holding only the default pixel is no longer stored, as
prune() would drop it. A copied tile is compared row by row with a row of the
default pixel after the copy (one memcmp per row; a tile with content stops at
its first row), and a shared uniform tile of the default value is skipped. Moves
by whole tiles still re-share the existing tiles unchanged.

Release, 24 cores, cargo xtask perf --quick (perf_scenarios, p50, HEAD vs this,
two runs each): move layer on 16-bit 15000x10000 + refresh 5.3 -> 3.9 ms; move
layer on 32-bit 20 layers + refresh 11.9 -> 5.9 ms; the other Move rows within
noise. translated() alone on 36 MP: full layer unchanged (27.0 -> 27.6 ms RGBA8);
40 sparse blocks 11.8 -> 10.6 ms RGBA8, 44.0 -> 39 ms RGBA16, 308 -> 190 tiles.

The new test covers RGBA8, RGBA16, RGBA32F and GRAY8 over a zero and a non-zero
default pixel, and both paths; it fails on the old code.

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-13 12:07:20 -04:00
Timothy DalyandClaude Opus 5.5 f1233bb43a io: a tagged CMYK file's merged image reads through its own profile (#4948)
merged_composite showed a CMYK merged image through the built-in coated
profile, while the compositor reads the file's layers through the
embedded one. Every oracle comparison of a tagged CMYK file therefore
measured two different profiles. psd-tools' CMYK files were 88-100% off
even with nothing to adjust: colormodes/4x4_8bit_cmyk.psd, a plain
image, was 100% off.

It now converts inside the document's CMYK space (compose::cmyk_space).
The test reads the merged inks back and checks them against the embedded
and built-in conversions.

psd-tools, bad pixels (pass count unchanged at 213/309):
- invert_cmyk: 98% -> 15%
- levels_cmyk: 95% -> 14%
- curves_cmyk: 98% -> 37%
- threshold_cmyk: 65% -> 7%
- posterize_16bits_cmyk: 88% -> 13%
- 4x4_8bit_cmyk: 100% -> 6%

With #4945's ink compositing, invert_cmyk and
brightnesscontrast_legacy_cmyk pass (215/309).

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-13 12:07:14 -04:00
Gert Pellin - GPE 7b0dbf182b PNG: a whole-ppi pHYs reads back whole (300 ppi stays 300) (#4857)
pHYs holds whole pixels per metre, so 300 ppi is stored as 11811 and
read back as 299.9994 (72 as 72.009). The ppi read is now the whole
number that rounds to the stored value when there is one, as Photoshop
reads it; other values stay as they are.

Fixes #4855
2026-10-13 12:07:08 -04:00
Nigel Ihza Firdausy 5320cecb6d fix(adjust): preserve midtone luminosity with multiple sliders (#4941) (#4990) 2026-10-13 12:07:03 -04:00
Dan HiornsandClaude Opus 5.5 87aed12d62 Brush import: Dual Brush spacing comes from its tip (#4809) (#4889)
Photoshop CC brush presets store the Dual Brush spacing the panel shows in
the dual tip (dualBrush > Brsh > Spcn), and also write a Spcn of 100 % on
the dualBrush object beside it. The importer read that outer value last,
so every dual brush came in at 100 % spacing (the reporter's 10 %, 18 %
and 24 % presets all did), which changes the stroke a lot.

The tip's spacing now wins; the outer value only stands in for a tip
without one.

Fixes part 1 of #4809

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-13 12:06:56 -04:00
F4LCON 1168a9dd73 Pen: a new path joins the selected path instead of replacing the Work Path (#4721) (#4730) 2026-10-13 12:03:43 -04:00
Nigel Ihza Firdausy 625141c2ad fix(ui): display uppercase hex values in the color picker (#4859)
* fix(ui): show uppercase RGB hex values in shared color picker

* test(text): expect uppercase hex in text style color picker

* fix(ui): uppercase displayed hex without changing color storage

* test(text): keep color serialization independent of picker display casing
2026-10-13 12:03:37 -04:00
F4LCON 5adba14cd6 Navigator: one zoom row as in Photoshop, the percentage, Zoom Out, the slider and Zoom In (#4840) (#4854) 2026-10-13 12:03:30 -04:00
Valentin KleinandClaude Opus 5.5 cca24dd0d8 Type options: the wheel over the font menu steps through the families (#3350) (#3524)
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-13 12:03:23 -04:00
jonathanwxh-cellandjonathnwxh f20605219e Keep applied guide layouts visible after preview closes (#5084)
Co-authored-by: jonathnwxh <258340642+jonathanwxh-cell@users.noreply.github.com>
2026-10-13 12:03:16 -04:00
Nigel Ihza Firdausy 35710a94ad Fix: native gesture fallback dedupe (#5080)
* fix(input): avoid double-applying native pinch during legacy fallback

* test(input): cover native trackpad rotation and preference gating
2026-10-13 12:03:11 -04:00
Alon 77d3478df1 fix(gradient): options-bar Opacity updates the live gradient layer (#5012) (#5045) 2026-10-13 12:03:05 -04:00
Marco Arnulfo 997406a584 Layer Style applies an open Free Transform first, so its preview shows (fixes #4821) (#4835) 2026-10-13 12:02:58 -04:00
Marco Arnulfo 78b70df227 Canvas: the CPU canvas shows crisp pixels when zoomed in (fixes #4810) (#4825) 2026-10-13 12:02:51 -04:00
Marco Arnulfo 665cb581ad Smart Filters: Delete removes the selected filter, not its smart object (part of #4751) (#4764) 2026-10-13 12:02:44 -04:00
Ivan · VanTan eae87bacfa i18n(ru): translate dynamic selection, theme and measurement labels (#4838) 2026-10-13 12:02:38 -04:00
Sir Starch 2ec1c8c0aa Themes: default light theme to Pro Light to preserve the Pro layout (#5016) (#5029)
When appearance mode was toggled between dark and light with default
settings, PhotoCraft switched from Pro Medium (Pro layout) to Studio Light
(Studio layout), altering the UI and displaying a Fit button in the status
bar.

Now that the Photoshop Pro light brightness levels exist (#2661), default
the saved light theme to Pro Light so the Pro interface layout and status
bar are preserved when toggling appearance. Also map the "light" alias to
Pro Light and list the Pro options first under Light Theme preferences.

Fixes #5016.
2026-10-13 12:02:32 -04:00
Dan HiornsandClaude Opus 5.5 334d378f0f Toolbar: clicking a tool's button again closes its open flyout (#4932) (#4937)
A right-click (or long press) on a tool button opens the flyout of the
tools grouped with it. Any press outside the flyout closed it, but the
press-outside check leaves out the tool button itself, so clicking that
button again, with either mouse button, left the flyout open.

A click on the button of its open flyout now closes it, as in Photoshop:
a right-click just closes it, a left click closes it and picks the tool
as any click does. The release that ends the long press that opened the
flyout still keeps it open.

Fixes #4932

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-13 12:02:26 -04:00
F4LCON 3d54e8cd68 Alerts: Enter is OK, but not the Enter that raised the alert (#4872) (#4880) 2026-10-13 12:02:12 -04:00
F4LCON e50b25f45c Shapes: test that the options bar Fill swatch recolours the shape just drawn (#3997, #3999) (#4510)
In v0.7.0 the options bar's Fill swatch only previewed the foreground
colour, so a drawn shape's fill could not be changed from it while Stroke
could. Main fixed that in #3019 and #3370; this test covers the reported
steps (draw a rectangle, pick a colour from the Fill swatch): the fill
changes, the stroke does not, and it is one undo step.
2026-10-13 12:02:07 -04:00
Marco Arnulfo 78dea7c6e4 fix(ui): an open Free Transform box takes Zoom tool drags and clicks (part of #4799) (#4808) 2026-10-13 12:02:01 -04:00
Marco Arnulfo 9a4efabe22 A file dropped on the canvas applies the open Free Transform and is placed at once (fixes #4833) (#4849) 2026-10-13 12:01:54 -04:00
mroxsoandhighperfocused 26e003059b fix(ui): a save name without an extension gets the suggested one (#5039) (#5049)
Co-authored-by: highperfocused <highperfocused@pm.me>
2026-10-13 12:01:47 -04:00
Michael HigginsandClaude Opus 5.5 6f9227efa3 Brush Settings: no section title above the options pane (#3477) (#5017)
* Brush Settings: no section title above the options pane (#3477)

The options pane repeated the selected section's name as a title line.
The selected row in the section list already names it, as in Photoshop,
so the title is gone and the options start at the top of the pane.

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

* i18n test: the brush section name is drawn once, without a heading (#3477)

dynamic_brush_sections_have_translations_and_draw_in_the_selected_language
still expected the translated section name twice: the list row and the
options pane heading this branch removes. It now expects the row alone.

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

---------

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-13 12:01:40 -04:00
Jyrki VesterinenandClaude Sonnet 5.5 1304765067 fix(canvas): Fit View opens small images at 100%, not at the display scale (#5089)
fit_view capped the fit at 1.0 in egui points and then multiplied by
pixels-per-point, so on a 150% display a small image opened at 150%. Since
View::zoom became physical pixels per document pixel (27263bb16, #1978) the
cap has to apply after the conversion: scale by ppp first, then min(1.0).

The test for fit_view asserted the old value (zoom 2.0 at ppp 2.0); it now
checks a small image opens at 1.0 for ppp 1.0, 1.5 and 2.0 and that a large
image still shrinks to fit.

Co-authored-by: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-13 12:01:34 -04:00
David MannandClaude Opus 5.5 8eef90bf7e Tests: generated large-file fixtures and a P20 number (#5107)
photocraft_psd::fixtures (feature testgen) generates the FILE-216-2
fixtures at test time: a 20 MP layered PSD and a 150-layer A4 layout
(groups, masks, hidden layers, drop shadows, known merged colours), a
2.02 GB PSD just under Photoshop's cap and the 4-5 GB PSBs, streamed row
by row. io tests/large_files.rs opens, checks, saves and re-opens each
(the 2 GB PSD and 4.38 GB PSB are opt-in). The ui-egui large_file_bench
times opening the big two, wired to P20 and a new P60.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-10-13 17:58:38 +02:00
Nigel Ihza Firdausy ccb33741f9 fix(psd): reserve imported layer IDs when allocating section dividers (#4885) 2026-10-13 11:56:55 -04:00
Nigel Ihza Firdausy 40c9c3c451 Validate merged PSD channel indices before decoding (#4994)
* fix(psd): validate merged channel index before decoding (#4981)

* test(psd): compare decoded channel bytes as vectors
2026-10-13 11:56:47 -04:00
mroxsoandhighperfocused 41e578b4d3 fix(io): a version 3 Photo Filter colour is XYZ relative to the D50 white (#4987)
Photoshop CS2 stores the phfl version 3 colour as XYZ divided by the D50
white point (X / 0.9642, Y, Z / 0.8249), so white is (32768, 32768, 32768).
We read those values as absolute D50 XYZ, giving every CS2 Photo Filter a
slightly wrong colour: the Yellow filter preset (24191, 25002, 3827) decoded
to #FFE03A instead of #F9E31C, and Warming Filter (85) (15548, 12029, 0) to
#F18600 instead of #EC8A00. Multiply by the white point before converting;
decoded this way, every version 3 colour in the reporter's CS2 files is
exactly a Photoshop preset (#4940).

Fixes #4940

Co-authored-by: highperfocused <highperfocused@pm.me>
2026-10-13 11:56:40 -04:00
1735c81e67 fix(segment): stop push/relabel looping forever on unreachable nodes (#4869)
Remove Background (and Select Subject's full-resolution refinement) could
hang at "Finding subject" on some photos. One refinement tile's max-flow
never finished, and the job ignores Cancel there.

Push/relabel's admissibility test was `dist[i] == dist[j].saturating_add(1)`.
A global relabel gives nodes reachable from neither terminal the height
u32::MAX, and MAX.saturating_add(1) == MAX, so two such neighbours counted
as admissible for each other. When f32 rounding strands excess on them
(source capacities near 3e8 have an ulp of 32), they pushed it back and
forth forever. Use checked_add so an unreachable node is never a push
target; its excess stays put and the loop ends.

The regression test is a 6-node graph that looped before and now matches
the Boykov-Kolmogorov cut.

Co-authored-by: lloyd <lloyd@stackworx.io>
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-13 11:56:34 -04:00
Marco Arnulfo 5c73f4fffa TIFF: save uncompressed by default, like Photoshop (fixes #4484) (#4491) 2026-10-13 11:56:27 -04:00
Gert Pellin - GPE cb6ccaafc4 Flat export: a Lab document with one layer isn't reported as flattened (#4853)
Lab skips the native single-layer path (the codecs have no Lab layout)
and goes through the sRGB composite, which always warned that the
layers were flattened. With a single plain layer nothing is flattened:
only the sRGB conversion is reported now.

Fixes #4852
2026-10-13 11:56:21 -04:00
Gert Pellin - GPE 88ed837a86 Save for Web: JPEG chroma is full (4:4:4) above quality 50, as in Photoshop (#4823)
Save for Web subsampled the chroma (4:2:0) up to quality 89. Photoshop
2025's Save for Web writes 4:2:0 up to quality 50 and 4:4:4 from 51
(measured on its output; its Save As keeps 4:2:0 at every quality, as
PhotoCraft's Save As already does).

Fixes #4822
2026-10-13 11:56:16 -04:00
Ufuk Efe fe04f6acaa docs: credit Ufuk Efe in people.toml (#3809) (#5056) 2026-10-13 11:46:50 -04:00
Fabio Lissi c7a34b04ec Scorecard: Smart Object corner skew and perspective is done (FILE-215-6, #426) (#5094)
#426 gave smart objects a projective placement (`SmartObject.perspective`,
with `transform` as its affine approximation): Distort and Perspective set
it, the layer re-renders from its source through the homography, Warp
composes with it, and the PSD placement reads and writes
`nonAffineTransform` / `Trnf`. The scorecard row still said "missing" with
the pre-#426 note, as did the ui-parity row and the #579 mention in gaps.md.
2026-10-13 11:46:42 -04:00
shmuel billerandskylar taylor-barrick 2d5a7497fd fix(format): harden directory-bundle cleanup
Protect Unix bundle cleanup with pinned directory handles and remove only recognized content-addressed files. Keep legitimate linked roots and write/search-only directories working, with regression coverage for GC races and permission failures.

Co-authored-by: skylar taylor-barrick <hi@skylar.cc>
2026-10-13 09:20:31 -06:00
David MannandClaude Opus 5.5 511b55bec3 History Brush source: Serbian labels
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-10-13 13:10:26 +02:00
David MannandClaude Opus 5.5 716e398ede Regenerate docs/scorecard.md after merging main
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-10-13 12:47:44 +02:00
David MannandClaude Opus 5.5 09af99c6f3 History panel menu: Serbian labels
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-10-13 12:41:44 +02:00
David MannandClaude Opus 5.5 c951480e99 Merge origin/main into feat/tool-213-7-live-retouch
Background Eraser: main now samples and matches against the pre-stroke pixels (#4552); the
shared dab effect takes the pre-stroke surface so the live preview, whose working areas are
per dab, reads the same pixels as the commit.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-10-13 12:33:25 +02:00
David MannandClaude Opus 5.5 03f929bffb Merge main into fix/history-brush-source
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-10-13 12:08:08 +02:00