From a review of who reads a refusal.
The rule is to leave it out and say so, and it held on some paths only. An image whose JBIG2 mask is
refused no longer comes out opaque. The first image past the allowance reads as that and not as
corrupt. A page reports what it left out. The Office warning stopped blaming forms. Redaction refuses
the run, leaving the document as it was: removing a readable image whole would take what no mark
covers with it, unreported.
The 2 GiB an export holds is twice the document if that is more, so a large honest file loses nothing.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01AmsdeUCdTaD72jkDw2pJKY
The inline rename field asked for focus without setting a selection, so the
caret landed at the end of the default text and typing appended to it. The
field now selects the whole title on the frame it takes focus, so typing
replaces the old title at once, for both new bookmarks and rename. Later
frames leave the user's own caret and selection alone.
Tests: three unit tests on rename_field (first focus, multibyte title, caret
kept across frames) and two kittest tests driving the panel like a user
(typing replaces the title; the caret survives a collapse).
Co-Authored-By: Zoo <noreply@zoo.local>
One image has its own limit, about 6 s to 17 s, but nothing bounded a file of thousands of images
made to reach it: an operation took that long for each, and an image sharing /JBIG2Globals decodes
them again.
Image::decode_within (hayro-jbig2 patch 11) spends from what a run may still do, starting with its
page's pixels, and charge_reading charges reading a stream and its globals, 4 steps a byte, before
anything is parsed, so an image that fails has paid too. pdfcraft_filters::decode_jbig2_within takes
it, and ImageBudget in cos is that allowance: 2^34 steps, about 1,000 scanned pages, and 2^13 more for
each byte of the file, so a long scan is read whole. An image past it spends all that's left, and
later ones aren't read, globals included.
Optimize leaves such an image as it is and counts it in Report::images_refused. Image export leaves it
out, with the reason, and stops at 2 GiB of files held. Office export shares one allowance, leaves the
picture out and says the page was truncated. Redaction removes it whole, as any image it can't read.
Rendering gives a page's images 2^30 steps between them, inside the watchdog, and each page starts
afresh in Context::new as its other budgets do (hayro-syntax patch 31, hayro-interpret patch 73).
It's a thread-local, unbounded until a page sets it.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01AmsdeUCdTaD72jkDw2pJKY
From a review of the work count. An item with no pixels was charged a step, though reading it can take
hundreds of decisions, so a 22,515-byte PDF of refined text instances of no size stayed inside the
limit and took 730 s to render. Empty height classes and export runs weren't charged at all, and 19 KB
of them took 47 s. Each integer an item reads now costs what one takes at worst, 38, so a decision
never costs more than a pixel. A full region of 10,000 instances is 0.14% of the limit, so a real file
doesn't notice.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01AmsdeUCdTaD72jkDw2pJKY
From CodeRabbit on the work count.
A symbol with no rows cost no work, though it is decoded all the same, so a stream of thousands of
dictionaries of such symbols, exporting none, went uncounted. Each symbol now costs a step more than
its pixels.
A refined text instance was charged after it was decoded, so one could still be 2^28 pixels past what
was left before the decoder refused. It's charged once its size is read, before anything is allocated.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01AmsdeUCdTaD72jkDw2pJKY
The Pattern fix opened a recursion path: a Pattern's underlying space is now followed, as an Indexed
space's base always was. Self-references and cycles between the two end as a space that can't be
read.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01AmsdeUCdTaD72jkDw2pJKY
- Resolve all 13 merge conflicts between upstream/main and main
- Preserve full image editing suite: OBB, crop, proportional resize, arrange, and context menu actions
- Preserve text editing formatting tools: bullet/numbered lists, subscript/superscript, and line spacing
- Cleanly integrate upstream's canvas modularization, font updates, and styling changes
- Refactor arrange_image and run_edit_page_image helpers to satisfy clippy cognitive complexity limits
- All engine, edit, and UI integration test suites passing cleanly
The decoder has the same 2^28 cap as decode_jbig2, and can refuse a page first, so the doc says either
error can come. The page at the cap decodes, which the test now asserts, rather than failing later.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01AmsdeUCdTaD72jkDw2pJKY
Since the renderer now draws an image with /Decode [7 0 R 0], export disagreed with it. Its check of
which way round a stencil mask or a CMYK JPEG is compared the two numbers as given, and a reference
reads as none, so the file came out a negative of what the page draws.
Tests for the function arrays the previous commit's tuple fix also covers: a function's /Domain,
/Range, /Encode and /Decode are read a pair at a time too.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01AmsdeUCdTaD72jkDw2pJKY
Two more ways a reference was lost in the same readers. A Pattern space read its family name twice,
which took the underlying space when that was a name or a reference, so an uncoloured pattern took
its components as RGB: [/Pattern /DeviceCMYK] drew a cyan as red, direct names included (patch 72).
Tuples of array items, which /Decode, /W and /W2 are read as, took a reference at their start for the
whole tuple. A tuple can't be given by reference, so it was lost and the array ended there: an image
with /Decode [7 0 R 0] wasn't drawn, and CID widths fell back to /DW. A tuple now reads its items
directly, and each follows its own reference (hayro-syntax patch 32).
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01AmsdeUCdTaD72jkDw2pJKY
A colour space [/Indexed /DeviceRGB 1 5 0 R] whose table is a string in object 5 couldn't be read:
the table was tried as a stream first, and a failed read uses up a reference, though not a direct
object, so nothing was left to read it as a string. The space then drew in grey from the index, so
a palette's colours were lost. The table is now read once, as whichever it is (hayro-interpret
patch 71).
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01AmsdeUCdTaD72jkDw2pJKY
The test said such a page is refused here, as Unsupported. #5491 makes hayro-jbig2 refuse it when
the stream is opened, as Corrupt, so whichever of the two PRs landed second would fail this test.
A page past the cap is refused either way, and that's what the test now says.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01AmsdeUCdTaD72jkDw2pJKY
Each JBIG2 bitmap is bounded, but nothing bounded how many a stream asks for, so a few hundred
bytes could tie up a render worker for minutes, and CodeRabbit raised the same on #5461. Measured
through pdfcraft-cli, before and after:
- one halftone segment, a 795-byte PDF: 265 s, now refused at once;
- 66 halftones over patterns 1 wide, a 3,894-byte PDF: about 330 s, now refused at once;
- about 226,000 generic regions behind nested Flate, a 923-byte PDF: unfinished at 40 s, some
88 hours by extrapolation, now 4.8 s;
- 8 dictionaries of symbols of no width, a 983-byte PDF: 81 s, now 0.7 s;
- about 1,048,000 generic regions of no width, a 34 KB PDF: unfinished at 90 s, now 2.6 s;
- an aggregated dictionary over 2^20 symbols, a 713-byte PDF: unfinished at 90 s, now 9.1 s;
- 10,000 instances of an 8192 × 8192 symbol, an 18 KB PDF: 64 s, now 3.2 s;
- 30 generic regions of no data: 48 s, now 4.7 s.
A stream may now ask for 2^30 units of work: four of the largest pages, about 6 s on data the
decoder predicts and about 17 s on data it can't, inside the viewer's 20 s watchdog. Decoding a
bitmap costs a unit a pixel and 4 a row, since a row costs time at any width. Copying one costs a
unit a word and 1 a row, so a tall thin bitmap costs its rows. Each item is charged before it is
decoded, apart from what is known only as it goes: new symbols and text instances are charged in
their loops. An aggregated symbol is charged for the symbol table it rebuilds. Past the bound the
image isn't drawn, as with the other caps, and the rest of the page is.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01AmsdeUCdTaD72jkDw2pJKY
An ordinary scan stored as JBIG2 was decoded for Group 4 by default, at 0.26 s a page at 300 ppi,
and came out unchanged: Group 4 of a symbol-coded page was 201,132 bytes against 17,291. Over a
500-page scan that is minutes for nothing. Dropping the guard changed no output, as a cost guard
wouldn't, so its test counts the pixels decoded.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01AmsdeUCdTaD72jkDw2pJKY
From a review of the spellings an indexed image can take:
- The palette was read for every 1-bit indexed image, resampled or not, and twice each. Images
share a table, so 1,000 images sharing one with 16 MiB of raw data took 16 to 60 s, none of
them resampled. It is now read only for an image about to be resampled, and a table over
4 KiB is refused before it is copied or decoded, since no palette needs that.
- A colour key can hide the dark sample a tie goes to, so a thin rule of the other was lost: a
white rule on a hidden black went from 255 to the backdrop's 128. An image with a colour key
now keeps its resolution. Grey had this since #5426 and is covered too.
- `/Indexed` by reference, and a base of `[/DeviceRGB]`, are the same colour space and are read
so. `/I`, an inline abbreviation, and a real hival aren't, and such an image is left alone.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01AmsdeUCdTaD72jkDw2pJKY
From CodeRabbit's review: default colour spaces apply to an indexed colour space's
device base, so one in force where an annotation's appearance (or a pattern, or
another image's /Alternates) draws the image may change which palette entry is darker,
and the tie a resampled image keeps. Only black-and-white RGB went through
used_elsewhere; indexed images now do too when downsampling is on, and one used beyond
the page and form resources the walk read keeps its resolution, as such RGB keeps its
colour space.
A grey image stays out of this check, since sample 0 stays dark in a grey space, and #5426's rule
for grey is unchanged: the test now has a grey image drawn on the page and in an annotation.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01AmsdeUCdTaD72jkDw2pJKY
From CodeRabbit's review of the stack:
- Every JBIG2 image read its /JBIG2Globals afresh, within 32 MiB each but with no
bound across an export or a redaction. A globals stream of the file, not edited since
it was read and with its /DecodeParms direct, now has its filters undone once a
document and is kept for the images that share it (in every clone, which share the
file), up to 64 MiB in all. An edited one, one added in memory, or one with
/DecodeParms by reference (another object, which can change under it) is read each
time as before. This saves the filters only: the decoder reads the segments afresh for
every image, so a real symbol dictionary still costs its decoding once an image.
- view.copy-image dropped "JBIG2 inside other filters" from its missing list on the
strength of Export's test; Copy Image now has its own: a JBIG2 picture behind
ASCIIHexDecode is found under the pointer and copied as its pixels and as a PNG, and
one whose page information is in its globals is read through them.
A JBIG2 image's /Width and /Height are read as given directly; one by reference is an
error rather than an image of no rows.
A /JBIG2Globals reference to no object still reads as no globals: it is a reference
to null (ISO 32000-2 §7.3.10), which an entry may hold for being absent (§7.3.7), and
the renderer draws the page so. An image that needed those globals then mostly fails
to decode; in a probe of twelve pictures that decoded with them, eleven were refused
and one decoded to other pixels.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01AmsdeUCdTaD72jkDw2pJKY
Chinese and Japanese text wraps between characters, so the word space
put at each joined line break showed up inside the text. No space is
added now when both sides of the break are Chinese or Japanese
characters, their punctuation or fullwidth forms; Korean, which spaces
its words, still gets one. A line ending in a full-width full stop,
exclamation or question mark (also inside closing corner brackets) now
ends its paragraph like one ending in a full stop.
The new test lines fail without the change. The wrapped sentence test
now checks the Markdown export too, which goes through the same
joining since it was added.
Word and LibreOffice often draw a list's bullet in a symbol font, a
tab stop before the item's text, so each bullet is a block of its own.
Table detection read the bullets and the items as two columns, and
every export wrote the list as a borderless two-column table.
Each bullet is now paired with the text that starts within three times
its size after it on its line. The text runs on from its bullet, and
bullets stay out of table detection, so Word gets one paragraph per
item with the bullet first. The Symbol and Wingdings bullets Word
writes (U+F0B7, U+F0A7) count. A dash or a middle dot counts only when
nothing starts left of it on its line, so a dash that stands for
"none" in a table row stays a cell.
The new export and automation tests fail without the change.
Reading splits a line wherever its style changes, and export joined
those blocks along one line only (#4158). A sentence with a bold name
in one line and an italic name in the next came out of Word, HTML, RTF
and PowerPoint as one paragraph per line.
A paragraph whose last line reaches the right edge of the page's text
now runs on into the line just below it, when that line starts at the
same left edge in the same size. Headings, list items, a line that
ends a sentence, an indented line and a line after paragraph space
keep their break. A slanted run whose box leans into the next word by
up to 0.3 of its size still runs on into it.
The new export and automation tests fail without the change.