Commit Graph
3466 Commits
Author SHA1 Message Date
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
David MannandClaude Opus 5.5 7d166edb2a Lens Flare: don't darken 32-bit pixels above 1.0 (#5785) (#5786)
Screen as 1 - (1 - c)(1 - a) pulls a 32-bit value above white down
towards 1.0 (2.0 with a = 0.5 became 1.5, the core became 1.0). Add
a·max(1 - c, 0) instead: the same result for c <= 1, unchanged above.

Co-authored-by: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-10-18 03:06:45 -04:00
David MannandClaude Opus 5.5 363d385114 Trap: refuse locked layers (#5623) (#5624)
Image > Trap wrote the active layer's pixels without checking locks, so a
layer under Lock All or the pixel lock (its own or its group's) was trapped
anyway. Check check_pixels_unlocked before writing, like the fill commands.

Co-authored-by: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-10-18 03:06:40 -04:00
Korg1ch 36d6b669d4 test(engine): Content-Aware Fill with an empty sampling area is refused and changes nothing (#6532) 2026-10-18 09:46:18 +03:00
Korg1ch 0f12ac151f fix(engine): refuse a Content-Aware Fill with nothing to sample instead of filling black (#6532) 2026-10-18 09:19:41 +03:00
skylar taylor-barrick 3794e6fbbe Merge pull request #7118 from nicolasfalesy/perf/stack-mode-moments
perf(stack): one loop per statistic, up to 2.2× faster, bit for bit the same
2026-10-17 23:28:16 -06:00
Nicolas FalesyandClaude Opus 5.5 9ff27ec204 perf(stack): one loop per statistic, 1.3–2.2× faster on one thread
#6462 left Mean, Variance, Standard Deviation, Skewness and Kurtosis 5–15%
slower than the old serial loop on one thread. The cause: `stat` matched on
the mode three times per pixel inside one shared loop. `combine` now picks
the statistic once and hands `combine_with` a closure with a constant mode,
so each statistic gets its own loop with the match folded away. The sample
buffers are sized to the layer count and filled by index instead of `push`,
which re-checked the capacity per sample.

Every output is bit for bit the same (121 hashes match main).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01FUAqfMqVHdoeDa2y68KQQw
2026-10-18 06:54:53 +02:00
skylar taylor-barrick 2d0f3b61cf Merge pull request #2376 from Unemars/content-aware-workspace
Content-Aware Fill workspace, as Photoshop's
2026-10-17 22:50:54 -06:00
skylar taylor-barrick 8c30a41e06 Merge pull request #7126 from dotCooCoo/fix/stack-statistics-same-bits-on-windows
Stack Mode: Kurtosis and Skewness give the same bits on Windows and Linux
2026-10-17 21:08:12 -06:00
Michael HigginsandClaude Opus 5.5 4d9f163c61 Linux: Reveal never hands xdg-open a file (#7166) (#7168)
Reveal in Finder on Linux opens the linked file's parent with xdg-open,
which launches whatever it is given. A link stored as `tool.jar/a.png`
names a file as its parent, so Reveal launched that file. Reveal now
refuses a parent that is not a folder.

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-17 20:53:59 -06:00
Michael Higgins 72b4a8c0bf Merge branch 'storytold:main' into fix/5884-delay-load-windows-data-pdf 2026-10-17 22:27:05 -04:00
skylar taylor-barrick ceb85009c7 perf(canvas): a Properties-panel adjustment drag refreshes only the view (#6839) (#7155)
`damage_since` had a case for the Image > Adjustments dialog but none for the
Properties panel's `live_adjust` preview, so every parameter change returned no
damage and recomposited the whole document — `gpu-full` at 24.0 MP.

That is what made #5778 a latency regression at 100 % zoom: it correctly stops
CPU-compositing a 1.5 MP proxy, but the full-size GPU composite replacing it is
per-pixel work and 24 MP of it costs more. The Properties path now gets what the
dialog had: a session-tagged key, the layer's area as damage, and of that only
what the view shows, catching up as the view moves.

Intel UHD P630, 24 MP document, 12 changes, medians:

  v0.9.0    70.3 ms   1.5 MP CPU proxy
  main     203.1 ms   gpu-full, 24.0 MP
  this      47.8 ms   gpu-rect, 1.10 MP

Byte-identical to the full-refresh base on all 12 changes and across a pan.
25 % zoom and PHOTOCRAFT_CPU_COMPOSE are unchanged.

`live_region` does not narrow by the layer's mask: a reveal-all mask keeps its
coverage in the default pixel and reports empty content bounds, which emptied
the damage and froze the preview. Every adjustment layer has one.
2026-10-17 20:21:51 -06:00
Brandon ThomasandClaude Opus 5.5 8407d9ccb1 Release 0.10.0 (#7192)
* Release 0.10.0

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

* Cargo.lock: photocraft-ipp at 0.10.0

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

---------

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
v0.10.0
2026-10-17 21:28:00 -04:00
David MannandClaude Opus 5.5 b450baa6eb Image: Trim and merged Duplicate report an error on documents over 2^30 px (#7223)
`compose::flatten` returns an empty buffer for canvases over MAX_RENDER_PIXELS.
Trim (Top Left) indexed `px[0]` of it and Duplicate › Merged Layers Only fed it
to `write_region`'s length assert: both panicked; Trim's other modes reported
"nothing to trim to". Both now return a "too large to flatten" error first.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-18 03:02:31 +02:00
dotCooCoo 0bdea678e7 Merge main into fix/abr-repeated-brush-names 2026-10-17 18:02:25 -07:00
carterwsmith 61db2b2bba Group all selected layers from the Layers panel footer 2026-10-17 18:00:15 -07:00
UnemarsandClaude Fable 5.1 66c0e4ce13 Content-Aware Fill workspace: review follow-ups (#2376)
Testing on a 6000 × 4000 document found the proxy's sampling state computed
per pixel against every stroke segment: each stroke got slower, and an undo
tested 1.7 M pixels × 3000 segments on the UI thread, blocking it for over a
minute. The proxy is now painted as the engine paints the full-resolution
mask: the option's area and the fill area are sampled once at the proxy's
pixel centres (set_base), and each stroke is painted over them in order,
visiting only its own pixels (paint, restate_all), so a dab costs its segment
and a rebuild costs the strokes' areas.

Also from the same review:
- `edit.contentAwareFill {"ui": …}` with the workspace closed is an error
  instead of a fill with the defaults (an agent polling the state after an
  Esc added a fill per poll).
- The view sends one Down per press: the press is the stroke's first point
  and the frames before egui's drag threshold are its next ones, instead of
  drag_started() beginning the stroke over (the lasso lost its first points
  the same way; the Zoom tool zoomed on every frame the button was held).
- The preview texture is box-averaged to the device's max_texture_side when
  the plan's rectangle exceeds it (a 17000 px wide plan gave a validation
  error every frame).
- A preview that fails counts as shown for that state (Preview::finish), so
  it is reported once instead of restarting on every repaint.
- The view shows the document's visible layers composited (an Apply to a new
  layer shows); the engine's limits on strokes, lasso edits and points hold
  at the brush with a message instead of failing Apply; the footer is
  Photoshop's reset, OK, Apply, Cancel on every platform; a leftover
  `let _ = i;` is gone.

Tests: strokes painted once and equal to the engine's sampling area; `ui`
with the workspace closed; the first points of a stroke and a lasso through
egui_kittest; the preview texture at a 2048 limit; a failing preview
reported once; the view after Apply to a new layer; the limits at the brush.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2026-10-18 02:20:20 +02:00
Higgs32584andClaude Opus 5.5 204c76ce36 fix(psd): an edited type layer keeps its Txt2 object instead of a bare stub (#6968)
When a type layer's text changed, build_txt2 replaced the layer's object in
Photoshop's own Txt2 block with a bare `<< /0 << /0 text /6 runs >> >>`:
no paragraph runs (/5), no view (/1, which names the object's frame) and none
of the style keys. Photoshop 27.10 quits while opening such a file
(layer_effects.psd after type.edit), as it refused stripped objects in #2374.

An edited object now starts from the file's object at its slot: the text is
replaced, the paragraph runs are remade one per paragraph of the new text,
and every other run array of the model (style runs, ...) is cut or extended
to the new length before the auto-kern modes are applied. The view and other
keys are kept; only the old text's glyph pen positions (/21) are dropped.
An object whose run arrays aren't shaped as Photoshop writes them still falls
back to a fresh one.

Test: text edited_text_keeps_the_files_txt2_object.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-17 20:03:18 -04:00