Commit Graph
2566 Commits
Author SHA1 Message Date
Korg1ch ccdf082c62 test(caf): erased sampling area previews as an error; a failed OK keeps the workspace open (#7384) 2026-10-18 17:49:09 +03:00
Korg1ch f0ef89baf6 fix(caf): an erased sampling area previews as an error, and a failed OK keeps the workspace open (#7384) 2026-10-18 17:48:53 +03:00
PerchenkoAntonandBrandon Thomas 8d2e615ab9 i18n(uk): translate color picker titles and rasterization warnings (#6153)
Co-authored-by: Brandon Thomas <echelon@gmail.com>
2026-10-18 03:50:35 -04:00
MarekSpodBiedryandBrandon Thomas c39d752758 Translate the brush size and hardness shortcut rows in every language (#6742) (#6831)
Co-authored-by: Brandon Thomas <echelon@gmail.com>
2026-10-18 03:48:59 -04:00
Marco ArnulfoandBrandon Thomas d6ea04299f Document tabs: keep whole names, hide what doesn't fit, list every document in the » menu (#4525)
* Document tabs: keep whole names, hide what doesn't fit, list every document in the » menu (fixes #4487)

* Document tabs: check the active document in the » menu (#4487)

Like Photoshop's, the » menu marks the active document with a check mark,
drawn as the menu bar's menus draw theirs.

---------

Co-authored-by: Brandon Thomas <echelon@gmail.com>
2026-10-18 03:48:54 -04:00
cc772ad9dc Saving onto a dangling symlink writes through to its target (#6605) (#6649)
* fix(format): saving onto a dangling symlink writes through to its target (#6605)

resolve_symlink fell back to the link path when canonicalize failed, so an
atomic save renamed over the symlink and replaced it with a regular file.
Dangling chains now resolve lexically (relative targets included, with a
depth cap), so the bytes land at the target and the link survives; a
symlink loop is a clear error instead of a silent link replacement.

* Follow dangling links without folding .. by text

A folder on the way may itself be a symlink, so leave .. for the OS to resolve.

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-18 03:35:35 -04: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
JunHyun ParkandClaude Opus 5.5 386368956f Korean: consistent terms in the Gradient Editor, Object Selection messages and shortcut hints (#6882)
Eighteen labels in ko.tsv used a different word than the rest of the
Korean catalog for the same term, or named a menu item by a label the
menu does not use (Reset All to Defaults reads 모두 기본값으로 초기화).
Only translation values change; placeholders and ellipses are untouched.

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-18 03:35:22 -04:00
Tom InglingandClaude Opus 5.5 b3c8a2a7f3 Assign Profile and Convert to Profile: Preview, as in Photoshop; Black Point Compensation spelled out (#6825)
- Both dialogs get the Preview checkbox (`PREVIEWED`).
- The canvas shows a filter preview through the preview's own colour management (its profile and
  mode), not the committed document's: Assign Profile changes only the profile, so its preview
  looked unchanged. The preview keeps the document's id, so Proof Colors still apply.
- Previews run with the app's Color Settings: `working` is the chosen working space, not the
  built-in default.
- The `bpc` checkbox (Convert to Profile, Proof Setup) reads "Black Point Compensation" instead of
  "Bpc"; every catalog already translates it.

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-18 03:35:17 -04:00
Carter Smith e9036c72c6 Focus font search when opening the picker (#7222)
* Focus font search when reopening the picker

* Keep font search focused through the opening click release
2026-10-18 03:35:12 -04:00
Jason Xiang 37beb18454 fix(codecs): keep EXR color channels within one layer (#5866) 2026-10-18 03:35:05 -04:00
David MannandClaude Opus 5.5 8a5a680674 Menus: one checkmark column keeps every label aligned (#5705) (#5809)
Menu rows prefixed their label with "✔ ", three spaces or nothing, so
unchecked toggles (Lock Guides, Lock Slices) sat at a different x than
checked and plain rows, most visibly in CJK translations. Every row,
submenu headers included, now reserves one fixed-width checkmark column
(the width of "✔ " in the button font) and draws the mark in it.

Co-authored-by: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-10-18 03:34:58 -04:00
Korg1ch 295a28a50b fix(ui): Ctrl/Cmd-drag in the Navigator zooms to the drawn rectangle, as in Photoshop (#6872) (#7140)
* fix(ui): Ctrl/Cmd-drag in the Navigator zooms to the drawn rectangle, as in Photoshop (#6872)

* test(ui): cover the Navigator zoom rectangle (#6872)
2026-10-18 03:34:52 -04:00
David MannandClaude Opus 5.5 44dc7c2c77 OpenRaster: layer locks no longer change silently on save (#7018) (#7019)
* OpenRaster: layer locks no longer change silently on save (#7018)

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

* ORA: the Background's position lock is not reported as lost (#7018)

Every new document's Background carries a position lock, so the warning made a
plain .ora unsavable in place.

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

---------

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-18 03:34:47 -04:00
Michael HigginsandClaude Opus 5.5 a914c58a63 fix(ui): wide ellipse for Elliptical Marquee, dashed C + brush for Quick Selection (#6875) (#6917)
The Elliptical Marquee used a dashed circle and Quick Selection a brush
inside a dashed circular arc, so the two read alike. Photoshop draws the
marquee as a horizontal ellipse and Quick Selection as a brush in an
upright dashed rounded C. New original ellipse-dashed.svg; quick-selection.svg
redrawn; tests check the ellipse is wider than tall and the C runs down
the left edge.

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-18 03:34:42 -04:00
NetterSilverandClaude Haiku 5.5 866e7bfe4e Upload 16-bit layer masks as R16Float instead of R32Float (#6845)
Fixes #6534

Co-authored-by: Claude Haiku 5.5 <noreply@anthropic.com>
2026-10-18 03:34:37 -04:00
NetterSilverandClaude Haiku 5.5 dfb4aa70ae Keep Color Sampler points on their pixel after flips and turns (#6720)
Found while testing crates/engine/src/commands.rs:117-120 (turn) with crates/engine/src/canvas_geom.rs:224 (Turn::affine); applied in crates/engine/src/lib.rs:807-814 (Session::edit)

Co-authored-by: Claude Haiku 5.5 <noreply@anthropic.com>
2026-10-18 03:34:32 -04:00
David MannandClaude Opus 5.5 0296c8e0b5 Remove tool: every queued stroke is tinted in full (#6555) (#6642)
With Remove after each stroke off, every finished stroke was fed into the
one shared tint trail, whose point counter still held the previous
stroke's length, so later strokes were stamped only past that count.
Trail::feed_stroke starts each new stroke's count at zero.

Co-authored-by: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-10-18 03:34:27 -04:00
denstu473 68dfaf1ae6 Check unclamped transform dimensions against the pixel budget (#6493) 2026-10-18 03:34:21 -04:00
David MannandClaude Opus 5.5 e6c31f4d4a Menus: rows scrolled out of view no longer open their submenus (#5896, #6225) (#6356)
A tall menu reopens where it was last scrolled to, so after reaching Edit ›
Preferences › Settings… or Window › Theme its top rows lie hidden above the
view, over the menu bar. egui opens a submenu whenever the pointer is over its
row, clipped or not: the hidden submenu row under the title opened on the
press, and the release on the title counted as a click outside that submenu,
which closed the whole menu. Hovering the top scroll arrow scrolled the menu
back up, which is why that "fixed" it.

Submenu rows are now drawn like `ui.menu_button` but skip egui's submenu
logic while the pointer is over a part of the row that is scrolled out of
view (an already open submenu, e.g. from the keyboard, is drawn as before).

Co-authored-by: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-10-18 03:34:16 -04:00
xsvm f2ae78ed93 perf(psd): 8-bit merged composite unmattes through a table, not a division per sample (#5694) (#6029)
composite_rgba8 removed the white matte from 8-bit RGB composites with
one integer division per colour sample (3 per pixel, ~108 million on a
36 MP file). The result depends only on the sample and its alpha, so the
same expression, now unmatte_u8, fills a 256 x 256 table once per call
and each sample is a lookup. Built per call, not as a static, so no
binary (wasm included) grows by the table.

The new test runs all 65 536 (sample, alpha) pairs through
composite_rgba8 against the replaced loop, kept verbatim as the
reference.
2026-10-18 03:34:11 -04:00
Jason Xiang c41a7957f9 fix(io): exclude transparent palette entries for opaque PNG pixels (#5869) 2026-10-18 03:34:05 -04:00
.CooCoo ea647146db Swatches: the .ase reader refuses files with more than 10,000 groups (#5811)
photocraft_psd::ase::parse counted colors toward MAX_SWATCHES but not
group starts, so the number of groups it read grew with the file's
size. An empty group start is six bytes, and parse held about 64 bytes
for each: in a test build, parsing a 60 MB file of 10,000,000 empty
group starts raised the process's peak memory from 63 MB to 705 MB.
swatches.import reads files of up to 512 MiB, and read_file built a
group for each group start before the import refused more than 10,000
groups.

parse now counts group starts and returns LimitExceeded past
MAX_GROUPS (10,000), the most groups the Swatches panel holds. Files
with at most 10,000 groups parse as before, so every file the import
accepted still loads. Files with more were refused before as well; now
parse refuses them before it holds more than 10,000 groups. write also
refuses more than MAX_GROUPS groups, as it refuses more than
MAX_SWATCHES colors.

ase_group_cap_is_enforced_on_hostile_input parses 10,001 empty group
starts and expects LimitExceeded, then parses the same file with one
block fewer and expects all 10,000 groups. too_many_groups checks that
write refuses 10,001 groups and that 10,000 groups are written and
read back.
2026-10-18 03:34:00 -04:00
David MannandClaude Opus 5.5 0f0f58c2ab PSD: layer_tree_max_depth hoists layers under unclosed dividers past the cap (#5758) (#5759)
Frames past the cap collected nothing, so the layers under dividers that
are never closed were lost once those dividers nested past max_depth,
and nothing was left to hoist at the end. A first pass now matches
dividers to folders and only closed dividers open a frame, so every
frame's depth is the final depth of its nodes and the capped tree equals
layer_tree cut at max_depth, still at most max_depth + 1 levels deep.

Co-authored-by: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-10-18 03:33:55 -04:00
David MannandClaude Opus 5.5 b1de6e8ade Preset Manager: renaming or deleting a brush keeps the current preset and names consistent (#5681) (#5682)
Edit > Presets > Preset Manager renamed and deleted brush presets directly, so a rename
left the current preset pointing at the old name (brush.presets.update then failed), accepted
a case-insensitive duplicate name, and a delete left the current preset dangling. It now uses
the same rules as brush.presets.rename / brush.presets.delete through two small shared
helpers (rename_at, forget_deleted_preset). Patterns and custom shapes are unchanged.

Co-authored-by: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-10-18 03:33:50 -04:00
068ca57210 Document tab says "Quick Mask" in Quick Mask mode, as in Photoshop (#6660)
* Document tab says "Quick Mask" in Quick Mask mode, as in Photoshop

In Quick Mask mode, painting and Free Transform target the mask, not the
layer. The tab still read "(Layer 1, RGB/8)", so a user who pressed Q by
accident and then rotated saw the red overlay turn with the box and the
layer stay put, with nothing on screen saying why. The tab now reads
"(Quick Mask/8)", the same condition `paint_target` routes on.

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

* Credit arnav-hiwarkar in the About window

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

* Keep contributors/people.toml as on main

The branch removed another contributor's entry; the added entry only repeated the username.

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

---------

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Co-authored-by: Brandon Thomas <echelon@gmail.com>
2026-10-18 03:33:45 -04:00
f6f53f657d Reject notes.set moves that make the popup non-finite (#6174)
Found while testing crates/engine/src/notes_cmds.rs:105

Co-authored-by: Claude Haiku 5.5 <noreply@anthropic.com>
Co-authored-by: Brandon Thomas <echelon@gmail.com>
2026-10-18 03:33:40 -04:00
Nicolas FalesyandClaude Opus 5.5 2301ecf9e9 fix(linux): file dialog probe sees a portal with no FileChooser backend (#5739) (#6422)
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>
2026-10-18 03:33:35 -04:00
Michael HigginsandClaude Opus 5.5 0007cad01a Paste: an image pastes with Ctrl+V whichever key is let go first (#6329) (#6360)
With no text on the clipboard (an image copied from a browser, a file from Explorer) egui-winit
swallows the Ctrl+V press and sends only the V release; the paste then ran only when that release
still carried Ctrl. Letting go of Ctrl first, as many people do, pasted nothing, so Ctrl+V seemed
to need several tries. A V release whose press never arrived, after Ctrl was held and before any
other key, is now that swallowed Ctrl+V and pastes once. A plain V tap (the Move tool) and the
web build, which always gets the key-down, are unchanged.

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-18 03:33:29 -04:00
Andrew Barnes 436ca3569c fix(gpu): clamp device limits to adapter support (#5986) (#6050)
Keep backend defaults within supported limits while preserving the
adapter's texture and buffer maxima. Add regressions for V3D shader
variables, weaker GL adapters, minimum alignments and capable devices.
2026-10-18 03:33:24 -04:00
David MannandClaude Opus 5.5 977cf5f6de Krita: uncompressed tiles are read as interleaved pixels (#7082) (#7083)
* Krita: uncompressed tiles are read as interleaved pixels (#7082)

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

* kra: the agent-surface test writes its raw tile interleaved, like Krita (#7082)

The krita.rs fixture wrote its flag-0 tile as byte planes, which only the old
decoder read back correctly.

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

---------

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-18 03:33:19 -04:00
David MannandClaude Opus 5.5 a002e66824 Snapping: layer targets ignore the selection, so a moved selection snaps to guides and the document (#6290) (#6355)
Snap To Layers built each layer's edges and centre from transform_bounds, which clips the
layer to the selection. With a selection, every layer's targets were the selection's own
bounds, so dragging the outline with a marquee snapped it back to where it started (shown
as layer alignments) and beat nearby guides and document edges. Layer targets now use the
layer's whole content bounds, as in Photoshop; the moving box (layer_rect) is unchanged.

Co-authored-by: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-10-18 03:33:14 -04:00
NetterSilverandClaude Haiku 5.5 275d4cbcf7 Reject out-of-range leading, baseline shift and scales in type styles (#6196)
Found while testing crates/engine/src/type_cmds.rs:287

Co-authored-by: Claude Haiku 5.5 <noreply@anthropic.com>
2026-10-18 03:33:09 -04:00
Michael HigginsandClaude Opus 5.5 16774eea6f Twirl, Pinch and Spherize fill the inscribed ellipse (#5643) (#5871)
They acted inside a circle fitted to the bounds' shorter side, so on a wide
or tall image only a central disc changed. Like Photoshop's, they now act
inside the ellipse inscribed in the bounds (selection or layer), reaching all
four edges; square bounds are unchanged.

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-18 03:33:04 -04:00
Michael HigginsandClaude Opus 5.5 c408c83a9f Pencil: a 1 px line paints the same pixels whichever end it starts from, at any Smoothing (#6336) (#6341)
The pixel-grid walker stepped between the pixels holding each segment's ends. Smoothing splits
a Shift-click line into pieces along it (so does the pointer on a Shift-drag), and snapping every
piece to its own pixels bent the line into a jagged staircase; rounding the half-pixel ties away
from zero also made the line depend on which end it started from. The walker now samples the
segment's own line at each column (or row) centre and resolves ties on the pixel grid, so the
pieces of a straight line paint exactly the pixels of the whole line, both ways.

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-18 03:32:58 -04:00
David MannandClaude Opus 5.5 175bdce659 GPU: a group's effect cache follows its vector mask and children's Blend If (#5706) (#5707)
* GPU: a group's effect cache follows its vector mask and children's Blend If (#5706)

group_key, which decides when a styled group's effect shape and distance
fields are rebuilt on the GPU, hashed the group's children and layer mask
but not the group's own vector mask, nor each child's Blend If and
Advanced Blending. A group's Stroke or Drop Shadow kept the old outline
after those changed. Hash them, as the CPU's layer_identity does.

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

* GPU: pass the pin callback in the group key test after main's signature change

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-18 03:32:53 -04:00
FinnNkandClaude Opus 5.5 9ae0f624fc test(type): PostScript names guessed apart from their installed family resolve exactly (#5471)
Covers #5044's two names: BMDoHyeon (family "BM DoHyeon", guessed "BM Do Hyeon") and Daum_SemiBold (guessed "Daum_Semi Bold"). Both resolve to their installed faces and aren't reported missing on main, through the exact PostScript lookup (#1844) that missing-font checks follow since #608.

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-18 03:07:55 -04:00
MarekSpodBiedry 2e1cc763a7 Stamp Visible fills an empty active pixel layer instead of adding another (#6785) (#6828) 2026-10-18 03:07:50 -04:00
NetterSilverandClaude Haiku 5.5 fb5896f43f Fix generator spaced size keeping size text in asset file name (#6628)
Found while testing crates/engine/src/web_cmds.rs:1171

Co-authored-by: Claude Haiku 5.5 <noreply@anthropic.com>
2026-10-18 03:07:45 -04:00
David MannandClaude Opus 5.5 e8ab6846c0 Place Scale Marker: lay out the label in points, not pixels (#5754) (#5755)
fontSize is in points and type.create renders it at resolution/72 px
per point, but the bar height, label height and baseline treated it as
pixels. At 300 ppi a 12 pt label (50 px) overlapped the bar. Lay out in
font_size * dpi / 72 px; type.create still gets points. 72 ppi is
unchanged.

Co-authored-by: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-10-18 03:07:40 -04:00
David MannandClaude Opus 5.5 5a9fb94b9b Type: Paste Lorem Ipsum inserts into the type layer named by "layer" (#5659) (#5660)
paste_lorem decided whether to insert into existing text from the active
layer, while with_text edits the layer the "layer" param names. With a
pixel layer active, {"layer": T} made a new paragraph layer and left T
unchanged. An explicit "layer" now always goes through with_text, so a
type layer gets the text and any other layer is an error; without
"layer" the behaviour is unchanged.

Co-authored-by: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-10-18 03:07:34 -04:00
David MannandClaude Opus 5.5 20c3e95a6b EXR: the ACEScg preference applies to every way of opening a file (#6782) (#6783)
The "Open color-less EXR as ACEScg" option was only applied by file.openAs; background
opens (File > Open, drag-drop, Open Recent via Session::start_open) and file.openExrParts
kept io's linear-sRGB default. Both now retag through tag_exr_colorspace, start_open before
the Ask When Opening prompt so it sees the ACEScg profile.

Co-authored-by: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-10-18 03:07:29 -04:00
Korg1ch 84f326fa25 Actions: a Paint Bucket step keeps its explicit colour through .atn (#7199) (#7208)
* fix(atn): a Paint Bucket step keeps its explicit colour through .atn export and import (#7199)

* test(atn): an explicit Paint Bucket colour survives an .atn round trip (#7199)
2026-10-18 03:07:23 -04:00
Korg1ch f04fc80267 fix(engine): flip and rotate presets turn about the selection's bounds, as Free Transform does (#6928) (#6967)
* fix(engine): flip and rotate presets turn about the selection's bounds, as Free Transform does (#6928)

* test(engine): cover flips and turns about the selection (#6928)
2026-10-18 03:07:18 -04:00
NetterSilverandClaude Haiku 5.5 e66941bde1 Ignore a leading UTF-8 BOM in variable data set CSV import (#6485)
Found while testing crates/engine/src/variables_cmds.rs:285-292 (import_data_sets_text; the file is read in import_data_sets at line 279)

Co-authored-by: Claude Haiku 5.5 <noreply@anthropic.com>
2026-10-18 03:07:13 -04:00
wwew 73cd277a03 fix(engine): check the define-brush tip size multiply before reserving (wasm32) (#6902) 2026-10-18 03:07:08 -04:00
NetterSilverandClaude Haiku 5.5 d722dd8b54 Clamp filter.cameraRaw exposure to its documented -5..5 range (#6876)
Found while testing crates/algo/src/camera_raw.rs:592 (value taken unchecked in crates/engine/src/lens_cmds.rs:137 raw_params)

Co-authored-by: Claude Haiku 5.5 <noreply@anthropic.com>
2026-10-18 03:07:03 -04:00
David MannandClaude Opus 5.5 8ef5337c29 Measurement Scale: large pixel lengths print their real value (#7016) (#7017)
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-18 03:06:59 -04:00
David MannandClaude Opus 5.5 13bbf0eefe Custom filter: in CMYK a positive offset brightens (#7076) (#7077)
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-18 03:06:54 -04:00
David MannandClaude Opus 5.5 f3a9b81b1f Duotone: view and flat export clamp ink density like the Multichannel conversion (#6854) (#6855)
Duotone::render used the raw curve output, so ink curves above 100 % pushed the
simulated print out of gamut (red + blue at full density showed green instead of black).

Co-authored-by: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-10-18 03:06:49 -04:00