chore: sync upstream (804 Commits) #5

Merged
alf merged 804 commits from chore/sync-upstream-20261022 into main 2026-10-22 09:27:53 +00:00
Owner

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

  • 9a0ec645 Merge pull request #3983 from aiongg/next
  • 022a970f Integration: #2173's restored-paper test lets the dialog relabel its button before clicking Save as PDF
  • 8662b2fe Merge r2-fix-ui: print test waits for its render; ui tests serialize GPU rendering
  • b99c774f Integration: serialize GPU rendering in the ui tests (batch render tests × lavapipe)
  • e85fca66 Integration: text field table labels in the batch's catalogues (upstream #3479 × r2 catalogues)
  • 3f3b9130 Integration: the dead-read print test waits for the preview rasters before checking idle (#2108 × print preview)
  • 8293832a Integration: Norwegian Bokmål page-gap labels (upstream #1836 × #3101)
  • 472717fc Merge upstream main 88dad98dd into next
  • 4bd08a75 Integration: Albanian signature labels all translated (#3101 × r2 catalogues)
  • bbdbca62 Merge r2-fix-ui: tests the combined batch broke
  • 38daa0ec Merge r2-fix-engine: tests the combined batch broke
  • 47ac03b8 Merge r2-fix-i18n: tests the combined batch broke
  • 9a2d2cfd Integration: panel snapshots for the assembled batch (#3525 × #2607, #2731, #3828)
  • 46417784 Integration: the live line-resize test follows lines across rotated pages (#3520 × #3781)
  • 8341a721 Integration: mode-tab hand tests use the tabs the mode bar has (#2240 × #3010, #3022)
Upstream-Sync von `upstream/main` (804 Commits). Fast-Forward. - 9a0ec645 Merge pull request #3983 from aiongg/next - 022a970f Integration: #2173's restored-paper test lets the dialog relabel its button before clicking Save as PDF - 8662b2fe Merge r2-fix-ui: print test waits for its render; ui tests serialize GPU rendering - b99c774f Integration: serialize GPU rendering in the ui tests (batch render tests × lavapipe) - e85fca66 Integration: text field table labels in the batch's catalogues (upstream #3479 × r2 catalogues) - 3f3b9130 Integration: the dead-read print test waits for the preview rasters before checking idle (#2108 × print preview) - 8293832a Integration: Norwegian Bokmål page-gap labels (upstream #1836 × #3101) - 472717fc Merge upstream main 88dad98dd into next - 4bd08a75 Integration: Albanian signature labels all translated (#3101 × r2 catalogues) - bbdbca62 Merge r2-fix-ui: tests the combined batch broke - 38daa0ec Merge r2-fix-engine: tests the combined batch broke - 47ac03b8 Merge r2-fix-i18n: tests the combined batch broke - 9a2d2cfd Integration: panel snapshots for the assembled batch (#3525 × #2607, #2731, #3828) - 46417784 Integration: the live line-resize test follows lines across rotated pages (#3520 × #3781) - 8341a721 Integration: mode-tab hand tests use the tabs the mode bar has (#2240 × #3010, #3022)
alf added 804 commits 2026-10-22 09:27:51 +00:00
Ita Translate
Adds the 267 keys that other catalogs translate and fr.tsv lacked, so French covers the union of all catalog keys. Entries were written from the English labels and checked against the existing French catalog for consistent wording; placeholders, edge spaces, trailing ellipses and the two French plural forms are preserved.
Rewords 70 existing or new French entries to the terms French prepress users know: Recadrer for Crop, Supprimer for Discard in the optimizer ("Ignorer les signets" read as if bookmarks were kept), Mode de sortie for Output intent, Annotations for Markup, Interrogeable for searchable OCR output. 22 terms are attested in Adobe's public French help pages; 48 come from the reviewer's knowledge of the French interface. The catalog header says so.
Co-authored-by: GLM <noreply@z.ai>
Co-authored-by: GLM <noreply@z.ai>
Co-authored-by: GLM <noreply@z.ai>
Co-authored-by: GLM <noreply@z.ai>
Co-authored-by: GLM <noreply@z.ai>
Final-review fixes for the focus-existing-tab branch:

- open_path returns OpenOutcome (Opened | Focused | Failed), so callers
  no longer infer success by counting views. Toasts are unchanged; the
  value is the programmatic contract.
- The control channel's ui open replies Ok with "focused": true when the
  file's tab was already open (an agent driving the app headlessly read
  the old Err as a read failure), and Err only on Failed.
- open_then_run runs the picked tool on Focused too: picking an
  already-open file focuses its tab and the command runs, as before the
  branch.
- open_recent silences the wasm-only unused `path` binding, like
  reopen_last_files.

Co-authored-by: GLM <noreply@z.ai>
Enables visible signature appearances in pdfcraft_sign to render
Unicode characters (specifically Latin Extended-A / Central European
characters such as č, ć, ž, š, đ) by embedding the bundled
Inter-Regular.ttf font with /Identity-H encoding and a /ToUnicode CMap
when the signer name, labels, reason, or location contain non-WinAnsi
text.

When all text is within WinAnsiEncoding, the existing zero-byte overhead
behavior (using standard /Helv) is preserved.

Closes #2207
The "Dialogs, panels, menus" paragraph appears twice, the same artifact
as the Auto paragraph in #1525. Two language additions each edited it
and both versions were kept: one ends "Czech and Turkish cover the menus
so far", the other lists Slovenian among the translated languages and
says only Czech covers the menus.

Neither list is current. Turkish has had a complete catalog since #1057,
and five of the shipped languages are in neither copy: Kazakh, Polish,
Greek, Turkish and Lithuanian, all with full catalogs per
docs/localization-parity.md. Czech is still menus only (135 of the
2,143-key union there).

So keep one paragraph, and stop enumerating the languages in it: that
list is what both additions edited at once, and the paragraph already
points at localization-parity.md, which carries the per-language counts
and coverage. The rest of the paragraph is unchanged.

Docs only, no code.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Fixes #1757
Two new complete catalogs (2542 entries each, full coverage of the
uk.tsv key set): Indonesian (id) and Vietnamese (vi), both with a
single plural form per CLDR. Registered in LANGUAGES (22 → 24) with
locale-tag tests. Also adds Devanagari font support (for the upcoming
Hindi catalog) to the fonts crate, and fixes 19 duplicate keys in
ru.tsv that crept in during a previous merge.

Tests: 110 passed, 0 failed.
Two complete catalogs, hi and ko, 2,578 entries each (every
(context, English) key the bundled catalogs hold), registered in
LANGUAGES (24 -> 26) and in the date languages (hi, ko). Both use a
single @plural form.

Korean has no Hangul face in craft-fonts, so desktop builds draw it
with one installed face (Malgun Gothic, Apple SD Gothic Neo, Nanum;
never Noto CJK / Source Han), added after the embedded faces like the
Devanagari fallback. PDFCRAFT_SYSTEM_FONTS=0 turns it off.

Tests: catalog registration and persistence, command / All tools /
tl!() literal coverage for both, date names, Hangul fallback and
candidate list. cargo xtask assets, fmt and clippy are clean. Also
refreshes the ru.tsv SHA-256 in ATTRIBUTION.toml and the docs.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Four new complete catalogs (2554 entries each, full coverage of the
uk.tsv key set including the 12 strings that reached main during the
rebase): Hindi (hi), Indonesian (id), Korean (ko), Vietnamese (vi).
All four use a single plural form per CLDR. Hindi uses the Devanagari
font support already in the fonts crate.

Registered in LANGUAGES (22 → 26) with locale-tag and catalog tests.
Also fixes 19 duplicate keys in ru.tsv.

Tests: 114 passed, 0 failed.
The rebase onto main resolved the theme.rs and system_fonts.rs conflicts
in favour of the new Hangul code and deleted upstream's Telugu fallback
(SYSTEM_TELUGU_FALLBACK, telugu_fallback, telugu_candidates,
script_fallback and their tests), while tests/fonts.rs kept the Telugu
tests, so the build failed. Keep both: Telugu restored as it is on main,
Hangul added alongside it through the same script_fallback helper.
Correct 28 context-sensitive Korean labels in the incoming translation, distinguish redaction marking, clearing, and permanent removal, and add a regression test. Document code evidence and outstanding runtime QA.
Rebasing onto newer main replayed ATTRIBUTION.toml with the hashes of the
older base, so every catalog entry failed the xtask manifest check.
Restore main's entries for the catalogs this branch does not touch and
recompute ours (ru and the four new catalogs) from the files as committed.
2,330-entry clean-room catalog, Albanian month and weekday names for date
formats, Preferences entry, locale matching for sq-*, coverage tests, font
coverage test, attribution entry and docs.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Applies the rest of the native-speaker review on #1976 on top of the
merged redaction redesign (fork PR 1): redaction marks read 제거 대상
표시 in all ten entries (not 수정 표시), every Flatten label uses 평탄화
(not 병합, which reads as merging documents, or 평면화), Verify is 검증 so
it no longer collides with OK (확인), and Collapse is 접기 so it no longer
collides with Zoom out (축소). ATTRIBUTION.toml hash updated.
The terminology rationale lives in the review discussion (fork PR 1 on
pechnikov2026-star/pdfcraft and the #1976 thread); docs/ holds the
project's reference documents, not contribution-process notes.
Fixes #2250

Co-Authored-By: Claude Haiku 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Clear the RUSTSEC-2026-0348 dependency advisory for PR 2280.
Upstream moved the Hungarian catalog and std14_metrics forward and added
the Next tab / Previous tab labels (document-tab cycling); the four new
catalogs translate both, and their ATTRIBUTION.toml hashes are
recomputed. The manifest check passes with 274 repository assets.
as_chunks::<4>() gives the pixel array directly; upstream main fails
the new chunks_exact_to_as_chunks lint here, which made every pull
request red under rust-clippy 1.99.0.
A Polish signature panel headed "Ważność podpisu jest NIEZNANA" and listed
its details in English: the catalog had none of the engine's signature
wordings (sign::pdf::WORDINGS, 57) or the changes after signing they name
(kind::ALL, 10), and lacked 34 UI literals besides.

All 101 are translated, and two tests keep them so:
- polish_covers_ui_literals, every tl!() literal (as Japanese has);
- polish_says_what_a_signature_says, the signature wordings and change kinds
  with their {} holes.
Both fail on the previous catalog. ATTRIBUTION.toml carries the new hash.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Shaped text (Arabic, Indic, combining marks) was written as one text
object per glyph: BT, the whole text state (Tf Tc Tw Tz TL Ts and the
colour), Tm, Tj and ET. A 15-glyph row took 15 text objects and 392
tokens (the whole stream), a long Arabic paragraph thousands of operators, and other
viewers saw every glyph as a separate piece of text.

Each run is now one text object: the state once, one Tm, and the glyphs
in TJ arrays. A glyph's adjustment moves it from where the glyph before
it ends (its width in the embedded font) to where shaping put it, and a
mark shaped above or below its letter is raised with Ts, then reset
(ISO 32000-1 9.4.3, 9.3.7). Character and word spacing are part of those
positions, so Tc and Tw are 0 in the run. The same row is now 1 text
object (90 tokens for the stream), and renders the same at 300 dpi (138 of 33
million bytes differ, by 1 level, from rounding).

The new test shapes marks beside and above their letters with Inter,
reads each glyph's origin back from the TJ displacements and checks it
against the shaped position. It fails without this change (15 text
objects in the run against 1).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Each edit that draws Japanese or Hebrew characters the page's fonts lack
generates new Type 3 fonts with the outlines of every character it draws.
Editing the paragraph again generated new ones and left the old ones in
the page's resources, so every edit added a set of glyph outlines to the
file.

After replace_line and rewrite_block, the page's generated fallback fonts
(Type 3, /Name /PCJapanese or /PCHebrew) that no Tf selects any more are
removed from its own font dictionary. The content read for this is the
page's streams, every form XObject it can reach that has no resources of
its own (those use the page's, ISO 32000-1 7.8.3), and the glyph
procedures of Type 3 fonts without resources. If any of those can't be
decoded strictly, nothing is removed. Registered fonts (PCFull) and the
file's own fonts are never touched.

Tests: a page with an unused generated font, one its content selects,
one only a resource-less form selects and a file's Type 3 font that only
shares the name prefix keeps all but the first after a rewrite, and keeps
everything when such a form can't be read. Rewriting Japanese text twice
(with craft-fonts) leaves only the second rewrite's fonts. The first test
fails without this change.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Register Catalan (`ca`) as an interface and date language with a
2,592-entry contributor-original catalog written from the meaning of the
English labels. Plurals use the one/other rule; every `ca-*` locale,
including Valencian (`ca_ES@valencia`), resolves to this catalog.

Tests cover registration and persistence, every command, menu, tool group
and section, every `tl!` literal in the UI source, and every key in the
other bundled catalogs. The Latin-script glyph test in tests/fonts.rs now
includes Catalan (the interpunct of l·l is covered by the bundled faces).

Catalan dates elide the preposition before a vowel (7 de març, 7 d'abril,
8 d'octubre), so the date language sets months_with_day, as Czech and Russian
do; a unit test pins it.

Update localization docs, the parity table and the roadmap log, and add the
ATTRIBUTION.toml entry (ATTRIBUTION.md regenerated). The contributor, a
native Catalan speaker, has reviewed the translation.

Validation: cargo fmt --check; clippy -D warnings and tests on
pdfcraft-ui-egui (i18n, fonts); pdfcraft-engine dates tests;
cargo xtask assets; cargo xtask layers.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
The Combine grid stopped at 200%, too small to read a value off an invoice
while reordering the pages of a statement. The main viewer already reached
6400%, so only the card grid was capped.

- The range is 60–500%, and the buttons and keys step
  60/80/100/125/150/175/200/250/300/400/500%.
- MAX_SIDE, the thumbnail texture cap, goes 768 → 1536, so a card near the top
  re-renders sharper instead of upscaling a 768px texture. It only binds near the
  top: a card below it asks for less and costs no more than it did. Up there one
  thumbnail is a few megabytes, so TEXTURE_BUDGET and UPLOAD_BYTES_PER_FRAME keep
  the cache at a screenful plus one frame's uploads — and a screenful at the top
  of the range is a card or two, because a card is wider than most windows.
- The control channel's range message is derived from the constant rather than
  repeating "60 and 200", so it cannot drift from the range again.
Agents fixed bugs that were already reported, or already fixed on main,
because nothing told them to look first. Say to search issues and PRs
(open and closed), check whether a merged fix is on main, and build on
an open PR rather than opening a competing one.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
CUPS drivers ship their names in other languages as PPD localization lines
(`*cupsLanguages`, `*de.Translation KMDuplex/Druckart: ""`, `*de.KMDuplex
Double/2-seitig: ""`), which the system print dialog uses. PdfCraft showed the
PPD's default (English) names in every language. `spool::printer_options_in`
now applies the lines for the UI language (`pt-br` → `pt_BR`, then `pt`;
`zh-hans` → `zh_CN`) to option, group and choice names. Each line is decoded as
UTF-8 on its own, as drivers write them in otherwise Latin-1 files; missing or
empty translations keep the driver's name. `printer_options` (automation) is
unchanged.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
A selected image gets a rotate knob (Shift: 15 degree steps); image_edit
gains rotate_by (degrees). The turn is one extra cm about the visible centre.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Found by looking at screenshots: the knob press was taken by the marquee when
the image was smaller than the page; the angle label vanished on dark pixels;
the knob could leave the visible area for an image at the top.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Adds crates/crypt/tests/qpdf_literals.rs (305 lines): three tests that hand our security
handler /O, /U, /OE, /UE and /Perms values from outside implementations, then require our
reader to authenticate them and report the intended role. Tests only - no product code, no
new dependency; PDFs stay outside the repository and only quoted hex is committed.

Why outsiders wrote these numbers: create() and open() share one derivation, so a
wrong-but-self-consistent implementation passes every internal test. Two review rounds proved
it by mutation - several key-derivation breaks stayed green in both suites because writer and
reader drifted together. Quoting bytes we did not compute breaks the circle: open() recomputes
/U (or the revision 6 hash chain) and compares it to what was stored, so a divergent derivation
cannot authenticate.

Six cases come from qpdf core 12.2.0 via pikepdf (R2/R3, R4 in both metadata modes, R6 in both).
Revisions 2-4 are deterministic given /ID and passwords, so the generator reproduces their quoted bytes
byte for byte (verified). Revision 6 salts are fresh on every run by design: these literals match the
fixtures as captured, and re-running the generator will produce different - equally valid - revision 6
numbers, so compare R/V/Length structurally there and re-quote if you regenerate. One
case is HAND-DERIVED, labelled as such in the source: an independent re-implementation of ISO
Algorithms 2/3 at n = 5, cross-validated by having that same routine reproduce qpdf's n=16 /O
byte for byte. It exists because no conforming producer emits R3 at 40 bits - qpdf writes 128
there and this crate's create() maps Rc4_40 to revision 2 - yet the derivation loop only runs
for r >= 3, so without a crafted row its truncation is unobservable and mutating it changes
behaviour that nothing catches.

Measured effect, same battery, upstream --lib run separately each time:
  r6-hash-selector      (%3 collapsed to %2)      CAUGHT here; upstream stays green
  r6-sha-arms-swapped   (SHA-384 <-> SHA-512)     CAUGHT here; upstream stays green
  r6-selector-window    (sums 17 bytes not 16)    CAUGHT here; upstream stays green
  r4-key-chain-truncation (loop hashes whole digest) CAUGHT here, via the crafted row;
      upstream stays green. Pristine 3/3 pass, mutated 2 passed / 1 FAILED - verified twice.
Still invisible, named rather than glossed: r6-round-floor (minimum rounds 64 -> 63) - the
terminator does not separate the two floors on these inputs; /OE,/UE length loosening and the
/Perms filler source, both unreached here.

Corrections carried from review: qpdf's advice against a static /ID does not weaken these
revision 6 vectors, because key_r6 never receives the file /ID at all; it matters only to the
revision 2-4 paths, where we quote /ID literally so the numbers stay exact. And no ATTRIBUTION
entry is added, matching crates/crypt/src/tests.rs:237 which already quotes qpdf vectors
unattributed; cargo xtask assets stays ok.

Gates on a clean tree: cargo test -p pdfcraft-crypt 28 passed / 0 failed (17 lib + the 8 properties from #2358, now merged,
plus 3 oracle tests - one
per concern - role authentication across seven dictionaries, metadata-flag key separation, /Perms
decode);
clippy --all-targets -D warnings clean; cargo fmt --check clean; xtask layers OK (35 crates).

Integrity note: the literals in this commit were checked field by field against the PDFs on disk -
27 of 27 fields quoted verbatim, no transcription drift. A regression test for that check belongs in the
generator's output comparison rather than here, since it needs pikepdf.
Adds crates/cos/tests/proptests.rs (464 lines) - three properties over generated catalogs (edit sequence
vs a plain ordered model; full save in both output styles; incremental save over bases built BOTH ways) -
and a self-credit entry in contributors/people.toml committed under the author's own identity. No product
code changes; total +466 lines across two files: people.toml +2; proptests.rs +464.

Two design decisions that came out of adversarial review, each measured rather than asserted:
  * catalog keys insert in a scrambled order ([5,0,7,2,6,1,4,3] filtered to the drawn keys), because with
    ascending insertion "preserves key order" and "sorted by key" are the same observable.
  * incremental expectations derive from the values written, not from reading the writer's own output back,
    so a consistently-wrong implementation cannot pass by agreeing with itself.

Measured against one-line breaks in crates/cos/src/writer.rs, each reverted and byte-compared afterwards
(verdicts from cargo exit codes; "pre-existing" = lib 48 + writer 10 + encryption 8 + shared_streams 3 +
bounded_writer 4, i.e. every target that existed before this PR):

  sort every dictionary by key              CAUGHT (props 1+2)      pre-existing suite FULLY GREEN
  reverse dictionary emission              CAUGHT (all three props)   pre-existing suite FULLY GREEN
  move /Type to the end of every dict     CAUGHT (prop 1)           pre-existing suite FULLY GREEN
                                            - caught only after catalog_keys() was added to pin the full key
                                              sequence; without it this break was green everywhere
  classic-table incremental startxref +1   CAUGHT (prop 2 alone; props 1+3 green), failure message names
                                            "classic-table", confirming the incremental table path executes
  drop null-valued dictionary entries      CAUGHT (all three)        at the pre-fix commit b51381c this same
                                            break left property 2 green - the self-reference it removed

Known limits, stated not hidden: incremental saves are asserted to add bytes and leave the original prefix
untouched - minimality is NOT claimed; catalog key order pins one fixed scramble rather than drawn
permutations, so 25.4% of cases draw too few keys for order to be meaningful; nested dictionary key order
is deliberately not pinned (canon sorts inside values); /Perms and permissions semantics are out of scope
here.

Gates at this tip (upstream 24f93e6, drift 0): cargo test -p pdfcraft-cos 85 passed / 0 failed / 2
ignored; cargo fmt -p pdfcraft-cos --check clean; clippy exit 0 for this crate with
-D warnings -A clippy::chunks_exact_to_as_chunks (that lint fires only in upstream's own
crates/oracle/src/pixels.rs:189 from f988cf8, tracked by #2342/#2324; flags after `--` forward into every
crate in the build graph, and crates/cos itself emits zero warnings under a forced recompile).
cargo xtask layers OK: 37 crates.

Scope: part of #1187 (items 1-3 partly; the reference-following comparison, undo state machine, password
property and damaged-file splices remain open). The encryption-side properties are PR #2358 by the same
author; earlier drafts of this text mis-cited #2014, which is certificate encryption and still open.
- Detect a one-row centred title through a new `is_centred` helper in
  `text_regions.rs`, shared by both rewrite paths, instead of the multi-row
  ragged-centres rule that can never fire on a single row.
- A lone row counts as centred when its centre sits within one font size of
  the page centre and its left edge is not the page's dominant left edge, so a
  full-width left-aligned line at the margin is not mistaken for a title.
- In the single-style path, a centred block with no explicit width or
  alignment no longer has its width stretched to the page margin; the line is
  centred in the box it already sits in, so a grown row keeps its centre and
  every wrapped row shares it.
- Gate the new rule behind main's `upright` flag: `is_centred` measures
  page-space rects, which are not meaningful across a turned baseline; frame
  coordinates (#1443) are the tool for that, not this change.
- Add `a_one_row_title_keeps_its_centre_when_it_grows_and_when_it_wraps`,
  built from the issue's fixture; red before the fix (centre 398.025 vs
  expected 306), green after.
No embedded interface face has Hangul, and the installed fallbacks covered Arabic, Devanagari,
Telugu, Sinhala, Tamil and (for a Chinese interface) Han only, so a Korean file name in a tab,
the title bar or the bookmarks panel drew as boxes (#2747, #407, #1043).

hangul_fallback() adds the first installed Korean face that has the whole syllable block, like
the Telugu and Tamil fallbacks, and it ends every family next to them. The probe is U+D7A3 (힣),
the last of the 11,172 modern syllables: Droid Sans Fallback has 가 but few other syllables, so
a 가 probe would accept a face that still draws boxes. Candidates: NanumGothic, UnDotum,
Baekmuk Dotum (Linux), Malgun Gothic, Gulim, Batang (Windows), Apple SD Gothic Neo, AppleGothic
(macOS). Noto CJK and Source Han are left out, as for Chinese (AGENTS.md §1.1).
PDFCRAFT_SYSTEM_FONTS=0 turns it off like the others.

Test: system_fallback_covers_hangul (skips on Linux without an installed Korean face).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01RNjF2Pc6JWeHW1UVPK8uVg
Claude-Session: https://claude.ai/code/session_01RNjF2Pc6JWeHW1UVPK8uVg
Main solved this branch's problem on its own: #1712 replaced the two
duplicated catalog paragraphs with one current paragraph that covers
every shipped language. Two and a half hours later #2320, branched
before that, added its two copies back, so main carries three again.

Keep #1712's paragraph exactly as main wrote it and delete only the two
stale copies, so this branch no longer rewords anything.

Docs only, no code.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
A file edited after it was made PDF/A or PDF/X can keep claiming a
standard it no longer meets. organize::remove_standard_id removes the
claim: the pdfaid: XMP properties for PDF/A, or GTS_PDFXVersion and
GTS_PDFXConformance in the document information and as pdfxid: XMP
properties for PDF/X. Output intents and other claims (PDF/UA) stay.

The XMP is edited with the packet editor Document Properties already
uses (sync_info and the new remove_properties share its read and write),
so the rest of the packet stays byte for byte and unsafe packets are left
alone. It is one undoable edit, in Standards ▸ PDF/A as a button worded
like its undo label, and the standards_remove_id tool (pdfa or pdfx).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Prepare a form ▸ Detect form fields places each field on a line or box the
page already draws, but every new widget got a grey /MK /BC and an opaque
white /MK /BG, so the fill hid the printed form underneath. Detected fields
now clear both in the same undoable step; fields drawn by hand keep the
defaults.

Refs #294, #2831.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Taiwan, Japan and China write dates year first with slashes (Windows'
zh-TW short date is yyyy/M/d), and neither the Fill & Sign presets nor
the header and footer date tokens offered it. Both now have yyyy/mm/dd
and yyyy/m/d. The header and footer list gains them at its end, since
its selection is an index.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01RUNQSPVXx3dDBabRZHAnjz
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01RUNQSPVXx3dDBabRZHAnjz
A clean-room Thai catalog covering every key the bundled catalogs translate, its LANGUAGES row (one plural form) and Thai month and weekday names in DATE_LANGUAGES.
Word count, untagged content, image crop, print as image, signature labels, full screen prompt and the other new interface strings. th.tsv now covers every key any bundled catalog translates (2,672).
In Edit text & images, a paragraph's width handle is drawn centred on its
box's edge, and the edge grabs from up to 6 px outside the box. Object
selection sees a press first, though, and handed it to the paragraph tool only
on the box itself. A press on the outer half of the handle started an area
selection instead: its rectangle followed the pointer like a resized frame,
and on release the paragraph kept its old width and the text never moved.

Object selection now also leaves a press on a closed paragraph's edge grip to
the paragraph tool, through edit_text_ui::grabs_edge. That uses the same edge
test, box and upright-page rule as the tool itself, so the two can't drift
apart. The rewrap is the existing EditTextBlock with a width, so undo, redo
and save are unchanged.

Fixes #2276
The prompt's field requested focus on every frame. In egui 0.36 each
request also interrupts the input method, and egui-winit answers with
set_ime_allowed(false) then (true), which recreates the X11 input
context (or re-enables Wayland text-input) every frame. On Linux typed
keys go through the input method, so nothing typed reached the field.

Request focus only when the field doesn't have it, as the command
search does (#1638). The permissions password prompt shares the box.

Tests type through synthetic IME pre-edits and commits after opening
an encrypted file from the command line, File > Open, the start page
and a drop, and into the permissions prompt; both fail without the fix.
With the All tools panel closed, page text scrolled up under the mode bar
gave the Read, Edit, Convert and E-Sign tabs the page's cursor: the
Select tool's I-beam, or Edit text's four-way arrow (the I-beam once a
paragraph was open). The page code tested egui's raw hover position
against the page's whole rectangle, which runs on under the chrome when
the page is scrolled, and it sets the cursor after the mode bar does.

The text layer and Edit text now set their cursor only while the pages'
widget contains the pointer, as Fill & Sign already does; a selection
being dragged keeps the I-beam past the edge. The open paragraph's
editor, a foreground Area placed on its paragraph, also floated over the
tabs and took their clicks: it is now clipped and hit-tested to the
document viewport and stays on its paragraph instead of being pushed
back into the window.

Fixes #2755
The Measure panel's drawing scale had its page side fixed in PDF points, so
"1 = 50 mm" was stored as 1 pt = 50 mm and every length came out
72 / 25.4 = 2.835 times too big (a 1:50 plan needed 50 x 25.4 / 72 = 17.64).

The page side now carries a unit (pt, in, mm, cm). It defaults to the
real-world unit, and to the display unit when that is not one of the four.
Scale::ratio converts it to points before building the /X, /D and /A number
formats (ISO 32000-2 12.9) and writes /R as entered ("1 mm = 50 mm").
measure_scale takes the same ratio through page_distance and page_unit.
Scales read from files, Scale::calibrate and units_per_point are unchanged.

Fixes #2752
Object-stream members were indexed with unchecked casts: `/First` and each
offset went through `as usize` and an unchecked addition, which truncate or
wrap on 32-bit targets such as the wasm32 build, and the member number went
through `as u32` on every target, so a member numbered 2^32 + 3 was read as
object 3. A negative offset was clamped to zero and read the stream's first
member instead.

Each member is now converted and added with checks, and kept as an unusable
slot (preserving positions, which cross-reference entries refer to) when its
number is not an object number or its start falls outside the decoded data.
Looking such a member up is a MissingObject error, as for any other absent
object.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Renaming a field changes only its own /T, so MASTER.a renamed to b
becomes MASTER.b. set_props compared the bare new partial name with the
other fields' fully qualified names: renaming MASTER.a to b succeeded
while MASTER.b existed, leaving two fields with one fully qualified
name, which are one field with a shared value (ISO 32000-2 §12.7.4.2);
and renaming MASTER.a to c was refused because of an unrelated
top-level field c.

The check now compares the new full name, leaving the field itself out
so a kid keeping its own partial name still succeeds, and the error
names that full name. Top-level renames are checked as before.
Save is chosen when the prompt opens, so Enter saves as before. The arrow keys
and Tab / Shift+Tab move the choice between Don't save, Cancel and Save, shown
with a focus ring; Enter or Space press the chosen button, Escape cancels.
Before, Enter always saved, even with another button reached by Tab.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
#2241 made leaving the Edit tab put Edit text & images down and apply a
paragraph still open in the editor, but only in select_mode, which only
the mode bar reaches. The Read mode command (View > Read mode, Ctrl+H,
the command search) went through switch_mode's other branch, which only
set the mode: Read mode then kept Edit text & images on, every paragraph
boxed, and an open paragraph editor floating over the page with its edit
not applied.

enter_mode now sets the workspace together with its tool, and both
routes go through it, so they can't drift apart again. The command still
leaves the panels as they were.

Test: editing.rs::the_read_mode_command_applies_an_open_paragraph_and_puts_edit_text_down
After a highlight, its note editor in the Comments panel kept the
keyboard, so V and H were typed into the note (#2669, item 2).

- The editor asked for the keyboard on every frame when no other widget
  had it, so it took it back after every click elsewhere. It now takes
  it once, when it opens (CommentView::open_editor, EditorFocus::Take).
- Adding another comment left the editor open, and drawing is a drag,
  which egui doesn't treat as a click elsewhere. An empty editor on
  another comment now closes when a comment is added; one with text
  stays open with it but gives the keyboard back (EditorFocus::Release).

Part of #2669.
The IRCC IMM forms are certified (DocMDP P=2) and then saved once more
with Reader Extensions: a later revision adds the UR3 usage-rights
signature and rewrites the XFA `datasets` packet. The diff classified any
rewritten stream that wasn't page content or the catalog's XMP as "other
changes", so these untouched forms validated as invalid.

A stream the signed revision's /AcroForm /XFA array names `datasets` or
`form` (the form's data and state) is now a form fill when rewritten,
permitted at P=2 and P=3 and refused at P=1 as before. The roles come from
the signed revision, so a later revision that names the template
"datasets" doesn't make rewriting it a form fill.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01RUNQSPVXx3dDBabRZHAnjz
A resource dictionary's DefaultGray, DefaultRGB and DefaultCMYK colour
spaces replace the device space content selects (ISO 32000-2 §8.6.5.6).
hayro ignored them: g/G, rg/RG and k/K set the device space directly,
cs/CS resolved the device names before looking at the resources, and
images took their /ColorSpace as written. Pages that calibrate device
colour this way, common in PDF/A, came out in the wrong colours.

The interpreter now looks the defaults up once per content window
(DefaultColorSpaces), in the resources and those they inherit, so a form
XObject without its own draws in its page's. They replace DeviceGray,
DeviceRGB and DeviceCMYK selected by the colour operators, by cs/CS (by
name or through a /ColorSpace resource that is a device space), and as
the /ColorSpace of image XObjects and inline images. A default that is a
Lab, Indexed, Pattern or device space, has another number of components
or is not a colour space is ignored, and device colour is drawn as
before. Device spaces nested in other spaces, shading dictionaries,
masks and transparency group spaces are unchanged. The decoded-image
cache keys a decode by the default it was drawn in, since one image can
be drawn with and without one.

Part of #1201.
Preserve the idle repaint and second-edit recovery regression while refreshing already-merged upstream work. Validated exact tree with full local CI and native Linux idle recovery.
# Conflicts:
#	ATTRIBUTION.md
#	ATTRIBUTION.toml
#	ROADMAP.md
#	crates/engine/src/dates.rs
#	crates/oracle/src/pixels.rs
#	crates/ui-egui/src/i18n/mod.rs
#	crates/ui-egui/src/i18n/ru.tsv
#	crates/ui-egui/src/i18n/vi.tsv
#	crates/ui-egui/src/system_fonts.rs
#	crates/ui-egui/src/theme.rs
#	crates/ui-egui/tests/fonts.rs
#	docs/gaps.md
#	docs/localization-parity.md
#	docs/localization.md
#	docs/roadmap.md
#	docs/target-app-parity.md
Hidden (/F bit 2) and NoView (/F bit 32) annotations are not drawn on
screen, so the text under them is in plain view; the audit now skips
them the way the renderer does. A Print-only square still covers.
hayro-interpret cached the colour space built from an /ICCBased profile
stream under the hash of the stream's dictionary bytes alone. Two
different profiles with the same /N and /Length (any two uncompressed
RGB profiles of one size) shared whichever was read first, so colours in
the second space were drawn through the first profile for the rest of
the document. Found while fixing #1201.

The space is now keyed by what its profile is read from (IccProfileKey):
the stream object a space names, looked up without reading the stream,
and on a miss the old dictionary key plus the stream's own bytes
(decrypted, not decoded), so byte-for-byte copies in other objects still
share one parsed profile and a lookup never decodes. A stream written
directly in the colour space array reports the object holding the array,
as every other stream written there does; it is recognised by its
dictionary lying within the array's bytes and keyed by content only.

InterpreterCache::icc_profile_reads counts the profiles read, so the
tests can check that the cache still hits. Tests: two synthetic RGB
profiles of one length (linear sRGB, and red and blue swapped) as
objects, a reused object, a copy and direct streams on one page.
Follows up #2755, which fixed the Edit text paragraph editor. The form
text field editor, the Fill & Sign text box and the Add text box are
foreground Areas placed on the page too. Scrolled up under the mode bar
with the All tools panel closed, they were drawn over the Read, Edit,
Convert and E-Sign tabs: the tabs showed the editor's I-beam and a
click on one went to the editor, so the mode didn't change.

The three Areas are now constrained to the document viewport without
being pushed back into it, as the paragraph editor is: they are clipped
and hit-tested only where the document shows, and stay on their field
or point. The form field cursor and Add text's I-beam, move and resize
cursors read the raw pointer against the page's whole rectangle, which
runs on under the chrome; they are now set only while the pages' widget
contains the pointer, as Fill & Sign already does.
`pdfcraft-cli info`, `text`, `render` and `audit` rejected the owner
password of RC4-40, RC4-128 and AES-128 files with "the password is
incorrect", while `run doc_open` accepted it (#2670, item 8). The renderer
authenticates only the user password of revisions 2–4. `Session::open`
works around that by handing it the user password that owner
authentication recovers, but the CLI passed the typed password straight to
`inspect` and `PageRenderer`.

`pdfcraft_engine::render_password_for` now gives any render path outside a
session the password the renderer takes, sharing the recovery with
`Session::open`. The CLI uses it for the renderer and keeps the typed
password for the engine session and the cos document, so the owner
password still lifts copy restrictions in `text`.

The render test decodes the PNG and checks its dimensions and that it
has non-white pixels, for every algorithm with either password.

Refs #2670 (item 8 only).

Checks: cargo fmt --check; cargo clippy -p pdfcraft-cli -p pdfcraft-engine
--all-targets -- -D warnings; cargo test -p pdfcraft-cli; cargo test -p
pdfcraft-engine (and --lib); cargo xtask layers, assets, parity.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Found while testing crates/js/src/formcalc.rs:2348-2352 ("time2num" | "isotime2num")

Co-Authored-By: Claude Haiku 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
The Print dialog always sent its Two-sided choice, Off unless changed, so a
two-sided default set in the printer's Properties never applied. Acrobat has no
default of its own here and follows the printer's. Two-sided now starts at
Printer default, which sends no duplex setting (craft-print's Job::duplex =
None). Off and the two edges still override it. Cut and stack still sends Off,
since it needs single-sided sheets.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The headless counterpart of the Print dialog's Printer default: duplex
"printer" sends no two-sided setting, so the printer's own default applies.
The default stays "off", so existing scripts behave as before. Cut and stack
with "printer" sends single-sided sheets, as the dialog does, and still
refuses an explicit edge.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Windows reports NTE_PROVIDER_DLL_FAIL when the key storage provider
holding a certificate's private key can't load its DLL: smart card or
token middleware that is missing or broken, or a provider that exists
only as 32-bit code a 64-bit process can't load. The certificate list
showed it only as "Windows error 0x8009001D"; it now says what that
means, as it already did for a missing card or provider.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01RUNQSPVXx3dDBabRZHAnjz
Found while testing crates/xfa/src/data.rs:265-281 (iso_to_pattern, with date_tokens at :242 and pattern_to_iso at :285; pattern built in crates/xfa/src/layout.rs:396 date_pattern)

Co-Authored-By: Claude Haiku 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
STATUS_NOT_FOUND passed through by a smart card or token provider (an A3
certificate whose token isn't connected) was shown as a bare code.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01RUNQSPVXx3dDBabRZHAnjz
write_real printed a whole real below 1e15 as an integer, and a larger one
through Display, which has no decimal point either. So 4e9 or 1e30 was
written as an integer token outside the range ISO 32000-2 Annex C gives
integers (±2^31-1), and read back as Int (or, in readers with 32-bit
integers, overflowed). Whole reals now use the integer form only within
that range; past it they keep a trailing '.' (§7.3.3 allows "4.").

Raised in the review of #2877.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01RUNQSPVXx3dDBabRZHAnjz
Patch 38 now also replaces the /ColorSpace of a shading that is
DeviceGray, DeviceRGB or DeviceCMYK with the default for it, whether the
shading is painted by sh or as a shading pattern. Poppler and MuPDF draw
these in the default too. The shading's cache key includes the default,
as the image's does.

The forms test also covers a form whose /ColorSpace has no default: it
draws in its page's, as in Poppler and MuPDF.
The Font size control on the Fill & Sign bar only set the size of text
typed afterwards; text already placed and picked up kept its size, so the
only way to resize it was to type it again. With a placed Fill & Sign text
or date selected, a size typed into the control (or the end of a drag) now
applies to it through Edit::FontSizeAnnotation, one undo step per change.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01RUNQSPVXx3dDBabRZHAnjz
The Layers panel had no automation tool: doc_info listed the layers, but
nothing could show or hide one, so layer visibility could not be checked
headlessly. layer_set does what the panel's eye icon does, naming a layer
by name (every group with that name, as CAD exporters repeat names) or by
its 1-based position in doc_info's list.

page_render built its own RenderConfig with only the password and the
reading colours, so it ignored layer visibility and comments_hide as
well: hiding comments through the tool changed nothing it drew. The
automation renderer now takes the document's own RenderConfig
(Session::render_config), and both tools drop the cached renderer when
they change a setting.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01RUNQSPVXx3dDBabRZHAnjz
A certification signature's /P was read from the current document, so
a later revision could rewrite P 1 to P 3 and have prohibited changes
judged as permitted. Rewriting the existing SigRef/TransformParams was
itself classified as a signature change, allowed at every level.

/P is now taken from the signature in the signed revision (the one
whose ByteRange ends that revision), and a changed Sig, DocTimeStamp,
SigRef or TransformParams that existed when signed is no longer a
signature change.

Test: a P 1 certification relaxed to P 3 in a later revision, plus a
comment, was Valid with the changes permitted; it is now Invalid.
The fix landed as #2698 (9dfc91e0) with its parity evidence, but no
progress-doc record did: add the status line and progress-log entry in
ROADMAP.md, the status line, revision row and M8 milestone note in
docs/roadmap.md, and gap 6's evidence and revision history in
docs/gaps.md — the wrong-result backlog the issue belongs to.
Estimates unchanged.
Main added the word-count command, signature-appearance labels, page
transitions, hover previews and the space-usage report; Hindi,
Indonesian and Korean translate all 80, with their ATTRIBUTION.toml
hashes recomputed. The Vietnamese field entries the parallel review
restored read 'trường' (the Microsoft glossary term), and both
registration tests agree on it now.
image_pixels_follow_the_frame_while_a_corner_is_dragged drags before the
asynchronous page raster has arrived on a slow runner, and the preview
(drawn from that raster) then draws no picture: 'not only at []' on
windows-latest for #3070 and #3071 (a deny.toml-only change). With a
300 ms render delay added locally, main's test fails the same way and
this one passes.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01RUNQSPVXx3dDBabRZHAnjz
An XFA form with no AcroForm fields was always classified as dynamic, so
opening it laid the template out into new pages and appended them as a
revision: the pages the user saw, and any later save, were PdfCraft's
layout rather than the file's own pages, with nothing saying so. Adobe
Designer writes <dynamicRender>forbidden</dynamicRender> in the config
packet of a static PDF form; such a form is now static and its pages are
kept. /NeedsRendering still makes a form dynamic, and with no word from
the config a form without fields stays dynamic as before.

The config packet is found in an /XFA array or a whole-XDP stream, read
with a 16 MiB decode cap, and scanned for the one element.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01RUNQSPVXx3dDBabRZHAnjz
Review follow-up. The config packet is decoded as UTF-16 (byte order mark,
or `<` written as two bytes) or UTF-8 before the scan, as crates/xfa reads
packets. The scan walks tags, skipping comments, CDATA sections, processing
instructions and declarations, and takes the value only at
config/acrobat/acrobat7/dynamicRender, so the same name in form data, in a
comment or elsewhere in the config no longer counts.

The engine test now adds the element to the fixture's own config packet.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01RUNQSPVXx3dDBabRZHAnjz
A /ToUnicode CMap with any unparsable part made the reading path
discard the whole map and fall back to the font encoding, so search
and redaction missed text the edit layer could read.  Vendor
hayro-cmap 0.1.0 and patch it to skip malformed bfchar/bfrange
entries, unresolvable usecmap references and unparseable tails,
keeping every readable entry, matching MuPDF/pdftotext and
PdfCraft's own edit-layer parser.

Fixes #3177.

Signed-off-by: Charan Rathore <180254320+charan-rathore@users.noreply.github.com>
- The truncated-stream guard was dead: completed was set on the break
  path too.  Detect truncation from the scanner position so a stream
  that yields nothing still reports failure and callers fall back to
  the font encoding.
- A skipped bfchar/bfrange entry consumed a different number of
  objects than a parsed one, shifting every later pair onto the wrong
  code.  Skip the entry's remaining objects as well.
- A bfrange array that runs out or breaks cannot be resynced to a
  member boundary; stop the loop instead of spinning over the rest of
  the range.

Signed-off-by: Charan Rathore <180254320+charan-rathore@users.noreply.github.com>
Character-collection metadata is not a mapping: a truncated stream that
parsed only /Registry //Ordering //Supplement produced an empty CMap,
which callers accept and which then blocks the encoding fallback paths.
Drop the metadata exemption so a truncated stream with no mappings
returns no CMap, exactly as a full parse failure did before.

Signed-off-by: Charan Rathore <180254320+charan-rathore@users.noreply.github.com>
# Conflicts:
#	vendor/README.md
# Conflicts:
#	vendor/README.md
With hayro-cmap vendored, its Adobe-authored CMap data no longer
arrives through a crates.io dependency: record it as an [[asset]] (same
SHA-256, licence_file at vendor/hayro-cmap/assets/LICENSE.txt) instead
of the [[bundled]] entry, and add the vendored path to the closed
first-party list in xtask so cargo xtask assets passes.

Signed-off-by: Charan Rathore <180254320+charan-rathore@users.noreply.github.com>
A standard-14 font with no /Widths was measured by code in the WinAnsi-indexed
table, so a /Differences remap took the width of the glyph WinAnsi gives that
code: Helvetica with [65 /W 87 /A] laid out "AAAA" four W's wide. The code is
now looked up through the font's encoding first; /Widths still go by code, and
Symbol, ZapfDingbats and glyphs WinAnsi has no code for keep the code itself.
When the cross-reference chain is unusable (here: a second, truncated PDF
appended after the first, so no startxref is in reach), the index is rebuilt
from object headers and the trailer taken from the last `trailer` dictionary
with a usable /Root. A file written with cross-reference streams has no
`trailer` keyword, so the rebuild fell through to the catalog alone and /Info
(and /ID, /Encrypt) were lost: Document Properties showed nothing, and a full
save after editing one Info field wrote only that field and /ModDate.

reconstruct() now also considers every /Type /XRef stream dictionary with a
usable /Root, taking whichever trailer comes last in the file.

Regression test: document::tests::reconstruction_takes_the_trailer_from_a_cross_reference_stream.
Verified on the issue's fixture (openpreserve PDF-HUL-138/6.2017-0960.pdf):
Creator, Producer, CreationDate and ModDate are read and kept by a full save.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_011K1peyrh5MdZ6ttHkVq8Gh
Document Properties are read through the inspector: the lazy cos
reader refuses a file that needed repair, and lopdf's rebuild only
looked for the trailer keyword, so a file whose only trailer is a
cross-reference stream failed to load there and every field showed
empty. lopdf's rebuild now takes a /Type /XRef stream dictionary as
the trailer, the latest in the file winning as a later trailer
keyword would.
Italian documents write landlines with a slash between area code and
number (0426/123456), and mobiles without separators (3451234567,
+393451234567). The phone pattern needed a separator other than a slash,
so these stayed readable after redaction.

- A slash joins two digits in a number with a trunk-0 area code; a
  North American or + number with a slash still doesn't match, so
  reference numbers like 2024/123456 stay unmarked.
- A number without separators matches only right after a phone label
  (Tel., cell., Fax:, telefono, mobile, ...). Unlabelled runs of digits
  (ids, VAT numbers) still don't match.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Gv2WwdgjmBz33GWkwgNn3Q
A simple font without a usable /ToUnicode reads its text from glyph
names, and a name missing from the glyph list dropped the glyph from the
extracted text while the page still drew. An 821-page book set in Type1C
fonts named C39, C40, ... extracted 4 KB of front matter and nothing else.

- glyph_name_to_text applies the Adobe Glyph List Specification's full
  mapping: drop from the first '.', split on '_', map each component
  through the list or as uniXXXX / uXXXX. f_i, f_l and hyphen.case now
  read as fi, fl and '-'. Type 1, CFF, standard and embedded TrueType
  simple fonts use it.
- A Type 1 / CFF / standard font name that only numbers the glyph (one
  or two letters then digits, ZapfDingbats' aN excepted) reads as the
  code's StandardEncoding character, so C174 is the fi ligature and C208
  an em dash.

The book now extracts 1.9 MB (pdftotext: 1.85 MB), with fi and em dashes
where pdftotext writes Latin-1 mojibake.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Gv2WwdgjmBz33GWkwgNn3Q
Review follow-up: "0426/12/3456" and "0426 12/3456" matched, since any
slash between digits counted. Only one slash is allowed now, and only
right after the first (area code) group.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Gv2WwdgjmBz33GWkwgNn3Q
Add a clean-room Romanian translation of the UI catalog (2,670 entries,
every bundled key), the CLDR plural rule for Romanian (one / few /
other, "20 de pagini"), the ATTRIBUTION.toml entry, coverage and plural
tests, and the localization docs and roadmap entries.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01JadgGo4G2GHaAMNb3qyygk
Review follow-up: in a whole-XDP stream, a config/acrobat/acrobat7/
dynamicRender path inside xfa:datasets/xfa:data came before the real
config packet and decided the form type. The lookup now follows direct
children only: the config child of the xdp root (or the config packet
itself), then acrobat, acrobat7 and dynamicRender.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Gv2WwdgjmBz33GWkwgNn3Q
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Gv2WwdgjmBz33GWkwgNn3Q
The Print dialog's Properties… ran lpoptions on the GUI thread with no
deadline, so a CUPS that stopped answering froze the window. The read
now runs on its own thread (the panel says so meanwhile, and a read
that dies offers nothing rather than waiting), and lpoptions gets ten
seconds through spool::output_within, which kills and reaps a command
at its deadline; the PPD's own defaults then stand. One UI string in
the 21 catalogues.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01RUNQSPVXx3dDBabRZHAnjz
The limit landed inside `command(...)` in one Unix-only test, which this
Windows machine never compiles; checked now with a Linux target.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01RUNQSPVXx3dDBabRZHAnjz
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01RUNQSPVXx3dDBabRZHAnjz
Review follow-up on #2108:
- output_within waited on the reader thread with join, so a process the
  command started could hold the output pipe, and the wait, long past the
  limit after the command exited or was killed. The output now comes back
  over a channel with recv_timeout; such a reader finishes on its own.
- the printer-list and options workers ask for a frame from a guard
  dropped on exit, also when the source panics, so an idle dialog sees
  the closed channel without waiting for another event.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01RUNQSPVXx3dDBabRZHAnjz
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Gv2WwdgjmBz33GWkwgNn3Q
A later cross-reference stream (or trailer) whose /Root points at another
dictionary, such as /Info, replaced an earlier good trailer, and a full save
then wrote that dictionary as /Root. A candidate whose /Root passes the
root_is_catalog rule (/Type /Catalog or /Pages) now beats one whose /Root is
only some dictionary, wherever it is in the file. The looser rule still
applies when nothing better is there, which FDF files (whose /Root has
neither key) and the repair path for a non-catalog /Root rely on.

Test: reconstruction_skips_a_trailer_whose_root_is_not_the_catalog
The mirror of the cos change for render::inspect: a later cross-reference
stream whose /Root points at /Info replaced the earlier trailer, and Document
Properties lost the catalog. A candidate whose /Root is /Type /Catalog or has
/Pages now wins; another dictionary is still taken when nothing better is
there (FDF).

Test: inspect::tests::rebuilt_file_prefers_a_trailer_whose_root_is_the_catalog
Found while testing crates/fonts/src/lib.rs:79 (wrap_with: `text.split(['\n', '\r'])`)

Co-Authored-By: Claude Haiku 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Haiku 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
usable() and is_catalog() checked the object number only, so a later stream
with /Root 1 1 R outranked the trailer naming 1 0 R; get_offset then found no
such object and Document Properties lost the catalog. cos resolves by number
alone, so its rank needs no change.

Test: inspect::tests::rebuilt_file_prefers_a_trailer_whose_root_is_the_catalog
Found while testing crates/js/src/formcalc.rs:2240-2276 ("format" builtin, number branch)

Co-Authored-By: Claude Haiku 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Haiku 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
A BricsCAD plan page (3370 × 1524 pt, 251,000 single-segment strokes and 310
one-triangle type 4 shadings painted with `sh` at 50% alpha) took 2 s per
1024 px tile at every zoom, and 7.9 s for a fit-to-width render; on a busy
machine the 20 s watchdog gave the page up ("page 57 took longer than 20 s and
was skipped"). A 16 px tile of the same page took 65 ms, so neither parsing nor
the strokes were the cost: `sh` fills the whole canvas with the shading, and
`set_paint` sampled the mesh texture over the whole path, one hash lookup and
one texel per canvas pixel, 310 times per tile.

hayro-interpret patch (35): `EncodedShadingPattern::extent`, the device-space
pixels a mesh's triangles touched when it was encoded (`None` for shadings that
paint everywhere, and for a mesh with a `/Background`). hayro patch (19):
`set_paint` returns `PaintSetup::Nothing` for a mesh with no triangle on the
canvas (an empty extent, tested before it is intersected: `Rect::ZERO` sits at
the canvas origin), which the fill, stroke, rect and stencil callers skip, and
otherwise intersects the texture with the extent and pads instead of repeating it.

Measured with `page_timing` on the page: fit 7864 → 880 ms, a 1024 px tile
2030 → 85 ms at 100% and 2030 → 70 ms at 400% (the 16 px tile's 65 ms is the
page's re-interpretation, paid per tile), with byte-identical output hashes.
The three `mesh_extent_tests` pin what the texture must and must not cover.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
A paragraph running past its page's bottom edge, as HTML-to-PDF converters
write when they rely on the page clip, opened in an editor that covered the
next page with its box and text, and paragraph outlines crossed the gap
between pages. The editor is clipped to its page within the document area
(it also covered the tool bars when scrolled up), and outlines to their page.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01RUNQSPVXx3dDBabRZHAnjz
Merges #2813 (dynamic-XFA layout in the CLI) into the owner-password branch. In
info, text and render the engine Session and the cos document keep the typed
password, while anything handed to `inspect` or `PageRenderer` gets the render
password (the user password recovered from an R2-R4 owner password), including
the dynamic-XFA detection in render and the original-bytes check in text.

Checked: cargo fmt --check; clippy -p pdfcraft-cli -p pdfcraft-engine --all-targets
-D warnings; cargo test -p pdfcraft-cli (75 passed) and -p pdfcraft-engine (283 lib
+ integration passed); cargo xtask layers, assets, parity; a throwaway RC4-40,
RC4-128 and AES-128 dynamic-XFA check with the owner and user passwords through
info, text and render (not committed).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Layer options in the Layers panel header resets every layer to the state the
file's default configuration (/OCProperties /D) gives it, as Acrobat does.
Visibility is view state, so the reset doesn't mark the document changed and
doesn't enter Undo. doc_info now reports each layer's initial state.
Pictures in templates and data that are BMP or TIFF were left out with a
warning. They are now recognised by their signature, sized from their
headers, and decoded through the same bounded reader as GIF (first page
of a TIFF). A BMP needs a valid info-header size after "BM", so text
starting with those two letters is still not taken for one.

A BMP or TIFF chosen for an XFA image field is kept in the data as
image/bmp or image/tiff, since the layout can now show it.
Review follow-up on #3192.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Gv2WwdgjmBz33GWkwgNn3Q
Review follow-ups on #3239.

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

Co-Authored-By: Claude Haiku 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
With signed attributes, the signature value is made over the attributes,
and their messageDigest is what ties it to the document. The digest was
compared only when the attribute was present, and the content-type
attribute was never checked. RFC 5652 section 5.3 requires both.

A signature whose signed attributes have no messageDigest, or name a
content type other than id-data, is now invalid. A missing content type
is reported and the signature is read anyway. A signature that still
validates with an RSA key under 2048 bits now gets a note, as SHA-1 and
RIPEMD-160 do. The four new sentences are in WORDINGS and in Spanish.
With a valid certification signature, the message bar said "Certified
by ..." whatever the other signatures said, so an approval signature
added later that is invalid, or whose validity is unknown, did not show
in the bar. The Signatures panel listed it correctly.

The bar now says "Certified by ..." only when every signature is valid.
Otherwise it shows "At least one signature is invalid." or "At least one
signature has problems.", as it already did for documents without a
certification.
The Lithuanian and Vietnamese catalogs came to main after this branch.
Their tests require every key the other catalogs have, so the four new
signature messages need a line in each.
The catalogs this PR changes are tracked assets, so their recorded
hashes follow the edits (cargo xtask assets).
Czech had every `tl!` literal but lacked ~220 keys the other catalogs
carry: the sentences the engine words (signature details, accessibility
findings and report, PDF/A problems, pages to print, history labels),
the date picker's weekday heads, the certificate viewer's fields and
shared buttons. All are now translated (2,674 entries).

"Tag" is "tag" throughout, as the catalog's own glossary says: the Tags
panel said "značky", which also names redaction marks, and "{n} tags"
read "1 značek".

Two new tests pin the engine wordings (with their holes), weekdays and
certificate fields; both fail on the previous catalog.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The rail laid its panel buttons out top-down and the page navigation
bottom-up in the same space, so in a window too short for both they were
drawn on top of each other, with the page number field over the icons.

The navigation cluster is now its own bottom panel and the panel buttons
sit in a vertical scroll area in the height left above it.
Resolve the conflict in crates/ui-egui/src/i18n/fr.tsv: main's new rows are
kept as they are and the branch adds only the keys main lacks.
Undo the effect of bd740ee9: every row that main already has is back to
main's text, the rows this branch adds keep the wording of the first commit,
and the header note recording the exception is removed. The catalog stays
clean-room as its header says.
`path_as_rect` decided a path was a rectangle from its element count and from
the four corners of its bounding box being covered. Its comment names the shape
it means to match -- one MoveTo, three LineTo, one ClosePath -- but `len() != 5`
also admits one MoveTo and four LineTo, a five-point open polyline, and
covering the corners says nothing about the points between them. Both call
sites then paint `device.draw_rect(bbox)`, so such a path was drawn as its own
bounding box.

Rendered through `pdfcraft-cli render`, an open zigzag whose ends reach the
corners of its bounding box

  80 38 m  80 21.75 l  60.571 29.875 l  40 21.75 l  40 38 l  S

lost its middle peak and gained a straight edge the file does not contain: no
dark pixel near the peak, and 36 along the bounding box's top. A six-point
zigzag and a real rectangle both drew correctly, which is why this shows on
some art and not the rest -- and `s` escapes it because its ClosePath makes six
elements.

A point that is not a corner now rejects the path. A rectangle still matches
whether written `m l l l` and closed or `m l l l l` back to its first corner,
since every one of its points is a corner. The same mistake also discarded a
clip of that shape, through `push_clip_path`, which this fixes too.

Found from craft-libs' ATTRIBUTION note that the shared reader centralizes the
patched Hayro trees from PdfCraft: the same flaw is in craft-libs, sent there
as storytold/craft-libs#175.
Fixes #3216

Co-Authored-By: Claude Haiku 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Haiku 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Haiku 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
An optional content membership dictionary may carry a visibility
expression (/VE, PDF 1.6): [/And e...], [/Or e...] or [/Not e], each e an
optional content group or a nested expression. It takes precedence
over /OCGs and /P (ISO 32000-2 8.11.2.2). hayro-interpret ignored it, so
content under such a dictionary stayed visible when its layers were
hidden in the Layers panel.

Vendored hayro-interpret patch (40) evaluates the expression, nested
up to 32 deep. A malformed expression (unknown operator, /Not without
exactly one operand, /And or /Or without any, an operand that is
neither a group nor an expression, too deep) falls back to /OCGs and
/P as before.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01RUNQSPVXx3dDBabRZHAnjz
Review follow-ups on #2987; the patch (40) test reference moves into the Test column.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Gv2WwdgjmBz33GWkwgNn3Q
hayro-interpret draws every image XObject inside a transparency group
that applies the fill opacity (ca). A stencil mask (/ImageMask true)
also takes the fill colour, with that same alpha, as its paint, so the
opacity was applied twice: at ca 0.5 a mask was drawn at 25 %, at
ca 0.25 at about 6 %. Rectangles and raster images were right.

Vendored hayro-interpret patch (36) resets the fill alpha to 1 once
the image's group is pushed, before the stencil's paint is taken; the
saved state is restored after the image. Type 3 glyph and pattern
stencils take their paint from the glyph or pattern and are unchanged.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01RUNQSPVXx3dDBabRZHAnjz
Review follow-up. A pattern paint keeps the fill or stroke opacity it was
set with (`scn`), so resetting the graphics state's fill alpha did not stop
a pattern-filled stencil mask from being drawn at the opacity squared:
`Pattern::without_opacity` now drops it for the mask's paint (patch 35).
The renderer also painted a pattern-filled stencil mask with the stroke
paint, so an uncoloured tiling pattern drew in its stroke colour (black by
default); it now uses the fill paint.

The stencil test covers a colour fill, an uncoloured tiling pattern and a
shading pattern at ca 1, 0.5 and 0.25; each of the three changes, removed,
makes it fail. Rebased onto main; the vendored hayro-interpret patch is now
number 35 (36 was taken on main).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01RUNQSPVXx3dDBabRZHAnjz
The mesh sampler took each pixel's colour at its corner, and only when the
corner was strictly inside a triangle. A corner on an edge two triangles
share belonged to neither, so a Type 4-7 shading drew a white seam along
every shared edge that ran through the sample grid, and dropped pixels
along the mesh's own sides (a 30 x 30 square cut along its diagonal:
812 of 900 pixels; MuPDF 900).

Such a pixel now goes to the triangle that covers its centre, edges
included (Triangle::covers), and still takes the colour at its corner, so
every pixel that was drawn keeps its colour. Vendored hayro-interpret
patch 41.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01RUNQSPVXx3dDBabRZHAnjz
Review follow-up: a red/blue regression for the centre-coverage pass. The
shared diagonal sits at x + y = 50.25 in device space (`cm` moves the mesh
down a quarter point), so pixel (20, 30) has its corner in the red triangle
and its centre in the blue one. It must stay red with either triangle
first; replacing `or_insert_with` with `insert` turns it blue when the red
triangle comes first, and the test fails.

Rebased onto main; the vendored hayro-interpret patch is renumbered 36
(41 before; 35 is taken by PR #2977).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Gv2WwdgjmBz33GWkwgNn3Q
M3: preserve reading position while joining page edges, with persisted defaults and opt-in UI control. Translate both labels in all 22 catalogs, update attribution, and cover catalog completeness with a regression test. Rebase PR #3101 onto current main while retaining navigation and rendering fixes.
Co-Authored-By: Claude Haiku 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
M3 / PR #3101: avoid stale organizer anchors and synchronize the active comparison gap choice across both panes through menu and control paths. Three regression tests fail before these fixes and pass after them. Document the gap helpers and test contracts.
Join multiple streams in a page's /Contents array with a space (b' ') rather than a newline (b'\n') in autotag::content_bytes, alt::page_content, and content::page_content so a % comment at the end of one stream continues through the start of the next stream per ISO 32000-2 section 7.8.2.

Update alt::artifact_marked_content to join page content streams with pdfcraft_content::Pieces::join, parse operators across the joined stream, and rewrite only matching BDC spans via Pieces::splice so operators split across stream boundaries and cross-stream comments are preserved.

Fixes #3217
Resolve the ROADMAP.md progress-log conflict by keeping both sides' entries.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
While the window was being resized, Windows stretched the window's swap
chain, still at the old size, to the new one: the text shimmered and the
left panel's width bounced for as long as the drag lasted. A DirectComposition
visual's last frame is shown at its own size instead. WGPU_DX12_PRESENTATION_SYSTEM
still overrides it.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Gv2WwdgjmBz33GWkwgNn3Q
A field's data picture (<bind><picture>, such as date{YYYYMMDD}) is how
its value is written in the XFA data, but PdfCraft always wrote dates
there as ISO (2026-10-18) and read them back as ISO, so a form that keeps
its dates as 20261018 got a different format from PdfCraft than from
other viewers, and data those viewers wrote was shown as stored.

The parser keeps <bind><picture> as Field::data_picture. A typed date is
still read in the field's own pattern and turned into ISO; it is then
written in the data picture when the field has one. Reading goes the
other way, on layout and when values are taken from the data of a form
laid out before. A value that doesn't match its data picture is read as
ISO, so data written by earlier versions still reads. The date-letters
rule for XFA picture clauses moves to one helper (date_letters) that the
layout's date_pattern now uses too.
The template field behind a SOM path is now found by its number, as the
form numbers same-named siblings and repeated instances; the live form's
value refresh uses the same lookup. A data picture without a year, month
and day (date{YYYYMM}) keeps ISO dates in the data, since it can't hold
the date the field shows.
Review on #3239: the Adobe Glyph List Specification's `uni` and `u` forms
take uppercase hexadecimal digits only (section 2), and its examples map
`uni20ac` to an empty string. glyph_name_to_text accepted lowercase too.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Gv2WwdgjmBz33GWkwgNn3Q
A two-digit year reads back as 1950 to 2049, so 2050 written as 50 came
back as 1950. A date is now written in the data picture only when it
reads back from it unchanged.
Opening a PDF from Finder (or Open With, or a Dock drop) while PdfCraft was
already running added the tab but left the window where it was: behind
Finder, minimized, or full screen on another Space, so nothing seemed to
happen. The same applies to files handed over by another launch on Windows
and Linux, which arrive through the same OsEvent::Open path.

After opening the files, un-minimize the window and focus it, which on
macOS activates the app and orders the window front (switching to its
full-screen Space).

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Review follow-up: the earlier assertion reopened the same path, which takes
the existing-tab shortcut. This one hands the running app a new file with
preparation held, checks Minimized(false) and Focus are sent before release,
and checks the new file is the selected tab once it is prepared.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Full screen is the page on black and nothing else: one page (or one spread,
when two-page view was on, #1353) fitted to the screen, from the current
page; a click, Space, Enter, the arrows and Page Up/Down turn pages, a
shift-click and Backspace go back; Esc brings the display from before
back at the page reached. The quick tools are not drawn in full screen.

Transitions, auto-advance, loop and the cursor hiding stay planned.

Refs #379.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01RUNQSPVXx3dDBabRZHAnjz
Review follow-ups on #2070.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Gv2WwdgjmBz33GWkwgNn3Q
Review on #2070: fit_zoom fitted the document's largest page in every
two-page display, so a smaller spread stayed small in full screen. While
presenting, one spread at a time, it now fits the spread shown, the cover
page on its own included; in the window, and while scrolling, the zoom stays
one for the document. The spread arithmetic is one helper, shared with
step_page.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Gv2WwdgjmBz33GWkwgNn3Q
hayro-syntax decoded every JPX image with resolve_palette_indices off. That
is right when the image dictionary gives a /ColorSpace (an /Indexed space
needs the raw indices), but with no /ColorSpace the colours come from the
JP2 header (ISO 32000-2 §7.4.9) and nothing applied its palette: the
indices were drawn as a gray image. The palette is now resolved exactly
when the dictionary has no /ColorSpace.

Two unit tests decode a 4x2 palette JP2 written for the test: without a
colour space it is red/blue RGB, with /Indexed the raw indices stay.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01RUNQSPVXx3dDBabRZHAnjz
Review follow-up: num_components is unset both when the dictionary has no
/ColorSpace and when it has one that does not resolve. In the second case
the JP2 header's colours are the only ones available, so applying the
palette is intended; the comment and vendor/README.md now say so.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01RUNQSPVXx3dDBabRZHAnjz
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Gv2WwdgjmBz33GWkwgNn3Q
In a knockout group (/K true) each element composites with the group's
initial backdrop, not with the elements drawn before it (ISO 32000-2,
knockout groups). The renderer read /K (hayro-interpret patch 28) but
drew such a group like any other, so overlapping semi-transparent
shapes in it blended with each other.

hayro-interpret patch (39) tells the device a group is a knockout
group (`Device::set_knockout`, default no-op). hayro patch (19) then
draws that group's fills, strokes, glyph outlines and shadings with
Compose::Copy, which vello_cpu weights by coverage, so each replaces
what the group drew under its shape. Images and nested groups are
drawn into a group layer of their own and still composite normally:
copying a whole layer would erase everything outside it.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01RUNQSPVXx3dDBabRZHAnjz
An image or a nested group is drawn into a layer of its own, which
covers the whole canvas (transparent where nothing was drawn), so it
can't be composited with Compose::Copy. What is under its shape is
now cleared first and the layer then composites over it:

- an unmasked image's shape is its placement rectangle: the
  interpreter hands it to the device (`Device::knockout_shape`), which
  clears it with a transparent Copy fill;
- a nested group's shape is the union of its elements' shapes: the
  interpreter draws its content once in the device's erase mode
  (`knockout_erase_wanted`, `begin_knockout_erase`,
  `end_knockout_erase`), where every paint clears under its shape and
  nothing is layered or drawn as an image, and then draws the group,
  always in a layer of its own (drawn straight onto the knockout
  group, its own elements knocked each other out).

A masked image's shape is its mask; it composites as before, which
matches MuPDF where the mask is opaque or transparent.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01RUNQSPVXx3dDBabRZHAnjz
Review follow-up on #2974: the nested group's checks no longer depend on the
parser repairing a short /Length.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Gv2WwdgjmBz33GWkwgNn3Q
After rebasing onto main: #3356's one-cell pattern renderer (merged) builds
a Renderer without the knockout fields this PR adds. Like the tiled cell's
renderer, a pattern cell is painted as its own artwork, so it starts with
no knockout group open.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Gv2WwdgjmBz33GWkwgNn3Q
Since #1557 the page background was painted inside the page
transparency group, as its initial backdrop, so that a Multiply
highlight under reading colours blends with the dark background. With
document colours that made every object blend with the white paper:
a green /BM /Screen fill over an empty part of the page came out
white, and Difference or Exclusion magenta. Only Normal, Multiply,
Darken and HardLight, for which blending with white gives back the
source colour, looked right. 0.8.0 was correct.

The page's objects composite with an initially transparent backdrop
and the page group is then put on the paper (ISO 32000-2, page group).
Vendored hayro patch (18) now paints the background inside the group
only under reading colours, where #1548 needs it, and outside it
otherwise, as before #1557.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01RUNQSPVXx3dDBabRZHAnjz
Hue, Saturation, Color and Luminosity blended with the white paper the
same way; the fix covers them, and the test now checks them over an
empty part of the page.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01RUNQSPVXx3dDBabRZHAnjz
Review on #2070 (CodeRabbit):
- A click turned the page on its release, so a double-click's second click,
  meant to select a word as the PR promises, landed on the next page. The
  turn now waits out egui's double-click time, and is dropped when a
  selection appears or full screen is left; a double-click on blank page
  area still turns once.
- Entering full screen from another tool left it on, and clicks only turn
  pages with Select while the quick tools are hidden. Full screen now selects
  Select and gives the earlier tool back on leaving, as Space's temporary
  Hand tool does.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Gv2WwdgjmBz33GWkwgNn3Q
Review on #2974. In the erase pass `set_paint` returned no clip, so a
shading fill cleared its whole path while the redraw is clipped to the
shading's /BBox (ISO 32000-2 Table 77): red outside the box stayed erased.
It now returns the shading's clip there too. And an unmasked image cleared
its placement before decoding, so an image that doesn't decode (over
MAX_IMAGE_PIXELS, damaged data) erased the group's earlier artwork and drew
nothing; the clear now happens only when the image decodes, asked only in a
knockout group (Device::in_knockout_group). The stencil case in the test now
also checks that the stencil draws.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Gv2WwdgjmBz33GWkwgNn3Q
Review on #3184: with no /BitsPerComponent the decoded image took the
codestream's precision, which for a palette image is that of the indices.
The resolved colours are decoded to 8 bits, so a 1-bit index rescaled them to
0 or 255 per channel ((64, 128, 192) became (0, 255, 255)). When the palette
is applied the samples are now reported as 8-bit; an image without a palette,
or one whose raw indices are kept for an /Indexed space, keeps its precision.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Gv2WwdgjmBz33GWkwgNn3Q
Review on #2070: a click on the page schedules a turn after the
double-click time, and a link clicked before then jumped and then the page
still moved on. Following a link now drops the pending turn.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Gv2WwdgjmBz33GWkwgNn3Q
Review on #2974: the interpreter decided with a full-resolution decode
(`decoded_raster(None)`) before the renderer decoded at its draw size, a
second decode of every downscaled image under another cache key; and an
image a preview skips for its size still cleared what was under it.
`Device::knockout_image` now lets the renderer decide, with the same
eligibility and decode size as `draw_image` (shared `image_draw_size`), so
the draw reuses that decode and a skipped image clears nothing.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Gv2WwdgjmBz33GWkwgNn3Q
`Session::reload` (the file changed on disk) opens the new bytes with a
default render config, so a document shown white on black, or with its
comments hidden, went back to the file's own colours and comments until
the preference was toggled. The UI re-applies the preference only on
open. Reload now carries both display settings over and rebuilds the
renderer with them.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Gv2WwdgjmBz33GWkwgNn3Q
The end-of-data look-ahead rejected a valid EI candidate when any byte
above 0x7F lay between it and the next EI. Those bytes are the content
stream that follows the image, and content streams carry such bytes
inside strings, so no candidate qualified and the image data ran to the
end of the stream. The operator check that follows now decides alone,
bounded as before by MAX_EI_CANDIDATES.

Fixes #2456
A footnote marker drawn with Ts (text rise) and a smaller Tf size sits on
the same visual row as the body text around it, but the row-joining
baseline tolerance was scaled by the smaller of the two runs' sizes
(20% of it) — far tighter than a typical superscript's rise, which runs
up to about half the body size. The marker split off into its own line,
and then its own block, fragmenting one paragraph into three separate
editable zones in Edit PDF > Edit text and images.

Scale the tolerance by the larger of the two sizes instead, which still
leaves ordinary next-line leading (about 1.2x the font size) well
outside it.

Added a regression test in crates/edit/src/tests.rs that fails on main
(reverted the fix locally and confirmed it goes red: 3 blocks instead
of 1) and passes with the fix.

cargo fmt --check, clippy -D warnings, cargo test -p pdfcraft-edit (186
passed), cargo xtask layers and cargo xtask assets all clean.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
A form with /NeedAppearances true (ISO 32000-2 Table 224: the writer
"has not provided appearance streams for all visible widget
annotations") showed its text fields empty: PdfCraft only read the flag
when writing, and the display copy that draws missing appearances skips
widgets. Filled forms from tools that leave drawing to the viewer looked
blank, while Acrobat, Firefox and MuPDF show the values.

pdfcraft_forms::with_field_appearances returns a copy with text and
choice widgets drawn from their values (every one when the flag is set,
otherwise those with no /AP /N), with the generator a typed value uses.
The engine's display copy (page view, page images, Print as Image, CLI
render) applies it after the comment appearances. The document itself
is not changed, so saving writes the file as it was.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Gv2WwdgjmBz33GWkwgNn3Q
Review follow-up (CodeRabbit on #3475). A form edit refreshes the display
copy on every keystroke, and with /NeedAppearances that redrew every field
of the form: 68 ms per edit on a 1000-field form against 7 ms on main.

FieldAppearanceCache keeps each widget's drawing with the objects it was
drawn from: the widget and its field ancestors, the form dictionary and
the fonts of its /DR, each with the objects its entries refer to. An
entry is used only while each of them is still the same object
(Arc::ptr_eq); the document replaces an object it edits, so a changed
input is a miss, never a stale hit. The engine keeps one per editor. A
field edit on that form now takes 13 ms.

The repair note on a failed display write now names field appearances
too.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Gv2WwdgjmBz33GWkwgNn3Q
describe() named an RSA key by its modulus length in bytes times
eight, so a 2047-bit key read "RSA 2048-bit" next to the new warning
that it is shorter than 2048 bits, and a modulus with a leading zero
byte read 8 bits too long. It now uses rsa_bits, as weakness() does.
verify_noting adds the note only when the signature verifies. The test
uses a synthetic 1024-bit key and its PKCS #1 v1.5 signature: the valid
signature carries the note, and one that doesn't verify has none.
WSCAD (and other CAD exporters) write `/ToUnicode /Identity-H` — a name,
not a CMap stream — on composite fonts whose two-byte codes are the UTF-16
code units. Acrobat, MuPDF and pdf.js read each code as its Unicode value.
hayro resolved the name to the Identity CMap, which maps codes to CIDs only,
so such pages had no text at all: nothing could be selected, searched,
copied or edited (147 of 147 pages of a WSCAD schematic).

- vendored hayro-interpret patch 35: `Type0Font::identity_to_unicode`
- crates/fonts: the same reading for Edit text, find and redaction
  (controls and lone surrogates map to nothing; simple fonts unaffected)

Tests fail without the fix: render `identity_to_unicode_names_give_text`,
fonts `identity_to_unicode_names_read_codes_as_unicode`.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
crates/fonts mapped codes U+007F and U+0080–U+009F to those controls while
the renderer's text layer (hayro patch 35) gives them no text. The generated
map now covers U+0020–U+007E and U+00A0–U+00FF in the first high byte, as
the text layer does; both tests check DEL and a C1 control.
(CodeRabbit review on #3222.)

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
My own people.toml entry (real name shown next to the username when the
About window's name toggle is on "Real name"), committed by me as
docs/contributors.md asks.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The two Identity-H tests are evidence for view.render-cid-fonts, which
stays partial; gaps.md records the change under gap 2. Estimates unchanged.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01LymhVaGZCWwB9HNEbLzeMM
2,670 entries in ckb.tsv, written in logical order, with the same
(context, source) key set as ar.tsv. Registered with rtl: true and
plural_one_other (CLDR ckb: one, other); Sorani keeps the noun singular
after a numeral, so the 13 @plural rows usually read the same in both
forms.

RTL reuses the Arabic path: the catalog is put into display order by
bidi::display_rtl when it loads, so Kurdish has the same four limits
docs/localization.md lists for Arabic. No new layout or bidi code.

DATE_LANGUAGES gains Kurdish month and weekday names, because
date_languages_are_the_interface_languages keeps both registries in
step.

ku-* locales are not mapped to ckb on purpose: ku is the macrolanguage
and means Kurmanji in Latin script. Only ckb* tags resolve.
# Conflicts:
#	contributors/people.toml
Type0Font::new and char_code_to_unicode get doc comments, and
unicode_map's names the identity ToUnicode name. The comments and
vendor/README.md cite Acrobat's observed behaviour only.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01LymhVaGZCWwB9HNEbLzeMM
# Conflicts:
#	crates/edit/src/tests.rs
# Conflicts:
#	parity/acrobat-features.toml
CodeRabbit review on #3471: the widened baseline tolerance (half the
larger run's size, for a same-row footnote marker) could also let two
genuinely stacked rows of ordinary body text join into one row, if
horizontal compression (Tz) narrows the first row enough to put the
second row's start within it — a negative `gap` inside the existing
overlap allowance, with a baseline shift inside the new 50% band but
outside the original 20% one.

Only take the wider tolerance when the two runs don't overlap
horizontally (`gap >= 0.0`), which is true for a footnote marker drawn
right after the text it follows. Overlapping runs keep the original,
tighter 20% tolerance.

Added stacked_rows_that_merely_overlap_do_not_join_into_one_row,
confirmed it fails without this change (merges into one row/block) and
passes with it. cargo test -p pdfcraft-edit (197 passed), fmt, clippy
-D warnings, xtask layers and xtask assets all clean.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
fontHorizontalScale on a font, and xfa-font-horizontal-scale in rich text,
condense the glyphs; PdfCraft ignored both, so condensed labels were laid
out at full width and overflowed or wrapped where the form fits them on
one line. Text is now measured at its scale and written with Tz, in a
graphics state of its own. Field values keep full width: the forms layer
draws them, and its appearances have no horizontal scaling.
Review follow-up on #3497: the editing map in crates/fonts and the vendored
renderer each read an identity /ToUnicode name as code = Unicode without
controls or lone surrogates; each now names the other, so a change to one
is made to both.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01LymhVaGZCWwB9HNEbLzeMM
While a handle of a Fill & Sign line, check, cross, dot, typed signature
or comment stamp was dragged, only the selection frame followed the
pointer; the stamp kept its old size (or place) until release. Image
signatures already had a live layer. Every stamp now gets one: the page
without the stamp, and the stamp drawn alone on a transparent page,
whose pixels are stretched with the frame. Resizing a stamp only changes
its /Rect, which its appearance is stretched to fit, so the preview is
what the release produces.

- render: RenderConfig.transparent clears the page to transparent.
- annot: isolate_annotation leaves a page showing one annotation alone.
- engine: Document::stamp_preview builds the two read-only layers; the
  page beneath gets the appearances drawn for comments without one.
- ui-egui: the signature drag layer covers every stamp, rendering the
  stamp's own tile sharp at the current zoom.
4.125 × 9.5 in (297 × 684 pt) in the print dialog's Paper list and
doc_print, fed to CUPS as Env10. Appended after the 4 × 6 label so
LABEL_PAPER keeps its index.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
`link_list` and `form_set_props` indexed the viewer's pages with a page number from the
editing layer. On a file whose page tree has a dangling `/Kids` entry (#2660) the viewer
sees three pages and the editing layer six, so `link_list` panicked ("the len is 3 but
the index is 3") and so did moving a field on one of the later pages.

Rectangles on a page the viewer could not read are now reported unchanged, as `doc_info`
already reports them (`link_list`, `comment_list`, `content_list`); `comment_list` no
longer borrows the last page's geometry for them. Placing a rect on such a page is an
error (`form_set_props`, and `sign` when the viewer has no page at all).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
When the current page's label is not its page number (front matter
numbered i, ii, then 1, 2 …), the rail shows "3/180" under the page box
instead of only the page count, with "Page 3 of 180" on hover. It falls
back to the count when the text would not fit the rail. The page box
itself already shows and accepts labels since #1667 (0.8.0 showed the
physical index).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Words keep the space after them, and the line-fit test counted it, so a
label written with a trailing space wrapped when its box was a fraction
of a point narrower than the label plus that space: DA Form 31's 'a.
DATE ' and 'b. TIME ' put their second word on top of the field below.
A line now fits by what it draws, and alignment ignores every trailing
space, not just one.
CodeRabbit review on #3471: even with the gap >= 0.0 overlap guard, two
equal-size rows with no horizontal overlap (not just overlapping ones)
could still land inside the widened 50% baseline tolerance at a 5pt
shift that the original 20% tolerance would have rejected, merging
distinct body-text lines into one row.

The wider tolerance is meant for a run distinctly smaller or larger
than its neighbour (a super/subscript), not two ordinary same-size
rows. Require prev.size.min(line.size) < prev.size.max(line.size) * 0.9
before taking it; equal-size runs keep the original 20% tolerance
regardless of overlap.

Added equal_size_rows_need_the_tighter_tolerance_even_without_overlap,
confirmed it fails without this change (merges into one row/block) and
passes with it, alongside the two existing regression tests from the
earlier rounds of this fix. cargo test -p pdfcraft-edit (198 passed),
fmt, clippy -D warnings, xtask layers and xtask assets all clean.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
The layout draws a non-interactive field's text itself, condensed, but
measured its height at full width, so a condensed value reserved lines
it didn't use. Fields now measure and draw their text in one face:
scaled when the layout draws it, as designed when the forms layer does
(the over-page fallback included).
Review on #2715: reverting only the content_list guard still passed. The fixture now has
text added by PdfCraft (`/PCAdded`) on page 6, and content_list must report it at its
unchanged rect [70 70 150 100]. form_set_props must refuse a rect for the field on page 6
with "name is on page 6, which could not be read" and leave form_fields unchanged.
Reverting any one of the link, content or form guards now fails the test.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
A word of spaces alone draws nothing, but after a word whose own
trailing space ran past the box it failed the fit test and opened a
blank line, so the text measured a line taller than it draws. Such
words now always stay on the line, keeping their width.
A draw or a button with no width took all the width available, so a
right-aligned label slid to the far side of its subform and a button
spread over what lay beside it: DA Form 31's Instructions button covered
its header and the email line, and its PERSONNEL/FINANCE line ran into
the field after it. They now grow to their text, unwrapped, within what
is available and never below their minimum width, as containers do.
A button with no height was sized like a text field in its field font,
with text-field padding, so DA Form 31's Instructions button (10 pt
field font, 8 pt caption) came out about 17 pt tall and over the form
below it. It is now as tall as its caption, never below its minimum.
Button appearances always drew the caption in regular Helvetica, sized
from the button's height, whatever the field's /DA asked for: DA Form
31's 'Attachments Menu' (9 pt bold) came out at 4 pt. The caption now
takes the /DA's font from the form's /DR, its size (an automatic size
still fits the button) and its color, as text fields already do.
Appearance text in a simple font was measured with Helvetica's widths,
whatever the font: a bold or Times caption or value came out narrower
than drawn, so centered and right-aligned text slid right of where it
belongs. Text is now measured with the font's /Widths, or a standard
font's published widths.
A button was given its content area (inside its margins) as its widget
rectangle, though its border belongs to the field: DA Form 31's
Instructions button, with a 1.25 pt top margin, came out shorter than
the Attachments Menu button beside it. Its box is now the whole field;
the margins pad the caption.
RFC 5652 allows one content-type and one message-digest attribute in
the signed attributes, each with one value. The parser kept the last
attribute and the first value, so a CMS with two of either could mean
one thing here and another in a different reader. Such a CMS is now
not read.

The content-type attribute must also name the type of the encapsulated
content. The signature covers the attribute but not eContentType, so a
CMS where the two differ is not read either.
A content-type attribute value or an eContentType that was not an OID
was read as missing, which skipped the check that the two match. Such a
CMS is now not read, and neither is one without an eContentType.

A PDF signature's eContentType must also be data. With a content-type
attribute this already follows from the checks on the attribute.
Without one, a signature whose eContentType named another type could
still validate as a signature of the document. It is now invalid.

The PDF test now covers a wrong eContentType and a repeated
content-type attribute, and the README says that a missing
content-type attribute is read anyway, with a note.
# Conflicts:
#	crates/ui-egui/src/control.rs
#	crates/ui-egui/src/lib.rs
#	crates/ui-egui/tests/control.rs
#	parity/acrobat-features.toml
The harness window is live, so open_path hands the read to the background
worker and run_steps(2) could not promise the worker had finished: under
load the file could be deleted before it was read, failing the test with
"no second tab". wait_until polls for the tab the way the drop tests
already do. Proven by delaying the worker 300 ms: the old test fails at
views.len() == 0, this one passes.
Byte-only drops (no filesystem path) have nothing for tab_showing to
match and open another tab, pinned by
opening_documents_without_a_path_still_open_twice; the drag-drop-open
note claimed focus for "a dropped document that is already open"
without that limit.
New settings start with Highlight fields on. A stored choice is kept; a missing or
malformed value falls back to on. Preferences > Appearance gets the checkbox.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
The stand-ins for the standard fonts (PDFium's Foxit fonts) have neither the
comma-below letters nor a commaaccent glyph, and the composite-letter patch
left comma accents out. A non-embedded font naming /scommaaccent therefore
drew nothing, which is the blank "si" left in Romanian XFA forms, and
/tcommaaccent drew a cedilla through the glyph list's alias to U+0163.

Romanian s/S/t/T and Latvian k/K/l/L/n/N/r/R/G with a comma below are now
built from the letter and the font's comma, scaled to three quarters and set
just under the baseline, moved back along the slant in italics. The uni021A and
uni021B names of T and t with a comma below, which the glyph list
does not name, resolve by code point. Lowercase g with its turned comma above
is still not built.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
load_certificates now reads PKCS #7 certificate bundles (degenerate
SignedData, RFC 5652 §5.2) in DER and as PEM PKCS7/CMS blocks, the way
Windows, Acrobat and certificate authorities export chains. PEM END
lines must match their BEGIN label.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
`[(FIRSTARR)] [(LASTARR)] TJ` is malformed (TJ takes one array
operand), but the operator still shows the one immediately before it,
as the renderer, MuPDF and pdftotext agree. Three readers here counted
operands from the front instead and showed the discarded first array:

- crates/edit/src/text.rs (text_lines/text_paragraphs, and the Office
  export through reading_lines)
- crates/redact/src/interp.rs (search-based redaction's content
  interpreter)
- crates/a11y/src/headings.rs (autotagged heading detection)

So the editor could offer a word the page never draws, a redaction
search could miss it, and a heading could read as text nobody sees.
Take the last array for TJ in all three, matching the `_ =>
op.operands.last()` arm already used for Tj/'/" alongside it.

Added a regression test per crate (`a_tj_with_a_surplus_array_reads_
the_last_one`, `a_tj_surplus_arrays_undrawn_glyphs_are_not_seen_
under_a_mark`, `a_heading_with_a_tj_surplus_array_reads_the_one_the_
page_draws`), confirmed each fails without its fix and passes with it.
cargo test -p pdfcraft-edit (196 passed) -p pdfcraft-redact (81
passed) -p pdfcraft-a11y (44 passed), fmt, clippy -D warnings, xtask
layers and xtask assets all clean.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
A font naming a Latvian comma-below letter by its code point (/uni0136) or by
the glyph list's name for it (/Kcedilla, /Gcedilla...) still drew nothing:
the name resolves to the cedilla name the glyph list prefers, which the
composite table does not hold. The thirteen comma-below letters are now
looked up by character, which also replaces the special case for U+021A and
U+021B. T and t with a cedilla (U+0162, U+0163) are not in that table and
keep their cedilla.

Raised by CodeRabbit on the PR.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Paragraph numbers are positions in a list regrouped from geometry after
every edit, so an edit that merged a paragraph into its neighbour made the
next edit by number rewrite another paragraph and lose its text.

After text_paragraphs, text_edit and text_edit_layout find the listed
paragraph again by its text and box, and refuse one that no longer exists on
its own (list the page again). An edit's result names the numbers that are
gone in stale_paragraphs. The inline editor's switch-paragraph click holds
the same key instead of an index. Without a listing a number is a position,
as before. An identity that survives save and reopen is still #2508.
page_render's hide_text_block used the paragraph number as a position in the page's current
paragraphs, so after an edit regrouped the page it hid a different paragraph from the one
text_edit would rewrite. It now resolves the number against the text_paragraphs listing and is
refused when the paragraph is gone.

text_edit also accepted any overlapping paragraph that contained the requested text as the
edited one. When the edit merged the paragraph into a neighbour, the merged paragraph contains
that text too, so the number followed it and a second edit by that number dropped the
neighbour's text. The edited paragraph must now show exactly the requested text; a merged one
leaves the number stale.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Marks behind the page content were all inserted at the start of
/Contents, so a second background went under the first and the old
colour stayed. Backgrounds now come first, then behind-content
watermarks, each in the order added. A watermark added after a
background no longer disappears under it.
Choosing Label under Page Sizing & Handling swaps the paper to the
4 x 6 in label stock. Leaving Label for Size, Poster, Multiple or
Booklet now gives back the paper chosen before, unless another paper
was picked while in Label.

Fixes #3564.

Co-Authored-By: Claude <noreply@anthropic.com>
On a page with no content of its own, an on-top watermark is the first
stream in /Contents, so it was counted with the behind-content marks and
a watermark added behind later was drawn over it. Nothing records
whether a watermark is behind, so a new behind mark now goes right after
the last background, which is always below any on-top watermark. A newer
behind watermark goes under older ones again, as before #3591.
Two of the five paths in the macOS Devanagari list do not exist —
`Supplemental/DevanagariSangamMN.ttc`, where Apple spells the family
`Devanagari Sangam MN.ttc` with spaces as this file already does for its
Telugu and Tamil siblings, and `Supplemental/Kohinoor.ttc`, one level above
where Kohinoor.ttc actually lives — so the family documented as second choice
has never been reachable. Both arrived together in #1033 and are present from
v0.7.1 through main; no CI could notice, since ci.yml has no macOS runner and
read() logs only on a hit.

List the families best-first, correctly spelled, and look each up in both
macOS system font folders. Preference is unchanged: all of Devanagari MT's
folders are tried before Sangam MN starts, Arial Unicode stays last, and at
most one installed face per script is still loaded (§1.4).

Devanagari MT wins on the machine tested, so Hindi renders identically here;
the change is that a Mac without it lands on the intended fallback instead of
skipping past it. That missing-Mac case is read off the code path, not
observed — no screenshot or log claims otherwise.

Tests: an exact assertion of the whole list (spellings, folders, order) that
runs on every OS and also pins devanagari_candidates() to it; a loader test
proving the first readable candidate wins, using two bundled faces of
different byte length; a spaced-name resolution test against a temp
macOS-shaped tree that panics rather than passing silently when the temp dir
is unusable; and a macOS-only disk oracle requiring every family folder that
exists here to be reachable, the list to cover Devanagari, Arial Unicode to
load under MAX_BYTES, and devanagari_fallback() to land on the same bytes and
face as this list. Families Apple stops shipping are reported, not failed —
that absence is what the list exists for.

Verified on macOS 26.6 (Apple M5 Max): fmt --all clean; clippy --all-targets
-D warnings quiet; system_fonts 12 passed; whole crate 1340 passed / 0 failed;
xtask layers (37 crates) and assets (272/6/22). Eight of ten deliberate
breakages turn a test red; MAX_FACES depth and the PDFCRAFT_SYSTEM_FONTS=0
guarantee do not, both disclosed rather than left implicit.
`a_script_fallback_is_the_first_candidate_with_the_probe` calls
script_fallback, which returns nothing when PDFCRAFT_SYSTEM_FONTS=0 — the
switch cargo xtask screenshots sets (AGENTS.md §1.4) — so it failed in that
mode on main, independently of the Devanagari fix. Skip both loader tests with
a printed reason instead, matching crates/ui-egui/tests/fonts.rs.

Split out from the previous commit so it can be dropped without touching the fix:
PDFCRAFT_SYSTEM_FONTS=0 cargo test -p pdfcraft-ui-egui --lib system_fonts now
reports 12 passed rather than one failure.
es.tsv was missing 143 of the 2,674 (context, English) keys the other
bundled catalogs carry, so those strings fell back to English inside an
otherwise Spanish interface: every calendar month and weekday, the whole
certificate context of the signature inspector, the X.509 key usages, the
signature-revision and digital-ID lines, password validation, eight plural
messages, the combine-from-folder strings, the rendering and stale-file
notices and the tooltips that spell their shortcut out.

Spanish now covers the whole union, as lt and vi do: 2,531 -> 2,674 of
2,674, measured as docs/localization-parity.md describes. Counts there and
in ROADMAP.md re-measured against the current union; vi is restated at
2,674 for the same reason, still 100%.

Translations reuse the vocabulary already in the file (ID digital, Acercar
/ alejar, Portapapeles, A doble cara, Cortar y apilar) and were written
from the English labels, not from another implementation.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
CodeRabbit caught that the macOS oracle asserted unconditionally that
/Library/Fonts/Arial Unicode.ttf resolves and loads under MAX_BYTES. On a Mac
without that font the assertion fails while Devanagari coverage is perfectly
fine — the loader treats candidates as optional, and so should this test.

Hardened beyond an is_file() guard: is_file() follows symlinks, so a link
whose target Apple has moved would read as "not installed" and skip — the very
dead-path defect this branch exists to catch. Gate on symlink_metadata so a
present-but-unresolved entry fails loudly, keep the resolution and MAX_BYTES
assertions where the face exists, report otherwise. The path is taken from the
list instead of repeated here, so it cannot drift from what ships.

Also pin three load-bearing constants in the CI-visible list test, because each
could otherwise be retuned silently (verified: all three were green before):
MAX_BYTES 32 MiB — the cap that decides whether a 23 MB Unicode face is
accepted, raised or lowered both now fail; DEVANAGARI_PROBE U+0915 — what makes
a Hindi check a Hindi check, since Arial covers both scripts and was masking a
probe swap; MAX_FACES 16 — the depth into .ttc collections the loader trusts.

Verified on macOS 26.6 (Apple M5 Max): cargo fmt --all clean; clippy
--all-targets -D warnings quiet; system_fonts 12 passed in normal mode and under
PDFCRAFT_SYSTEM_FONTS=0; whole crate 1340 passed / 0 failed. Mutations: cap
lowered (2 tests red), cap raised to 512 MiB, probe swapped to Arabic alef and
MAX_FACES cut to 1 all red via the pins. One limit stays open, stated rather
than glossed: nothing verifies 16 faces is deep enough for real collections —
that needs a synthetic .ttc with the covering glyph past index 16, and here every
needed face sits at index 0.
`cargo xtask assets` pins a SHA-256 for every catalog, so editing `es.tsv` left its
ATTRIBUTION.toml entry stale and `assets::tests::the_real_manifest_passes` failed on both
runners. Re-verified the file's provenance, updated the hash, recorded that the coverage
additions are AI-assisted and not separately native-reviewed -- as #2510 did for `lt` and
`vi` -- and regenerated ATTRIBUTION.md.

Review notes on docs/localization-parity.md:

- Lithuanian's `2,585 entries` was measured on 2026-10-13 and read as if `lt` were behind.
  Re-measured today it covers the whole 2,674-key union, so it is restated like `es` and `vi`.
- *How this was measured* now says outright that the union is a moving target: every change
  that adds a string widens it, a count only compares with others of the same date, and a
  catalog at 100% falls behind without losing a translation. Earlier revisions quote the
  union of their own date, not this one.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
annot::page_refs, the forms page_refs and organize's walk queued every
/Kids entry before the seen check, and organize cloned the inherited
attributes, Resources included, for each queued entry. A page tree whose
/Kids lists the same child many times, or whose nodes share one /Kids
array, held all of those entries at once before they were dropped as
already seen.

Each walk is now a small iterator with one cursor per open /Kids array,
the way pdfcraft_model::pages has walked since #1926. organize carries
inherited attributes as the nodes that set them and copies them only for
the pages it returns. Page order, the pages found, the inherited values
and the million-node cap are unchanged.
Review on #3239: a name that only numbers the glyph (C39, c251) says
nothing about the character. Reading it as the code's StandardEncoding
character invents text wherever the font's /Encoding puts the glyph
elsewhere: with /C1 at 233 it extracted O-slash for a glyph drawn as
e-acute. numbered_glyph_name and the fallback are removed; the AGL
specification's full mapping stays.

The test puts one glyph per line: the test font has none of these glyphs,
so they have no width, and on one line a later 'fi' sat on the first and
was dropped as its fake-bold copy once the guessed glyphs between them
were gone.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01R3niW7kVeT8mi7TpWc8eAJ
Follow-up to review of this branch, no production change.

The oracle now asserts the list still ends at Arial Unicode before checking it. Without
that, deriving the path from list.first() instead of last() stayed green on this machine and
printed "not installed" — the check disappeared because /System/Library/Fonts/DevanagariMT.ttc
is absent here. Deriving the path from the list is still better than repeating a literal, but
only if something verifies it points where the test thinks.

The three retune tripwires (MAX_BYTES, DEVANAGARI_PROBE, MAX_FACES) move into their own test,
so the layout test stays about Apple's directory layout and these are labelled for what they
are: constants nothing else pins, each confirmed to be the only guard by deleting it and seeing
the matching regression go green again. Comments corrected accordingly, including not claiming
Apple moved a target when a user could equally have removed it.

Verified on macOS 26.6 (Apple M5 Max): fmt --all clean; clippy -D warnings all-targets quiet;
system_fonts 13 passed with and without PDFCRAFT_SYSTEM_FONTS=0; whole crate 1341 passed /
0 failed / 5 ignored; xtask layers (37 crates) and assets (272/6/22). Mutations re-run at this
commit: folder-major expansion, folder-order swap, typo restore, loader reversal, cap raised to
512 MiB, probe swapped to Arabic alef and MAX_FACES cut to 1 are all red; deleting the
PDFCRAFT_SYSTEM_FONTS=0 early return leaves all 1341 green, which is stated rather than hidden.
The root's MediaBox and the page's own both have four numbers, so the
length check passed even if the page lost its own value. Compare the
values instead.
A page coloured with Edit a PDF > Background exported to Word as a
white page: export pages carried text and images only, and the Word
section has no page colour.

- pdfcraft-edit `page_background`: the colour of the first thing a
  page paints, when that is an opaque fill (no soft mask, Normal blend)
  in a plain gray, RGB or CMYK colour, of a path with a rectangle
  covering the visible page, not clipped smaller; `None` otherwise.
- `export::Page::background` carries it; the Word writer sets it as
  the document background (`w:background`, plus the
  `w:displayBackgroundShape` setting Word needs to show it) when every
  page shares one non-white colour. Word has one background per
  document, so differing colours are left out.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01RUNQSPVXx3dDBabRZHAnjz
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Gv2WwdgjmBz33GWkwgNn3Q
Review follow-ups on #2994: under even-odd any second rectangle, under nonzero
one wound the other way, can leave a hole the Word page colour would cover.
The parity note says the page colour hasn't been opened in Word yet.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Gv2WwdgjmBz33GWkwgNn3Q
Review on #2994: fill rectangles and the clip were kept as their page-space
bounding boxes, so a rectangle turned or sheared by the CTM could pass as
covering the page while leaving its corners out, and a following fill became
Word's page colour. Each rectangle is now kept as its four page-space
corners, the clip as the rectangles it intersects, and the crop box counts as
covered when its corners lie inside each.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Gv2WwdgjmBz33GWkwgNn3Q
Round-6 review caught that this branch rejected is_file() for one path and kept using it
forty lines above. The family loop filtered candidates with is_file(), which follows symlinks,
so an entry that exists but no longer resolves was classified "not installed" and skipped —
exactly the defect this branch fixes elsewhere.

Now both blocks share one rule: presence via symlink_metadata, then a hard requirement that
the entry resolves. Absence is still tolerated and reported; a broken entry fails loudly with
the family named, because that is the failure we are trying to catch rather than a machine
difference. Checked against this Mac first: no family or last-resort path is present-but-dangling
here, so the stricter rule cannot produce a spurious red on current macOS.

Also corrected two stale comments — the tripwire test is separate from the layout test *below*
it, and the oracle's doc comment now states the exception the PR description already does: a Mac
where nothing in the list covers Devanagari fails, since such a machine cannot validate anything.

Verified solo on macOS 26.6 (Apple M5 Max), no concurrent writers this time: fmt --all clean;
clippy -D warnings all-targets quiet; system_fonts 13 passed with and without
PDFCRAFT_SYSTEM_FONTS=0; whole crate 1341 passed / 0 failed / 5 ignored; xtask layers (37 crates)
and assets (272/6/22). Mutation evidence for the change, via font folders redirected to /tmp so
system paths were untouched: with a dangling DevanagariMT.ttc in the system folder the current
code fails at "exists but does not resolve", while reverting to is_file() filtering makes the
identical defect pass silently. Re-ran the pinned regressions too: cap raised to 512 MiB,
MAX_FACES cut to 1, probe swapped to Arabic alef, last_resort derived from first(), misspelling
restored — all red.
A content stream that uses l, c, v, y or h before any m or re has no
subpath to extend. The edit-side image scan (crates/edit/src/images.rs)
handed the operators straight to BezPath's line_to/curve_to/close_path,
which panic on an empty path by contract, so object_list, page_images,
image_hit and the Office export's image walk all crashed on such a page,
while the renderer, text extraction, MuPDF and pdftotext skip the
malformed segment and render the rest.

The scan now skips the segment when the path has no elements, mirroring
the v arm's existing guard and kurbo's own condition (debug_assert on
!is_empty). A path started with m or re extends and closes exactly as
before. A clip set by a stray segment (W n on an empty path) takes a
zero bounding box, which kurbo defines as Rect::default, not a panic.

Regression test: each of the five segment operators before any m, then
a well-formed m l h and a re W n clip, all before an image Do - the
image is still listed in every case.

Verified against kurbo 0.13.1's source (line_to/curve_to/close_path
debug_assert on empty; bounding_box unwrap_or_default) and the issue's
repro; no Rust toolchain on this machine, so cargo test was not run
here - saying so plainly for the reviewer.

Co-authored-by: rahul05ranjan <11793455+rahul05ranjan@users.noreply.github.com>
Found while testing crates/forms/src/af.rs:166 (Parser::string)

Co-Authored-By: Claude Haiku 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
A number written with a doubled sign was read by the renderer's lexer as
two numbers. `--280` set the sign, found no digit before the second
`-`, and returned 0, leaving `-280` to be read next: a `cm` carrying it
got seven operands, kept the last six, and the matrix `0 0 1 0 -280 10`
hid everything drawn under it. The page rendered blank while text
extraction still reported the text, because the glyphs were there and
only the matrix was wrong.

Measured on the report's own file, at 72 dpi, counting pixels darker
than mid-grey:

    double_minus.pdf  before: 0 dark px (blank)   after: 581, leftmost at x=121
    control.pdf (-280)        581, leftmost at x=121 -- unchanged

so the two files now render identically, which is what the report asks
for and what MuPDF does.

One extra sign is absorbed and the value is negative if either sign was
a minus, matching the COS lexer since #3299 (`--280` and `+-280` are
both -280). `Number::skip` already consumed the whole token, so read and
skip had disagreed about where the number ended.

Fixes #3565. #3299 fixed the same input in the COS lexer only.
A pre-publish review caught both comment errors, and Apple's own documentation settles the
rest of it:

- installed_faces_switched_off said "both loader tests". It now guards three sites — two
  loader tests plus the macOS oracle — ever since 21dc64e added the third call without
  touching this sentence.
- MACOS_FONT_DIRS claimed the Telugu, Sinhala and Tamil lists "still name one folder
  each". They do not: Telugu names /System/Library/Fonts, Supplemental/ and /Library/Fonts
  (four paths), Tamil three, Sinhala two. The accurate statement is that no family in
  those lists is looked up in both system folders, which is the property Devanagari now
  has and they do not. Their first choices all exist on macOS 26.6, verified again here.
- The oracle's doc comment now records the authority for treating listed faces as optional:
  Apple's "Fonts included with macOS Tahoe" page (support.apple.com/122869) is titled
  "Installed or downloadable fonts" and states that most install automatically while others
  download via Font Book — without marking which entries are which. Absence of a face is
  therefore expected on some Macs, which is exactly what the reported-not-failed behaviour
  accommodates.

Comment-only change: fmt --all clean, clippy -D warnings quiet, system_fonts 13 passed with
and without PDFCRAFT_SYSTEM_FONTS=0.
The control channel's accepting thread dropped failed accepts with
incoming().flatten() and asked again at once. When accepting keeps
failing, for example while the process has no file descriptors left,
that loop keeps a core busy until it recovers.

Wait 100 ms after a failed accept, as the single-instance listener
already does. The loop goes through a small helper so the wait can be
tested without running out of file descriptors.
Review on #3070: the gap between pages isn't one colour on every platform;
on ubuntu CI the pixel at x = 416 was one level off the fixed sample taken
40 px from the page's right edge. The test now takes a screenshot of the
same pages before Edit text is on (no paragraph outline), checks that the
pages stay where they were, and compares every gap pixel with the same
pixel there. Removing either clip still fails it.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01R3niW7kVeT8mi7TpWc8eAJ
The JHOVE PDF-HUL-157 files (tiff2pdf output) end their first trailer with
`/Prev 297298 0 R`: the offset written as a reference. The chain reader took
/Prev as an integer only, so the chain stopped after the first section without
a word, and every object of the earlier sections (here pages 2-4 and their
images) read as null. The document showed one page where Poppler and MuPDF
show four, and a full save wrote the missing kids as null, so pages 2-4 were
unreadable afterwards ("cycle in page tree", 1x1 renders).

A reference-valued /Prev is now read as the offset it names, with a repair-log
note; any other non-offset value is noted and ends the chain as before.

Regression test: document::tests::a_prev_written_as_a_reference_still_reaches_the_earlier_sections.
Verified on JA3-1998-PDF.pdf: four pages read, and after a Subject edit and a
full save Poppler renders all four pages without a warning (before: "Kid
object (page 2) is not an indirect reference (null)").

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_011K1peyrh5MdZ6ttHkVq8Gh
The cos reader learnt this in the previous commit, but the viewer and
the save's reopen check count pages with hayro, which still stopped at
the reference-valued /Prev: a four-page file showed one page, and the
correctly written full save was refused as a page-count change.
iText's indirectPrev.pdf writes /Prev 6 0 R, and object 6, listed in the
same update, holds 470, the offset of the first section. Both readers took
the object number as the offset, so cos found no section at 6, rebuilt the
file from object headers and lost its revision list, and hayro's chain
stopped there.

When /Prev is a reference, the sections read so far (cos) or the section's
own entries (hayro, which now reads them before following /Prev) are asked
for that object first. If it holds a non-negative integer, that is the
offset; otherwise the object number still is, as the tiff2pdf files need.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01NH3itd85jPuM6wNjgcGfEH
The upstream merge brought in this new tl! literal; the parallel
session's force-push had reverted the earlier translation. The
Vietnamese catalog already carries it.
Fixes #3573

Co-Authored-By: Claude Haiku 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
UniJIS-UCS2-HW-H maps code 0x0020 to CID 231 / U+2002. The OpenType
substitute for a non-embedded Japanese CID font lacks that glyph, so
.notdef boxes were drawn. Type0Font::map_via_unicode now falls back to
the substitute's ordinary-space glyph for explicit spacing characters
its charmap lacks. Native and variation-selector glyphs keep priority;
PDF advances and Unicode text are unchanged.

Fixes #3601.

Co-Authored-By: Claude <noreply@anthropic.com>
`read_inner` now reads `-+280` and `++280` as well as `--280`, but
`Number::skip` consumed one sign and then required a digit, a dot or a
further `-`. So a skip failed where a read succeeded, and `ReaderExt::skip`
puts the reader back where it started -- `Object::skip` would leave the
number in place rather than step over it. `--280` was already covered,
by the `b'-'` arm.

One more sign is consumed, and the six sign forms are asserted skippable
to the end of the token. Without this the test says `-+280: not skipped`.

A third sign is still read and skipped differently (`---280` reads as 0
and skips whole), which was already so before this branch and is left
alone.

Found in review of #3630.
Fixes #3579

Co-Authored-By: Claude Haiku 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Fixes #3578

Co-Authored-By: Claude Haiku 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Redaction removed annotations and form fields under a mark from the pages
but left the structure tree's /OBJR references to them, so a full rewrite of
a tagged PDF kept the comment's /Contents and the field's /V. apply now
drops references to every annotation no page lists afterwards (widgets of a
removed field on other pages included), as Sanitize does since #2532, and
fails closed with RedactError::Tags when an element can't be edited.
A glyph narrower than a twentieth of its height was matched as a fake-bold
redraw by a fifth of its height, so in OCR text layers squeezed to 20 Tz or
less the second l of "Hallo", and at 10 and 5 Tz the tt, ss, mm and ff and
letters repeated two or three places on, were dropped. Every glyph with a
width is now matched within half of it; only zero-width marks keep the
height-based distance.
Any value in a PDF may be written as an indirect reference, the tagged
flag included: `/MarkInfo << /Marked 9 0 R >>` with object 9 holding
`true` is valid, and veraPDF's corpus has pass files that write it that
way. Both readers of the flag tested the reference itself, so such a
document was reported as untagged:

    doc_info                     "tagged": false
    Accessibility Checker        "The document isn't marked as tagged
                                  (MarkInfo Marked)"

Both now resolve the value first. On a pair of otherwise identical
tagged pages, one with `/Marked true` and one with `/Marked 8 0 R`:

    before   direct: tagged true    indirect: tagged false
    after    direct: tagged true    indirect: tagged true

and the checker reports the finding for neither.

A document with no flag at all still fails the rule, which the test
asserts alongside, so this does not turn the rule into one that always
passes.

Fixes #3671.
The page's Escape handler runs in logic(), before the UI is drawn, and
ignored open popups, so one Escape closed the main menu and also cleared
the text selection. It now leaves Escape to an open popup, as
combine_ui.rs already does for its keys; the next Escape clears the
selection as before.
A PEM file whose last block never ends is now an error instead of a
shorter certificate list (CodeRabbit on #3561). The test's doc comment
for the bundle builder is back above its function.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
An outline whose items name shared children (/First and /Next pointing at
nodes that other items also name) was counted by following every path. With
60 levels of two items each, a walk that follows every path takes 2^60
steps. The outline count never finished, and Remove Hidden Information and
Sanitize both run that count first.

The count now keeps the set of items it has reached. An item reached a
second time is not counted again, and the level stops there: the walk that
reached the item first goes on to count the rest of that level. A total
budget of 100,000 distinct items bounds the work for an outline with very
many items. The per-level cap is unchanged.

Ordinary outlines count as before. The existing hidden-information test
(two bookmarks) passes unchanged, and a new nested outline test (five
items) pins the count for nesting and siblings.

Tests added in crates/redact/src/tests.rs:
- an_outline_whose_items_share_children_is_counted_without_spinning
- removing_bookmarks_from_an_outline_whose_items_share_children_finishes
- an_outline_counts_no_more_items_than_its_total_budget
- a_nested_outline_is_counted_item_by_item

The first, second and third were seen to fail on the code before this
change: the first two timed out after 10 seconds, and the third read 120000
where 100000 is expected. The fourth passes before and after the change and
guards the ordinary count. The spin tests run the walk on a thread with a
10 second bound, so a regression fails the test instead of hanging the run.
Reverting only the shared-item check, or only the budget check, fails the
tests they pin.

Checked with cargo fmt --all -- --check, clippy with -D warnings on
pdfcraft-redact, and cargo test for pdfcraft-redact.
Word and other writers split one font into several subsets, each a font
resource of its own with its own six-letter tag: "Two-line" can be drawn as
/F1 (Two) /F2 (-l) /F1 (ine) with F1 and F2 both Helvetica-Bold. `text_lines`
ended a line wherever the resource name changed, so Edit text showed three
lines and Word/HTML export wrote three paragraphs -- out of order when the
subsets' boxes differ by a fraction of a point.

A run now continues a line when its `/BaseFont` names the same font as the
line's, ignoring the subset tag (ISO 32000-2 §9.6.4), as well as when it is
the same resource or another subset of our Japanese/Hebrew fallback face.
Block grouping in `text_regions` uses the same comparison, so the exporters
see one block.

Tests: the issue's two-resource case and Word's two-subset case read as one
line and one block; two different fonts still split. Before the fix the first
gave ["Hel", "lo"] and the second ["ab", "ca", "b"].
The listings kept for paragraph numbers are capped at 256 pages, and the 257th cleared them all.
A page that lost its listing fell back to reading a number as a position in the page's current
paragraphs, so after an edit regrouped it a number could rewrite another paragraph.

Documents that lose a listing to the cap are remembered, and a page of one with no listing is
refused with a message to list it again. doc_close drops a document's listings and that mark.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
FredApps's note on #3327: five points that are all on the bounding box's
corners still need not trace the rectangle. Three sides and a step back
along the third --

    10 10 m 30 10 l 30 30 l 10 30 l 30 30 l S

-- visits every corner, so it was taken as the box and stroked with a
left side the path does not have.

path_as_rect now asks what a rectangle actually is: four distinct
points, each step from one to the next along a single axis, and the last
point back at the first (written as the fourth LineTo or as ClosePath).
That is a rectangle and nothing else is, so the corner test and its
off_corner flag are gone -- four points of an axis-aligned traversal are
necessarily the corners of their own bounding box. A degenerate path,
whose points are not distinct, now takes the general route as well,
where it painted a zero-width or zero-height rectangle before.

The vendored-patch table has the row this change was missing: without
it a re-vendor of hayro-interpret would drop the patch silently, which
is the other half of FredApps's review.
A .p7b must be SignedData with version, digestAlgorithms,
encapContentInfo, optional certificates and CRLs, then signerInfos
(RFC 5652 §5.1), and nothing may follow it (CodeRabbit on #3561).

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Document::resolve follows one reference, and an indirect object's value
may itself be a reference (ISO 32000-2 7.3.10): `/Marked 8 0 R` with
object 8 holding `9 0 R` holding `true` is a tagged document. The rule
was left holding a reference and reported it untagged, while the render
inspector -- which follows chains through lopdf's dereference -- read
the same file as tagged. The point of this branch is that the two
readers agree, so the chain is followed here too.

Bounded at 32 references and cycle-safe: a damaged file can point a
reference at itself, and the test covers that case as well as the
two-hop chain. Without the fix the chain case reports

    left: Failed, right: Passed

Reported by CodeRabbit on #3686.
The bundle's encapContentInfo must start with its eContentType, and the
optional [0] certificates and [1] CRLs appear at most once each, in that
order (RFC 5652 §5.1; CodeRabbit on #3561).

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Review follow-up. `Metrics::from_dict` leaves `base_font` empty when a font
has no `/BaseFont`, and a bare subset tag such as `/ABCDEF+` left an empty
name after stripping. Two consequences:

- `same_style` split one paragraph into separate reading blocks for the
  exporters when both names were empty, though they grouped before subset
  joining;
- `/ABCDEF+` and `/GHIJKL+` compared equal after stripping, so unrelated
  malformed fonts could join into one line.

`same_base_font` now strips both tags first and requires a non-empty, equal
name, so a missing name or a bare tag matches nothing. `same_style` keeps
grouping lines whose fonts both lack a name, since the resource or fallback
face `same_font` already matched is their whole identity.

Tests: a page in a font without `/BaseFont` still reads as one block (it
split into two before this change), and a bare tag matches no other name.
A Keychain identity came back with an empty `chain`, so a signature made with
it carried only the signer's certificate. A validator that doesn't already
hold the intermediates (Acrobat, or PdfCraft on another machine) then can't
build the path to the root. The Windows certificate store identities already
carry the chain Windows builds.

`keychain::find` now asks the Security framework for the chain (SecTrust with
the basic X.509 policy, no network fetch) and keeps every certificate but the
signer's. It is best effort: a certificate macOS can't chain still signs.

`chain-legacy.p12` is `chain.p12` re-exported in the legacy format that
`security import` accepts, for a new ignored Keychain test.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Found while testing crates/forms/src/af.rs:638-642 and 750-755 (parse_date)

Co-Authored-By: Claude Haiku 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Found while testing crates/fonts/src/std14.rs:84

Co-Authored-By: Claude Haiku 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Found while testing crates/js/src/formcalc.rs:2024-2050 (word_num)

Co-Authored-By: Claude Haiku 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Found while testing crates/xfdf/src/lib.rs:313-317 (xfdf_annots) and :~1010 (annot_from_xml, `unwrap_or("print")`)

Co-Authored-By: Claude Haiku 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
PageText::nearest measured the distance to each glyph's axis-aligned bounding
box. In sheared or turned text that box is wider than the glyph and overlaps its
neighbours, so a pointer on a letter was also inside the box of the letter before
it, at distance 0, and the earlier glyph won. A drag ending on the last letter of
a word or line then ended one or two letters short.

A glyph whose drawn em box (quad) holds the point now wins, the nearest centre if
several do. With no quad hit the bounding-box distance still decides, so pointers
past a line end or between lines behave as before; a tie there goes to the nearer
centre rather than the first glyph.

Tests: pdfcraft-render lib 312 -> 314. The sheared and 45-degree test fails on
the old hit-test (glyph "i" resolves to the glyph before it) and passes now.
pdfcraft-ui-egui text_select: 14 pass.
Treat predefined CJK CMaps and Adobe CID collections as alternative mapping routes. Resolve indirect metadata and reject unknown names and foreign registries without changing ToUnicode handling. Add synthetic checker and read-only automation regressions with red/green evidence.

Co-Authored-By: Claude Code <noreply@anthropic.com>
A Redact annotation with neither a usable QuadPoints array nor a usable
Rect was dropped silently by marks(). Applying the readable marks beside
it then reported success, and the text under the dropped mark stayed in
the file.

apply() now counts such marks on the pages being applied and returns
RedactError::UnreadableMark(n) before anything changes. The message goes
out through the existing error text: the engine edit error, the
automation reply and the UI failure line all print it. Marks that were
readable through the existing fallbacks (a Rect alone, a usable
QuadPoints array) work as before.

Tests: three in crates/redact/src/tests.rs (refusal with the count and
no change, Rect-only still redacts, an unreadable mark on another page
does not block the selected page) and one end to end in
crates/automation/tests/automation.rs (the reply carries the reason and
the page keeps its text). On upstream main with only the variant added,
the two refusal tests and the automation test fail; the Rect test passes.
Reverting each pinned production line turns the tests red again.
Gates: cargo fmt --check, clippy -D warnings for pdfcraft-redact and
pdfcraft-automation, and the redact test suite pass.
The apply section of the redact README described the fail-closed rules but not the new refusal of a Redact annotation with neither a usable QuadPoints array nor a usable Rect. Documented here with the error name and the page scope of the count. No behaviour change in this commit.
The total budget of the outline count (100,000 items) was checked against the
number of indirect items the walk had reached. Items written inline, as a
direct dictionary, were counted in the result but never entered that set, so
an outline of 60,000 indirect siblings, each with one inline child, counted
120,000 items.

The walk now keeps a separate running total of every item it counts, inline
items included, and stops when that total reaches the budget. The set of
indirect items is still used to count shared items once. Ordinary outlines
count as before.

The doc comment of the walk now states the depth limit in one sentence: an
item first reached through a deeper path is not visited again from a shallower
one, so items nested past depth 64 may stay uncounted. The depth behaviour is
unchanged.

Test added in crates/redact/src/tests.rs:
inline_outline_children_count_towards_the_total_budget. It reads 120000 on the
previous head and 100000 now. Reverting the budget check to the indirect-only
count fails it; reverting the increment fails it and the existing total budget
test. The other outline tests pass unchanged.

Checked with cargo fmt --all -- --check, clippy with -D warnings on
pdfcraft-redact, and cargo test for pdfcraft-redact (85 passed, 0 failed).
Fixes #3570

Co-Authored-By: Claude Haiku 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
is_nearly_equal allows a 256th of a path unit, and stroke_path_impl
applies the matrix after path_as_rect, so a cm of 10,000 turns that
tolerance into tens of device units: a path that only nearly closes was
painted as the rectangle, with a side it does not have, and an edge off
true by that much counted as axis-aligned.

The fast path now compares coordinates exactly. Nothing that is a
rectangle loses it: a rectangle's coordinates are equal in the content
stream, not approximately equal. A path that misses takes the general
route and is painted as written, which is the safe direction.

I could not make a rendered case that distinguishes the two: this
renderer paints a 10,000x cm as a solid page whatever the line width, so
there are no pixels to compare at the scale where the tolerance shows.
The change rests on the argument above, and the three existing tests
still pin the behaviour that is measurable.

Reported by CodeRabbit on #3327.
Each of the eleven right-hand panels (Comments, Bookmarks, Pages, Fields,
Layers, Attachments, Signatures, Accessibility, Tags, Search, Compare) opens
on one synthetic document, and its AccessKit tree is compared with a text
file in crates/ui-egui/tests/snapshots/. Text rather than pixels: the same
on every platform and GPU, not an asset under AGENTS.md §1, and readable in
a diff. Positions are left out, so a moved widget is not a change but an
added, removed or renamed one is. UPDATE_SNAPSHOTS=1 rewrites the files.

An exhaustive match over RightPanel stops the file compiling when a panel
is added without a snapshot.

Seen failing: renaming the Signatures panel's "Validate all" button fails
signatures_panel alone, showing both trees. Stable under TZ=UTC,
America/Los_Angeles and Asia/Tokyo.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Review (CodeRabbit): every_panel_has_a_snapshot only checked that each
panel's file existed, so a new panel with a file but no test would pass.
Now one function opens each RightPanel with no wildcard in its match, and
every_panel_matches_its_snapshot compares all eleven, collecting every
mismatch before failing. Snapshots unchanged.

Seen failing: renaming "Validate all" reports "1 of 11 panels differ" with
both trees for the Signatures panel.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Shortcut labels follow the OS egui reports ("Ctrl+B" against "⌘B"), so
the snapshots were only host-independent because kittest happens to
default to Nix. Pin it explicitly. With the OS set to Mac the test fails
on the Bookmarks panel's "New bookmark (⌘B)" button.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Read all six boolean ViewerPreferences through the document resolver. Synthetic regressions cover direct/indirect dictionaries, true/false, malformed values and save/reopen.
Add the required progress-log entry without changing readiness estimates.
Update the M4 milestone note and revision history; preserve existing gaps and estimates.
Keep the initial-view feature status and missing-feature notes unchanged. Add the named synthetic regression and supported indirect-boolean behavior.
From @mtlcl410's independent run of this PR:

- a second subset of the same font can be re-encoded with `/Differences`, so
  "-l" has different codes from the first subset's; the line still joins,
  reads in coding order, and editing it replaces every run, including the
  re-encoded subset's;
- `ABCDE+Helvetica-Bold` has a five-letter prefix, not the six-letter subset
  tag of ISO 32000-2 §9.6.4, so it stays a different font from
  `Helvetica-Bold` and the runs stay apart.

The first case (and its edit) fails before the subset fix; the second guards
the tag-length rule against stripping any `+` prefix.
Once drawn, a line or arrow could be moved but not changed: selected, it showed a frame and
no handles, so a wrong length or angle meant drawing it again. A selected line, connected
lines, polygon or cloud now has a handle on each end or corner instead of the frame; dragging
one moves that point (Shift holds it to 45° from the point before), the new shape is drawn
while dragging, and the release is one undo step. The drawing tools keep drawing from an end
(a new line starts there); the Select and Fill & Sign tools pick the handles up.

pdfcraft_annot::set_points sets /L or /Vertices, lets /Rect follow with room for the width
and arrowheads (the rule a new comment is drawn with) and draws the appearance anew;
measurements keep the points they measured, locked comments their shape. Edit::ReshapeAnnotation
("Reshape comment"), comment_edit `points`; the comment list (Summary, DocInfo, comment_list)
carries a line's ends and a polygon's corners.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The eraser always rubbed 6 pixels either side and drew a 12-pixel
preview, whatever its thickness setting. Both now follow the setting,
at three pixels per point, so the default 2 pt keeps today's 12-pixel
eraser and the slider's 0.5-12 pt gives 3 to 72 pixels across.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
From review of #3749: the user-space radius had a 1-unit floor, so at a
high zoom the eraser rubbed past its preview; and zoom was read from the
screen rectangle's width, which is the page's height when the view is
turned a quarter. PageXform::zoom reads the side the page's width is on;
the eraser and the drawing preview use it, and the floor is gone. A page
with no size on screen erases nothing.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
From review of #3749: the hit test alone would pass with a fixed
12-pixel preview. Read the preview stroke from the frame drawn mid-drag:
12 pixels at the default 2 pt, 72 at 12 pt, and gone once released.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Headers and footers were Helvetica only. HeaderFooter takes the same
standard-face Family as Add text: the content selects /PCHelv, /PCTimes
or /PCCour, the page gains that font, and centred and right-aligned text
is placed by the chosen face's standard-14 widths. doc_header_footer
takes font, and the dialog's fixed "Font: Helvetica" label becomes a
Helvetica / Times / Courier menu.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
From review of #3756: add_resources set /PCTimes (or /PCCour, /PCHelv)
in the page's fonts whatever was there, so a page that already drew
another font by that name would have its own text change face. Headers
and footers now pick the usual name when it is free or already names the
same standard face, otherwise the first free name_1, name_2…, and add
only that face, never replacing an entry. An update finds its own name
again.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Opening the Print dialog reset the printer to the system default every
time, while copies, range and paper carried over, so Save as PDF or a
non-default printer had to be picked again for each print.

The dialog now keeps the printer picked last time, Save as PDF included,
while it is still listed. A fresh list that no longer has it (the
printer was removed) falls back to the default.
Fields keep their <format><picture>. A date display picture turns the
ISO value into its pattern for scripts' formattedValue (JavaScript and
FormCalc), so date arithmetic on formattedValue works as forms expect.
Once a script sets the value, formattedValue gives that value. Number
and text display pictures still give the raw value.
A date the field's data picture writes as 20261018 reaches scripts as
2026-10-18, the rawValue another viewer gives them, so a script that
checks String(rawValue).length == 10 works (DA Form 31's 10b TO).
Doc comments on the functions this branch touches.
A checkmark, cross, dot or line placed on a page with /Rotate was drawn
in the page's own axes, so on a page turned 180 degrees the checkmark
came out upside down, and on a quarter turn it lay on its side.

Marks now record the page rotation (PCStampRotate, as rubber stamps do)
and their appearance is drawn upright in the displayed box and mapped
into /Rect, sharing the stamps' code for it. The mark's box is sized in
displayed axes (FillMark::rect_at), used by both the Fill & Sign tool and
fill_sign_add, so a line runs across the page as displayed.
date{DD MMM YYYY} and date{EEE …} have parts formatted_value can't show
yet, so it gives the raw value instead of text like 20 mmm 2026.
2026-1-2 isn't an ISO date and stands as it is. A test also pins that
parentheses hold a picture's locale (date(en_US){YYYYMMDD} formats) and
that a clause without braces has no pattern.
FredApps' approval on #2359 carried one non-blocking nit that is a real defect: the header told
contributors to "regenerate with make-oracle-fixtures.py in the workspace root" - a script that
exists only in my checkout, next to a venv (.dc-tools/) likewise outside the repository. A
contributor following it finds nothing, and this repository has no committed generator for these fixtures
(crates/oracle treats qpdf strictly as an external process, and CLAUDE.md keeps the product and build in
Rust).

The header now carries a self-contained recipe, measured on this host rather than asserted:
  * one blank 200x200 page, pikepdf 9.11 / qpdf core 12.2.0, Encryption(owner=OWNER,
    user=USER, allow=Permissions(extract=False), R=<revision>, aes=…, metadata=…),
    save(static_id=True), then read /Encrypt. At revisions 2-3 `aes=False, metadata=False` are
    FORCED - pikepdf refuses AES ("Cannot encrypt with AES when R < 4") and refuses encrypted
    metadata ("Cannot encrypt metadata when R < 4") - so leaving those flags to the reader makes
    the export-grade row unreproducible. My first draft of this comment made exactly that mistake.
  * Reproduction scope, by revision: each of the four deterministic rows quotes eight dictionary
    fields (32 per comparison leg). Revisions 2 and 3 reproduce as whole files - fresh writes today
    carry the same sha256 (63f2d95b…, 39afbdd5…) as the October originals these literals came
    from. Revision 4 reproduces all eight fields, but its file moves by exactly one ~32-byte
    stream because /CFM /AESV2 prepends a fresh random IV (ISO 32000-2 §7.6.4); the dictionary
    and /ID are unchanged. Revision 6 reproduces structurally only (/ID, R/V/Length/P and the
    metadata flag); its salts are new each run.

Running the generator at the reviewer's prompting exposed that it had not been executed since its
last edit: unless both aes=False and metadata=False are forced pikepdf refuses R<4, checking metadata first, so it wrote 4 of the 6 fixtures - revisions 2
and 3 missing, including this file's headline export-grade row - while printing a "skipped:" line
and exiting 0 regardless. It writes 6 of 6 now, and every quoted field reproduces as above.

The overstatements flagged during review are corrected in this file; they are quoted verbatim,
each with the basis for its new wording, in the PR description rather than here - commit messages
cannot be edited after a push, descriptions can. Two further fixes live in this message alone: an
ISO subsection written as §7.6.4.3, a form this repository does not cite (it uses bare §7.6.4 or the
deeper §7.6.4.x.y), and one clause duplicated mid-cycle.

What nothing here verifies, stated instead of implied - established by deliberately breaking the
table (4 mutations: 2 red, 2 green): authentication never reads /Perms (a corrupted one fails
only this file's own decrypt), and renaming a row's /StmF to Identity or replacing its
crypt-filter set leaves all three tests green, so the filter names are transcription held to
review. Pinning them, and re-deriving these rows through pdfcraft_oracle::qpdf() on the runner
that installs qpdf, belong in a follow-up. Also recorded honestly: the revision-6 fixtures these
rows were copied from are gone - my own regeneration overwrote them before I archived the folder -
so those two rows rest on internal consistency plus this record, not a surviving artifact. Revisions 2 and 3 carry their own proof, byte for byte (sha256 63f2d95b… and
39afbdd5…, October and today); revision 4 reproduces every quoted field, with only the one
IV-carrying stream moving.

Scope: comments plus one assertion message (a revision 3 /U description that wrongly claimed qpdf
leaves sixteen zero bytes; its condition, len == 64, is unchanged). No assertion logic, fixture,
dependency or product code touched. Parent remains the approved cdbce8ac, so this lands as a
fast-forward rather than a rewrite of the head FredApps reviewed. Gates at this tip
(upstream/main 22bedbf2; branch 326 behind, trial merge clean): cargo fmt --all -- --check rc=0;
clippy -p pdfcraft-crypt --all-targets -D warnings rc=0; cargo test -p pdfcraft-crypt
--no-fail-fast 28 passed / 0 failed (17 lib + the 8 from #2358 plus these 3); cargo xtask layers
OK, 37 crates; cargo xtask assets ok (272 repository, 6 bundled by dependencies, 22 fetched at
build time). CI had not run on this tip when this was written - the commit was local.

Co-authored-by: Qwen3.8-Flash-Next-oQ4e-WLSRefit-MTP <noreply@qwen.ai>
Co-authored-by: DeepSeek V4.1 Flash <noreply@deepseek.com>
From review of #3756: a /PCTimes naming Times-Roman with more to it (a
/ToUnicode map, /Widths) passed as the same face and was replaced, so
the page's own text could lose its map. A name is reused only when it
holds exactly the dictionary a header writes, and then it is left as it
is; anything else gets a free suffixed name.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Adds a Finnish (Suomi, `fi`) catalog with 2,592 entries: every key of
the other bundled catalogs, written in sentence case from the English
labels. Finnish uses the one/other plural rule with the partitive
singular after any number but 1 (1 sivu, 2 sivua, 0 sivua).

Finnish is also a date language: calendar headings use the nominative
month (Lokakuu 2026) and dates the partitive (14. lokakuuta 2026).
Any fi-* locale selects it.

Tests cover registration, locale tags, persistence, plurals, calendar
wording, commands/tools, scanned UI literals, every bundled catalog key
and the Latin UI fonts. The catalog has its ATTRIBUTION.toml entry
(ATTRIBUTION.md regenerated with `cargo xtask assets --write`).

Clean-room, AI-assisted (Claude Code) translation; not yet proofread
line by line by a native speaker.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
CodeRabbit on #3788: the handles and outline of a selected line, connected lines, polygon or
cloud followed a dragged end but not a drag of the whole comment, which kept its old handles
until the page was drawn again (the frame such comments had before showed the move). The
points now follow a body drag and the released move as well (changing_points, one rule for
reshaping and moving). Test: a_moved_or_reshaped_line_is_drawn_at_its_new_points.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
import_pages_with and insert_file_pages took the objects numbered above the
destination's highest number for the ones the import added. When no number
is left above the highest (an exhausted /Size, or object 4294967295),
Document::add fills free numbers below it: the copies skipped resource
sharing, and a refused import left its objects in the destination. With
object 4294967295 the `n + 1` overflowed, a panic in debug builds and in
release a wrap to 0 that counted every object as added.

Record the object numbers before the import and take the ones present
afterwards but not before.
HeaderFooter::shrink scales the original page content evenly into the
band the header and footer leave free, centred across the page, in
display space so rotated pages shrink the way they are shown. The scale
is a content stream first inside the q/Q wrapper, tagged as part of the
header and footer, so Remove and Update take it away again and
backgrounds behind the page stay full size. Margins that leave no room
are refused before anything is drawn.
The tool passes shrink through to the header and footer, with an end-to-end
test that reads the moved text back from a saved copy, refuses margins that
leave no room and checks that Remove brings the content back to full size.
The Header and Footer dialog (also opened by Bates numbering) gets a
checkbox that turns on the shrink, and its preview shows the band the
page content shrinks into. The label is translated in every catalog (the
catalog checksums in ATTRIBUTION.toml follow). The parity entry for the
shrink is shipped, with its limits: annotations and added text and images
are not moved yet.
main now keeps valid tables as a compact index; the gap filling reads entries through it and materializes them only when it repairs.
With junk before %PDF the table's offsets are relative to the header, so the "1 G obj" check is also tried there, as load does; otherwise object 1 stayed free and a full save dropped the image.
The XFDF export nested one <field> element per period-separated part of a field's full name. A /T can hold periods of its own, so a name can have far more parts than the field tree has levels. Past 64 levels the import refuses the file the export wrote, and around 100,000 parts the recursive write overflows the main thread's stack and aborts the process.

The export now nests at most as many levels as the import reads back. The parts past the last level stay in the deepest element's name, and the import joins them back into the same full name.
with_missing_appearances_cached checked each /Annots entry against the source document, which never gets the appearance, so an annotation listed K times added K appearance streams to the display copy. The engine writes that copy on open and after every edit, so the display bytes grew with the number of listings: 20,000 listings of one Ink comment in a 136 KB file came to 428 MB.

Each annotation reference is now handled once per call, across all pages.
- English (and Auto where offered) stays first as the source language and fallback; the rest sort by native name with accents folded, so Čeština is under C and Magyar under M; Cyrillic and CJK names follow the Latin ones
- The list used the registry order, which is the order the languages were added
Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Joining runs into one line compared their character and word spacing to
1e-6. Word documents exported through macOS Quartz write 0.0001 Tc on
some words, so a line like "for company AEROSOL" split into "for
company A" and "EROSOL", two paragraphs that edit separately.

Spacing below a thousandth of an em, the unit glyph widths are given in,
can't be seen. Compare character and word spacing to that; scale and
rise still have to match exactly, and visible spacing still keeps runs
apart.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The paragraph editor is an egui Area, and Areas fade in over the style's
animation time (0.2 s) each time they appear. While the editor faded, so
did its cover and its frame: the page's own copy of the paragraph showed
through it under the editor's text, and the frame pulsed from dim to
full. Every click into a paragraph did this.

Show the in-place editors at once: the paragraph editor, the editor for
text added in this session, the Fill & Sign text box and the form field
editor.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Applying a paragraph rewrites its page, but the page raster on screen
still has the old text until the page is rendered again. The textless
background and the editor went away on the frame the edit applied, so
for those frames the old text came back under the paragraph's new
outline, and then jumped to the new one.

The editor now remembers how it drew its text each frame. When the
edit that closes it is that paragraph's, the textless page and that
last look stay on screen until the page's new raster arrives, as
MoveBackground does for moved items. Any other edit, a zoom, or the
raster ends it.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
A dragged page image is drawn from the page raster, blanked where it was
and stretched into its new frame. On release the edit applied and the
drag state cleared in the same frame, but the page raster on screen
still had the picture at its old size and place until its replacement
arrived. For those frames the old picture showed again around the new
frame, then jumped.

Keep the old and new frames on the selection after a release, and draw
the picture the way the drag did while the page is out of date: its
raster, or on a tiled page any tile, still waits for a replacement. A
failed edit never makes the page stale, so nothing is kept.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The page without an added item is rendered once per gesture so the
item's pixels can follow a move (MoveBackground). A corner resize got
none of that: only the outline followed the corner, the picture stayed
at its old size under it for the whole drag, and showed there again
after the release until the page was rendered.

Give a picture's corner resize the same background, and stretch its
pixels from the box it had into the dragged box, then into the applied
box while the page settles. Text resized by a corner rewraps, so it
keeps the outline only.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
A bookmark made in the app always went to the top of the page. It now goes to
the top-left of the selected text, or with nothing selected to the top of the
window, as /XYZ left top null: the reader's zoom is kept, and with only the top
pinned so is the horizontal position.

- render: PageInfo::xyz_for_view, the inverse of dest_fraction. It pins only
  the coordinates given; on a page turned a quarter the displayed y decides the
  user left and the displayed x the user top.
- organize: add_bookmark_at and OutlineError::BadPosition (a coordinate that is
  not finite is refused before anything changes).
- engine: Edit::AddBookmark takes left and top (None, None is the page top, as
  before).
- automation: bookmark_add takes top and left in points from the top-left of the
  displayed page, as bookmark_list reports them, bounded and refused when not a
  usable number.
- ui: DocView::bookmark_position. It follows the window's vertical when the view
  is turned a quarter (View > Rotate View), picks the selection's corner that
  shows at the top-left, and in a spread of unequal pages uses the page that
  crosses the window's top edge.
- parity: organize.bookmark-add evidence and what is still missing.
Applying a paragraph takes the editor away before the edit runs. When the
edit failed (a letter no font can draw, for one), the error was shown
and the typing was gone with the editor. A refused form field already
opens again with its value.

Keep the editor whose edit was queued (applying it, or clicking into
another paragraph). If that edit fails it opens again with what was
typed and takes the focus, instead of the box the click moved to; a
successful edit drops it.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The look of an applied paragraph ended with the first whole-page raster.
On a tiled page the stale tiles are painted over that raster and can
still carry the old text. End it only once the page is no longer drawn
from before the edit: its whole-page raster (a tiled page's backdrop
too) and every one of its tiles has been replaced, whichever arrives
last.

Keep the page's turn on screen with the look, too: after the view is
turned, its position would be misplaced, so the raster shows instead,
as after a zoom.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Two cases kept the old picture on screen after a move or resize:

- A page that had no tiles when the edit applied was taken as drawn
  again as soon as a first tile arrived, while its backdrop still had
  the picture. Out of date now means its whole-page raster or any tile
  still waits for a replacement.
- A second drag before the page was drawn again took the picture's
  pixels from its new frame, where the raster doesn't have it, and
  blanked only that. It now takes them from where the raster still has
  the picture and blanks that, and keeps it as the source on release.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
A TextSession opens a paragraph once: it keeps the document as it was
and the page's lines then. Each draft is laid out on a copy of that
document, so no draft sees another's output, and the commit writes into
the document it was opened on, refusing ("the paragraph changed while it
was being edited") when that document has changed since.

text_edit_layout and rewrite_block now open a session and lay out or
commit once, so there is one path and their output is unchanged. No
caches yet: those come next, each under the same equivalence test.

First step of #2512.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The preview was always monochrome, so the Print in grayscale checkbox appeared to do nothing but flicker between the colour thumbnail and the (wrongly) greyed sheet raster.

Move GRAYSCALE_TAG to a free tag bit (1 << 59) so it no longer overlaps PRINT_TAG, and let the colour and grayscale preview requests carry different tags so set_print_grayscale actually re-renders the right one.

Test: print_preview_tag_distinguishes_grayscale_from_colour failed before the change and passes after.
The field's /TU is read into FormField::tooltip and shown in the Fields
panel, but resting the pointer on the field on the page showed nothing.
page_input now attaches it to the hover, as Acrobat does.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01R3niW7kVeT8mi7TpWc8eAJ
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01R3niW7kVeT8mi7TpWc8eAJ
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01R3niW7kVeT8mi7TpWc8eAJ
The settings scroll when the window is short, but OK was the last thing inside
the scrolled content, so in a short window it was off screen until you scrolled
to it, and the dialog was cut off partway through the theme cards.

OK is now a row below the scroll area, outside it. The scroll area gets the
height the window leaves (at least 60 pt, so OK stays on screen in the smallest
window, 520 pt, at the largest interface size, 300%) and a scrollbar of its own
instead of the floating one that lay over the right edge of the theme cards.

A scroll area starts from the room its modal offers, and a modal offers more
each frame for the first few, so with OK outside the scroll the dialog opened
small and stretched by about 45 pt a frame for ten frames, and clicks in the
first frames missed. The scroll area is asked for its full height from the start
(min_scrolled_height, which egui applies only when the settings overflow it),
so it opens at full size and does not stretch in a tall window.

- test: OK and the title are on screen at window heights from 173 to 1100; OK
  closes the dialog and does not move while the settings scroll, and a far-down
  setting scrolls into view between the title and OK; the dialog does not grow
  after it opens; a very tall window has no blank gap above OK. Both the
  original layout and the stretching variants fail them.
- parity: misc.preferences-dialog evidence and what is still missing.
Create a new digital ID rejects a password shorter than
sign::pkcs12::MIN_PASSWORD_CHARS (8) with "The password must have at
least 8 characters.", which the dialog shows through tl!. Italian only
had the older 6-character wording, so the message appeared in English.
Czech, Lithuanian and Vietnamese already carry the current key.

The other 67 keys some catalogs have and Italian lacks are not looked
up by the code: shortcut tooltips the code now fills from {key} rows,
older wordings, plain-string plurals and generic labels only ar/lt/vi
carry. The catalog's SHA-256 in ATTRIBUTION.toml is updated.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The old doc comment was left above the constants added in this change, where it
documents PREFERENCES_BUTTON_ROW wrongly, and its "Returns true to close" no
longer holds for preferences_body, which returns ().

Move its first sentence onto preferences_body, drop the "Returns" sentence, and
leave each constant with only its own comment.
Drawing a half can close its own tab (the snapshot bar's Close), which removed
or shifted the view indexes compare_area kept using: the right half's close
panicked at the scroll sync, the left half's in document_area for the other half.
Each half is now found again by id, and the frame ends when one is gone.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01R3niW7kVeT8mi7TpWc8eAJ
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01R3niW7kVeT8mi7TpWc8eAJ
encode_jpeg and encode_tiff composited the raster over white with
as_chunks::<4>(), which drops a partial trailing pixel, and handed the
result to encoders that trust its length. Run before this change:

- JPEG: a partial pixel at the end was dropped and the rest written as a
  valid image; a buffer a pixel short, a pixel too many, empty, or with a
  size whose byte count overflows panicked (image's JpegEncoder checks
  the length with assert_eq!).
- TIFF: a partial pixel or a pixel too many was written as a valid
  image; a buffer a pixel short panicked (tiff's write_data compares the
  buffer with the pixel count, not the sample count, then slices).

The length check encode_png gained in #2425 is now check_raster, and all
three encoders call it first. Rendered pages are always the right size,
so what Export > Image writes is unchanged.

export::raster_tests::rasters_of_the_wrong_length_are_rejected now runs
its seven cases through PNG, JPEG and TIFF; it fails without the fix at
"JPEG, opaque, a partial pixel after". rasters_of_no_pixels_are_an_error
covers 0x0, 0x3 and 3x0, which are the right length and still errors.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01BfMGHQNgdq5yf5tKZHGR3z
Edit text & images outlines every picture from the page's inventory,
which still has the dragged one where it was until the release; that
outline trailed behind the picture and caught up after it was let go.
Leave the dragged picture's own outline out while it moves.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
A 0×0, 0×n or n×0 raster is the right length (no bytes) and already
failed in all three encoders, but through each one's own error (PNG's
"Zero width not allowed", the JPEG encoder's size check, tiff's
dimension check). check_raster now refuses it first, with one message
for every format, so the rule doesn't depend on the encoders' versions.

rasters_of_no_pixels_are_an_error asserts that message for PNG, JPEG and
TIFF; without the check it fails on PNG's own error. From CodeRabbit's
review.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01BfMGHQNgdq5yf5tKZHGR3z
The whole-page raster and the tiles of a page come back separately. Once
the whole-page raster is fresh it has the picture where it went, but the
settled preview still took its pixels from where it was, now empty,
and drew them over it until the last stale tile arrived; a drag begun
in that interval did the same.

Keep blanking the old frame while anything is still stale, but take
the pixels from the old frame only while the whole-page raster itself
is stale (page_raster_out_of_date), else from where the picture went;
a drag in that interval blanks the first edit's frame and samples its
own.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Clicking a paragraph covered it with an opaque box until its textless
background was rendered, then redrew it in the editor's own fonts; a
quick click from one open paragraph into another also painted the first
one's textless page with no editor over it, and framed the second one
with the first one's measured extent.

The textless page now goes into a place kept under the page's later
shapes, and the editor fills it only once the paragraph has changed;
until then the page raster shows its own text and the editor draws its
text in no colour, for the caret and the selection only. The cover is
kept for a changed paragraph whose background isn't ready, decided
after the frame's typing. The frame uses a measured extent only when it
was measured for the open paragraph.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Fall back only when missing sample or terminator bytes cannot fit the
content-window budget, including retained header and separator overhead.
Preserve available deterministic boundaries and ordinary large streams.
Add oversized, exact-budget, overflow and large-stream regressions.
Keep both roadmap progress entries and all upstream changes while
preserving the inline-image parser fix and regression coverage.
The Replace text tool saves a StrikeOut with /IT /StrikeOutTextEdit and a caret
reply. The Comments panel named it by subtype only, and tool_for mapped it to the
Strikethrough tool, so Make Current Properties Default on it changed the
Strikethrough tool's defaults. Both now read the intent.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01R3niW7kVeT8mi7TpWc8eAJ
Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
TextDevice::draw_glyph called std::env::var_os twice for every glyph. getenv scans the whole environment under a lock, so text extraction paid for it on each glyph. Read the variable once into a OnceLock.
Retain the calculation-order update and inline-image repair entry.
Position the repair entry away from concurrent top-of-log additions;
parser and render regression code remain unchanged.
Add --proto '=https' to curl invocations so redirects cannot downgrade to
plaintext HTTP. Resolves SonarCloud S6506 (shell/docker/githubactions).
CffFontBlob::glyph_index_by_name searched the whole charset, with a string lookup per glyph, for every character code of a font (there was a TODO noting it). Build a name to glyph map on first use and share it between clones. The first glyph with a name still wins, so results are unchanged.
Pin third-party actions to full-length commit SHAs (with version comments)
instead of mutable tags/branches to prevent supply-chain tampering via
retagged releases. Resolves SonarCloud rule githubactions:S7637.
* M6: renaming a field renames it in the calculations that read it

A rename silently broke every formula that used the field: a sum dropped
it, notation read it as 0 and a script kept a stale value. set_props now
rewrites the name in each field's Calculate script, in the same edit:
AFSimple_Calculate field lists, simplified field notation (the BVCALC
comment and its translation) and getField calls naming it in one string
literal. The rest of each script is kept as written; names built at run
time, non-UTF-8 script streams and a name another field still has are
left alone, and a self-check keeps a script whose meaning would change.

Notation is read by one lexer shared by notation_js, eval_notation and
the rename. form_set_props returns formulas_updated; Field Properties
says how many calculations now use the new name.

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

* M6: form_set_props leaves the renamed field out of formulas_updated by object

The renamed field was matched by the name given, but set_props stores it trimmed and under
its parent's prefix. When the same call changed the field's own formula, formulas_updated
listed the field as one the rename had rewritten.

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

* M6: calculation order: read, check, fix and set the order fields calculate in

A form's calculations run in its calculation order (/CO), which a field
joins when it gets a calculation, so a chain defined in the wrong order
shows stale values. calculation_order reads the order against the fields,
with what each formula reads (field lists, notation names, literal getField
names) and what is wrong: a field reading one that calculates later, a loop
(strongly connected components, found without recursion), a field /CO
leaves out, and /CO entries that calculate nothing. Fix order is a stable
topological sort that moves only what must move and leaves loops alone.
set_calculation_order writes /CO afresh; a name two fields share (a
malformed form) stands for the next of them in the current order.

Edit::SetCalculationOrder sets it in one undo step and recalculates, and
form_calculation_order reads, sets, moves one field or fixes the order
headlessly. An edited formula now keeps its field's place in /CO, a field
/CO lists twice calculates once, and recalculation runs each field by
object, so two fields sharing a name both calculate. The /CO code of the
Calculate tab, field scripts and recalculation goes through one place.

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

* M6: Prepare a form ▸ Calculation order: a page to reorder, check and fix the order fields calculate in

The calculated fields in the order they calculate (/CO), each with its
formula and the value it shows, moved with Up/Down arrows (as Acrobat's Set
Field Calculation Order), Alt+Up/Down or by dragging. A card says which
fields read one calculated later, which read each other in a loop and which
/CO leaves out, and holds Fix order; a loop's card opens one of its
formulas. On the page, arrows show what the field in focus reads and what
reads it, with each linked field's step. The order is a page of the Prepare
a form panel, opened from its Fields tools, with a back arrow to Prepare a
form; opening it ends Preview and closes the page grid.

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

* M6: form.calculation_order runs form_calculation_order headlessly

The command had no tool linked, so command_list showed none for it and
command_run refused it. form_calculation_order is now its tool: run without
a change, it reads the order.

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

---------

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Co-authored-by: aiongg <aiongtaigi@gmail.com>
Vendored hayro-interpret refuses an embedded font whose `post` table
version isn't 1.0, 2.0, 2.5 or 3.0 (a guard from pdf.js issue 9462).
ScanSoft PDF Create! writes the version of every font it embeds with its
16-bit halves swapped (`00 00 00 03` for 3.0), so those fonts were refused
and their text drawn with a substitute: Hebrew came out as Latin
look-alikes or not at all.

1.0, 2.0 and 3.0 with swapped halves are now accepted too; any other
unknown version is still refused. PdfCraft patch (38) in vendor/README.md.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01RUNQSPVXx3dDBabRZHAnjz
Review follow-up on #2949.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Gv2WwdgjmBz33GWkwgNn3Q
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Gv2WwdgjmBz33GWkwgNn3Q
* M6: Prepare a form ▸ Text field table: rows and columns of text fields as one edit

Drag an area with the Text field table tool and set it up in a movable
window: rows, columns, row and column spacing, the first row number,
column widths (draggable dividers, arrow keys once focused) and a name
pattern per column, with the table previewed on the page. Add fields adds
every cell as one undoable edit ("Add text field table").

- forms: `table` lays the table out in view space and names its cells. It
  refuses the whole table for a cell under MIN_FIELD_SIDE (now one
  constant) or a name with a period, used twice or already taken, and
  free_base picks the first free TableK for default names.
- engine: `field_table::edit` maps the cells onto the page, /Rotate and
  /UserUnit included, and checks their size again there.
- automation: `form_add_field_table`, with an end-to-end test.
- ui-egui: the window belongs to one page of one document and closes when
  that stops being the one prepared; Enter commits a number instead of
  adding the table; dividers between many columns no longer panic in
  f64::clamp. Strings in all 22 catalogs, and the Spanish engine-wording
  tests cover the table's mistakes.
- parity: form.multiple-copies is partial; copying an existing field into
  a grid is still missing.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* M6: text field table: refuse corners past the page's number range, run it by its command

Review follow-up on #3479.

- engine: a corner finite as an f64 but past f32's range came back from
  the page's f32 mapping infinite. With one such corner a side was
  infinite rather than NaN, so the cell passed and add_field refused it
  only once the edit was applied ("the field is too small"). The edit now
  refuses any corner that isn't finite on the page with the table's own
  reason, before anything is added.
- automation: form.add.text_table runs form_add_field_table through
  command_run, as comment.stamp runs stamp_custom.
- ui-egui: doc comments for the window's three remaining helpers.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* M6: text field table: close on document edits, lay out once per change, keep Escape for fields

Review follow-up on #3479 (benandez).

- The window belongs to the document as it was when it opened: it closes
  when the edit generation moves (a page rotated, moved or deleted, an edit
  undone), so a table set up for one page never lands on another or off it.
- The table is laid out and checked once per change, not several times a
  frame: the window and the page preview share one `Laid` (cells and the
  edit), and the preview draws only the cells in view, naming only those
  with room for a name.
- Escape in a field of the window leaves the field and keeps the setup; a
  second Escape closes the window.
- A table placed with a click is moved in from the page's edges, so all of
  it is on the page.
- `free_base` takes one pass over the names; `{seq}` is documented as
  counting from the first row number; `form_add_field_table` takes its
  names from the batch instead of laying the table out again; the
  unreachable `Dialog::FieldTable` arm says why it is there.

Each new test fails with its fix reverted.

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

* M6: text field table: drawn with its panel closed, the window opens Prepare a form instead of closing

With the tool picked and the Prepare a form panel closed, the form is
still prepared and a table can be drawn. Drawing sends the tool back to
Select, though, so nothing was prepared on the next frame, and the table
window, which lasts only while the form is prepared, closed itself at
once: the table couldn't be set up. Opening the window now opens the
Prepare a form panel it belongs to. Closing the panel while the window
is open still closes the window.

Found by CodeRabbit, which saw the preview stop drawing; the window
closing first was the larger effect. prepare.rs
a_table_drawn_with_the_panel_closed_opens_it_again fails without the fix
("the window stays open": (None, false)).

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

* Drop the second generation binding the main merge left in document_area

---------

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Co-authored-by: aiongg <aiongtaigi@gmail.com>
parity/acrobat-features.toml: form calculation order entry keeps main's commands, tools and notes; evidence is the union of both sides.
ATTRIBUTION.toml: kept main's sha256 lines; hashes recomputed after the track.
crates/content/src/lib.rs: kept #3259's eod argument; image_data_end takes eod and passes it to both end_of_image calls; inline_image tries unfiltered, then declared /L, then image_data_end(filter_eod).
ROADMAP.md: kept both progress-log entries, newest first.
ATTRIBUTION.toml: sha256 lines taken from the base side; recomputed after all merges.
crates/xfa/src/script.rs: both sides added doc comments to the same functions; kept #3582's (they describe its presence(name, ...) signature).
ATTRIBUTION.md: kept main's asset count; regenerated after all merges.
ROADMAP.md: progress-log lines from both sides, newest first.
ROADMAP.md: union of progress-log lines.
crates/ui-egui/src/i18n/*.tsv (20 catalogs): union of appended keys.
ATTRIBUTION.toml: kept main's sha256 lines; hashes recomputed after the track.
crates/ui-egui/src/print_ui.rs: kept both offered() (#3764) and WakeOnExit.
crates/ui-egui/tests/print.rs: kept both sides' tests (#3596 Label tests, options-reading tests).
contributors/people.toml: union of both new entries
ROADMAP.md: union of progress-log lines.
docs/roadmap.md: union of revision-history rows.
ROADMAP.md: progress log keeps both sides' lines.
docs/roadmap.md: revision history keeps both sides' rows.
contributors/people.toml: kept both new contributor entries.
parity/acrobat-features.toml: edit.edit-text evidence and notes take both sides' additions
# Conflicts:
#	ROADMAP.md
ROADMAP.md: union of the progress-log lines
Conflicts:
- crates/ui-egui/src/i18n/*.tsv: union of both sides' new keys
- ATTRIBUTION.toml: ours; hashes recomputed at the end of the track
- ROADMAP.md, docs/roadmap.md: both progress-log entries, newest first
- parity/acrobat-features.toml (organize page grid): main's evidence and remembered-size note, with the PR's Pages inspector sentence
vendor/README.md: main's single hayro-interpret row plus the PR's patch 38 and its tests, placed before 41
vendor/README.md: hayro-syntax row already has a (22) on main; this PR's patch and tests are numbered (23).
crates/edit/src/tests.rs: kept both new tests (#3571's surplus-TJ test and this PR's spacing test).
ROADMAP.md, docs/roadmap.md: union of the progress-log and revision lines
crates/ui-egui/src/edit_text_ui.rs: union of #3952's slot/reveal (unchanged paragraph keeps the page's text) and #3863's look/settling (applied paragraph's look stays until the page re-renders); the settled look draws in the non-editor branch of paint.
docs/roadmap.md: revision history keeps both sides' rows, newest first.
ROADMAP.md: progress log union, newest first
crates/engine/src/tests.rs: union of #3180's new test and #3397's doc comment on the next test.
crates/xfa/src/layout.rs: kept #3782's formatted_value (it calls date_pattern, now built on date_letters) and button_action doc.
crates/xfa/src/parse.rs: parse_field doc comment from #3397 (names the data picture).
crates/xfa/src/script.rs: field() doc from #3582 with #3397's "as scripts read it".
crates/xfa/src/tests.rs: union of fixture imports and of the new tests; one doc comment each for the date-conversion and data-fill tests.
Conflicts:
- crates/sign/src/pdf.rs: details switch to Inter as the PR does; drawn labels keep main's
  Helvetica check and refusal, so the PR's label-driven switch to Inter is dropped. Doc comment
  merged.
- crates/sign/tests/pdf.rs: both tests kept.
fill_xref_gaps scanned every xref oddity for each object header; a file with many offset-0 rows and many headers took quadratic time.
ROADMAP.md: progress log keeps both sides' lines
crates/xfa/src/layout.rs: both sides added a draw_field doc comment; kept #3542's (the more complete one).
crates/print/src/spool.rs: printer_options_via_within (#2108) takes the language too: localize
  the PPD names, then lpoptions under the deadline.
crates/print/src/tests.rs: the CUPS tests call printer_options_via_within with LIMIT and "en";
  kept both sides' new tests.
crates/ui-egui/src/print_ui.rs: kept the background read (#2108); its default source reads
  printer_options_in in the UI language current when Properties… is clicked.
ROADMAP.md: union of progress-log lines.
docs/roadmap.md: union of status lines and revision-history rows.
crates/ui-egui/src/comments_panel.rs: card() takes main's `info` argument and the PR's borrowed reply slice.
crates/ui-egui/tests/editing.rs: kept both new tests (#3863's applied-look test and #3878's failed-edit test).
crates/xfa/src/layout.rs: both sides added doc comments to value_height, caption_reserve, field_height, unique_name and draw_field; kept HEAD's, with #3542's natural_width.
parity/acrobat-features.toml: form.fill-text evidence and notes from both sides
Conflicts:
- crates/ui-egui/tests/content.rs: both sides' new tests kept, one after the other
Missing translations never block; the Finnish coverage tests need every key
the other catalogues carry. The keys stay English until a translator fills them.
ATTRIBUTION.toml: kept main's sha256 lines; hashes recomputed after the track.
crates/ui-egui/tests/print.rs: kept both sides' tests.
Conflicts:
- ATTRIBUTION.toml: sha256 lines for es/lt/vi taken from main; recomputed after all merges.
Inter, a bundled face, draws U+049B, so the test stops failing the edit once Inter is a fallback; no bundled face draws U+0E01.
ROADMAP.md: progress-log line added at the top
docs/roadmap.md: status line, M10 row note and revision row applied onto main's
parity/acrobat-features.toml: export-docx evidence and notes insertions applied onto main's
crates/edit/src/tests.rs, crates/engine/src/tests.rs: new tests appended after main's
ROADMAP.md, docs/roadmap.md: kept both sides' log lines.
crates/print/src/tests.rs: kept both sides' additions.
Conflicts:
- crates/ui-egui/tests/editing.rs: both sides' new tests kept, one after the other
crates/ui-egui/src/edit_text_ui.rs: #3862's Area builder with fade_in(false), keeping the reveal/look locals and their uses after the Area (#3952, #3863).
crates/ui-egui/tests/editing.rs: kept all new tests.
contributors/people.toml: union of the new entries
crates/xfa/src/script.rs: template_field and named_in moved to crate::data in #3397; field_grows_with_text calls crate::data::template_field.
parity/acrobat-features.toml: form.xfa evidence lists unioned, notes take both sides' insertions.
crates/cos/src/document.rs: kept #2220's shifted_subsection_end and object_headers and this PR's prev_offset.
vendor/README.md: hayro-syntax row already has (22) and (23); this PR's patch and tests are numbered (24).
crates/ui-egui/src/edit_text_ui.rs: kept the per-paragraph draft_rect and the fade_in(false) Area builder; added #3070's page_clip and the clip on the editor's ui.
vendor/README.md: hayro-interpret row keeps #2914's patch (26) extension and #2949's new patch (38) with both tests
parity/acrobat-features.toml: form.highlight-fields evidence and notes from both sides (the renamed remembered test, the read-only test)
docs/roadmap.md: M6 row keeps both added sentences
ROADMAP.md: union of the progress-log lines
crates/ui-egui/src/edit_text_ui.rs: #3008's paragraph_box for the outlines with #3070's page clip on their painter.
vendor/README.md: main's rows plus the PR's hayro-interpret patch 35 and hayro patch 19 with their tests
ROADMAP.md, docs/gaps.md, docs/roadmap.md: the PR's status lines, log line, notes and revision rows applied onto main's
Conflicts:
- ROADMAP.md: the PR's progress-log entry added on top
- crates/ui-egui/src/lib.rs: main's covered-by-another-dialog guard kept, calling the PR's save_prompt_key with the chosen button
ROADMAP.md: kept both sides' progress-log lines.
crates/edit/src/lib.rs: re-export list is the union (TextSession from #3942; BlockKey, block_overlapping from #2756).
vendor/README.md: hayro-interpret row keeps patches (38) and (44) and both tests, in number order
ATTRIBUTION.toml: sha256 lines taken from main, recomputed after all merges
crates/engine/src/tests.rs: union of #2624's tests and #3408's test.
parity/acrobat-features.toml: form.xfa evidence lists unioned, notes take both sides' insertions.
The #2624 merge inserted its engine tests between #3782's doc comment and the test it documents.
ATTRIBUTION.toml: kept main's sha; rehashed after all merges.
ROADMAP.md: both progress-log lines, newest first.
crates/ui-egui/src/i18n/cs.tsv: both sides' entries, no duplicate keys.
docs/localization.md, docs/roadmap.md: main's single paragraph and row, with the PR's Czech wording applied.
crates/engine/src/js.rs: auto_detect_fields keeps main's comb and turn handling, builds props with no border and no fill for every field, and pushes SetFieldProps unconditionally (the review's fix)
parity/acrobat-features.toml: form.detect-fields keeps main's comb evidence and notes and adds the no-border test and sentence
Conflicts:
- docs/ui-parity.md: one status line naming both changes (#3808, #1459); both revision rows, newest first
vendor/hayro-interpret/src/encode.rs: both sides; a pixel sampled by either test (corner held, or centre covered) widens the mesh extent from #3296
vendor/README.md: main's row plus the PR's patch 36 and its test
crates/cos/src/document.rs: #2220 moved the header scan into object_headers; it also returns the position after `obj`, and reconstruct applies this PR's /XRef candidate check (same 1024-byte window) to each header there. Tests: kept both sides.
ROADMAP.md: kept main's stage line and status lines, added this PR's status and log lines.
docs/roadmap.md: kept main's status lines and M8/M9/M10 rows (main had merged the duplicate M8
  row), added this PR's status line and its M10 sentence.
crates/ui-egui/src/lib.rs: persist keeps main's `v` and its separately set keys; print_paper
  set the same way.
crates/ui-egui/tests/print.rs: kept both sides' tests.
Conflicts:
- ATTRIBUTION.toml: ours (sha256 lines only); hashes recomputed at the end of the track
crates/edit/src/text.rs: interpret takes the page resources (#3310) and the named property lists for /ActualText (main's #3301); every call site passes both. In BDC handling, #3310's layer push runs before main's property-list lookup; carry.layers is saved before positioned_tracking.
crates/edit/src/tests.rs: kept both sides' new tests.
ATTRIBUTION.toml: kept main's sha; rehashed after all merges.
ROADMAP.md: both progress-log lines, newest first.
docs/localization-parity.md: the PR's status line; both revision rows, newest first.
docs/roadmap.md: the PR's rows (main's rows plus the PR's wording).
ROADMAP.md, docs/roadmap.md: the PR's status lines, log line, M11 note and revision rows applied onto main's
ATTRIBUTION.md: generated header count set to 277 (main's 276 plus the CMap blob moved from bundled to asset).
ATTRIBUTION.toml: kept main's sha; rehashed after all merges.
crates/ui-egui/src/i18n/it.tsv: both sides' appended entries.
ROADMAP.md, docs/roadmap.md: both status lines and progress-log lines kept; M12 row takes both 'Done since' insertions.
crates/automation/tests/automation.rs, crates/xfa/src/fixtures.rs: union of #2624's and #2619's tests and fixtures.
parity/acrobat-features.toml: form.xfa evidence lists unioned, notes take both sides' insertions.
Conflicts:
- crates/ui-egui/src/chrome.rs (right_rail): the PR's bottom panel and scrolling buttons, with #2857's page position label under the page box moved into the panel
- crates/ui-egui/tests/ui.rs: both sides' new tests kept, one after the other
ROADMAP.md: progress-log line added at the top
docs/roadmap.md: the M11 note applied onto main's row (which #3622 extended)
crates/optimize/tests/optimize.rs: new tests appended after main's
A begin… block cut off at the end of the stream returned None through ? and discarded the whole map; it now ends the scan like any other unparseable tail. Test: stream_cut_off_inside_a_block_keeps_the_entries_before_it.
ROADMAP.md, docs/roadmap.md: all status lines and progress-log lines kept; M12 row takes both 'Done since' insertions.
parity/acrobat-features.toml: form.xfa evidence lists unioned, notes take both sides' insertions.
ATTRIBUTION.toml: kept main's sha; rehashed after all merges.
ROADMAP.md: both progress-log lines.
crates/ui-egui/src/i18n/pl.tsv: both sides' appended entries, no duplicate keys.
parity/acrobat-features.toml: main's single notes line with the PR's Polish clause applied.
vendor/hayro/src/renderer.rs: main's PaintSetup::Clip match with the PR's fill paint (is_stroke false) and comment
vendor/README.md: main's row plus the PR's hayro-interpret patch and test, numbered 46 (35 is #3296's)
Conflicts:
- ROADMAP.md: the PR's progress-log entry added on top
crates/edit/src/text.rs: interpret takes the page resources (#3310) with #3289's CachedFont cache and Result return; walk/enter return Result and use main's form budget; call sites pass resources and propagate with ?. TextLine and the text state keep both sides' fields (clips, gs; unreadable_mode, vertical, unreadable). The shown run keeps main's tracking: 0.0 with #3289's vertical advance and tm. replace_line checks ensure_editable before main's clipping-text refusal.
crates/edit/src/tests.rs: kept both sides' new tests.
Conflicts:
- ATTRIBUTION.toml: ours (sha256 lines only); hashes recomputed at the end of the track
- crates/engine/Cargo.toml: main's workspace chrono entry, plus the PR's pdfcraft-testkit dev-dependency
The five panel labels (Panels, Float panel, Move to group, Resize panels,
Resize panel window) stay English until a translator fills them, so that
polish_covers_ui_literals passes.
vendor/hayro-syntax/src/content/mod.rs tests: kept #2483's test and this PR's three.
Adds content::tests::inline_image_ei_may_be_followed_by_a_delimiter to its test list.
vendor/hayro-interpret/src/pattern.rs: ShadingPattern::new takes the default colour spaces (#3018) and no opacity (this PR); without_opacity (#2977) and set_alpha both kept
crates/xfa/src/tests.rs: union of fixture imports (tiny_bmp, tiny_tiff).
parity/acrobat-features.toml: form.xfa and the XFA image entry: evidence lists unioned, notes take both sides' insertions.
Conflicts:
- ROADMAP.md, docs/roadmap.md: the PR's progress-log entry, revision row and status line added on top
ATTRIBUTION.toml: kept main's sha; rehashed after all merges.
ROADMAP.md: summary names Vietnamese, Brazilian Portuguese and Spanish against the current union; both log lines.
docs/localization-parity.md: main's status line with the Spanish change added; the PR's denominator paragraph naming Brazilian Portuguese too; the PR's lt row plus main's fi row; revision rows newest first.
crates/xfa/src/data.rs: #2993's month and weekday helpers and date_tokens doc, with date_tokens kept pub(crate) for #3782's formatted_value and #3397's data pictures.
Conflicts:
- crates/ui-egui/src/lib.rs (OsEvent::Open): #2461's window raise kept, with the PR's open_path call that discards its outcome
crates/xfa/src/tests.rs: union of #1648's TIFF orientation test and #3521's tests.
crates/ui-egui/src/i18n/it.tsv: main's catalog plus the 11 PR rows for keys it lacked.
For 68 keys main already translates differently, main's text is kept; 133 PR rows
were identical to main's.
crates/render/src/raster.rs: both new tests (#2975 stencil masks, #2971 knockout groups)
vendor/hayro/src/renderer.rs: set_paint returns main's PaintSetup; the PR's knockout erase early return wraps its clip in PaintSetup::Clip
vendor/README.md: main's rows plus the PR's hayro-interpret patch 39 and hayro patch numbered 22 (19 is #3296's), with tests
Squash of PR #1594 (its branch history holds a reverted rewording commit).
crates/ui-egui/src/i18n/fr.tsv: both sides' entries, no duplicate keys.
Conflict resolutions:
- crates/ui-egui/src/system_fonts.rs: Hangul fallback kept from #2872 (U+D7A3 probe, its candidate list); #1976's duplicate probe, hangul_fallback and hangul_candidates dropped; #1976's hangul_candidates_are_font_files_without_adobe_designs test kept (checks #2872's list).
- crates/ui-egui/src/theme.rs: SYSTEM_HANGUL_FALLBACK and its fallback entry kept once, at #2872's position and doc wording.
- crates/ui-egui/tests/fonts.rs: #2872's system_fallback_covers_hangul kept, #1976's duplicate dropped.
- ATTRIBUTION.md: asset count 279 (both sides' rows).
Conflict resolutions:
- crates/ui-egui/tests/content.rs: both test sets appended at the file end kept (main's format-text test, then #3879's picture-resize helpers and test).
crates/render/src/raster.rs: main's tests plus the PR's #2970 test after the knockout test
vendor/README.md: main's rows with the PR's rewording of hayro patch 18
crates/engine/src/dates.rs, crates/ui-egui/src/i18n/mod.rs: both sides' language rows (vi, fi, sq), arrays sized 25.
crates/ui-egui/tests/fonts.rs: both catalog lists; Albanian added to main's comment.
ATTRIBUTION.toml/.md: both sides' assets; hashes and counts regenerated after all merges.
ROADMAP.md: both log lines.
docs/localization-parity.md: main's rows plus the sq row; 25 interface languages; both revision rows.
docs/localization.md: main's text with Shqip, the sq locales and code added.
vendor/README.md: main's row plus the PR's hayro-interpret patch and tests, numbered 47 (35 is #3296's)
vendor/README.md: main's row plus the PR's hayro-interpret patch 40 and its test
crates/engine/src/dates.rs, crates/ui-egui/src/i18n/mod.rs: both sides' language rows (fi, sq, th), arrays sized 26.
vendor/README.md: main's row plus the PR's hayro-interpret patch and tests, numbered 48 (40 is #2987's)
Conflict resolutions:
- ATTRIBUTION.toml: main's entries kept; #3101's page-gap provenance note appended to the 22 catalogs it translates (hashes recomputed separately).
- ATTRIBUTION.md: main's side kept; regenerated from ATTRIBUTION.toml separately.
- ROADMAP.md: #3101's two 2026-10-18 progress-log lines placed after the 2026-10-19 entries.
- docs/roadmap.md: both 2026-10-18 status lines kept; M3 row carries both sides' insertions.
crates/engine/src/dates.rs, crates/ui-egui/src/i18n/mod.rs: both sides' language rows, arrays sized 27.
crates/ui-egui/tests/fonts.rs: both catalog lists; Catalan added to main's comment.
ATTRIBUTION.toml/.md: both sides' assets; hashes and counts regenerated after all merges.
ROADMAP.md: both log lines.
docs/localization-parity.md: main's rows plus the ca row; 27 interface languages; both revision rows.
docs/localization.md: main's text with Català, the ca locales, code and catalog paragraph added.
docs/roadmap.md: main's M2 notes with the PR's sentence appended
ROADMAP.md: the PR's progress-log line moved to the top (newest first)
page_gap_labels_are_translated_in_every_catalog (#3101) requires both labels translated in every catalog, including the three #1976 adds; English filler would fail its assert_ne. AI-assisted translations, noted in ATTRIBUTION.toml.
ROADMAP.md: progress log union, newest first
docs/roadmap.md: main's M2 notes with the PR's sentence appended
crates/engine/src/dates.rs, crates/ui-egui/src/i18n/mod.rs: both sides' language rows, arrays sized 28.
crates/ui-egui/tests/fonts.rs: both catalog lists.
ATTRIBUTION.md: both sides' rows; count regenerated after all merges.
ROADMAP.md: main's summary with Romanian added (15 more catalogs); both log lines.
docs/localization-parity.md: main's rows plus the ro row; 28 interface languages; both revision rows.
docs/localization.md: main's text with Română, the ro locales and code, and the PR's catalog-list edits.
crates/ui-egui/src/i18n/mod.rs: both sides' language rows, array sized 29.
ATTRIBUTION.md: both sides' rows; count regenerated after all merges.
DATE_LANGUAGES gains the lo row in the same position as in LANGUAGES, so
date_languages_are_the_interface_languages holds; lo.tsv loses a key that
matches no English source.
crates/engine/src/dates.rs, crates/ui-egui/src/i18n/mod.rs: arrays sized 30.
ATTRIBUTION.md: both sides' rows; count regenerated after all merges.
docs/localization.md: main's text with the ckb name, code and catalog-list entry added.
Catalog hashes recomputed after #1976, #3101 and the page-gap fill; ATTRIBUTION.md regenerated with cargo xtask assets --write.
Main and the PR each added the same Font key; the catalog reports a duplicate.
Romanian (47) and Catalan (50) lack keys main added since they were written;
their bundled-key coverage tests need every key the other catalogs carry.
Albanian lacks 101 UI literals and 4 command labels its coverage tests check.
The entries stay English until a translator fills them.
* M14: add Norwegian Bokmål interface and date language

A 2,592-entry `nb.tsv` covering every key the other bundled catalogs have,
written from the meaning of the English labels (clean-room); terminology
reviewed by a native-speaking contributor. Registers `nb` with the English
one/other plural rule, adds Norwegian month and weekday names for Fill &
Sign dates, resolves `no` and `nn` locales to Bokmål until a Nynorsk catalog
exists, attributes the catalog and records it in parity.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>

* M14: translate the strings main added since the Norwegian catalog was written

69 keys (signature appearance labels, word count, full-screen prompt, print
as image, image crop, redaction mark appearance, …) and the attribution hash.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>

* M14: translate the picture-menu and label-paper strings in the Norwegian catalog

Seven keys main added (Cut/Redact Image, Create Link…, Recognize Text…,
Turn upside down, the 4 x 6 in label paper) and the attribution hash.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>

* M14: translate the page-group folding commands in the Norwegian catalog

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>

* M14: drop the superseded localization summaries left beside the current ones

Concurrent merges on main left older copies of several localization
summaries next to their replacements. Review of #1836 flagged them:

- localization-parity.md: remove the 2026-10-13 status line and the
  14-language summary that still listed Norwegian as missing; the
  current summary keeps the Italian (and now Norwegian Bokmål)
  measurement dates.
- localization.md: keep one catalog-coverage paragraph (now naming
  Norwegian Bokmål, up to about 2,700 entries) and one font paragraph.
  The font paragraph folds in what only the older copies said: the
  installed Chinese face fallback (#826) and Slovenian's Latin faces and
  four plural forms.
- roadmap.md: end the M14 Missing list before the Fontconfig note.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

---------

Co-authored-by: Claude Fable 5.1 <noreply@anthropic.com>
Co-authored-by: aiongg <67543387+aiongg@users.noreply.github.com>
Co-authored-by: aiongg <aiongtaigi@gmail.com>
# Conflicts:
#	crates/edit/src/tests.rs
#	crates/edit/src/text.rs
#	crates/ui-egui/src/canvas.rs
#	crates/ui-egui/src/edit_text_ui.rs
# Conflicts:
#	crates/automation/tests/automation.rs
#	crates/engine/src/tests.rs
#	crates/ui-egui/src/content_ui.rs
#	crates/ui-egui/src/edit_text_ui.rs
#	crates/ui-egui/src/fill_sign.rs
#	crates/ui-egui/src/forms_ui.rs
#	crates/ui-egui/tests/editing.rs
# Conflicts:
#	crates/engine/src/tests.rs
#	crates/ui-egui/src/canvas.rs
#	crates/ui-egui/src/forms_ui.rs
#	docs/ui-parity.md
# Conflicts:
#	crates/automation/tests/automation.rs
#	crates/render/src/raster.rs
# Conflicts:
#	crates/edit/src/tests.rs
#	crates/render/src/raster.rs
#	vendor/README.md
# Conflicts:
#	crates/automation/src/tools.rs
#	vendor/README.md
# Conflicts:
#	ATTRIBUTION.toml
#	crates/ui-egui/src/lib.rs
# Conflicts:
#	ATTRIBUTION.toml
#	crates/automation/src/tools.rs
#	crates/edit/src/lib.rs
#	crates/ui-egui/tests/compare.rs
#	crates/ui-egui/tests/ui_group/marks.rs
# Conflicts:
#	ATTRIBUTION.toml
# Conflicts:
#	ATTRIBUTION.md
#	crates/annot/src/tests.rs
#	crates/forms/src/tests.rs
#	crates/organize/src/tests.rs
# Conflicts:
#	ATTRIBUTION.toml
#	crates/annot/src/tests.rs
# Conflicts:
#	crates/edit/src/tests.rs
#	crates/engine/src/tests.rs
# Conflicts:
#	ATTRIBUTION.toml
#	contributors/people.toml
#	crates/automation/src/comments.rs
#	crates/edit/src/tests.rs
#	crates/engine/src/tests.rs
#	parity/acrobat-features.toml
# Conflicts:
#	ATTRIBUTION.md
#	ATTRIBUTION.toml
#	apps/pdfcraft/src/main.rs
#	crates/engine/src/dates.rs
#	crates/fonts/src/lib.rs
#	crates/ui-egui/src/canvas.rs
#	crates/ui-egui/src/i18n/mod.rs
#	crates/ui-egui/src/system_fonts.rs
#	crates/ui-egui/src/theme.rs
#	crates/ui-egui/tests/content.rs
#	crates/ui-egui/tests/fonts.rs
#3101's page_gap_labels_are_translated_in_every_catalog asserts that every
catalog translates its two labels. The Finnish, Albanian, Thai, Catalan,
Romanian, Lao and Central Kurdish catalogs from track t12 lack them. The
translations are AI-assisted and noted as such in ATTRIBUTION.toml.
The track merges used a union driver for these files, which kept two
versions of a milestone row or status line wherever two tracks changed the
same one. They are recomputed as base plus each track's changes, with two
edits of one line combined into one line. The status lines that #2805, #3808
and #3532 put above the Stage line join the status list below it, newest
first.
Prepare a form's arrow-key nudge (#2621) consumed Alt+↑/↓ as plain arrows,
because egui's consume_key ignores an extra Alt. Calculation order (#3733)
moves the selected field with Alt+↑/↓, so the field moved a point on the
page instead of in the order. The nudge now leaves Alt+arrow alone.
#2425 writes opaque rasters as 8-bit RGB; the owner-password render test
was written against the RGBA encoder. A rendered page is always opaque,
so the test asserts RGB and still counts inked pixels.
#2609 put the Japanese faces ahead of the bundled Inter face and rewrote
the #1760 test to expect Inter, which holds only without craft-fonts.
With CRAFT_FONTS_DIR, BIZ UDPGothic has ő and draws it. The test now
expects the Japanese sans face when the build has one, Inter otherwise.
#3515 measures a growing non-interactive field at its text's scale but
with a widget's 4pt of padding, while the layout draws its text without
it. Once #3521 fits lines by their ink, the narrower measuring width
wrapped to one more line than the drawn text, leaving a blank line
below it. The measurement leaves out the padding the draw doesn't have.
English fill (the "Untranslated" section of each catalogue), for keys the
coverage tests only need present:
- ca, fi, ro, pt-br: keys other catalogues gained (print, thumbnails,
  layers, signature checks, Reshape comment, Remove {standard}
  identification)
- sq: UI literals and the visible-signature labels
- hi, ko, id: calculation-order, rich-text and print/thumbnail literals
- pl: signature-check wordings

AI-assisted translation (noted in ATTRIBUTION.toml):
- cs: the four new signature-check wordings and Remove {standard}
  identification, since Czech asserts every entry is translated

Lao's file.word_count command id said its own id; it reads as the
plain "Word count…" translation.
#2240 (on main before this round) took the Read tab out of the mode bar.
#3010 and #3022 were written against a mode bar that still had it, so
their five tests looked for a Read button that is gone. The tests now
check All tools in its place where the scrolled content reaches under it
(added text, the Fill & Sign type box). Page 1's text and the form's name
field start right of All tools, so those tests check Edit, Convert and
E-Sign, the tabs they lie under.
#3781 draws a Fill & Sign line across the page as /Rotate displays it, so on
a page turned a quarter its rect runs along user y. #3520's test expected
the line along user x on every page and failed on the /Rotate 90 cases.
The test's points and rects are now given along the line, so each rotation
checks the same grow, shrink and release.
#3525 asked for its snapshots to be regenerated once the batch is
assembled. Each change comes from a merged PR:
- panel_compare: the Show: Text / Visual filters (#2607).
- panel_layers: the Layer options button (#2731, Reset to Initial Visibility).
- panel_pages: the thumbnail size label, buttons and slider (#3828).
# Conflicts:
#	ATTRIBUTION.md
#	ATTRIBUTION.toml
#	crates/engine/src/dates.rs
#	crates/ui-egui/src/i18n/ar.tsv
#	crates/ui-egui/src/i18n/bg.tsv
#	crates/ui-egui/src/i18n/cs.tsv
#	crates/ui-egui/src/i18n/de.tsv
#	crates/ui-egui/src/i18n/el.tsv
#	crates/ui-egui/src/i18n/es.tsv
#	crates/ui-egui/src/i18n/fr.tsv
#	crates/ui-egui/src/i18n/hu.tsv
#	crates/ui-egui/src/i18n/it.tsv
#	crates/ui-egui/src/i18n/ja.tsv
#	crates/ui-egui/src/i18n/kk.tsv
#	crates/ui-egui/src/i18n/lt.tsv
#	crates/ui-egui/src/i18n/mod.rs
#	crates/ui-egui/src/i18n/pl.tsv
#	crates/ui-egui/src/i18n/pt-br.tsv
#	crates/ui-egui/src/i18n/ru.tsv
#	crates/ui-egui/src/i18n/sl.tsv
#	crates/ui-egui/src/i18n/te.tsv
#	crates/ui-egui/src/i18n/tr.tsv
#	crates/ui-egui/src/i18n/uk.tsv
#	crates/ui-egui/src/i18n/vi.tsv
#	crates/ui-egui/src/i18n/zh-hans.tsv
#	crates/ui-egui/src/i18n/zh-hant.tsv
#	crates/ui-egui/tests/fonts.rs
#	docs/localization-parity.md
#	docs/localization.md
#	docs/roadmap.md
#	parity/acrobat-features.toml
a_dead_options_read_wakes_an_idle_dialog asserted the dialog was idle after one
Harness::run. The print preview's page rasters render on the pool and ask for a
frame every 30 ms until they arrive; with kittest's 0.25 s step that is an
immediate repaint, so on a loaded machine run() exceeded its 4 steps
(canvas.rs finish_render_frame). The test now steps until no render is pending.
Under 24 parallel copies the old test failed 3 of 192 runs, the new one 0 of 72.
Albanian, Catalan, Finnish, Hindi, Korean, Indonesian and Romanian get all
22 text-field-table strings; Brazilian Portuguese gets the 4 limit messages
its full-union test requires. AI-assisted, marked in ATTRIBUTION.toml.
Each kittest harness creates its own wgpu/Vulkan instance on first render, and
lavapipe segfaults when one test creates an instance while another renders
(core dump: vkCreateSampler in libvulkan during a concurrent vkCreateInstance).
The batch tripled the rendering tests in editing.rs. editing and content take
the GPU lock view.rs and fill_sign.rs already use. Crashes over default-thread
runs: editing 11/40 before, 0/40 after; content 4/20 before, 0/20 after.
Windows CI has a default printer, so the button still read Print in the
frame where the test cleared the printer; the other print tests already
run a frame first.
Merge pull request #3983 from aiongg/next
Some checks failed
freebsd.yml / Merge pull request #3983 from aiongg/next (pull_request) Failing after 0s
nix.yml / Merge pull request #3983 from aiongg/next (pull_request) Failing after 0s
packaging-lint.yml / Merge pull request #3983 from aiongg/next (pull_request) Failing after 0s
sonarcloud.yml / Merge pull request #3983 from aiongg/next (pull_request) Failing after 0s
freebsd.yml / Merge pull request #3983 from aiongg/next (push) Failing after 0s
nix.yml / Merge pull request #3983 from aiongg/next (push) Failing after 0s
packaging-lint.yml / Merge pull request #3983 from aiongg/next (push) Failing after 0s
sonarcloud.yml / Merge pull request #3983 from aiongg/next (push) Failing after 0s
CI / test (ubuntu-latest) (push) Has been cancelled
CI / test (windows-latest) (push) Has been cancelled
CI / wasm32 check (push) Has been cancelled
CI / dependency licences and advisories (push) Has been cancelled
CI / fuzz (mutation, 15 min) (push) Has been cancelled
CI / wasm32 check (pull_request) Has been cancelled
CI / dependency licences and advisories (pull_request) Has been cancelled
CI / test (ubuntu-latest) (pull_request) Has been cancelled
CI / test (windows-latest) (pull_request) Has been cancelled
CI / fuzz (mutation, 15 min) (pull_request) Has been cancelled
9a0ec64568
Integration batch: 218 PRs on main
alf merged commit 9a0ec64568 into main 2026-10-22 09:27:53 +00:00
alf deleted branch chore/sync-upstream-20261022 2026-10-22 09:27:54 +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!5
No description provided.