A rubber stamp from another program with no normal appearance was not
drawn at all. The display copy now gives it one:
- the fourteen standard names of Table 184 draw that stamp with
PdfCraft's stamp drawing (eight from the palette, six with labels of
their own), fitted to /Rect, in /C or the stamp's colour;
- no /Name draws Draft, the table's default, unless /IT says the stamp
is an image or a snapshot;
- any other name (names match byte for byte, §7.3.5) draws a thin grey
frame with both diagonals, so the stamp stays visible and selectable.
The drawing is display-only, like the other comments without an
appearance: Save writes the file as it was, and build() still declines
these stamps, so restyling and flattening never write it into the file.
A stamp with its own /AP /N is never touched. stamp() now takes the
label and the sign-here flag instead of a StampKind.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0175FWj4xFhMhSH3UpMU8DSC
* test(automation): give sign_document a real digital ID and document in the hostile-arguments sweep
sign_id_create, earlier in the sweep, wrote a digital ID to the in.pdf
placeholder that sign_document then took as its id. Almost every
sign_document call failed to decrypt it and never reached the hostile
value under test, and the decrypts took most of the test's time.
Each tool now starts from an empty root, and sign_document gets a test
ID with its password and a fresh blank document per call. 370 of its
440 calls get past the ID (1 before) and 340 past the document (0
before); the sweep takes 7 s instead of 89 s.
* test(automation): empty the sweep's root through its canonical path
form_merge_data, given the hostile name " ", writes a file of that name
into the root through the canonical \\?\ path, where Windows takes it
literally. Emptying the root through the plain path dropped the trailing
space, so the entry named the root itself and removing it deleted the
root. Entries are now reached through the canonical path, their type is
read from the directory entry, and a failed removal is reported.
* test(automation): fail the sweep when the root can't be emptied
Listing, typing or removing an entry left in the root now fails the test
with the entry's path, so no tool runs against an earlier tool's files.
* M4: the Bookmarks panel shows each bookmark's colour and style (#3193)
ISO 32000-2 §12.3.3: an outline item's /C is "An array of three numbers
in the range 0.0 to 1.0, representing the components in the DeviceRGB
colour space of the colour that shall be used for the outline entry's
text" (Table 151), and its /F flags say italic (bit 1) and bold (bit 2)
(Table 152). Both were ignored: every title was drawn in the theme's
text colour and face.
The inspector (OutlineItem) and the bookmark list (organize::Bookmark,
used once a document is edited) now read them: each component is kept
within 0-1, and black (the default) or a /C that isn't three numbers
reads as no colour. The panel draws bold titles in the semibold face,
italic ones slanted, and the colour as given, lightened towards the text
colour only as far as it needs to read on the panel (3:1), so dark blue
stays visible in the dark theme. bookmark_list reports color, bold and
italic where they differ from the defaults.
Mutants, each failing a test: italic or bold bit ignored (both readers,
the panel), black kept as a colour, no clamping, a four-number /C
accepted, colour ignored or truncated in the panel, the contrast floor
removed, always jumping to the text colour. Checks for non-finite
numbers were equivalent (clamping already covers any size), so they
are gone.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* M4: set a bookmark's style and colour from its menu or bookmark_set_style (#3193)
#3193 asks for a way to mark bookmarks by colour, for example chapters
that are rarely used. ISO 32000-2 §12.3.3 gives every outline item a
title colour (/C, Table 151, DeviceRGB, default black) and style flags
(/F, Table 152: bit 1 italic, bit 2 bold, default 0); the previous
commit shows them, and this one lets the user change them.
- organize: set_bookmark_color writes /C, removing it for black (the
default) and refusing a component outside 0-1; set_bookmark_style
sets or clears the bold and italic bits, keeps any bit the table
doesn't define, and removes /F when no bit is left.
- engine: SetBookmarkColor and SetBookmarkStyle edits, undoable, under
the same assemble permission as the other bookmark edits. Their undo
labels are the menus' own names, already translated in every
catalogue.
- ui-egui: the bookmark menu gains Style (Bold and Italic check boxes)
and Colour (the comment swatch grid, with Other colour...). Only
existing UI strings are used.
- automation: bookmark_set_style takes any of color ([r, g, b], 0-1),
bold and italic, and changes only what it is given.
Mutants, each failing a test: no range check (library and tool), black
kept as /C, a bit not set, a bit not cleared, an untouched bit cleared,
/F 0 written, the bits swapped (library and menu), the tool accepting
nothing to change or ignoring bold/italic, the menu's colour ignored.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* M4: a colour and a style set in one bookmark_set_style call are one undo step (#3193)
Review on #4171 (CodeRabbit). Given both, the tool applied two edits,
so undo took back only the style and left the colour, and a failing
second edit left the first applied. They now go in one Edit::Batch
named "Style", all or nothing. "Colour" and "Style" join EDIT_LABELS,
and "Style" BATCH_LABELS, so the history can translate them.
The test sets colour and style together and undoes once; with the two
edits applied separately it fails.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
* M12: set a table's Summary and a header cell's Scope (#4103, #4104)
accessibility_set_tag_property takes `summary` (Table tags) and `scope`
(TH tags: Row, Column or Both). Both are Table attributes, written into
the tag's attribute object with /O /Table in /A: an existing one is
edited in place, keeping other attribute objects and their revision
numbers; one reached by reference is copied into the tag's own /A first,
so tags sharing it keep their value; a new one is added when there is
none. Clearing drops the key, an attribute object left with only /O,
and an empty /A. The tag type is checked after the role map.
accessibility_tags lists both. Setting a summary clears the
table-summary finding.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* M12: edit the Table attribute object that holds the value (#4103, #4104)
With more than one /O /Table attribute object in a tag's /A, setting a
summary or scope edited the first one even when a later one held the
key, and clearing left that later value in place (both still read back
as the old value). Setting now edits the object holding the key (else
the first Table object, else a new one), and clearing removes the key
from every Table object holding it.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
* M3: a Launch link that names a PDF opens it in PdfCraft (#4168)
Documents that cross-reference each other often use Launch actions to
open another PDF next to them (#4168: 268 links such as
..\se\se_ST.pdf). PdfCraft follows no Launch action, so clicking them
did nothing, while the same link written as a GoToR opens.
ISO 32000-2 §12.6.4.6: a Launch action "launches an application or
opens or prints a document". Starting programs is not something a
document should be able to do, but opening a PDF is what a remote
Go-To already does. So a Launch action whose /F names a .pdf file, and
that has no /Win launch parameters (application, operation,
arguments), is read as the remote Go-To to that file's first page
(LinkTarget::RemoteGoTo with launch: true). It then goes through the
same path as one: PDF only, --root confinement, confirmation for
absolute paths, opened as a document and never handed to the OS.
Windows writers separate such paths with \, where §7.11.2.1 uses /, so
a path without any / has its \ read as /. doc_info's link target says
"action": "Launch" or "GoToR".
Tests: launch_actions_that_name_a_pdf_open_it_like_a_remote_link
(relative with \, a file specification, an executable, /Win, a bare
.pdf, a mixed path, a URL file system) and
a_launch_link_to_a_pdf_opens_it_like_a_remote_link (link_open opens
the PDF in the root; a program link opens nothing). Mutants, each
failing: the /Win check dropped, \ always or never read as /, any
extension, a bare ".pdf", case-sensitive ".pdf", Launch not read.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* M3: a Launch link with a stray /D still opens its PDF (#4168)
Launch (ISO 32000-2 Table 207) has no /D, but launch_pdf went through
remote_go_to, which rejects the action when /D isn't a valid GoToR
destination. A Launch naming c.pdf with /D 0 stayed Other(Launch).
The /F parsing is now shared (action_file) and Launch ignores /D.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0175FWj4xFhMhSH3UpMU8DSC
---------
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
A note's /Name picks its icon, and ISO 32000-2 §12.5.6.4, Table 175 asks
every viewer to provide icons for seven standard names: Comment, Key,
Note, Help, NewParagraph, Paragraph, Insert ("Additional names may be
supported as well"). PdfCraft drew two shapes: Comment, and a page for
everything else. So choosing Key, Help, Insert, NewParagraph or
Paragraph in Properties changed nothing visible, and notes in received
files that use these names, or further ones such as Check or Star, all
looked like the same page. Recolouring such a note redrew it as a page.
note_icon now draws a shape of its own for each standard name and for
seven further names in common use (Check, Circle, Cross, CrossHairs,
Star, RightArrow, UpArrow). They are simple geometric drawings written
for PdfCraft, filled in the note's colour with a dark outline like the
existing two. NoteIcon gains the further names (NoteIcon::ALL); the
Properties chooser and comment_add offer all fourteen. Another name
still draws as Note, the table's default.
Tests: every_note_icon_draws_its_own_shape (all fourteen differ; an
unknown name and a missing one draw Note) and
a_star_note_keeps_its_star_when_recoloured. Mutants, each failing them:
from_name limited to the standard seven, Star or Key drawn as Note, an
unknown name drawn as Comment. One survivor reshaped Circle while
keeping it distinct, which the distinctness test rightly allows.
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
* M5: stamp_custom places a picture fitted inside a rect
stamp_custom only centred a PDF page or image at its natural size, at
most 200 pt, so a vector diagram could not be placed at a text column's
width. It now takes rect [x0, y0, x1, y1] (displayed-page points, like
at) as an alternative to at: the picture is scaled to fit inside the
box, keeping its shape, and centred in it. A PDF page stays a form
XObject, so it remains vector.
- Engine: Edit::AddCustomStamp fits the picture inside a non-point rect
instead of stretching it (fit_picture). A box already of the
picture's shape is kept exactly, so signatures and other callers that
size the box themselves are unchanged. The displayed sides are
swapped on quarter-turned pages, as for the natural size.
- Automation: rect is validated (four finite numbers, each side
at least 1 pt and at most 14400 pt, the largest PDF page; not clipped
to the page, like at); passing both at and rect,
or neither, is refused. path still goes through the read root.
- Tool schema and description, parity note and evidence.
Fixes#4077
* M14: update ctutils to 0.4.5 for RUSTSEC-2026-0385
The dependency-advisory gate rejects ctutils 0.4.2 (non-constant-time
mask generation and u128 comparisons, fixed in 0.4.4). It comes in
through crypto-bigint for the signing crates. Only its version and
checksum change in Cargo.lock.
(cherry picked from commit 8a687ee2c6012e3d18399633d2fb9fb3742f02ae)
* M10: odd and even pages are page numbers, as Acrobat counts them (#3984)
Even pages only with the range 4–11 printed pages 5, 7, 9 and 11: `narrow` took odd and
even places in the chosen pages, and its comment said Acrobat does the same. Acrobat
counts page numbers: "if you set the range to 2, 7–10 and choose Even Pages Only, only
pages 2, 8, and 10 print" (Adobe Acrobat DC Help, last updated 11/28/2019, Options in the
Print dialog box, p. 515; the same example in Using Adobe Acrobat 9 Pro, p. 413). Odd and
even now keep the pages whose number is odd or even, for a typed range, picked thumbnails
and the current page alike, in the order given, and Reverse turns that round. The header,
footer and watermark ranges (marks_ui) and Extract's odd/even filter in #3729 already
count page numbers.
The new test pins Acrobat's example and the report's ranges. select_listed's tests and
cut_stack_printing_through_tools counted places; the latter now asks for even pages of
2–10, which by page number are the 2, 4, 6, 8 and 10 it printed before, so its expected
output is unchanged and would fail with places (3, 5, 7, 9).
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* M10: say that odd and even are page numbers in doc_print, and what page labels still lack (#3984)
Review on #4034: the doc_print description says odd/even count page numbers, and the parity note says a document with page labels still counts page numbers there, which the Acrobat help consulted doesn't settle.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* M10: cite the cut-stack print test at its path under tests/it (#3984)
main moved the automation integration tests into tests/it, so the
evidence path added for odd/even printing no longer existed and
`cargo xtask parity` failed after the rebase.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0175FWj4xFhMhSH3UpMU8DSC
---------
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
text_lines returned a copy-restricted document's lines, ocr_recognize returned
the words it recognised, and text_edit on a paragraph echoed the paragraph's
own text, all without checking the copying permission that the other text
tools check. text_lines is refused like them; recognising and restyling still
work with permission to change the document, and leave the text out.
* 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.
Conflicts:
- ATTRIBUTION.toml: ours (sha256 lines only); hashes recomputed at the end of the track
- crates/engine/Cargo.toml: main's workspace chrono entry, plus the PR's pdfcraft-testkit dev-dependency
ROADMAP.md, docs/roadmap.md: all status lines and progress-log lines kept; M12 row takes both 'Done since' insertions.
parity/acrobat-features.toml: form.xfa evidence lists unioned, notes take both sides' insertions.
ROADMAP.md, docs/roadmap.md: both status lines and progress-log lines kept; M12 row takes both 'Done since' insertions.
crates/automation/tests/automation.rs, crates/xfa/src/fixtures.rs: union of #2624's and #2619's tests and fixtures.
parity/acrobat-features.toml: form.xfa evidence lists unioned, notes take both sides' insertions.
ROADMAP.md: kept both sides' progress-log lines.
crates/edit/src/lib.rs: re-export list is the union (TextSession from #3942; BlockKey, block_overlapping from #2756).
crates/xfa/src/script.rs: template_field and named_in moved to crate::data in #3397; field_grows_with_text calls crate::data::template_field.
parity/acrobat-features.toml: form.xfa evidence lists unioned, notes take both sides' insertions.