chore: sync upstream (797 Commits) #21

Merged
alf merged 797 commits from chore/sync-upstream-20261109 into main 2026-11-09 11:25:30 +00:00
Owner

Upstream-Sync von upstream/main (797 Commits). Fast-Forward.

  • 9ee213eff Fix main's build and tests after the latest merges (#6706)
  • dafb3be3d fix(xfa): one decoding allowance for all XFA packets (#6000)
  • b4679deaf fix(cli): cap input reads, budget check's edit pass and escape its stderr records (#5997)
  • ba47f7bbc fix(cli): render needs the copying permission, as text does (#5990)
  • f800c12a7 fix(xtask): render-oracle writes page pixels into a private temporary folder (#5988)
  • 4cdc3d15d fix(redact): read the page tree once per sanitize pass, not per page (#5983)
  • 8f29ed66f fix(engine): open linked documents without blocking and check the handle is a regular file (#5982)
  • 3fe86a118 fix(print,a11y): read summary scans one operator at a time and borrow form resources (#5968)
  • 0d4074755 fix(automation): sign_trust deduplicates certificates with a set (#5965)
  • ca56433f5 fix(redact): read text-state operands from the end, as the renderer does (#5951)
  • 44a26f4b9 fix(model,oracle): read a page box only when it has four entries; skip empty images (#5941)
  • 743a772ee fix(content): an inline image only the renderer reads counts as ambiguous (#5874)
  • e78102c90 M11: PDF Optimizer can discard form submission, import and reset actions (optimize.discard-form-actions) (#4416)
  • ba2846cba M4: a recovered document's first Save asks where instead of writing the sidecar's path (#3923) (#4327)
  • 5588db4b6 M10: (n) names the page at position n, whatever its label, in page ranges and the page box (print.page-range) (#4282)
Upstream-Sync von `upstream/main` (797 Commits). Fast-Forward. - 9ee213eff Fix main's build and tests after the latest merges (#6706) - dafb3be3d fix(xfa): one decoding allowance for all XFA packets (#6000) - b4679deaf fix(cli): cap input reads, budget check's edit pass and escape its stderr records (#5997) - ba47f7bbc fix(cli): render needs the copying permission, as text does (#5990) - f800c12a7 fix(xtask): render-oracle writes page pixels into a private temporary folder (#5988) - 4cdc3d15d fix(redact): read the page tree once per sanitize pass, not per page (#5983) - 8f29ed66f fix(engine): open linked documents without blocking and check the handle is a regular file (#5982) - 3fe86a118 fix(print,a11y): read summary scans one operator at a time and borrow form resources (#5968) - 0d4074755 fix(automation): sign_trust deduplicates certificates with a set (#5965) - ca56433f5 fix(redact): read text-state operands from the end, as the renderer does (#5951) - 44a26f4b9 fix(model,oracle): read a page box only when it has four entries; skip empty images (#5941) - 743a772ee fix(content): an inline image only the renderer reads counts as ambiguous (#5874) - e78102c90 M11: PDF Optimizer can discard form submission, import and reset actions (optimize.discard-form-actions) (#4416) - ba2846cba M4: a recovered document's first Save asks where instead of writing the sidecar's path (#3923) (#4327) - 5588db4b6 M10: (n) names the page at position n, whatever its label, in page ranges and the page box (print.page-range) (#4282)
alf added 797 commits 2026-11-09 11:25:27 +00:00
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
The viewport minimum was 820x520 points, wider than half of a 1440 px
screen. It is now MIN_WINDOW_SIZE = 480x520 points. At that width the five
mode tabs were hidden under the search field, so the mode bar folds them
into one menu named after the current mode, shrinks the search field and
turns it into an icon button when it gets too narrow. Notice bars stack
their buttons under the message in a narrow document area.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
The rail's page-navigation cluster measures 360 pt since the two view-history
buttons landed; RAIL_BOTTOM_H was 316, so in a short window the cluster
overflowed its allocation and overlapped. The min-window test now uses the
minimum height and checks the page box, history and zoom buttons.

a_quick_flick_on_either_edge_rewraps_the_paragraph grabbed the widened edge 2 px
outside the box, at the limit of the grab zone; the rail's scroll area moved the
canvas width by 3 px and the press missed. It now grabs 1 px outside.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Resolve the right_rail conflict in chrome.rs: keep the scrolling rail and the
RAIL_BOTTOM_H cluster sizing, take main's editable zoom field.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Resolve the ROADMAP.md progress-log conflict by keeping both sides' entries.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
After merging main's editable zoom field, get_by_role(TextInput) in the min-window
test found two nodes. Check every text field of the rail sits below the rail buttons.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Upstream dropped the Read mode tab and added its own scrolling rail (#3282);
keep upstream's rail, keep the PR's narrow-window mode menu, and move the
min-window tests into the tests/it binary (rail test dropped, covered upstream).

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
UserRegex::find now returns an error instead of silently stopping at
10,000 matches, so redact_mark cannot report success while later matches
stay readable. Empty-match detection probes several short texts so
patterns like \b are refused with the clear message. Schema notes that
the 1024 limit is in bytes.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
The narrow-window mode menu fell back to "All tools" while Read was active.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
ObjectReader::try_get measured every parsed object in its cache (up to 128)
after each read to check the byte budget. Keep a running total instead and
measure the whole cache only when something other than one freshly parsed
object changed it. Eviction timing is unchanged, so output is identical.

Fixes #4867

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Reconstruct parsed every object to see whether it is an object stream, and
searched each object's next 1,024 bytes and the whole file byte by byte.

Only objects whose dictionary holds ObjStm or a # (a name can hide letters as
#xx) are now parsed for that check. Every /XRef position is found once, and
find() scans for the needle's rarest byte in 32-byte blocks. The index, trailer
and repair log are unchanged.

Fixes #4868

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
write_full parsed each object once to number it and again, after the
128-object reader cache had dropped it, to write it. Keep the parsed object
from the traversal and write that, within a budget of 64 MiB of owned bytes
and 2M tree nodes; past it the object is read again as before, which keeps
the memory bound and the "read differently while saving" check.

Fixes #4869

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
serialize wrote every byte of a hex string, every integer and every object
number through write!. A table lookup and a small digit loop give the same
bytes in a fraction of the instructions. The octal escape in literal strings
and the #xx escape in names use the same helpers.

Fixes #4870

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
The running total trusted cache length and pointer equality to mean that
a read only added the requested object. A read whose nested loads made the
editor cache evict (it then keeps only the newcomer) leaves the length
unchanged, so a large string stayed cached past the 8 MiB budget. Count
the inserts a read makes instead; anything but a hit or exactly one insert
measures the whole cache again.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
document.rs was split into modules upstream; the running total and the
insert counter move with ObjectReader into document/state.rs.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
The pre-check cut the dictionary at the next object header or at a
'>>' + 'stream' byte pattern, so text inside a string could hide a later
/Type /ObjStm. It now reads the dictionary as the parser does (strings,
hex strings, comments, nesting) and treats an object whose end is not
certain as a candidate, so the reconstructed object set stays the same.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
document.rs was split into modules upstream; the reconstruct changes move
into document/reader.rs and the unit test into document/tests.rs.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Also count name and dictionary-key bytes in owned_bytes (keys twice in an
indexed dictionary), so the 64 MiB retention budget of write_full also
covers a huge name or key. owned_bytes moves to document/state.rs with the
upstream split.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
unlink_type3 (#4579) unlinks the Type 3 glyph programs that nothing
shows any more. It found what is still shown by scanning page content
and the forms reached through /XObject only. Annotation appearances,
tiling pattern cells and soft mask groups draw too. A glyph only they
showed counted as unused, so its program left /CharProcs, or the whole
font lost /CharProcs and left the page's /Font. This ran on every Apply
Redactions, even when the mark covered text in another font.

The scan now also walks the appearance streams of each page's
annotations, the tiling pattern an scn or SCN selects and the group of
the soft mask a gs sets. Each is scanned once per stream, font and
holder, through its own /Resources or its caller's, with no current
font. Forms take the same path through scan_stream_object.

Three tests, one per drawer. All three fail without the fix.
canvas.rs was split upstream; notice_bar now lives in canvas/bars.rs, so the stacked narrow layout is re-applied there.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
The doc_optimize body moved from automation/src/lib.rs to documents.rs upstream; the target_bytes ladder is re-applied there. The parity evidence keeps upstream's new optimize_ui path and adds the target-size test.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Both sides added modules to crates/redact/src/lib.rs (user_regex here, type3 upstream); keep all of them.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
An annotation's /AP can hold entries that no reader draws. Only the
normal, rollover and down appearances (N, R, D) now keep a Type 3 glyph,
so an extra entry such as /X can no longer keep a redacted glyph program
in the saved file.

Rewriting a page also dropped a Type 3 font that only an annotation
appearance without /Resources selects, although that appearance draws
through the page's resources. The page prune now reads those appearances
too. It reads them only when the page lists a Type 3 font, so other files
are not refused over an appearance it cannot parse.

Two tests, both failing without this change.
Keep the upstream monochrome optimizer options alongside target_bytes.
The page font prune counted the page content, forms without /Resources
and annotation appearances, but not tiling patterns or soft mask groups
without /Resources. Those draw through the page's resources too, so a
Type 3 font only they selected left the page and their text lost its
font. They are now read as well, only when the page lists a Type 3
font.

Two new 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.
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.
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.
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>
Co-Authored-By: Codex <noreply@openai.com>
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Annotation appearances draw through a page's resources, never through a
form's own, so the form path passes no page. The form's own patterns
and soft masks are still counted. Asked in review.
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
The image allowance charged a Flate stage the bytes it made, and its input
only as what the stage before it made. Inflating does work for every block it
reads, whether or not the block makes anything: a stream of empty
fixed-Huffman blocks, ten bits each, took 1,383 ns a byte to make nothing
(miniz_oxide 0.9.1, release), one of a literal each 768 ns a byte, empty
dynamic blocks 106, against the 3 ns a step budget.rs states. A 250-byte
stream of four Flates over empty blocks took 47 s and was charged about
34 million steps, so a review of #5698 worked out about 7 hours before a
240 KB file of them was refused, through clearing, Optimize, export, compare
and redaction's images and dictionaries alike.

Where decoding is paid for (decode_tolerant_within, decode_within), a Flate
stage now pays FLATE_READ_WORK, 128 steps, for each byte it reads, before it
runs: a stage the allowance can't pay for doesn't start. Per byte read, the
worst of the shapes measured comes to about 11 ns a step at that price, inside
what a JBIG2 step takes. The raw retry of a zlib header that only looked valid reads the
bytes again, so it pays again, makes no more than is left once it has, and is
refused when it can't be paid for. Byte counts for a byte allowance
(decode_counted, decode_tolerant_counted, as a page's content uses) are
unchanged.

With a tiny file's allowance (2^34 steps), the 250-byte stream is decoded
three times in 141 s, and the fourth stops before its last stage reads the
empty blocks (8.2 ns a step, release). Data an encoder makes took 0.4 to 3.2 ns a byte it
made, reading included, and each byte of a file adds 64 times the price to an
operation's allowance.

Tests: a_flate_stage_pays_for_what_it_reads (empty blocks: 129 steps a byte,
refused a step short with nothing inflated) and a_second_reading_is_paid_for;
the exact costs in filters, cos and optimize tests add what Flate reads.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01AmsdeUCdTaD72jkDw2pJKY
Mutation testing of the commit before this one left the preset-dictionary check in flate.rs
alive: the test's bytes after the header fail to inflate either way. A header
with FDICT set followed by data that inflates as raw deflate is refused too.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01AmsdeUCdTaD72jkDw2pJKY
The Resources review of the commits before this one found that pricing what Flate reads at 128
steps a byte refused real redactions. Redaction decodes a picture once for
each page that draws it, so 240 pages sharing a 1 MB scan-like Flate picture,
a mark on each, went from Ok (240 cleared, 20.3 s) to ImagesPastAllowance, its
allowance paying for 188 decodes: each paid about 27 times what it had.

The cost being priced is miniz_oxide's: about 1.7 us for every fixed-Huffman
block, empty or not. flate2's zlib-rs backend took about 11 to 13 ns a fixed block,
and the Windows build already used it (craft-update turns
on zip's deflate-flate2-zlib-rs, and with both backends on, flate2 inflates
with zlib-rs, measured). Every platform now builds flate2 with zlib-rs, which
it prefers to the miniz_oxide other dependencies still build.
In a release build (zlib-rs 0.6.8), per byte read: empty fixed blocks 9 ns,
fixed blocks of one literal 6, empty dynamic 21, empty stored 1.3, against
1,383, 768, 106 and 2 before. Each backend inflating what its own
encoder made, 16 MiB of text took 2.6 ms against 6.3 to 13.7, and a
scan-like picture 38 ms against 53.

FLATE_READ_WORK is now 2: per byte read, the worst shape comes to about 7 ns a
step, and a picture an encoder made pays twice its stored length more than unpriced
(an_image_an_encoder_made_pays_little_for_its_reading: at most half again).

The same review: export and compare's Office export read an indexed image's
lookup table outside the allowance, once per image, even with nothing left
(1.4 s each for 1 MiB of empty blocks under miniz_oxide). It is read through
ImageBudget::decode_filters now, and refused past it
(a_lookup_table_is_read_within_the_exports_allowance).

filters, cos, optimize, redact, create, edit, export and engine pass with the
new backend.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01AmsdeUCdTaD72jkDw2pJKY
The Consumers review of the commit before this one: zlib-rs refuses a back-reference to before
the start of the output, which miniz_oxide filled with zeros and went on
from. Read strictly such a stream is now refused, and read tolerantly what
came before it is kept, as zlib has it. flate.rs's quirks and the filters
README say so, and a_copy_from_before_the_start_is_damage checks it, which
also fails under miniz_oxide, so it shows which backend runs: the read price
is set for zlib-rs.

The same review: other dependencies still build flate2's default miniz_oxide
backend, and zlib-rs runs because flate2 prefers it when both are built, not
because it is the only one. The Cargo.toml comment says so. The miniz_oxide
measured was 0.9.1, flate2's (0.8.9 is png's); FLATE_READ_WORK's doc says so.
The filters README named miniz_oxide as the backend.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01AmsdeUCdTaD72jkDw2pJKY
The Claims review of the branch: the docs said miniz_oxide "builds a block's
tables for every block", which a timing doesn't show. They now say what was
timed: about 1.7 us for every fixed-Huffman block, empty or not. "The worst
comes to about 7 ns a step" was per byte read; a stage's start, about 0.25 us
under zlib-rs (1.6 us for eight over an empty stream, release), isn't priced,
since every way to repeat one takes bytes of the file. The letterhead test's
doc promised more than it checks.

The same review found a second shape zlib-rs refuses where miniz_oxide read
on: a dynamic block whose code has no end-of-block symbol. flate.rs's quirks
and the README row say so, and a_dynamic_code_without_an_end_is_damage checks
that nothing of it is kept.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01AmsdeUCdTaD72jkDw2pJKY
CodeRabbit on #5825: a JPEG that isn't inverted read its colour space to
know whether it is CMYK, and then, with a soft mask, a stencil mask or a colour key keeping it
from being written as it is, read it again for its samples. An indexed one
(not valid, written as it is in the end) paid for its lookup table twice,
and could be refused with an allowance that covers the image and one reading.
The first reading is kept and used.

a_jpegs_lookup_table_is_paid_for_once: an indexed JPEG with a soft mask pays
one reading of its table, and is written as it is.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01AmsdeUCdTaD72jkDw2pJKY
draw_value_text re-measures overflowing text up to six times to shrink it, and
a repeating subform draws its text once per instance, with no shared cache. A
small file with large text drawn across many instances therefore cost one full
text layout per instance per pass, for seconds of work, and finished with a
valid layout, so neither the page limit nor the measurement budget stopped it.

Each pass over a draw's text now counts its characters against a new draw budget
(MAX_DRAW_CHARS), the way the measurement budget bounds container sizing. A real
form's draws stay far below it; a template that lays out far more text stops with
TooLarge. The budget is reduced under test so the guard is exercised without
laying out hundreds of megabytes.

Test: large_text_drawn_in_many_instances_spends_the_draw_budget (fifty instances
of a 20,000-character draw give TooLarge; one copy lays out); it fails without
the cap.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Pq7HyG234xeYkJfr41E6iP
CodeRabbit's follow-up on #5825: the indexed JPEG test ran only with an
unlimited allowance, so nothing checked that a refused lookup-table read
leaves a JPEG out (`a_lookup_table_is_read_within_the_exports_allowance`
checks a Flate image). The test now measures what the soft mask costs
alone (no colour space, so nothing is read after it) and allows a step
less than the mask and the table: the export is refused with the
reason, and only the table is refused, nothing after it. A step short
of the mask, the mask alone is refused, so the mask is read first and
the case above is refused at the table.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01AmsdeUCdTaD72jkDw2pJKY
Thai has no spaces between words, so rewrapping treated a whole Thai run as
one word and Add text broke rows at any shaped cluster, splitting words such
as ประปา or สะอาด across rows.

Find the words with ICU4X's dictionary line segmenter (icu_segmenter 2.3,
compiled dictionary data, no LSTM model; Unicode-3.0). linebreak::breaks
gives every place a row may end inside a run without spaces: the existing
Chinese and Japanese rule plus the dictionary's breaks next to Thai. A row
never starts with a following vowel, a tone or other mark, Mai Yamok or
Paiyannoi, nor ends with a leading vowel, whatever the dictionary says, and
the CJK pair rule now honours the same Thai marks.

- text.rs wrap: unchanged; linebreak::pieces now cuts Thai too.
- text_regions.rs: breaks are worked out once per run of non-space
  characters instead of pair by pair, since Thai needs the whole run.
- added.rs thai_lines: clusters are grouped into words and whole words
  wrap; only a word wider than the row breaks between clusters, as before.
  The spaces where a row ends are dropped, as in the Latin wrapper.

Rewriting a paragraph into Thai still needs a source font with the Thai
glyphs; there is no Thai fallback face for it yet. The compiled dictionaries
(Thai, Lao, Khmer, Burmese) add about 1.8 MB to a stripped release binary.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_018cECVMvRnc9mHNFfVp7nXd
A Thai Add text paragraph that opens with spaces held a row of only those
spaces. When the first word did not fit after them, that row was ended
(trimmed to an empty row) and the opening spaces were lost. A row of only
spaces now counts as empty when deciding to end it, so the first word stays
on the first row after the spaces, or breaks between clusters there.

Reported by the CodeRabbit review on #3492; the new case fails without the
fix (at 16 pt the rows were "", "ป", "ระ", ...).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_018cECVMvRnc9mHNFfVp7nXd
A first word that fit a row on its own but not after the spaces opening
its paragraph was placed whole and ran past the row's end. Whether the
word is too wide now counts the width those spaces already use, so it
breaks between clusters on the first row instead.

Reported by the CodeRabbit review on #3492. The new case fails without the
fix: "      ประปา" stayed one row, wider than its 52 pt box. shaped_width is
pub(crate) so the test can measure rows.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_018cECVMvRnc9mHNFfVp7nXd
icu_segmenter_data compiles the Thai, Lao, Khmer and Burmese word
dictionaries (data/segmenter_dictionary_extended_v1.rs.data, 1,773,172
bytes by the file's own count) into the binary, so they get a [[bundled]]
entry per AGENTS.md §1.3, and NOTICE carries the Unicode licence's
copyright and permission notice, which it requires in the documentation.
The line segmenter loads only these dictionaries; the Chinese and Japanese
one is not linked.

The data is under Unicode-3.0, which deny.toml already allows for code but
the asset allowlist (AGENTS.md §1.2, xtask ALLOWED_LICENCES) did not list,
so this adds it there. That is a policy change for the maintainers to
accept; without it `cargo xtask assets` refuses the entry.

Requested in FredApps' review of #3492.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_018cECVMvRnc9mHNFfVp7nXd
The license-check gate (xtask ci) failed on two things this PR caused:

- NOTICE changed, so its recorded hash in LICENSES.toml no longer
  matched. The ICU4X notice text was re-audited against the
  icu_segmenter_data 2.3.0 crate's LICENSE: the copyright line, the ICU
  portions line, and the permission, warranty and name paragraphs match it
  word for word. NOTICE now also points to the full licence; the hash is
  updated.
- The bundled icu_segmenter_data dictionary had no licence-document
  mapping. licenses/ICU4X-LICENSE.txt is the crate's LICENSE, unchanged
  (Unicode-3.0, origin the crates.io archive), mapped as a [[document]] and
  from a [[bundled]] entry for the dictionary file.

`cargo xtask license-check` now passes.

Requested in FredApps' review of #3492.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_018cECVMvRnc9mHNFfVp7nXd
Fixes #5955

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
unlink_doomed and unlink_from_forms asked, name by name, whether the
remaining content still draws an XObject, and each question parsed the
content and decoded every form without /Resources of its own again. A
holder with many aliases of one removed image, drawn by a large
inheriting form, multiplied that decoding by the number of aliases.

names_drawn now works out once per content which names it draws,
directly or through such forms (each read once), and whether any of it
could not be read; every alias is answered from that set. A name not in
the set while something could not be read still fails closed.
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
A font that only the interactive form's default resources hold
(/AcroForm /DR, ISO 32000-1 12.7.2) draws nothing, yet Verify PDF/A
reported it as not embedded, so a compliant form failed PDF/A-2b and 3b
on clause 6.2.11.4. Only the fonts a resource dictionary names (a page's,
a form XObject's or appearance stream's, a pattern's or a Type 3 font's)
are now checked; an appearance stream that draws with a /DR font names
it in its own /Resources and still has to embed it.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Pq7HyDBXygTf7jcqdsjcfn
A /Font entry can point at an object that is nothing but a reference to
another (ISO 32000-1 7.3.10). The fonts something draws with were
recorded by the entry's own reference, so the font dictionary at the end
of such a chain went unchecked. Follow the chain, as resolve does, up to
32 references. (CodeRabbit review on #6172.)

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Pq7HyDBXygTf7jcqdsjcfn
Word export named no language, so Word checked the spelling of every
export as English and underlined a whole document in any other
language. The PDF already says its language in the catalog's /Lang.
When that is a well formed language tag, Word gets it as the default
language of the text (w:docDefaults), in the attribute Word keeps for
its script: w:eastAsia for Chinese, Japanese and Korean, w:bidi for
Arabic, Hebrew, Persian and Urdu, w:val for the rest. Both HTML
exports, with and without CSS, get <html lang>. Without /Lang, or with a
malformed one, the files are exactly as before.

docx_with_language, html_with_language and html_simple_with_language
take the language; docx, html and html_simple stay as they are and call
them with none. The engine reads /Lang through the existing initial
view.

The new export and engine tests fail without the change.
BCP 47 sets no maximum length; 35 characters is only the least a protocol
must hold. A well formed tag such as en-US-u-ca-gregory-nu-latn-co-phonebk
was left out of both exports. The bound is now 255, kept only so a PDF's
/Lang can't put an unbounded string in the file.
A singleton (one letter or digit) starts an extension, or with x private
use, and BCP 47 needs a subtag after it. en-x and en-a were written as the
document's language. They and a singleton straight after another are now
left out; one-character subtags inside private use stay allowed.
pa-Arab-PK is Punjabi in Arabic script, which Word keeps as right-to-left
text, but word_lang looked only at the language and wrote w:val. A script
subtag (Arab, Hebr, Syrc, Thaa; Hans, Hant, Hani, Jpan, Kore, Hang, Hira,
Kana; Latn, Cyrl, Grek) now decides over the language. It is read only in
its own place, after the language and any extended languages, so private
use such as en-x-arab names no script.
PDF/A-2 6.1.13 keeps a file within the limits of ISO 32000-1 Annex C
(Table C.1 and the page sizes after it). Verify PDF/A now reports an
integer outside 32 bits, a string over 32767 bytes, a name over 127 bytes
(counted decoded, so #xx escapes don't inflate it) in an object or in
content, and a page boundary under 3 or over 14400 units, read after
inheritance so a page's own box overrides its parent's. Character
identifiers over 65535 are not checked yet.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Pq7HyDBXygTf7jcqdsjcfn
Separation spaces of one colorant name must agree on their alternate
space and tint transform, inside a DeviceN space's /Colorants too, and a
DeviceN space must name each spot colorant it uses in its /Colorants
(ISO 32000-1 8.6.6.5, Table 71). Process colorants (Cyan, Magenta,
Yellow, Black and an NChannel space's /Process components) and None need
no entry. Verify PDF/A now reports both.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Pq7HyDBXygTf7jcqdsjcfn
From the review of #6202:
- the trailer is scanned with the indirect objects;
- a page's /Contents streams are read joined, as one content stream:
  operands may end one stream and their operator start the next
  (ISO 32000-1 7.8.2);
- values are followed as deep as the parser nests them (128);
- content is decoded within one 256 MiB allowance for the document;
- a page box whose size overflows to infinity is reported;
- the test covers a long name used as a content operand.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Pq7HyDBXygTf7jcqdsjcfn
Each pass over a draw's text counted only its own characters, so a short
text embedding a long field value, drawn in many instances, never spent the
budget. The embedded values each pass lays out are counted too.
(CodeRabbit review on #5852.)

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Pq7HyDBXygTf7jcqdsjcfn
From the review of #6203:
- Separation definitions are compared as objects, references followed at
  any depth (to 32), so identical streams under different numbers agree;
- All and None are compared like any other colorant name;
- colorant names and /Process components given by reference are resolved;
- /Process exempts its components only in an NChannel space;
- a null /Colorants entry, or one referring to no object, defines nothing;
- colour spaces are found as deep as the parser nests them (128).
Corpus results are unchanged (15 of 23 6.2.4.4 files right, no new false
failure).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Pq7HyDBXygTf7jcqdsjcfn
Upstream now counts the parsed tree and keys in held_bytes and tracks the cache total itself,
which replaces this branch's copy of the cache accounting and its owned_bytes. write_full's
retention budget uses held_bytes; the separate node budget is dropped since held_bytes counts nodes.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
With embedded values counted, the 1 MiB test budget can stop this draw
before its last pass; the bound on what is copied out still holds.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Pq7HyDBXygTf7jcqdsjcfn
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Pq7HyDBXygTf7jcqdsjcfn
Finishes the clause. Table C.1 of ISO 32000-1 Annex C bounds real values
at about ±3.403 × 10^38 and character identifiers at 65535. A real past
that range (an integer token too long even for a real reads as one) is
reported, and so is an embedded CMap of a Type 0 font, or one it uses
through /UseCMap, whose begincidrange entry reaches past CID 65535 or
whose begincidchar entry names one (ISO 32000-1 9.7.5.4). A CMap named
rather than embedded is a predefined one, within the limit. The CMaps are
decoded within one 16 MiB allowance.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Pq7HyDBXygTf7jcqdsjcfn
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Pq7HyDBXygTf7jcqdsjcfn
A draw's fit passes ran on after one had spent the budget, each a full
measure, before the draw was refused; each pass now checks the budget before
it starts and after it ends. The final layout pass, which expands the
embedded values once more, is checked too, so the draw that crosses the
budget there is refused even when it is the form's last. (CodeRabbit review
on #5852.)

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Pq7HyDBXygTf7jcqdsjcfn
A /Colorants entry counts only as a Separation space of its own colorant
(ISO 32000-1 8.6.6.5, Table 71). Tint-transform and other streams compare
by decoded data, so the same function stored under another filter is the
same. Equal reference pairs are remembered and comparisons are bounded,
so a structure shared by many definitions is compared once.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Pq7HyDBXygTf7jcqdsjcfn
The file must start with %PDF-1.0 to %PDF-1.7 and an end-of-line marker,
followed by a comment of % and four bytes above 127. A document whose
next save is a full rewrite is judged by the header that save writes.
Save as PDF/A makes the next save full, at version 1.7 when the kept
version isn't 1.0 to 1.7.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Pq7HyDBXygTf7jcqdsjcfn
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Pq7HyDBXygTf7jcqdsjcfn
A graphics state may not have /TR, may have /TR2 only as /Default, may
use halftones of type 1 or 5 only and without /HalftoneName (also the
colorant halftones of a type 5 one), and a rendering intent from ISO
32000-1 Table 70. Graphics states are read from every /ExtGState
resource dictionary and from shading patterns.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Pq7HyDBXygTf7jcqdsjcfn
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Pq7HyDBXygTf7jcqdsjcfn
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Pq7HyDBXygTf7jcqdsjcfn
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Pq7HyDBXygTf7jcqdsjcfn
An engine test converts files without a binary comment and with a 2.0
header, saves them through Session::save_bytes and verifies the
reopened bytes; the preflight test adds a good version with a short
comment, and a good header that conversion leaves alone.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Pq7HyDBXygTf7jcqdsjcfn
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Pq7HyDBXygTf7jcqdsjcfn
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Crop pages moves into the hotbar's core tool buttons, where main now builds the quick bar.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Keeps main's reworded mono-image text and the target_bytes paragraph in the doc_optimize description.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Graphics states come from /Resources and /DR entries and from shading
patterns, never from private /PieceInfo data, and maps shared by
reference are read once. A halftone without a /HalftoneType, a
rendering intent that isn't a name, and a halftone /TransferFunction
where it isn't allowed or is missing (non-primary colorants of a type 5
halftone need one; ISO 32000-1 Table 130) are reported. A null entry
counts as absent.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Pq7HyDBXygTf7jcqdsjcfn
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Pq7HyDBXygTf7jcqdsjcfn
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Pq7HyDBXygTf7jcqdsjcfn
PDF Association TechNote 0010 A018: PDF/A-2 does not allow the nonzero
reals that ISO 32000-1 Table C.1 lets a reader round to zero.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Pq7HyDBXygTf7jcqdsjcfn
Objects are followed from the trailer through a queue of references,
never into /PieceInfo, so an object only private data refers to is not
read as a graphics state holder. /HalftoneType must be an integer
(ISO 32000-1 Table 128); a real of the same value is reported.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Pq7HyDBXygTf7jcqdsjcfn
Document::get fetches by object number, so references that differ only
in generation reach the same object; the reached and read sets now go
by number, and each object is walked once.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Pq7HyDBXygTf7jcqdsjcfn
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Pq7HyDBXygTf7jcqdsjcfn
Gray, Red, Green and Blue are primary colour components as well as Cyan,
Magenta, Yellow and Black (ISO 32000-1 10.5.5.6, ISO 32000-2 10.6.5.6):
their halftones need no transfer function and may not have one.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Pq7HyDBXygTf7jcqdsjcfn
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Pq7HyDBXygTf7jcqdsjcfn
The trailer the last startxref points to (an update's, or a linearized
file's first-page trailer) must have an /ID of two non-empty strings
(ISO 32000-1 14.4). Save as PDF/A gives a document without a good one a
new identifier, made by the writer's generator, now public in cos as
new_file_id.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Pq7HyDBXygTf7jcqdsjcfn
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Pq7HyDBXygTf7jcqdsjcfn
The PDF 1.2 SetState and NOP actions, which ISO 32000-1 no longer
defines, are forbidden, and a Named action may only name NextPage,
PrevPage, FirstPage or LastPage (ISO 32000-1 Table 212). Save as PDF/A
removes the others as it does other forbidden actions.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Pq7HyDBXygTf7jcqdsjcfn
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Pq7HyDBXygTf7jcqdsjcfn
The first /ID string is the file's permanent identifier (ISO 32000-1
14.4); when it is a non-empty string, conversion keeps it and makes only
the second afresh. The test also checks that a good /ID is left as it is.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Pq7HyDBXygTf7jcqdsjcfn
A Named action whose /N refers to /NextPage (or another page move) is
allowed and survives Save as PDF/A; the test checks that conversion
keeps such an action.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Pq7HyDBXygTf7jcqdsjcfn
The first /ID string is kept only when a conforming file can hold it:
non-empty and within 32767 bytes (ISO 32000-1 Annex C). The report says
"Completed the file identifier" when it was kept and "Added a file
identifier" when both strings are new.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Pq7HyDBXygTf7jcqdsjcfn
An action whose /S refers to /Named or /NOP is checked like one that
names it directly, so verification reports it and conversion removes it.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Pq7HyDBXygTf7jcqdsjcfn
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Pq7HyDBXygTf7jcqdsjcfn
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Pq7HyDBXygTf7jcqdsjcfn
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Pq7HyDBXygTf7jcqdsjcfn
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Pq7HyDBXygTf7jcqdsjcfn
Properties of the Dublin Core, XMP Basic, Rights Management, Media
Management, Basic Job Ticket, Paged-Text, Dynamic Media, Adobe PDF,
Photoshop and Camera Raw namespaces must have the types ISO 16684-1
clause 8 and XMP Specification Part 2 give them: simple values by their
lexical form, arrays by kind and items, structures by their fields, in
every RDF/XML form a value can take. xmp::parse is shared with the
identification read.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Pq7HyDBXygTf7jcqdsjcfn
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Pq7HyDBXygTf7jcqdsjcfn
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Pq7HyDBXygTf7jcqdsjcfn
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Pq7HyDBXygTf7jcqdsjcfn
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Every metadata stream is read, within one decode allowance, not only the
catalog's. Predefined properties inside a Pantry item keep their types;
rdf:value may be an attribute; rdf:resource makes a URI, which only a
text type takes; a value's text is all its text nodes; an array item is
rdf:li; a parseType="Resource" structure has no character data; a time
zone is Z, +hh:mm or -hh:mm; and a value nested past every type's depth
doesn't have its type.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Pq7HyDBXygTf7jcqdsjcfn
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Pq7HyDBXygTf7jcqdsjcfn
A metadata stream is decoded by its byte order mark (UTF-16 big- or
little-endian, or UTF-8; ISO 16684-1 7.3) before it is parsed. A GUID
is a simple value but not a URI (ISO 16684-1 8.2.2.3), so the
document and instance IDs of xmpMM, stRef and stEvt don't take
rdf:resource.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Pq7HyDBXygTf7jcqdsjcfn
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Pq7HyDBXygTf7jcqdsjcfn
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Pq7HyDBXygTf7jcqdsjcfn
The TIFF, EXIF and Additional EXIF namespaces come from the September
2005 XMP Specification that PDF/A-2 cites ("EXIF Schemas", pages 58-70),
with its EXIF value types (pages 83-87): Rational as numerator/
denominator, GPSCoordinate as DDD,MM,SSk or DDD,MM.mmk, and the Flash,
OECF/SFR, CFAPattern and DeviceSettings structures.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Pq7HyDBXygTf7jcqdsjcfn
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Pq7HyDBXygTf7jcqdsjcfn
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Pq7HyDBXygTf7jcqdsjcfn
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Pq7HyDBXygTf7jcqdsjcfn
Qualifiers beside an rdf:value that are predefined properties keep their
types; rdf:value may be an attribute of an empty property element; an
rdf:resource element is empty; and character data around array items or
in an rdf:Description structure breaks the value, as it already did in a
parseType="Resource" structure.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Pq7HyDBXygTf7jcqdsjcfn
An rdf:parseType or rdf:resource on a property element makes its value a
resource or another literal, not the rdf:Seq, rdf:Bag or rdf:Alt it holds.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Pq7HyDBXygTf7jcqdsjcfn
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Every pdfaExtension:schemas description needs its entries: a schema its
schema, namespaceURI and prefix; a property its name, valueType,
category and description; a value type its type, namespaceURI, prefix,
description and field; a field its name, valueType and description.
Each description namespace is written with its own prefix. One pass over
the metadata streams serves this check and the value types.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Pq7HyDBXygTf7jcqdsjcfn
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Pq7HyDBXygTf7jcqdsjcfn
All five extension schema container namespaces are written with their
own prefixes, pdfaExtension included (PDF Association TechNote 0009,
4.1). The module comment cites TechNote 0009 section 4 for the schemas,
entries and prefixes, and says where PDF/A-2 asks more than it does.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Pq7HyDBXygTf7jcqdsjcfn
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Pq7HyDBXygTf7jcqdsjcfn
A qualified value has exactly one rdf:value; a parse type other than
Resource, which XMP doesn't use (ISO 16684-1 7.9.2.5), breaks the value;
and an rdf:resource element holds no element content in any form.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Pq7HyDBXygTf7jcqdsjcfn
# Conflicts:
#	crates/preflight/src/tests.rs
# Conflicts:
#	crates/preflight/src/tests.rs
A schema's property or value type list, or a value type's field list,
written more than once is read each time; and a pdfaExtension:schemas
that isn't one rdf:Bag (TechNote 0009, 4.2) is reported.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Pq7HyDBXygTf7jcqdsjcfn
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Pq7HyDBXygTf7jcqdsjcfn
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Pq7HyDBXygTf7jcqdsjcfn
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Pq7HyDBXygTf7jcqdsjcfn
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Pq7HyDBXygTf7jcqdsjcfn
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Pq7HyDBXygTf7jcqdsjcfn
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Pq7HyDBXygTf7jcqdsjcfn
Word and PDFMaker write an Arial they don't embed as a Type 0 CIDFontType2
with Identity-H, often without /ToUnicode, so each code is a glyph index in
the author's Arial. hayro drew such a font with the standard CFF substitute
and, with no Unicode route, took each code as one of the substitute's own
glyph indexes: "Low Power" drew as "@ck 'Dck Yf".

Arial numbers its first 258 glyphs in the standard Macintosh order of the
TrueType post table (L is 47, o 82, w 90). For a non-embedded CIDFontType2
whose descendant /BaseFont is one of Arial's four faces, and whose CIDs are
not of an Adobe character collection, the CID is now taken through its
/CIDToGIDMap to that order's glyph name, and the substitute draws its glyph
of that name (.notdef when it has none). Arial Narrow, Black, Unicode MS
and Rounded, and CIDFontType0 fonts, keep the old path. The names come from
read-fonts' post::DEFAULT_GLYPH_NAMES, already in the tree through skrifa.

Vendored hayro-interpret patch 76.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
is_arial compared the family and style without regard to case but
stripped the MT suffix case-sensitively, so arialmt and Arial-Boldmt
weren't read in Arial's glyph order (CodeRabbit review on #6210). The
suffix is now stripped in any case, slicing only at a char boundary.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Its explanation becomes the test's doc comment (CodeRabbit docstring
coverage on #6210).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
ISO 32000-2 7.3.8.2: Length "indicates how many bytes of the PDF file are
used for the stream's data". cos and hayro-syntax used /Length only when
`endstream` followed it, and otherwise searched for the keyword; with
none left in the file the stream was an error and its object null. A
producer that writes `endstrea` after every stream lost every font and
every page's content, and its pages drew blank with no warning. With no
`endstream` left, a /Length the file holds is now the stream's extent.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Pq7HyDBXygTf7jcqdsjcfn
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Pq7HyDBXygTf7jcqdsjcfn
A stencil mask (/ImageMask) painted with a pattern was drawn through a
luminance mask, which vello_cpu only takes at canvas size: every draw
rendered, converted and composited a full-page mask. And a tiling
pattern's cell pixmap was drawn again for every fill, each copy kept
until the page was composited. pdf.js issue 8078 paints 73,047 16 x 16
stencils with one pattern whose cell is 3.5 MB at 36 dpi: the render
ran for 5 minutes and then failed to allocate.

hayro patch (30): the stencil is drawn under a clip of its device
rectangle (padded by two pixels for a stencil snapped to whole pixels),
the pattern in an isolated layer cut to the stencil by a layer holding
it composited with Compose::DestIn (ISO 32000-2 8.9.6.2: the pattern
shows where the mask's samples paint). A coloured tiling pattern's cell
is drawn once per setting of the pattern and cell scale (TilingCells,
eight entries per renderer). hayro-interpret patch (78) numbers each
setting of a tiling pattern (TilingPattern::instance).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Pq7HyDBXygTf7jcqdsjcfn
The raster cache key now includes the drawn size, so a soft mask drawn at
several sizes was decoded in full once per size: issue16263's rotated and
scaled copies hit four sizes and took about 10 s. Sizes are rounded up to a
power of two, and every averaged-down size is made from one cached
intermediate mask at most 4096 on a side, so the full mask is decoded once.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Pq7HyDBXygTf7jcqdsjcfn
Review follow-ups for hayro patch (30):
- The stencil coverage that cuts the pattern is drawn with in_type3_glyph
  cleared, as the sub-render it replaced was: inside a glyph a shrunk
  stencil was nearest-sampled and lost strokes thinner than the shrink.
- The bordered cell of a pattern whose step shows one cell (patch (20))
  goes through the same cache (Renderer::tiling_cell, keyed apart by
  one_cell), so its fills no longer draw and keep a cell each.
- A test pins a patterned stencil under a soft mask to the soft mask's
  opacity once.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Pq7HyDBXygTf7jcqdsjcfn
`has_larger_mask` read only /Width and /Height, while `ImageXObject::new`
also takes the abbreviations /W and /H, so a large mask sized that way
cached its image's raster without the drawn size and a later, larger draw
reused the small mask.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Pq7HyDBXygTf7jcqdsjcfn
With hayro-syntax patch 36 and its cos counterpart, a stream with no
endstream left in the file is the bytes its /Length gives, so the fixture's
`<< /Length 0 >>` stream without endstream now reads as an empty stream.
The test needs an object that cannot be read: a stream with no /Length
either still is one.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Pq7HyDBXygTf7jcqdsjcfn
A snapshot was drawn at the zoom on screen, so one taken with the page zoomed
out came out too coarse to read. It is now drawn at 300 ppi, or at the zoom
if that is finer, or as fine as the renderer draws in one tile: it stops at
4096 pixels a side and cuts a larger area short, which 300 ppi would reach on
any area over about 983 pt a side.
Review of #5761: the scale came from zoom × device pixels, without the
96/72 the viewer draws at, and was capped at 8, so a snapshot zoomed in
held fewer pixels than the screen showed: 417 against 534 for 100 pt at
400%. It now takes the viewer's own render scale, and the tile limit is
what stops it. The roadmap entry says when 300 ppi can't be reached.
A crypt filter whose /AuthEvent is /EFOpen needs its password only when
an embedded file is accessed (ISO 32000-2 7.6.6, Table 25). A document
whose strings and streams are in the clear (/StmF, /StrF /Identity) and
whose encrypting filters are all EFOpen ones, /EFF naming one, now opens
without a password in cos and in the renderer (hayro-syntax patch 36).

Without the password there is no file key: the embedded files' data is
held and saved as stored, and Document::embedded_files_locked and
reads_as_stored say so. Asking for such an attachment's bytes is an
error. A full save that would renumber data whose key mixes in the
object number (RC4, AES-128), or change the document's security, is
refused. The password still opens the document and its attachments.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Pq7HyDBXygTf7jcqdsjcfn
A font whose head.indexToLocFormat holds neither 0 nor 1 (256, the bytes
of 1 swapped) had its long loca offsets read as short ones, and every
glyph came out wrong: "LINE UP" drew as boxes. The format is now taken
from the loca table's length.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Pq7HyDBXygTf7jcqdsjcfn
Combine refuses to copy an embedded file stream that its source holds
encrypted (opened without the password its embedded files need), rather
than write that data into another document as the file. An engine test
opens and saves such a document through the session, and the parity
entry core.attachment-only-encryption records what this covers.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Pq7HyDBXygTf7jcqdsjcfn
The empty password opens an EFOpen-only document as no password does,
and the credentials of one opened without the key open no other. An
embedded file may name its EFOpen filter in a /Crypt filter of its own,
with no /EFF; handlers older than crypt filters (V1, V2) don't qualify
(hayro-syntax patch 36 too). Saves without the key refuse what they
would have to encrypt: a removed /Encrypt, or an edited or added
embedded file. The password given after opening unlocks the embedded
files, unless one read as stored has been edited.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Pq7HyDBXygTf7jcqdsjcfn
Room for numGlyphs + 1 long offsets means long, room only for short ones
means short (padding after them allowed), and a table too short for
either keeps the field as it is.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Pq7HyDBXygTf7jcqdsjcfn
Patch (77) was also taken by #4459, opened earlier; this PR's number moves
past the highest any open PR of ours or main uses (80, with 81 for #5632):
README row, code comments and the render tests that name it.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Pq7HyDBXygTf7jcqdsjcfn
Patch (79) was also taken by #3969, opened earlier; this PR's number moves
past the highest any open PR of ours or main uses (82): README row, code
comments and the render tests that name it.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Pq7HyDBXygTf7jcqdsjcfn
Patch (36) was also taken by #6290, opened earlier; this PR's number moves
to the next unused one in hayro-syntax (main's highest is 35): README row
and the code comments that name it.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Pq7HyDBXygTf7jcqdsjcfn
The CMap scan stopped at the first token the scanner couldn't read, so a
stray `>` after a CMap name, or a CMap name with spaces in it, ended it
before any bfchar entry: such fonts had no text to copy or find. The
scanner steps one byte past an unreadable token (hayro-postscript patch 2)
and the scan goes on, at most 1,024 times per CMap.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Pq7HyDBXygTf7jcqdsjcfn
parse_object_or_skip reads on from where a failed read stopped, stepping
one byte only when the read made no progress, so unclosed strings, each
reading to the end of the data, cost one pass over it instead of one each.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Pq7HyDBXygTf7jcqdsjcfn
ISO 32000-2 §9.10.2: a Type 0 font of the Adobe-GB1, -CNS1, -Japan1 or -Korea1 collection maps
code to CID to Unicode through the collection's UCS2 CMap. Only fonts that are not embedded took
that route; an embedded one without /ToUnicode, or with one that maps nothing for a code, gave no
text. /ToUnicode still comes first; the UCS2 CMap answers the codes it gives nothing for.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Pq7HyDBXygTf7jcqdsjcfn
A /MediaBox without area ([0 0 0 0]) is read as a missing one (the
default page), and a /CropBox without area, or with none in common with
the media box, as a missing one (the media box, ISO 32000-2 §14.11.2,
Table 33). Kept, the page's visible region had no area: nothing was drawn
and no text was extracted.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Pq7HyDBXygTf7jcqdsjcfn
A font whose /ToUnicode maps every code it shows never parses it.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Pq7HyDBXygTf7jcqdsjcfn
A Type 0 font's /ToUnicode written as a CID CMap (begincidchar /
begincidrange, destinations as numbers, as Word writes it) now gives each
number as the code's Unicode value for text, as glyph lookup
(map_via_unicode) already reads it and as pdftotext and PDFium extract it.
ISO 32000-2 §9.10.3 has beginbfchar / beginbfrange there; read as a bf map
only, such text extracted nothing.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Pq7HyDBXygTf7jcqdsjcfn
The previous commit was made on main's newer head with the older tree, so
it undid #977 and other changes from main. This commit puts main's files
back: the PR changes only vendor/hayro-syntax/src/page.rs, vendor/README.md
and crates/render/src/text/tests.rs again.

Review: a page's own box without area now gives way to an inherited one
with area before the default (A4 for a media box, the media box for a crop
box); the test adds a page whose own /MediaBox [0 0 0 0] inherits a larger
box with text past A4.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Pq7HyDBXygTf7jcqdsjcfn
Review: a /MediaBox or /CropBox read as missing because it has no area is
logged with the reader's other repairs (`warn!`).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Pq7HyDBXygTf7jcqdsjcfn
Review on #6359: the fallback tried byte lengths 0 to 3, and hayro-cmap
keeps entries by length 1 to 4, so an entry written with a four-byte code
was never found. It tries 1 to 4 now; the test adds a map written with
four-byte codes.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Pq7HyDBXygTf7jcqdsjcfn
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Pq7HyDBXygTf7jcqdsjcfn
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Pq7HyDBXygTf7jcqdsjcfn
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Pq7HyDBXygTf7jcqdsjcfn
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Pq7HyDBXygTf7jcqdsjcfn
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Pq7HyDBXygTf7jcqdsjcfn
A Type 3 font now keeps each glyph procedure it decoded, so a glyph drawn many
times isn't inflated again for every copy. A real file uses a one-glyph Type 3
font as a fill pattern and draws that glyph about 650,000 times on a page; it
rendered in 11.2 s and now in 5.9 s, pixel for pixel the same. A procedure over
64 KiB isn't kept, and the fonts of one InterpreterCache keep at most 32 MiB
together, as fonts live as long as the cache.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Pq7HyDBXygTf7jcqdsjcfn
CodeRabbit on #6409:
- a kept procedure holds its bytes within the page's content budget while the
  glyph is painted, as a decoded one does (context::hold_content), so a page
  skips the same glyphs once the budget is spent, kept or not;
- one decoded with no more than 64 KiB of budget left, which the budget may
  have cut short, isn't kept;
- each kept procedure counts its name and 64 bytes besides its bytes against
  the cache's 32 MiB.
Tests for the 64 KiB and 32 MiB limits and for two fonts with the same glyph
name.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Pq7HyDBXygTf7jcqdsjcfn
# Conflicts:
#	crates/cos/src/writer.rs
# Conflicts:
#	crates/cos/src/lib.rs
vello_cpu keeps every path's coverage (alpha) values in one buffer until
the scene is rasterized, and a strip packs its index into that buffer in
31 bits; Strip::new asserts it fits. A page that painted one form 2^22
times over (22 nested forms, each drawing the next twice) grew the
buffer past 2^31 values at about 7 GB and panicked, and the page failed.

- vello_common (2): StripStorage::room_for_path{,_within} says whether
  one more path's coverage, at most the whole surface counted twice,
  still fits below that limit or a smaller one.
- vello_cpu (1): the single-threaded dispatcher leaves out fills,
  strokes, fast rectangles and layer clips once there is no room,
  records it (RenderContext::coverage_full) and can be kept to fewer
  values (set_max_coverage).
- hayro (31): RenderSettings::max_coverage passes that limit down, and
  a page whose paths were left out reports
  InterpreterWarning::ContentTruncated, which PdfCraft already shows as
  content skipped past the page's safety budget.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Pq7HyDBXygTf7jcqdsjcfn
A simple Type 1 font without /ToUnicode whose glyphs are named only by a
number (`C39`, `C0032`, `MT72`, `G49`, as Windows printer drivers name
subset glyphs) extracted no text at all. ISO 32000-2 §9.10.2 gives such a
name no route to Unicode and leaves the reader to choose; pdftotext, PDFium
and MuPDF all read a glyph whose name's number (decimal or hexadecimal) is
the very code it is shown with as that code's Latin-1 character.

PdfCraft now does the same, and only then: a number naming another code
(`/C1` at 233, the counterexample on #3239) and control characters still
read as nothing, so the StandardEncoding guess #3239 removed stays out.

On the PDFBox JIRA attachments flagged for a text count far from
pdftotext's, three files (two Cnn built-in encodings, one Gxx hex /Differences)
now extract their full text, matching pdftotext and MuPDF.

Test: text::tests::numbered_glyph_names_read_as_their_own_code (fails with
the fallback reverted); glyph_names_outside_the_glyph_list_still_read_as_text
now expects `C39` at 39 and `C174` at 174 to read as `'` and `®`.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Hidden layers read only the default configuration's ON, OFF and
BaseState. A layer listed on there but turned off on screen by a usage
rule (/AS, ISO 32000-2, 8.11.4.4: the View event in the View category,
with the layer's /Usage /View /ViewState OFF) is hidden in viewers that
apply the rules, yet its content stayed.

Such a layer now counts as hidden. Rules that turn a layer on, or that
apply only when printing or exporting, leave it as the lists say, so the
removal never keeps less than it did.
Hidden layers took a removed layer out of the default configuration's
top-level ON, OFF, Order and Locked lists only. A sub-list of /Order, a
radio-button group (/RBGroups), a usage rule (/AS) and every alternate
configuration in /Configs still named it, so its dictionary, name
included, stayed in the full-rewrite save.

The removal now cleans all of these lists, through nested arrays, in the
default configuration and in every alternate one. A usage rule left with
no layer goes.
With a selection of a file's pages, Combine and Insert Pages from File
brought every bookmark title along, the ones whose page was left out
without a destination, and Combine brought every attachment.

Only the bookmarks that lead to a copied page, and those above them,
come along now, as Extract keeps them since #4041. Attachments belong to
the whole file: they come along with all of its pages only.
Delete Image takes only the Do out of the page, and Replace Image adds a
new name and leaves the old image listed. Remove Hidden Information only
pruned the names its own content pass replaced, so a deleted image stayed
in the page's resources and in the full-rewrite save.

Every run now unlinks the image and form XObjects that nothing draws from
the page-tree resource dictionaries that name them. A name stays when
anything drawing through the dictionary uses it with Do: the pages, their
annotations' appearances, and every form, tiling pattern, Type 3 glyph and
soft mask they reach, with /Resources of their own or not, since a name
those don't define is looked up in the page's. Going by name keeps more
than it must, never less. Each stream is read once against one decode
budget; a page or stream that won't decode, or a spent budget, leaves the
dictionary as it is. The page-tree walk is shared with unlink_doomed.
When a page lists a Type 3 font, prune_type3_from_resources (#5197) adds the
operators of the page's annotation appearances without resources of their
own. Each appearance was checked against the 2,000,000 cap on its own, so
several under it could add up to far more, and the prune went on with a
total no other path allows.

The appearances now share the running total, and passing it refuses the
redaction with the same kind of error as the other caps.
A `/ToUnicode` that gives one code both a `beginbfchar` / `beginbfrange`
entry and a `begincidchar` / `begincidrange` entry extracts the bf value
(ISO 32000-2 §9.10.3); the CID number is read only for codes no bf entry
covers. The case is checked with two-byte and one-byte entries: with
two-byte entries the CID lookup already falls back to bf entries at its
one-byte step, so the one-byte case is the one that fails if the CID
entries are read first.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
A 6.6.2.3.1 issue now says which metadata stream holds the property
("in metadata stream 5 0 R") and carries its ObjRef in Issue::object, so a
property mistyped in more than one stream is reported once per stream.
Wordings that name more than one thing use named holes, such as {object},
filled in one pass with the {} value, so a translation can reorder them.

Tests: the stream is named and located per issue; a non-preferred prefix
bound to the TIFF, EXIF or aux namespace URI is checked, and the preferred
prefixes bound to other URIs are not, proving the lookup is by URI.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
# Conflicts:
#	crates/preflight/src/lib.rs
#	crates/preflight/src/tests.rs
#	crates/preflight/src/xmp_types.rs
#	parity/acrobat-features.toml
- `Scanner::parse_object_or_skip` skips whitespace and comments before
  taking its start offset, so a token after a space fails once, not
  twice, whoever calls it. (The CMap scan already skipped whitespace in
  `at_end`, so CMap results do not change.)
- Unit tests in hayro-postscript (stepping over a token, a failed read
  not read again, never past the end of the data) and in hayro-cmap
  (bfchar and bfrange after a stray `>` or a spaced name, the 1,024-token
  limit at its boundary, a CMap of unreadable tokens only failing).
- The render test checks a bfrange entry after the bad token, a run past
  the limit before the mappings, and a CMap of unreadable tokens only;
  its wall-clock assertion is gone, the no-re-read property is the
  deterministic hayro-postscript test.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
6.6.2.3.3 findings are now reported per description instead of once per
entry name: each names the schema by its pdfaSchema:namespaceURI (or #n),
the property, value type or field by its own name (a field as type/name),
and the metadata stream, with Issue::object set. A misused prefix names
the namespace URI and the prefix written (or that none was); a container
that isn't one rdf:Bag names what it holds instead. Findings are capped at
1000 per document and repeated names at 200 characters.

6.6.2.3.1 issues now name the type the XMP Specification gives the
property, as the specification writes it (Integer, seq Rational,
Lang Alt, Flash); structure types carry their names for this.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
hayro patch (30) gains a test hook, `offscreen_draws`: per thread, the tiling cells drawn
and the pixels of every pixmap drawn offscreen, all made by `offscreen_pixmap`. The two
wall-clock bounds become counts: 5,000 patterned stencils draw one cell and less than a page
of pixels offscreen, and 3,000 fills of one setting draw one cell. New tests interleave the
settings of two coloured patterns with cells of one size and one of them at two scales, each
showing its own pattern in place and drawing one cell per setting, and paint an uncoloured
pattern in two colours and, at half opacity, both as a fill and as a stencil mask's copy.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Pq7HyDBXygTf7jcqdsjcfn
Each of the dispatcher's other room checks gets a case: a page whose only
coverage is strokes, or axis-aligned rectangles, drawn 2^10 times through
forms, then a red marker through the same path, rendered with the
rasterizer kept to 200,000 values (the marker is left out and the page
says so; unlimited, it is drawn), and clipped layers pushed through
vello_cpu's RenderContext directly (hayro pushes no clipped layers). Each
case fails with only its own path's check removed.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
A Times New Roman the PDF doesn't embed, written as a Type 0 font with a
CIDFontType2 descendant (TimesNewRoman,Bold, TimesNewRomanPSMT and the
other faces) and no /ToUnicode, now goes through the rule of patch (76):
each CID gives, through /CIDToGIDMap (ISO 32000-2 Table 115), a glyph
index in the author's font, and the substitute draws that glyph by its
name in the standard Macintosh order of the TrueType post table. Before,
the CID was taken as one of the substitute's own glyph indexes, which
ISO 32000-2 9.7.4.2 notes "are not defined consistently from one font to
another", and the ameli.fr mutation form drew "Salarié(e) du régime
général" as "Salari²*g+f w't²i lo g'ì²p²tcn".

Times New Roman numbers its first 258 glyphs in that order: the same
form's embedded Times New Roman subsets keep their glyph indexes, and 941
of their 942 /ToUnicode entries below 258 name that order's character.
Times and Times New Roman MT Std are other fonts and keep the old path.

Fixes #6449

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Pq7HyDBXygTf7jcqdsjcfn
- The widened appearance box and /Rect reach the /QuadPoints only as far
  as the page's visible region (crop box within media box, ISO 32000-2
  §14.11.2), never shrink /Rect, and are not widened at all by quads
  wholly off the page or by more than MAX_MARKUP_QUADS (10,000)
  quadrilaterals (counted before the numbers are read).
- One /Rect normalisation (normalised_rect) for appearance_context and the
  reach; markup_reach takes the normalised /Rect and the page box, and
  apply_appearance takes the saved /Rect from the built /BBox
  (widened_rect) instead of computing the reach a second time.
- Tests: the restyle (set_appearance) path saves the widened /Rect, stops
  at the page and keeps a fitting /Rect as written; a markup with its own
  /AP and a non-markup subtype are not widened; crop box and cap cases.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Tests for the UCS2 route of hayro-interpret patch 84: Adobe-GB1, -CNS1 and
-Korea1 fonts with Identity-H and no /ToUnicode (CFF and TrueType), each
with a CID-to-Unicode pair read from its bundled UCS2 CMap; Identity-V
and /H over a CFF program; and a Type 0 font of a non-Adobe collection,
which gets no text through this route. Each collection's case fails when
its family is pointed at another collection's UCS2 CMap, and the
non-Adobe case fails when such a family is given one.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
A number read stops at the first byte that can't be part of a number,
which may be inside the token: `parse_object_or_skip` then read the rest
(`beginbfchar` in `1beginbfchar`) as a token of its own, and the pairs
after it became mappings. The rest of the token is now skipped with the
failed number, one failure per token, as PostScript reads a run of
regular characters as one token.

The render test's run of unreadable tokens before the mappings is spaced:
a run of `-` with nothing between them is one token now.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Pq7HyDBXygTf7jcqdsjcfn
A namespace URI, prefix, name, value type, type or category entry that is
there but empty names nothing; it is now reported with its own wording.
Descriptions stay free to be empty Text.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Pq7HyDBXygTf7jcqdsjcfn
The unknown-blend-mode issue carries its page, as the transparency issue
does. Pages keep what was found under the indirect objects they share
(resources, forms, patterns, Type 3 fonts, graphics states) in a
document-wide map, filled only from objects read to the end, so the next
page takes them in one read. The walk reads objects in place instead of
copying resources dictionaries and their entries, and gathers at most 32
unknown blend mode names.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Pq7HyDBXygTf7jcqdsjcfn
A 6.6.2.3.1 issue now names the metadata stream that holds the property
and carries its ObjRef in Issue::object, so a property mistyped in two
streams is two issues. Wordings that name several things use named holes,
filled in one pass. The message names the value type the XMP
Specification gives the property (Integer, Lang Alt, seq ResourceEvent,
Dimensions), and, when the packet writes the property with another prefix
than the preferred one or none, the name as written.

Tests also show that a property is known by its namespace URI, and that a
property a predefined namespace doesn't define, or a namespace that isn't
predefined, is left alone.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Pq7HyDBXygTf7jcqdsjcfn
The first clip is asserted to fit, and a fill inside it shows within the
circle only; a scene with room for exactly one path shows the clip pushed
after a fill is itself refused.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Pq7HyDBXygTf7jcqdsjcfn
# Conflicts:
#	crates/preflight/src/lib.rs
#	crates/preflight/src/tests.rs
#	crates/preflight/src/xmp_types.rs
#	parity/acrobat-features.toml
The markup test gains a highlight whose /Rect spans -1e308 to 1e308 and
whose on-page quad lies above it, so the finite-size guard in
markup_reach is what keeps its /Rect.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Pq7HyDBXygTf7jcqdsjcfn
Brings in #6220's written-name wording for 6.6.2.3.1 alongside the
extension schema findings; the duplicated type-name test keeps the
version that covers both the base and the EXIF types.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Pq7HyDBXygTf7jcqdsjcfn
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Pq7HyDBXygTf7jcqdsjcfn
Conflict in crates/sign/src/pdf/changes.rs: main moved the signed-page
check into ChangedObjectClassifier::page_change_kind. The method now
uses same_but_annots, as the free function it replaced did on this
branch; the branch's tests reach it through a test helper of the same
name, and both test sets are kept.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Pq7HyDBXygTf7jcqdsjcfn
A string object is its bytes; literal and hexadecimal are two ways to
write them (ISO 32000-2 §7.3.4.1-7.3.4.3). The modification check
compared objects with the derived equality, which also compares how a
string was written, so a later save that rewrote the catalog's /Lang
from (en-US) to <656E2D5553> made an untouched signature read as a
document structure change.

same_object now compares strings by their bytes, and every comparison
in changes.rs goes through it: the unchanged-object skip, the catalog,
its /AcroForm and /Perms, developer extensions, signed widgets, page
resource containers, the security store and the timestamps check.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Pq7HyDBXygTf7jcqdsjcfn
A writer declared /Predictor 15 on an image and its soft mask without
applying it, so the inflated data is exactly /Height rows of the row length
with no filter-type byte before each (ISO 32000-2 §7.4.4.4 requires one per
PNG-predicted row). Read with the predictor, every row's first sample was
taken for a filter type, the image came out a row short and shifted, and the
page drew it in fragments; image export said its data was shorter than its
size. PDFium, MuPDF and Poppler read such data as the samples it is.

Only that exact length takes the new path, and only for the last Flate
filter of an image whose height is known; anything else is predicted as
before. The renderer (vendored hayro-syntax, patch 40) reports it with
warn!, like its other lenient filter decodes; pdfcraft-filters returns it as
tolerated damage (partial in tolerant decoding, an error in strict), through
the new decode_image_tolerant_within that pdfcraft-cos uses for images.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Pq7HyDBXygTf7jcqdsjcfn
A CCITTFaxDecode image whose /DecodeParms /Rows exceeds the image's
/Height (PDFBOX-771: /Rows 1084975104 on 2,592 columns) asked the decoder
for /Rows rows; the size check refused that, and every page drew blank.
The image is /Height rows (ISO 32000-2 Table 87), so the decoder is asked
for those; /Rows still counts for a stream decoded without an image's
height. pdfcraft-cos already decodes /Height rows; a test pins it.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Pq7HyDBXygTf7jcqdsjcfn
Any value may be an indirect object (ISO 32000-2 §7.3.10). The appearance
builder read only direct values, so a border style dictionary given as
`/BS 5 0 R` (Table 168) was ignored and the border drawn 1 pt solid. The
display copy, a redraw, the properties and a new width now read the
annotation with its style references followed (bounded in depth and in
copied values; links such as /P and /Popup and streams stay references).
A new width keeps an indirect /BS's other entries instead of dropping them.

/RD is optional (Table 177): a FreeText callout without it now has its
text box at /Rect and is drawn, where it was not drawn at all.

PDFBOX-4515 (annotations-2.0-without-AP).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Pq7HyDBXygTf7jcqdsjcfn
Following references can turn up a value the appearance builder leaves
alone, such as a cloudy /BE on a square. The comment then keeps the
drawing made without the references followed, instead of none.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Pq7HyDBXygTf7jcqdsjcfn
A Type 3 font without /Widths (PDFBOX-2943 misspells the key) gave its
glyphs no advance, so the text was drawn as one blot and extracted as
characters stacked on one spot. ISO 32000-2 Table 110 requires /Widths,
and 9.6.4 has the glyph procedure state the same width with d0/d1, so
such a font now takes that width, read once per code. A font with
/Widths is unchanged; a code outside FirstChar..LastChar keeps width 0
as Table 110 says. Rendering and text extraction share the advance.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Pq7HyDBXygTf7jcqdsjcfn
When a file's last incremental update stops after its cross-reference rows,
with no trailer or startxref, the last startxref in the file is the older
revision's. pdfcraft-cos finds no startxref in the last 4 KiB and rebuilds
from object headers (latest objects win); vendored hayro-syntax read the older
table, so the view showed the older revision (PDFBOX-3934: 12 pages, the model
13). ISO 32000-2 §7.5.6: a later section supersedes earlier ones; repair is
unspecified and PDFium, MuPDF, Poppler and qpdf take the latest objects.

hayro-syntax patch 43: when the last startxref is more than 4 KiB from the end
and N G obj headers follow it, Pdf::new_with_password rebuilds first; the older
table is read only when the rebuilt one has no page. Trailing data without
object headers and a startxref near the end read as before.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Pq7HyDBXygTf7jcqdsjcfn
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Pq7HyDBXygTf7jcqdsjcfn
ISO 32000-2 §7.3.3: wherever a real number is expected, an integer may
be used instead. PdfCraft's own writer writes a whole real as an
integer, so a form fill that rewrites a signed widget turned
/Rect [20.0 20.0 120.0 40.0] into [20 20 120 40] and was refused as a
page content change. same_object now compares an integer and a real by
value (exactly, in i128), for the widget /Rect and every other signed
comparison, such as a page /MediaBox rewritten when a comment is added.

Also states the reordered page's verdict as None explicitly, in a test
of its own next to the one from #5962.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Pq7HyDBXygTf7jcqdsjcfn
props() reads the whole annotation with references followed, so an
indirect /C or /IC (ISO 32000-2 7.3.10) shows its colour, and
"restylable" and the set_style / set_fill / set_part_opacity checks use
the same build and fallback as the display (draw::can_draw), so such a
comment can be restyled.

Following references now counts every value copied, inner arrays and
dictionaries included, and one display pass copies at most 4M values in
all, so an object many annotations point at is not copied for each of
them without bound. A cached appearance keeps what storing it needs
(the undrawable character and a note's /Rect), so a cache hit follows
no references at all.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Pq7HyDBXygTf7jcqdsjcfn
Through /Indexed, samples past the decoded data read as black, so an
all-black image could not tell a decoder that stopped after one row
from one that decoded all four. A second Group 4 image striped black,
white, black, white (horizontal-mode rows, ITU-T T.6 §2.2.3.3) is now
rendered through both colour spaces with every /Rows value, and each
row is checked for its own colour.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Pq7HyDBXygTf7jcqdsjcfn
Add:
- Recover editor-only Unicode cmaps for Identity-H TrueType subsets and shape
  verified THSarabunPSK positional glyphs into their original PDF character codes.
- Recognize narrowly fingerprinted damaged Thai exporter maps without guessing
  mappings for unrelated fonts; callers must record and isolate any repairs.

Change:
- Preserve the original PDF font programs and refuse missing contextual glyphs.
- Allow modern Thai GPOS positioning without requiring optional GSUB, adapting
  that correction from dekkeng's PR #2811 at fba0a29a.

Refs #3185 and #2142. Font/edit tests: 535 passed. Formatting, scoped strict
Clippy, layers, assets, parity and the fonts/edit wasm check passed.
Add:
- Prepare supported Form placements as original page operators through the
  existing-text UI and text_prepare, with shared-form isolation and undo/redo.
- Repair narrowly identified Thai exporter maps on copied page fonts, recording
  repairs without changing the embedded source glyph programs.

Change:
- Reuse legacy Thai Regular/Bold glyph codes and same-face punctuation subsets;
  retain point size, spacing, horizontal scale, rise, colour and paint settings.
- Keep cross-family Thai replacement explicit and write matching bundled Thai
  subsets through the existing bounded outline path rather than static embedding.
- Keep replacement-only menu choices out of the source-font preview atlas so
  opening an ordinary paragraph does not wait an extra frame for unused fonts.
- Refuse unsafe compositing, tagged or malformed groups, and missing contextual
  glyphs without losing the user's document or changing other placements.

Chores:
- Document the editing limits and headless workflow; keep text-edit parity partial.

Builds on dekkeng's Thai fallback work in #2811 (fba0a29a) and mtlcl410's Form
Ungroup implementation in #3067 (6eafca9c), adapted to the current module layout.
The Ungroup menu and older UI font-priority changes are not included.

Refs #3185, #2964 and #2142. Synthetic regressions cover source-font preservation,
shared forms, unsafe refusals, layout, undo/redo, rendering and save/reopen.

Validation: library suites 1,675 passed; automation integration 331 passed;
document-font integration 2 passed; editor flows 248 passed. Existing ignored
tests remain. Formatting, scoped strict Clippy, layers, assets, parity and wasm
checks passed. The unchanged opacity regression failed before the choice-only
atlas correction and passed afterwards.
A referenced object rejected as over the per-annotation budget was walked
up to that budget and charged nothing, so many annotations (or many
references in one annotation) pointing at the same oversized nested array
each walked it again, and PASS_BUDGET did not bound the work. The walk is
now charged to the annotation's budget and through it to the pass, and a
spent budget follows no further references.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Pq7HyDBXygTf7jcqdsjcfn
decoded_images_serve_every_size_they_are_drawn_at asserts image and mask
decodes separately, through a new InterpreterCache::decoded_mask_decodes
(hayro-interpret patch 82), so a wrong split behind the same total fails.
The truncated-stream case of
an_averaged_down_soft_mask_follows_its_samples_and_its_drawn_size asserts
the exact mask handed over: 32 x 10 (the decoded rows), all opaque.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Pq7HyDBXygTf7jcqdsjcfn
The decoded-image cache stored a failed decode as an empty entry and read
an empty entry as a miss, so a picture or stencil mask that doesn't decode
(damaged data, a JPX or JBIG2 stream its decoder refuses) was decoded again
at every draw, and at every tile, preview and zoom of a render worker that
shares the cache.

The cache now keeps a failure for its key, with the warnings the decode
reported; a later draw reports the same warnings and gets nothing, as the
decode did. A failure counts 128 bytes towards the cache's 64 MiB budget.
A failure the page's JBIG2 allowance may have caused (ContentTruncated, or
the allowance spent) isn't kept, since the next page starts afresh.
Successful decodes are cached as before.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Pq7HyDBXygTf7jcqdsjcfn
`uni` takes groups of exactly four uppercase hex digits and `u` four to six,
outside the surrogate range, so `uni65`, `u65`, `uni20ac` and `uni+065` read
as nothing instead of `e` and the euro sign. hayro-interpret patch 92 reads a
glyph's text by `agl_unicode_from_name` and keeps upstream's lenient reading
for finding glyphs in a font, so drawing is unchanged; pdfcraft-fonts'
`glyph_text` stops accepting lowercase digits.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Pq7HyDBXygTf7jcqdsjcfn
Add:
- Report text grouped more than eight levels deep instead of leaving it
  silently grouped, and refuse forms whose resource-naming operators carry
  surplus operands, which would otherwise lose nested artwork when inlined.
- Return the applied preparation's own counts from Session::prepare_text, so
  text_prepare no longer reports a separate preview's.
- Shape a THSarabunPSK program that has its own Thai mark positioning by that
  layout, in its own codes. A span the subsets can't show in place is shaped
  whole, Latin rows included, by a complete program of the same face and style,
  such as a CFF face from Load font.

Change:
- Expand only forms that show text, directly or through nested forms; image-
  and vector-only forms stay grouped and uncounted. Match a Do's raw resource
  name bytes, and refuse RGB-blended groups on pages naming no blending space.
- Check the recovered THSarabunPSK map and the Windows PUA alternates glyph by
  glyph against the subset's advances, leave dropped glyphs unmapped, and
  insert the repair after the last endcmap.
- Shape a Thai run split across same-face subsets cluster by cluster and skip
  subset glyphs that are empty. Take the legacy path only for programs in the
  legacy glyph order, needing only the glyphs the new text uses.
- Require GPOS mark/mkmk in the script and language system HarfRust selects.
- Match same-face candidates by weight and slant from the name, descriptor
  weight or a non-zero ItalicAngle; the Italic flag alone is ignored, as
  exporters set it on upright faces. Never complete bold or italic text with
  the Regular bundled Thai face.
- Name the missing character, or the unavailable builtin:thai choice, in errors.

Chores:
- Document text_prepare's fields, builtin:thai, the app's refusal reporting
  limits and the new routing; extend the text-edit parity evidence. Tests that
  need craft-fonts now say they skipped instead of returning silently.

Validation: with and without craft-fonts, fonts 105 (+3 build-input), edit
465, engine 482 (+28 integration) and automation 50 + 352 passed; editor flows
248 passed. Formatting, strict Clippy, layers, assets and parity passed; the
wasm check was not run locally. Each new regression failed with its fix
reverted. Real-PDF edits on Royal Gazette 26288 kept their source fonts.
A CMap that writes its CID mappings as `bfchar` was looked up without
the code's byte length, so the first byte of a two-byte code matched a
two-byte entry of the same value and drew as its own glyph: `<fe96>`
drew as "þ–" (PDFBOX-1282, Characters_Arial.pdf page 3). The bfchar
fallback of `lookup_cid_code` now applies only when the codespace
ranges give the code the length being tried (ISO 32000-2 §9.7.6.2),
byte by byte; a CMap without codespace ranges keeps the old fallback.
An unmapped code still advances one byte. Vendored hayro-cmap patch 4.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Pq7HyCHD1R3rywQBPCHXZA
A quad counts only when all 8 entries are present, numeric and finite.
Anything else contributes nothing — never folded, shifted or zero-filled
into a box the document does not describe. With no usable quad the
existing /Rect fallback runs, and with neither the mark stays unreadable.

The rule lives in one helper, geom::quad_points::usable_quads, and redact,
render-inspect, annot summaries and annot appearance drawing all read
through it, so the outline shown is the area apply removes. Redact also
follows indirect numbers in /QuadPoints now, which it used to drop.
A CID CFF program whose subsetter emptied the font dicts no kept glyph
uses (no Private DICT) dropped the whole font: CffFontBlob collected its
subfonts with `collect::<Option<Vec<_>>>()?`. The subfonts are now kept
as `Option`s; a glyph whose font dict can't be read is missing, the rest
draw. The program is still refused when no font dict is usable.

ISO 32000-2 §9.7.4.2, §9.9 Table 124; Adobe TN #5176 (FDArray, FDSelect,
Private DICT). PDFBOX-5843 (13069603-123.pdf) now matches PDFium, MuPDF
and Poppler.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Pq7HyCHD1R3rywQBPCHXZA
Text drawn one glyph per show operation, with zero-advance Thai tone marks
starting a hair past their base's advance, came out with the mark after the
next letter ("ห้อง" read "หอ้ง") because a line's glyphs were sorted by x
alone. A combining mark (Mn/Me) of a left-to-right script drawn right after
the glyph it sits over now rides on that glyph: placement sorts clusters, so
the mark keeps its content-stream place. Word gaps are measured between
clusters, and the tight-cluster rule now guards only loose combining signs,
so a Thai or Indic word gap with no space glyph reads as a space.

Fixes #6517

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Pq7HyCHD1R3rywQBPCHXZA
A font dictionary whose /Subtype is missing or the empty name
(`/Subtype /`) was refused, and its text drew in the Helvetica
fallback: a `/Courier-Bold` lost its fixed pitch and its columns
shifted (PDFBOX-1988 Test1.pdf). /Subtype is required (ISO 32000-2
§9.6.2.1, Table 109), but other readers read such a font as Type 1 by
its /BaseFont, and so does this. Any other unknown subtype is still
refused. Vendored hayro-interpret patch 94.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Pq7HyCHD1R3rywQBPCHXZA
A non-embedded TrueType `SymbolMT` (Symbol's TrueType name) with
/WinAnsiEncoding and the nonsymbolic flag was not recognised as the
standard Symbol font and fell back to Helvetica, so code 0x6D drew "m"
where PDFium, MuPDF and Poppler draw "mu". `SymbolMT`, and `Symbol` or
`SymbolMT` with a `,`/`-` style suffix, now select Symbol, which draws
through its built-in encoding like `/Symbol` already does.

ISO 32000-2 §9.6.2.2, §9.6.5.1. PDFBOX-5844 (13069708-exportByExcel.pdf).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Pq7HyCHD1R3rywQBPCHXZA
A page with no /MediaBox, its own or inherited, rendered as A4, while
PdfCraft's page model and inspector already sized it as US Letter and
other readers draw it at 612 x 792 pt (PDFBOX-4001-reduced.pdf).
ISO 32000-2 §7.7.3.3 Table 31 requires the box and gives no default;
follow the readers. `base_dimensions`' fallback for a visible region
without area uses the same Letter size, so a /MediaBox [0 0 0 0] page
is Letter too. Vendored hayro-syntax patch 44. The model test pins the
size it already gave.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Pq7HyCHD1R3rywQBPCHXZA
The previous commit read a glyph name that only numbers its glyph (`C70`,
`MT72`, `G4E`) as its code's Latin-1 character whenever the number, in
decimal or hexadecimal, was that code. In fonts whose names number
something else (`/Differences [1 /C70 /C105 ...]`, a subset's order), a
name can match its code by coincidence: `/MT40` at 0x40 read `@` where the
page draws `(` (PDFBOX-1078), `/C39` at 0x39 read `9` (PDFBOX-1783).

The rule now applies only in a Type 1 font where every numbered name in
its encoding names its own code (`numbered_names_name_their_codes`,
worked out once per font). `/C1` at 233 beside `/C70` at 70, the review
counterexample on #3239, turns the whole font back to nothing, and
#3239's test font (`c251` at 97, `C1` at 233 beside `C39` at 39) reads
exactly as #3239 expected again. Codes 128-255 read as Latin-1
(`C174` = `®`), as pdftotext, PDFium, MuPDF and Acrobat read them.

Vendor ledger: hayro-interpret patch 99.

Test: text::tests::numbered_glyph_names_read_as_their_own_code (fails
with the guard switched off, and with the fallback switched off).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Pq7HyCHD1R3rywQBPCHXZA
A Type 3 font already reads codes 32-126 by the code when its glyph names
say nothing. Above 126 a name that only numbers its glyph (`C174` at 174,
`GE9` at 0xE9) now reads as the code's Latin-1 character, under the same
font-level guard as a Type 1 font: every numbered name in the encoding
names its own code, so `/C1` at 233 turns the font back to nothing.

Test: text::tests::numbered_type3_glyph_names_read_as_their_own_code
(fails with the fallback switched off, and with the guard switched off).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Pq7HyCHD1R3rywQBPCHXZA
Change:
- Leave a form found to be artwork, or refused for a reason in the form
  itself, out of later preparation passes, so a page with many logos or
  unsupported groups no longer reaches the 64-placement limit and prepares
  nothing. ungroup_ops now marks each refusal as intrinsic or contextual;
  contextual ones (translucency, the resources it would merge into, pattern
  placement, the decode budget) are re-evaluated where the form appears, so
  a form refused in one place is still prepared where a parent draws it.
- Refuse a page-level Do with an operand too many for that placement only:
  the form found by its first operand may not be the one drawn. Check a
  form's own operand counts before the nested walk, as an intrinsic refusal.
- Use the Windows PUA alternates only when every kept alternate whose code
  the PDF's map names a Thai character names that alternate's base
  character, so alternates in another order that the map reveals are refused.

Chores:
- Say that the font program confirms each recovered glyph's advance class,
  not its identity, and document the alternate check.

Validation: with and without craft-fonts, fonts 105 (+3 build-input), edit
468, engine 482 (+28 integration) and automation 50 + 352 passed; editor flows
248 passed. Formatting, strict Clippy, layers, assets and parity passed; the
wasm check was not run locally. Each new regression failed with its fix
reverted. Real-PDF edits on Royal Gazette 26288 kept their source fonts.
The fallback to the glyph procedures' d0/d1 widths keeps such a page
readable, but ISO 32000-2 Table 110 requires /Widths in a Type 3 font,
so the reader should also be told the font is malformed. Selecting a
Type 3 font without /Widths (or /FirstChar, /LastChar) now reports
InterpreterWarning::Type3WidthsMissing, surfaced once per page as
RenderWarning::Type3WidthsMissing: `pdfcraft-cli render` prints it and the
automation render tools list it with the page's other warnings.

Test: raster::tests::type3_proc_widths::a_type3_font_without_widths_is_reported
(fails with the report switched off).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Pq7HyCHD1R3rywQBPCHXZA
A second mark stacked on a letter now rides on the letter as well as on the
mark before it; a mark drawn up to an em above or below its letter stays on
the letter's line, and the glyph after it continues from the letter; marks in
a zero-width run are ordered by their letter; and a word gap after a mark is
measured from the letter's advance, not the mark's offset start.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Pq7HyCHD1R3rywQBPCHXZA
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
The mesh sampler walked each triangle's whole bounding box. A patch is cut
into a fixed 19 x 19 grid, so across a narrow curved band the triangles are
thin and lie at every angle, with boxes many times their area. Painted through
a path the size of the mesh's own box (a pattern fill of a polygon, or `sh`
under a clip), the visit budget (eight per pixel of the area, four per
triangle) ran out before the last patches, and a rainbow of type 7 patches
lost its inner bands (PDFBOX-1915 asy-tensor-rainbow).

Each row of a triangle is now walked only across the columns where that row's
sample points can lie in it; a box at most two columns wide is walked whole,
and a row with no column still costs a visit. The budget is unchanged.
ISO 32000-2 8.7.4.5.8, 8.7.4.2.

Test: raster::tests::thin_patches_of_a_mesh_in_a_tight_path_are_all_painted
(fails on main: the inner band is white).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Pq7HyCHD1R3rywQBPCHXZA
- hayro-syntax patch 44: a /MediaBox without area counts as missing, so
  the page is sized, placed and clipped by the same Letter region and its
  content is drawn (MuPDF and PDFium both draw it on 612 x 792).
- pdfcraft-model: Page::crop falls back to Letter for such a box too.
- vendor/README.md: the raw-crop summary names the Letter fallback.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Pq7HyCHD1R3rywQBPCHXZA
hayro-cmap patch 4: text CMaps record each bf entry's source length
(BfRange::code_len). Where no codespace range gives a code its length,
the bfchar fallback of lookup_cid_code takes the entry's own length, so
the first byte of <fe96> is no longer taken for <00fe>. CMaps with
codespace ranges are unchanged.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Pq7HyCHD1R3rywQBPCHXZA
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
- CffFontBlob::new moves the subfonts it validated into the yoke instead
  of reading every font dict a second time.
- a_program_with_no_usable_font_dict_draws_no_square now checks that the
  page renders exactly as it does with no embedded program; the old
  inequality passed with the empty-dict check removed.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Pq7HyCHD1R3rywQBPCHXZA
- is_symbol_name: after `,` or `-` only a style name (SYMBOL_STYLES) keeps
  the Symbol font, so `Symbol-Helvetica` keeps its declared encoding and
  Latin stand-in.
- Tests: the negative names draw the same letters as Helvetica; a SymbolMT
  with /Differences [109 /alpha] draws alpha; an embedded SymbolMT draws
  its own program.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Pq7HyCHD1R3rywQBPCHXZA
Patch 99 is taken by #6542.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Pq7HyCHD1R3rywQBPCHXZA
The failure is kept per key, and a JPX decode's key includes the size it
decodes for, so the same failed picture drawn at two sizes is decoded once
at each. The test fails if the cache key leaves the target out.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Pq7HyCHD1R3rywQBPCHXZA
The test of the glyph list's hex forms now has well-formed `u` names too,
so a change that broke that branch of `glyph_text` would fail it. The rule
itself is unchanged.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Pq7HyCHD1R3rywQBPCHXZA
Change:
- Read and count only the placements a preparation pass newly inlines. The
  others were read on an earlier pass in the context they still have, so a
  group refused where it is drawn (translucent, for example) is no longer
  counted again on every pass: 33 such placements beside one text group used
  to reach the 64-placement limit on the second pass and prepare nothing. A
  form refused in one place is still read where an expanded parent draws it
  anew. If the page doesn't parse back to the operators written, every
  placement is read again, as before.
- Read a group refused because its resources needed renaming again, without
  counting it, once names merged in for other groups make the renaming
  unnecessary (checked from its resource dictionaries alone, without decoding
  its content), and drop its earlier refusal when it is then prepared.

Validation: edit 470 and the 2 automation text_prepare tests passed;
formatting, strict Clippy for pdfcraft-edit, layers and assets passed. The
wasm check was not run locally (the wasm32 target is not installed), nor were
the engine suite and the workspace sweep. Each new regression failed with its
fix disabled.
A Flate stream damaged partway decodes to what came before the damage, and
`DecodeBudget::decode` gave that back as the stream's data: the decoder's
partial flag was dropped in `Stream::decoded_within_counted`. Editing a text
line, a block, an image or an added item in such a stream wrote it again from
that part, so everything after the damage was lost on save, though other
readers may still read it.

- `DecodeBudget::decode_with_damage` decodes as `decode_within` does and says
  whether the stream is damaged partway. A missing checksum or end-of-data
  marker is no damage, as before.
- Edit reads the page's streams with it (`ContentStream`), and
  `rewritten_contents` refuses, before adding anything, a change to a damaged
  stream. Changes in the page's other streams go on, and the damaged one stays
  as stored.
- Selecting objects (`editable_objects`, moving added items) refuses a page
  with a damaged stream, as it refuses one that doesn't decode: what follows
  the damage could change every later item's graphics state.
- Marking a tag as an artifact or a figure as decorative refuses when the
  tag's content is in a damaged stream (`ArtifactError::Damaged`,
  `AltError::Damaged`), and changes nothing, the structure tree included.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01AmsdeUCdTaD72jkDw2pJKY
Optimize read a page's content up to any damage in its streams as the whole
of it. Merging a page's image strips then wrote the page's content again as
one stream, without what followed the damage, and the image pass measured
each image by its uses before the damage only, so one drawn larger past it
could be resampled below that use.

- `DecodeBudget::decode_contents_with_damage` reads a page's `/Contents` as
  `decode_contents` does, each stream also saying whether it is damaged
  partway; `decode_contents` is it without the flag.
- `content_bytes` gives `None` for a page with a damaged stream, as for one
  that doesn't decode: the content is not all known. Its strips aren't merged
  (`merge_page` already leaves such a page alone), and no image is resampled
  (`effective_resolutions` gives every image `INFINITY`).
- A form damaged partway leaves the measuring incomplete in the same way.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01AmsdeUCdTaD72jkDw2pJKY
Extracting, splitting and combining pages drops the `/XObject` entries no
page draws. A page or form damaged partway was read up to the damage as the
whole of it, so an image named only after the damage was dropped, though
the stream still names it and other readers may still read that far.

- `content_bytes` says whether a page's stream is damaged, and pruning keeps
  everything such a page could reach, as it does for content that doesn't
  decode. A form is read the same way.
- A page made a form XObject (stamps, watermarks, backgrounds from a PDF)
  still draws what decoded before the damage.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01AmsdeUCdTaD72jkDw2pJKY
A stream that doesn't decode at all is not damage to write over: edit and
accessibility keep it as stored and go on with the page's other streams,
and only running past the page's decode allowance refuses. Nothing pinned
either, nor that selecting objects reads a stream of a few MB.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01AmsdeUCdTaD72jkDw2pJKY
`decode_with_damage` and `decode_contents_with_damage` read a stream's
`/DecodeParms` only when direct, as `Stream::decoded` does, so content behind
a predictor whose parameters, or a number in them, are an indirect object
decoded with the predictor's tag bytes left in, and was taken as whole. Edit
then listed and wrote back the wrong bytes, and pruning dropped an image such
a form draws. ISO 32000-2 lets those values be indirect.

- `decode_with_damage` takes the document and reads the parameters through
  it, and so does `decode_contents_with_damage`, and `decode_contents` with
  it: printing, accessibility's reading of a page and a page copied as a form
  read them too. Code that calls `decode` on a stream itself doesn't yet.
- It reads through `Document::decoding_view`: `with_direct_parms`, but a copy
  of the stream holds only its `/Filter` and the parameters, its data shared,
  so a stream a page's `/Contents` lists again and again isn't copied whole
  for each.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01AmsdeUCdTaD72jkDw2pJKY
A tag's content not found before the damage in a damaged stream may be
past it. Marking a figure decorative took it for absent: it succeeded,
took the figure out of the structure tree and left its content tagged.
Marking a tag as an artifact said its content wasn't on the page. Both now
refuse with `Damaged`, as when the content is before the damage, and change
nothing.

Clearing the parent tree read the page's `/StructParents` only when direct,
so with it given by reference (ISO 32000-2 lets it be) the tree went on
pointing the content at the retired tag. It is read through the reference.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01AmsdeUCdTaD72jkDw2pJKY
`DecodeBudget::decode_strict` read a stream's `/DecodeParms` only when
direct, so content behind a predictor whose parameters are an indirect
object decoded with the predictor's tag bytes left in. Redaction then found
no glyph under a mark, left the text in the file and reported the mark
applied. ISO 32000-2 lets those parameters be indirect.

`decode_strict` takes the document and reads through `decoding_view`, as
`decode_with_damage` does. Its other readers read the parameters too:
Remove Hidden Information's hidden text and layers, the audit's covered-text
check, and edit's backdrop scan.

The accessibility_mark_tag_artifact description says that content which is,
or may be, in a stream damaged partway is refused.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01AmsdeUCdTaD72jkDw2pJKY
Once a redaction removes an image or form under a mark, it takes the name
out of the page's resources unless the page's content still draws it
elsewhere. It read the page's streams again for that with `decoded`, which
reads `/DecodeParms` only when direct, so a stream behind a predictor whose
parameters are an indirect object seemed not to draw it, and the page lost
the image there.

It now looks in what it read for the redaction, with the parameters by
reference, and decodes again only the streams it has just written, which
have none.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01AmsdeUCdTaD72jkDw2pJKY
Since marking a figure decorative or a tag as an artifact refuses content
not found beside a damaged stream, the refusal also covers content that
only may be past the damage. Its message said the content is in a damaged
stream; it now says it is, or may be.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01AmsdeUCdTaD72jkDw2pJKY
A page's content streams are read as one stream, so what a stream
damaged partway hides past the damage is unknown to the streams after
it too. A string or comment the damage leaves open runs on into the
next stream, and operators the damage hides can change the graphics
state the later streams draw in. Marking a tag's content as an artifact
took what read as a BDC in a later stream for the tag's own, and could
rewrite bytes inside a string. Moving an image after the damage worked
the move out without a cm the damage may hide.

Now an edit refuses a change to any stream after one damaged partway
(rewritten_contents), and an image isn't arranged into a new stream
placed after one. Marking an artifact or a decorative figure refuses
content that reads as the tag's in a stream after the damage. Edits and
marking before the damage go on as before.

A content stream that doesn't decode at all was read as empty and
whole. A styled edit whose paragraph marker closes past the page's last
operator appended the close to it, so the edit replaced that stream
with one holding only the close. Edits now refuse a change to such a
stream, and still go on around it. Marking an artifact or a decorative
figure whose content isn't found takes it as possibly in such a stream,
as it does for content past the damage, and refuses rather than take it
for absent.

Tests: an_edit_never_rewrites_a_content_stream_damaged_partway (an edit
after the damage refused, one before it made),
text_a_damaged_stream_runs_on_into_is_not_edited,
images_after_a_content_stream_damaged_partway_are_not_moved,
a_content_stream_that_does_not_decode_is_never_written_over,
a_tags_content_after_a_damaged_stream_is_not_rewritten,
a_tags_content_not_found_beside_a_stream_that_does_not_decode_is_not_taken_for_absent,
and a_tags_content_in_a_stream_damaged_partway_is_not_rewritten now
checks the damaged stream itself and marks content before the damage.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01AmsdeUCdTaD72jkDw2pJKY
Earlier in this change, a page or form damaged partway, like one that
doesn't decode, kept its own /XObject dictionary whole and was read no
further. So a form it draws was never read: when the form's resources
are shared with another page, the names the form draws there went, and
extracted or split pages lost those images. On main the part before the
damage was read, so forms drawn there kept theirs, and forms drawn past
it didn't.

Now every form the resources of such content name is read as drawn, as
any of them may be drawn before the damage or past it. Each set of
resources has its forms read once.

Test: extract_keeps_images_forms_drawn_around_damage_draw_through_shared_resources,
with a page and a form each damaged before and past the form they
draw, and each not decoding.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01AmsdeUCdTaD72jkDw2pJKY
- cos: a filter chain whose first stage stops at damage and whose
  second reads all it was given is damaged.
- optimize: a page damaged partway stops resampling for the whole
  document, so another page drawing the same image whole doesn't get
  it resampled.
- redact: after redacting content with /DecodeParms by reference, no
  stream in the saved file holds the text, read with its parameters.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01AmsdeUCdTaD72jkDw2pJKY
Forms are read 8 deep. A damaged form that deep has every form it
names read as drawn, so one drawing through resources another page
shares keeps what it draws: a_damaged_form_at_the_depth_limit_has_its_forms_read.
Mutation testing found nothing pinned that boundary.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01AmsdeUCdTaD72jkDw2pJKY
The sweep flags the read in Pruner::form, moved there by this change. A /Subtype given by reference reads as no form wherever the workspace tells forms from images; resolving it is a change across all of them, and the comment says so.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01AmsdeUCdTaD72jkDw2pJKY
Autotagging a text-only page rewrites its content, marked, from what it
read. It read each stream through its filters with /DecodeParms only
when direct, so content behind a predictor whose parameters are given
by reference was read with the predictor's tag bytes in it, and written
back so: with eight-byte rows, a \0 every eight bytes split `24 Tf` into
`2 4 Tf` and the text's string, and the tool returned ok. Found by the
consumers review of the CodeRabbit round; inherited from main.

It now reads through DecodeBudget::decode_strict, which reads the
parameters in the document and is strict on damage as before, within
the same 16 MiB. pdfcraft-a11y no longer uses pdfcraft-filters.

Test: autotag_text_reads_content_with_parameters_given_by_reference.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01AmsdeUCdTaD72jkDw2pJKY
Content past 16 MiB says so, and content that does not decode strictly says that: autotag_text_says_why_it_cannot_read_a_page. Mutation testing found nothing pinned which message each gets.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01AmsdeUCdTaD72jkDw2pJKY
Read through DecodeBudget::decode_strict since cbc3fc221, a stream counts what each of its filter stages decodes against the 16 MiB, as a page's decode allowance does, where main bounded each stage alone. A page of 12 MiB behind ASCIIHex and Flate, with 9.1 MiB between them, was tagged on main and is refused now; the refusal said the decoded content exceeds 16 MiB, which it does not. It now says decoding the content takes more than that.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01AmsdeUCdTaD72jkDw2pJKY
A chain of 100,000 forms, whole or each damaged partway, is read only down to the depth limit, so the walk's recursion stays bounded: a_long_chain_of_forms_is_read_only_to_the_depth_limit. Nothing pinned it: with the depth step dropped in either of the two calls that go one form deeper (reading a form whole, and reading one that is damaged), or the depth check in the walk of damaged content made to apply only with the read-once guard, the walk follows the chain to its end and the stack overflows.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01AmsdeUCdTaD72jkDw2pJKY
Marking a figure decorative or a tag's content as an artifact refuses content that may be in a
damaged or unreadable content stream, or after the damage. The two errors now say so in one
clause, and that nothing was changed, which holds: both return before touching the document. The
mark-as-artifact tool's description names the case in a few words.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01AmsdeUCdTaD72jkDw2pJKY
Since autotagging reads page content through DecodeBudget::decode_strict, the reason it gave for
content that doesn't decode carried that error's own text, so it said twice that the data
couldn't be decoded: "content cannot be decoded strictly: stream data could not be decoded:
FlateDecode: …". It now gives the decoder's reason right after, as main does: "content cannot
be decoded strictly: FlateDecode: …".

Test: autotag_text_says_why_it_cannot_read_a_page now checks the decoder's reason follows the
prefix.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01AmsdeUCdTaD72jkDw2pJKY
Review round on #6525 (CodeRabbit, plus eliets2's first-pass finding):

- usable_quads returns usable values on the f32 grid, so readers folding
  f32 and f64 agree on the numbers as well as on the usable set
  (67108869 is 67108872 for every reader). New geom test pins it.
- The end-to-end redact agreement test gains the 4e38 case.
- Appearance drawing read /QuadPoints off the raw dictionary, dropping an
  indirect array or indirect entries the other readers keep.
  appearance::build now takes the document and resolves both, as redact's
  reader does; the restyle probes pass the document they already hold.
  New test pins both indirect shapes against the direct output.
- One shipped test block reformatted (rustfmt).

Drafted with AI assistance; verified by the contributor, fail-before
included.
The proof that came with #5557 decodes every stream it sweeps through `Ctx::decode`, which called
`decode_strict` with the stream alone. This PR's `decode_strict` also takes the document, to read
`/DecodeParms` given by reference, so the branch merged with main no longer built. `Ctx::decode`
now takes the document, and each of its callers passes the one it reads, so the proof reads such
a stream as redaction reads page content and forms. Before, it decoded the stream with the
parameters' defaults: a PNG predictor's rows kept their tag bytes, and a copy of the removed text
split by them went unreported (`a_survivor_behind_parameters_given_by_reference_is_found`). The
test helpers' streams are direct, so they pass an empty document.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01AmsdeUCdTaD72jkDw2pJKY
The redaction proof now reads a font's /ToUnicode with its /DecodeParms given by reference, and
`Metrics::from_dict`, which redaction takes the removed text from, read it only with direct ones.
Behind a predictor so described, the two got different text for the same codes, so a copy the
redaction rightly left counted as a survivor and the proof failed a sound redaction
(`a_unicode_map_behind_parameters_given_by_reference_is_read_alike`).

Every stream `from_dict` reads now goes through `Document::with_direct_parms`: the /ToUnicode; a
composite font's /Encoding, which sets how its codes split
(`cmap_streams_read_their_parameters_given_by_reference`); a subset's program and /CIDToGIDMap,
which say which of its CIDs have a glyph
(`a_subsets_program_and_cid_map_read_their_parameters_given_by_reference`); and the program a
simple font without /Widths is measured with
(`a_font_without_widths_measures_with_its_embedded_program`). Read without them, as on `main`,
such a font was measured with widths guessed from its name, so redaction removed glyphs the mark
doesn't cover and left some it does, and the proof, which also guesses from the name, passed.
Redaction now measures such a font with its program. The proof still guesses, so it fails that
redaction, as it already does on `main` when the program's parameters are direct.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01AmsdeUCdTaD72jkDw2pJKY
An update whose cross-reference section has no /Prev (ISO 32000-2 §7.5.6,
§7.5.8.2) cuts the earlier revisions, and the page tree with them, off the
chain. The signed PDFBOX-2720 and PDFBOX-2942 files have this shape: the
document model found no page tree and the renderer showed 2 blank pages
found by scanning for page dictionaries.

When the chain reaches only the newest section, the file holds another
startxref and the catalog's /Pages can't be read, both readers now rebuild
the table from the file's objects, as for a damaged file:

- cos: the existing reconstruction (newest in-file definition wins), a
  repair-log line saying why, and `Document::rebuilt_unlinked_update`.
- hayro-syntax (44): `root_xref` repairs the table before the page tree is
  read; `Pdf::rebuilt_unlinked_update` says so.
- render: an open warning, and the lazy structure reader accepts such a
  rebuilt file, so links, fields and bookmarks are read too.

A save rewrites the file in full (main's rule for reconstructed files).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Pq7HyCHD1R3rywQBPCHXZA
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Pq7HyCHD1R3rywQBPCHXZA
The renderer (hayro-syntax (44)) counted the sections its chain reached, and
a hybrid section's /XRefStm stream (ISO 32000-2 §7.5.8.4) counted as one, so
a hybrid update without /Prev was rebuilt by cos but not by the renderer. It
now asks what cos asks: whether the newest section names an earlier one
through /Prev. The /XRefStm stream belongs to the same update.

Tests: a synthetic hybrid fixture (pdfcraft-testkit), with and without
/Prev, in cos and render.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Pq7HyCHD1R3rywQBPCHXZA
A tiling pattern must have /Resources of its own, so its cell never
draws through the page's. Since #3680, Apply Redactions refuses a page
whose tiling pattern shows text or has no /Resources (pattern_guard), so
prune_type3_from_resources reads only soft mask groups now. Remove Hidden
Information runs no pattern guard: there, a page font that only such a
malformed pattern uses goes, as it did before this PR.

unlink_type3 still follows a pattern's cell, for patterns on pages the
redaction does not touch. Its test now puts the pattern on a second page.
prune_type3_from_resources decoded every appearance without resources of
its own into one list before counting any operators, each up to the
single-stream limit, so their sum had no bound. They are now decoded one
at a time against one page's DecodeBudget, as the page content is, and
passing it refuses the redaction.
Unlinking images nothing draws ran before the check for an empty result,
so a run where it was the only removal returned NothingToApply. The
remove-hidden edit and the Action Wizard step then dropped the working
copy, and the image stayed in the file. A run that unlinked anything now
succeeds and asks for the full rewrite.
A page's content streams are read as one, and forms named through a
reference-only object or listed in a Type 3 font's own resources are
read too, so the images they draw stay. A page tree that lists a node
twice is left alone. The pass repeats while names go, so an image only
an unused form drew now leaves with that form.
Removing hidden layers copied each array a configuration list names by
reference, once for every path that reaches it, so lists that name one
array many times could grow without bound. Each array, usage rule and
configuration named by reference is now cleaned once where it is, and
the lists keep the reference.
An /Order sub-list or radio-button group whose layers were all removed
stayed behind, an /Order heading with it, and a list or usage rule that
named a removed layer directly instead of in an array kept that layer
in the file. Such sub-lists and groups are now dropped, a top-level
list keeps its place, and a direct reference is treated as an entry.
Many usage rules can name one long layer list. Each rule, each list and
each layer is now looked at once, with sets for the membership checks,
so the work grows with the file and not with how often it repeats a
list. Which layers count as hidden does not change.
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Overlapping unterminated dictionaries could each be read up to 1 MiB. A shared
budget (the file length) is now taken off on every path; a spent budget keeps the
object a candidate, as before.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
full_save_order retained up to 64 MiB of parsed objects for the write pass, which
reachable_objects then dropped unused. It now asks for no retention.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
i32::MIN is outside Annex C's integer range and stays a real token (-2147483648.).
The generator now stops at -i32::MAX and a separate test pins the i32::MIN output.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
A rung equal to settings already run is not optimized again, and attempts counts
only runs. The target test uses a TempDir so it cleans up even when it fails.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
An embedded symbolic TrueType font with no /Encoding, no /ToUnicode and
no glyph names (post 3.0) read its codes as WinAnsi in text extraction.
A code now reads as Mac OS Roman when two things agree: the font's
Macintosh Roman (1, 0) cmap subtable selects the glyph the code draws
(ISO 32000-2 9.6.5.4), and that glyph sits where the standard Macintosh
glyph order puts the code's Mac OS Roman character. pdf.js's
issue2017r.pdf now extracts its accented line as PDFium does.

Windows subsets in the same order whose codes are WinAnsi keep their
reading, as do fonts that name their glyphs, fonts without a (1, 0)
subtable and fonts whose (1, 0) subtable selects other glyphs.
pdfcraft-fonts reads the same codes the same way for the editor.
Glyph selection is unchanged.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Pq7HyCHD1R3rywQBPCHXZA
Without the feature the lazy-DFA cache does not exist and the limit set in
UserRegex::new does nothing. Matching stays linear-time.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
The print, save and info buttons each have 4 pt after them (three gaps), so the
reserved width is 116 pt, not 112; the search field no longer overlaps the tabs.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
A field's fully qualified name is built from the partial names of the field
and its ancestors, and a child names its parent with /Parent (ISO 32000-2
§12.7.4.2, Table 226). A /Kids entry with /T whose /Parent does not name the
field listing it is now named up its own /Parent chain, in the form reader
and in document inspection, as PDFium and MuPDF name it: pdf.js's
issue15096.pdf lists its two radio fields as Group1, not Group1.Group1.

Fields with the same fully qualified name are one field (§12.7.4.2), so a
radio option that a later one of them draws is now chosen by the name: it is
written through the first field that draws it and the others turn off.
Before, only the first field's options were accepted.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Pq7HyCHD1R3rywQBPCHXZA
Only a non-empty signature value counts when deciding whether the first real
signature may drop /NeedAppearances.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
A glyph of a vertical-writing Type 0 font now spans its width w0 from
/W or /DW across the column (ISO 32000-2 9.7.4.3), instead of its outline's
width. Uneven outline widths made a narrow letter's line allowance smaller
than the spacing of widely tracked text, so a column of Latin capitals with
-40 Tc read as "A\nB\nCD\nE" and Find found nothing.

hayro-interpret patch 102: OutlineGlyph::vertical_glyph_width.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Pq7HyCHD1R3rywQBPCHXZA
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Pq7HyCHD1R3rywQBPCHXZA
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Pq7HyCHD1R3rywQBPCHXZA
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Pq7HyCHD1R3rywQBPCHXZA
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Pq7HyCHD1R3rywQBPCHXZA
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Pq7HyCHD1R3rywQBPCHXZA
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Pq7HyCHD1R3rywQBPCHXZA
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Pq7HyCHD1R3rywQBPCHXZA
A right-to-left show string in reverse order starts with the space that ends
its word (ISO 32000-2 §14.8.2.5.3), and a word processor pulls the next letter
back over it with a TJ adjustment a little larger than the space's width. The
letter then starts a hair left of the space, so sorting a line by x put the
space inside the word: copied and found text read "המדינ ה" for "המדינה" and
"כולם ?" for "כולם?" (pdf.js #11656), and Find missed both.

A blank glyph has no ink, so its box cannot place it against a glyph that
covers it. On a line with right-to-left text, when the glyph drawn right after
a blank is a right-to-left letter or a neutral, starts at or left of the
blank's start and covers at least half of it, the blank keeps its drawing
place before that glyph (§14.8.2.6.2: the space is a word break in the
character stream). Left-to-right text, a glyph that only touches the blank, and
a blank inside a letter-and-mark cluster keep their x order.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Pq7HyCHD1R3rywQBPCHXZA
# Conflicts:
#	crates/render/src/text/tests.rs
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Pq7HyCHD1R3rywQBPCHXZA
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Below about 100 pt of content width the stacked message wrapped to a column
thousands of points tall and pushed the hotbar off screen, which broke the
narrow-viewport hotbar picker test merged from main. That width is under the
smallest supported window, so the single row stays there.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
A `50 Tz` variant of the tracked vertical Latin test: the box across the
column is w0 x size x Th, centred on the column (ISO 32000-2 §9.4.4 and
§9.7.4.3), and the column still reads and finds as one line.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Pq7HyCHD1R3rywQBPCHXZA
An edit was refused after a content stream damaged partway, since what the damage hides can
change how the later streams read, the graphics state they draw in among it. A stream that
doesn't decode at all hides all of its operators, and an edit after one went ahead. It now stops
there too: text lines and blocks, and images (arrange, crop, transform, move), as selecting
objects on `main` already does for a page with such a stream. An edit in a stream before it goes
on, and the stream is kept as stored.

A file may give a stream's /Filter by reference, which isn't read here, so the case needs no
damage: `content_after_a_stream_that_does_not_decode_is_not_edited` has such a stream move the
text and image after it with a `cm`. `a_content_stream_that_does_not_decode_is_kept_and_edited_around`
becomes `..._kept_and_edited_before`, its edit now in the stream before.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01AmsdeUCdTaD72jkDw2pJKY
Adds the layout issue15096.pdf's last revision has: the two same-named
radio kids carry no /P (their page comes from /Annots), and both are
drawn on with /V that disagree (Off with /AS /Choice1, Choice2 with
/AS /Choice2). Choosing Choice1, Choice2 and Choice1 again by the name
leaves exactly the chosen button on after each save and reopen.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Pq7HyCHD1R3rywQBPCHXZA
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Pq7HyCHD1R3rywQBPCHXZA
Pruning reads forms 8 deep. Past that a form's /XObject dictionary was kept whole, but the forms
it draws weren't read, nor what they draw through resources of their own. When those resources
are shared with content that was read, they were cut to the names that content draws, and the
images only the deep forms draw were dropped from the part. This was so on `main` for a form read
whole, and in this change's reading of a damaged form too.

A walk the depth limit stops now leaves the file unpruned, as an overlarge Type 3 resource graph
already does: `nothing_is_pruned_past_the_depth_limit` (a form 9 deep, whole or damaged partway,
drawing a form that draws through resources the second page shares). That also settles a form
first reached past the limit and drawn again higher up, whose second reading `forms_seen` skips.
Down to the limit pruning goes on, under a page whole or damaged partway and with the form 8 deep
whole or damaged: `pruning_goes_on_down_to_the_depth_limit`.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01AmsdeUCdTaD72jkDw2pJKY
Main now reports unreadable objects from the parse of every object in
Document::reconstruct, so the pre-check that skipped that parse is gone.
The single-pass /XRef positions and the rarest-byte find stay.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
ISO 32000-2 §12.7.4.2: fields with the same fully qualified name have
the same value. Choosing a radio option by the name now writes the
choice as /V of a same-named twin that draws no button for it (its
buttons stay off), and of a field whose /Kids lists them without being
their /Parent, so it shares their name (issue15096.pdf's group, whose
/V was a stale Choice2). Exactly one button stays drawn on.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Pq7HyCHD1R3rywQBPCHXZA
Recognize text sizes each word from its own box and a guess at how much
of the em its letters fill, so one phrase's words come out several
points apart (9 to 15.6 pt on a LaTeX scan). Editing a phrase drew and
saved every word at its own size.

An invisible block's size is now the median of its words' sizes
(TextBlock::phrase_size). The styled rewrite draws every run at it and
scales each word's width calibration by its own size over the phrase's,
so words keep the width the scan shows them at; the single-style
rewrite (a new family or bold) uses it in place of the first word's
size; and the inline editor lays each word out at it while typing.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Review follow-ups:
- The phrase's median counts each word once: neighbouring words OCR
  sized alike share a run, so 15, 9 and 9 pt gave 12 pt, not 9.
- The styled rewrite's width calibration is not clamped again after the
  phrase ratio, so a word sized at more than twice the phrase keeps the
  width the scan shows it at.
- The single-style rewrite (a new family or bold) scales the first
  word's Tz by its own size over the phrase's, so the line is as wide
  whatever size OCR guessed for that word.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Moving or resizing an image in a stream before one damaged partway, or one that doesn't decode,
goes on, as the edit changes no stream past it. But the image's `q cm … Q` can close past that
stream, and what the stream draws inside the scope is unknown. When the image looked alone there,
the move changed the scope's `cm` in place, and anything the stream draws in it moved too: a
square drawn there in the image's `cm` moved with the image, as on `main`.

A scope still open after such a stream now counts as one that paints something else
(`first_op_after_damage`, read by `image_scopes` and `enclosing_transform`). An operator counts
by its keyword there: a page may split its content between an operator's operands and its
keyword, so a `Q` whose operands come before the hidden stream still closes after it. The move
gives the `Do` a `cm` of its own and leaves the scope's as it was; with a clip on the image, the
clip stays, as one the stream may share. A crop goes around the `Do` alone rather than at the
scope's start, where it would clip what the stream draws. A crop PdfCraft put in such a scope
isn't removed, and the refusal says why rather than that there is no crop. Arrange is
unchanged: it takes out the scope it finds, which changes the stream past it and is refused.

`an_image_whose_scope_closes_past_a_stream_not_read_whole_moves_alone`: a stream that doesn't
decode and one damaged, each also with the square's operands before what is hidden (ending the
stream before, or in the damaged stream before its damage); the image with no clip, a clip, and a
PdfCraft crop; moved, cropped, and its crop removed. A scope that closes in a damaged stream
before its damage still takes the move in its `cm`.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01AmsdeUCdTaD72jkDw2pJKY
A text object can run on from one content stream into the next. When it is still open at a
stream damaged partway, or one that doesn't decode, the text that stream shows starts where the
text before it ends: editing that text moved what the hidden stream shows, its advance changed.
An edit in the text object before such a stream went ahead, as edits before the damage do.

`rewritten_contents` now refuses a change from the start of a text object still open at the
first stream not read whole (`changes_text_open_into_damage`); a change before that text object
goes on. `text_a_stream_not_read_whole_goes_on_from_is_not_edited` (the hidden stream damaged
from its start and one that doesn't decode, between the text and a stream closing it or last on
the page; a line in an earlier text object of the same stream is still edited).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01AmsdeUCdTaD72jkDw2pJKY
Marking a tag as an artifact, or a figure as decorative, refused content after a stream damaged
partway, placing each operator by its first operand. A page may split its content between an
operator's operands and its keyword. When a `BDC`'s operands end the stream before one damaged
from its start, or one that doesn't decode, the keyword the page reads is in what wasn't read;
joined without it, those operands and the next tag's read as one `BDC`, and marking rewrote the
stream after the hidden one, the next tag's `BDC` taken out of it.

An operator now counts after damage by its keyword, and one whose operands and keyword lie on
either side of a stream not read whole is refused: that stream may end them itself.
`a_tags_operands_before_a_stream_not_read_whole_are_not_rewritten` (damaged from its start, and
not decoding). A tag entirely after a stream that doesn't decode is still marked: such a stream
holds all of its content, between tokens. `accessibility_mark_tag_artifact`'s description names
content a stream that doesn't decode may hide or change too, not only a damaged one.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01AmsdeUCdTaD72jkDw2pJKY
A field dictionary that also carries a zero-area widget and lists widget
kids now has the kids as its buttons (ISO 32000-2 §12.7.4: field and
widget merge only for a single widget). Widget annotations listed beside
named kids, and nameless /Fields entries whose /Parent /Fields doesn't
list, are read as buttons of that parent's name.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Pq7HyCHD1R3rywQBPCHXZA
The box a comment without an appearance is drawn in now takes in the
points it is drawn through: /L and its leader lines (ISO 32000-2
§12.5.6.7, Table 178), /Vertices (§12.5.6.9, Table 181) and /InkList
(§12.5.6.13, Table 185), as text markup already did with /QuadPoints.
Once a point lies outside /Rect, the box also takes in half the
border width, line endings and cloudy borders, up to the crop box. A
/Rect that holds every point is kept as written.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Pq7HyCHD1R3rywQBPCHXZA
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Pq7HyCHD1R3rywQBPCHXZA
A neutral taken back over a space keeps the space before it only when the
nearest strong glyphs on both sides are right-to-left (or the line edge), so a
parenthesis opening Latin text is placed by x as on a line without Hebrew.
Combining marks drawn between the space and its letter no longer stop the
move, and the space, the marks and the letter keep drawing order. The pass
now runs after zero-width marks are merged, since that merge relies on x
order. Adds tests for the pdf.js #11656 content stream and for two
taken-back spaces on one line.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Pq7HyCHD1R3rywQBPCHXZA
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Pq7HyCHD1R3rywQBPCHXZA
The letter after a blank and the marks drawn between them are gathered from
both sides of the blank in x order, so a tanwin pulled back over the space
with its alef starting at the space's own start still moves the space before
the word (PDFBOX-5487). Numbers beside a taken-back neutral count as
right-to-left, as in the bidi pass (UAX #9 N1), so '15)' followed by a full
stop keeps the stop after the number.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Pq7HyCHD1R3rywQBPCHXZA
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
- fill: the first same-named field's read-only flag still applies when a later one draws the
  choice, and no radio field of the name is written when any of them is read-only.
- enumerate: a nameless /Fields widget its parent already claimed is not read again as a nameless
  field; one its parent's /Kids omits is still read on its own.
- walk: only kids whose /Subtype is /Widget replace a merged field's own widget.
- inspection: a kid whose /Parent is another field is named from that chain even under a direct
  listing field, and widget annotations beside named kids are listed as a field of the parent's
  name (ISO 32000-2 §12.7.4), as the form reader lists them.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Pq7HyCHD1R3rywQBPCHXZA
Once the flag is gone, viewers show each field's stored appearance, so a
filled field whose /AP /N draws no text (a /Tx BMC EMC placeholder or a
stale look) went blank. Session::sign and sign_with_timestamp now sign the
copy with_field_appearances draws, as the document is displayed, when the
signature is the first. A field whose own appearance draws text keeps it
where PdfCraft's drawing would show a character as `?`.

The earlier-signature check is now pdfcraft_sign::has_signatures, shared by
the engine and the signer, and it also finds signed fields without a /T.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Tests only: a CID missing from /W is /DW wide across the column (ISO 32000-2 §9.7.4.3), widths
are looked up by CID through an embedded vertical CMap, and two columns of /DW-wide letters set
13.5 pt apart at 20 pt read as two lines.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Pq7HyCHD1R3rywQBPCHXZA
The web build has no model files: it downloads them. `Ocr::from_bytes`
loads the pair only when each file's SHA-256 matches ATTRIBUTION.toml
(rten's ONNX parser can spin forever on bytes that aren't a model), and
`OcrsRecognizer::from_ocr` wraps the result as an engine that stays
available without files.

The web build's workers send what they read back as JSON:
`lines_to_json` / `lines_from_json` carry each word's box and, when it
has one, its confidence; the reader credits the words to the engine the
worker ran, and refuses anything else (a missing or non-finite number,
too many words) with an error.
`OcrJob::steps` splits `run` where the engine reads: `OcrSteps::next_page`
renders the page, crops it to the region and preprocesses it, an engine
reads `OcrShot::image`, and `OcrShot::place` maps the words back through
the preprocessing and the crop. `run` is the same loop with
`OcrShot::read`, so desktop results don't change. With only one engine,
every strategy is that engine's reading, which is what the web build's
workers return.

`FileJob` splits `recognize_file` the same way (open, read, finish),
keeping the skip-files probe on opening. `ocr::install` puts a recogniser
loaded from downloaded bytes in the engine cache, so `recognizers`,
`available`, `engine_unavailable_reason` and `engines_status` see it.
The web build downloads the OCR models the first time Recognize Text (or
Edit on a scanned page) needs them, installs the engine, and reads pages
in up to two Web Workers (apps/pdfcraft-ocr-worker) built with WebAssembly
SIMD, so the page keeps responding. The page prepares each page and sends
its image to an idle worker, which runs ocrs and sends the lines back.
Where no worker can start, the page reads with the installed engine.
Answers go back to the run that sent the page (ocr_route), so a single
run and a batch can overlap.

Recognize Text in Multiple Files on the web keeps main's skipped-files
bucket, low-confidence notes and cancel between files. Files chosen
before the models are in wait for them. A scan Edit finds while no engine
can read it waits for the models instead of counting as no scan.

Trunk builds the worker beside the app; `cargo xtask web-worker-link`
hashes its files and names it in index.html. The web zip ships the
models in models/.
Hosting notes for the models and workers, the platform parity text, the
ocr.ocr-web parity entry (now citing the ported tests, with the known
limits: the page runs the cleaning steps, the Verify screen re-reads on
the page, ocrs only), and the roadmap logs.
In the web build, Verify's Run OCR and its page/region re-reads never
started the model download. On a fresh page they failed at once with the
desktop's "set PDFCRAFT_MODELS" message, and worked only after Recognize
Text had downloaded the models.

Both now start the download (web_models_missing), say "Downloading the
text recognition models…" and remember what was asked; poll_models runs
it once the models land, or shows "Couldn't download the text
recognition models: {e}" when the download fails. Nothing runs if the
screen was closed meanwhile, and an engine the browser can never run
says why instead of waiting.

Tests: models_landed starts a waiting run and a waiting re-read, reports
a failed download without touching the review, and runs nothing for a
closed screen. The first three fail without the fix. Checked by hand in
Chrome: a fresh page, Verify > Run OCR downloads the models and reads
the page (47 words on a real scan); with the models missing it shows
the HTTP 404 and a second click after restoring them works.
page_modules_read_the_pointer_only_through_page_pointer treats any file
that mentions `.router.` as a page module, and failed on
workflows/ocr_web.rs: its `self.router` is the OCR pool's
ocr_route::Router, which hands each page's text to the run that sent it,
not the pages' PageRouter. Adding the file to PAGE_MODULES would put
the workers under the page-input checks, so the field is renamed
`runs` instead.

Tests: the page_pointer tests and all 808 pdfcraft-ui-egui lib tests
pass, and cargo check for wasm32-unknown-unknown builds.
- Text markup reaches past its quads by half its line width, and a squiggle by its peaks over a
  low quad; the drawing and the reach share the stroke sizes.
- Ink takes in the turning points of its smooth curve (cubic extrema), not only its points.
- A line takes in its caption's box and the caption tick.
- Points off the page whose strokes reach onto it widen the box as far as the page shows.
- page_box_listing reads the page /P names when that page lists the comment (ISO 32000-2
  §12.5.2: one page lists an annotation), inheriting its boxes up its /Parent chain, before
  walking the page tree.
- AppearanceCache entries carry the crop they were drawn for, so a changed crop box redraws.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Pq7HyCHD1R3rywQBPCHXZA
- reverse_rtl applies UAX #9 W1: a nonspacing mark takes the class of the letter it rides on,
  so a Latin accent at a word end on a right-to-left line stays after its letter.
- keep_taken_back_blanks reads a line's edge as the line's direction (UAX #9 X10) instead of
  right-to-left, so a neutral at the edge of a mostly left-to-right line keeps its place.
- Rows of overprinting runs are ordered by where their clusters start, so a riding mark that
  reaches left of its letter cannot put its run first.
- The two-taken-back-spaces test checks the untrimmed text.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Pq7HyCHD1R3rywQBPCHXZA
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Pq7HyCHD1R3rywQBPCHXZA
Tests only: the page model and the renderer size a page whose /Pages parent carries the
/MediaBox (ISO 32000-2 §7.7.3.4) at that box, 300 x 400, not US Letter.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Pq7HyCHD1R3rywQBPCHXZA
Tests only: a named /Encoding or /BaseEncoding keeps its own reading, /Differences without a
base encoding still gets the Mac OS Roman reading under its own entries, and a Symbol font keeps
no implicit mapping (ISO 32000-2 §9.6.5.4, §9.6.6.1).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Pq7HyCHD1R3rywQBPCHXZA
Tests only: a complete update, trailing data without objects and a short cut-off update each
have their own test, and the trailing-data case asserts the model's page count as well.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Pq7HyCHD1R3rywQBPCHXZA
Tests only: the search for the smallest JBIG2 allowance that draws the first picture renders the
first page alone and halves the range, about ten draws instead of up to two thousand.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Pq7HyCHD1R3rywQBPCHXZA
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Pq7HyCHD1R3rywQBPCHXZA
- `has_signatures` treats a /Contents of only zero bytes as unsigned and finds
  signature dictionaries in one pass over the file (field /V values, plus
  /ByteRange dictionaries and document timestamps) instead of two.
- Before the first signature, a field's own appearance counts as drawing text
  only if it or a form XObject it paints shows text; a background-only XObject
  no longer keeps an appearance that would leave the field blank. The look
  inside opens at most 16 streams; past that it counts as text, as before.
- Tests: zero-filled placeholders (hex and literal), document timestamps, a
  widget without /AP, background-only and text-drawing XObject appearances
  with an unencodable character.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
- resolve_marks applies UAX #9 W1 only to a mark riding on the glyph before it (its anchor), so a
  Hebrew mark placed after a Latin letter keeps its own class and stays with its word.
- reverse_rtl takes the paragraph direction before marks take their letter's class, as
  keep_taken_back_blanks counts the line.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Pq7HyCHD1R3rywQBPCHXZA
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Pq7HyCHD1R3rywQBPCHXZA
Tests only: a /MediaBox two /Pages levels up, with its origin away from 0 0, gives the page its
crop, size and view matrix, and the renderer sizes such a page at that box.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Pq7HyCHD1R3rywQBPCHXZA
Tests only: a JPEG 2000 failure kept for one size is still kept after drawing at another, and
the second page draws the picture decoded on the first and the one the first page's allowance
left out, through one cache.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Pq7HyCHD1R3rywQBPCHXZA
- Pdf::new_with_password reads the table the last startxref names first: a wrong password is
  refused through it, and the rebuilt table is taken only when it is encrypted exactly when
  that table is.
- Tests: an encrypted file with a cut-off update needs its password; data after %%EOF holding
  an object header reads as the document model reads it (both take its page).
- vendor/README.md lists the split tests.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Pq7HyCHD1R3rywQBPCHXZA
qualified_name reads each ancestor through Document::resolve, which follows an object that is
nothing but a reference to another, as document inspection does, so both name the kid the same.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Pq7HyCHD1R3rywQBPCHXZA
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Pq7HyCHD1R3rywQBPCHXZA
reverse_rtl's paragraph direction and keep_taken_back_blanks' line direction both leave glyphs
made only of nonspacing marks out of their counts (is_nonspacing_mark), so a few Hebrew letters
carrying many points don't turn a mostly Latin line right to left.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Pq7HyCHD1R3rywQBPCHXZA
Tests only: an intermediate /Pages node's box wins over the root's, and content is placed by a
box whose origin isn't 0 0.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Pq7HyCHD1R3rywQBPCHXZA
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Pq7HyCHD1R3rywQBPCHXZA
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Pq7HyCHD1R3rywQBPCHXZA
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Pq7HyCHD1R3rywQBPCHXZA
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Pq7HyCHD1R3rywQBPCHXZA
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Pq7HyCHD1R3rywQBPCHXZA
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Pq7HyCHD1R3rywQBPCHXZA
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Pq7HyCHD1R3rywQBPCHXZA
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Pq7HyCHD1R3rywQBPCHXZA
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Pq7HyCHD1R3rywQBPCHXZA
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Pq7HyCHD1R3rywQBPCHXZA
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Pq7HyCHD1R3rywQBPCHXZA
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Pq7HyCHD1R3rywQBPCHXZA
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Pq7HyCHD1R3rywQBPCHXZA
A widget's /AP /N that is neither a stream nor a state subdictionary with
a stream for /AS draws nothing (ISO 32000-2 12.5.5, Table 170), yet the
display copy treated any /N as the file's own appearance. Text, choice and
push-button widgets whose /N is null, a form XObject dictionary without its
stream data, or a subdictionary without /AS now get the drawn appearance a
widget without /N gets, and check boxes and radio buttons whose state entry
is not a stream are drawn from /MK. The test is the same one the renderer
uses to pick the stream.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Pq7HyCHD1R3rywQBPCHXZA
XFDF export and import carry each comment's formatting as XFDF 3.0
(ISO 19444-1) names it: contents-richtext (/RC), defaultstyle (/DS),
width/style/dashes (/BS, or an older /Border), cloudy style and
intensity (/BE), rotation, justification (/Q), fringe (/RD) on every
type, leaderLength/leaderExtend/leader-offset and the caption
attributes of lines, the caret symbol and redaction overlay text.

A stamp's appearance travels as <appearance>: base 64 of the
DICT/STREAM/ARRAY/NAME/INT/FIXED/BOOL/STRING/NULL XML other tools
write, with stream data in DATA as XFDF 3.0 "Stream encoding" gives it.
Copy and paste carries every comment's appearance. Import keeps an
appearance it can read (never on a redaction mark), records an
unreadable one in Report::skipped and draws the comment instead; it is
bounded at 16 MiB per appearance, 64 MiB per import, 64 levels and
100,000 values.

FDF export keeps /AP with the forms and images it draws (ISO 32000-2
§12.7.8.3.4); FDF import copies those streams into the document, once
when shared, only for comments that land on a page, and no longer
nulls values past 16 levels (a gradient stamp's shading functions sit
deeper).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Pq7HyCHD1R3rywQBPCHXZA
A page or page tree reached from a comment's appearance is written as
NULL in <appearance>, as FDF export already nulls source /Page and
/Pages dictionaries, so the XFDF never carries the document's pages.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Pq7HyCHD1R3rywQBPCHXZA
FDF export now leaves /AP out of every comment but a stamp, as XFDF
3.0 gives <appearance> to stamps only: a text box, square or line
carries its formatting entries (/DA, /DS, /RC, /Q, /Rotate, /BS, /RD)
and is drawn where it is imported. FDF import still keeps any /AP the
file carries.

XFDF export writes a comment's /IT under both spellings XFDF files
use, `intent` and `IT`, and import reads either, so a typewriter or
callout text box from a file that writes `IT` keeps its intent.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Pq7HyCHD1R3rywQBPCHXZA
M2.6: never rewrite a content stream from the part before its damage
fix(filters): inflate with zlib-rs everywhere, and charge the image allowance for what Flate reads
contributors: set names for vangel76
Add Crop pages to the document quick toolbar (#5330)
Select the bookmark title when creating or renaming a bookmark (#5529)
perf(cos): write hex strings, integers and references without the formatter
Documentation only.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Pq7HyCHD1R3rywQBPCHXZA
# Conflicts:
#	crates/filters/src/lib.rs
UI: allow a half-screen window, and fold the mode bar when narrow (#1913)
Optimize: optional target_bytes for doc_optimize (#1889)
perf(cos): speed up byte searches during cross-reference reconstruction
perf(cos): write_full keeps the objects it parsed while numbering
Redact: mark text by caller-supplied regex (#1886)
Signing a form that sets NeedAppearances no longer makes Acrobat ignore the signature
Main merged #1977's rules.rs (which destructured content_streams as
(entry, data) tuples) after content_streams started returning
ContentStream records, so pdfcraft-edit stopped compiling.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Pq7HyCHD1R3rywQBPCHXZA
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Pq7HyCHD1R3rywQBPCHXZA
Refs #2964, #3005
fix(edit): restore page rules after the content stream API change
Conflict: crates/edit/src/rules.rs, where main joins the content streams with
crate::text::joined; that line replaces this branch's earlier build fix, so the file is main's.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Pq7HyCHD1R3rywQBPCHXZA
#5154 and #6289 each passed on their own base; together page_rules read content_streams as pairs after it began returning ContentStream.
# Conflicts:
#	parity/acrobat-features.toml
Clippy's nonminimal_bool asks for is_none_or: a glyph with no outline range, or an empty one, is skipped as before.
text/forms/expand.rs was a drifted copy of ungroup.rs. It is gone: text preparation now opens each
placement with crate::ungroup, so Ungroup and preparation share one implementation.

What the copy added is now in ungroup.rs: a Refusal that says whether the form itself or only
this placement is the reason (intrinsic) and whether a rename alone was the problem (renaming),
the shows_text result, merges_without_renaming, refusals for unreadable /Resources and /Group
entries and arrays with a non-number, a cap on resource counts and q nesting, a total size for
pattern copies, and ending an unfinished path with the form. Ungroup's newer behaviour stays:
names are read from the operand the renderer uses, so a surplus operand is renamed correctly
rather than refused; default colour spaces are compared with the page's; and a group blending
in DeviceRGB on a page that names no blending space is drawn inline, as Ungroup does, rather
than refused as the copy did.

What a form draws is now read within one decode allowance shared with the form itself, so a
crafted group can no longer make one Ungroup decode 64 MiB for each of 1024 nested forms. A
group whose content can't be checked is refused; for a form without a group, blend modes inside
don't matter, and content that can't be checked only means preparation can't tell whether it
has text (shows_text is None), which it reports.
A form with resources of its own and no /DefaultRGB (likewise Gray, CMYK) draws its device
colours as they are. Inline, the page's resources apply, and a /DefaultRGB there would remap
them (ISO 32000-2 §8.6.5.6). Only the form having an entry the page lacks was refused; now either
side having one the other doesn't share is.
A Q with no q before it restores nothing; the renderer ignores it. The check for translucency at
the Do took it as restoring the opaque default, so a transparency group drawn after /H gs Q was
inlined although it is drawn translucent.
Ungroup and text preparation now check the placement in one place, ungroup_ops, which is given
the page's operators and the Do's index:

- The form is looked up by the name the Do writes, byte for byte. Ungroup looked it up by the
  picture's display name, a lossy UTF-8 copy, so a form named /F#FF was 'missing'.
- A Do with more than one operand is refused: the picture was read for its first, the renderer
  draws its last.
- A Do inside BT…ET or an unfinished path is refused. Inline, the form's text objects would be
  nested in the page's, and its clip would take in the page's path. Preparation already refused
  these; Ungroup didn't.
The repair reads the page's own fonts. It ran only after a group was expanded, so the same
damaged map on text that was never grouped kept reading wrong. Preparation now repairs it
whenever it finds it, recorded in the repair log as before, and PrepareText (text_prepare)
succeeds when it repaired a map even with nothing to expand. A repaired map no longer matches
the damage's fingerprint, so preparing again changes nothing.
Edit text queued Prepare grouped text for every page that came into view, after a full
preparation on a copy of the document on the UI thread to see whether it could. Scrolling cost
that work per page, marked the document changed, added undo steps and inlined shared forms
although nothing was edited.

Now nothing happens while pages are shown. A click (press and release, no drag) on grouped
artwork, routed to the picture by the page router as before, asks once whether that artwork
shows text preparation would open (text::forms::grouped_text_at, which reads the page without
copying or changing it). If it does, the page is prepared as one undoable step and the
paragraph at the point clicked opens for typing. A logo or a group preparation would refuse is
selected as before, with no message and no change. A drag still moves the selected group, and
while a paragraph is open or an edit is waiting the click selects as before. text_prepare is
unchanged.

The docs and the parity entry no longer say text is prepared on entering Edit text.
The hybrid update test now checks that the update's page and catalog
win and that the repair log says why, and that the linked file keeps
both revisions; the save test checks that the reopened file keeps the
update's page and catalog.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Pq7HyCHD1R3rywQBPCHXZA
A grid drawn with inside lines only no longer gets a box in Word.
A predictor whose parameters are an indirect object left its row tags in
the decoded font program and glyph map, so the Thai map repair, the glyph
codes and the shaper read the wrong bytes. Resolve the parameters as the
other font readers do, keeping the decode budgets.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Pq7HyCHD1R3rywQBPCHXZA
* M10: Correct recognized text keeps the window's tabs and toolbar and shows the page (ocr.correct-recognized-text)

Scan & OCR > Correct recognized text drew the OCR Verify screen before
the tab strip and the mode bar and returned, so it took the whole
window: the tabs, menus and toolbar went away (on macOS the traffic
lights landed on the PAGES caption). Its source pane drew only a
document the screen had recognized itself, so opened on a document
(already searchable or not) it was blank until a run.

The screen now takes the document area under the tab strip and the
mode bar; the command palette shows over it too. Before a run, and
after a review is accepted or rejected, the source pane shows the
document in front at the page it is on. Another mode or the Home tab
puts the screen away, as Exit does; the command brings it back with
its review as it was. A run now shows the first page it reads (the
current page for that scope) instead of page 1.

Fixes #6485

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Pq7HyDBXygTf7jcqdsjcfn

* M10: OCR Verify keeps its review when the document is closed and shows a page it read (ocr.correct-recognized-text)

- Closing a document whose OCR review is still open (File > Close, the tab's
  Close, the snapshot bar's Close, Close all) brings the review forward with
  a notice to accept or reject it first, instead of closing the document under it.
- After a run the source pane shows a page that was read, over one skipped for
  its text; a run that read no pages keeps the page it was started on.
- PAGE shows the page's number over the document's page count.
- A cached preview of another document or page is dropped before rendering.
- The re-entry test checks a review and its correction survive another mode
  and Correct recognized text.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Pq7HyDBXygTf7jcqdsjcfn

* M10: OCR Verify holds Quit for an open review, lets a failed Accept be rejected, and survives a worker that cannot start (ocr.correct-recognized-text)

- Quit and closing the window bring a document under OCR review forward
  with its review instead of exiting, as Close all does.
- Reject also throws away a reading whose Accept failed.
- A run or re-read whose worker thread cannot start leaves the screen
  usable and an existing review as it was.
- A page preview that fails to render is not rendered again on every
  repaint, only once the document changes.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Pq7HyDBXygTf7jcqdsjcfn

* M10: the web build's leave warning asks while an OCR review is open (ocr.correct-recognized-text)

A browser reload or a closed tab cannot be held the way Close and Quit are,
so the beforeunload flag the web page reads now also counts an OCR review
that is not yet accepted or rejected, by the same check that holds Close all
and Quit.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Pq7HyDBXygTf7jcqdsjcfn

* M10: keep LF line endings in the files the leave-warning change touched (ocr.correct-recognized-text)

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Pq7HyDBXygTf7jcqdsjcfn

* M10: an OCR review holds a deferred close, a run on another document, and the embedding page's leave warning (ocr.correct-recognized-text)

A close that waited for a save checks again for an OCR review started
meanwhile: the reviewed document stays open, and Close all and Quit stop.
Run OCR on another document while a review is held is refused with a
notice, so the review keeps its corrections. The embedding page's
`host_dirty` hears an open review as work at risk, as the reload warning
does.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Pq7HyCHD1R3rywQBPCHXZA

---------

Co-authored-by: mtlcl410 <chuanlin@niar.org.tw>
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
# Conflicts:
#	parity/acrobat-features.toml
a_file_job_reads_in_two_halves compared the two saves' bytes, which carry the
modification time, so it failed when the saves landed a second apart.
width * height * 4 could wrap for sides near u32::MAX and let a short buffer
through as the right size; the product is now checked.
M10: adopt file-backed Windows printing with approved dependencies and the crash fix
Visible text keeps its runs' sizes, and a phrase whose size is NaN, zero,
negative or infinite falls back to its words' sizes.
A legacy subset can keep a cmap of its own that maps only Latin, so the
editor program returned it unchanged and an edit such as a consonant with a
tone mark failed although the subset keeps both outlines. Add the PDF's
verified Thai and alternate mappings to the editor-only cmap where the
program's own lacks them, keeping its existing mappings.
Every resource name of a page's Thai subsets decoded and kept its own copy
of the font program, so many names of one large program could hold about
2 GiB for a single edit. Names of one font object now share its program,
the subsets of an edit are decoded within a shared 64 MiB allowance, and
an edit that needs more is refused with the reason, changing nothing.
A space that no subset of the face encodes was drawn as a quarter-em gap,
narrower than the text-space fallback the source font uses elsewhere.
Keep each subset's source space with it and use that for the gap.
The first program of the face that could shape a span was chosen even when
its embedding permissions forbid embedding it, and the edit then failed
while a later program of the same face could have been used. Check that a
program may be embedded before choosing it.
The resource limit was checked before a form's resources were merged into
the page's, so one form could take the page past it and every later form
was then refused, even one whose resources were all the page's already.
Check the merged dictionary instead.
The source shaper refused any Thai text in a program without GPOS mark
positioning, so a row of Thai digits failed although nothing in it is set
on a base. Require the positioning only for text with a vowel or tone
mark, or Sara Am.
A complete program of the source's family in another style, such as one
its descriptor marks bold, stopped the bundled Regular Thai outlines from
completing a Regular subset, and the text was then drawn in that bold
program. Count only a program in the source's style, as the subset
completion itself does.
A legacy THSarabunPSK subset whose map gives a shifted alternate (glyphs
342-368) as the nominal mark, its nominal glyph dropped, shaped that
alternate wherever the nominal was needed, drawing the mark out of place.
The shaping copy now reaches an alternate only through its presentation
form, so text that needs the dropped nominal is refused instead.
storytold/craft-contrib#40 (RFC-0026) added Unicode-3.0 to the shared list.
# Conflicts:
#	ROADMAP.md
Word and HTML exports name the document's language (#5498, merged with main)
Brings the Thai word-breaking branch up to date with main. The only conflict was the edit.add-text parity entry: main's evidence and notes (print inks) are kept, with the new Thai test added and the Thai wrapping sentence updated.
An edited OCR phrase is drawn at one size (#5454, with the requested test)
OCR in the web build (#1641, merged with main, two fixes)
# Conflicts:
#	docs/roadmap.md
storytold/craft-contrib#40 (RFC-0026) added Unicode-3.0 to the shared list.
- XFDF export: the finished appearance XML is checked against the import limit;
  a page whose /Type is a reference stays out; stream data with a carriage
  return goes out as hex (XML 1.0 §2.11 end-of-line handling); where /AS selects
  one of several states, the selected stream is written.
- XFDF import: dash arrays of any length come back; consecutive <br/> keep the
  blank line in /Contents; rich text written as text on a comment with no
  /Contents comes back as /RC; namespace prefixes rich text inherits are declared
  on it; appearances share one budget over the import (now tested through it).
- Redaction marks: the file's /AP and a /BE (none in ISO 32000-2 Table 195) are
  dropped and the mark is drawn by PdfCraft; one with an area it cannot draw is
  refused rather than placed invisible.
- FDF import: /P is not copied (the page is /Page, §12.7.8.3.4 Table 254); a
  comment nested past 256 levels refuses the file instead of nulling part of its
  appearance (a chain of 60 forms comes back whole).
- Engine: appearances an import left out go to the repair log for Import and
  Paste comments.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Pq7HyCHD1R3rywQBPCHXZA
M7: edit grouped Thai text with one click, on Ungroup's form expansion (#6249 + review fixes)
thai_lines' row filling moves into wrap_words, so both it and thai_words are
tested with synthetic advances where no Thai face is built in.
redact: treat unusable /QuadPoints as absent in every reader
- import and import_comments work on a copy, kept only on success.
- XFDF export shares the 64 MiB appearance budget import reads over all
  comments; import refuses a normal appearance of several states (XFDF has
  no /AS to select one, ISO 32000-2 §12.5.5).
- FDF import nulls a page or page tree an appearance reaches (/Type resolved).
- Rich text: an inherited default namespace other than XFDF's is declared,
  and a namespace declaration with spaces around `=` is recognised.
- A redaction mark's /Rect grows to hold its quads, so the outline shows.
- The repair log gets one line per import (first five, then a count), and
  no longer claims the comment is drawn.
- Tests: the sixty-form chain, and the redaction outline is stroked.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Pq7HyCHD1R3rywQBPCHXZA
Resolved against main's ruled tables (#5154, #6626) and clipboard export:
the new tests pass main's rules argument to tables(), and the parity
notes keep both sides' wording.
Review asked for the near misses of joining wrapped lines. A short line,
a numbered item and a symbol-font bullet were already covered; these add
a line beside another column and a poem between paragraphs.
A page whose only text is short unpunctuated lines takes its longest
line for the right edge, so those lines join into one paragraph.
# Conflicts:
#	Cargo.lock
# Conflicts:
#	ROADMAP.md
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Pq7HyCHD1R3rywQBPCHXZA
M7: break Thai rows between dictionary words (#3492, merged with main)
# Conflicts:
#	crates/fonts/src/pdf/tests.rs
# Conflicts:
#	crates/annot/src/appearance.rs
#	crates/annot/src/draw.rs
#	crates/annot/src/info.rs
#	crates/annot/src/modify.rs
Refs #5989. Ctrl/Cmd+A selects every object on the page; copied objects paste as page content that draws the same, within a document or into another, with organize's bounded deep copy across documents. Headless: object_copy, object_paste, object_move all.
Refs #2354. Thai typed into these tools is shaped and drawn from the Thai face through Add text's writer (now pdfcraft_fonts::shaped_text), searchable and copyable, instead of being written as ?????; a build without a Thai face refuses it with a message.
view.render-annotation-appearances: main's appearance builder now takes the document and draws
only the usable /QuadPoints quads (resolved, malformed ones skipped). The markup reach reads the
same quads, so an indirect or partly malformed /QuadPoints that is drawn also widens the box;
the far-off-page test quads move from 1e308 (past the PDF real range, never drawn) to 1e30.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Pq7HyCHD1R3rywQBPCHXZA
- Appearance export checks a stream's size in the encoding it is written in
  (printable data as text, not as hexadecimal).
- Copy no longer writes a redaction mark's appearance, which import never
  reads, so it does not use up the budget the other comments share.
- Rich text: a namespace declaration inside another attribute's value is not
  a declaration, a '>' in a value does not end the start tag, and a prefixed
  root keeps the default namespace its unprefixed descendants inherit.
- import_comments never applies the file's field values, whatever the
  placement arguments (a non-finite offset used to fall through to them).
- The repair-log line for skipped appearances escapes control characters and
  cuts each description to 200 characters.
- Tests for the shared budget through the export loop, a near-limit appearance
  that round-trips, and the six-comment summary.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Pq7HyCHD1R3rywQBPCHXZA
# Conflicts:
#	crates/annot/src/draw.rs
#	crates/annot/src/tests.rs
# Conflicts:
#	crates/annot/src/draw.rs
Deleting one word from a recognised line covered the whole line and
redrew it in Helvetica, though nothing but that word had changed.

A text edit of an OCR phrase now covers and redraws only the words it
changes; the rest stay as the scan shows them, still invisible text. Each
changed stretch is rewritten as a phrase of its own, on a copy of the
document, and the edit falls back to the whole-phrase rewrite when words
can't be told apart or a kept word would move. A deleted word leaves its
gap. A replacement a little too wide is squeezed across to fit, by up to
a quarter; wider than that it takes over the next word. A change of
formatting, or a typed line break, still redraws the whole phrase.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Exports keep a wrapped paragraph and a bulleted list as paragraphs (#5404, merged with main)
The test compared two saves' bytes, which carry the save time, so it failed
when the saves landed in different seconds (seen on CI). It now checks that
the edit generation, the unsaved state and the undo step are unchanged and
that the one annotation is all there is.
* Combine files: add a drop as one step, in order, and name what it can't take

Files dropped on the Combine files tab are handled as one batch: they join
the end of the list in the order dropped, as one undo step, from their path
or, when a drop carries only bytes, from those. A folder still adds the
files in it and its subfolders. Files of a kind Combine doesn't take are
left out and named in a notice, instead of joining the list as files that
can't be read. In a browser the dropped files are read one after another,
so they keep their order too.

* Combine files: let the web build use Incoming for dropped files

* Parity: Combine files takes drops in order, as one step, and names what it leaves out
- A draft with the same words (a space typed after the last) changes
  nothing and is no undo step. It used to redraw the whole phrase.
- Rewriting words alone is refused when it would move any other text on
  the page, visible text too, not only other OCR words.
- A cover stops at the words either side that stay: the padding of a
  large word could reach over the ink of a small one beside it.
- Spacing other than one space between words goes to the whole-phrase
  rewrite, which keeps it, instead of being dropped.
- A squeeze is relative to the scale the words already have.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Backspacing over a word leaves the spaces either side of it, and the rule
that sent spacing to the whole-phrase rewrite took that for spacing someone
asked for, so the most ordinary deletion redrew the whole line again.

Spaces beside a change are nothing to draw: the words around it stay where
the scan has them, with the gap already between them. Only what has to be
drawn goes to the whole rewrite: spacing inside the new words, between two
words nothing else changed, or any whitespace other than spaces.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Pq7HyCHD1R3rywQBPCHXZA
A rewrite also takes away copies of its words. With a copy earlier on the
page, the words after it moved up the list, so the next change rewrote the
word after the one it meant: deleting John removed Smith and left John.

Before each change, the words up to the next one that stays must still be
where they were. If they aren't, the edit falls back to the whole-phrase
rewrite.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
A combining mark whose font gives it a real advance (a synthetic or
broken /Widths entry, as in the grouped Thai body edit test) moves the
pen, so the next letter starts at the mark's right edge. The cluster
ended at the base's right edge, so that advance read as a word gap and
a space opened after every cluster with a mark. The cluster now ends at
the mark when the glyph drawn after it starts at the mark's box edge;
a zero-advance mark, whose box draw_glyph widens and the next letter
overlaps, still leaves the gap measured from its base.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Pq7HyCHD1R3rywQBPCHXZA
A subset that counts its glyphs in order of first use (`/Differences [1 /C1
/C2 ...]`) names its own codes, so numbered_names_name_their_codes held and
`/C65` (the 65th glyph used) read as `A`. A numbered name at a C0 control
code, which no text shows, now gives the counter away and the font reads
nothing. The check sits in the one function both the Type 1/CFF and the
Type 3 paths call.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Pq7HyCHD1R3rywQBPCHXZA
numbered_glyph_names_read_as_their_own_code gains a font with `/C32` at 32
beside `/C70` at 70 and `/C0071` at 71. The font still reads, and code 32
gives a space, so a guard that also rejected code 32 would now fail.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Pq7HyCHD1R3rywQBPCHXZA
* M5: a comment whose colour is written [r g b null] is drawn, as CMYK with the null read as 0 (view.render-annotation-appearances)

ISO 32000-2 Table 166 /C (and /IC, Tables 178, 180, 181; /MK /BC and /BG,
Table 192) is an array of 0, 1, 3 or 4 numbers whose length picks the
colour space. A four-entry array with a non-number in it, such as
[1 0 0 null] (PDFBOX-2699), made the /C parser give up, so a shape
without an appearance stream was not drawn at all. PDFium, MuPDF and
Poppler all read such an array as CMYK with the non-number as 0.

- annot: a four-entry /C or /IC reads non-numbers as 0; other lengths
  still need every entry to be a number.
- annot: the properties' fill uses the same parser (it read
  [r g b null] as RGB before), and /C no longer drops a non-number
  to make three numbers.
- forms: /MK /BC and /BG read a four-entry array the same way instead
  of dropping the non-number and drawing RGB.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Pq7HyDBXygTf7jcqdsjcfn

* M5: comment properties report the stroke colour as drawn; a reference among four colour entries is not read as 0 (view.render-annotation-appearances)

props() reads a non-FreeText /C through the appearance parser, so a
[1 0 0 null] stroke reports CMYK cyan as drawn, and grey and CMYK
strokes report their converted colour. A reference among the four
entries of a /C array (ISO 32000-2 7.3.10) is left unreadable instead
of standing in as 0, so /C [1 0 0 6 0 R] is never drawn cyan.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Pq7HyDBXygTf7jcqdsjcfn

* M5: re-run CI (fill_sign Escape test depends on the clock's second, passes locally)

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Pq7HyDBXygTf7jcqdsjcfn

* M5: the comment list reports a comment's colour as drawn (view.render-annotation-appearances)

summaries() accepted only a three-entry /C, so a Square with /C [1 0 0 null],
drawn cyan and reported cyan by props(), had no colour in the comment list;
grey and CMYK colours were dropped the same way. Non-FreeText comments now
read /C through drawn_color (the array itself followed when it is given by
reference), keeping the three-number fallback and the FreeText /DA handling.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Pq7HyDBXygTf7jcqdsjcfn

* M5: the render inspector lists a comment's colour as drawn (view.render-annotation-appearances)

The inspector accepted only a three-entry /C, with a referenced entry read
as 0, so a /C [1 0 0 null], a grey or a CMYK colour listed none, unlike the
comment list and the drawing. Non-FreeText comments now read /C by the
appearance builder's rule (one entry grey, four CMYK with a non-number as 0
and a reference unreadable, finite and clamped), with the three-number
fallback reading referenced entries.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Pq7HyDBXygTf7jcqdsjcfn

---------

Co-authored-by: mtlcl410 <chuanlin@niar.org.tw>
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
TIFF predictor 2 (ISO 32000-2 §7.4.4.4, Table 10) went through the
bit-level BitChunks/BitWriter path for every bit depth, re-reading the
previous output pixel through a new BitReader per pixel. Whole-byte
samples (8 and 16 bits per component) are now undone on byte slices,
each sample added to the same component of the pixel to its left,
wrapping at its width; 1-, 2- and 4-bit samples keep the bit-level path.
The output is byte-identical, including the dropped partial last row.

A page of 45 full-page 8-bit grey images so predicted (PDFBOX-1350)
renders in about 4 s instead of 23 s with the same pixels.

Vendored hayro-syntax patch 42.


Claude-Session: https://claude.ai/code/session_01Pq7HyDBXygTf7jcqdsjcfn

Co-authored-by: mtlcl410 <chuanlin@niar.org.tw>
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
* M2: a form XObject that draws itself is drawn once (hayro-interpret patch 89)

A form whose content names itself, directly or through another form, a
pattern or a glyph, drew itself again at every level until the page's
nested-paint budget (50,000) ran out. PDFBOX-5782's poppler-748 page 2,
whose form names a /Pages dictionary as /X0 and so finds itself in the
page's resources, took 38 s.

hayro-interpret now keeps the forms being drawn on the context and skips
a form already on it, reporting ContentTruncated (ISO 32000-2 §8.10.1: a
form is a self-contained description). The lenient fallback from a form's
own resources to the page's stays: MuPDF, Poppler and PDFium all draw a
name from the page's resources when the form's /Resources has no
XObject subdictionary (MuPDF and Poppler also when it has an empty one).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Pq7HyDBXygTf7jcqdsjcfn

* fix(render): a form that reaches itself through a pattern or a glyph is drawn once (hayro-interpret patch 89)

The forms being drawn are now kept per page and thread rather than per
context, so the tiling patterns, Type 3 glyphs, soft masks and groups
painted inside a form, which hayro interprets in contexts of their own,
see that the form is being drawn and skip it inside themselves. A form
drawn again after it has finished draws as before.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Pq7HyCHD1R3rywQBPCHXZA

* chore: re-run CI after the Fill & Sign save comparison failed on the clock (#6066)

`fill_sign_enter_adds_lines_outside_click_commits_and_escape_cancels`
compares two saves whose /ModDate fell in different seconds; it passes
locally on this commit. No change.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Pq7HyCHD1R3rywQBPCHXZA

---------

Co-authored-by: mtlcl410 <chuanlin@niar.org.tw>
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
* M2: `info` lists the repairs made while opening, as Document Properties does (misc.cli-inspect)

`pdfcraft-cli info` now prints a `repairs` array: the notes about damage
repaired while opening that the app shows in Document Properties and
doc_info reports. It opens the document as the app does, once (the session
the dynamic XFA summary already used), so notes the app adds after reading
the objects, such as a page tree rebuilt from the pages found by scanning,
are listed too. A file the app cannot open for editing falls back to what
reading its objects repaired; a sound file prints an empty array.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Pq7HyDBXygTf7jcqdsjcfn

* M2: `info` runs none of the document's scripts while inspecting it (misc.cli-inspect)

The session `info` opens to read the app's repair notes and the dynamic XFA
layout now has JavaScript turned off, so an XFA form's initialize and
calculate scripts, which the app runs on opening, do not run during
inspection of an untrusted file. Before this branch the session was opened
for dynamic XFA forms only, and their scripts ran there too.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Pq7HyDBXygTf7jcqdsjcfn

* M2: `info` reads the standards and repairs from the session it opens, and a laid-out form keeps its repair notes (misc.cli-inspect)

`info` now takes the declared standards (`Document::standards`, as the
Standards panel reads them) and the repair notes from the session it
opened, and reads the file once more only when the app cannot open it.
When opening lays out a dynamic XFA form, or writes any other revision,
the notes about damage repaired while reading the file now stay with the
re-read document, so Document Properties and `info` both list them.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Pq7HyCHD1R3rywQBPCHXZA

---------

Co-authored-by: mtlcl410 <chuanlin@niar.org.tw>
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
A cubic whose second control point is its end point (as the `y` operator builds,
ISO 32000-2 8.5.2.1 Table 58) has a zero derivative at its end. kurbo 0.13.1's
offset curve looks for cusps with a root search on the curvature, which grows
without bound towards that end; at some widths the search stops on the end point
and normalising the zero tangent there gives NaN. vello_common's fill drops any
outline with NaN, so the whole stroke vanished (a two-curve `y` path drew nothing
at 16, 32 and 63 pt; a PDFBox tracker file lost its 32 pt "XX" outline).

The NaN fallback that #4913 added to `flatten::stroke` now also moves such a
control point the stroke tolerance towards the previous control point before
stroking again (the curve moves by at most 4/9 of the tolerance). Outlines that
come back finite take the old path unchanged. The vendor ledger row also gets
back #4913's entry, which a merge had dropped.


Claude-Session: https://claude.ai/code/session_01Pq7HyCHD1R3rywQBPCHXZA

Co-authored-by: mtlcl410 <chuanlin@niar.org.tw>
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
* fix(engine): map URL text line rects with view_rect_to_user and filter non-finite coordinates

* test(engine): assert concrete rotated user rects and overflow guards (#6469)
* fix(js): decode &apos;, numeric, and HTML named character references in FormCalc Decode

* test(js): verify all HTML named entities and 8-byte name boundary (#6464)

* docs(js): remove duplicate decode_entities summary and document LATIN1_ENTITIES order (#6464)
Fixes #6466
M9: a signed string respelled in hexadecimal is no change after signing (sign.changes-after-signing)
Resolve conflicts with the XMP value-type checks (6.6.2.3.1): keep both
sets of tests, and combine the pdfa-verify parity evidence and notes.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Pq7HyCHD1R3rywQBPCHXZA
Co-authored-by: austin62jake <340374338+austin62jake@users.noreply.github.com>
Resolve conflicts with the XMP value-type checks (6.6.2.3.1): keep both
sets of tests, and combine the pdfa-verify parity evidence and notes.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Pq7HyCHD1R3rywQBPCHXZA
# Conflicts:
#	crates/preflight/src/tests.rs
#	parity/acrobat-features.toml
The test now places each of them in the catalog's /OpenAction (direct and
by reference), in an action of its own that a page's /AA refers to, and in
a link's /A, and adds a Named action whose /N refers to /Print. Save as
PDF/A removes each and keeps a Named page move.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Pq7HyCHD1R3rywQBPCHXZA
# Conflicts:
#	crates/redact/src/tests.rs
# Conflicts:
#	crates/preflight/src/tests.rs
#	crates/preflight/src/xmp_types.rs
#	parity/acrobat-features.toml
# Conflicts:
#	crates/redact/src/tests.rs
# Conflicts:
#	crates/redact/src/tests.rs
# Conflicts:
#	crates/preflight/src/tests.rs
# Conflicts:
#	vendor/README.md
# Conflicts:
#	vendor/README.md
# Conflicts:
#	crates/organize/src/tests.rs
# Conflicts:
#	vendor/README.md
# Conflicts:
#	vendor/README.md
# Conflicts:
#	crates/preflight/src/lib.rs
#	parity/acrobat-features.toml
# Conflicts:
#	vendor/README.md
# Conflicts:
#	parity/acrobat-features.toml
#	vendor/README.md
# Conflicts:
#	vendor/README.md
# Conflicts:
#	crates/preflight/src/lib.rs
#	crates/preflight/src/tests.rs
#	parity/acrobat-features.toml
# Conflicts:
#	crates/redact/src/tests/surviving_references.rs
# Conflicts:
#	vendor/README.md
# Conflicts:
#	crates/redact/src/tests/surviving_references.rs
# Conflicts:
#	crates/render/src/text/tests.rs
#	vendor/README.md
#	vendor/hayro-interpret/src/font/cid.rs
# Conflicts:
#	vendor/README.md
# Conflicts:
#	crates/preflight/src/tests.rs
#	parity/acrobat-features.toml
# Conflicts:
#	vendor/README.md
# Conflicts:
#	ROADMAP.md
#	docs/roadmap.md
# Conflicts:
#	vendor/README.md
#	vendor/hayro-syntax/src/page.rs
# Conflicts:
#	crates/render/src/raster/tests.rs
#	vendor/README.md
# Conflicts:
#	crates/preflight/src/lib.rs
#	crates/preflight/src/tests.rs
#	parity/acrobat-features.toml
# Conflicts:
#	crates/render/src/raster/tests.rs
#	vendor/README.md
#	vendor/hayro-interpret/src/font/type3.rs
# Conflicts:
#	crates/xfa/src/layout/draw.rs
#	crates/xfa/src/layout/pages.rs
#	crates/xfa/src/tests.rs
# Conflicts:
#	crates/automation/src/tools.rs
#	crates/cos/src/lib.rs
#	crates/preflight/src/lib.rs
#	crates/preflight/src/tests.rs
#	parity/acrobat-features.toml
# Conflicts:
#	crates/render/src/raster/tests.rs
#	vendor/README.md
#	vendor/hayro-interpret/src/x_object.rs
# Conflicts:
#	vendor/README.md
# Conflicts:
#	crates/measure/src/snap.rs
# Conflicts:
#	vendor/README.md
# Conflicts:
#	parity/acrobat-features.toml
#	vendor/README.md
#	vendor/hayro-syntax/src/pdf.rs
# Conflicts:
#	vendor/README.md
# Conflicts:
#	vendor/README.md
# Conflicts:
#	crates/preflight/src/tests.rs
#	parity/acrobat-features.toml
# Conflicts:
#	vendor/README.md
# Conflicts:
#	crates/render/src/raster/tests.rs
#	vendor/README.md
# Conflicts:
#	crates/render/src/inspect/tests.rs
#	vendor/README.md
# Conflicts:
#	crates/annot/src/tests.rs
#	parity/acrobat-features.toml
# Conflicts:
#	crates/preflight/src/lib.rs
#	crates/preflight/src/tests.rs
#	parity/acrobat-features.toml
# Conflicts:
#	vendor/README.md
#	vendor/hayro-interpret/src/font/mod.rs
#	vendor/hayro-interpret/src/font/type3.rs
# Conflicts:
#	crates/render/src/raster/tests.rs
#	vendor/README.md
# Conflicts:
#	crates/render/src/raster/tests.rs
#	vendor/README.md
# Conflicts:
#	vendor/README.md
# Conflicts:
#	crates/render/src/raster/tests.rs
#	vendor/README.md
# Conflicts:
#	crates/annot/src/draw.rs
#	parity/acrobat-features.toml
# Conflicts:
#	vendor/README.md
Patch 90 adds InterpreterWarning::Type3WidthsMissing and patch 91 matches every warning to a flag, so the merged match needs the new variant.
# Conflicts:
#	crates/forms/src/read.rs
#	crates/render/src/inspect/annotations.rs
#	crates/render/src/inspect/tests.rs
#	parity/acrobat-features.toml
# Conflicts:
#	parity/acrobat-features.toml
# Conflicts:
#	parity/acrobat-features.toml
# Conflicts:
#	crates/xfdf/src/xfdf.rs
# Conflicts:
#	crates/edit/src/text.rs
#	crates/edit/src/text_regions.rs
Land render and syntax patches (#6355, #6501, #6506, #6510, #6534, #6561, #6290, #6312, #6348, #6532, #6438)
# Conflicts:
#	crates/engine/src/tests.rs
verify() went past clippy's cognitive complexity limit once every check was in it. The checks move as they are into helpers, run in the same order, so the issues and their order don't change.
# Conflicts:
#	crates/engine/src/tests.rs
#	crates/render/src/inspect/tests.rs
#	crates/render/src/text/tests.rs
# Conflicts:
#	crates/render/src/raster/tests.rs
#	crates/render/src/text/tests.rs
#	parity/acrobat-features.toml
#	vendor/README.md
Patch 82 sent every soft mask through the decoded-image cache, so a mask that patch 91 keeps failed inside its image's entry got a second entry and counted as another decode. Only a mask averaged down needs an entry of its own; any other decodes as it did before patch 82.
Land render and font patches (#6292, #6287, #6314, #6353, #6359, #6368, #6409, #6491, #6545, #6516, #6521, #6528, #6533, #6529, #6542, #6543, #6569, #6572)
The header check reports a file without the binary comment, and Save as PDF/A then makes the next save a full one. The known-answer documents and the generation-repair file predate it: the minimal document got a 6.1.2 issue, and an undamaged file needed a full save for its header instead of being saved incrementally. They now start with the comment, as the crate's unit test documents do.
# Conflicts:
#	ROADMAP.md
#	crates/render/src/text/tests.rs
Land PDF/A verify and convert checks (#6232, #6254, #6172, #6202, #6203, #6206, #6207, #6214, #6215, #6219)
# Conflicts:
#	crates/engine/src/tests.rs
Land redaction, organize, forms, annotation, text, measure, XFA, XFDF and edit fixes (#6458, #6459, #6460, #6463, #6474, #5969, #5761, #5852, #6231, #6366, #6504, #6570, #6610, #6597, #6620, #6650)
Updates: take craft-update from craft-libs main at 1b34eba41
Co-authored-by: jonathanwxh-cell <jonathanwxh-cell@users.noreply.github.com>
Co-authored-by: jonathanwxh-cell <jonathanwxh-cell@users.noreply.github.com>
Fix main's build and tests after the latest merges (#6706)
Some checks failed
ci.yml / Fix main's build and tests after the latest merges (#6706) (pull_request) Failing after 0s
freebsd.yml / Fix main's build and tests after the latest merges (#6706) (pull_request) Failing after 0s
house-rules.yml / Fix main's build and tests after the latest merges (#6706) (pull_request) Failing after 0s
nix.yml / Fix main's build and tests after the latest merges (#6706) (pull_request) Failing after 0s
packaging-lint.yml / Fix main's build and tests after the latest merges (#6706) (pull_request) Failing after 0s
sonarcloud.yml / Fix main's build and tests after the latest merges (#6706) (pull_request) Failing after 0s
house-rules.yml / Fix main's build and tests after the latest merges (#6706) (push) Failing after 0s
ci.yml / Fix main's build and tests after the latest merges (#6706) (push) Failing after 0s
freebsd.yml / Fix main's build and tests after the latest merges (#6706) (push) Failing after 0s
nix.yml / Fix main's build and tests after the latest merges (#6706) (push) Failing after 0s
packaging-lint.yml / Fix main's build and tests after the latest merges (#6706) (push) Failing after 0s
sonarcloud.yml / Fix main's build and tests after the latest merges (#6706) (push) Failing after 0s
9ee213eff7
alf merged commit 9ee213eff7 into main 2026-11-09 11:25:30 +00:00
alf deleted branch chore/sync-upstream-20261109 2026-11-09 11:25:30 +00:00
Sign in to join this conversation.
No reviewers
No milestone
No project
No assignees
1 participant
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set.

Reference
iris/pdfcraft!21
No description provided.