mirror of
https://github.com/storytold/pdfcraft.git
synced 2026-10-14 19:27:04 -04:00
bde9dc52fedb6399376834ea6ab1d7452f256ece
89
Commits
| Author | SHA1 | Message | Date | |
|---|---|---|---|---|
|
|
bde9dc52fe |
honour the /Lab colour space /WhitePoint (#1203) (#1237)
* fix: recognize paragraph drafts as unsaved work (Issue #811) * fix(color): honour the /Lab colour space /WhitePoint (#1203) The old path fed every Lab value to the bundled ICC transform as if the space's white point were D50, so a /Lab space with any other /WhitePoint (e.g. D65 [0.9505 1 1.089]) rendered off by several levels per channel and a D65 blue came out 45 levels too red. Map L*a*b* to XYZ in the space's own white coordinates (ISO 32000-2 §8.6.5.4), Bradford-adapt it to D65 and convert to sRGB directly, as Poppler does. Missing or degenerate white points fall back to a flat white and hostile components are clamped without panicking. Add a render regression that checks the five reported flat patches of the D65 white-point fixture against their CIE sRGB values. * Lab: map normalised Indexed palette components onto L*/Range; drop unused LAB.icc An /Indexed palette over Lab passes its entries as byte/255 (manual_scale), which the new Lab conversion read as raw L*a*b* and drew near black. Map them onto L* 0..100 and the a*/b* /Range, with a regression test. Record the patch as hayro-interpret (25) in vendor/README.md, and remove LAB.icc (no longer referenced) with its ATTRIBUTION, NOTICE and assets README entries. Co-Authored-By: Claude Opus 5.5 <[email protected]> * Main gates: rustfmt tests/viewer.rs; a type alias for the refresh-steps array (clippy type_complexity) Co-Authored-By: Claude Opus 5.5 <[email protected]> --------- Co-authored-by: Brandon Thomas <[email protected]> Co-authored-by: Claude Opus 5.5 <[email protected]> |
||
|
|
1970935587 |
M14: edits let go of the previous working file before writing the next (gap 13) (#1279)
* M0: keep the camera edge test NaN-safe under clippy's manual_range_contains Clippy 1.97 flags `d < 0.35 || d > PI - 0.35` in the camera edge hit test. Its suggested `!(0.35..=PI - 0.35).contains(&d)` would count a NaN direction as a hit, which the two comparisons did not, so the NaN check is kept explicitly. Behaviour is unchanged. Co-Authored-By: Claude Opus 5.5 <[email protected]> Claude-Session: https://claude.ai/code/session_011AsuqvnFXhY9ASL1bA2jtm * M14: edits let go of the previous working file before writing the next (gap 13) Every edit writes a new working file: the original plus one appended revision. The previous one stayed alive in the working bytes, the display bytes and the renderer while the next was written, so after a few edits a 1 GiB file peaked at three copies. Each refresh path (full, comments/form, content, undo, redo, after save) now releases them first: the working and display bytes fall back to the original (already held by the editor) and the renderer is replaced by an empty one until the refresh rebuilds it. A refresh that still fails after rollback draws the original instead of nothing. Save still writes from the editor, never from these. Measured on a synthetic 1 GiB file (release build, Linux): five edits peak at 2.1 GiB instead of 3.1 GiB, in 8.5 s instead of 16 s. Saving still peaks at three copies; reading from disk on demand stays blocked on the renderer (hayro parses one contiguous buffer, gap 2). gaps.md, the parity notes and the roadmap record both. Co-Authored-By: Claude Opus 5.5 <[email protected]> Claude-Session: https://claude.ai/code/session_011AsuqvnFXhY9ASL1bA2jtm --------- Co-authored-by: Claude <[email protected]> |
||
|
|
84a75eae1b |
M10: Presentation (.pptx) export (#1137) (#1162)
* feat: presentation (.pptx) export (#1137) - Implement OpenXML PresentationML (.pptx) generator in crates/export/src/pptx.rs: - Generate [Content_Types].xml, _rels/.rels, core properties, presentation.xml - Build slide master, blank slide layout, and DrawingML theme - Render slides per page: largest text block as title shape, remaining paragraphs as body text box with font size, bold, italic, and color formatting - Embed images with proper DrawingML picture shapes and relationships - Generate DrawingML table shapes with column grid and span merging - Wire OfficeFormat::Pptx into crates/engine/src/compare.rs and export_office - Register export.pptx command and mark Presentation (.pptx) Ready in catalog - Support pptx in automation doc_export_office tool - Wire export.pptx into UI commands and file save dialog - Add unit and integration tests across crates/export, engine, automation, and ui-egui - Update create.export-pptx parity feature evidence and docs * pptx export: saturating slide positions, matching cell-border tags, theme ea/cs fonts, package test - Heights and the running vertical position use clamped/saturating arithmetic, so a hostile text size or image rectangle (1e300, NaN, inf) can't overflow i64; the item loop uses get(). - The table cells' a:lnR and a:lnT borders were closed with </a:lnL>, which made every slide with a table malformed XML; they now close with their own tags. - The theme's major/minor fonts carry the schema-required a:ea and a:cs children. - New test walks the produced zip entry by entry (sizes, inflation, central directory), checks every XML part's tags balance, the required parts, content-type overrides, relationship targets and that no placeholders are used; a hostile-sizes test covers the overflow. - Test literals gain main's Block/Image rotation and size fields. - Parity notes say what is still unverified. Co-Authored-By: Claude Opus 5.5 <[email protected]> --------- Co-authored-by: KrzysztofC <[email protected]> Co-authored-by: Brandon Thomas <[email protected]> Co-authored-by: Claude Opus 5.5 <[email protected]> |
||
|
|
012b34a2a5 |
M4: rename colliding form field names when combining (organize.field-name-conflicts) (#1155)
* M4: rename colliding form field names when pages are imported or combined
Insert Pages from File, Combine Files and the grouped combine registered
incoming form fields under names the destination already used, silently
blurring them into one field (fields of one fully-qualified name are one
field, so their values mix). Importing now walks both sides' fully
qualified names (parent.T.child) — including names that hang only off
page widgets, which /AcroForm never lists — and renames a colliding
field's own /T to the first free variant (Text1-1, Text1-2, …), so its
children keep their place in the tree and the second of two colliding
incoming fields is renamed too.
A field_conflicts policy (rename by default; merge keeps the older
behaviour) and reporting — ImportOutcome::renamed_fields, the combine
outcomes, and a field_name_conflicts(dst, src) pre-scan — let callers
choose and prepare; engine, tool and Combine-tab wiring follow.
* M4: engine and tool wiring for renamed field names
Insert Pages from File carries a field_conflicts policy (rename by
default) and reports what it renamed: a repair-log note names each
old → new pair and warns that scripts referring to the old names were
not rewritten, and the undo step's label counts the renames, so nothing
is slipped in silently. Undo restores the original names, as ever.
The session's combine paths gain reporting variants —
combine_unlocked_reporting, combine_grouped_reporting — returning the
bytes and the renames, and doc_combine takes field_conflicts
("rename" | "merge") and lists renamed_fields [[old, new], …] in its
result when fields were renamed, so agents can find what their scripts
must call the field now.
* M4: the Combine tab warns before combining that field names collide
The Warnings column previews, per file, the form fields whose names
collide with earlier files' and will be renamed (Text1 → Text1-1, …),
computed once per list by a new in-order pre-scan (organize
combine_field_conflicts over the whole list, engine wrapper over the
tab's byte sources; a file that cannot be opened previews as nothing,
as its row already says why). The message is plural-aware and
translated in all sixteen catalogs.
* M4: parity for renamed field names on combine and insert
organize.field-name-conflicts ships: rename by fully-qualified name
(or merge, on request), reported everywhere it happens — repair log
and undo label on insert, doc_combine's result, the Combine tab's
warning column. The notes say what is still missing: the tab's preview
ignores narrowed page ranges, the UI offers no merge choice,
Create ▸ Multiple files renames silently, and scripts and XFA bindings
that reference old names are never rewritten.
* M4: review fixes for the field-name conflict work
Four findings from the PR's code review:
- Clippy: the -D warnings gate failed on the new engine tests (a
format! with no interpolation, two drop(doc) no-ops on a
reference); the branch could not pass its own CI.
- The Combine tab's preview warned that a file split into parts
collides with itself: the preview made one source per row, while
the combine groups parts of one file and imports it once. The
preview now dedupes by group (each group's first entry stands for
the file, as the combine does), and every part shows the file's
preview.
- Collision detection compared /T bytes, so one name written in two
encodings (PDFDocEncoding, UTF-16BE) did not collide — the exact
silent blur this work exists to prevent — and renaming a UTF-16BE
name rewrote it as from_utf8_lossy mojibake. Names are now compared,
suffixed and written as decoded text (PdfString::to_text /
PdfString::text), which is also how the forms crate reads them.
- The engine's combine_unlocked/combine_grouped duplicated their
_reporting twins line for line; they now delegate with the Rename
policy, as organize's own wrappers already did.
* Field-name conflicts: only Combine and Insert from File rename
import_pages (Duplicate, Replace) and extract_pages (Extract, Split) go back to Merge, so
copies of a document's own fields keep their names and a source with two same-named field
roots isn't split apart. Pasting pages into the document they were copied from also merges;
pasting into another document renames as Insert from File does. Regression tests for
duplicate, replace, extract/split (organize), duplicate (engine) and paste (ui-egui); the
PR's rename tests call import_pages_with(Rename) explicitly.
Merge fix-up: main made edit labels a translatable Label, so the rename suffix is now two
wordings in EDIT_LABELS ("Insert pages from {name} — 1 field renamed" / "— fields
renamed"), translated in all 20 catalogs. The Combine warning's @plural row is added to
el, kk, sl and tr. Catalog hashes refreshed.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
---------
Co-authored-by: Brandon Thomas <[email protected]>
Co-authored-by: Claude Opus 5.5 <[email protected]>
|
||
|
|
7caaa35a81 |
Fix Word invoice layout and pagination (#773) (#1126)
* M10: keep invoice overlays out of Word body flow (#773) Export inferred tables without visible borders. Keep text and images positioned on pages with artwork, rotated or large text, and retain each source page's size, crop rotation and units. Cover invoice conversion through the automation tools, and handle unavailable Windows symlink setup in the confinement test. Co-authored-by: Codex <[email protected]> * M10: add @lollinoo's chosen contributor credit (#773) The contributor requested their public GitHub profile name while preparing the #773 PR. Co-authored-by: Codex <[email protected]> * Word export: float a page only when text overlaps an image or watermark positioned() turned every page with any image, or with text twice the body size, into absolutely positioned VML text boxes, so a page with a logo or a big title lost its flowing paragraphs, headings and tables. Now a page floats only when it has rotated text, or when an image or a watermark-sized text block actually overlaps other text (bounded pairwise test). Other pages flow with images inline. Adds a regression test for a page whose image and title sit clear of the text, and updates the docs and parity notes. --------- Co-authored-by: Codex <[email protected]> Co-authored-by: Brandon Thomas <[email protected]> |
||
|
|
634f3d1144 |
Issue #1028: OCR can read pictures on pages that already have text (#1184)
* Issue #1028: OCR can read pictures on pages that already have text Recognize Text skipped every page with text, so a picture of text on a page that also has real text never became searchable. The engine and the ocr_recognize tool already had skip_text_pages, but the desktop dialog never offered it, and turning it off read the page's own text again and added it a second time, so search found every word twice. - Engine: always collect the page's own glyph boxes; with skip_text_pages false, a recognised word at least half covered by them (an earlier OCR layer included) is left out. OcrPage counts those words as already_on_page. The default still skips pages with text. - Dialog: "Also recognize pages that already have text", kept with the other choices and used by Recognize text in multiple files too. - Tools: ocr_recognize reports already_on_page; ocr_recognize_files accepts skip_text_pages. - The two new labels in the 15 catalogs that translate the OCR dialog. - Synthetic tests (a picture of a sentence plus real text) for the engine, the tool and the dialog; the engine test fails with the filter turned off. Unit tests for the coverage check, including NaN, infinite, inverted and overlapping boxes. - parity, docs/roadmap.md M10 row and the ROADMAP.md progress log. Co-Authored-By: Claude Opus 5.5 <[email protected]> Claude-Session: https://claude.ai/code/session_01JqiZ658tGndQaHao5Q6mdm * Issue #1028: Czech labels for the OCR text-pages option The Czech catalog was completed (#1066) while #1028 was in review, and czech_covers_ui_literals requires every tl! literal: the two new Recognize Text labels were missing, so the test failed after the merge. Co-Authored-By: Claude Opus 5.5 <[email protected]> Claude-Session: https://claude.ai/code/session_01JqiZ658tGndQaHao5Q6mdm * Contributors: credit rp6186030 as Raj Patel in the About window Co-Authored-By: Claude Opus 5.5 <[email protected]> Claude-Session: https://claude.ai/code/session_01JqiZ658tGndQaHao5Q6mdm --------- Co-authored-by: Claude Opus 5.5 <[email protected]> Co-authored-by: copilot-swe-agent[bot] <[email protected]> Co-authored-by: rp6186030 <[email protected]> Co-authored-by: Brandon Thomas <[email protected]> |
||
|
|
931dfa0d7d |
Issue #1185: Protect refuses passwords that encryption would make empty or equal (#1186)
Encryption derives its keys from a prepared password, not the typed one:
AES-256 (R6) applies SASLprep, falls back to the password as given if
SASLprep refuses it, and truncates the UTF-8 to 127 bytes; RC4/AES-128
(R2-R4) use PDFDocEncoding cut or padded to exactly 32 bytes, and the
padding starts with "(". Protection::validate checked only the typed
strings, so a permissions password of U+00AD wrote a file that opens with
owner rights and no password, and an open password "A" with a permissions
password U+FF21 (or RC4/AES-128 passwords "a"x31 and "a"x31 + "(") wrote a
file whose open password lifts the restrictions.
pdfcraft-crypt gains effective_password(algorithm, password), the bytes the
keys are derived from (it and create share the private prepared_password
helper), re-exported from pdfcraft-cos. validate refuses a password whose
effective bytes are those of "", and an open and a permissions password
whose effective bytes are equal. Opening files is unchanged.
Fixes #1185.
|
||
|
|
82b709d516 |
XFA: import and export data files, and pictures for image fields (#1175)
Export Data on an XFA form writes its data as Acrobat does: XML (the data alone, rooted at the form's root data node, its namespaces declared) or an XDP holding the whole datasets and a reference to the PDF (.xdp, a new format). Import Data takes an XDP, a datasets or data element, or a data document whose root is the form's root data node, and makes it the form's whole data, keeping data descriptions; a laid-out form is laid out again from it (rows added and removed) and its calculations run; XFDF, FDF and tables still go the AcroForm way. Undoable. Image fields are now buttons marked /PCXfaImage (no PDF action): the forms layer treats one as an image field, so clicking it (or form_set_image) picks a picture, which is kept in the data as base64 with its xfa:contentType, as Designer saves it, and the laid-out form shows it from there. xfa: replace_data, write_data_image (hostile content types cleaned, 32 MB cap), export_data, import_data (64 MB cap, the parser's node limit, self-closing data elements). engine: export routing by document kind, import routing by the file's root element, re-layout and calculation after the data changes outside the fields, the picture's content type from its bytes. UI: XML and XDP in the export dialog of an XFA form, .xdp in the import filter. Tool texts, README and parity (form.xfa-data) updated. Hardened after an adversarial review pass: the file's data is held as a tree and written with the packet's own namespace prefix (never its markup), every packet rewrite is parsed back before it is written, files capped at 16 MB and 64 levels (a tag scan before the recursive parser, also guarding the datasets parser), pictures at 8 MB and PNG/ JPEG/GIF only, the file's shape and root name checked, UTF-16 and ISO-8859-1 decoded, pictures into data groups refused, cleaned names for pictures, static forms empty the fields the file doesn't hold, more rows than the layout cap reported, one datasets stream per edit, and only image fields take pictures into the data. |
||
|
|
6091deb829 |
M7: preserve overlapping text when dragging paragraphs (#594) (#1052)
* M7 #594: preserve ordinary paragraph drags (source-only GGEO candidate) [skip ci] Route body drags through source-preserving MoveObjects, bind gestures to the document generation, and keep added-content input ownership with the Edit panel open or closed. Add nine real-shell source regressions. Source-only transfer to the owner's fork, explicitly authorized before validation. Independently source-reviewed; formatting, builds, tests, rendering and all gates remain pending on ggeo-ai02. No upstream PR. AI-assisted with OpenAI. * M7 #594: record validated ordinary paragraph drags Format the source-reviewed candidate and record successful GGEO validation: focused UI/engine tests, exact-base source loss, native drag/history/save/reopen, independent Poppler checks, and all eight repository CI gates. Credit: @FredApps * M7: use generic validation wording * Tests: pin the clock where two saves or combines are compared byte for byte Co-Authored-By: Claude Opus 5.5 <[email protected]> --------- Co-authored-by: FredApps <[email protected]> Co-authored-by: Brandon Thomas <[email protected]> Co-authored-by: Claude Opus 5.5 <[email protected]> |
||
|
|
b91d38c073 |
M14: text the fonts can't draw is kept and reported, never written as ? silently (#1112)
* M14: text the fonts can't draw is kept and reported, never written as ? silently Form values and captions, text boxes, callouts, Fill & Sign text, measurement labels and stamp by-lines keep text the standard fonts can't draw, draw ? for it, mark the annotation (PCUndrawn) and report the edit (interface notice, tools' warnings); forms set /NeedAppearances (not in PDF/A). Flattening, redaction overlay text, typed and visible signatures and create-from-text refuse instead. Reports escape such characters as [U+XXXX]. * Catalogs: the two new can't-draw messages in Czech, Slovene, Kazakh, Turkish and Greek; refresh hashes --------- Co-authored-by: Brandon Thomas <[email protected]> |
||
|
|
c65f32a078 |
Content edits on heavy documents: narrow refresh, cached appearances, per-page invalidation (#1122)
* Content edits on heavy documents: narrow refresh, cached appearances, per-page invalidation Every content edit (drag, resize, retype) on a large document ran a full synchronous refresh on the UI thread: a whole-file re-inspection plus a redraw of every missing annotation appearance, then dropped all page rasters. On a 72 MB / 202-page CAD file with 27k appearance-less annotations each commit cost ~1.5 s before anything re-rendered. Now ~0.3 s. - New Scope::Content for edits that touch only page content, resources and added content/marks (move/resize/retype/images/added items/marks): the page tree, annotations, fields, links, outline and fonts are unchanged, so refresh_content rebuilds working bytes, display bytes and renderer and validates with a light check_pages (parse plus page count, keeping the roll-back guarantee) instead of a full inspect. added/marks are re-read only when the edit can change them; undo/redo re-read both. - AppearanceCache in pdfcraft-annot: built display appearances are reused while no annotation edit runs (content edits leave every annotation dictionary alone); cleared on comment, structural and full refreshes. set_appearance is split into build/apply halves so a hit skips the draw. - check_pages in pdfcraft-render: light post-edit validation. - Viewer: single-page content edits invalidate only their page (page_changed); other pages keep tiles, text and thumbnails. - XFA dataset sync now also runs for content edits. Co-Authored-By: Muse Spark <[email protected]> * Fonts added by a content edit show in Properties and doc_info (read again on demand); form edits clear cached appearances too * M14: DATE_LANGUAGES is a static (21 entries trip clippy's large_const_arrays); rustfmt the Greek entry Co-Authored-By: Claude Opus 5.5 <[email protected]> * Preferences scroll in a short window; tests scroll to controls below the fold (#487's roomier rows); the highlighter test waits for its mark under load Co-Authored-By: Claude Opus 5.5 <[email protected]> * Select tool: a multi-click selects only on the frame egui reports it, so a cleared selection (or the highlighter's marked word) isn't selected again The highlighter test now clicks at a human pace, with frames in between, which is what exposed it. Co-Authored-By: Claude Opus 5.5 <[email protected]> --------- Co-authored-by: Muse Spark <[email protected]> Co-authored-by: Brandon Thomas <[email protected]> Co-authored-by: Claude Opus 5.5 <[email protected]> |
||
|
|
513a1ecc3a |
i18n: let the layers below hand their sentences over, so they can be translated (#478)
* i18n: let the layers below hand their sentences over, so they can be translated The accessibility findings, the PDF/A problems, the signature details, the page-range mistakes and the edit labels were English in all ten languages, because matching their wording from the UI with a tl! literal would break silently whenever the engine changed it. So each layer now hands the wording over instead, apart from the document's own words: a WORDINGS list of every sentence it can say, with holes in it, and said(&words), which looks the wording up through the closure it is given and fills the holes in a single pass. in_english changes nothing, for the CLI and the tests; the UI passes &i18n::words, a catalog lookup. A wording with no entry comes back as it stands, which is the English, so the nine catalogs this does not touch read exactly as before. a11y::report_html takes a language tag and a words closure, so a saved report reads the way the panel read, down to its lang attribute. print::PrintError gained a variant per thing a person can be wrong about, with the numbers apart from the words; faults rather than mistakes keep their own text. Spanish translates all of it (107 entries). spanish_covers_what_the_engine_says requires an entry per wording in the eight lists and spanish_keeps_the_holes_in_what_the_engine_says requires the holes to survive the translation, so the catalog cannot fall behind the engine unnoticed. No new tl! literal, so the Chinese, French and Russian UI-literal gates are unaffected. Co-Authored-By: Claude Opus 5 <[email protected]> * Sign tests from #1099: compare against the &'static str kinds * Camera tests: undo returns a Label now --------- Co-authored-by: Fernando Areán <[email protected]> Co-authored-by: Claude Opus 5 <[email protected]> Co-authored-by: Brandon Thomas <[email protected]> |
||
|
|
5df8f3b33a |
Issue #995: unlock a restricted document with its permissions password (#1127)
A PDF protected with only a permissions (owner) password opens without asking, restricted, and nothing let the user enter that password afterwards: the security notice only led to a read-only Security tab and the hint said to reopen the file with the password, which the app had no way to do (#995, #1077). Now the security notice and Document Properties > Security offer Unlock..., and Change settings / Remove security on a restricted document ask for the permissions password first and then carry on, as Acrobat does. The open password is refused there. - crypt: SecurityHandler::authenticate_owner accepts only the owner password and refuses a different file key. - cos: Document::unlock upgrades the open document in place; owner and user passwords unlock the same file key, so nothing is decoded again. - engine: Session::unlock (not an edit: no undo step, not marked unsaved) also unlocks the undo and redo states and reopens saves as owner; Document::can_unlock, EditError::WrongPassword, command protect.unlock. - ui: the permissions password prompt shares the open prompt's modal; the notice and Security tab get Unlock...; the disabled-command hints no longer point at reopening the file. - i18n: the new strings in all 15 full catalogs (catalog hashes updated). Tests cover crypt, cos and engine (all four algorithms, wrong and open passwords, undo, redo, save, removing security afterwards) and the UI (notice flow, Remove security asking first). Closes #995 Closes #1077 Co-authored-by: Brandon Thomas <[email protected]> |
||
|
|
8f8e0236aa |
M10: Enhance camera image cleans the photo up and chooses its look (part 4 of #1008, closes #1034) (#1038)
* M10: enhance camera images: straighten a page from four corners (ocr.camera-perspective) Four corners on a page (any order, a convex quadrilateral) are warped to an upright rectangle that replaces what the page shows, read at the page picture's own resolution (at most 600 dpi) and kept at that physical size. One undoable step through Replace Pages, so comments, links and bookmarks stay. - geom: a pure 3x3 homography solve and corner ordering (no dependencies); collinear, concave and non-finite input gives None. - engine: camera::warp (bilinear, over white, capped at 40 MP) and Session::enhance_camera_page. - automation: page_enhance_camera (corners in top-left-origin points). - parity: ocr.camera-perspective is partial; the UI handles, automatic edge detection and contrast cleanup are still missing. Refs #1008. Co-Authored-By: Claude Opus 5.5 <[email protected]> * M10: Enhance camera image: drag four corners on the page, then Enhance page (ocr.camera-perspective) Scan & OCR ▸ Enhance camera image (ocr.enhance_camera) shows four corner handles on the current page, inset 5% from its edges, with what will be cut away dimmed. Enhance page straightens what they enclose through Session::enhance_camera_page (one undoable step); Cancel and Esc leave the page alone. The buttons float at the top right, clear of a centred page's corners, and the instructions are a notice, as for Crop. A command of its own, beside Enhance scanned file (ocr.enhance), which stays for scan correction (#1012). - ui-egui: camera_ui (handles, dimming, bar), QuickTool::EnhanceCamera, quick_tool "enhance-camera" for ui.state and the shot example. - Catalogs: the three new labels in every language with UI coverage (plus es, pl, zh-hant); ATTRIBUTION.toml hashes updated. - automation: page_enhance_camera is linked to the command. - parity: ocr.camera-perspective lists the command and the UI test. Refs #1008. Co-Authored-By: Claude Opus 5.5 <[email protected]> * M10: Enhance camera image finds the document's edges (ocr.camera-perspective) The corners now start on the photographed document's edges when they can be found, else inset 5% as before. camera_edges::detect_document, pure Rust and deterministic: shrink to 400 px, blur, Sobel edges thinned and thresholded against the 98th percentile; a Hough transform where each edge votes within 6 degrees of its own normal; pairs of near-horizontal and near-vertical lines make convex quadrilaterals (at least 12% of the picture), kept when every side lies on a matching edge along at least 55% of its length and scored by area times the square of the mean support; each side is then refitted through its edge points (total least squares). None when nothing convinces. - Session::detect_camera_corners (user space); page_enhance_camera finds the corners itself when `corners` is omitted and reports the corners it used and whether they were detected; the UI command starts from them. - Tests on synthetic photos: plain table, text on the sheet, wood grain with a shadow, strong perspective, a 30 degree turn, a sheet running off the picture, no document, white on white, bad input; end to end through the engine and the tool. - Parity notes: what is still missing (turning the result upright, rounded corners, clean-up). Refs #1008. Co-Authored-By: Claude Opus 5.5 <[email protected]> * M10: after Enhance page, rotate, crop or organize the page (ocr.camera-perspective) Enhance camera image no longer closes on Enhance page: the bar then offers rotate counterclockwise and clockwise for the enhanced page (one undo step each), Crop pages and Organize pages (which leave the tool for those tools), and Done; Esc leaves too. A photo of a card often comes out a quarter turn off until the page is turned upright automatically. The two icon buttons are named for screen readers. Every label already exists in the catalogs. Refs #1008. Co-Authored-By: Claude Opus 5.5 <[email protected]> * M10: Enhance camera image cleans the photo up and chooses its look (#1034) After Enhance page the bar offers a look (Original, Colour, Greyscale, Black and white) and a Clean-up slider (0-100, default Colour at 50). Choosing a look or letting go of the slider enhances the page again from the straightened photo and turns it as before, so the history keeps one enhancement (and one turn), never a pile of them. camera_clean, pure Rust: - clean: the paper's illumination is estimated per channel on a coarse grid (each block's 90th percentile, lifted to the 75th percentile of the 5x5 blocks around it so a stamp or picture isn't taken for paper, then smoothed) and divided out, so casts, shadows and darker edges go and the paper turns white; a contrast stretch grows with the level, and from 60 isolated dark pixels are removed. Colour ink stays. - apply(look): Original is the photo as taken; Greyscale is the cleaned page in grey; Black and white evens the lighting fully, thresholds the whole page (the level sets the threshold: higher keeps fainter strokes) and despeckles. Black and white is stored as PNG (lossless): JPEG would smear grey around every letter. - Session::enhance_camera_page_at(look, level); page_enhance_camera takes `mode` (original, colour, greyscale, bw) and `level` (0-100), refusing anything else. - "Clean-up", "Original", "Greyscale" and "Black and white" in every catalog with UI coverage, plus es, pl and zh-hant. - Tests: shadow and yellow cast become white paper with dark text, a blue stamp stays blue, strength grows with the level, specks go but text stays, the four looks (black and white is only black and white, the threshold moves with the level), bad sizes; a shadowed sheet through the engine; level and mode through the tool; the slider and the looks re-enhancing from the photo and keeping the turn. - Parity: ocr.background-removal, ocr.edge-shadow-removal and ocr.despeckle are partial (camera images only). Co-Authored-By: Claude Opus 5.5 <[email protected]> * M10: Enhance camera image cleans the photo up and chooses its look (#1034) After Enhance page the bar offers a look (Original, Colour, Greyscale, Black and white) and a Clean-up slider (0-100, default Colour at 50). Choosing a look or letting go of the slider enhances the page again from the straightened photo and turns it as before, so the history keeps one enhancement (and one turn), never a pile of them. camera_clean, pure Rust: - clean: the paper's illumination is estimated per channel on a coarse grid (each block's 90th percentile, lifted to the 75th percentile of the 5x5 blocks around it so a stamp or picture isn't taken for paper, then smoothed) and divided out, so casts, shadows and darker edges go and the paper turns white; a contrast stretch grows with the level, and from 60 isolated dark pixels are removed. Colour ink stays. - apply(look): Original is the photo as taken; Greyscale is the cleaned page in grey; Black and white evens the lighting fully, thresholds the whole page (the level sets the threshold: higher keeps fainter strokes) and despeckles. Black and white is stored as PNG (lossless): JPEG would smear grey around every letter. - Session::enhance_camera_page_at(look, level); page_enhance_camera takes `mode` (original, colour, greyscale, bw) and `level` (0-100), refusing anything else. - "Clean-up", "Original", "Greyscale" and "Black and white" in every catalog with UI coverage, plus es, pl and zh-hant. - Tests: shadow and yellow cast become white paper with dark text, a blue stamp stays blue, strength grows with the level, specks go but text stays, the four looks (black and white is only black and white, the threshold moves with the level), bad sizes; a shadowed sheet through the engine; level and mode through the tool; the slider and the looks re-enhancing from the photo and keeping the turn. - Parity: ocr.background-removal, ocr.edge-shadow-removal and ocr.despeckle are partial (camera images only). Co-Authored-By: Claude Opus 5.5 <[email protected]> * Camera level: when another edit broke the run, redo what was undone instead of warping the page again --------- Co-authored-by: magdale76 <[email protected]> Co-authored-by: Claude Opus 5.5 <[email protected]> Co-authored-by: Brandon Thomas <[email protected]> |
||
|
|
9644aeb9e2 |
Cap unbounded editing and text caches; index form fields once (#1115)
* Cap editing and text-layer caches that grew without bound The cos editing cache kept every touched object until save, so editing a large document pinned its whole object graph. Bound it like the scan reader: 1024 objects / 32 MB of owned bytes, 256 object streams / 32 MB, evicting to the newcomer. Returned objects outlive eviction, and clones share the caches, so this only costs re-parsing. The viewer text cache had the same shape: every visited page's text stayed for the session. Retain visible, selected and matched pages plus a 24-page prefetch window each frame; evicted pages re-extract on revisit. Skipped while a search is active, where demand still wants every page. Failures are never evicted: they cost nothing to remember. cli info on a 1024-page/147 KB file (debug build, same host/command): before 66.7 MB RSS, after 54.0 MB RSS (3 runs, tight). Tests: editor count bound, byte bound, snapshot-clone reads (cos); evict-far/keep-visible+matched+selection, search-active keep-all (canvas). All mutation-checked: each bound goes red when disabled. Signed-off-by: Chiranjeevi U <[email protected]> * Index form fields once per refresh instead of once per field The form-scope refresh ran a linear find over all fields for every field, O(F^2) on each keystroke; forms reach 10k fields. Build one name index up front, O(F). entry().or_insert() keeps the old first-match semantics for repeated field names. New test with two same-named widgets pins first-wins and goes red under insert. 141 engine tests green, clippy warnings and perf lints clean. Signed-off-by: Chiranjeevi U <[email protected]> --------- Signed-off-by: Chiranjeevi U <[email protected]> |
||
|
|
57c9e3a9b4 |
Issue #431: reload a document when its file changes on disk (#459)
* Engine: reload a document from new bytes, keeping its id * Issue #431: reload a document when its file changes on disk --------- Co-authored-by: Brandon Thomas <[email protected]> |
||
|
|
42cfc6a8f2 |
M14: scan PdfCraft page-content markers once per document refresh (#1016)
* M14: Scan PdfCraft page-content markers once on open Combine the engine-open marks and added-content discovery passes while keeping the existing public scans and document state unchanged. Add scanner equivalence and engine-open coverage, and retain the local Clippy binding needed for the workspace gate. Co-authored-by: Codex <[email protected]> * M14: reuse the combined marker scan on full refresh Keep full refresh from repeating the separate marks and added-content traversals, with a mark_saved regression against the independent scans. Co-authored-by: Codex <[email protected]> * M14: scan marker metadata without cloning content containers Borrow direct content objects and keep referenced objects alive during marker inspection. This avoids copying /Contents arrays and /PCAdded dictionaries for the combined read-only scan while preserving one-reference resolution and content-object identity. Co-authored-by: Codex <[email protected]> * M14: record combined marker scan measurements Update the #307 progress entry with full-refresh reuse, the borrowed scan implementation, and exact before/after timing and memory measurements. Co-authored-by: Codex <[email protected]> * M14: align marker measurements with final benchmark run Replace the preliminary values with the final exact-revision alternating-run medians and corresponding pair counts. Co-authored-by: Codex <[email protected]> * marks_present and list_added read through scan_markers, so the parsing lives in one place --------- Co-authored-by: Codex <[email protected]> Co-authored-by: Brandon Thomas <[email protected]> |
||
|
|
acda408192 |
Issue #568: honour the document's copy permission (Table 22, bit 5) (#914)
* Issue #568: honour the document's copy permission ISO 32000-2 §7.6.4.1 has readers restrict access to an encrypted file according to its permissions, and Table 22 bit 5 covers "Copy or otherwise extract text and graphics". PdfCraft reported the bit (Document Properties shows "Content copying: Not allowed") but ⌘C copied selected text anyway. - Engine: Document::allows_copying / check_copying, and Needs::Copying for Take a snapshot, Export to image/text/Word/HTML/RTF and Export all images. Save Image As refuses too. - UI: ⌘C moves from canvas::shortcuts to the app, which knows the document. Refused copies, snapshots and exports say why. The snapshot tool and the exports check again when they act, since the tool stays selected across tabs. - Automation: text_extract (and the MCP text resources), text_paragraphs, doc_export_text/images/all_images, doc_export_office and image_save refuse. Find, read aloud and the accessibility tools keep working: the spec exempts assistive technology, and searching isn't copying. Page extraction stays with the assemble bit, as before. Owners and unencrypted files are unaffected. * Refresh catalog hashes --------- Co-authored-by: Brandon Thomas <[email protected]> |
||
|
|
d818f72a57 |
M10: stream Create Multiple sources without a fixed file-count cutoff (#978)
Create Multiple (UI and automation, combine mode) and the page-grid drop refused or truncated selections above MAX_CREATE_FILES (1,000). The cap was the only bound on how many sources were held at once, and the combine still held every source together: all picked files were read, converted and opened before anything was combined. organize::Combiner adds one source at a time and keeps only what the rest of the combine needs: the source's bookmarks, its attachments, and its document only when it declares output intents. Session::combine_each drives it from a lazy iterator, so a source is read, copied and released before the next one is read. The desktop picker and the grid drop read files as their turn comes. The cap is removed from the engine, the automation schema and the UI. Output is byte-identical for ordinary inputs. Co-authored-by: Claude Haiku 5.5 <[email protected]> |
||
|
|
b1e958cf63 |
M10: Enhance camera image finds the document's edges; rotate, crop or organize after Enhance page (part 3 of #1008) (#1027)
* M10: enhance camera images: straighten a page from four corners (ocr.camera-perspective) Four corners on a page (any order, a convex quadrilateral) are warped to an upright rectangle that replaces what the page shows, read at the page picture's own resolution (at most 600 dpi) and kept at that physical size. One undoable step through Replace Pages, so comments, links and bookmarks stay. - geom: a pure 3x3 homography solve and corner ordering (no dependencies); collinear, concave and non-finite input gives None. - engine: camera::warp (bilinear, over white, capped at 40 MP) and Session::enhance_camera_page. - automation: page_enhance_camera (corners in top-left-origin points). - parity: ocr.camera-perspective is partial; the UI handles, automatic edge detection and contrast cleanup are still missing. Refs #1008. Co-Authored-By: Claude Opus 5.5 <[email protected]> * M10: Enhance camera image: drag four corners on the page, then Enhance page (ocr.camera-perspective) Scan & OCR ▸ Enhance camera image (ocr.enhance_camera) shows four corner handles on the current page, inset 5% from its edges, with what will be cut away dimmed. Enhance page straightens what they enclose through Session::enhance_camera_page (one undoable step); Cancel and Esc leave the page alone. The buttons float at the top right, clear of a centred page's corners, and the instructions are a notice, as for Crop. A command of its own, beside Enhance scanned file (ocr.enhance), which stays for scan correction (#1012). - ui-egui: camera_ui (handles, dimming, bar), QuickTool::EnhanceCamera, quick_tool "enhance-camera" for ui.state and the shot example. - Catalogs: the three new labels in every language with UI coverage (plus es, pl, zh-hant); ATTRIBUTION.toml hashes updated. - automation: page_enhance_camera is linked to the command. - parity: ocr.camera-perspective lists the command and the UI test. Refs #1008. Co-Authored-By: Claude Opus 5.5 <[email protected]> * M10: Enhance camera image finds the document's edges (ocr.camera-perspective) The corners now start on the photographed document's edges when they can be found, else inset 5% as before. camera_edges::detect_document, pure Rust and deterministic: shrink to 400 px, blur, Sobel edges thinned and thresholded against the 98th percentile; a Hough transform where each edge votes within 6 degrees of its own normal; pairs of near-horizontal and near-vertical lines make convex quadrilaterals (at least 12% of the picture), kept when every side lies on a matching edge along at least 55% of its length and scored by area times the square of the mean support; each side is then refitted through its edge points (total least squares). None when nothing convinces. - Session::detect_camera_corners (user space); page_enhance_camera finds the corners itself when `corners` is omitted and reports the corners it used and whether they were detected; the UI command starts from them. - Tests on synthetic photos: plain table, text on the sheet, wood grain with a shadow, strong perspective, a 30 degree turn, a sheet running off the picture, no document, white on white, bad input; end to end through the engine and the tool. - Parity notes: what is still missing (turning the result upright, rounded corners, clean-up). Refs #1008. Co-Authored-By: Claude Opus 5.5 <[email protected]> * M10: after Enhance page, rotate, crop or organize the page (ocr.camera-perspective) Enhance camera image no longer closes on Enhance page: the bar then offers rotate counterclockwise and clockwise for the enhanced page (one undo step each), Crop pages and Organize pages (which leave the tool for those tools), and Done; Esc leaves too. A photo of a card often comes out a quarter turn off until the page is turned upright automatically. The two icon buttons are named for screen readers. Every label already exists in the catalogs. Refs #1008. Co-Authored-By: Claude Opus 5.5 <[email protected]> --------- Co-authored-by: magdale76 <[email protected]> Co-authored-by: Claude Opus 5.5 <[email protected]> Co-authored-by: Brandon Thomas <[email protected]> |
||
|
|
db26eb266d |
M14: Reuse form enumeration during document open (#1011)
Co-authored-by: Brandon Thomas <[email protected]> |
||
|
|
2912b21d33 |
XFA: single-block forms paginate, open in their saved state, and show their signatures (#1098)
* XFA: forms in one wrapped block lay out across pages, with their rows and growing fields A dynamic form whose body is a single block that can't be flowed (left-to-right wrapped, positioned or fixed-height) laid out to one page, the rest drawn past the bottom edge with no warning. - A block taller than a content area is spread over the following ones. Breaks fall between fields, draws and lines of text; fills and lines are cut at the break; a field or draw moves whole, border and all. - Repeating subforms in a left-to-right wrapped container get the instances the data holds, as in the other layouts (only the first was drawn). - A multi-line text field without a fixed height grows to hold its text. Past the height of a content area it is drawn as text that continues across pages (not editable; reported as a warning) instead of one widget cut off. - Text too long for the fixed height it was given is drawn smaller, down to 70%, rather than over what lies below (the standard faces are wider than the template's). - Nested measurements carry their SOM path, so instance counts and field values are found below the first level. Synthetic tests cover each. Run: fmt, clippy and tests for pdfcraft-xfa, and the pdfcraft-engine tests; the workspace-wide gates were not run. Co-Authored-By: Claude Opus 5.5 <[email protected]> * XFA: a form opens in the state it was saved in, and wrapped sections are as tall as their content The form packet's saved presence, access and captions are applied to the template, so rows a script showed before saving appear, locked fields stay locked and a button keeps the label it was saved with. A flowed subform no longer stretches to a leftover minH: a public form whose own form packet records 3 pages laid out as 4 and now lays out as 3. Also: a text field without a border of its own has no box, and a CR LF pair is one line break. Co-Authored-By: Claude Opus 5.5 <[email protected]> * XFA: a signed signature box shows the signature the file holds A laid-out signature box takes the value and the appearance of the signed field of the same SOM name that the signing application left in the AcroForm, and is read-only. The engine no longer redraws a signed box as an empty one, on open or on a script's re-layout. Co-Authored-By: Claude Opus 5.5 <[email protected]> --------- Co-authored-by: Claude Opus 5.5 <[email protected]> |
||
|
|
dd065b39bb |
M4, #569: Combine files takes images and text files (#1032)
* M10: refuse a PNG whose header claims more than 2 GiB of pixels before allocating The png crate's limits don't cover the caller's frame buffer, which png_image sized from the header alone: a few-KB PNG claiming 60000x60000 RGBA made Create allocate and fill 14.4 GB. Apply the 2 GiB ceiling TIFF pages already have. Reached from Create, Insert pages and (with #569) Combine files. Co-Authored-By: Claude Opus 5.5 <[email protected]> * M4, #569: Combine files takes images and text files PNG, JPEG, TIFF, GIF, BMP, JPEG 2000 and .txt files become PDFs as they join the Combine files list (Add files…, drops, folder scans), so each is an ordinary row or card with its pages, thumbnails and page range. Session::as_pdf does the conversion; PDFs pass through unread (Combine reads them itself, with their passwords), as does a file named .pdf without a header, which the reader may still open. doc_combine accepts the same file types, and a converted file's bookmark drops its extension. A file that can't be read (a damaged image or PDF, a type that can't be converted) no longer is refused at the door: it joins the list with why in its Warnings (a red card in the grid). Combine then asks "Leave out files that can't be read?", naming them; Combine the rest goes on without them, Cancel changes nothing. Password-protected files still block Combine until unlocked. The picker offers "PDF, images and text", and the empty state, folder tooltips and folder notices say what is accepted; translated in every full catalog (catalog hashes updated). Co-Authored-By: Claude Opus 5.5 <[email protected]> --------- Co-authored-by: Claude Opus 5.5 <[email protected]> Co-authored-by: Brandon Thomas <[email protected]> |
||
|
|
82b1093b73 |
M7: select and move mixed PDF objects together (#844) (#907)
Preserve source glyphs and artwork, resolve the group atomically, and expose generation-scoped automation tools with selection and rendering regressions. Co-authored-by: Tevinhead <[email protected]> Co-authored-by: Codex <[email protected]> Co-authored-by: Brandon Thomas <[email protected]> |
||
|
|
9c2d9c02bf |
fix(crypt): R2–R4 passwords with non-ASCII characters open (PDFDocEncoding) (#905)
Revisions 2–4 of the standard security handler take the password in PDFDocEncoding (ISO 32000-2 §7.6.4.3.2, Algorithm 2). PdfCraft authenticates an encrypted file twice, and neither did that: - pdfcraft-crypt took Latin-1 and dropped every other character, so "ı" (0x9A) and "€" (0xA0) vanished and "ılık€" became "lk"; - the renderer (vendored hayro-syntax, "TODO: Convert to PDFDocEncoding") used the UTF-8 bytes, so any non-ASCII password failed, even "Çok güzel", which Latin-1 and PDFDocEncoding write alike; - the engine handed the renderer an owner-recovered user password as Latin-1 characters, which the renderer then read as UTF-8 again. Files that qpdf or Acrobat protected with such passwords could not be opened with the right password. - pdfcraft-crypt encodes R2–R4 passwords in PDFDocEncoding and, for a password with characters PDFDocEncoding lacks (ğ, ş, İ), also tries its UTF-8 bytes, which qpdf writes for those. The PDFDocEncoding table moves here from pdfcraft-cos (which depends on this crate) and cos uses it. - hayro-syntax (vendored patch 14) tries an R2–R4 password as given, then in PDFDocEncoding. - The engine passes a recovered user password to the renderer as UTF-8 when its bytes are UTF-8 and as PDFDocEncoding otherwise, so the renderer gets exactly those bytes back. Co-authored-by: Brandon Thomas <[email protected]> |
||
|
|
9035ae3c82 |
fix(create): read UTF-16, byte-order-marked and Windows code page text files (#903)
Text files were decoded with String::from_utf8_lossy wherever they are
read: Create PDF and Combine from a .txt file, the automation `create`
tool's text `path`, and Search & Redact's word list file. So
- a UTF-16 file (Windows Notepad's "Unicode") became NULs and replacement
characters ("??G?r?????e?" for "Grüße"),
- a UTF-8 byte order mark became a stray character at the start, and
- a Windows-1252 or 1254 file lost every accented letter ("caf�", "K��").
For a word list that means the words were never found and stayed
unredacted.
pdfcraft_create::decode_text picks UTF-8 or UTF-16 (LE/BE) by the byte
order mark and drops it, keeps valid UTF-8, and otherwise decodes the
Windows code page: 1254 when the bytes 1254 uses for Ğ İ Ş ğ ı ş appear
without á í ó ú (which Turkish never writes but Icelandic and Faroese,
the only Western languages using those bytes, always do), else 1252.
encoding_rs is already in the build through lopdf.
Co-authored-by: Brandon Thomas <[email protected]>
|
||
|
+21 |
f74c19de87 |
Combine files: move and remove single pages before combining (3 of 3, closes #611) (#790)
* Combine files: a thumbnail grid, card sizes and card actions (refs #611) Combine files shows each file as a card (the default; Grid/List kept in the settings, --combine-view): the first page it adds, stacked when it adds more, its name shortened in the middle. Cards select (click, Ctrl, Shift, arrows by card and by row), drag to reorder (one undo step; the grid scrolls at its edges) and share the list's toolbar, selection and history. Locked, unreadable, bad-range and unmeasurable files say why. Thumbnails render off the UI thread, only for cards in view: at most three render threads at once, counted until they exit (no watchdog replacements), a 32 MB texture budget, keys that never show another page, password or file, and one heavy step per frame on the web. Cards are sized from 60% to 200% (toolbar slider and buttons, Ctrl +/-/0, Ctrl+wheel or pinch over the grid, kept in the settings, --combine-zoom), keeping the card under the pointer in place. Hovering a card shows its actions over the top of its page: remove that file, and a magnifier that shows it large, flipping through the pages it adds; the same on a right click, and Space by keyboard. The toolbar wraps between groups at narrow widths. RenderPool gains retire(), without_replacements() and threads() so a caller can bound the render threads that exist at once. Co-Authored-By: Claude Opus 5.5 <[email protected]> * Combine files: expand a file into its pages (refs #611) A file that adds more than one page (up to 2000) expands into a card per page it adds, in its range's order, each labelled with its page and the file's name, on a shared tinted band: from its card's hover bar, a right click, or Expand all / Collapse all in the toolbar. A page card's magnifier opens the preview on that page; Up and Down step rows of cards. Expanding is a view: not an undo step, kept across views and zoom, and the combined file is the same either way. Thumbnails are now kept per file and page, looked up on demand (the cost follows the files, not the pages of an expanded file), and a render's document stays open for two seconds so the next page of the same file isn't read again. Adds the Lucide minimize-2 icon (ISC, recorded in ATTRIBUTION). Co-Authored-By: Claude Opus 5.5 <[email protected]> * Combine files: move and remove single pages before combining (closes #611) With a file expanded, single pages are selected (click, Ctrl, Shift), dragged anywhere and removed (a page card's trash, the toolbar, Delete), each one undo step; the files themselves are never changed. A page moved between another file's pages splits its file into parts that share its bytes: the list shows each part as a row with its own page range, and parts that end up side by side join again. A split file still counts, unlocks and combines as one file. organize gains combine_grouped (and Session::combine_grouped): each file is copied once and its pages put in the order shown, so it keeps one bookmark at its first page, its nested bookmarks, links between its own pages, its fields and one copy of its attachments. A page listed twice now gets its own copies of its annotations, and a form field's widget on it joins the same field, so the field keeps one value. The existing combine is unchanged; doc_combine gains an optional groups array (entries with the same number are one file split into parts), so the same combine is reachable headless. Co-Authored-By: Claude Opus 5.5 <[email protected]> * Progress docs: target-app parity, gaps, alpha gate, readiness by audience (#873) Progress docs per craftrules standards/progress-docs.md: target-app parity, gaps, roadmap with alpha gate, readiness by audience (full / mainstream / essentials) with Opus 5.5 hours, and feature, format, PDF-spec, hardware, UI and localization parity docs. Co-Authored-By: Claude Opus 5.5 <[email protected]> * Measure: snap to the implicit closing edge of filled paths (#803) (#862) * fix(organize): reject rotation angles that are not multiples of 90 (#801) (#833) Page rotation only turns in quarter steps, but the degree value was read with an integer division by 90 that threw the remainder away. Asking for 45 degrees saved an unrotated page, 91 and 135 saved a 90 degree turn, and the command still reported success. Reject the value before touching the document instead, so every caller gets an actionable error and the pages are left alone. The automation tool already rejected it; the single edit command did not. Fixes #801. Co-authored-by: Big Pickle <[email protected]> * Autosave: snapshot a new unsaved document before its first edit (#815) (#850) * OCR: Recognize text in multiple files refuses paths outside the root (#795) ocr_recognize_files turned a path outside the automation root into a per-file error and returned success, so no_tool_escapes_the_root reported 9 leaks once the OCR models were installed. Without them, loading the models failed first and hid it, and CI never installs them. Every path is now checked before any work starts, and one outside the root refuses the whole call, like the other tools. A missing file inside the root stays a per-file error. Nothing was ever read from outside the root. ocr_recognize_files_refuses_paths_outside_the_root checks this without the models. Co-authored-by: Claude Opus 5.5 <[email protected]> * fix(text): match phrases across repeated whitespace (#814) (#831) The phrase search collapsed runs of whitespace in the query but not in the page text it was matching against, so a document drawn with two space glyphs in a row like the issue example never matched the phrase. Single words still matched, which is why it looked like only some files were affected. Collapse whitespace while the text is flattened, the same way the query is normalised. The regression test fails on the old code because the phrase find returns an empty range, and it also covers tabs and single word queries. Fixes #814. Co-authored-by: Big Pickle <[email protected]> * fix(edit): keep a comment whose appearance is missing when flattening (#805) (#838) Viewers draw notes, FreeText, ink and stamps from their dictionary when the file stores no /AP /N. flatten only drew annotations that already had an appearance and then removed the rest, so a supported note disappeared and the saved page came back blank. Build the appearance a viewer would show for every selected, visible, non-widget annotation that has none, then draw it as usual. A subtype this build cannot render still fails to build and is handled exactly as before. The new synthetic test draws the note icon into the page and asserts it is baked in; without the fix flatten reports 0 drawn and the icon is lost. Co-authored-by: Big Pickle <[email protected]> * fix(comments): keep a reply to a reply in the comment listing (#806) (#839) comment_list collected only the replies whose /IRT pointed at the root comment, so a reply to a reply was listed nowhere at all. Roots and the root count were already right; the nested reply was simply dropped from its thread. Follow the /IRT chain to any depth, with a bound, so every reply under a comment is listed in document order. A reply to a reply now stays discoverable through the tool that promises every comment and its replies. The new test builds a root note, a direct reply and a reply to that reply, then checks both texts are listed and the two notes stay roots. It listed only the direct reply before the fix. Co-authored-by: Big Pickle <[email protected]> * Issue #774: keep the catalog out of object streams in encrypted files (Acrobat) (#836) Acrobat reports an encrypted PDF as "damaged and could not be repaired", before asking for the password, when the document catalog sits in an object stream. Every other reader tried (qpdf, pypdf, poppler, PDFKit, PdfCraft) accepts such files, and qpdf keeps the catalog standalone too. Full saves of encrypted documents now write the catalog as its own object; unencrypted output is unchanged. Verified with Acrobat DC on macOS: the same file fails with the catalog packed and opens with it standalone, in AES-256 and RC4-128. Claude-Session: https://claude.ai/code/session_01REz7wTfQgnopyvN9eQJJVu Co-authored-by: Tristan von Kries <[email protected]> Co-authored-by: Claude Fable 5.1 <[email protected]> * fix(edit): refuse a header start number that would wrap the next page (#804) (#834) Page numbering adds the start number to the position of the page in the run. At the top of the u32 range that addition wrapped, so a release build saved zero for the second page and still reported success, and a debug build hit a caught overflow error instead. Check the whole run before drawing anything. When the last page number would pass the u32 limit the operation returns an actionable error and the document keeps no header, rather than a half applied run. Fixes #804. Co-authored-by: Big Pickle <[email protected]> * OCR: a scanned euro sign reads as €, not E (#796) 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 <[email protected]> * fix(comments): include replies to replies in the comment summary (#820) (#840) 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 <[email protected]> * fix(xfdf): drop the replaced note's popup when re-importing a comment (#807) (#860) * fix(view): check Shift-Command-G before Command-G for find navigation (#810) (#835) * view: check Shift-Command-G before Command-G for find navigation (#810) * Tests: the find shortcut test waits for find by a deadline (cherry picked from commit eaf3180bd8ebb353b3d8c5ca7b9ef7548ed55c37) --------- Co-authored-by: Brandon Thomas <[email protected]> * fix(editing): recognize paragraph drafts as unsaved work (#811) (#843) * view: check Shift-Command-G before Command-G for find navigation (#810) * fix: recognize paragraph drafts as unsaved work (Issue #811) --------- Co-authored-by: Brandon Thomas <[email protected]> * fix(automation): retain batch results with identical filenames (#877) * fix(automation): retain batch results with identical filenames * Format --------- Co-authored-by: Brandon Thomas <[email protected]> * M13: preserve desktop Action Wizard batch results (#886) * M13: preserve desktop Action Wizard batch results Co-Authored-By: Codex <[email protected]> * ROADMAP: date the desktop action outputs entry today --------- Co-authored-by: Tevinhead <[email protected]> Co-authored-by: Codex <[email protected]> Co-authored-by: Brandon Thomas <[email protected]> * Fit to width fits the shown page in single-page view (#808) (#859) * fix(links): find URLs when lowercasing changes the text length (#816) (#830) find_urls case-folded a copy of the page text and then sliced the original characters with offsets taken from that copy. Case folding can change a string's length — U+0130 (İ) lowercases to "i" plus a combining dot — so the offsets drifted past the end of the source and `chars[i..j]` panicked. The CLI exited 101 before it could create the link; MCP reported an internal tool error but kept serving. Match the ASCII prefixes case-insensitively against the original characters instead, so every offset stays in the source's own index space, and use get() so no lookup can index past the end. A new test reproduces the reported panic on the old code (range start index 41 out of range for slice of length 39) and passes with the fix. pdfcraft-annot's tests, clippy and rustfmt are green. Fixes #816. Co-authored-by: Big Pickle <[email protected]> * organize: deleting the opening page drops the catalog OpenAction (#821) (#855) * fix(text): Turkish dotted and dotless i match in case-insensitive search (#892) Case-insensitive search folded the query and the page with str::to_lowercase, which follows no locale: U+0130 (İ) becomes "i" plus a combining dot above (two characters) and U+0131 (ı) has no case pair. So "istanbul" never found "İSTANBUL", and "ırmak"/"kış" never found "IRMAK"/"KIŞ" (or the reverse). The same PageText::find serves Find, Search & Redact (phrases and word lists) and the automation text search, so Turkish words could also be left unredacted. Fold I, i, İ and ı to "i" and every other character with its own lowercase, one character for one character. Case-sensitive search is unchanged. * XFDF keeps polygon and polyline vertices (#809) (#848) Export wrote polygon and polyline comments without their points and import had nowhere to read them from, so an XFDF round trip left the comments with no /Vertices and no appearance. Export now writes the points as XFDF <vertices>x,y;x,y;…</vertices>, and import reads them back (whole x,y pairs only) before the appearance is drawn. Co-authored-by: DWAA1660 <[email protected]> Co-authored-by: Claude Opus 5.5 <[email protected]> Co-authored-by: Brandon Thomas <[email protected]> * XFDF keeps a callout's leader, text-box inset, intent and arrowhead (#819) (#854) Co-authored-by: Brandon Thomas <[email protected]> * Web: warn before reloading a document with unsaved changes (#812) (#856) * Issue #813: a cancelled recovery password no longer attaches to the next unlocked document (#858) Recovering an encrypted snapshot parked its metadata in `pending_recovered` until the password prompt was answered. Cancel left it there, and the next encrypted document unlocked in the foreground took the snapshot's original path and recovery key: Save would overwrite the other file and delete its snapshot. The association now travels with the prompt (`PasswordPrompt::recovered`). Cancel puts the entry back on offer and leaves the store alone; a wrong password keeps it on the re-raised prompt; the correct one finishes the recovery of the tab that was just opened. `pending_recovered` is gone. Test: crash with an encrypted snapshot of A, Recover, Cancel, open encrypted B with its password. B keeps its own path and is clean, A's snapshot stays in the store, also after closing B. Fails on main at "B keeps its own path". Co-authored-by: Tristan von Kries <[email protected]> Co-authored-by: Claude Fable 5.1 <[email protected]> * Viewer: touchpad scrolling tracks the finger 1:1 (#759) (#868) A precision touchpad sends pixel-precise `MouseWheel` events. On Windows they arrive with no `TouchPhase::Start`, so egui never takes its no-smoothing "in touch" path and eases any delta of 8 px or more over several frames, leaving the page a frame behind the finger. The page view now applies precise (Point-unit) wheel input itself, 1:1, through the same un-eased scroll path as keyboard and middle-button auto-scroll, and suppresses egui's eased copy for a short window so its internal easing tail neither doubles nor trails the motion. A coarse mouse wheel (Line/Page units) keeps egui's smoothing, which is pleasant for chunky notches, and single-page paging still owns the vertical axis. Regression test: one 200 px precise swipe scrolls the full 200 px at once (an eased path would ramp there over ~0.1 s) and leaves no drift. Co-authored-by: Justin John <[email protected]> Co-authored-by: Claude Opus 4.8 <[email protected]> * Keep every part when bookmark titles repeat in doc_split (#800) (#863) * Windows: add reproducible security diagnostics for storytold/pdfcraft#747 (#769) Add packaging/windows/security-audit.ps1, a read-only maintainer helper that records a release file's SHA-256, Authenticode status and, with an external YARA binary and rule, every matched string identifier and offset. Document the procedure in docs/releasing.md ("Scanner detections"). No application, dependency, installer or network behaviour changes: the diff touches only the helper and the release documentation. Co-authored-by: Claude Haiku 5.5 <[email protected]> * Notice bar: the message wraps beside its buttons and lines up with them (#785) (#832) The notice bar above the pages (security, forms, XFA, repaired file, warnings, signatures) laid its message out before its buttons. In a narrow document area (a side panel open, a longer translation) the message ran under "Security settings" and on into the side panel, and it wasn't centred on the buttons. A shared notice_bar helper now lays the buttons out first, right to left, centred on a 28 px row, and gives the message the width that's left: it wraps there (cut short with the full text on hover when less than 120 px is left) and its first line is centred on the buttons' row. Regression test: editing::the_security_notice_wraps_beside_its_buttons_and_lines_up_with_them (one line, wrapped and cut-short widths; fails on main). Co-authored-by: DWAA1660 <[email protected]> Co-authored-by: Claude Opus 5.5 <[email protected]> * M10.2: scan JPEG metadata beyond frame headers (#764) Co-authored-by: Codex <[email protected]> Co-authored-by: Brandon Thomas <[email protected]> * Issue #878: Combine stores an image once when its colour space is an indirect name (#883) ImageMagick writes an image's colour space as its own object (`/ColorSpace 10 0 R`, `10 0 obj /DeviceRGB`). Each source's copy of that object was never merged, since it isn't a resource, so identical images compared unequal and every copy was kept: 8 copies of an 80 MB ImageMagick PDF combined into 648 MB instead of 81 MB. Dedupe now compares a reference to a plain value (name, number, boolean or string) as that value. The value objects themselves are still never merged. Combine, Insert Pages and Reduce File Size share this code. Co-authored-by: Claude Opus 5.5 <[email protected]> * organize: drop named destinations and their links when a page is deleted (#817) (#851) * JavaScript: limit util.printf widths and output, and array arguments (#770) util.printf accumulated a conversion's width and precision with unchecked arithmetic and no maximum, and its output had no bound; the array arguments of resetForm and the field value/colour setters were read for as many items as the array's length says, which a sparse array makes free. Saturate the numbers and refuse widths and precisions above 4096, output above 1 MiB and arrays longer than 2^20 with a script error, before anything is allocated from them. Co-authored-by: Claude Sonnet 5.5 <[email protected]> * Linux: "Use system setting" asks the desktop, not winit (#881) winit reports no system theme on Linux — `ActiveEventLoop::system_theme` is `None` there on every desktop — so `ThemePreference::System` never saw the desktop's choice and silently fell back to whatever was on screen: PdfCraft came up light on a dark desktop, and picking "Use system setting" after Dark kept it dark. The default setting did not follow the system. `pdfcraft-platform` now reads the XDG Desktop Portal's Settings interface off the UI thread and the app follows the answer, including a change made while it runs. The colour scheme is asked for under its GNOME schema name first, which GNOME's and XFCE's portals both answer, then under the older freedesktop one, which KDE answers. A desktop that answers neither leaves the user's own choice alone, exactly as before. Co-authored-by: GyuriX <[email protected]> * Extract/Combine: a page repeated in one selection gets its own annotations (#818) (#852) * M5.7: Fill & Sign tools pick up placed signatures to move and resize them (#822) A signature placed with Fill & Sign could only be moved with the Select tool. While a Fill & Sign tool was active (Sign, Initials, Add text, Check, Date...), hovering the signature showed the placement crosshair, a drag did nothing and a click stamped another mark on top of it. Now, as in Acrobat, a Fill & Sign tool picks up the Fill & Sign marks already on the page: hovering one shows the move cursor, a click selects it, a drag moves it and the selected mark's handles resize it. Away from them the tool places as before, including on other comments. Image signatures keep their live move/resize preview. Comments now carry a fill_sign flag (pdfcraft_annot::is_fill_sign for the engine's list, the same rules in the viewer's inspector), so this also works on reopened files. Co-authored-by: lloyd <[email protected]> Co-authored-by: Claude Opus 5.5 <[email protected]> * Web: opt-in embedding bridge for host pages (?host=parent) (#842) A page that frames PdfCraft on the same origin can now pass it PDFs and receive saves through postMessage, instead of Save becoming a browser download. Built for a Nextcloud integration, but nothing in it is host-specific. - ui-egui: HostSaveFn/HostDirtyFn hooks (`host_save`, `host_dirty`), None by default. On wasm, Save hands the bytes to `host_save` when set; `host_dirty` hears each frame whether any tab has unsaved work. - pdfcraft-web: `host::Bridge`, enabled only by `?host=parent` inside a frame. Messages: pdfcraft:ready / open / save / dirty / failed; only the parent window on our own origin is heard or answered. `?author=` names comment authors unless the user chose a name. - Test: host_dirty_hears_when_unsaved_work_appears_and_goes. Without `?host=parent` nothing changes. Claude-Session: https://claude.ai/code/session_01G6GtUCaPiCMEQCN4QcdQnp Co-authored-by: Claude Opus 5.5 <[email protected]> Co-authored-by: Brandon Thomas <[email protected]> * Print: a scaled annotation appearance no longer prints larger than its rectangle (#802) (#861) * Single instance: enable tab handoff in Windows portable mode (#889) (#891) * Single instance: enable tab handoff in Windows portable mode (#889) * Single instance: a portable copy's socket stays in %LOCALAPPDATA%, under its own name (cherry picked from commit c7db1e80a374e80a11504efefbaac680dfa5e872) --------- Co-authored-by: Brandon Thomas <[email protected]> * Issue #390: build the DMG with hdiutil create so the app passes codesign --strict (#823) * Issue #390: build the DMG with hdiutil create so the app passes codesign --strict makehybrid -hfs wrote a com.apple.FinderInfo (Finder location -1,-1) on every file in the image, and codesign --verify --deep --strict rejects that as "Finder information, or similar detritus", on the app in the DMG and on a copy installed from it. hdiutil create -srcfolder writes the files as they are; it attaches a device while copying, so it gets up to five tries. A gate mounts the finished image read-only and verifies the app inside with --strict. Co-Authored-By: Claude Fable 5.1 <[email protected]> Claude-Session: https://claude.ai/code/session_01REz7wTfQgnopyvN9eQJJVu * Issue #390: retry hdiutil attach and detach in the DMG gate too As suggested in review: the gate mounts the signed image under set -e, so a "Resource busy" there would stop a release after the DMG was built. The same five tries with growing pauses now cover create, attach and detach. Co-Authored-By: Claude Fable 5.1 <[email protected]> Claude-Session: https://claude.ai/code/session_01REz7wTfQgnopyvN9eQJJVu --------- Co-authored-by: Tristan von Kries <[email protected]> Co-authored-by: Claude Fable 5.1 <[email protected]> * M4: make page-label generation lazy across large documents (#845) * M4: make page-label generation lazy across large documents Page labels were expanded into one String per page, and the whole sequence was refused once the labels passed 4 MiB in total. A valid document with long labels could therefore neither be read nor edited. Add a PageLabels view over the label ranges: label(page) builds one label, and iter() builds the labels one at a time, so no string is kept per page. Edits validate each range from its largest number (Roman numerals by their thousands) instead of building every label, so the check no longer depends on the page count. The per-label length limit, the number-tree work and depth limits and the overflow checks are unchanged. page_labels() keeps its signature and returns the same labels as before for ordinary documents. Co-Authored-By: Claude Haiku 5.5 <[email protected]> * M4: drop the aggregate page-label cap from document inspection Document inspection refused every custom label once their total passed 4 MiB, so a valid document with long labels showed physical page numbers and a warning. Labels are now applied to every page. The per-label length limit, the number-tree work, depth and cycle limits and the prefix bounds are unchanged, and labels are still applied only after every range is accepted. MAX_LABEL_TOTAL_BYTES is removed. The inspector keeps one label per page (PageInfo.label), so its label memory grows with the page count times the label length. The page count is bounded by the objects in the file, and each label by 1,024 bytes. Co-Authored-By: Claude Haiku 5.5 <[email protected]> * M4: keep the inspector's 4 MiB page-label cap until its labels are lazy Commit |
||
|
|
8309c84a59 |
M12: XFA rows in areas get their instances, and areas stay out of data paths (#853)
A Designer form whose table (a top-to-bottom subform inside an area of a positioned page) asks for several rows with `<occur min="10"/>` was laid out with one row, and a FormCalc total looping over the rows stayed empty. - layout: a repeating subform inside a top-to-bottom container in positioned content is drawn and measured once per instance (occur and data), with its own SOM index, as flowed content already was. One `instances` helper now serves flowing, table rows and this case. - data: areas are named in SOM expressions but are not data scopes. `data_path(tpl, som)` leaves them out (unknown segments are kept), and layout, the script tree, reading and writing the datasets and the engine's value and row operations use it. Before, values Adobe's viewers saved were not read, what was filled here went under a new element named after the area (which Adobe's viewers would most likely not read), and scripts counted the rows there: one, so a loop over the table's rows failed on the missing ones. - write_datasets_with / read_values_with take the parsed template when the caller has it (the engine's cached one); the old functions parse the document's. - Tests: layout regression, data_path rules, and an engine round trip shaped like such a form (rows, Adobe-written values, a FormCalc total over every row, the data written back, reopening). Signed-off-by: Hadi Chokr <[email protected]> Co-authored-by: Claude Opus 5.5 <[email protected]> Co-authored-by: Brandon Thomas <[email protected]> |
||
|
|
5ab928d529 |
fix(comments): include replies to replies in the comment summary (#820) (#840)
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 <[email protected]> |
||
|
|
c7fcee4ed9 |
OCR: a scanned euro sign reads as €, not E (#796)
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 <[email protected]> |
||
|
|
f3f4019fad | Autosave: snapshot a new unsaved document before its first edit (#815) (#850) | ||
|
|
aa6242d0c6 |
M4: bookmarks from tagged headings (organize.bookmarks-from-structure) (#754)
* 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 <[email protected]> * 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 <[email protected]> * 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 <[email protected]> --------- Co-authored-by: Claude Opus 5.5 <[email protected]> Co-authored-by: Brandon Thomas <[email protected]> |
||
|
|
e4e39f4bef |
M4: keep the render pool across metadata edits (#750)
* 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 <[email protected]> * M4: a metadata edit reads the other Info entries as the full inspection does Co-Authored-By: Claude Opus 5.5 <[email protected]> --------- Co-authored-by: Claude Haiku 5.5 <[email protected]> Co-authored-by: Brandon Thomas <[email protected]> |
||
|
|
3d45b598ee | test(engine): audit edit history side effects (#713) | ||
|
|
306bbcedf9 |
M3: harden render and CLI failure surfaces (#669)
* 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 <[email protected]> * 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 <[email protected]> Co-authored-by: Brandon Thomas <[email protected]> |
||
|
|
a2f0fb7363 |
Forms: fields listed only as page widgets join the form (#649)
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 <[email protected]> Co-authored-by: Brandon Thomas <[email protected]> |
||
|
|
4fc99e4845 |
Tab strip: shrink tabs before scrolling, keep Open in sight; ignore about:blank titles (#612)
* 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 <[email protected]> * 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 <[email protected]> * 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 <[email protected]> --------- Co-authored-by: William Max & Co. <[email protected]> Co-authored-by: Claude Opus 5.5 <[email protected]> Co-authored-by: Brandon Thomas <[email protected]> |
||
|
|
e7a15783f3 |
Export to Word/HTML/RTF: read text and images drawn by form XObjects (fixes #314) (#406)
* 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 <[email protected]> |
||
|
|
1ba8b194b2 |
XFA: choice lists, signature, password, image and barcode fields; PNG and GIF pictures (#414)
* 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 <[email protected]> * 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 <[email protected]> * M7: select and edit grouped Form XObject figures Co-Authored-By: Codex <[email protected]> * M14: let neutral Windows locales fall through to display language Co-authored-by: Codex <[email protected]> * 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 <[email protected]> * 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 <[email protected]> --------- Co-authored-by: Tanish Manku <[email protected]> Co-authored-by: Claude Opus 5 <[email protected]> Co-authored-by: sanyamsingh001 <[email protected]> Co-authored-by: Codex <[email protected]> Co-authored-by: Flowershangfromthebranches <[email protected]> Co-authored-by: Codex <[email protected]> Co-authored-by: Streetblock <[email protected]> Co-authored-by: lloyd <[email protected]> Co-authored-by: Brandon Thomas <[email protected]> |
||
|
|
8e3782d5ca |
Flatten Fill & Sign marks when saving (#464)
* 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 <[email protected]> * 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 <[email protected]> * 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 <[email protected]> * 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 <[email protected]> * 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 <[email protected]> * 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 <[email protected]> * uk: the author's wording for the flatten setting --------- Co-authored-by: samehelhawary <[email protected]> Co-authored-by: Claude Opus 5.5 <[email protected]> Co-authored-by: Cody Vogt <[email protected]> Co-authored-by: William Max & Co. <[email protected]> Co-authored-by: Brandon Thomas <[email protected]> |
||
|
|
3af93c7585 |
M5: draw all ten PDF line-ending styles (#520)
* 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 <[email protected]> |
||
|
|
2aca0692fc |
Fix #526: keep text colour in Word, HTML and RTF export (#547)
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 <[email protected]> |
||
|
|
32fbef684a |
M5.7: keep natural image stamps upright on rotated pages (#473)
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 <[email protected]> |
||
|
|
07b95409bf |
Comments without an appearance stream are drawn (display only, never saved) (#371)
* 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 <[email protected]> * Test the Ink stroke on its apex row: Ink is now a curve through its points (#374) Co-Authored-By: Claude Opus 5.5 <[email protected]> --------- Co-authored-by: Claude Opus 5.5 <[email protected]> Co-authored-by: Brandon Thomas <[email protected]> |
||
|
|
2a35a1a76f |
M11: Optimize PDF and Reduce File Size run in the background with a progress bar (#370)
* 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 <[email protected]>
* 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 <[email protected]>
* Optimize: report a failed worker spawn instead of waiting forever
Co-Authored-By: Claude Opus 5.5 <[email protected]>
---------
Co-authored-by: Claude Opus 5.5 <[email protected]>
Co-authored-by: Brandon Thomas <[email protected]>
|
||
|
|
f88a59f762 |
M10.2: Create PDF from multiple files, edited as pages in the Organize grid (#361)
* 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 <[email protected]> * 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 <[email protected]> * 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 <[email protected]> * 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 <[email protected]> --------- Co-authored-by: Claude Opus 5.5 <[email protected]> Co-authored-by: Brandon Thomas <[email protected]> |
||
|
|
9140b44792 |
M4: Combine files in its own tab, with a sortable table and unlocking of protected files (#321)
* 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 <[email protected]> * 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 <[email protected]> --------- Co-authored-by: Claude Opus 5.5 <[email protected]> Co-authored-by: Brandon Thomas <[email protected]> |
||
|
|
87f635cf65 |
XFA: run FormCalc scripts (#301)
* 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 <[email protected]> --------- Co-authored-by: Brandon Thomas <[email protected]> Co-authored-by: Claude Opus 5.5 <[email protected]> |
||
|
|
22752dc915 |
Run set-layer-visibility actions from links and push buttons (#292)
* 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 <[email protected]> --------- Co-authored-by: Brandon Thomas <[email protected]> Co-authored-by: Claude Opus 5.5 <[email protected]> |
||
|
|
11ffedcc98 |
XFA: run the forms' JavaScript: calculations, validation, rows, visibility (#60) (#230)
* 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 <[email protected]> --------- Co-authored-by: Brandon Thomas <[email protected]> Co-authored-by: Claude Opus 5.5 <[email protected]> |