Commit Graph
5750 Commits
Author SHA1 Message Date
David Mann 9c5a7b907a Merge remote-tracking branch 'origin/main' into integration-batch-7
# Conflicts:
#	crates/ui-egui/src/i18n/tr.tsv
#	docs/ui-parity.md
2026-10-24 00:34:11 +02:00
David MannandClaude Opus 5.5 f38004e721 Address CodeRabbit review: an opacity-only style preset added with ⇧ sets the opacity (#4049)
`style.presets.new` with `includeBlending: false` and an explicit `opacity` stores only that
opacity, but `apply` with `add: true` skipped the blending block unless the preset had a blend
mode, so the layer kept its opacity. Test: an_opacity_only_style_added_sets_just_the_opacity
(also checks the layer's blend mode and Fill stay).

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-10-24 00:23:15 +02:00
David Mann 8c04b175ec Merge remote-tracking branch 'origin/main' into integration-batch-7
# Conflicts:
#	crates/automation/src/rpc.rs
#	crates/automation/src/server.rs
#	crates/gpu/src/health.rs
#	crates/text/src/tests.rs
#	crates/ui-egui/src/i18n/zh-hans.tsv
#	crates/ui-egui/src/panels.rs
#	docs/control-protocol.md
#	docs/ui-parity.md
2026-10-24 00:23:15 +02:00
Brandon ThomasandClaude Opus 5.5 29e4039c72 fix(contributors): drop a duplicate people.toml table (#9009)
Two PRs that landed together each added [people.ronibalthazar], and a
duplicate table makes the file invalid TOML (ui-egui's build script
warns that it can't parse it, so the About window's names are lost).

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-23 18:19:36 -04:00
fc0b40dae5 fix(atn): keep Photoshop's other transform interpolation modes on import (#8698)
The .atn importer kept only bicubic, bilinear and nearest from a
transform's Intr, so a recorded Bicubic Automatic, Sharper or Smoother
Transform Selection replayed with the command's default (bilinear) and
re-exported without its choice. The bicubic variants are now kept (the
transforms already render them as Bicubic), Automatic becomes Bicubic
Automatic and Preserve Details Bicubic, and both transform commands list
the variants (#8697).

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Co-authored-by: Brandon Thomas <echelon@gmail.com>
2026-10-23 17:55:54 -04:00
Marco ArnulfoandBrandon Thomas 2b83be6c81 Levels, Curves and the other adjustments: ⌥ plus the shortcut opens the dialog at its last settings (#8900) (#8969)
* Levels, Curves and the other adjustments: ⌥ plus the shortcut opens the dialog at its last settings (#8900)

* Image › Adjustments: ⌥ while choosing an item opens it at its last settings (#8900)

* Tests: the ⌥-click test picks the Image menu's Adjustments row, not the panel tab (#8900)

* Image › Adjustments: an item opened with ⌥ lands in Edit › Search's recents too (#8900)

---------

Co-authored-by: Brandon Thomas <echelon@gmail.com>
2026-10-23 17:55:18 -04:00
Roni BalthazarandBrandon Thomas 3a870ca3ce fix(ipp): preserve printer group metadata (#8828)
Co-authored-by: Brandon Thomas <echelon@gmail.com>
2026-10-23 17:55:05 -04:00
Batuhan GözegüandBrandon Thomas 9fe6be6d86 i18n(tr): fix mistranslations, inconsistencies and untranslated rows (#8871)
- Color Decontaminate: "Renkli" (colourful) -> "Renkleri Arındır"
- Warp split commands and "Cancel warp": "Çözgü" (textile warp) -> "Çarpıtma"
- Constraint hint: "Shift" was translated as "kaydırma"; keep the key name
- Highlights (tonal range) -> "Parlak Tonlar", as in the other rows
- Luminance -> "Lüminans" (was "parlaklık", which is Brightness)
- Indexed -> "İndekslenmiş", Trilinear -> "Üç Doğrusal"
- Levels In/Out Black/White: distinct "Giriş/Çıkış Siyahı/Beyazı"
- Layer Knocks Out Drop Shadow now says "Alt Gölge" like Drop Shadow
- Contact Sheet II -> "Kontak Sayfası II"
- Fix word order of the clone-source hints, "After (X)", "On Black/White"
- Translate "Preview size" and "Print preview image"
- Keep theme names Solarized and Breeze; Pro Light -> "Pro Açık"
- Add contributor entry for the author

Co-authored-by: Brandon Thomas <echelon@gmail.com>
2026-10-23 17:54:52 -04:00
Roni BalthazarandBrandon Thomas 3190b4ea4f fix(io): preserve unsupported custom-domain Color Lookup LUTs (#8879)
Co-authored-by: Brandon Thomas <echelon@gmail.com>
2026-10-23 17:54:39 -04:00
5750ed40f9 Fix pasted shape group bounds and effects across documents (#8992)
* fix(clipboard): render copied shapes in destination canvas space

When a copied shape group is placed beyond its source canvas, rebuild its vector caches against the destination canvas and pixel format. Previously a centered paste into a larger document could clip the caches to an empty source-sized frame, leaving no transform bounds and incorrect effects until a point edit refreshed each shape.

Restore missing shape caches and refresh source-clipped caches when canvas dimensions change. Cover Paste and Copy to Document, nested group contents, 8/16-bit destinations, rendered cache equality, transform bounds and movement Undo.

* fix(clipboard): measure shape placement before destination clipping

Restore missing source-space shape caches before measuring copy bounds, then adapt and translate for the destination. Re-render empty caches when shapes move back onto the canvas instead of shifting an empty raster.

Add focused integration coverage for 32 combinations of partial and full destination clipping, grouped shapes, missing caches, 8/16-bit output, centered and point placement, and Undo/Redo. Clippy and WASM checks pass.

---------

Co-authored-by: kotulsky <7805437+kotulsky@users.noreply.github.com>
Co-authored-by: Brandon Thomas <echelon@gmail.com>
2026-10-23 17:54:26 -04:00
Mathieu Rancourt 5004d96acb Lost window device: quitting goes through while recovery can't be saved (#8620)
After the window's GPU device is lost, nothing can be drawn (#6464). When
saving the unsaved work for recovery keeps failing (a full disk, an
unwritable or missing recovery folder), the app keeps retrying. Quitting
(the title bar's close button, File › Exit, the window manager's close)
then parked the quit behind the unsaved-changes prompt, which can never
show, so the blank window could not be closed. discard_ui::intercept now
lets a quit through once the device is lost, with a warning in the log.
2026-10-23 17:41:31 -04:00
Mathieu Rancourt 5de94a7e05 Slice tool: Style offers Fixed Ratio and Fixed Size (#8360) (#8638)
* Slice tool: Style offers Fixed Ratio and Fixed Size (#8360)

The Slice tool's options bar showed "Style: Normal" as plain text, so a
slice could only be drawn freely. It now has the marquee's Style dropdown
(Normal, Fixed Ratio, Fixed Size) with Width and Height, and a new slice
follows it while it is drawn and when it is made, through the marquee's
chrome_ui::marquee_end. The options are kept with the other tool options,
checked when loaded, and reset by Reset Tool.

* Slice tool: a Fixed Size slice has exactly its size, also a pixel or two

A press inside a pixel (10.25) made a 64 x 64 Fixed Size slice 65 x 65
pixels, since its far edge was rounded out. Fixed Size now starts on the
pixel the press is in. A Fixed Size of 1 or 2 pixels was taken for a click
that selects; Fixed Size makes its slice at any size.

* Slice tool: Fixed Ratio keeps its ratio and Fixed Size whole pixels

A fractional drag rounded each edge of a Fixed Ratio slice apart, so 2 : 1
could become 5 x 3. A fixed style now starts on the pixel the press is in
and takes both sides from one scale: a whole-number ratio comes in whole
multiples of it in its lowest terms, any other as close as whole pixels
allow. Fixed Size rounds its Width and Height to whole pixels, in the
field and when the slice is made, so 1.5 px no longer grows to 2.

* Slice tool: Fixed Ratio follows the drag, Fixed Size fields in whole px

Whole multiples of the ratio grew a 1366 : 768 slice in steps of 683 px,
made a 4 x 1 slice from a plain click under 4 : 1, and jumped a 39.8 px
16 : 9 drag to 48 x 27. The longer side now follows the drag to within a
pixel and the shorter side takes the ratio from it. Fixed Size Width and
Height show whole pixels, so the field matches the slice it makes.
2026-10-23 17:41:25 -04:00
JGF 35cb32be20 fix(export): report skipped layers in doc_export_layers (#8491) (#8713)
* fix(export): report skipped layers across automation surfaces

* test(export): use the MCP visible_only parameter spelling

* test(export): restore old adapter for hosted regression proof

* Revert "test(export): restore old adapter for hosted regression proof"

This reverts commit 397a5ffa2a.

* docs(export): qualify workspace-only response guarantees
2026-10-23 17:41:19 -04:00
FinnNkandClaude Opus 5.5 2cd1bd8664 fix(ui): cap text fields and oversized text events (#8722)
* fix(ui): cap text fields and oversized text events

Plain text fields took any amount of text, so pasting a huge string grew
the string (and its laid-out galley) without bound; #2258 fixed this for
numeric fields only. The shared text_field now holds at most 1024
characters unless its caller sets a limit, and the app cuts every text,
paste and IME event to 1 MiB of characters (at a character boundary)
before any widget sees it, so the dozens of hand-built TextEdits are
bounded too.

Part of #2150

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

* fix(ui): bound oversized existing text and synthetic text events

A text_field whose value is already past its limit lays out a bounded
copy and writes it back only on an edit; the text-event cap runs after
the control channel's synthetic events join the input.

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

* test(ui): raw_input_hook caps text the control channel queues

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

---------

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-23 17:41:14 -04:00
Michael HigginsandClaude Opus 5.5 5697c40361 PSD: write pattern transparency where Photoshop keeps it (#8054) (#8756)
* PSD: write pattern transparency where Photoshop keeps it (#8054)

A pattern defined over transparency (delete the Background, Define
Pattern, then a Pattern Fill layer from it) was saved with its alpha
plane in the virtual memory array list's user mask slot (24), and
Photoshop refused the PSD. Photoshop keeps pattern transparency in the
last (sheet mask) slot and leaves the user mask unwritten (psd-tools
multichannel-pattern-fill.psd, written by Photoshop CC 2019). The same
writer serves `Patt`/`Pat2`/`Pat3`, `.pat` and `.abr`.

Files saved by older builds still open (the reader takes the first
written array after the colour channels as transparency), and saving
them again rewrites their pattern block instead of keeping the raw
bytes, which `block_has_user_mask` detects.

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

* PSD: rewrite only legacy pattern blocks, keep unreadable ones (#8054)

When the document's patterns are unchanged, a pattern block with
transparency in the user mask array is now rewritten in place from its
own patterns, and every other block (pattern blocks that fail to decode
included) is kept verbatim. Previously a legacy block forced the
replace-all path, which dropped an unreadable pattern block that an
unchanged save used to keep. Blocks are decoded in order with one shared
budget, as from_global_blocks does.

Addresses CodeRabbit review on #8756.

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

---------

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-23 17:41:08 -04:00
FinnNkandClaude Opus 5.5 def4646c0e fix(codecs): WebP export keeps the resolution (#8775)
* fix(codecs): WebP export keeps the resolution

Every WebP export with metadata warned that the resolution would be
dropped, and lost it unless the image already had EXIF with resolution
tags. WebP has no resolution field of its own, so both encoders now
write the resolution into EXIF, adding the tags (or a minimal EXIF
block when the image has none), and WebP reports DPI support.

Part of #2399

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

* fix(codecs): report WebP DPI as dropped when the source EXIF can't be parsed

That block is written as it is rather than replaced, so its metadata
survives and the lost resolution is reported. The metadata test also
checks the rest of the EXIF (Orientation) survives.

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

* test(codecs): spell the WebP metadata test's bytes as escapes

The Orientation tag and the unparseable EXIF block were raw control
bytes, which also made git keep the file's CRLF line endings; both are
escapes now and the file is LF again.

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

* fix(codecs): no EXIF block just for WebP's default 72 ppi

A WebP without EXIF already opens at 72 ppi, so writing a minimal EXIF
for the default only turned every web export into the extended (VP8X)
container (and broke the automation test expecting a simple lossy file).
EXIF the image already has still gets its resolution tags updated.

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

* fix(codecs): WebP DPI kept by XMP isn't reported as dropped

When the image's EXIF can't be parsed but its XMP records the
resolution, the WebP keeps it through XMP, which readers check first.

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

---------

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-23 17:41:02 -04:00
FinnNkandClaude Opus 5.5 89d127d259 fix(curves): 32-bit Curves continue past 0..1 like Photoshop (#8782)
* fix(curves): 32-bit Curves continue past 0..1 like Photoshop

In a 32-bit RGB document an edited Curves clipped every sample to the
curve's table, so HDR values above 1 (and negative ones) were flattened.
Photoshop extends the curve beyond its end points along its end segments:
the straight curve [[0,0],[255,128]] maps -0.5, 0.5 and 2 to
-0.250980407, 0.250980407 and 1.003921628 (measured in Photoshop 27.11 on
the issue). The CPU compositor, Image > Adjustments and the GPU kernel
now do the same, with the end slopes measured over 16 table steps.
8/16-bit documents and unedited curves are unchanged.

Fixes #7157

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

* fix(curves): extend 32-bit curves in RGB documents only

The curve's space is not the document's: a grayscale, CMYK or Lab
32-bit document with an RGB-space curve keeps the table's ends. The
document mode now reaches the decision on the CPU (apply_in, the
compositor) and the GPU (adjustment_program).

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

---------

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-23 17:40:56 -04:00
MarekSpodBiedry b491de8e29 fix(layers): keep blend mode, Opacity, locks and Fill visible but greyed with no layer selected (#8880) (#8904) 2026-10-23 17:40:51 -04:00
Yongjip Kim cd2b4aca0c test(liquify): expand freeze-boundary regression coverage (#8082) 2026-10-23 17:40:45 -04:00
paulou-jpgandClaude Opus 5.5 0bd37f81af Puppet Warp pins go where they are dragged, without snapping (#8217)
A pin drag took the active tool's snapping. With the Move tool that was a
layer move's Smart Guides, which View > Snap doesn't turn off, so pins jumped
to other layers' edges with snapping off. Puppet Warp and Perspective Warp
drags no longer snap, as in Photoshop.

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-23 17:40:39 -04:00
Zuujis 4bda8d7388 test(panic_hunt): give the panic-counting self-test its own deadline (#8473)
contained_panics_count_and_graceful_errors_do_not used the 4 s hang
budget meant for fuzzed commands. With RUST_BACKTRACE=1 the panic hook
prints a backtrace, and on loaded Windows runners the first symbol
lookup took long enough (19.6 s) that run_watched returned None instead
of Some(false). The self-test now waits up to 120 s; the fuzz keeps 4 s.
2026-10-23 17:40:32 -04:00
NetterSilverandClaude Opus 5.5 03dd76022b Reject non-hex characters in engine hex color parser (#8541)
Found while testing crates/engine/src/commands.rs:283-291 (parse_hex)

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-23 17:40:27 -04:00
NetterSilverandClaude Haiku 5.5 c1158f92e8 Accept all-caps possessives in spell check (#8628)
Found while testing crates/text/src/spell.rs:91 (Dictionary::check)

Co-authored-by: Claude Haiku 5.5 <noreply@anthropic.com>
2026-10-23 17:40:22 -04:00
SimoneandLucenx9 4092fe79c9 fix(cms): reject malformed hex data in .look LUTs (#8657)
Co-authored-by: Lucenx9 <185146821+Lucenx9@users.noreply.github.com>
2026-10-23 17:40:16 -04:00
Gabriel Luís da Silva 7bc6fd8a4f Write the Levels and Curves composite channel as Chnl.Cmps in .atn export (#8686)
Photoshop records the composite entry of a Levels or Curves Set as
Chnl/Chnl/Cmps and silently leaves the adjustment unchanged when it is
Chnl/Ordn/Trgt, which is what the export wrote. Import already reads both.

Fixes #8543
2026-10-23 17:40:11 -04:00
FinnNkandClaude Opus 5.5 159fe710a2 feat(guides): New Guide Layout takes lengths with units (#8719)
* feat(guides): New Guide Layout takes lengths with units

view.newGuideLayout took pixels only. Its widths, gutters and margins now
also take text with a unit as typed in Photoshop's dialog ("1 cm",
"0.25in", "12 pt", "3pc", "10%"), at the document's resolution; percent
is of the target's width for columns, gutters and left/right margins and
of its height for rows and top/bottom margins, per artboard. The dialog's
Width and Height fields already pass such text through, so they accept
units now too.

Part of #4984

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

* test(guides): New Guide Layout percent resolves per selected artboard

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

* test(guides): assert the full guide lists after a selected-artboard layout

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

---------

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-23 17:40:06 -04:00
HEOJUNFOandClaude Opus 5.5 44db720b22 Black & White: keep 32-bit superwhites (#8537) (#8748)
The grey was clamped to 0..1 at every depth, so a 32-bit (2, 2, 2) came
out as (1, 1, 1). In 32-bit documents it now clamps to 0..16, as
Photoshop 27.11 does; 8 and 16-bit still clip at 1. CPU and GPU both.

Co-authored-by: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-10-23 17:40:00 -04:00
haiandzhanghaijun01 7264368a75 Layers: show effect row names through tl! (#8754)
The Layers panel drew Effect::label and "Effects" in English even when catalogs already translated them.

Co-authored-by: zhanghaijun01 <zhanghaijun01@bianfeng.com>
2026-10-23 17:39:55 -04:00
FinnNkandClaude Opus 5.5 c06dd9c27c fix(gpu): rebuild the GPU canvas after an out-of-memory error (#8774)
* fix(gpu): rebuild the GPU canvas after an out-of-memory error

An uncaptured wgpu error of any kind dropped the GPU canvas for the rest
of the session, so one recoverable Out of Memory on a working device
left every redraw on the CPU until Retry GPU. Dropping the canvas frees
its memory, so after an out-of-memory error a new GPU canvas is now
built at once on the same device, with a fresh health flag and no
dialog, up to three times a session; past that the CPU renderer and the
Retry GPU dialog stay as before. Validation errors and device losses
are unchanged.

Fixes #6564

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

* fix(gpu): only wgpu's out-of-memory error is retried

The fault is classified by wgpu's Error::OutOfMemory variant and recorded
as exactly "Out of Memory", so a validation error that mentions memory
keeps its fallback.

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

* fix(gpu): a later out-of-memory error replaces an earlier error

So the canvas still rebuilds itself when memory ran out after a
validation error in the same frame; a device loss still wins.

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

---------

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-23 17:39:50 -04:00
FinnNkandClaude Opus 5.5 0986582a61 fix(actions): a recorded Layers-panel drag keeps its drop target by name (#8781)
* fix(actions): a recorded Layers-panel drag keeps its drop target by name

Dragging a layer while recording stored layer.moveTo's target as a layer
id, which means nothing in the document the action later plays in, so
playback stopped with "no such layer". The target is now recorded by
name (with targetOccurrence among same-named layers), as layer.select
records its layer, and layer.moveTo accepts a target by name.

Fixes #7845

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

* fix(actions): count a drag's same-named targets without the moved layer

Steps are recorded after they run, so a target's occurrence among
same-named layers could shift when the moved layer shares its name. It
is now counted among the other layers only, at recording and at replay,
which the move doesn't reorder. Only a drag of the selected layer is
recorded by name; drags of several layers, or of an unselected one, keep
their ids.

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

* fix(actions): a moved group's contents don't count as drop targets

Same-named layers inside a moved group also changed order with the
move, so they are left out of the occurrence count with the group.

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

* fix(actions): leave every moved layer out of a named drop target

A move of several layers given a target by name no longer counts the
moved layers (or their contents) as candidates.

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

* fix(actions): a recorded ⌥-drag copy beside itself replays by name

A copy leaves its source in place, so the source stays a candidate for
the named target, at recording and at replay.

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

* fix(actions): a recorded copy's own contents don't count as drop targets

The step is recorded after the copy exists; the copy (and, for a group,
its same-named contents) is left out of the occurrence count, as at
replay it doesn't exist yet.

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

---------

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-23 17:39:45 -04:00
Gert Pellin - GPE 10a20743ea Soft Light: dark backdrops follow Photoshop's W3C curve (#8804) (#8817)
* fix(blend): Soft Light uses the W3C curve over dark backdrops (#8804)

Photoshop's Soft Light moves the backdrop towards D(cb) in the upper half,
and D(cb) is the cubic ((16·cb − 12)·cb + 4)·cb below a quarter, not
sqrt(cb). With sqrt the Soft Light cell of psd-tools rgb-blend-modes.psd
was up to 20 levels too light over the dark rainbow; with the cubic it is
within 4 of Photoshop's merged image.

Same change in compose.wgsl so the GPU compositor stays at CPU parity.
Krita's own soft_light (the sqrt curve) now opens as the approximation and
svg:soft-light / krita:soft_light_svg as the exact match, without warning.

Fixes #8804

* compose: the see-through Soft Light test expects the W3C curve over a dark backdrop (#8804)

The #6232 test computed Photoshop's Soft Light of yellow as sqrt(b) for red and
green. Its backdrop #04170F is below a quarter in both channels, where the W3C
curve (Photoshop's, measured in #8804) uses ((16b - 12)b + 4)b instead.
2026-10-23 17:39:40 -04:00
Khawar Zamman Wani 20d61a90fe fix(io): read XCF linked-layer payloads by field size (#8865) 2026-10-23 17:39:35 -04:00
jturnbachandJohn Turnbach 316a07c8c9 fix(text): lay out a variable font's named instance from its PostScri… (#8909)
* fix(text): lay out a variable font's named instance from its PostScript name

A PSD type style set in a named instance of a variable font, such as
Bahnschrift Light Condensed, keeps the instance only as its PostScript
name ("Bahnschrift-LightCondensed"); its variations are empty. When
PhotoCraft re-laid the layer out, FontDb::select_named_face looked for a
face with that PostScript name, or one whose style and weight matched. A
variable family is a single face, so neither matched, and the text was
laid out at the font's default instance: about 35% wider than the
instance Photoshop drew with Bahnschrift.

select_named_face now falls back to the family's variable faces when the
PostScript name matches no face. It reads each face's fvar named
instances and compares the name with the instance's own PostScript name,
or, when the instance has none, with the name Adobe Technical Note #5902
derives (variations prefix or family name, a hyphen, then the subfamily
without spaces). Names compare without punctuation or case. On a match,
that face is selected and the instance's axis values are added to the
style's variations, keeping any axis the style already sets. Static
faces still match exactly as before, and PSD import is unchanged.

Tests:
- font_names_tests::postscript_name_of_a_named_instance_selects_its_axis_values
  rebuilds the bundled Inter at test time as a variable font (fvar with
  wght/wdth axes and two named instances, one with its own PostScript
  name and one derived) and checks the axis values each name selects, an
  unknown name, and a style's own axis value. It needs no installed font
  and no font file in the repository.
- font_names_tests::exact_static_face_gets_no_instance_axes checks that
  an exact static match gets no axis values.
- tests::postscript_name_of_a_variable_instance_lays_out_at_that_instance
  lays out "Hamburgefonstiv 0123" with Bahnschrift (Windows) or Ubuntu
  Sans (Ubuntu) when installed and checks that the instance's PostScript
  name gives the same width as its axis values. With Ubuntu Sans before
  the fix: UbuntuSans-CondensedLight 980.3 px (= the default instance),
  its axes 766.4 px; after the fix both are 766.4 px.

A style with only a weight (no PostScript name) is still not turned into
a wght axis value on a variable font; the issue reports that too, but its
fix and test are about the PostScript name, so that is left for a
separate change.

Fixes #8797

* fix(text): try a variable font's named instance before the style fallback

Review pointed out that select_named_face ran the style-and-weight
fallback before the named-instance lookup. A style that carries a named
instance's PostScript name and also a populated font_style matching the
variable face itself ("Regular", 400) selected that face at its default
instance, and the instance's axis values were never added.

Faces are now tried in order: exact PostScript name, named instance,
then style, weight and italic.

Review also noted that the layout-width test only runs when Bahnschrift
or Ubuntu Sans is installed. The synthesized variable font in
font_names_tests now carries an HVAR table that narrows every advance on
wdth, so layout width shows which instance was used. Two tests run
everywhere:

- named_instance_wins_over_a_matching_font_style
- postscript_name_of_a_named_instance_lays_out_at_that_instance

Both fail before this change. The installed-font test is kept as a check
against a real font.

---------

Co-authored-by: John Turnbach <jturnbach@users.noreply.github.com>
2026-10-23 17:39:30 -04:00
skylar taylor-barrickandskylartaylor 2d9640bb0b fix(ui): restore extras when showing the grid (#8962)
Co-authored-by: skylartaylor <26588250+skylartaylor@users.noreply.github.com>
2026-10-23 17:39:24 -04:00
Zuujis 151eae66be fix(app): no unused screen picker code on FreeBSD (#8469)
FreeBSD (and every other OS without a capture backend) compiles the
screen colour picker's stub capture, which never reads monitor placements
or builds images. The build warned about the unused ScreenImage import,
the never-read Placement fields, find_monitor and count. Compile those
only where a capture backend or the tests use them.

Seen in the FreeBSD aarch64 CI build; checked with `cargo check -p
photocraft --all-targets` on FreeBSD 15.1 aarch64 (no warnings) and macOS.
2026-10-23 17:39:19 -04:00
NetterSilverandClaude Haiku 5.5 45833ece7c Fix EngineData format_real emitting a bare trailing dot (#8486)
Found while testing crates/text/src/engine_data.rs:331 (format_real)

Co-authored-by: Claude Haiku 5.5 <noreply@anthropic.com>
2026-10-23 17:39:13 -04:00
ZFzuofeng 13e4d8ceed fix(engine): select files correctly in Windows Explorer (#8518) 2026-10-23 17:39:08 -04:00
41599c591c Crop default mode: moving the image under the box snaps (#8580) (#8622)
* Crop default mode: moving the image under the box snaps (#8580)

In the default (non-Classic) crop mode a drag inside the box moves the
image under it, but snapping was switched off for that drag, so content
couldn't be centred in a larger box without leaving for the Move tool.

The drag now snaps like a Classic Mode frame move: the frame moves by the
image's travel the other way over the document, its edges and centre snap
to the document, layers, guides and grid (smart guides when those are
empty), and the pointer is corrected in the press camera, as crop_ui reads
the drag. A turned box still doesn't snap.

Fixes #8580

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

* Crop image move: smart guides snap when no Snap To target is in reach (#8622 review)

The document's bounds keep the regular targets non-empty, so the smart
guides were never tried: with View › Snap To › Layers off, the box never
snapped to a layer while the image moved under it.

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

---------

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Co-authored-by: Khawar Zamman Wani <wanikhawar@Khawars-MacBook-Air.local>
2026-10-23 17:39:02 -04:00
SimoneandLucenx9 5948a3fb2a fix(geom): reject non-finite affine inverses (#8651)
NaN determinants bypassed the singularity check, while determinant and translation overflow could return Some with a bogus or non-finite inverse. Reject non-finite determinants and computed inverse coefficients so callers can take their existing failure paths.

Cover all six coefficients with NaN and both infinities, determinant overflow, translation overflow with a finite determinant, and valid extreme translations.

Co-authored-by: Lucenx9 <185146821+Lucenx9@users.noreply.github.com>
2026-10-23 17:38:57 -04:00
FinnNkandClaude Opus 5.5 51e45fd394 fix(engine): refuse partial Color Balance and Hue/Saturation ATN exports (#8701)
* fix(engine): refuse partial Color Balance and Hue/Saturation ATN exports

A `layer.setAdjustment` step changes only the settings it names, but the
Color Balance and Hue/Saturation descriptors written to the ATN carry
every setting and filled absent ones with defaults, so a partial edit
replayed as one that also reset the rest. Strict export now refuses such
a step (with skipUnsupported it is listed in Skipped); complete ones
export and round-trip as before.

Fixes #8531

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

* fix(engine): refuse Hue/Saturation ranges missing their own settings

A colour range written without its own hue, saturation and lightness
would replay with the missing ones reset to 0, so strict export refuses
it like a partial master setting.

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

* fix(engine): refuse wrong-shaped Color Balance and Hue/Saturation settings

Strict export now checks the triplets, numbers and flags it needs are of
the shape the descriptor writer reads, since it would write anything else
as 0.

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

* fix(engine): refuse malformed Hue/Saturation range bounds

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

* test(engine): cover malformed and valid Hue/Saturation range bounds

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

* fix(engine): refuse fractional Hue/Saturation range bounds

The descriptor stores them as integers, so a fractional bound would be
rounded on export.

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

---------

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-23 17:38:48 -04:00
FinnNkandClaude Opus 5.5 b8ee65e030 test(text): per-colour passes match full-size passes (#8716)
The rasteriser made one coverage pass per distinct colour over the whole
text's ink rect (nine with faux bold), so text with many coloured runs
cost colours x area: 400 colours of faux-bold 48 pt text took 13.0 s.
Each colour's pass now covers only that colour's glyph bounds (all bold
offsets and decorations included) and composites into that part of the
accumulator: 163 ms for the same text. A test compares the visible
(premultiplied) pixels with full-size passes for plain, faux bold,
underlined, rotated and warped text in RGB and CMYK.

Part of #8602

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-23 17:38:42 -04:00
FinnNkandClaude Opus 5.5 272554f971 fix(type): type.edit with new text keeps the styles of unchanged text (#8766)
* fix(type): type.edit with new text keeps the styles of unchanged text

`type.edit {"text"}` replaced the whole string, so every character took
the first run's style and other styled ranges were lost. It now replaces
only the span that differs (after the common prefix and suffix), so the
unchanged text keeps its character and paragraph styles and an added
character takes the style beside it, as the params doc promised.

Fixes #8489

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

* fix(type): text inserted at a paragraph's start joins that paragraph

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

---------

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-23 17:38:36 -04:00
Przemyslaw Majdak 612c4a3c7f Codecs: warn when DPX or Cineon saves drop bits (#8793)
DPX at 10 or 12 bits and Cineon (always 10) are planned as U16, so
DepthReduced, which compares sample types, never fired: a 16-bit document
saved at 10 bits lost six bits per sample silently, and a float one was
told it would be reduced to 16-bit.

FidelityWarning::BitsReduced { from, bits } names the stored depth. Float
samples always get it; U16 ones only when some sample is not already a
code value at that depth, so a frame opened from a 10- or 12-bit file
saves back without a warning. U8 widens losslessly and stays silent.
2026-10-23 17:38:29 -04:00
Yongjip Kim 78013e2256 perf(mcp): move command list rows instead of cloning (#8844) 2026-10-23 17:38:23 -04:00
Yongjip Kim 08a606c7d1 perf(liquify): reuse interpolation taps and bound render buffers (#8855)
* perf(liquify): reuse cubic field taps across tile axes

* perf(liquify): bound completed render tiles
2026-10-23 17:38:18 -04:00
Ignat Ai 0c124bf388 i18n(zh-hans): translate status messages and preview labels (#8872) 2026-10-23 17:38:13 -04:00
Dylan M. Taylor cfd894aad3 security(paint): cap spaced steps and procedural primitives (#8957)
* security(paint): cap distance-spaced steps per segment and procedural tip primitives

* test(paint): count steps through a Cell so the walker closure can be shared

* paint: keep the spacing carry-over when a segment ends at the step cap; lower the tip work budget
2026-10-23 17:38:07 -04:00
Kushagra Sharma 7e3594b822 fix(ui-egui): a click on the bottom layer's Smart Filters selects them again (#8764) (#8974)
* fix(ui-egui): a click on the bottom layer's Smart Filters selects them again (#8764)

The empty space below the last Layers panel row deselects every layer on a
click (#8100). It started under the last layer's own row, so it covered that
layer's effect and Smart Filter rows and, registered after them, took their
clicks: on the bottom smart object (a new document's Layer 0), clicking a
filter deselected everything instead of selecting it.

The click area now starts under everything drawn for the last layer. The
drop zone below it (#1721) is unchanged.

* Contributors: credit drunkeninja42 as Kushagra Sharma
2026-10-23 17:38:02 -04:00
kotulskyandkotulsky 8d1926433f Reuse filled-shape effect maps while moving styled groups (#8993)
* perf(gpu): reuse filled-shape effect maps during translation

Hash filled-shape outlines in local coordinates so whole-pixel movement can use the existing effect-map translation reuse path. Keep its region-size and exact coverage checks, and invalidate the key when the outline deforms.

Add a shape-key regression and a synchronized styled-shape group benchmark with fresh-render parity verification. On RTX 4070 Laptop, the 1600x1200 fixture improves from 61.336 to 33.120 ms median; a 24 MP full-canvas fixture improves from 223.018 to 184.289 ms. Warm frames rebuild zero effect programs, and the quick fixture matches fresh rendering exactly.

* fix(gpu): stabilize fractional shape effect fingerprints

Quantize only origin-normalized fingerprint coordinates to a 1/65536 pixel grid and canonicalize signed zero. Preserve the render path and exact coverage guard.

Cover fractional anchors and handles, positive and negative integer translations and handle deformation. Exercise fractional origins in the benchmark and document its entry points. All 40 GPU tests, Clippy and WASM checks pass; reused output matches a fresh compositor exactly.

---------

Co-authored-by: kotulsky <7805437+kotulsky@users.noreply.github.com>
2026-10-23 17:37:56 -04:00
David Mann 27caf2823b Merge remote-tracking branch 'origin/main' into integration-batch-7
# Conflicts:
#	crates/engine/Cargo.toml
2026-10-23 23:13:46 +02:00