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.
* 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
* 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
* 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
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.
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.
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.
* 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
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.
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.
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.
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.
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.
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>
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>
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>
#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>
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>
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.
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).
* 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>
* 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>
* 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>