Commit Graph
8755 Commits
Author SHA1 Message Date
Dylan M. Taylor 6e8d3fd45c xtask: only follow https redirects when fetching PngSuite 2026-11-04 04:20:53 +00:00
Dylan M. Taylor 37a01ca874 format: bound blob inflation by the load's remaining budget; browser budgets (#10984)
LoadFetch::blob_bounded decompressed a new blob up to max_blob_bytes
and only then charged it against max_total_bytes. Successful blobs are
retained, so several distinct, highly compressible blobs each allocated
a full max_blob_bytes before the total was checked. A new blob may now
inflate only to what the total budget has left.

The browser build loaded native documents with the desktop defaults
(1 GiB a blob, 16 GiB in all), far beyond a wasm32 instance's 4 GiB of
linear memory that the editor also uses. On wasm32 the defaults are now
512 MiB a blob and 1 GiB in all.
2026-11-03 20:55:42 -05:00
Dylan M. Taylor 3225307e57 codecs: admit animated GIF screens, sparse RLE BMPs and icon PNG profiles (#10979)
* codecs: admit animated GIF screens, sparse RLE BMPs and icon PNG profiles

- Animated GIF decoding (decode_gif_frames) skipped the logical-screen
  plausibility check the first-frame decode applies, although the
  backend initializes the whole screen before reading a frame. Both now
  share gif_plausible().
- RLE BMPs skipped the file-size plausibility check entirely, so a
  header-only file could make the decoder allocate and clear a
  multi-gigabyte canvas. Like a GIF's screen, 256 MiB of canvas is now
  free of data and the rest must be backed by the file.
- The image crate's ICO decoder opens an embedded PNG with no limits,
  and png inflates an iCCP profile while the decoder is built. Icon
  PNG profiles are now inflated into a sink first and refused past
  255 x 65519 bytes (or max_alloc), with a 64 MiB total across entries.

* address review: visit each ICO profile chunk offset once
2026-11-03 20:54:58 -05:00
Dylan M. Taylor d82e166494 codecs: charge dropped PNG text against the metadata budget (#10980)
* codecs: charge dropped PNG text against the metadata budget

read_text_metadata debited the shared text allowance only for text it
kept. A compressed iTXt chunk that inflated up to the whole remaining
allowance and then failed UTF-8 validation (or hit a corrupt or
truncated stream) was dropped without being charged, so every further
such chunk could repeat the same inflation. bounded_text now reports
the bytes it produced and the caller charges them whether or not the
text is kept.

* codecs: charge the output a failing text decompress wrote

* address review: charge Latin-1 zTXt error batch at UTF-8 length
2026-11-03 20:51:19 -05:00
Dylan M. Taylor 20f92ee405 codecs: leave Extended XMP fragments out of zune-jpeg's header parse (#10981)
* codecs: leave Extended XMP fragments out of zune-jpeg's header parse

zune-jpeg 0.5 keeps every Extended XMP APP1 fragment until its GUID's
set is complete, and re-sorts and re-checks the whole pending list
after every marker it parses. A file with many small, consistent but
incomplete fragments (or a few followed by many other markers) costs
quadratic time before any pixel is decoded.

Nothing in PhotoCraft reads zune's reassembled Extended XMP (metadata
comes from scan_metadata over the original bytes), so the decoder now
hands zune a copy without those segments when a file has any. Files
without Extended XMP are passed through unchanged.

* codecs: reserve only the bytes kept after dropping Extended XMP segments
2026-11-03 20:50:58 -05:00
Dylan M. Taylor 4c33a7b8f2 raw: keep at most 64 GainMap opcodes from one list (#11023)
Development applies every gain map to every pixel it covers, so a list
of thousands of maps multiplied the work of an otherwise admissible
image. Cap them like MapPolynomial; the rest are named as not applied.
2026-11-03 20:50:37 -05:00
Dylan M. Taylor 87f0bb3942 raw: refuse JPEG XL tiles with compressed metadata boxes or splines (#11039)
jxl-oxide inflates Brotli-compressed auxiliary boxes (Exif, XMP) into
storage its allocation tracker doesn't see while it reads a container,
and samples spline geometry into an untracked vector with no bound on
the sample count. A raw tile needs neither: refuse a container holding a
brob box before reading it, and a codestream whose frames draw splines
before rendering.
2026-11-03 20:37:49 -05:00
Dylan M. Taylor c004e38e75 raw: decode a tile range once when several tiles share it (#11041)
read_plane decoded every segment separately, even when a file pointed
every tile at the same bytes. Each lossless JPEG decode rebuilds its
Huffman tables and each result is retained until assembly, so one small
range repeated across many tiles multiplied header work and memory by
the tile count.

Decode each distinct (offset, length, geometry) once and scatter the
shared samples into every tile that names it.
2026-11-03 20:37:28 -05:00
Dylan M. Taylor 06bedca787 raw: refuse lossy DNG tiles that carry Extended XMP (#11042)
* raw: refuse lossy DNG tiles that carry Extended XMP

zune-jpeg keeps every Extended XMP fragment until its set is complete
and re-sorts and re-checks all pending fragments after each marker, so a
lossy (compression 34892) tile of many small incomplete fragments costs
quadratic time before a pixel is decoded. A raw tile has no metadata:
refuse one with an Extended XMP APP1 segment before parsing it.

* address review: bound Extended XMP identifier to APP1 payload
2026-11-03 20:37:07 -05:00
Dylan M. Taylor 4367131008 raw: check retone's pixel count with checked multiplication (#11043)
retone compared the buffer length with an unchecked w * h. In a release
build a product that wraps passed the check for a tiny buffer, and the
banded passes then ran over the original, huge height.
2026-11-03 20:31:35 -05:00
Dylan M. Taylor f960a65eb3 ipp: resume the reply's header search where the last read stopped (#11049)
read_response searched every byte read so far for the blank line after
each read, so a server trickling a 16 MiB reply in tiny fragments made
the client rescan it once per fragment. Resume the search three bytes
before the previous end.
2026-11-03 20:31:14 -05:00
Dylan M. Taylor e47dee26d0 fix(gpu): saturate effect band edges at the coordinate ceiling (#11057) 2026-11-03 20:30:53 -05:00
Dylan M. Taylor d5bd171810 text: merge a run of union components once when fitting area type (#11065)
AreaShape::spans folded each Combine/Join component into the growing
interval list with a full merge, so a shape of many disjoint components
cost components x intervals per row, repeated for every row the layout
tries. Gather a run of union components' intervals and merge them once
(sort and sweep) when the run ends; other operators fold as before.
2026-11-03 20:23:51 -05:00
Dylan M. Taylor 282be0927e text: group glyphs by colour once instead of rescanning per colour (#11069)
The rasterizer found each glyph's colour with a linear search of the
colours seen so far, then walked every glyph and decoration twice per
colour (bounds, then coverage) to draw that colour's items. A text layer
with a colour per letter cost colours x glyphs before drawing anything.

Build each colour's glyph and decoration lists in one pass, keyed by the
colour's bits, and draw each pass from its own lists.
2026-11-03 20:23:29 -05:00
David Mann 205db62a29 Merge pull request #11157 from davidmann4/integration-batch-14
Integration batch 14: 9 PRs planned
2026-11-04 01:42:35 +01:00
Dylan M. Taylor 3fd5304dfe text: find paragraphs and trailing blanks without rescanning (#11045)
Layout looked up each paragraph's style with a reverse scan of every
paragraph start, tab placement re-checked the whole rest of the line for
every cluster, and PSD import found each character run's paragraph by
summing paragraph lengths from the first one. Each was quadratic in
counts a file controls.

Use the existing binary search over run starts for paragraphs, one
reverse scan for the line's last inked slot, and precomputed paragraph
ends with a binary search on import.
2026-11-03 19:35:50 -05:00
David Mann dff83780d0 Merge main into integration-batch-14 2026-11-04 00:59:38 +01:00
David Mann 545d447c10 Merge PR #10998: Actions: map recorded Scale Effects and Create Layers 2026-11-04 00:51:01 +01:00
David MannandClaude Opus 5.5 c430647bb0 Batch: drop the Info panel's crop Ratio line from #10453
The reference application's Info panel shows the crop frame's W and H but no
ratio line, so it stays out for now; W and H keep following the frame.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-04 00:18:12 +01:00
David MannandClaude Opus 5.5 75a6828f53 Batch: picking the Crop tool again keeps a committed crop's frame hidden
Measured in the reference application: after ↵ commits a crop, the Crop
tool shows no frame; pressing C again, or another tool and back, keeps it
hidden, and only a press on the canvas brings the frame back. A crop
committed by leaving the tool is different: back on the Crop tool the idle
frame shows at once. #10642 brought the frame back on re-picking; picking
the tool no longer ends the hidden state, and a commit on leaving the tool
does not start it. Tests follow.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-04 00:13:42 +01:00
David MannandClaude Opus 5.5 37c84c18e5 Batch: Ctrl-held Straighten test expects transparent corners with Delete Cropped Pixels off
Main's #10905 (#10867) turns the Background into Layer 0 when a crop keeps
the cropped pixels, so the corners a turn uncovers stay transparent instead
of taking the background colour. The #10950 test now expects that for its
Delete Cropped Pixels off cases; with it on the corners are still the
background colour.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-03 23:39:48 +01:00
David MannandClaude Opus 5.5 c466cac619 Batch: H/P and Info panel tests pick the Crop tool again after a commit
#10642 hides the frame after a committed crop until the next press or
re-picking the tool. The #9855 and #10453 tests expected the default frame
right after the commit; they now check that it is hidden, pick the tool
again and then test the default frame as before.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-03 22:56:21 +01:00
David Mann e06d1cbdc1 Merge PR #11067: Layers panel: filter by layer effect (#10880) 2026-11-03 22:55:47 +01:00
David Mann 3c9ae63951 Merge PR #10955: Actions: map recorded Shear curves 2026-11-03 22:55:46 +01:00
David Mann d8ea8061ee Merge PR #11109: Crop: turning the image keeps the frame inside it (#10869) 2026-11-03 22:55:46 +01:00
Psychotoxical 66b9f2f68b Merge remote-tracking branch 'origin/main' into feat/layer-filter-effect
# Conflicts:
#	crates/automation/src/server.rs
#	crates/ui-egui/src/panels/layer_filter_ui.rs
#	crates/ui-egui/src/state.rs
#	docs/control-protocol.md
2026-11-03 21:55:06 +01:00
ljkx c15a281045 Merge remote-tracking branch 'origin/main' into crop-turn-keeps-frame-inside 2026-11-03 21:49:55 +01:00
David Mann 81bd97376f Merge pull request #10387 from davidmann4/fix/flaky-split-route
CMYK: adjustment layers in the special eight keep the Fill rule on the ink path (#10386)
2026-11-03 21:39:21 +01:00
yoriswind 27f86af55b Merge main into recorded layer style mappings 2026-11-03 12:12:41 -08:00
yoriswind 7763e2578f Merge main into recorded Shear mapping 2026-11-03 12:11:27 -08:00
David Mann 576b705a07 Merge PR #10850: fix(algo): Filter Gallery reach no longer overflows on out-of-range stored effect values 2026-11-03 20:47:27 +01:00
David Mann 0d4e565062 Merge PR #9573: fix(ui): Free Transform keeps plain layers over the moving one under a blend or adjustment layer (#9387) 2026-11-03 20:47:24 +01:00
David Mann f199789b00 Merge PR #10453: Crop: the Info panel shows the crop frame's W and H (#3145) 2026-11-03 20:47:20 +01:00
David Mann b91a75185a Merge PR #9855: Crop: H and P pick the Hand and Pen tools over the default frame (#9731) 2026-11-03 20:47:16 +01:00
David Mann 8e29e9d55d Merge PR #11019: Crop: give each overlay its own glyph in the menus and on the options-bar button (#10889) 2026-11-03 20:47:13 +01:00
David Mann 940f3b4715 Merge PR #10565: Free Transform: lift a pixel's coverage up to all of it, as Move does 2026-11-03 20:47:09 +01:00
David Mann e54f07a4dd Merge PR #10642: Crop: no frame around the cropped image until the next press 2026-11-03 20:47:06 +01:00
David Mann 0ec0255533 Merge PR #9842: Fix Crop Clear retaining custom proportions 2026-11-03 20:47:03 +01:00
David Mann 5c3f2e1392 Merge PR #10950: Crop: a Ctrl-held Straighten line keeps the frame size (#10870) 2026-11-03 20:46:59 +01:00
David Mann d93e65d001 Merge pull request #10952 from davidmann4/integration-batch-13
Integration batch 13: 13 parity epic PRs on main
2026-11-03 20:37:25 +01:00
David MannandClaude Opus 5.5 72c2585177 Merge origin/main (287352bf92) into integration-batch-13
Conflict in crates/engine/src/jobs.rs: main's save commit point (#5333,
OPEN/COMMITTED/CANCELLED phase) met the batch's cancel state
(CANCEL_REQUESTED/WORK_FINISHED). Both now share one atomic with a
COMMITTED bit; commit vs cancel and the batch's late-cancel guard each
stay one compare-and-swap. A Batch that refuses a repeated or late cancel
reports CancelRefused::Finishing ("finishing") in jobs.cancel's
notCancelled list, still {"cancelled": 0}. control-protocol.md documents both.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-03 19:35:44 +01:00
FredApps 287352bf92 Merge pull request #5333 from vistaero/codex/streamed-psb-save
Integrate streamed PSB saves with the shared save service
2026-11-03 18:41:16 +01:00
yoriswind 0edb29a18c Merge upstream main into Shear action mapping 2026-11-03 09:32:21 -08:00
skylar taylor-barrick b323c1ba27 Merge pull request #10731 from RickiNano/fix/10665-pasteboard-pick-offcanvas
Don't auto-select off-canvas layers from the pasteboard (#10665)
2026-11-03 18:17:45 +01:00
David Mann 3d36330d94 Merge remote-tracking branch 'origin/main' into HEAD
# Conflicts:
#	docs/scorecard.md
2026-11-03 18:08:29 +01:00
FredApps 996a52cb8f PSD export: a cancelled save stops between the bands of a banded layer
A banded layer checks the save's interrupt before each band and before each
channel's part of a band. Once cancelled it stops there and doesn't encode the
layer again raw; the incomplete model is discarded by the caller (which returns
IoError::Cancelled), so the save never reaches its commit point and the target
is untouched. Ex carries the export's Interrupt for this.

Addresses CodeRabbit review on crates/io/src/psd_export/banded.rs.
2026-11-03 18:00:23 +01:00
ljkx 3c82477fac Crop: turning the image keeps the frame inside it (#10869)
In the default mode a drag outside the box turns the image, but the
frame never changed, so its corners left the turned image and the crop
filled them with the background colour.

Each move of the turn now starts from the frame at the press: it keeps
its size and place if it fits the turned image, else moves the shortest
way in, else shrinks about its centre (keeping its proportions) to the
largest size that fits. The view follows the frame's centre, so the box
stays put on screen. Ctrl (Cmd) held during the turn leaves the frame as
it was. Content-Aware Fill and a frame already past the canvas keep
today's behaviour (not measured).
2026-11-03 17:57:16 +01:00
FredAppsandClaude Opus 5.5 d07684a379 fix(ui): Overscroll off keeps an image smaller than the window centred (#10488) (#11085)
* fix(ui): Overscroll off keeps an image smaller than the window centred (#10488)

With Tools › Overscroll off, an image smaller than the window stayed
wherever zooming around the pointer left it, inside the window, so
zooming out drifted it off centre. #1718 measured that on Photoshop 25.4,
but two reports of Photoshop itself (#10488, #4799) say it stays centred
with Overscroll off. The owner chose the centred behaviour:
scrollbars::clamp_axis now centres an axis on which the image is no
larger than the view; a larger image still can't leave its edges, and
Overscroll on keeps the free view.

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

* test(ui): Overscroll off centres a fitting rotated image (#10488)

CodeRabbit on #11085: the rotated clamp path was only tested from an
already-centred view. A 600 px square at 45 degrees, started off centre
in a 900-point view, now ends with its screen bounds centred.

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

* style(ui): keep the rotated centring test within rustfmt's width

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

---------

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-03 17:55:50 +01:00
FredAppsandClaude Opus 5.5 d9dc644db1 fix(ui): Roundness previews its radius within 1 px, and a held frame survives a factor change (#10474) (#11081)
* fix(ui): Roundness previews its radius within 1 px too, and a held frame survives a factor change (#10474)

Follow-up to #10524 from its last CodeRabbit review.

- Roundness's disc reaches floor(r) pixels along an axis, so at 6 px
  and k = 4 the proxy radius 1.5 reached 1 proxy px: a 4 px preview for
  a 6 px result. The factor now keeps floor(r / k) * k within 1 px of
  floor(r), as Squareness already does with round (k = 3 at 6 px, k = 2
  at 50 px). One whole-pixel function per mode replaces the special case.
- A canvas test drives the worker across a factor change: the frame for
  k = 2 stays on the canvas, at its own size, until the k = 1 frame lands.

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

* test(ui): a fractional Roundness radius previews closer at its chosen factor (#10474)

CodeRabbit on #11081: only the factors were pinned. At 6.5 px the
Roundness preview at the chosen k = 3 is compared with OK's result and
must differ less than the k = 4 preview did.

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

* fix(ui): a mid-drag frame for the previous proxy factor still reaches the canvas (#10474)

CodeRabbit on #11081: the display gate keeps a held frame at any factor,
but the mid-drag publish arm still required the same factor, so a frame
finished for the factor the drag just left was dropped. It now uses
same_target_at_any_scale too; a frame already exact for the dialog is
still never displaced.

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

---------

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-03 17:55:42 +01:00
d21c403e5f Type: add OpenType Numerals submenu (figure styles) (#11051)
* Type: add OpenType Numerals submenu (figure styles)

Type › OpenType › Numerals sets lnum/onum/pnum/tnum as a radio style
(Default through Tabular Oldstyle). Checkmarks follow the selection
start; TYPE-218-5 scorecard/gaps updated; parity floor 640.

* refactor(engine): one style-at-character lookup for the OpenType menus

feature_on_at and numerals_style_at each walked the runs the same way;
they now share style_at. NumeralsStyle::from_id and numerals_style_of
read the existing tables instead of repeating them.

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

* fix(text): persist OpenType numeral tags through PSD EngineData

Map lnum/pnum/tnum as EngineData bools and the width×height combos as
FigureStyle (Krita/Illustrator), so Numerals choices survive PSD round-trip.

* docs(gaps): note Numerals PSD figure-tag round-trip

* revert(text): keep PSD EngineData to the keys Photoshop writes

Backs out the PSD half of 18610324 and its gaps note (58813de3); the
engine reformat in that commit stays.

- `LiningFigures`, `ProportionalFigures` and `TabularFigures` are not
  Photoshop EngineData keys (the commit says Photoshop only has
  `OldStyle`), so writing them puts keys of our own into users' PSDs.
- The `/FigureStyle` code table (1-4) has no oracle: no Photoshop-authored
  file in photocraft-corpus, and neither ag-psd nor psd-tools reads it.

AGENTS.md rule 3 asks for public specs or observation. PSD persistence
of the numeral styles can come back with a Photoshop-authored oracle.
The Numerals menu still round-trips through .pcraft.

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

* test(text): lock PSD EngineData to Photoshop OldStyle for numerals

Document AGENTS.md clean-room: only onum survives TySh round-trip; lnum/pnum/tnum stay .pcraft-only until a Photoshop-authored corpus oracle.

---------

Co-authored-by: FredApps <153173854+FredApps@users.noreply.github.com>
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-03 17:55:35 +01:00