embedded_sfnt decoded each program with a fresh 16 MiB allowance and
charged nothing when the program turned out unusable, so a page whose
font resources all named one bad program inflated it once per alias.
embedded_sfnt_within charges every decode to a caller's DecodeBudget.
Callers with a tighter limit for one stream (an embedded font program)
need it alongside the shared allowance. A decode that runs past that
bound charges the bound, the work it took, so repeated failures still
spend the allowance.
The editor's page readers decoded every /Contents entry and every form
entry with a fresh 1 GiB allowance, and the reading-order image
inventory added a record per image draw in every form entered, so a few
shared forms drawing each other produced millions of records. Content
streams and form entries are decoded against one per-page DecodeBudget
(content past it is an error, so no edit rewrites a page from part of
its content; forms past it are reported truncated), and an inventory
past 100,000 image draws is refused.
The cover walk decoded every /Contents entry and every form draw with
a fresh 1 GiB allowance; the operator bound does not count whitespace,
so a large blank form drawn many times meant hours of decoding. The
walk charges a per-page DecodeBudget with the strict decoder, and
spending it fails the audit.
The interpreter entered a form again at every Do with only a nesting
limit, so a few shared forms drawing each other produced exponential
work in apply, sanitize and verify. Scope carries a per-page
DecodeBudget for form entries and form bytes; past it, apply refuses
the page and verification counts the form as possible residue. Page
content streams are decoded against a page budget too.
Each field action and document-script entry decoded its /JS stream
with a fresh 1 GiB allowance and kept its own copy, so many fields or
name-tree pairs naming one compressed stream retained gigabytes. A
reading of the fields, one field's actions, and the document scripts
each share a 64 MiB DecodeBudget for their script streams.
Pruning and page_as_form joined every /Contents entry with a fresh
1 GiB allowance each, so a page naming one compressed stream many
times became gigabytes. Both share one helper that decodes against a
per-page DecodeBudget and fails when it runs out.
Printing and the label text-direction check decoded every /Contents
entry with a fresh 1 GiB allowance, and the direction check re-entered
forms at every Do with only a depth limit, so a few hundred bytes of
nested repeated draws meant hundreds of thousands of form walks. Both
charge a per-page DecodeBudget: printing refuses a page whose content
runs past it, and the direction check stops reading there.
Image measurement and fragment merging decoded every /Contents entry
and every form occurrence with a fresh 1 GiB allowance, so repeated
references to one compressed stream multiplied memory. Both charge a
per-page DecodeBudget. A page whose content does not decode within it
is left unmerged, and its images are not resampled, since a use may be
missed; fragment merging no longer rewrites a page from content it
could not fully decode.
The checker decoded every /Contents entry and every form a page drew
with a fresh 1 GiB allowance each, so a page repeating one compressed
stream reference produced gigabytes. The two copies of the page-content
helper are one now, and it and the form walk charge a per-page
DecodeBudget.
MAX_DECODED bounds one decode, so a page whose /Contents repeats a
reference to one compressed stream, or a form drawn many times, gets a
fresh 1 GiB allowance per occurrence. DecodeBudget is one cumulative
decoded-byte allowance plus a form-entry count and a seen-set of stream
references, shared by a whole page walk. A decode that runs past the
remaining allowance spends all of it.
The audit returned the text under each cover whatever the document's
permissions, and the CLI and the automation tool printed it. A user
whose password withholds copying could read through the audit the text
that text extraction refuses them. The audit now reports every finding
for such a document with empty covered text; the owner password lifts
the restriction as it does elsewhere.
Every cover was compared with every text line of its page, and each
finding kept its own copy of the text it covered. Both grow with the
square of the content: a few megabytes of covers over one long line
meant hours of comparisons and gigabytes of report. The audit now
counts comparisons per page and covered-text bytes across the document
and fails with an error past either bound, never with a partial
report.
A text line joins compatible runs on one baseline, and the audit took
the line's last painting operator as its paint position. Text drawn
after a cover on the same baseline joined the covered words and moved
the line past the cover, so the covered words went unreported. The
audit now compares against the line's first painting operator: any
run painted before the cover is checked, and runs painted after it
in the same low-contrast color are unreadable on it too.
The annotation scan and the form XObject walk read /Subtype only as a
direct name. A /Subtype stored as a reference to the name read as
none, so a /Redact annotation or a covering form named that way was
skipped as an unrelated object and the page reported clean. Both now
follow the reference, and a reference that cannot be loaded fails the
audit.
The cover walk cleared the current path when it restored the graphics
state, but the current path is not part of the graphics state. A
rectangle built before Q and filled after it paints a box over the
text, and the audit missed it and reported the page clean.
Replaces the English placeholders added during the #2210 integration
(calculation order, text formatting, print, thumbnails, layers, comment
reshape and signature checks, plus the two signature wordings from
3e180944) with Finnish, and updates the fi.tsv sha256 and the entry
count in docs/localization.md.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
With a comment selected, the arrow keys scrolled the view (↑ ↓) or turned the page (← →), so
a text box, rectangle or line could only be placed with the mouse. Now an arrow key moves the
selected comment a screen pixel, and ⌘/Ctrl or Shift and an arrow ten, as Edit text nudges
objects; up is up on screen whatever the zoom, the page's rotation and the view's. Each press
is one undo step (Edit::MoveAnnotation). The keys are taken before the view's navigation,
which has them back once nothing is selected. Text markup stays with its text, and a locked
comment where it is.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
A paragraph selected by its frame (or objects taken by an area selection) answered the
arrow keys and Escape but not Delete, so the only way to remove text was one typed
character at a time in the inline editor. Delete or Backspace now removes the whole
selection as one undo step: paragraphs and images last-first in their inventories, added
content after them, under the existing "Delete content" history label.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Review of #3849: both tests asserted `stderr.contains("No such file")`, which
is the Unix wording. On Windows the same error reads "The system cannot find
the file specified. (os error 2)", so `test (windows-latest)` failed on them.
Neither test is about the wording. What they are about:
- `a_flag_that_takes_no_value_does_not_swallow_the_file`: the error is about
`missing.pdf`, and is not "check-one: missing file" -- the file reached the
command instead of being eaten as `--edit`'s operand.
- `a_file_whose_name_starts_with_dashes_is_still_reachable`: the error names the
file the command could not open (`missing.pdf`, or `./--dash.pdf` for the
`info` case), and is not a usage error about a value or an unknown option.
Both now assert exactly that, plus the absence of our own usage phrases. Every
string they match is one this CLI prints, so the OS has no say in it.
A timestamp counted as trusted time whenever its signer's certificate
chained to a trusted certificate, whatever the certificate was issued
for. Any key from a trusted CA could then vouch for an arbitrary time.
Trusted time, for a document timestamp and for a signature's embedded
one, now also needs a certificate whose extended key usage includes
timeStamping, whose key usage allows signing, and which has no critical
extension PdfCraft doesn't process (RFC 3161 §2.3). The test timestamp
authority gets such a certificate.
Chain building accepted an intermediate CA without looking at its critical
extensions, which were checked only on the signer's certificate. A
subordinate CA limited by critical name constraints could vouch for a
signer outside its namespace, because PdfCraft does not enforce those
constraints. A CA certificate (other than a self-signed root, whose
limits are the trust store's business) with a critical extension
PdfCraft doesn't process is refused as an issuer, and the reason is
reported.
Revision checks compared objects only. A later revision that pointed the
trailer at another catalog already present in the signed file changed no
object, so the signature stayed valid while the reader showed a different
document. A /Root that differs from the signed revision's is now a
disallowed change to the document structure.
An OCSP response's signer counted as the issuer itself whenever its
certificate carried the issuer's subject name. A response naming its
responder by key hash could embed a certificate for any key with the
issuer's name copied in, skip the delegation checks, and mark a valid
certificate revoked. Whether the responder is the issuer now follows from
how it was selected (the issuer's own name or key hash); every embedded
certificate must be signed by the issuer and carry the OCSP-signing
purpose.
The example passed a constant seed to set_encryption. AES-256 derives the
file key from the seed alone, so anyone could rebuild the key and read a
file it protected without either password. The seed now comes from the
operating system's randomness through RandomState, as in the engine.
The qpdf oracle test wrote into temp_dir()/pdfcraft-enc-<pid>, reusing the
directory if it existed and following any symlink inside it, so another
local account could prepare it and have the test overwrite a file of the
developer's. The directory now has an unpredictable name, is created by
the test (mode 0700 on Unix) and the files are created new.
A stream that named its own crypt filter (/Filter /Crypt with
/DecodeParms /Name) kept that name when the document was given new
password protection. The new /Encrypt defines only /StdCF, so the
inherited name resolved to /Identity and the stream was written in
clear into a file the user asked to protect. When saving with protection
applied by set_encryption, a selector the new handler does not define is
removed with its parameters and the stream follows the new policy.
Saving with the original handler keeps the source's selectors.
Saving with encryption left a dictionary's /Contents in clear whenever it
had a string /Contents and an array /ByteRange, whatever else it was. A
source file could add a /ByteRange to an ordinary annotation and its
comment text was then written unencrypted. A dictionary with a /Subtype or
a /Type other than Sig or DocTimeStamp is never a signature dictionary, so
its /Contents is encrypted like any other string.
The tab caches the audit per document and edit generation. Pin both
halves: frames with the file unchanged keep the same rows, and an edit
made while the tab is open (a new bookmark) shows up on the next frame.
With the cache keyed on the document alone, the test fails.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Extract pages only took the selected pages. Its dialog now has a Pages
field for ranges such as "1-5, 8, 11-13", read like Combine files' and
Print's ranges, in the order typed and each page once. Left empty (as
the dialog opens), it takes the selection as before, shown as the hint.
A range it can't use says why and turns Extract off; Extract and delete
removes the typed pages.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Conflict in parity/acrobat-features.toml: main moved the integration
tests into tests/it/, so the evidence paths take main's side and this
branch's two dialog_chrome tests are added on the new paths; the notes
keep this branch's wording. The custom-properties entry is now partial
through the tools, but the dialog still has no Custom tab, so the
"No Custom tab" note stays true.
Also fixes a break the merge could not show as a conflict: the cursor
test's form.pdf fixture is ../data/form.pdf from tests/it/.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
- ratio_text is derived from the scale fields (one function, sync_ratio_text):
on page seeding and after a hand edit of drawing points, real distance, unit
or page unit it shows the matching 1:N, or is emptied when the fields are not
a plain m/mm drawing scale. The "Common scales" highlight follows it.
- format_ratio no longer rounds (1:1.234, 1:0.001), so the field shows exactly
the ratio that applies.
- The smallest accepted ratio is 1:0.001, keeping the real distance within the
minimum of its field; set_ratio refuses out-of-range ratios too.
- "Scale", "Common scales", "Enter a scale such as 1:50." and a hint that
applying a scale switches units to m / mm are in every i18n catalog, with a
completeness test.
Co-Authored-By: Claude <noreply@anthropic.com>
The click-cycle test assumed a fresh install starts Light; it starts on
System now, so the cycle is System, Light, Dark.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
The theme preference defaults to System: dark on a dark desktop, light
where the desktop says nothing. Saved settings and --theme still win.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
The ArtCraft community card on Home gets a close button. Closing it hides
the card until the next launch; the choice is deliberately not remembered,
and the same links stay in the top bar's Discord button and the Help menu.
A UI regression covers closing and the fresh-launch default.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>