Commit Graph
3831 Commits
Author SHA1 Message Date
Grzegorz MartensandClaude Opus 5.5 7bfdfd293a M2.6: image export pays for a JPEG mask, and reads a JPEG's size first
A JPEG that was itself a mask paid nothing, and images can share one: 300
images in 66 KB took 135 s with none refused, about 0.45 s for 220 bytes. It
now pays for its samples each time an image reads it.

Export priced a JPEG by its dictionary and decoded the size in its header,
comparing them only afterwards: an image of one pixel over a header of 500
megapixels cost 2 steps and 0.3 s. Both paths now read the header first and
leave a JPEG that disagrees undecoded, as Optimize and the CMYK path do.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01AmsdeUCdTaD72jkDw2pJKY
2026-11-01 01:01:22 +01:00
Grzegorz MartensandClaude Opus 5.5 ade8ebbdb4 M2.6: a run pays for what a stream costs, not what it holds
Four ways round the allowance, each measured by a review of hostile input:

A Flate stream read as zlib that fails at once, and then as raw deflate that
decodes everything before failing, gave nothing back and so was charged
nothing, here and in DecodeBudget: 110 s for a 387 KB file, all decoded. What
the retry decoded now counts, though the data is still dropped as it was.

Fax and 1-bit Flate images paid by the byte for work done by the pixel: 19
bytes can be a blank page of 100 million pixels, about a second each. Every
image decoded here now pays for its pixels first, as JBIG2 already did.

A JPEG paid nothing, and what one asks for doesn't grow with its length: 156
bytes declared 80 megapixels, 72 s for an 8 KB file. A JPEG pays for its
samples before it is decoded, in Optimize and in an export that has to decode
one for its mask.

Merging fragments paid for reading strips and not for the picture it built:
393 s for 1,154 bytes. It pays for the picture before allocating it.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01AmsdeUCdTaD72jkDw2pJKY
2026-11-01 00:20:43 +01:00
Grzegorz MartensandClaude Opus 5.5 696728686f M2.6: Optimize's LZW pass spends the run's allowance, and Cancel reaches it
Storing LZW streams with Flate decoded image streams outside the allowance,
with a limit for each stream and no total, and nothing asked whether to go
on: a 593 KB file of 100 images took 113 s with none refused, and images
refused by the image pass were still rewritten. The pass now pays for what
it decodes and for storing and checking it, at 8 steps a byte since Flate
takes up to 78 ns a byte, and streams past that stay LZW.

An image is counted as refused only when it stayed as it was: one packed
before the allowance ran out on its second read was rewritten. doc_reduce
reports images_refused as doc_optimize does, and the app's action says so
in its log. A refused image no longer reads as an unsupported filter:
FilterError::Spent says what happened.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01AmsdeUCdTaD72jkDw2pJKY
2026-10-31 23:26:25 +01:00
Grzegorz MartensandClaude Opus 5.5 083191f970 M2.6: every filter of a run's images pays, whether or not it works
Filters were charged only for what they handed a JBIG2 decoder, once they
had handed it over: a chain that failed, or went past its image's limit,
cost its time and nothing else, and images that weren't JBIG2 weren't
charged at all. A 734 KB file of 200 images of Flate inside Flate took an
image export 111 s, and that grew with the file. It now takes 12 s.

Each byte read and each byte a stage decodes is now a step of the same
allowance, paid as it happens, so no stage runs past what is left. That
holds for every image read through ImageBudget, and a fax image pays for
its rows and its code. JBIG2 globals pay when they have to be read, and
kept ones don't. Optimize's two reads that went round the budget go
through it, and merging fragments shares it.

Optimize reads an image twice, to pack it and to resample it, so a refused
image is now counted once a run rather than once a read.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01AmsdeUCdTaD72jkDw2pJKY
2026-10-31 22:23:06 +01:00
Grzegorz MartensandClaude Opus 5.5 621f17f3b5 M2.6: an image already at the target resolution isn't decoded to find that out
Found by CodeRabbit.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01AmsdeUCdTaD72jkDw2pJKY
2026-10-31 18:07:06 +01:00
Grzegorz MartensandClaude Opus 5.5 a6944cbbdf M2.6: the widest image that can be averaged is decoded
The exclusion's test only looked past the limit, so a mutant moving it by one survived.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01AmsdeUCdTaD72jkDw2pJKY
2026-10-31 17:43:46 +01:00
Grzegorz MartensandClaude Opus 5.5 199354246b M2.6: redaction reads past /Crypt, and what can't be averaged spends nothing
Redaction checked the raw /Filter array, so /Crypt in a chain removed an image it could have cleared,
though decoding leaves /Crypt out. It leaves it out too.

Decoded only to be resampled, an image that can't be averaged came to None after the decode, having
spent the run's allowance and perhaps been counted as refused. Each such case now stops before it.

Found by CodeRabbit.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01AmsdeUCdTaD72jkDw2pJKY
2026-10-31 17:36:44 +01:00
Grzegorz MartensandClaude Opus 5.5 bd5bcb7f10 M2.6: merge #5472's last fix
The JPEG test's doc comment goes back beside it, past this branch's allowance test.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01AmsdeUCdTaD72jkDw2pJKY
2026-10-31 17:36:07 +01:00
Grzegorz MartensandClaude Opus 5.5 2ad72dcbd1 M11: the JPEG test has its doc comment back
The JBIG2 test went in between the two.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01AmsdeUCdTaD72jkDw2pJKY
2026-10-31 17:32:10 +01:00
Grzegorz MartensandClaude Opus 5.5 e7cabea981 M2.6: the patch's row says where the allowance is set and what it leaves out
Found by CodeRabbit.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01AmsdeUCdTaD72jkDw2pJKY
2026-10-31 16:19:43 +01:00
Grzegorz MartensandClaude Opus 5.5 a78356eefc M2.6: a spent page speaks for its JBIG2 streams only, and ends with the page
What a page has left is shared by all of it, so it says nothing about a stream that isn't JBIG2: a bad
Flate or JPEG mask dropped its picture on a page that had spent its allowance, and a failing image of
another kind raised a false "content skipped". Both ask about the stream now.

The allowance also outlived the page on its thread, and a Context used again kept it. interpret_page
sets it, and a guard ends it with the page.

Found by CodeRabbit.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01AmsdeUCdTaD72jkDw2pJKY
2026-10-31 15:43:05 +01:00
Grzegorz MartensandClaude Opus 5.5 663af0a169 M2.6: merge #5491's last fix
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01AmsdeUCdTaD72jkDw2pJKY
2026-10-31 14:35:48 +01:00
Grzegorz MartensandClaude Opus 5.5 e3bc4e855f fix(render): a JBIG2 height class of no symbols decodes no bitmap
Rows are charged by the symbol, so an empty class decoded its collective bitmap for nothing: 65,535
rows a class, a thousand classes a dictionary. 806 KB took 196 s. It steps over the bytes instead.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01AmsdeUCdTaD72jkDw2pJKY
2026-10-31 14:33:57 +01:00
Grzegorz MartensandClaude Opus 5.5 0f325d44b6 M2.6: a picture whose JBIG2 mask is past the allowance isn't drawn either
Without its mask a picture shows what the mask hides, so the page leaves it out, as export does. Any
other unreadable mask still leaves the picture, which is the renderer's tolerance for bad masks.

What an export may hold is a function of its own, tested down to the step and at the top of the range.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01AmsdeUCdTaD72jkDw2pJKY
2026-10-31 14:24:53 +01:00
Grzegorz MartensandClaude Opus 5.5 4aa732ea91 M2.6: what is left out past the JBIG2 allowance is said, and redaction stops
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
2026-10-31 14:13:48 +01:00
Grzegorz MartensandClaude Opus 5.5 0ed339901e M2.6: the JBIG2 images of an operation or a page share one allowance of decoding
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
2026-10-31 13:21:45 +01:00
Grzegorz MartensandClaude Opus 5.5 78d4877ae2 Merge fix/jbig2-retained-bitmaps: what an item reads is charged
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01AmsdeUCdTaD72jkDw2pJKY
2026-10-31 13:21:21 +01:00
Grzegorz MartensandClaude Opus 5.5 b7ec3669cc fix(render): what a JBIG2 item reads is charged, 38 an integer
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
2026-10-31 13:10:50 +01:00
Grzegorz MartensandClaude Opus 5.5 72e763ad12 fix(render): a JBIG2 symbol with no rows costs a step, and a refined instance is charged first
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
2026-10-31 11:51:15 +01:00
Grzegorz MartensandClaude Opus 5.5 4ba8d3f530 Merge feat/redact-jbig2: the JBIG2 cap's doc and test
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01AmsdeUCdTaD72jkDw2pJKY
2026-10-31 10:46:52 +01:00
Grzegorz MartensandClaude Opus 5.5 7d53e07f43 M2.6: the JBIG2 cap's doc and test say what happens
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
2026-10-31 10:36:41 +01:00
Grzegorz MartensandClaude Opus 5.5 1352b3f0a2 Merge upstream/main
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01AmsdeUCdTaD72jkDw2pJKY
2026-10-31 09:04:38 +01:00
Grzegorz MartensandClaude Opus 5.5 738cbadab7 M2.6: a page past the JBIG2 cap may be refused by the decoder first
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
2026-10-31 08:33:12 +01:00
Grzegorz MartensandClaude Opus 5.5 967c610aae Merge feat/redact-jbig2: a page past the cap may be refused by the decoder first
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01AmsdeUCdTaD72jkDw2pJKY
2026-10-31 08:33:12 +01:00
Grzegorz MartensandClaude Opus 5.5 fc389071e2 Merge fix/jbig2-retained-bitmaps: the run's allowance spends the decoder's work count
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01AmsdeUCdTaD72jkDw2pJKY
2026-10-31 08:28:07 +01:00
Dylan M. Taylor f137a289ba Merge pull request #5522 from aiongg/docs-tidy
docs: tidy agent, contributor and asset docs
2026-10-31 01:06:25 -04:00
Dylan M. Taylor 4a5f9ced2d Merge pull request #5527 from aiongg/update-result-dialog
Updates: shared check-result dialog from craft-update
2026-10-31 01:05:42 -04:00
aiongg 635a840164 updates: show check results with craft-update's CheckResultDialog 2026-10-31 04:43:07 +00:00
aiongg da4cc2c145 docs: tidy wording 2026-10-31 04:25:36 +00:00
aiongg 44ddb348a9 docs: tidy agent, contributor and asset docs 2026-10-31 04:17:20 +00:00
Brandon ThomasandClaude Opus 5.5 24bb85308e Release 0.16.1
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
v0.16.1
2026-10-30 23:25:37 -04:00
Brandon ThomasandClaude Opus 5.5 35c187ff19 Flatpak bundle: skip the debuginfo pass manifest-wide (runner has no eu-strip)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-30 23:25:35 -04:00
Brandon ThomasandClaude Opus 5.5 8f16855097 Release 0.16.0
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-30 23:22:57 -04:00
Brandon ThomasandClaude Opus 5.5 4b70916883 Flatpak bundle: skip the debuginfo pass for cups-client (runner has no eu-strip)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-30 23:22:50 -04:00
Grzegorz MartensandClaude Opus 5.5 8243952569 fix(render): a JBIG2 stream's decoding work is bounded in total (hayro-jbig2 patch 10)
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
2026-10-31 02:26:56 +01:00
skylar taylor-barrick 65ff3a00c0 Merge pull request #4172 from songzhihui/fix/windows-cjk-viewer
M2: continue CJK viewer fallback for Windows GBK PDFs
2026-10-30 18:45:08 -06:00
Grzegorz MartensandClaude Opus 5.5 7d422a0998 M11: JBIG2 alone isn't decoded for Group 4 unless it is resampled
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
2026-10-31 01:39:27 +01:00
Grzegorz MartensandClaude Opus 5.5 d41d4c5eaa Merge the fixes to the PR below into this one
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01AmsdeUCdTaD72jkDw2pJKY
2026-10-31 01:16:58 +01:00
Grzegorz MartensandClaude Opus 5.5 24c6529f77 M11: test: a palette stream is read up to exactly 16 KiB of raw data
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01AmsdeUCdTaD72jkDw2pJKY
2026-10-31 01:15:52 +01:00
Dylan M. Taylor ec33924e80 fix(flatpak): allow reading launcher version pins (#5490)
* fix(flatpak): allow reading launcher version pins

* fix(flatpak): allow reading launcher version pins
2026-10-30 20:12:04 -04:00
Grzegorz MartensandClaude Opus 5.5 e0e40c6460 Merge the fixes to the PR below into this one
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01AmsdeUCdTaD72jkDw2pJKY
2026-10-31 01:06:26 +01:00
Grzegorz MartensandClaude Opus 5.5 6ad95f0aa4 M11: an indexed image's palette is read once it matters, within 4 KiB; a colour key keeps its image
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
2026-10-31 01:05:58 +01:00
Grzegorz MartensandClaude Opus 5.5 fbb1942933 Merge the fixes to the PR below into this one
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01AmsdeUCdTaD72jkDw2pJKY
2026-10-31 00:41:23 +01:00
Grzegorz MartensandClaude Opus 5.5 e2adbe7faa M11: tests: an indexed palette is read by its colours' luma, and only a real palette counts
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01AmsdeUCdTaD72jkDw2pJKY
2026-10-31 00:41:23 +01:00
Grzegorz MartensandClaude Opus 5.5 4d30af00a9 Merge the fixes to the PR below into this one
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01AmsdeUCdTaD72jkDw2pJKY
2026-10-31 00:20:54 +01:00
Grzegorz MartensandClaude Opus 5.5 d1677b4b3f M11: an indexed image also drawn where the optimizer doesn't read keeps its resolution
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
2026-10-31 00:20:10 +01:00
Grzegorz MartensandClaude Opus 5.5 7fbb40a2c1 Merge the fixes to #5461 into this PR
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01AmsdeUCdTaD72jkDw2pJKY
2026-10-31 00:20:10 +01:00
Grzegorz MartensandClaude Opus 5.5 5480dbe163 M2.6: shared JBIG2 globals are read once a document; Copy Image is tested on JBIG2
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
2026-10-31 00:06:56 +01:00
Grzegorz MartensandClaude Opus 5.5 85c1005148 fix(render): a JBIG2 page's segments are bounded in what they hold and refer to (hayro-jbig2 patches 5 to 9, hayro-syntax 29)
Each JBIG2 bitmap was bounded on its own, but a few kilobytes could still exhaust memory:
nothing bounded what a page's segments hold for later ones, or how far one segment could
multiply them. Measured on black-box streams, peak memory before and after:

- intermediate regions, 16 in 774 bytes: 514 MiB, now 98 MiB;
- an 8 MiB symbol exported 1,200 times, a 2,989-byte PDF: killed at 8 GiB, now 79 MB;
- dictionaries of zero-width symbols: 781 MB, now 72 MB;
- dictionaries retaining their coding contexts, 1.1 MB: 2.89 GB, now 71 MB;
- 65,535 references to one dictionary, 78 KB: about 17 GB, now 6 MB;
- a halftone grid of 16,384 cells a side, 98 bytes: 1.1 GB, now 2.4 MB;
- a pattern dictionary of 65,535 patterns, 48 bytes: 539 MB, now 15 MB, and it still draws;
- a custom Huffman table behind nested Flate, a 1,297-byte PDF: killed at 8 GiB, now 278 MB,
  which is mostly the decoded stream;
- 11-byte segments behind nested Flate in a stream and its globals, a 4,765-byte PDF: 5 GB, now
  602 MB, which is mostly the two decoded streams;
- an Indexed image whose page header claims 2^20 a side, a 743-byte PDF: aborted asking for
  137 GB, now 23 MB;
- a chain of 4,000 /JBIG2Globals streams that are JBIG2 themselves, on a 2 MiB stack: overflowed
  it and aborted, now 24 MB.

So PageState counts what it stores: regions, pattern and symbol dictionaries and Huffman
tables, with bitmap headers, retained contexts and a charge per item, up to 64 MiB, two square
bitmaps of 16,384 a side. A dictionary's new and exported symbols count against what is left as they are
made, not once it is stored. A segment may refer to at most 2^20 symbols, checked before any are
collected. A halftone grid has at most 2^25 cells. A pattern dictionary keeps at most 16 MiB of
shifted copies, which only speed up drawing, and none that wouldn't fit what the page has left. A custom Huffman table has at most 2^16 lines, and a
stream with its globals at most 2^20 segments and 2^22 references, checked as they are read. A
page past the bounds on a bitmap is refused when the stream is opened, since callers size their
output from its header before the page bitmap is made. And in hayro-syntax, a /JBIG2Globals
stream that is itself JBIG2 isn't decoded: globals are raw segments, so it reads as globals that
can't be decoded.

An image past a bound isn't drawn, as with the other caps, and the rest of the page is. This
bounds memory, not time: decoding work has no total yet.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01AmsdeUCdTaD72jkDw2pJKY
2026-10-30 23:43:50 +01:00
Grzegorz MartensandClaude Opus 5.5 39eb03a0b6 M11: Monochrome Images reads JBIG2 scans: averaged like any, and stored again as JBIG2
#5461 made Document::decoded_image read JBIG2, page exactly the image's size, and
kept the optimizer's monochrome step off it until it could weigh JBIG2 again. Now
one_bit reads JBIG2 last after any filters, within the raster's allowance, and mono
treats it as it does any black-and-white image:
- above the threshold it is averaged, and stored as the compression asks, the
  smallest of what that may store; under Retain it is stored as JBIG2 again, its kind
  of compression kept;
- not resampled, it is stored again as the compression asks when that is smaller: a
  generic region becomes symbols with JBIG2 asked for, and JBIG2 behind ASCIIHex is
  stored again alone (as Group 4 where that is smaller);
- a JBIG2 image stored again refers to no /JBIG2Globals; one whose page isn't its
  size isn't read, and stays.

jbig2_images_are_left_as_they_are becomes jbig2_scans_are_averaged_and_stored_again:
a JBIG2 scan at 600 ppi, alone, behind ASCIIHex and with globals, averages to the
same samples as its Flate copy under each compression (Retain keeping JBIG2); at 300
ppi a generic region of rendered text stays under Group 4 (larger) and Retain and
becomes symbols under JBIG2 (under half the size); behind ASCIIHex it is stored again
alone, as Group 4 where the hex is spaced out; a taller page stays.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-30 19:05:09 +01:00