* fix(create): cap decoded size of imported PNGs
A PNG's declared dimensions sized an infallible allocation, so a few-byte file claiming huge dimensions aborted the process when the allocation failed. Cap the decoded size at the TIFF ceiling and reserve every input-sized buffer fallibly, returning CreateError::Image instead. (Security audit run-2: v3-024.)
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
* fix(xfa): bound total picture bytes kept during layout
Each laid-out picture kept its own copy, so one picture in a repeating subform multiplied without bound and could exhaust memory on open. Add a per-layout aggregate cap (256 MiB) that stops with XfaError::TooLarge. (Security audit run-2: v3-103.)
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
* fix(xfdf): bound XML nesting and protect redaction marks on import
Deeply nested XFDF/XML overflowed roxmltree's recursive parser and aborted the process; a non-recursive depth scan now rejects it before parsing. Imported comments could also replace a locked or redaction mark by /NM or add a hidden one; place() no longer replaces locked/Redact annotations and clears the hidden/invisible flags on imported redaction marks. (Security audit run-2: v3-105, v3-111.)
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
* fix(sign): classify post-signing changes by the signed revision's roles
A signed object's modification was classified from labels in the attacker-written new version, so relabelling a signed page /Type /Sig (or giving it /ModDate), or relabelling a drawn stream /ObjStm or /XRef, let a forged page validate as Valid even under DocMDP P=1. Decide an object's role (page content, signature, metadata, file plumbing) from what it was in the signed revision, not its new dictionary. (Security audit run-2: v3-082, v3-083.)
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
* fix(redact): remove redacted content from the saved file, fail closed otherwise
Apply Redactions and Remove Hidden Information left the originals behind: dropped or cleaned image/form XObjects kept their resource names (so the full rewrite saved the originals), an XFA form kept the redacted value in its datasets packet, and vertical-writing text under a mark was never hit by the horizontal removal geometry yet reported success. Prune replaced XObject names the page no longer draws, drop /XFA and /NeedsRendering when redacting an XFA form, and fail closed (RedactError::VerticalText) rather than silently leave vertical text. (Security audit run-2: v3-064, v3-067, v3-062.)
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
* fix(ui-egui): trust the displayed signer, not the document's embedded root
The Signatures panel's one-click 'Add to trusted certificates' trusted the top of the chain the document supplied, installing the author's self-signed root as an anchor instead of the signer shown. Trust the displayed signer certificate. (Security audit run-2: v3-098.)
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
* Use main's decoded-size ceiling for PNGs too; keep the depth tests on main's checker
---------
Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
Co-authored-by: Brandon Thomas <echelon@gmail.com>
The Email pattern of Search & Redact accepted only ASCII letters and
digits, so addresses such as "ayşe.yılmaz@örnek.com.tr", "müller@bücher.de"
or "иван@пример.рф" (RFC 6531 local parts, IDN domains) were not found
and stayed visible after a redaction by pattern.
Letters and digits of any script written with spaces now count in the
local part, the domain and the top-level domain. Scripts written without
spaces (Chinese, Japanese, Korean, Thai, …) still don't, so text that
runs straight into an address, as in "メールはtaro@example.jpまで", is not
swallowed into it. A domain still needs a dot and an alphabetic top-level
domain of two or more letters.
The Dates pattern of Search & Redact knew only English month names, so
"10 Ekim 2026", "3 Mayıs 2025" or "5. Oktober 2026" stayed visible after a
redaction by pattern. It also lowercased with str::to_lowercase, under
which "MAYIS" never equals "mayıs" and "EKİM" grows a combining dot.
Month names are now listed for English, Turkish, German, French and
Italian, with English and Turkish abbreviations ("Şub.", "Ağu"), and are
compared one character at a time with I, i, İ and ı folded to i, so any
case matches. A day may end in a full stop before the month name
("5. Oktober 2026"). Words that only begin like a month ("Martı",
"Ekimde", "Oktoberfest") and month names without a day and year still
don't match.
* M8: redact lists of words or phrases
Find text and redact gains "Multiple words or phrases": a list with one word
or phrase per line, typed or imported from a .txt file (Import list…), whose
every match is marked in one undo step. Lists are trimmed, deduplicated and
bounded (1,000 entries of 256 characters; files up to 1 MB are read).
redact_mark takes words and reports matched_words, the marks each entry made.
protect.redact-word-lists is shipped; whole-word and case-sensitive matching
and exporting the list are still missing.
* M8: translate word-list labels into Arabic
The Arabic catalog arrived on main after this branch, so the new Find
text and redact strings were still English and failed
arabic_covers_ui_literals.
* Tests: drop sharp grid renders when the thumbnail will cover the cell
Zooming the organize grid back out left the sharp page raster in place
until a thumbnail texture existed, so the zoom test failed when that
texture had not arrived yet.
* M8: Czech rows for the word-list redaction labels
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
---------
Co-authored-by: Brandon Thomas <echelon@gmail.com>
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
* M8: redaction code sets (U.S. FOIA, Privacy Act) as overlay text
Redaction properties offers "Custom text" or "Redaction code" under "Use overlay
text". Picking the U.S. FOIA (5 U.S.C. 552(b)) or U.S. Privacy Act (552a) set and
ticking codes makes new marks carry them as overlay text, in the set's order.
redact_mark takes code_set and codes and rejects unknown sets or codes with the
allowed list.
Also merges a duplicated evidence/notes pair in form.field-list that made
parity/acrobat-features.toml fail to parse on main.
* M8: say why the default redaction code set can't fail
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* Tests: pin the clock in form_bulk (saves a second apart differed in /ModDate on Windows CI)
---------
Co-authored-by: Brandon Thomas <echelon@gmail.com>
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
A page's /Contents array is one content stream in pieces that writers may
split between any two tokens (ISO 32000-2 §7.8.2). Edit text parsed each
piece on its own, so an operator whose operands ended one piece and whose
keyword started the next arrived with no operands: a `[(After) -20 (wards)]`
/ `TJ` split made the line vanish from text_lines, text_paragraphs and
text_edit, and from the Edit text tool.
The joined-parse logic the redaction interpreter used (#153) moves into
pdfcraft-content as `Pieces`: join the pieces, parse them as one stream,
map positions back to pieces, and splice joined byte ranges back into
per-piece bytes (an edit replacing or removing an operator that spans
pieces rewrites every piece it touches; untouched pieces keep their
objects). text_lines now interprets the joined operators, and replace_line
and rewrite_block splice through `Pieces`. Redaction uses the same helper.
Lines and paragraphs are grouped by the piece their first operator starts
in, as before, so pages whose pieces are separate drawings don't merge.
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
The Phone Numbers redaction pattern accepted only 10 digits (11 with a
country code), the North American form. For "+49 2151 123456" (12 digits)
the `+` candidate was rejected and the scan then matched "2151 123456",
leaving "+49" visible: a partial redaction in a document the user thinks
is clean.
The pattern now accepts, still grouped by at least one separator:
- E.164 numbers with a leading `+`: 8-15 digits in all;
- national numbers with a trunk 0 followed by 6-14 digits, whose first
group has at least two digits (so decimals don't match) and which are
not a day-first date (left to the date pattern);
- North American numbers as before.
A separator may now be followed by `(`, so "+49 (0) 2151 123456" is one
match instead of a partial "(0) 2151 123456".
Refs #525. The other items (page-number and date tokens, stamps) are not
addressed here.
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
* Redact: operators split across a page's content streams are redacted whole
A page's /Contents array is one content stream in pieces, and a writer
may split it between any two tokens (ISO 32000-2 §7.8.2). Some office
and layout exporters do: a TJ array ends one piece and its TJ operator
starts the next, or a marked-content dictionary closes in the next piece.
Redaction parsed each piece on its own, so such an operator was cut off
from its operands. Glyphs shown by a split TJ were never checked against
the marks: Apply drew the black box but left the text in the file, and
Verify reported nothing under the box. When the piece holding the
operands was rewritten for another mark, they were dropped, so the text
after it vanished and the next piece began with an operator without
operands, which other readers reject.
process() now parses the pieces joined, gives each operator back to the
piece its keyword is in, and rewrites every piece a split operator spans.
Pages whose pieces split only between operators are unchanged.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* Use the pdfcraft crate names after the rename
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
---------
Co-authored-by: EthanSuquet <292885762+EthanSuquet@users.noreply.github.com>
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Co-authored-by: Brandon Thomas <echelon@gmail.com>
The app, crates, binaries, bundle id, packaging, icons, screenshots and docs are now
PdfCraft / pdfcraft (repo storytold/pdfcraft). On first launch the desktop app moves the
PrintCraft settings and crash-recovery folders to the new name, and both apps read settings
saved under the old storage key, so upgrades keep recent files, preferences and unsaved work.
The signing test fixtures keep their PrintCraft subject names (they are binary).
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
annot, compare, content, cos, export, geom, model, ocr, preflight, print and redact have no
production `unwrap`/`expect`/`panic!`/`unreachable!`/`todo!`/`unimplemented!`. The attribute
keeps it that way.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Stable clippy 1.99 denies chunks_exact/chunks_exact_mut with a constant
size (32 sites), which failed the test job on main and every open PR.
Behaviour unchanged; MSRV 1.90 has as_chunks.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Text added with Edit ▸ Add content keeps its source string in the stream dictionary's /PCAdded.
Applying a redaction rewrote the stream's glyphs but copied the dictionary unchanged, so the
redacted text survived in the saved PDF. The redacted item now stays as plain drawn content and
is no longer editable.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>