Commit Graph
3518 Commits
Author SHA1 Message Date
Grzegorz MartensandClaude Opus 5.5 b71914b1de M2: colour spaces nest at most 32 levels
Reading an object already being read is refused, which ends a cycle but not a chain. 5,000 spaces
each naming the next, 180 KB of file, overflowed a 2 MiB stack and aborted the process, through a
Pattern's underlying space, an Indexed base, or a Separation's or DeviceN's alternate. An ICC
profile's alternate is the same recursion. Indexed, Separation and DeviceN could already do this;
the fix for Pattern added a way in.

A legal file goes five levels at most. All five ways through count against the one limit, in
ColorSpaceType::new, so what comes in from outside is unchanged.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01AmsdeUCdTaD72jkDw2pJKY
2026-10-31 19:33:43 +01:00
Combiler01 6de6339736 fix(ui): preserve selection clearing semantics after format application 2026-10-31 23:52:00 +05:30
Combiler01 fdc0e6404a fix: address review feedback on arrange, image flips, rotate knob, and format panel
- [P1] Arrange: track and replay inherited non-ExtGState drawing state (colors, line widths, etc.) across q/Q scopes for stencil and form artwork
- [P2] Flips: route ImageEdit::Flip through coordinate-aware transform logic to swap axes when rotated at odd quarter turns (90°/270°)
- [P2] Rotation knob: measure clockwise angle delta from drag start relative to the rotated image center to prevent reapplying existing angle
- [P2] Format panel: connect superscript/subscript controls to line editor and content draft selections, and update parity evidence
2026-10-31 23:18:58 +05:30
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
Combiler01 9b169fae50 test(optimize): accept Flate or CCITT compression in monochrome scan test 2026-10-31 20:33:48 +05:30
Combiler01 d2d28c7728 test(edit): restore regression test rewritten_paragraph_is_drawn_after_backgrounds_between_its_rows (#2377) 2026-10-31 19:40:44 +05:30
Combiler01 6da8ec4b86 fix(ui): preserve test-verified selection and paste recency semantics 2026-10-31 19:37:22 +05:30
Combiler01 d67e720eed fix: address CodeRabbit review feedback on transforms, formatting, and UI 2026-10-31 19:20:44 +05:30
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
Combiler01 b512e3627f fix(engine): fold quarter-turns into AddedImage rotation and reject non-axis-aligned transforms 2026-10-31 18:44:20 +05:30
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
Combiler01 e781d31187 fix(ui, test): resolve font-size inspect query and command-key paste tracking 2026-10-31 17:09:50 +05:30
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 43557fa046 M2: a test that a colour space referring to itself ends
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
2026-10-31 11:44:23 +01:00
Combiler01 79eed72c08 Merge upstream/main into main preserving custom features
- 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
2026-10-31 15:50:31 +05:30
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 ae94b54505 M10: export reads a /Decode array's numbers through references
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
2026-10-31 10:01:27 +01:00
Combiler01 87c1e858e3 Coderavbit Fixes 2026-10-31 14:23:19 +05:30
Grzegorz MartensandClaude Opus 5.5 fa7acd3156 M2: a Pattern space's underlying space and a group of array items keep their references
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
2026-10-31 08:58:25 +01:00
Grzegorz MartensandClaude Opus 5.5 84a2005f9e M2: an Indexed lookup table in an indirect string is read
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
2026-10-31 08:49:24 +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
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
aiongg 635a840164 updates: show check results with craft-update's CheckResultDialog 2026-10-31 04:43:07 +00:00
aiongg 44ddb348a9 docs: tidy agent, contributor and asset docs 2026-10-31 04:17:20 +00:00
Grzegorz MartensandClaude Opus 5.5 53874375cb M8: test: a cleared copy stored as Group 4 or JBIG2 can be redacted again; the README's not-yet list
The README still listed metadata and hidden-information sanitising as not done, though sanitize
does it.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01AmsdeUCdTaD72jkDw2pJKY
2026-10-31 02:53:16 +01: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
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
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 689a1e34cf M8: test: a cleared image of more than one bit stays Flate
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 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 d7bd36251b 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:28 +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 7f9554f29b M8: a cleared black-and-white scan is stored as Group 4, or JBIG2 again, when smaller
Redaction wrote every cleared copy as Flate, so a redacted Group 4 or JBIG2 scan
page grew several times over until the optimizer ran. A cleared one-bit image of one
component is now stored as the smallest of Flate, CCITT Group 4 and, when it was
JBIG2, JBIG2 (PDF 1.4, which such a file already is), as stored, dictionary included:
a tiny image can lose by Group 4's parameters. Both are coded from the
cleared samples, so nothing of the original's code is carried over; rows Group 4 or
JBIG2 can't code (too wide) leave Flate. Images of more bits stay Flate.

Tests: a cleared one-bit Flate scan of ragged word blocks (where Group 4 is smaller
than Flate) comes out Group 4 and an eight-bit image Flate, each decoding to the
cleared samples (the whole buffer); a tiny image whose Group 4 code is shorter but whose
stream isn't, and rows wider than Group 4 codes, stay Flate and clear; the redacted
JBIG2 scan of #5461's test comes out JBIG2, smaller than
Flate, with no globals and no stream holding the original code. The tests' image
helpers decode through Document::decoded_image, as image codecs need.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-30 19:08:32 +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
Grzegorz MartensandClaude Opus 5.5 c412093dc7 M11: an indexed image's palette says which of its samples is dark, so it is averaged too
#5426 left indexed images at their resolution rather than guess which sample is
dark. The palette says: dark_is_one now maps samples 0 and 1 through the decode array
to palette entries (rounded and clipped to hival, ISO 32000-2 §8.9.5.2) and compares
how light those entries are (`lightness`: the grey, or Rec. 601 luma over DeviceRGB).
Only DeviceGray and DeviceRGB bases count, the table a string or a stream (within
4 KiB), directly or by reference: there the palette's values are the colour, and
under /DefaultGray or /DefaultRGB no polarity is asked for. Other bases (CMYK,
calibrated, ICC-based, a resource name), two entries alike, an entry the table
doesn't hold or a hival past 255 still say nothing; separations and DeviceN still
aren't averaged, their tint transforms unread. #5426's tone guard applies as to any
image.

#5426's indexed thin-rule test now checks what it was for: with the palette either
way round and the decode array either way, the averaged image holds the averaged
DeviceGray one's samples (flipped where its samples are) and renders the same. A unit
test covers each base, both orders, an inverting decode array, the weights of luma,
the table by reference and in a stream, and every refusal; the older polarity test's
doc says what is known now.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-30 19:04:42 +01:00
songzhihui a57095c872 M2: assert exact GBK glyph rasters for installed Chinese families 2026-10-31 01:39:00 +08:00
songzhihui 9447cdc810 M2: merge main and resolve CJK viewer integration conflicts 2026-10-31 01:14:02 +08:00
Grzegorz MartensandClaude Opus 5.5 78fc1e5568 M8: redaction clears the covered pixels of JBIG2 images instead of removing them
Part of #283. A scan stored as JBIG2 (as the optimizer writes them) is decoded
through Document::decoded_image_within, its /JBIG2Globals included, and its covered
pixels cleared in a Flate copy that refers to no globals, as CCITT fax images are.
Before, any JBIG2 image under a mark was removed whole, blanking the page.

Still removed whole (fail-closed): a JBIG2 page of another size than the image,
which decoded_image refuses (the renderer draws the whole page into the image, so
the rows cleared wouldn't be the ones shown); globals that can't be read; damaged
data; an image claiming more than one bit or one component, even where its rows
take as many bytes; a page far past the image's raster, refused before it is
decoded.

Globals no other image uses leave the file with the original on the full rewrite
(tested); globals another image still uses stay, and the README and parity note say
they may hold the shapes of marks from under the area, not where they were.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-30 18:03:06 +01:00
Grzegorz MartensandClaude Opus 5.5 ea6c329731 M2.6: JBIG2 decoding in pdfcraft-filters; Document::decoded_image reads JBIG2 images
decode_jbig2 sits next to decode_ccitt, through hayro-jbig2 as Export ▸ Images
used it, and shares CCITT's row sink. The JBIG2 page has to be the image's size:
PdfCraft's renderer draws the whole page into the image (black box: a 4-row page
behind /Height 2 fills the image box with all four rows), so the rows of a taller
page aren't the image's. The decoder decodes a page whole, so the page is checked
before it is decoded: that size, within the caller's bound as rows of one bit, and
at most 2^28 pixels (MAX_JBIG2_PIXELS; the decoder keeps a bit a pixel, about 34 MB
there).

Document::decoded_image_within decodes a JBIG2 image as it does a fax image: the
filters before it undone first, then the decoder with the /JBIG2Globals of the
image's own /DecodeParms entry (/Crypt counts in the position; fax_params and the
globals now share last_filter_parms). Globals have a limit of their own,
MAX_JBIG2_GLOBALS (32 MiB, moved from Export), and globals that can't be read are an
error rather than ignored.

Export ▸ Images reads JBIG2 through it, so its own decoder, row sink and DecodeParms
reader go, and hayro-jbig2 leaves pdfcraft-create. JBIG2 behind another filter now
exports, and so copies (Copy Image uses the same function); a page taller than the
image, which Export cut to the image's rows, now skips the image, as one of another
width did. doc_export_all_images' description no longer lists JBIG2 inside other
filters as skipped.

The optimizer's monochrome step read images through decoded_image_within and
relied on JBIG2 failing there: it now leaves JBIG2 images alone explicitly, as
before (a resampled one would come out Flate under Retain), and the README and
parity note list them as not yet handled.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-30 18:03:06 +01:00
Grzegorz MartensandClaude Opus 5.5 c0e3c6bec5 M11: the docs say which black-and-white images monochrome downsampling averages since #5426
The integration in #5426 made downsampling conservative: dark_is_one answers only for
DeviceGray and image masks (a decode array of two different numbers or none), and not
under /DefaultGray or /DefaultRGB, and a square of four new pixels that each cover both
samples keeps the image at its resolution. The README, the crate docs, mono()'s comment
and the optimize.downsample-images parity note still said separations and indexed
images were averaged, a tie going to full ink or entry 0.

Measured on main: a 600 ppi page of the ragged-text fixture is no longer averaged
(it is with the square guard switched off), so the docs say so.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-30 17:09:02 +01:00
FredApps 6772772440 test: accept preserved CCITT filter arrays in lossless regression 2026-10-30 15:52:18 +01:00
FredApps 7fe33d1cbd test: verify conservative review fixes across rendering and automation 2026-10-30 15:32:44 +01:00