22 Commits

Author SHA1 Message Date
Dylan M. Taylor dad86bf9f5 fix(redact,sign,cos): close high-severity findings from the agent scan
Redact: refuse Redact marks written in place; remove replies and pop-ups listed on
other pages; delete form fields by object, not name; resolve Do by its last operand
like the renderer; sanitize drops PCAdded and flattens widgets before hidden-content
cleanup. Sign: freed signed objects and edits to an already-signed widget are
modifications. Cos: only reading trusts /Type /XRef to skip encryption.
2026-10-23 10:11:10 -04:00
Dylan M. Taylor 93b25f67a6 Merge main into fix/coderabbit-c5-forms-misc
Keep the Ops iterator from this branch and carry main's long-number skip
(MAX_RETRIED_TOKEN) into its operand error path.
2026-10-22 22:07:19 -04:00
aiongg 923e499597 Merge upstream/main into fix/coderabbit-c5-forms-misc
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.
2026-10-22 20:51:14 +00:00
aiongg 60132bbfbf Merge upstream/main into fix/coderabbit-c4-core 2026-10-22 20:29:34 +00:00
aiongg 01aa6bcc17 docs(redact): describe the overlay, text state, description and inline image rules 2026-10-22 15:17:44 -04:00
aiongg b72078871a fix(content): end inline images where the renderer ends them
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.
2026-10-22 15:17:44 -04:00
aiongg 0a30b4e081 fix(preflight): read content colour one operator at a time
content_colour parsed each content stream into a full list of operators
before looking at any of them, so a small compressed stream of millions of
tiny operators became gigabytes of parsed operators in memory. The content
parser's loop is now an iterator (pdfcraft_content::ops), which parse
collects; preflight reads it one operator at a time and stops once gray,
RGB and CMYK are all found.
2026-10-22 17:57:53 +00:00
aiongg 07aea983d0 fix(content): splice copies only the pieces a range touches
Pieces::splice copied the unedited bytes between edits by visiting
every piece for every edit, so a page of many content pieces and many
replaced operators cost pieces x edits steps. The copy starts at the
piece holding its first byte and stops past its last one.
2026-10-22 17:43:03 +00:00
aiongg 3c64f9314e fix(content): skip a long unreadable number whole
When an operand failed to parse, the content parser went back to the
byte after its start and tried again, so a run of n bytes that never
reads as a number (a long run of '+', or digits ending in a letter) was
read again from every one of its bytes: n^2/2 bytes from a tiny
compressed stream. A failed number token longer than 64 bytes is now
skipped whole; shorter ones still recover byte by byte.
2026-10-22 17:43:03 +00:00
aiongg bf27724f06 Merge PR #3652: Keep filtered inline images whole when their data contains EI
crates/content/src/lib.rs: kept #3259's eod argument; image_data_end takes eod and passes it to both end_of_image calls; inline_image tries unfiltered, then declared /L, then image_data_end(filter_eod).
2026-10-22 01:45:07 +00:00
Ozt-y 9451e0652c fix(content): recognize ~>EI terminator for ASCII85 inline images and cover filter arrays 2026-10-20 18:57:43 +05:30
NetterSilver 07f3c97491 Keep filtered inline images whole when their data contains EI
Fixes #3579

Co-Authored-By: Claude Haiku 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-20 12:57:55 +00:00
Ozt-y 4945677717 fix(content): restrict '>EI' terminator detection to ASCIIHex filter 2026-10-18 19:32:09 +05:30
Ozt-y d1b0293a5d fix(content): accept inline image EI without preceding whitespace (#3108) 2026-10-18 19:05:46 +05:30
skylar taylor-barrick 392406eb4a fix(content): read a comment at the end of a content stream as the renderer does (#3081)
Pieces::join separated a page's content streams with a newline, which ended
a comment left open at the end of one stream. hayro, MuPDF and Poppler join
them with a space, so the comment runs on into the next stream and hides its
first line. Edit text, object_list, the redaction audit, redaction and the
Office/HTML export therefore saw text and boxes that nothing draws, and
redacting unrelated text on the page made the hidden content visible: the
rewritten stream dropped the comment.

- Pieces::join uses a space, so every reader of the joined streams agrees
  with rendering.
- The parser records, per piece, whether a top-level comment runs into it
  and out of it (boundaries only, not every comment). A redaction rewrite
  of a piece ends an incoming comment before its first operator and leaves
  an outgoing one open, so hidden content stays hidden.
- Reading for export parses the streams joined, instead of one by one.
- The redaction tag check compares the joined streams the same way.
- Pieces::piece_of uses a binary search.
2026-10-17 21:50:58 +02:00
Mex 95a972a3e7 M2: preserve known unfiltered inline-image sample boundaries (#1562)
Use checked sample lengths and row padding before the EI delimiter heuristic.
Cover valid binary terminator-like samples, masks, Indexed, aliases, invalid
layouts and saved classic/packed PDFs.

Co-authored-by: MMexicann <MMexicann@users.noreply.github.com>
Co-authored-by: Codex <noreply@openai.com>
Co-authored-by: Brandon Thomas <echelon@gmail.com>
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-15 03:20:10 -04:00
Marco Arnulfo d7590ebfbe fix(content): count an unreadable inline image with no EI as skipped (#1318) (#1418)
Follow-up to #1404. An inline image whose parameters can't be read and that has no EI after them ran to the end of the stream as a BI op with no data, and those bytes were not counted. It is now skipped like one with an EI, its bytes counted in Parsed::skipped. A stream that ends inside the parameters (BI /W 1) still keeps its BI op.
2026-10-12 08:38:01 -04:00
Marco Arnulfo 240ea53b8b fix(content): skip an inline image with an unreadable key to its EI (#1318) (#1404)
When a key in an inline image dictionary was not a name (`BI /W 4 H 4
ID ... EI`), the parser ended the image at the end of the stream and
counted nothing as skipped, so every operator after the image was lost:
text_lines found no text on a page whose text follows such an image.

An image whose parameters can't be read is now skipped up to the EI
that ends it, the skipped bytes are counted, and parsing resumes after
it. Without an EI the image still runs to the end of the stream, as
before.

Adds tests::an_unreadable_inline_image_is_skipped_to_its_end, which
fails before this change.
2026-10-12 07:53:44 -04:00
Brandon Thomas 72d640f9f0 Fix #155: edit text across content stream pieces (#628)
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>
2026-10-09 17:57:02 -04:00
Brandon Thomas d3db99818f Rename PrintCraft to PdfCraft (#174)
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>
2026-10-07 16:03:57 -04:00
Brandon Thomas 1434db626a Never crash: deny the panic lints in crates that are already clean
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>
2026-10-04 13:12:21 -04:00
Brandon Thomas f8175fb97e M8.5/M8.6/M8.7: Redact a PDF: new content crate (L2: content-stream operators with spans, inline images, serializer, matrices) and redact crate (L4): Redact annotations; apply removes glyphs (TJ displacement keeps the rest in place; simple, composite/CMap and Type 3 fonts), clears covered image pixels (or removes images it can't re-encode), removes or clips vectors and shadings, rewrites form XObjects copy-on-write, removes overlapping comments, links and fields, draws the boxes with overlay text, then verifies and fails closed; redacted documents save as a full rewrite. Search & Redact text and patterns (phone, e-mail, credit card, SSN, dates). UI: Redact tool (text or box), Redact pages, Find text and redact, Set properties, Clear all / Redact all with confirmation; redact_mark/redact_apply/redact_clear tools (57 tools). Fix: deleting a field when /AcroForm is a direct dictionary
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-02 00:24:29 -04:00