Conflict in crates/content/src/lib.rs: main ends inline images where the
renderer's tokenizer ends them and counts the images whose end is a guess
(Parsed::ambiguous_images); this branch reads operators through the
streaming Ops iterator that parse collects. The iterator's BI branch takes
main's inline_image result and keeps the ambiguous-image count, which parse
copies into Parsed and Ops::ambiguous_images reports.
A section naming an /XRefStm that a newer section had already read
skipped the stream entirely, so an entry in its own table that
disagreed with the stream was no longer recorded in xref_oddities.
Signature validation reads those records to refuse an appended section
another reader could follow. The reader keeps each hybrid stream's
entries (up to MAX_XREF_OBJECTS in all) and compares every later
naming section's table with them, without decoding the stream again.
#4114 moves the bodies of Stream::decoded_within and decoded_strict into
decoded_within_typed and decoded_strict_within_typed, which return
FilterError so DecodeBudget can tell a spent limit from corrupt data.
This branch checks /Filter with readable_filters there. Kept #4114's
typed functions and made readable_filters return
FilterError::Unsupported, so a /Filter that is not a name fails both
typed decodes without charging the budget; decoded_first_filter_strict
maps it to CosError as before.
From review of #4096: when a secured form's scripts were turned off, the
bar shows that message instead of the security one, but still offered
"Don't show again", which would turn off a notice the user had not seen.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The notice that a document is secured came back on every open; Dismiss
lasted only for that view. Its new "Don't show again" turns it off in
every document, kept in the settings as show_secured_notice and turned
back on in Preferences (or --secured-notice). Only the notice changes:
the restrictions, the encryption and Document Properties > Security stay
as they are. The two labels are translated in all 33 catalogues, whose
ATTRIBUTION hashes and provenance notes are refreshed.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The text layout cut each embedded value to the bytes its block had left, but
the resolver cloned the whole value first. With a byte or two left and a
value starting with a multibyte character, every later run cloned the full
value and kept none of it. The resolver is given the bytes left and returns
only a prefix that fits.
set_value charged the snapshot's copy, changed the field, then charged the
effect's copy in push. When only the first fit, the effect was dropped while
later reads in the script saw the new value. Both copies are charged at once
before the field changes.
Reviewers flatten their comments and Fill & Sign signature once done, so
nothing on the signed or reviewed pages moves any more. The command was
only in All tools and the palette; it now also ends the page's
right-click menu (with or without a comment selected, while the document
has comments) and the Comments panel's More options menu.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
A positioned root draws its children instead of flowing them, so nested
repeating subforms under it multiplied their instances without spending
the measurement budget that flowed instances spend. Drawing a subform
instance spends it too.
The evidence caps counted repeated references to one stream, so a /DSS
array could list one benign stream 1024 times ahead of a revoking CRL,
and the check never reached it. Repeats are skipped before the entry
cap counts anything, and when a cap still stops an array being read in
full without a revocation found, the signature's status is unknown
rather than valid, with the reason in its details.
Reaching the 64 MiB comment budget ended the whole annotation scan, so
links and file attachments listed after it were missing, and a later
JavaScript link left the document unmarked. Past the budget, comments
are no longer built, and the scan goes on for links and attachments.
An address of invalid UTF-8 grows up to three times when converted, so
the listing's 16 MiB cap counted fewer bytes than it kept. The cap now
counts the converted text; the raw length is still checked first, so an
address already over the cap is never converted.
Review follow-up: an indirect /Annots array that drops a signed
annotation while gaining a signature widget was judged by the widget
alone, so a form-fill certification let a signed comment be deleted.
A removed comment is now a comment change, and a removed widget is a
change no permission covers.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Redaction reads page content and form XObjects strictly (upstream) and
within one page's decode budget (this branch): both now go through
DecodeBudget::decode_strict.
Filtered inline image data has no length, so its end is an EI chosen
among candidates, and the content parser chose by a different rule
than the renderer's tokenizer (hayro-syntax): the renderer also takes an
EI with no whitespace before it, and checks a candidate by reading what
follows as operators. With image data such as "7FEI 1 0 0 1 200 0 cm",
the renderer ran a cm that redaction read as image data, so text was
drawn 200 pt from where removal and its verify pass looked.
The parser now takes each inline image's end from the renderer's
tokenizer (already in the tree through pdfcraft-cos), so both read the
same operators after the image; a test runs both on the same fixtures.
Only where that tokenizer reads no image (no EI it accepts, or
parameters it can't read) does the parser fall back to its own rules,
counting the image in Parsed::ambiguous_images, and applying redactions
refuses such a page as unreadable.
Structure cleanup cleared alternate text, actual text and expansions
only on the element that directly owned changed marked content, and an
element that lost an annotation reference kept its descriptions unless
it lost every kid. An ancestor's /ActualText covering a redacted span,
or a link element's /Alt repeating the address of a removed link, stayed
in the saved file. Elements that lose an annotation now always lose
their descriptions, and every ancestor of an affected element (found
through both /P and the tree's /K, which may disagree) loses its own.
An element written in place above one with a description to clear
refuses the operation, as other in-place cases already do.
Finding which marked content a redaction changed copied every operator
into the buffer of each MCID enclosing it, so content nested n deep was
held n times: a modest stream of nested BDCs could exhaust memory.
Each operator inside marked content is now serialized once, and each
MCID keeps the ranges its sequences span. The bytes compared are capped;
MCIDs past the cap count as changed, so their descriptions are cleared
rather than kept.
Clearing the pixels of a partly covered image decoded its stream with
the global 1 GiB allowance and only then cut it to the raster length its
Width, Height and bit depth declare. A small Flate stream could make
every such image allocate and inflate far more than it can use. Decoding
now stops at twice the raster plus a byte per row (filter-chain and
predictor overhead) and a little slack; an image past that is removed
whole, as an undecodable one already is.
After applying redactions, removed images and forms were unlinked from
page-tree resource dictionaries but not from the resources of forms the
page still draws. A form sharing the page's original resource dictionary
(a common producer layout) kept the removed image reachable, so a full
save wrote it. The walk over surviving forms now drops XObject entries
for removed objects that the form's own content does not draw, and fails
closed when that cannot be determined.
The redaction interpreter started every form XObject with an empty text
state (no font, size 0, no spacing) although a form inherits its caller's
graphics state, and it ignored the /Font entry of an ExtGState set by gs.
Text shown that way was placed at the wrong positions, so removal and
the verify pass could both miss glyphs under a mark. Forms now start
from the caller's state, and gs sets the font and size as Tf does.
The engine's renderer cross-check test used the gs case as its example
of interpreter/renderer disagreement; it now runs the check directly on
an unredacted page, which also covers an input stream labelled as a
redaction overlay.
Both checks for text left under applied redaction marks (redaction's own
verify pass and the engine's renderer cross-check) skipped any page
content stream whose dictionary said /PCMark /Redaction, so that the
overlay's label would not count. An input file could put that label on
its own content stream and keep it out of both checks. Applying
redactions now records the overlay streams it adds (stamp returns the
new stream), and only those are skipped.
Deleting a field took it off its pages and out of the field tree but left
its entry in the form's /CO array. That reference kept the field
dictionary, its value and its widgets reachable, so a full save after
applying redactions over the field still wrote the redacted value.
PAdES LTV writers add the /DSS and, in the same revision, declare the
ESIC developer extension in the catalog's /Extensions (ISO 32000-2
§7.12). Any catalog change outside a short key list was "document
structure", so the signature such an update protects showed as invalid.
/Extensions may now gain declarations after signing; one that was
signed may still not change or go.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
A page whose /Annots is an object of its own keeps its dictionary when a
later revision adds a signature widget or a comment: only the array
changes. That array fell through to "other changes", so countersigning
such a document (as many signing services do) turned every earlier
signature invalid, including PdfCraft's own second signature.
A changed array that was a signed page's /Annots is now classified like
an inline /Annots: by the annotations it gained (signature, form fill or
comments), under the same DocMDP rules.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
A run of images that is not one picture was searched again from each of
its groups, collecting the rest of the run every time, so many images drawn
over one another made a page quadratic. The search reads at most 16 groups
per content operator (at least 65 536) and leaves the rest of the page as
it is once that is spent.
The pixel reader capped the image size the dictionary gives, then decoded
the JPEG at the size its header gives and compared afterwards, so a small
image dictionary over a huge JPEG allocated the huge one first. The reader
takes the size from the JPEG header and refuses a mismatch before decoding.
The body-size tally and the RTF colour table searched every slot seen so
far for each block, and the RTF writer searched the colour table again for
every paragraph and cell, so a document with many distinct sizes or colours
made export quadratic. Both keep their first-seen order and look slots up
in a hash map.