`comment_add` converted a note's or attachment's `at` (the icon's top-left
as displayed) as a single point through `PageInfo::view_to_user`, and the
engine's `rect_for` extends the icon square from `at` toward +x/-y in user
space. On an unrotated page the displayed and user-space top-left are the
same corner; under `/Rotate` they are different corners of the square, so
the icon landed one icon width (20 pt) away from the requested point.
`icon_anchor` converts the whole displayed square to user space and takes
its user-space top-left, so the `/Rect` covers the requested square under
any rotation. `PageInfo::view_rect_to_user` is the render-side counterpart
of `view_rect_to_quad`, and `NOTE_SIZE` is re-exported from the engine.
Test: `note_icons_anchor_at_the_requested_corner_on_rotated_pages` adds a
note and an attachment on pages rotated 0/90/180/270 and asserts the
`comment_list` rectangles; before the fix the 90° page reports
[52, 72, 72, 92] for a note asked for at [72, 72].
Co-authored-by: wstock <1394687+wstock@users.noreply.github.com>
Co-authored-by: Claude Fable 5.1 <noreply@anthropic.com>
Co-authored-by: Brandon Thomas <echelon@gmail.com>
The schema documented printing/changes/copy/accessibility and their
defaults unconditionally, but the engine (like Acrobat) only enforces
/P behind an owner password: open_password alone encrypts the document
and leaves everything allowed, even when a caller passed copy: false
or changes: "none" explicitly, and the call reported success.
State the condition in the tool and property descriptions, and turn an
explicit restriction without permissions_password into InvalidArgs that
names the argument and what is needed, instead of writing an
unrestricted file. open_password alone is unchanged.
Co-authored-by: wstock <1394687+wstock@users.noreply.github.com>
Co-authored-by: Claude Fable 5.1 <noreply@anthropic.com>
Co-authored-by: Brandon Thomas <echelon@gmail.com>
doc_info (and the MCP document-info resource built on it) serialised
annotation and link rectangles straight from the PDF /Rect, in user
space, while every other tool reports and accepts rectangles with the
origin at the top-left of the displayed page. Feeding a doc_info rect
to link_edit therefore moved the link, and on rotated pages the two
conventions disagreed in more than a y flip.
Convert both through the page's user_to_view, the same conversion
comment_list and link_list use, so the three agree. Pages that an
annotation cannot be matched to keep the raw rect.
Co-authored-by: wstock <1394687+wstock@users.noreply.github.com>
Co-authored-by: Claude Fable 5.1 <noreply@anthropic.com>
* M12: run XFA forms' JavaScript: calculations, validation, rows, visibility (#60)
Laid-out XFA forms had their fields but none of their behaviour: totals
didn't compute, "Add row" buttons did nothing, and subforms a check box
should reveal stayed hidden.
The js crate gains the XFA scripting object model as native boa objects
over a snapshot of the form tree: xfa.form navigation by name, SOM
resolution (absolute, relative with the containing-subform search,
descendants, indexes and wildcards), rawValue/presence/access, instance
managers (_name, addInstance, removeInstance, setInstances, count),
xfa.host (messageBox, resetData, print, setFocus, gotoURL), xfa.layout
page numbers, xfa.event, app, console and util, plus unqualified sibling
names as XFA resolves them. Scripts run with `this` bound to the node and
yield their completion value; everything they change or ask for comes
back as effects.
The xfa crate builds the live form tree (instances from the data, values,
presence and access with overrides) and the event table from the
template, keeps presence and access overrides on the AcroForm
(/PCXfaOverrides) so undo and reopening preserve them, adds and removes
row instances in the datasets packet through the existing merge
pipeline, and lays the form out again on demand.
The engine runs initialize and calculate scripts on open (an appended
revision, nothing dirty), exit, validate and calculate after a field
changes (a failing validate shows its message and keeps the value, as
Acrobat does), and click scripts for buttons through run_javascript, so
the app's buttons and the js_run tool work unchanged. Values a script
sets go to the fields and the data at once; rows and visibility changes
re-lay the form out. All of it is one undo step. JavaScript off turns it
off. FormCalc scripts are recognised and reported, not run.
Bounds: script length and nesting as before, loop and recursion limits,
at most 1000 instances added per run, 2000 scripts and 50 re-layouts per
event; a calculate script that adds rows, or a click script looping on
addInstance/setInstances, ends quickly with a bounded form (tested).
Checked on IRCC's IMM 5669: opens with its page-number scripts, the Print
button's xfa.host.print runs through js_run, fill and save keep the
original bytes.
* M12: Save and SaveAs named actions run the file commands (#60)
XFA forms' Save buttons are app.execMenuItem("SaveAs"), which the layout
turns into a SaveAs named action; the shell answered it with "the SaveAs
action isn't supported yet". Named Save and SaveAs now run file.save and
file.save_as, like Named Print opens the print dialog.
The buttons test covers Save against a file on disk (SaveAs opens the
native dialog, which a headless test must not).
* M12: bound XFA form scripts against hostile forms (#60)
Review fixes for the XFA scripting PR (#230):
- Effects: values, presence and access set again on the same object merge
(last wins, behind order-dependent effects); 10k effects, 100 message boxes
and 1000 console lines per script and per event, reported when hit. An
event's data writes are kept in memory and written as one rewrite of the
datasets packet; later writes in the same edit replace that stream in place.
- form_tree stops at 200k objects and says so; forms without scripts build no
live form and run nothing.
- Only click scripts may print, save, open links or move the focus; others are
noted in the console. Open-time messages and errors show right after open.
- Lower loop limits for initialize/calculate/validate, a time budget per event
after which remaining scripts are skipped and reported, and a per-script time
limit after which a runaway script is abandoned and the document's scripts
turned off.
- What open-time scripts change is noted in the XFA warnings.
- A document's Named /Save no longer writes the file on click (SaveAs opens
the Save As dialog, as a script's execMenuItem does).
- XFA click scripts are no longer written as /S /JavaScript actions: the
button carries /PCXfaClick and runs by SOM path. Private keys documented.
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>
* M12: add calibrated 2D measurement tools
* M12: harden measurement tools for landing (#208)
Bounded snap targets (endpoints only for accepted segments, page-wide cap),
lenient extraction (deep q nesting and undecodable streams are skipped and
reported), per-page snap cache, measure::list reports unsupported entries
instead of failing, closed polygons with a repeated vertex, printable unit
labels (m²), cached panel listing, WinAnsi captions, indirect /VP kept,
measure.export skipped in the command test like other pickers, missing
translations, honest parity (scale and distance partial) and ROADMAP.
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>
* M12: lay dynamic XFA forms out from their template (#60)
A dynamic XFA form is a PDF shell around an XML template: one placeholder
page ("requires Adobe Reader") and /NeedsRendering true. Every viewer
without an XFA engine shows the placeholder, and so did PrintCraft.
New crate printcraft-xfa (L3, already reserved in the layering table). It
reads the XDP packets, parses the template (XFA 3.3: subforms, areas,
fields, draws, exclusion groups, page sets, fonts, paragraphs, borders,
captions, rich text, inline JPEGs, occurrences, breaks, overflow leaders,
column widths), lays it out (positioned, tb, lr-tb, table/row; containers
without a width size to their content; flow across content areas and page
masters; header rows repeat after a break; hidden takes no space; page
number and count fields resolve) and writes real pages and AcroForm
widgets into the document: text, date (AFDate format and keystroke
actions from the picture clause), check boxes, radio groups, push buttons
with reset/print/save/URI actions. Standard-14 fonts stand in for Arial,
Courier New and friends; nothing is embedded.
The engine lays a dynamic form out when it opens it. The original bytes
stay: the layout is one appended revision, the widgets get appearances
from the forms crate, and the XFA packets remain so Adobe's viewers keep
rendering the form their way. A marker on the AcroForm (/PCXfaLayout)
stops a saved form from being laid out twice; each field keeps its SOM
path (/PCSom) for the data round trip to come. doc_info, the CLI's info
and the notice bar report the layout.
Checked on IRCC's IMM 5669 (Schedule A, Designer 6.2, 464 KB template,
AES-encrypted, usage rights): four pages and 308 fields in 27 ms, the
same page structure as the form, filled through form_fill, saved in
place with the original bytes intact and reopened with the values.
Not done, and said so in the parity notes: the datasets packet is neither
read nor written, so Adobe's viewers don't show values filled in here;
no FormCalc or XFA JavaScript (rows don't grow, nothing calculates);
choice lists are text fields; signature, image and barcode fields are
blank; metrics are approximate and fidelity is eyeballed, not measured.
Tests: crates/xfa (parsing, sizing, pagination with repeated headers,
PDF writing, hostile input), the engine (open, fill, incremental save,
reopen without a second layout), the automation tools (open, info,
render, fill, save, reopen) and the UI notice (kittest).
* M12: XFA forms read and write their datasets packet; static forms too (#60)
With the layout alone, a form filled in PrintCraft opened blank in Adobe
Reader, and one filled in Acrobat opened blank here: Adobe's viewers keep
the values in the XFA datasets packet, not in the AcroForm fields.
The xfa crate's new data module reads the datasets packet (normal binding:
a field's node is named after it under its named ancestor subforms,
repeated instances as repeated siblings) and writes it back from the
fields. Laying a form out now takes each field's value from its data node
(dates in ISO form shown in the field's pattern, check boxes and radio
groups by their on values) and gives a repeating subform or table row as
many instances as the data has, so rows another viewer added appear.
Every laid-out field carries its SOM path and its on/off items, so the
packet can be rebuilt from the AcroForm at any time.
The engine writes the datasets after every form edit (one revision with
the field change), and on open applies the datasets to the fields of a
static XFA form, or of a dynamic one laid out earlier, when another
viewer changed them since. The static-form notice now says the two are
kept in step instead of warning that Acrobat may disagree.
Checked on IRCC's IMM 5669: fill, save in place (original bytes intact),
and the file holds <IMM_5669><page1><familyName>SINGH</familyName>…, the
structure Acrobat writes for this form; reopening shows the values.
Not done: explicit bind refs, global binding and data descriptions are
not followed; no import or export of standalone XML or XDP data files.
* M12: XFA layout memoizes container sizes, bounding measurement work (#60)
A container without a width measured its content for its width and again for its
height at every level, so nested width-less subforms took 2^depth measurements, all
synchronously in Session::open. Container sizes are now cached per (node, width,
depth), the root and page-area items keep stable addresses instead of being cloned
per measurement, and a measurement budget turns anything left into TooLarge.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* M12: rustfmt the merged XFA notice (#60)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* M12: XFA datasets are merged into, not replaced; capped; UTF-16 kept; rewrites recorded (#60)
- write_datasets merged nothing: it replaced <xfa:data> with the AcroForm-bound values
alone, dropping unbound, namespaced and deep data. It now edits the existing packet
text in place: a bound node's text is replaced only when it differs, missing nodes are
added below the deepest existing one, and everything else (unbound data, other
namespaces, attributes, comments, layout) stays byte for byte. A packet of an XFA array
that relies on the envelope's prefixes is read inside a wrapper declaring them.
- What can't be written (a node holding structured content, too-deep paths or absurd
SOM indices, conflicting fields, an unreadable or non-UTF packet) comes back as
DatasetsWrite.warnings; the engine keeps them in Document.xfa_warnings and
info.warnings (surviving re-reads), and doc_info reports xfa_warnings.
- Building data counted same-name siblings for every one it added (quadratic in an
untrusted index) with no total cap. Children are now indexed by name, indices above
10,000 and paths deeper than 64 are refused, and one write creates at most 20,000 nodes.
- Packets are decoded exactly (UTF-8, UTF-16 LE/BE with or without BOM) and written back
in the encoding they came in; anything else is reported and left untouched instead of
being rewritten from a lossy UTF-8 reading.
- A static XFA form whose fields are rewritten from the datasets on open now says so in
info.warnings, naming the fields.
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>
* Signatures: RFC 3161 timestamp tokens (PAdES B-T) behind a transport-free TSA interface
* Signatures: document timestamps, DSS/VRI evidence embedding, trusted verification time
* Signatures: revocation evidence verification (CRL and OCSP) wired into validation
* Signatures: real-world certificate compatibility (CRLDP, tolerant extensions) and XFA-aware DocMDP classification
* Signatures: RFC 6960 encodings, delegated-responder verification, TSA trust anchoring, try_oid overflow
* Signatures: harden PR #69 validation (XFA, metadata, revocation, TSA trust, token digest)
- Remove the XFA "form fill" reclassification: it read the new revision, so
an update could add /XFA + /NeedsRendering and replace the page tree while
approval and P=2/3 certification signatures stayed Valid. Classification
(including catalog /Extensions) is back to main's.
- A changed stream tagged /Type /Metadata counts as metadata only when it is
the signed catalog's /Metadata stream or was already metadata when signed;
a relabelled page Form XObject is "other changes".
- A verified revocation stays Invalid when the certificate was also outside
its validity at signing time (was softened to Unknown).
- An embedded timestamp whose TSA does not chain to the trust store is
reported as an unverified timestamp and no longer sets the validity or
revocation time; the signer's claimed time is used, as on main.
- parse_token checks the TSTInfo messageDigest with the SignerInfo digest
algorithm, not the imprint's.
Each fix has a regression test in crates/sign/tests/pdf.rs that fails on
the PR head.
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>
doc_export_data, accessibility_report and image_save each write to their required path and replace an existing file there, but the tool table marked all three read-only, so MCP clients were told the calls change no file. They now carry the destructive flag the table defines for a tool that overwrites files, and a test pins the three names.
Co-authored-by: Mohammed Munzir <Mohammed.Munzir@iga.gov.bh>
Co-authored-by: Brandon Thomas <echelon@gmail.com>
* Automation: --root refuses every outside path the same way (#136)
With a root set, `resolve` canonicalized a path before checking it against
the root, so an existing outside path got "is outside the allowed
directory" while a missing one got the OS error ("No such file or
directory", "Not a directory", "invalid path"): a confined MCP client or
script could probe which files and folders exist anywhere on the disk.
Containment is now decided before anything about an outside path is
reported:
- `..` is resolved by name first (as Windows does), so a path can't pass
through an outside folder on its way back in;
- on Windows another network share or device namespace (`\\host\share`,
`\\?\UNC\...`, `\\.\...`) is refused by name, without contacting it;
- the deepest existing ancestor, links followed, must be inside the root
before anything is said about the rest;
- a broken link is refused the same way, so whether its target exists
stays hidden too.
Every outside path now gets `<path> is outside the allowed directory
<root>`, existing or not, file or folder, read or write. Filesystem errors
such as "not found" are still reported for paths inside the root.
Behaviour change: on Unix, `..` now means the parent by name, as on
Windows (`link/../x` is `x` beside `link`), and a write such as
`new/../x.pdf` that stays in the root now succeeds instead of failing with
"'..' is not allowed here".
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* CLI: script images stay inside --root (#137)
`pdfcraft-cli run --script steps.json --root DIR` wrote each step's
`"out"` PNG with `std::fs::write`, never through the root check the tools
use: an absolute `"out"` created or replaced any file the process could
write, and a relative one landed in the working directory instead of the
root.
The image is now saved through the new `Automation::write_output`, which
uses the same resolver and atomic write as a tool's own output: outside
paths get the usual "is outside the allowed directory" refusal (the step
fails, nothing is written), relative paths resolve inside the root, and
without --root the path is written as given, in place as before (so
`/dev/stdout` still works). `run <tool> --out` goes the same way.
Found while testing this, the same "." problem in two tools:
- a write path naming a folder, such as "." (the root itself), staged its
temporary file beside that folder, i.e. outside the root for ".",
overwriting and then deleting any file there called
`.root.pdfcraft-tmp`. `write_atomic` now refuses a folder before
writing anything;
- `image_save` adds an extension to a path without one, so "." became
`root.png` beside the root. It now refuses a folder first.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* Automation: files named after a document stay in the output folder
Found while reviewing the --root fixes for #136 and #137. Folder outputs
name their files after the document and joined that name straight onto the
resolved folder, without checking the result against the root:
- `doc_export_images` and `doc_export_all_images` used the whole document
name, so `doc_create` with `name: "../../escape"` followed by
`doc_export_images` wrote `escape_page_1.png` outside the root (an
absolute name went anywhere);
- `page_extract` (separate files), `doc_split`, `ocr_recognize_files` and
`action_run` took the last component, but on Windows a name such as
`x/C:evil.pdf` gives `C:evil`, and `folder.join("C:evil-page1.pdf")`
replaces the folder with a path relative to the current directory on C:.
Every such name now goes through `child()`, which turns separators,
colons and control characters into `_` (and prefixes a name of only dots),
so the file is always a direct child of the folder. `with_root` also
refuses a root that is not a folder.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
---------
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>
Field Properties > Options > Check Box Style (Button Style for radio
buttons): Check, Circle, Cross, Diamond, Square or Star, as Acrobat
offers. The choice is stored in each widget's /MK /CA with Acrobat's
ZapfDingbats codes (4 l 8 u n H), so other viewers draw the same mark,
and PrintCraft draws it as paths, needing no symbol font. Styles set
by other apps are read back too; the defaults stay a check for check
boxes and a circle for radio buttons.
- forms: CheckStyle, check_style(), FieldProps::check_style; set_props
writes /MK /CA (resolving an indirect /MK so its colours survive) and
redraws; text and choice fields refuse a style.
- engine: Document::field_check_style.
- automation: form_set_props takes check_style, form_fields reports it.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
XFA forms aren't read yet, and opening one gave no hint: a dynamic XFA form
showed only its "Please wait…" placeholder page, and a static one looked like
an ordinary form though Acrobat shows its XFA data in preference to the
fields that get filled.
- render: DocInfo.xfa (Static / Dynamic) from /AcroForm /XFA; dynamic when
/NeedsRendering is true or there are no fields. Exported for doc_info
(automation and CLI) as "xfa".
- ui: the notice bar explains each case (above the form-fields notice); a
static form keeps its Highlight fields button.
- Tests: tests/xfa.rs (dynamic with and without /NeedsRendering, static,
ordinary forms, a malformed /XFA). Parity notes for form.xfa-static and
form.xfa-dynamic (still planned: this explains XFA, it doesn't support it).
Reported by @Sudachi-dev.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
In Edit a PDF ▸ Edit text, dragging a paragraph's box moves it, and dragging
the handle on its right edge rewraps it to a new width; a click still opens
it for typing.
- edit: BlockStyle gains offset (user space) and width. rewrite_block puts the
move inside the paragraph's own q … Q as a cm, taken back through the CTM's
linear part so a page-space drag lands right in scaled/rotated content
(the CTM is now recorded at each line's origin). Non-finite numbers are
refused; widths are clamped to one character .. 14 400 pt.
- ui: BlockDrag on DocView; the box follows the pointer (or its right edge
does) and the release applies one undoable Edit text step. The width handle
shows on upright pages only (on a quarter-turned page the screen's
horizontal is the text's vertical).
- automation: text_edit takes dx, dy and width.
- Tests: engine (move, move under a scaling cm, narrower/wider rewrap,
non-finite and zero inputs), kittest pointer drags, automation tool.
Parity: new edit.text-move-resize (shipped).
Requested by @TCB13.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
- Renderer cache: built with the entry API instead of insert-then-`expect`.
- Page-text cache: filled and read with `get`/`get_mut`.
- Schemas and form JSON: extended through `if let` instead of `as_object_mut().expect`.
- A replace-text mark of the wrong shape is an invalid-argument error.
The crate now denies the panic lints.
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>