Commit Graph
4320 Commits
Author SHA1 Message Date
Brandon ThomasandClaude Opus 5.5 22bedbf2af Skip CodeRabbit reviews on Dependabot PRs (#3692)
Adds .coderabbit.yaml so CodeRabbit no longer reviews or comments on PRs opened by dependabot[bot].

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-20 11:38:59 -04:00
Brandon ThomasandClaude Opus 5.5 b77d57000c Release 0.10.1: rebuild with the shared workflow's exact Linux tarball pick
0.10.0's Arch Linux and Flatpak aarch64 jobs picked up the
pdfcraft-cli tarball (storytold/craft-common-actions#35).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
v0.10.1
2026-10-20 11:38:32 -04:00
Dylan M. Taylor 59a5a83774 docs: add SonarCloud and CodeRabbit badges to README (#3271)
* docs: add SonarCloud and CodeRabbit badges and standardize to flat-square

* docs: put SonarCloud and CodeRabbit badges on their own line
2026-10-20 11:29:59 -04:00
Hazel Granados 604529e929 M3: let Escape close an open menu before it clears the selection (#3535)
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.
2026-10-20 10:28:33 -05:00
Hamza bfc7487384 Tagged PDF: resolve /MarkInfo /Marked when it is an indirect reference
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.
2026-10-20 20:28:27 +05:00
Combiler beb1a70d71 Merge branch 'main' into main 2026-10-20 15:26:13 +00:00
Jayesh-Prajapati9 ebc7b6d801 M2: text extraction keeps doubled letters in text condensed to 20 Tz and below (#2457)
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.
2026-10-20 20:34:15 +05:30
Jayesh-Prajapati9 e9fc4cadcf M8: applying redactions drops structure-tree references to the comments and fields it removes (#2874)
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.
2026-10-20 20:05:01 +05:30
Hamza ab7b584811 Redaction: follow nested forms as deep as the readers do
A form past the pass's depth limit is removed rather than walked. That
is the fail-safe choice for a redaction -- marked content inside a form
we cannot read would otherwise survive -- but it also removes whatever
else that form drew, marked or not. At a limit of 12, a page with a
13-deep chain lost the text of its innermost form with nothing in it
marked.

Measured on a page that draws HEAD, a chain of forms whose innermost
draws DEEP, then TAIL, with only HEAD marked:

    chain   before            after (12)      after (50)
    1       HEAD DEEP TAIL    DEEP TAIL       DEEP TAIL
    12      HEAD DEEP TAIL    DEEP TAIL       DEEP TAIL
    13      HEAD DEEP TAIL    TAIL            DEEP TAIL
    14      HEAD DEEP TAIL    TAIL            DEEP TAIL
    50      HEAD DEEP TAIL    TAIL            DEEP TAIL
    51      HEAD TAIL         TAIL            TAIL

The limit is now 50, which is what the renderer follows
(MAX_NESTED_INTERPRETATION_DEPTH) and what Edit text's reader follows
(text::MAX_FORM_DEPTH). So anything a reader can see, this pass can
walk. The last row is why 50 and not more: at 51 the renderer does not
draw DEEP either, so removing that form costs nothing visible and the
fail-safe still holds.

Fixes #3645's data loss. The other half of that report -- that nothing
tells the user when a form is removed -- is untouched: Report.forms_removed
is counted but the engine's edit pipeline does not carry it out, and the
one channel that reaches the result (doc.edit_notices) is typed
TextNotice, for text that could not be drawn. Widening that type or
adding a channel is a design choice for the maintainers rather than
something to slip into this fix.
2026-10-20 19:25:01 +05:00
NetterSilverandClaude Haiku 5.5 b85af272c3 Accept a delimiter after EI when ending an inline image
Fixes #3578

Co-Authored-By: Claude Haiku 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-20 14:11:33 +00:00
Eric Kaden 572e01a71d M12: the wrapping test checks the text stays within its room 2026-10-20 09:39:32 -04:00
Ozt-y 9451e0652c fix(content): recognize ~>EI terminator for ASCII85 inline images and cover filter arrays 2026-10-20 18:57:43 +05:30
Eric Kaden b6cd6b44ca M6: doc comments for the functions around this change 2026-10-20 09:26:34 -04:00
Eric Kaden f9daeeb1fa M12: doc comments for the functions around this change 2026-10-20 09:25:19 -04:00
Eric Kaden 15eef03efc M12: doc comments for the functions around this change 2026-10-20 09:24:47 -04:00
Eric Kaden ad453129f0 M5: doc comments for the functions around this change 2026-10-20 09:23:23 -04:00
Eric Kaden ee91673162 M12: doc comments for the functions around this change 2026-10-20 09:21:31 -04:00
Eric Kaden ec14687c6d M12: a draw without text or a width keeps the width it has room for 2026-10-20 09:20:55 -04:00
jonathanwxh-cell 811a3a84c1 fix(xtask): wait for Chrome child processes to settle PDF after launcher exits (#3103) 2026-10-20 21:13:46 +08:00
NetterSilverandClaude Haiku 5.5 07f3c97491 Keep filtered inline images whole when their data contains EI
Fixes #3579

Co-Authored-By: Claude Haiku 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-20 12:57:55 +00:00
wstock 801980ba35 cos: the /Prev repair note names the reference's own generation (#2366) 2026-10-20 12:43:21 +00:00
Hamza 386c92c034 Render: skipping a number covers the signs reading accepts
`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.
2026-10-20 17:12:11 +05:00
jpeskovandClaude 9175291a8f fix(render): draw missing spacing glyphs in Japanese CID substitutes as spaces (#3601)
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>
2026-10-20 17:33:37 +05:30
Ahmad Mujahid 69109cd43e fix(print): reuse shared appearance promotions (#3532) 2026-10-20 15:51:16 +04:00
NetterSilverandClaude Haiku 5.5 25e273a33d Pair /ActualText correctly past 256 levels of marked content
Fixes #3573

Co-Authored-By: Claude Haiku 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-20 13:47:09 +02:00
Ahmad Mujahid 3b3787078e Fix named properties in forms without resources 2026-10-20 15:39:35 +04:00
sanches 756a2f6fe2 i18n: add the Zoom percentage-string to Hindi, Indonesian and Korean
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.
2026-10-20 14:39:26 +03:00
wstockandClaude Opus 5.5 3af350026f Issue #2366: a /Prev reference to an object holding the offset reads that offset
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
2026-10-20 11:32:17 +00:00
wstock c351febe85 hayro-syntax: a trailer /Prev written as a reference reaches the earlier sections (PdfCraft patch 22)
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.
2026-10-20 11:32:17 +00:00
sanches 21652973bb Merge remote-tracking branch 'origin/main' into i18n-id-vi 2026-10-20 14:30:10 +03:00
wstockandClaude Fable 5.1 0e8ebf00e6 Issue #2366: a trailer whose /Prev is a reference still reaches the earlier sections
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
2026-10-20 11:29:42 +00:00
mtlcl410andClaude Opus 5.5 de25be16e5 test: the off-page paragraph test compares the gap with the same pixels without Edit text (#3061)
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
2026-10-20 18:50:05 +08:00
Ahmad Mujahid 1489436392 fix(print): preserve existing appearances without Subtype (#3532) 2026-10-20 14:34:40 +04:00
oracola91 deca1e03d1 Control channel: wait after a failed accept instead of retrying at once
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.
2026-10-20 13:26:03 +03:00
Nick Nyssen 4d2e40155a M14: correct two drifted comments and cite why candidates are optional
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.
2026-10-20 03:15:29 -07:00
Hamza 766ff9d05a Render: a doubled sign is one number, so a cm with it still has a matrix
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.
2026-10-20 14:47:52 +05:00
Ahmad Mujahid 25eb568839 fix(forms): preserve content stream comment boundaries (#3502) 2026-10-20 13:30:02 +04:00
NetterSilverandClaude Haiku 5.5 1cd36bfcb2 Decode JavaScript hex and control escapes in AF script strings
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>
2026-10-20 09:25:47 +00:00
Ahmad Mujahid ab9a5009ac Fix #3507: preserve content boundaries when merging image fragments 2026-10-20 13:23:20 +04:00
Rahul Ranjan 631ca00bdd The image scan skips a path segment before any m instead of panicking (#3581)
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>
2026-10-20 14:50:30 +05:30
Nick Nyssen 79cfdc0020 M14: apply the dead-path rule to families, not just the last resort
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.
2026-10-20 01:59:35 -07:00
mtlcl410andClaude Opus 5.5 d5002807be M10: a turned fill or clip covers the page only if its corners do (#2992)
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
2026-10-20 16:55:20 +08:00
mtlcl410andClaude Opus 5.5 ca0e0b08f3 M10: a fill whose second rectangle cuts a hole is no page background (#2992)
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
2026-10-20 16:55:20 +08:00
mtlcl410andClaude Opus 5.5 8707e87aff M10: give the #2806 export tests' pages a background after rebasing (#2992)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Gv2WwdgjmBz33GWkwgNn3Q
2026-10-20 16:55:20 +08:00
mtlcl410andClaude Opus 5.5 d85d49a4c6 M10: Export to Word keeps the page background colour (#2992)
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
2026-10-20 16:55:20 +08:00
oracola91 586246d69a organize: check the page's own MediaBox values in the walk test
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.
2026-10-20 11:23:29 +03:00
Nick Nyssen 4e69ca14d3 M14: name the last resort and keep the pins honest
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.
2026-10-20 01:01:27 -07:00
mtlcl410andClaude Opus 5.5 f6255c0ed6 M3: numbered glyph names read as nothing again, not as a StandardEncoding guess (#3169, #3170)
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
2026-10-20 15:54:51 +08:00
oracola91 15f9203243 Page-tree walks in annot, forms and organize skip repeated children
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.
2026-10-20 10:30:20 +03:00
Fernando Areán MagánandClaude Opus 5 5fc66c4595 M14: refresh the Spanish catalog's ATTRIBUTION entry and date the parity figures
`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>
2026-10-20 09:15:23 +02:00