- `has_signatures` treats a /Contents of only zero bytes as unsigned and finds
signature dictionaries in one pass over the file (field /V values, plus
/ByteRange dictionaries and document timestamps) instead of two.
- Before the first signature, a field's own appearance counts as drawing text
only if it or a form XObject it paints shows text; a background-only XObject
no longer keeps an appearance that would leave the field blank. The look
inside opens at most 16 streams; past that it counts as text, as before.
- Tests: zero-filled placeholders (hex and literal), document timestamps, a
widget without /AP, background-only and text-drawing XObject appearances
with an unencodable character.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Once the flag is gone, viewers show each field's stored appearance, so a
filled field whose /AP /N draws no text (a /Tx BMC EMC placeholder or a
stale look) went blank. Session::sign and sign_with_timestamp now sign the
copy with_field_appearances draws, as the document is displayed, when the
signature is the first. A field whose own appearance draws text keeps it
where PdfCraft's drawing would show a character as `?`.
The earlier-signature check is now pdfcraft_sign::has_signatures, shared by
the engine and the signer, and it also finds signed fields without a /T.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* M9: redraw named-layer signature status layers
- /N drawing one Form that holds n1 and n3: n1, n3 and n4 redrawn for the state
- copies made once per original and state; originals never edited
- Lucide icons; captions in Helvetica, embedded Inter, or craft-fonts outlines
Signed-off-by: Chiranjeevi U <244287281+chiranjeevi-max@users.noreply.github.com>
* M9: draw signature states in the display copy, exports and print
- marks follow the bytes shown; a changed verdict redraws after a metadata edit
- background opens pick up the trust and captions current at commit
Signed-off-by: Chiranjeevi U <244287281+chiranjeevi-max@users.noreply.github.com>
* M9: status captions in the interface language
- the three captions in all 22 catalogues
- Signatures panel colours from sign::status_rgb
Signed-off-by: Chiranjeevi U <244287281+chiranjeevi-max@users.noreply.github.com>
* M9: signature badge in the widget's corner
- a quarter of the shorter side, 48pt cap; outline only under 32pt
- dropped for a drawn valid state; bottom-right where the appearance draws one
- sign_list reports verdict_shown; parity and roadmap
Signed-off-by: Chiranjeevi U <244287281+chiranjeevi-max@users.noreply.github.com>
* M9: keep Arabic signature captions in logical order
Pass raw RTL catalogue text to the signature appearance shaper so UAX #9 is applied exactly once. This keeps the rendered word order and exported /ActualText consistent.
Signed-off-by: Chiranjeevi U <244287281+chiranjeevi-max@users.noreply.github.com>
* M2: read /ActualText over painted paths and pictures
- A replacement that marks no glyph reads where it paints, cut to what is visible and along the way it is drawn.
- A span the page content leaves open ends before the annotations.
- OCR and scan field detection still count such a page as having no text of its own.
Signed-off-by: Chiranjeevi U <244287281+chiranjeevi-max@users.noreply.github.com>
* M8: redaction strips /ActualText around paint it clips
- A path or shading written again under a clip counts as changed, so a partly redacted word drawn as outlines leaves no name in the file.
Signed-off-by: Chiranjeevi U <244287281+chiranjeevi-max@users.noreply.github.com>
* M9: recognise only layer forms that draw n1 and n3
- Settle on the one form holding both first, then read its content once per call.
- Skip an /N that is its own layer form; copies keep the layer's /OC.
- A verdict counts as drawn when any visible widget shows it.
Signed-off-by: Chiranjeevi U <244287281+chiranjeevi-max@users.noreply.github.com>
* M9: keep the signature badge and redraw wherever a state changes
- The badge stays for every state; its mark sits bottom-right as the widget lies on the page.
- An edit that changes a verdict redraws every page; captions apply before startup files open.
- Print, the CLI's render and the repair log follow the display copy.
Signed-off-by: Chiranjeevi U <244287281+chiranjeevi-max@users.noreply.github.com>
* M9: restore the attribution usage the rebase dropped
- Usage lines for the three icons, Inter and the craft-fonts input; ATTRIBUTION.md rebuilt from them.
- Roadmap and localization notes for this round.
Signed-off-by: Chiranjeevi U <244287281+chiranjeevi-max@users.noreply.github.com>
* M9: pin signature field names on a nested form
Signed-off-by: Chiranjeevi U <244287281+chiranjeevi-max@users.noreply.github.com>
* M9: bound the signature field walk
- kids share their parent's name and type instead of each copying them
- a widget stays listed under every field that claims it, as one shared copy
- a field written in place in a shared /Kids array is walked once
- caps of 20,000 fields, 100,000 entries and 4096-byte names; no states drawn past them
Signed-off-by: Chiranjeevi U <244287281+chiranjeevi-max@users.noreply.github.com>
* M9: keep a document signed when the field walk stops early
- list_cached adds a signed row of unknown validity for the fields the walk didn't read
- Save As and actions then append, and Optimize, Sanitize and full rewrites are refused, as for any signed file
Signed-off-by: Chiranjeevi U <244287281+chiranjeevi-max@users.noreply.github.com>
* M8: removing hidden content no longer strips the names of shadings
- With no regions to clip, a shading is left alone instead of wrapped and marked clipped
- clean_replacements skips scopes already marked, so deep nesting stays linear
Signed-off-by: Chiranjeevi U <244287281+chiranjeevi-max@users.noreply.github.com>
* M2: read a picture's /ActualText over the whole picture
- draw_image's transform maps pixels, not a unit square, so the text sat on one pixel in the corner; the test now draws a 2x2 picture
- Painted grows in place instead of being copied through an Option
Signed-off-by: Chiranjeevi U <244287281+chiranjeevi-max@users.noreply.github.com>
* M9: cap the bytes of names the signature field walk keeps
- A kid's name repeats its parent's, so one long name over 20,000 kids came to 80 MB; the walk now stops at 4 MiB of names and counts the rest as unread
- Cover the widget written into a shared /Kids array and the field cap among kids, which no test caught
- sig_fields is private again, and the cap docs say what happens past each cap
Signed-off-by: Chiranjeevi U <244287281+chiranjeevi-max@users.noreply.github.com>
* M9: skip status-layer work that changes nothing; fix tests that could not fail
- show_status_layers and has_status_layers return before reading the form when nothing is signed, so an unsigned form no longer walks every field on each edit
- revalidate and draw_captions leave documents with no drawn state alone, and no longer copy the signature list to update it
- Scan form detection reads the page's text once; comment and badge tips share one hover rule
- Tests: drop a table row that repeated another, stop taking an expectation from the code under test, stop comparing an Arc with itself, give bare asserts messages
Signed-off-by: Chiranjeevi U <244287281+chiranjeevi-max@users.noreply.github.com>
* M9: test the signature field name cap at the engine boundary
- A 4,096-byte name is read and blocks a full rewrite; at 4,097 bytes the list holds the unread-fields row and a full rewrite is still refused
Signed-off-by: Chiranjeevi U <244287281+chiranjeevi-max@users.noreply.github.com>
* M9: test signature states beside fields the viewer draws
- One display copy holds both a field's drawn appearance and the signature's state.
Signed-off-by: Chiranjeevi U <244287281+chiranjeevi-max@users.noreply.github.com>
* M9: draw Thai and Devanagari status captions in their own script
- script_of names Thai and Devanagari, so a craft-fonts face meant for either draws the caption as outlines; without one it stays English.
- Localization, parity and the craft-fonts usage line list the new scripts.
Signed-off-by: Chiranjeevi U <244287281+chiranjeevi-max@users.noreply.github.com>
* M2: paint left out by an exclusion mixes its /ActualText replacement
- A replacement placed by outlines or pictures, some of them excluded, fails filtered extraction as one with excluded glyphs does.
- Glyphs still place a replacement that has them.
Signed-off-by: Chiranjeevi U <244287281+chiranjeevi-max@users.noreply.github.com>
* M9: a long-named field that can't hold a signature leaves the walk whole
- A leaf of another type passed over for its name is no signature field and has none below it, so an unsigned form stays unsigned.
- A signature field, or any field with kids, still counts as unread.
Signed-off-by: Chiranjeevi U <244287281+chiranjeevi-max@users.noreply.github.com>
* M9: status captions use document faces only
- Each script takes the face PdfCraft writes it into documents with, so Chinese stays English although the pinned craft-fonts' interface-only Chinese face is meant for Hani.
- The craft-fonts usage line names Devanagari: the pinned revision has a face for it, and Hindi captions are drawn in it.
Signed-off-by: Chiranjeevi U <244287281+chiranjeevi-max@users.noreply.github.com>
* M9: refuse a new signature field the field walk can't read back
- Signing adds a field only when the walk reads the whole form with it: past a cap the signature would be listed as unread fields, and its name could repeat an unread field's.
- A field the walk already reads is signed as before.
Signed-off-by: Chiranjeevi U <244287281+chiranjeevi-max@users.noreply.github.com>
* M9: refuse a new signature field with a blank name
- The walk lists no field without a name, so signing now also requires the walk to list the new field: one named only with spaces or NULs would be a signature nothing shows.
Signed-off-by: Chiranjeevi U <244287281+chiranjeevi-max@users.noreply.github.com>
* M9: an unnamed signed field or an unreadable form counts as unread
- A signed field with no usable /T, or an /AcroForm or /Fields of the wrong type, cuts the field walk short: the document keeps its unread-fields row and stays signed.
- Signing refuses a blank new field name before it adds the field, so the refusal still says why.
Signed-off-by: Chiranjeevi U <244287281+chiranjeevi-max@users.noreply.github.com>
* M9: no display copy when no signature state is drawn
- display_bytes_for makes a copy only when it added an appearance or drew a state, as the app does.
Signed-off-by: Chiranjeevi U <244287281+chiranjeevi-max@users.noreply.github.com>
* M9: the verdict mark stays on the page
- A widget past the crop box takes its mark in the part on the page, or none when too little of it is there.
Signed-off-by: Chiranjeevi U <244287281+chiranjeevi-max@users.noreply.github.com>
* M9: typed values stay drawn while a signature redraws every page
- A form edit that changes a signature's state puts its values back in unbaked after the pages are invalidated.
Signed-off-by: Chiranjeevi U <244287281+chiranjeevi-max@users.noreply.github.com>
* M9: the action, Save As and shading tests check their effects
- The signed action and Save As tests reopen the output for the title and the comment; the shading test checks its clip.
Signed-off-by: Chiranjeevi U <244287281+chiranjeevi-max@users.noreply.github.com>
* M9: values typed earlier stay drawn while a signature redraws every page
- A form edit that changes a signature's state keeps every value still waiting on its page, not only its own.
Signed-off-by: Chiranjeevi U <244287281+chiranjeevi-max@users.noreply.github.com>
* M9: comment and content edits keep typed values drawn too
- An edit to one page's comments or content that changes a signature's state moves no page, so the values still waiting on their pages stay pending, as after a form edit.
- The pending-value test runs a field deletion and a comment after a fill, under a certification that allows the fill; its messages say what it checks.
Signed-off-by: Chiranjeevi U <244287281+chiranjeevi-max@users.noreply.github.com>
* M9: the language-change test skips without catalogs
- Like the other language tests, it returns early when the build has no craft-i18n catalogs.
Signed-off-by: Chiranjeevi U <244287281+chiranjeevi-max@users.noreply.github.com>
* M9: signed fields behind kids without a name count
- A kid with no /T but kids of its own is walked as a field node, so a signed field under it is listed by its full name.
- An unnamed kid claiming a signature its field doesn't have cuts the walk short, so the document stays signed and keeps its unread-fields row.
Signed-off-by: Chiranjeevi U <244287281+chiranjeevi-max@users.noreply.github.com>
---------
Signed-off-by: Chiranjeevi U <244287281+chiranjeevi-max@users.noreply.github.com>
* Add dry-run preview to doc_batch without writing outputs
Users cannot preview a batch before it writes files, so they cannot check the planned steps and output names for a folder first.
Add a dry_run option to doc_batch that reuses discovery, returns each file's ordered steps and planned output name, and skips processing and publishing.
Test: doc_batch_dry_run_lists_outputs_without_writing failed before the change and passes after.
* Address review for pull request 6486
---------
Co-authored-by: eliets2 <287273517+eliets2@users.noreply.github.com>
Marking a tag as an artifact, or a figure as decorative, refused content after a stream damaged
partway, placing each operator by its first operand. A page may split its content between an
operator's operands and its keyword. When a `BDC`'s operands end the stream before one damaged
from its start, or one that doesn't decode, the keyword the page reads is in what wasn't read;
joined without it, those operands and the next tag's read as one `BDC`, and marking rewrote the
stream after the hidden one, the next tag's `BDC` taken out of it.
An operator now counts after damage by its keyword, and one whose operands and keyword lie on
either side of a stream not read whole is refused: that stream may end them itself.
`a_tags_operands_before_a_stream_not_read_whole_are_not_rewritten` (damaged from its start, and
not decoding). A tag entirely after a stream that doesn't decode is still marked: such a stream
holds all of its content, between tokens. `accessibility_mark_tag_artifact`'s description names
content a stream that doesn't decode may hide or change too, not only a damaged one.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01AmsdeUCdTaD72jkDw2pJKY
A text object can run on from one content stream into the next. When it is still open at a
stream damaged partway, or one that doesn't decode, the text that stream shows starts where the
text before it ends: editing that text moved what the hidden stream shows, its advance changed.
An edit in the text object before such a stream went ahead, as edits before the damage do.
`rewritten_contents` now refuses a change from the start of a text object still open at the
first stream not read whole (`changes_text_open_into_damage`); a change before that text object
goes on. `text_a_stream_not_read_whole_goes_on_from_is_not_edited` (the hidden stream damaged
from its start and one that doesn't decode, between the text and a stream closing it or last on
the page; a line in an earlier text object of the same stream is still edited).
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01AmsdeUCdTaD72jkDw2pJKY
Moving or resizing an image in a stream before one damaged partway, or one that doesn't decode,
goes on, as the edit changes no stream past it. But the image's `q cm … Q` can close past that
stream, and what the stream draws inside the scope is unknown. When the image looked alone there,
the move changed the scope's `cm` in place, and anything the stream draws in it moved too: a
square drawn there in the image's `cm` moved with the image, as on `main`.
A scope still open after such a stream now counts as one that paints something else
(`first_op_after_damage`, read by `image_scopes` and `enclosing_transform`). An operator counts
by its keyword there: a page may split its content between an operator's operands and its
keyword, so a `Q` whose operands come before the hidden stream still closes after it. The move
gives the `Do` a `cm` of its own and leaves the scope's as it was; with a clip on the image, the
clip stays, as one the stream may share. A crop goes around the `Do` alone rather than at the
scope's start, where it would clip what the stream draws. A crop PdfCraft put in such a scope
isn't removed, and the refusal says why rather than that there is no crop. Arrange is
unchanged: it takes out the scope it finds, which changes the stream past it and is refused.
`an_image_whose_scope_closes_past_a_stream_not_read_whole_moves_alone`: a stream that doesn't
decode and one damaged, each also with the square's operands before what is hidden (ending the
stream before, or in the damaged stream before its damage); the image with no clip, a clip, and a
PdfCraft crop; moved, cropped, and its crop removed. A scope that closes in a damaged stream
before its damage still takes the move in its `cm`.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01AmsdeUCdTaD72jkDw2pJKY
Main now reports unreadable objects from the parse of every object in
Document::reconstruct, so the pre-check that skipped that parse is gone.
The single-pass /XRef positions and the rarest-byte find stay.
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Pruning reads forms 8 deep. Past that a form's /XObject dictionary was kept whole, but the forms
it draws weren't read, nor what they draw through resources of their own. When those resources
are shared with content that was read, they were cut to the names that content draws, and the
images only the deep forms draw were dropped from the part. This was so on `main` for a form read
whole, and in this change's reading of a damaged form too.
A walk the depth limit stops now leaves the file unpruned, as an overlarge Type 3 resource graph
already does: `nothing_is_pruned_past_the_depth_limit` (a form 9 deep, whole or damaged partway,
drawing a form that draws through resources the second page shares). That also settles a form
first reached past the limit and drawn again higher up, whose second reading `forms_seen` skips.
Down to the limit pruning goes on, under a page whole or damaged partway and with the form 8 deep
whole or damaged: `pruning_goes_on_down_to_the_depth_limit`.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01AmsdeUCdTaD72jkDw2pJKY
An edit was refused after a content stream damaged partway, since what the damage hides can
change how the later streams read, the graphics state they draw in among it. A stream that
doesn't decode at all hides all of its operators, and an edit after one went ahead. It now stops
there too: text lines and blocks, and images (arrange, crop, transform, move), as selecting
objects on `main` already does for a page with such a stream. An edit in a stream before it goes
on, and the stream is kept as stored.
A file may give a stream's /Filter by reference, which isn't read here, so the case needs no
damage: `content_after_a_stream_that_does_not_decode_is_not_edited` has such a stream move the
text and image after it with a `cm`. `a_content_stream_that_does_not_decode_is_kept_and_edited_around`
becomes `..._kept_and_edited_before`, its edit now in the stream before.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01AmsdeUCdTaD72jkDw2pJKY
Below about 100 pt of content width the stacked message wrapped to a column
thousands of points tall and pushed the hotbar off screen, which broke the
narrow-viewport hotbar picker test merged from main. That width is under the
smallest supported window, so the single row stays there.
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Only a non-empty signature value counts when deciding whether the first real
signature may drop /NeedAppearances.
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
The print, save and info buttons each have 4 pt after them (three gaps), so the
reserved width is 116 pt, not 112; the search field no longer overlaps the tabs.
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
A rung equal to settings already run is not optimized again, and attempts counts
only runs. The target test uses a TempDir so it cleans up even when it fails.
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
i32::MIN is outside Annex C's integer range and stays a real token (-2147483648.).
The generator now stops at -i32::MAX and a separate test pins the i32::MIN output.
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
full_save_order retained up to 64 MiB of parsed objects for the write pass, which
reachable_objects then dropped unused. It now asks for no retention.
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Overlapping unterminated dictionaries could each be read up to 1 MiB. A shared
budget (the file length) is now taken off on every path; a spent budget keeps the
object a candidate, as before.
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
The renderer (hayro-syntax (44)) counted the sections its chain reached, and
a hybrid section's /XRefStm stream (ISO 32000-2 §7.5.8.4) counted as one, so
a hybrid update without /Prev was rebuilt by cos but not by the renderer. It
now asks what cos asks: whether the newest section names an earlier one
through /Prev. The /XRefStm stream belongs to the same update.
Tests: a synthetic hybrid fixture (pdfcraft-testkit), with and without
/Prev, in cos and render.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Pq7HyCHD1R3rywQBPCHXZA
An update whose cross-reference section has no /Prev (ISO 32000-2 §7.5.6,
§7.5.8.2) cuts the earlier revisions, and the page tree with them, off the
chain. The signed PDFBOX-2720 and PDFBOX-2942 files have this shape: the
document model found no page tree and the renderer showed 2 blank pages
found by scanning for page dictionaries.
When the chain reaches only the newest section, the file holds another
startxref and the catalog's /Pages can't be read, both readers now rebuild
the table from the file's objects, as for a damaged file:
- cos: the existing reconstruction (newest in-file definition wins), a
repair-log line saying why, and `Document::rebuilt_unlinked_update`.
- hayro-syntax (44): `root_xref` repairs the table before the page tree is
read; `Pdf::rebuilt_unlinked_update` says so.
- render: an open warning, and the lazy structure reader accepts such a
rebuilt file, so links, fields and bookmarks are read too.
A save rewrites the file in full (main's rule for reconstructed files).
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Pq7HyCHD1R3rywQBPCHXZA
The redaction proof now reads a font's /ToUnicode with its /DecodeParms given by reference, and
`Metrics::from_dict`, which redaction takes the removed text from, read it only with direct ones.
Behind a predictor so described, the two got different text for the same codes, so a copy the
redaction rightly left counted as a survivor and the proof failed a sound redaction
(`a_unicode_map_behind_parameters_given_by_reference_is_read_alike`).
Every stream `from_dict` reads now goes through `Document::with_direct_parms`: the /ToUnicode; a
composite font's /Encoding, which sets how its codes split
(`cmap_streams_read_their_parameters_given_by_reference`); a subset's program and /CIDToGIDMap,
which say which of its CIDs have a glyph
(`a_subsets_program_and_cid_map_read_their_parameters_given_by_reference`); and the program a
simple font without /Widths is measured with
(`a_font_without_widths_measures_with_its_embedded_program`). Read without them, as on `main`,
such a font was measured with widths guessed from its name, so redaction removed glyphs the mark
doesn't cover and left some it does, and the proof, which also guesses from the name, passed.
Redaction now measures such a font with its program. The proof still guesses, so it fails that
redaction, as it already does on `main` when the program's parameters are direct.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01AmsdeUCdTaD72jkDw2pJKY
The proof that came with #5557 decodes every stream it sweeps through `Ctx::decode`, which called
`decode_strict` with the stream alone. This PR's `decode_strict` also takes the document, to read
`/DecodeParms` given by reference, so the branch merged with main no longer built. `Ctx::decode`
now takes the document, and each of its callers passes the one it reads, so the proof reads such
a stream as redaction reads page content and forms. Before, it decoded the stream with the
parameters' defaults: a PNG predictor's rows kept their tag bytes, and a copy of the removed text
split by them went unreported (`a_survivor_behind_parameters_given_by_reference_is_found`). The
test helpers' streams are direct, so they pass an empty document.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01AmsdeUCdTaD72jkDw2pJKY
Since autotagging reads page content through DecodeBudget::decode_strict, the reason it gave for
content that doesn't decode carried that error's own text, so it said twice that the data
couldn't be decoded: "content cannot be decoded strictly: stream data could not be decoded:
FlateDecode: …". It now gives the decoder's reason right after, as main does: "content cannot
be decoded strictly: FlateDecode: …".
Test: autotag_text_says_why_it_cannot_read_a_page now checks the decoder's reason follows the
prefix.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01AmsdeUCdTaD72jkDw2pJKY