Rewriting a paragraph in one style split its text at white space and
joined the words with one space, so a run of spaces, an indent or a
trailing space typed in the editor was saved as one space or none. The
text is now kept as typed (other white space becomes a space, and CR or
CRLF become line breaks), and the wrapper keeps each run of spaces,
dropping only the spaces where a row wraps.
A justified (stretched) row still drops its indent and the spaces after
its last word, as before, so word spacing reaches only the spaces
between its words; keeping an indent on a justified row is a possible
follow-up. Centred and right-aligned rows, and the editor's draft box,
are measured to the last word, and a paragraph at 0 Tz is not justified.
Fixes#4199Fixes#3797
crates/organize/src/import.rs: upstream's name_tree_entries (seen-node set, each node read once) kept whole; this PR's only change to it, pub(crate) for attachments.rs, applied on top.
crates/organize/src/attachments.rs: its name_tree_entries call passes the new seen set.
crates/automation/tests/attachment_key.rs, crates/ui-egui/tests/attach_picker_target.rs: moved into tests/it/ and registered in main.rs, following upstream's one-binary layout (organize keeps its single test file, as upstream's rule says).
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Review on #3943. A black-box test of Acrobat (synthetic form, Save As and
Ctrl+S, every decoded object searched) showed how it saves a Password
field: the typed value is nowhere in the file; a field with /DV is saved
holding /DV, one without is left empty; the asterisks are drawn from the
saved value (5 for a 5-letter default, not 13 for the typed password);
/DV and /AA are kept.
clear_password_values now does the same: it restores the (inherited)
/DV as the value, or leaves the field empty, and redraws the widgets, so
the appearance no longer shows how long the typed password was. A field
already at its default no longer counts as holding a password, so such
a form needs no cleaned copy. The parity note for form.fill-text says so
and lists the two limits Acrobat shares (a script copying the password
into an ordinary field, and an earlier revision kept by an incremental
save).
Mutants, each failing a test: no /DV restore, no redraw, the default
ignored in password_fields, emptiness ignored (an empty string in an
array). Dropping the empty-default filter was equivalent, so the filter
is gone.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
CodeRabbit on #3943: sign, sign_with_timestamp, timestamp_document and dss::embed
each write a new incremental file from editor.cos, outside the writable() boundary
saves use, so a typed Password value was signed into the file. Each now writes from
the writable() copy. The new test signs a form with a typed value and checks the
signed file holds the masked appearance but not the value; the three TSA/DSS paths
need a timestamp server or validation data, and use the same one-line change.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
CodeRabbit on #3943: Extract Pages copies the fields of the pages it takes, and
write_new (Combine, Extract, Split, Create) wrote that copy without the writable()
boundary saves use, so the new file kept a typed Password value. write_unchecked, which
write_new and Split's size measuring both go through, now writes the sanitized copy. The
other write_full/write_incremental calls in the engine build in-memory bytes (display,
previews, the working copy, print rasters), not files.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
A text field with the Password flag kept its typed value in /V, so Save, a full
rewrite, Reduce File Size and the recovery snapshots all wrote it in plain text.
ISO 32000-2 §12.7.5.3, Table 231 (note to the Password flag): PDF processors
should never store the value in the PDF file. Each of those paths now writes a
copy without the values; the open document keeps them in memory, and the masked
appearance is kept.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The fixture's image stream held six bytes under /Length 4, and the test
only checked that the picture was missing at a zero budget, which a
picture that could not be prepared also passes. The stream is four
bytes, and the picture is first shown to export at a 1 MiB budget.
Description read Title, Author, Subject and Keywords only from /Info. A file
that states them only in its XMP packet (/Info is deprecated in PDF 2.0, and
some writers leave it out) showed empty fields, while Acrobat shows them.
organize::info now falls back to the packet's dc:title, dc:creator,
dc:description, pdf:Keywords, xmp:CreatorTool and pdf:Producer: a language
alternative gives its x-default item, and several authors are joined with
"; ". The engine fills the inspection's missing fields the same way, so the
read-only Description rows and doc_info agree with the editable fields.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
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.
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.
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.
Word assembly checked whether the whole word so far was underscores for
every glyph it added, so a long run of underscores on one line took
quadratic time. The underscore state of the current word is kept as it
grows.
Office export encoded and kept a separate copy of an image for every place
a page drew it, so a short content stream repeating Do on one large image
multiplied it into gigabytes before the writers' limits applied. Each image
object is now encoded once and its bytes shared (pdfcraft_export::Image
holds an Arc<[u8]>), distinct pictures share a 512 MiB budget whose
overflow lists the page as truncated, and RTF, which leaves pictures out,
prepares none.
Pages printed as pictures were drawn with everything on them, so form fields only
printed the page content, and the document with stamps printed every comment. The
pictures are drawn from a copy holding only what the vector print draws.
A step that applies an edit logged any error and carried on, which is right
for a flatten with nothing to flatten but not for removing hidden
information or sanitizing: on a signed file the removal was refused, and the
unchanged file was written out as the action's successful result. Those
steps now fail the action unless there was nothing to remove.
Printing checked the print permission but not the separate high-quality one
(Table 22, bit 12), so a document limited to low-resolution printing came out
as its original vector pages, text included. Such a document now always
prints as pictures of at most 150 dpi, whatever the settings ask for.
Compare read both documents' words out with no check of the copying
permission, and its result, report and difference comments carry that text,
so a copy-restricted document's text could be taken out through a comparison.
Both documents must now allow copying.
* Build each crate's integration tests as one binary
Every tests/*.rs file is its own executable that links the whole crate
graph. Move each crate's test files into tests/it/ as modules of one
tests/it/main.rs, for apps/pdfcraft, apps/pdfcraft-cli, automation, cos,
crypt, engine, render, sign and ui-egui. ui-egui's ui_group binary and
the support modules move into tests/it/ too. Crates with a single test
file keep it. The test executables go from 150 to 52, and every crate
runs the same number of tests as before.
ui-egui perf keeps its own binary (own process): its frame-time budgets
would charge each measurement for the other tests running beside it.
What the move needs:
- include_str!/include_bytes! and #[path] paths gain one "../".
- ui-egui: one GPU lock in tests/it/main.rs replaces the five per-file
ones (content, document_links, editing, fill_sign, view), so one
process creates one wgpu device at a time. The drop-line test in ui.rs
and the screenshot blocks behind PDFCRAFT_*_SHOT(S) and
CRAFT_UI_DOCKING_DIR, which render without holding it, take it too.
- pdfcraft winit_wayland_scroll: the vendored winit source it compiles
names crate::event, so that shim lives in main.rs.
- parity evidence names the new paths.
* Name the one-binary test paths in docs and comments
- READMEs, docs, ATTRIBUTION and source comments name tests/it/<file>.rs.
- The oracle README's example filters with --test it encryption::qpdf_agrees.
- pdfcraft's GPU lock comment points at the UI tests' shared lock in
crates/ui-egui/tests/it/main.rs.
With a fixed font size, a text field's appearance cuts a long value at
the edge of its box, but form_fill only answered "filled": n. A person
sees the cut; an agent filling forms through the CLI or MCP did not.
The text appearance builder now notes when the text it lays out does
not fit: one line wider than the box, or wrapped lines below it.
forms::appearance::value_overflow asks that for a field's widget,
Document::field_overflow for a field by name, and form_fill adds a
warning for each filled field that is cut off. The value is still saved
whole and drawn as before; auto-sized fields shrink to fit and are not
reported.
The automation tests' inline PDF builder moves to a shared pdf_of.
Fixes#4018
Conflicts:
- crates/ui-egui/src/panels.rs: main made `panel_header` pub(crate) with
`back: Option<&str>` (the Calculation order page uses it too); this branch
needs its leading button to be Expand on All tools. It now takes
`lead: Option<(&str, &str)>`, the button's icon and tooltip, and the three
callers pass theirs (calc_order_ui.rs included).
- i18n catalogues: both sides appended entries at the same place; kept both.
- ATTRIBUTION.toml: catalogue hashes recomputed.
Main added eleven catalogues since this branch was cut (ca, ckb, fi, hi, id,
ko, lo, nb, ro, sq, th), and their coverage tests require every UI literal, so
the eight All tools view strings are translated in them too.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Conflicts:
- crates/edit/src/lib.rs: keep both `backdrop` (this branch) and main's new
`background` module (#2992, page background colour for Export to Word).
- crates/engine/src/lib.rs, crates/ui-egui/src/marks_ui.rs: union of the
imports (main added BlockKey, block_overlapping and FontFamily).
- ATTRIBUTION.toml: catalogue hashes recomputed.
Main added eleven catalogues since this branch was cut (ca, ckb, fi, hi, id,
ko, lo, nb, ro, sq, th), and their coverage tests require every UI literal, so
the six Add Background backdrop messages are translated in them too.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Resolved by the keeper:
- ATTRIBUTION.toml: upstream's copy, hashes recomputed
- crates/organize/src/tests.rs: additions from both sides, upstream's first
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>