The comment summary gathered a comment's direct replies only, matching /IRT against the comment itself. A reply to a reply points at the direct reply, so its text never reached the generated summary even though the stored discussion kept it.
Replies are now walked to any depth, with a bound, and indented one level per reply. The existing output for direct replies is unchanged.
The new test builds a rectangle, a reply and a reply to that reply, then checks the summary carries both texts with the nested one indented under its parent. It omitted the nested reply before the fix.
Co-authored-by: Big Pickle <noreply@opencode.ai>
The ocrs crate's default alphabet has an E where its recognition model was
trained with € (ocrs-models DEFAULT_ALPHABET), so a scanned "€250" was
recognised as "E250". pdfcraft-ocr now gives ocrs the alphabet the model was
trained with.
the_alphabet_is_the_trained_one checks the alphabet without the models;
recognize_text_reads_the_euro_sign reads a scan end to end when they are
installed.
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
* Parity: merge the duplicate form.field-list evidence and notes
#649 and #650 each added `evidence` and `notes` to the same entry, so the
TOML has duplicate keys and `cargo xtask parity` fails on main.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* M4: bookmarks from tagged headings (organize.bookmarks-from-structure)
Bookmark options > New bookmarks from structure (command
bookmark.from_structure, tool bookmark_from_structure) turns a tagged
document's headings into bookmarks, as Acrobat's New Bookmarks From
Structure does for headings: H1-H6 through the role map, a plain H by its
Sect/Part/Art nesting, nested by level under a new first "Untitled"
bookmark, each linked to its structure element (/SE, ISO 32000-2
§12.3.3). Titles come from /ActualText, the marked text drawn on the page
(decoded through the font's ToUnicode or encoding), /Alt or /T.
The structure walk is bounded (depth, visits, heading and title caps,
seen-set) and survives self-referencing elements; an untagged document
gets an actionable error; the edit is one undo step under the assemble
permission. Not yet: choosing other element types, and destinations at the
heading's position rather than its page.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* Tests: size the shared raster allowance from thumbnail demand
The canvas test planted a 132×171 thumbnail whose tag did not match the
request finish_render_frame computes. On FreeBSD that real render finished
in the same frame, so the retained raster exceeded 132×171×4. Plant a
thumbnail that already satisfies demand and compare against that size.
* M4: a heading's text stops gathering once it is far past the title length
A heading whose marked-content list repeats one mark up to the visit cap could otherwise collect
about a gigabyte before the title is cut to 256 characters.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Co-authored-by: Brandon Thomas <echelon@gmail.com>
* M4: keep the render pool across metadata edits
Metadata edits (SetInfo) no longer rebuild the render pool or the display
bytes. The engine refreshes the document-information fields, the file size,
the signatures (when present) and the working bytes, and keeps the renderer.
Undo and redo of metadata edits reuse the same pool.
Comment and form edits still rebuild the renderer and their view data.
Tests:
- metadata_edits_and_history_reuse_the_render_pool
- field_and_comment_edits_still_rebuild_the_render_pool
- metadata_edit_keeps_the_renderer_without_changing_page_content (the
metadata invalidation audit; its assertions now expect the kept renderer)
Supersedes #711 and #712.
Co-Authored-By: Claude Haiku 5.5 <noreply@anthropic.com>
* M4: a metadata edit reads the other Info entries as the full inspection does
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
---------
Co-authored-by: Claude Haiku 5.5 <noreply@anthropic.com>
Co-authored-by: Brandon Thomas <echelon@gmail.com>
* M3: harden render and CLI failure surfaces
Make content truncation observable, reject unknown CLI options, expose deterministic render work counters, and scope render failures to exact requests. Refuse silently reduced whole-page export rasters while preserving tiled rendering, and add heavy-vector and CLI regression coverage.
Co-authored-by: Codex <noreply@openai.com>
* Render: remember a failed render by page and kind again
A key with scale and tag made every zoom level and tile a new key: a
pathological page tied up a worker for the watchdog limit per request
(exhausting worker replacements) and was re-parsed each time. The
repeated-parsing check asks for a new generation and scale again.
* Export: pages too large at the chosen dpi export at the most the renderer allows, and say so
#669 made every Exporter refuse oversized whole-page renders, which
reached File > Export to image, doc_export_* and OCR: at 600 dpi any page
side over 13.65 in (Legal, A3, Tabloid) failed instead of exporting. The
exporter clamps again; Exporter::dpi_used reports the resolution a page
is drawn at, which the Export dialog's message and doc_export_png's
lower_dpi list mention. A source with reject_oversize still refuses
(CLI render keeps its strict config).
* doc_export_images: describe lower_dpi
* Render: ContentTruncated is reported once per page; the hayro-interpret patch is recorded
Nested forms, Type 3 glyphs and patterns are interpreted again and each
reported the exhausted budget; pdfcraft-render now keeps one. The warning
is part of hayro-interpret patch (18) (the page content budget): marked
in the code and described in vendor/README.md.
* CLI render: print render warnings (skipped content) to stderr
* CLI: the option check refuses unknown options only; commands report missing values themselves
text's own --page parsing (and its test on main) says what a page value
must be; the generic "needs a value" pre-empted it.
---------
Co-authored-by: Codex <noreply@openai.com>
Co-authored-by: Brandon Thomas <echelon@gmail.com>
A PDF whose AcroForm /Fields array is empty or missing listed no
fields, so none were fillable even though every widget sat on a page
with its name and value: Acrobat and the browsers fill those. The page
widgets are now adopted as fields — after the tree-listed ones, through
each widget's topmost unlisted /Parent so a field split across several
widgets stays one — and the leniency is recorded, never silent.
- forms: fields() walks /Fields as before, then adopts the unlisted
page widgets; adopted_page_fields() counts them for the note.
- render: the inspector's field list adopts them the same way, so the
Fields panel, the field-count notice and menu gating follow.
- engine: opening such a file notes "the form's /Fields list omits N
fields; they were read from the page annotations" in the repair log
(Document Properties ▸ Advanced).
Co-authored-by: GLM-5.3 <noreply@z.ai>
Co-authored-by: Brandon Thomas <echelon@gmail.com>
* M3: tabs shrink to fit before they scroll, and Open stays in sight
With more documents open than fit, the tabs kept their full width: they ran
under the buttons on the right (#416 made them scroll) and the Open button
scrolled away with them. Now the tabs shrink first, as browsers do: narrow
tabs keep their width, wider ones share the rest equally and end their names
in "…" (the tooltip has the whole name). Only once they are at their narrowest
does the strip scroll. Open sits after the tabs, outside the scrolling area,
so it is always in sight next to the controls on the right.
Refs #397.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* M3: a placeholder title like about:blank leaves the file name on the tab
A browser's "Save as PDF" of a blank page or pop-up titles the document
"about:blank" and asks viewers to show the title, so its tab and window said
"about:blank". A one-word browser address (about:, blob:, data:) or a bare
"Untitled" now counts as no title, and the file name is shown. A tab that
does show a document title names its file on hover.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* M3: arrows step through tabs that don't fit, and tabs keep most of their name
Tabs shrank to 110 points before the strip scrolled, so with many open they
became a few characters each and nothing showed that more were hidden. Tabs
now stop at 180 points (most of a name), and once even that doesn't fit,
arrows appear either side of them: each steps about one tab, and an arrow is
dimmed at its end, so it is clear whether more tabs lie that way. The mouse
wheel still scrolls them, and the active tab is still brought into view.
New labels "Scroll tabs left" / "Scroll tabs right" are translated in every
catalog (machine-written; native review welcome).
Refs #397.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
---------
Co-authored-by: William Max & Co. <info@williammax.com>
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Co-authored-by: Brandon Thomas <echelon@gmail.com>
* Export to Word/HTML/RTF: read text and images drawn by form XObjects (fixes#314)
The office export built its paragraphs from text_blocks and its images from
page_images. Both read only the page's own content streams (they back Edit
text and Edit image, which rewrite those streams), so a page drawn through
`/Fm1 Do`, as many print-to-PDF tools emit, exported as an empty document.
Add read-only reading_blocks and reading_images to pdfcraft-edit. They
follow Do into form XObjects (nested ones too), apply each form's /Matrix,
use its own /Resources (or the caller's when it has none), and start it
from the graphics state at the Do. Depth is capped at MAX_FORM_DEPTH and a
form already being read is skipped, so self-referencing and mutually
recursive forms are read once. export_pages uses them; editing is
unchanged and still offers only the page's own text and images.
* Export: a page's walk through form XObjects has a total visit budget, so forms that fan out can't hang it
* Export: keep the image walk within clippy's argument limit
---------
Co-authored-by: Brandon Thomas <echelon@gmail.com>
* XFA: choice lists, signature, password, image and barcode fields; PNG and GIF pictures
The xfa crate lays out the field kinds that were left blank or shown as
text fields:
- Choice lists become combo boxes (editable with textEntry) or list
boxes (open="always"; multi-select with open="multiSelect"), with the
shown items and the saved values of <items save="1"> as /Opt pairs,
/V, /I and /DV as saved values, several on separate lines in the
data. Field::saved_value maps shown text to saved values for the
layout and the script tree alike, so scripts read what is saved.
- Password fields are password text fields; signature fields are /Sig
fields the app can sign.
- Image fields show the picture the data (base64) or the template
holds; barcode fields show their value as text, with a warning that
no bars are drawn.
- PNG and GIF pictures (fields and draws) are decoded to Flate image
XObjects with a soft mask for transparency; JPEGs stay as they were.
Sizes come from the headers, so layout never decodes; decoding is
capped at 40 million pixels and failures are warnings, not errors.
Engine: multi-select list values from scripts and data are one per
line. README and parity notes updated (form.xfa-dynamic).
Hardened after an adversarial review pass: picture sizes refused from
the header past 20 million pixels or 65,535 a side, the GIF decoder
given limits (a frame far larger than its screen is refused before
allocation), template pictures read whole up to 32 MB, pictures
decoded once and remembered in the catalog by content hash so
re-layouts reuse them, data pictures that aren't images reported by
field; choice values matched against saved values first, several
values deduplicated and capped, /I ascending and unique, /DV through
the same mapping, <value> children read, the item cap reported; the
reopen path takes another viewer's choice values (several per line);
CMYK JPEGs inverted only with Adobe APP14.
* D5: README for cos and engine, and an honest cos status line
Thirteen crates have no README, although the session protocol tells every session to read the README of the crate it is touching. These are the two biggest: cos is where every hostile file lands first, and engine is the whole API surface a frontend gets.
Each states its layer and the rules that bind it, sketches the API against the real signatures, and ends with what is deliberately not done. The cos README spells out the clone-is-a-snapshot property and why both writers return bytes rather than taking a sink; the engine README spells out the four-step editing model that makes what you see what Save writes.
Also corrects the module doc on cos/src/lib.rs, which still said encryption and object-stream output for full saves were not done. Both shipped: security.rs handles R2-R6 with crypt filters and /EFF, and SaveOptions::object_streams defaults to true.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
* Preserve alternate appearances and indirect push-button values
Keep rollover, down and custom appearance entries when normal appearances are regenerated, without modifying shared dictionaries. Resolve indirect button caption and layout values. Cover full and incremental saved output, and wait for raster readiness in the existing Linux scroll test.
Co-authored-by: Codex <codex@openai.com>
* M7: select and edit grouped Form XObject figures
Co-Authored-By: Codex <noreply@openai.com>
* M14: let neutral Windows locales fall through to display language
Co-authored-by: Codex <codex@openai.com>
* pdfcraft-cli --help lists `mcp` only when the build has it
Follow-up to #540: a build without the `mcp` feature (`--no-default-features`) still listed
`pdfcraft-cli mcp` in its usage, and `pdfcraft-cli mcp` then said "unknown command".
USAGE is now put together with `concat!`, and its `mcp` lines come from a macro that is empty
without the feature. In such a build `pdfcraft-cli mcp` says the MCP server was left out instead
of calling it an unknown command.
- tests/help.rs: usage_lists_mcp_only_when_built_with_it (runs in both feature sets) and
mcp_without_the_feature_says_it_was_left_out (without the feature)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* engine README: the execution plan is local-only, so don't link a path that doesn't exist
* Merge land/450 into land/414; XFA pictures are found again by a SHA-256 on their XObject
The image cache was a /PCXfaImages map in the catalog, keyed by DefaultHasher
(not stable across Rust releases) and read back from the file. Each image
XObject now records the SHA-256 of its picture's bytes, and a re-layout finds
the XObjects on the pages it replaces by that hash; nothing goes in the catalog.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
---------
Co-authored-by: Tanish Manku <tmanku.dev@gmail.com>
Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
Co-authored-by: sanyamsingh001 <314051937+sanyamsingh001@users.noreply.github.com>
Co-authored-by: Codex <codex@openai.com>
Co-authored-by: Flowershangfromthebranches <152056395+Flowershangfromthebranches@users.noreply.github.com>
Co-authored-by: Codex <noreply@openai.com>
Co-authored-by: Streetblock <github@streetblock.de>
Co-authored-by: lloyd <lloyd@stackworx.io>
Co-authored-by: Brandon Thomas <echelon@gmail.com>
* M5: flatten Fill & Sign marks when saving
Saving can now bake typed text, marks and signatures into the page, from Preferences or doc_save. The option defaults off, so a normal save still leaves them editable, and other comments and form fields stay.
* M4: zoom the Organize page grid
The page grid draws its pages at 50% to 300%: toolbar buttons with a
percentage that resets to 100%, trackpad pinch or Ctrl/⌘ + wheel about
the page under the pointer, and ⌘+ / ⌘− / ⌘0 (which no longer change the
hidden page view's zoom while the grid shows).
Thumbnails are re-rendered sharper in a few steps as the grid grows,
capped so one document's thumbnails stay within 256 MB. The page under
the pointer (or the first one in view) stays where it is across a zoom.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* M4: zoomed grid pages are rendered at the size drawn
A zoomed page grid stretched 132 pt thumbnails, and the stepped detail
that was meant to help was capped by a memory budget, so a long document
stayed blurry at any zoom.
Pages drawn larger than their thumbnails now get a render at exactly the
size drawn while they are in view; the thumbnail stands in until it
arrives. Only pages in view keep one, so memory no longer depends on the
page count, and thumbnails go back to their one fixed size.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* View: the middle button pans instead of drawing with markup tools
egui's drag responses accept every pointer button, so on Windows and macOS a
middle-button drag reached the page tools: with Draw (ink) selected it drew a
stroke, and other tools cropped, selected or moved. Linux was unaffected
because its middle-click auto-scroll already blocks page input.
Outside Linux a middle-button drag that starts in the viewport (document or
Organize Pages) now pans 1:1 while the button is held, with a Grabbing
cursor, and page input is blocked from the press through the release. Linux
keeps its latched auto-scroll.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* M3: a short page gone to from the Pages panel stays current
The current page was whichever page showed most of itself. Going to a short
page (a cheque) above a tall one scrolls it to the top, where the tall page
below shows more, so the current page and the Pages panel's highlight moved
to the tall page instead of the one clicked. The page navigated to now stays
current while it is wholly on screen.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* M4: page operations from the Pages panel: context menu and drag to reorder
Right-clicking a thumbnail offers Extract pages, Cut, Copy and Paste after, on
the selection or on the page clicked (which becomes the selection), as the
organize grid does. Dragging thumbnails moves the selection, or the page
grabbed, to the gap it is dropped on: a bar marks the gap, the page count
follows the pointer, and the panel scrolls near its edges. The move is one
undoable step; the moved pages stay selected and the current page follows
its page. Documents that forbid assembly keep click-only thumbnails.
Parity: view.thumbnail-context-ops is shipped, with what it still lacks.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* Pages panel: only the primary button drags thumbnails; a drag left over after the panel closed or the document changed is dropped
* M5: translate Fill & Sign flatten labels into Ukrainian
The coverage test failed because the new preference strings had no Ukrainian entries.
* Fix main CI: move the rotated-field render test out of pdfcraft-forms (#591)
#516 added pdfcraft-render as a dev-dependency of pdfcraft-forms for one
rendering test. Both crates are layer 3, so `cargo xtask layers` (run in
CI) rejects the sideways edge and main went red. The test now lives in
crates/engine/tests/form_rotation.rs, where both crates are already
dependencies, and forms drops the dev-dependency. The parity evidence
points at the new location.
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
* uk: the author's wording for the flatten setting
---------
Co-authored-by: samehelhawary <basio20044@gmail.com>
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Co-authored-by: Cody Vogt <Cody.Vogt@caltechsurveys.com>
Co-authored-by: William Max & Co. <info@williammax.com>
Co-authored-by: Brandon Thomas <echelon@gmail.com>
* M5: draw all ten PDF line-ending styles
Imported lines, polylines and callouts kept Square, Circle, Diamond, Butt, Slash and reverse arrows in /LE but would not redraw them. Those styles now draw, and comment_add and Comment properties can set them.
* hu, uk: Line ending
---------
Co-authored-by: Brandon Thomas <echelon@gmail.com>
The export model (`pdfcraft_export::Block`/`Cell`) had no colour, so
`export_pages` dropped the fill colour that `edit::TextBlock` already
carries and every run came out black.
Block and Cell now carry an sRGB colour, mapped from TextBlock.color.
Word runs get `<w:color w:val="RRGGBB"/>`, HTML paragraphs, headings and
cells a `style="color:#RRGGBB"`, and RTF a colour table with `\cfN`.
Black text keeps each format's default; NaN and out-of-range components
clamp.
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Refs #434 (partial). Preserve displayed bounds and standard appearance orientation through stamp edits; image signatures and explicit rectangles remain outside this fix.
Assisted-by: OpenAI GPT-6.1 (gpt-6.1-sol)
Co-authored-by: Brandon Thomas <echelon@gmail.com>
* M5: comments without an appearance stream are drawn (display only, never saved)
#260 (page 7): a FreeText box with /DA and an orange /C background, a
Text note and an Ink stroke, all without /AP, were not drawn; Acrobat,
MuPDF, PDFium and Poppler draw them. The renderer only draws /AP (plus
the Highlight fallback of #274), and pdfcraft-annot, which already
draws all of these for PdfCraft's own comments, can't be used from the
renderer (both are L3).
pdfcraft_annot::with_missing_appearances returns a copy of the document
in which every annotation without /AP /N that appearance::build can
draw (FreeText, Ink, Text, FileAttachment, stamps, shapes, lines,
markup, …) has the appearance build draws from its dictionary; links,
form fields and pop-ups are left alone, as is anything build declines.
The engine keeps that copy, written as an in-memory incremental update,
in a new Document::display: the page view's render pool, page_render
and page images (export_source) read it. Document::bytes, what Save,
signatures and the inspector use, is unchanged. display is rebuilt
wherever the renderer was (open, refresh, scoped refresh) and is bytes
itself when nothing is missing or the document can't be edited.
Tests: annot tests::comments_without_appearances_get_one_for_display_only
(FreeText, Ink and Text get /AP /N in the copy, an existing /AP is kept,
Link, Widget and an unknown subtype aren't touched, the original is
untouched, none means no copy) and engine
tests::comments_without_appearances_are_drawn_but_not_saved (the page
view draws the FreeText background and the Ink stroke, before and after
an edit; the working bytes stay the opened file; Save writes no /AP).
On main the page view shows white where the FreeText box is. Parity:
view.render-annotation-appearances cites the test and notes what's left
(Link borders without /AP, printing).
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* Test the Ink stroke on its apex row: Ink is now a curve through its points (#374)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Co-authored-by: Brandon Thomas <echelon@gmail.com>
* M11: Optimize PDF and Reduce File Size run in the background with a progress bar
Advanced optimization and Reduce File Size did all their work on the UI
thread, so on a large file the window stopped repainting until they were
done and looked as if it had crashed.
- pdfcraft-optimize: optimize_with_progress reports each image and the
clean-up; returning false stops with OptimizeError::Cancelled.
- engine: Session::optimize_job captures a copy of the document and the
choices; OptimizeJob::run reports each OptimizeStage (removing user data,
images, cleaning up, merging, writing), stops on request with
EditError::Cancelled and turns a panic into an error so a worker always
reports back. optimized_bytes is the job run in place.
- ui: both commands run the job on a worker thread (inline in tests and on
the web), like OCR and Export. A card at the bottom centre shows the
stage ("Optimizing... image 14 of 40"), a bar that eases towards the
fraction with a light sweeping across it, the percentage and Cancel;
notices stack above it. A second run is refused while one is going. The
control channel's state includes the progress.
The headless doc_optimize and doc_reduce tools stay synchronous. New
strings are translated for es, fr, ja, zh-hans, zh-hant.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* Clippy: as_chunks in the signature image background test (follow-up to #355)
#360 switched the image test to as_chunks; #355 added another chunks_exact(4)
in the background test, which clippy's chunks_exact_to_as_chunks rejects.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* Optimize: report a failed worker spawn instead of waiting forever
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Co-authored-by: Brandon Thomas <echelon@gmail.com>
* M10.2: create PDF from multiple files
Create a PDF ▸ Multiple files takes PDFs, images and .txt files, lets the
user reorder them, and makes one combined PDF (a bookmark and an optional
page range per file) or one PDF per file in a chosen folder, never
overwriting what is there.
- create: source_kind() tells PDFs, images and text apart (moved from the shell)
- engine: Session::convert_to_pdf, shared by Open, the dialog and the tool
- automation: doc_create_multiple (combine or separate)
- ui: Multiple files dialog reusing the Combine files list; translations
- parity entry and ROADMAP updated; catalog hashes refreshed
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* M10.2: multiple files open in the page grid; insert files at any gap
Create a PDF ▸ Multiple files now opens the picked files as one unsaved
document in the Organize page grid instead of a file-list dialog: pages
are removed and reordered there, and Save pages writes what is shown.
- grid: a "+" at every gap inserts files right there; Save pages button
- insert from file: several files in order, images and text converted,
inserted pages selected
- the file-list dialog is removed; one PDF per file stays in the
doc_create_multiple tool
- translations, catalog hashes, parity notes and ROADMAP updated
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* M4: insert files by dropping them on the page grid
On desktop, PDFs, images and .txt files dropped on the Organize page grid
are inserted at the gap under the pointer, in order, and selected; a
dropped file still opens as a document everywhere else.
winit does not report the pointer while files are dragged over the
window on every platform, so a drop waits (up to a second) for the
pointer to show up and otherwise appends at the end.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* M4: files dragged over the page grid follow the pointer (macOS)
winit sends no pointer moves while files are dragged over the window, so
egui kept the place the pointer entered at: the insertion bar froze there
and the files were inserted there. During a file drag the pointer is now
read from AppKit (safe objc2 calls, crates already in the tree) and fed
to egui, so the bar and a "+" badge follow the pointer and the files go
where they are released.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Co-authored-by: Brandon Thomas <echelon@gmail.com>
* M4: Combine files in its own tab, with a sortable table and unlocking of protected files
Combine files was a modal dialog with a cramped list and up/down buttons on
every row. It is now a tab next to Home and the documents, showing a table
of the files: File name | Pages | Size | Modified | Warnings.
- Columns sort when their heading is clicked (anywhere in the cell; again to
reverse; names in natural order), resize from their edges and move by
dragging the heading. Order and widths are kept in the settings; a
right-click on a heading resets them.
- Rows select like a file manager (click, Ctrl/Cmd+click, Shift+click,
Shift+arrows, Ctrl/Cmd+A). One toolbar moves, removes and unlocks the
selection; Alt+arrows and dragging move it too. Every change to the list
undoes and redoes with Cmd/Ctrl+Z (also toolbar buttons).
- Add files, Add folder, Add folder and subfolders (a split button), Add open
documents, or drop files and folders on the tab.
- Problems show before Combine: password-protected files, security settings
that withhold page assembly, page ranges (checked as typed, with the pages
they take), duplicates. Files that open but are protected say that the
combined file won't keep their security; the result is never encrypted.
- Unlock... asks for a file's open or permissions password (one password can
be tried on several files). Passwords stay in memory, are never printed,
and reach the engine through combine_unlocked; doc_combine takes them as
`passwords`. The engine decides whether a password works, since the
inspector rejects some it accepts (RC4 owner passwords).
organize.combine-sort is shipped and organize.combine-options partial
(include open files). Strings are translated for es, fr, ja, zh-hans, zh-hant.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* Credit Marlon Konrad (mlkonrad) in the About window
Adds my own entry to contributors/people.toml, as docs/contributors.md asks.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Co-authored-by: Brandon Thomas <echelon@gmail.com>
* XFA: run FormCalc scripts
A FormCalc interpreter (XFA 3.3 part 8) in the js crate, over the same
form table, effects and budgets as the XFA JavaScript model, so the
engine runs both languages alike: expressions and FormCalc coercions,
var, if/while/for/foreach/func, accessors ($, $host, $form, $record,
$layout, $event, xfa., .., [n], [*], _name instance managers) and the
arithmetic, logical, string, date/time, financial and unit built-ins.
Get/Post/Put refuse.
The JavaScript model's XHost, HNode, SOM resolution and instance
operations are shared (pub(crate)) with Context-free add_instance and
remove_instance that the boa wrappers call.
Bounded like the JavaScript path: steps, loop iterations, recursion,
parse depth, string and list sizes, clamped dates, the instance cap,
and the per-activity time budget with abandonment.
Tests: language, built-ins, control flow, form objects and hostile
scripts in the js crate; a FormCalc fixture form in the engine that
initializes, calculates, validates, adds and removes rows, shows a
subform and round-trips through save; hostile scripts at the engine
level. parity form.xfa-formcalc planned -> partial.
Hardened after two adversarial review passes: statement nesting and
chain length count against the limits (verified on a 1 MiB stack), the
script honours the engine's deadline itself, strings are capped after
every built-in and messages/values clipped and noted (shared host),
lists hold each object once and the shared SOM resolver deduplicates,
values go into fields only (shared host, JavaScript too), saturating
Stuff and time arithmetic, FormCalc-specific size check instead of the
JavaScript refuse scan. Semantics: bare row[*] and row[n] from inside a
row see every row, relative [-n]/[+n] indexes, Count of non-null,
Parse with a date picture gives the ISO date, assignment is an
expression.
* form.xfa-formcalc: bound FormCalc values, results, errors and copying (#301 review)
- XHost::set_value cuts a value to 64 Ki chars before storing it, so later
reads match what is saved and the snapshot stays bounded.
- A script's completion value (calculate result) is cut to 64 Ki chars with a
note, in both the FormCalc and the JavaScript paths.
- throw clips its value like any message.
- Copying, dereferencing and comparing long strings is charged to the step
budget (len/256), so wasm (no clock) can't spend unbounded time on it;
OneOf charges per comparison.
- Exists propagates step/deadline/depth/size limits, answering 0 only for
lookup failures.
- IPmt/PPmt charge a step per period.
- The parser counts each top-level expression's chain from zero, so an
elseif condition no longer inherits the previous arm's count.
- Comment on the interpreter's depth limit fixed; parity notes say memory
isn't capped and Uuid/Date/Time are deterministic on wasm.
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>
* Layers: run set-layer-visibility actions, keeping radio-button groups
The engine can apply a set-layer-visibility action (SetOCGState,
ISO 32000-2 §12.6.4.13) to a document's layers: each ON, OFF or Toggle
in order, naming the layer by its optional content group, so a toggle
sees the state the changes before it left. With /PreserveRB (the
default), a layer turned on turns off the other layers of its
radio-button groups, now read from /OCProperties /D /RBGroups; turning
one off leaves them alone. The renderer is rebuilt once per action, not
once per layer.
Like the Layers panel, this changes the view, not the document.
Tests: engine set_layer_actions_toggle_in_order_and_keep_radio_groups
and set_layer_actions_change_what_renders (pixels before and after),
render radio_button_layer_groups_are_read.
* Links: run set-layer-visibility actions
Clicking a link whose action is SetOCGState showed "SetOCGState actions
run in the JavaScript engine (M6)" and did nothing. Links now read the
action (/State, also when indirect, and /PreserveRB) and clicking runs
it; the Layers panel follows. Hovering shows "Set layer visibility"
(translated in the ja, es, zh-hans and zh-hant catalogs), and doc_info
describes the action in its link list.
Tests: render links_read_set_layer_actions, automation
doc_info_describes_set_layer_links and ui-egui
links_set_layer_visibility.
* Forms: push buttons run set-layer-visibility actions
A push button whose mouse-up action is SetOCGState counted as having no
action, so clicking it did nothing. Buttons now read the action as
links do, and clicking runs it.
Tests: forms push_buttons_read_set_layer_actions and ui-egui
push_buttons_set_layer_visibility.
parity: edit.action-set-ocg-state is partial. The notes say what is
missing: creating the action, bookmarks and other triggers, /Next
chains, JavaScript ocg.state, and the Layers panel keeping radio-button
groups itself.
* French: Set layer visibility
The French catalog is coverage-checked since #291.
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: 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>
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>
`printcraft_engine::guard` wraps `catch_unwind` and returns an error message.
- `Session::apply`: edits run on a copy of the document, so a panic leaves it unchanged and is
reported as an error. A panic while refreshing rolls the edit back.
- `Session::open`, `save_bytes`, `save_full_bytes` and `export_data` report a panic as an
open, write or export error.
- `PrintCraftApp::execute`: a command that panics shows a notice, and the app and its documents
keep running.
- The desktop app installs a panic hook that logs every panic to stderr, with a backtrace when
`RUST_BACKTRACE` is set.
Autosave and crash recovery (`RecoveryStore`) were already in place for anything that escapes.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>