- Copy checks the document's copying permission first, then reads the
pages whose text wasn't read for display on a worker thread
(DocumentCopy; in place on the web), keeping only each page's text as
it goes; the clipboard gets the text on a later frame. A new copy or
the document closing cancels it.
- Any press on a page ends Select All of the document, whatever the tool
and whether its text is read; so do switching to one page at a time, a
page selection, and an edit of the document.
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.
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>
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.
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>
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.
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>
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"].
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.
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>
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>
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.
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.
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.
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 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.
`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.
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>
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.
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 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.
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
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 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.
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.
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.