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.
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>
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.
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.
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 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>