mirror of
https://github.com/storytold/pdfcraft.git
synced 2026-10-25 10:39:01 -06:00
f5452cf126d73ef1e080bb717bca2804489cf06a
184
Commits
| Author | SHA1 | Message | Date | |
|---|---|---|---|---|
|
|
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 <noreply@anthropic.com> 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 <noreply@anthropic.com> 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 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01JqiZ658tGndQaHao5Q6mdm --------- Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com> Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com> Co-authored-by: rp6186030 <25706807+rp6186030@users.noreply.github.com> Co-authored-by: Brandon Thomas <echelon@gmail.com> |
||
|
|
38df401687 |
M4: replace selected pages from organize toolbar and context menus (#786) (#1149)
Add 'Replace pages…' action to the Organize Pages toolbar, organize grid thumbnail context menu, and Pages panel context menu to replace selected pages. - Add ViewAction::ReplacePages triggering replace_pages_dialog() - Expose 'Replace pages…' button with 'replace' icon on the organize toolbar - Expose 'Replace pages…' on the organize grid and Pages panel thumbnail context menus - Update parity evidence for organize.replace-pages - Add integration tests for toolbar and context menu replace actions in crates/ui-egui/tests/editing.rs Co-authored-by: KrzysztofC <krzysztofc@localhost-live.lan> |
||
|
|
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 <noreply@anthropic.com> --------- Co-authored-by: FredApps <FredApps@users.noreply.github.com> Co-authored-by: Brandon Thomas <echelon@gmail.com> Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com> |
||
|
|
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 <echelon@gmail.com> |
||
|
|
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 <noreply@meta.com> * 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 <noreply@anthropic.com> * 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 <noreply@anthropic.com> * 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 <noreply@anthropic.com> --------- Co-authored-by: Muse Spark <noreply@meta.com> Co-authored-by: Brandon Thomas <echelon@gmail.com> Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com> |
||
|
|
3d2bdfe71a |
feat(i18n): add complete Turkish interface language support (#1057)
* feat(i18n): add complete Turkish interface language support * Turkish: rows for the labels that landed alongside this catalog; refresh hashes --------- Co-authored-by: Brandon Thomas <echelon@gmail.com> |
||
|
|
d1980ec99f |
M14: Kazakh interface language (#955)
* M14: Kazakh interface language A complete clean-room Kazakh (kk) translation of the interface: 2105 catalogue rows covering every English key (commands, tool catalogue, UI literals), with the plural forms and the DATE_LANGUAGES entry (months, months_short, weekdays) the other languages have. - Register 'kk' (Қазақша) in LANGUAGES; every kk-* locale resolves to it, and it appears in Preferences and the date-language picker. - Three coverage tests cloned from the Ukrainian battery: every registered command and All tools label, every tl! literal in the UI sources, and no untranslated English left (format-only strings excepted). - tests/fonts.rs covers the catalogue's Kazakh Cyrillic (әғқңөұүһі) against Inter and JetBrains Mono, which both carry the letters. - ATTRIBUTION.toml gains the kk.tsv entry (contributor-original, MIT OR Apache-2.0). cargo test -p pdfcraft-ui-egui (lib 171 + fonts 7), clippy -D warnings, cargo fmt --check and cargo xtask assets all pass. * Tests: give the Kazakh literal check its own doc comment and restore the Ukrainian one * Kazakh: rows for the labels that landed alongside this catalog; refresh hashes --------- Co-authored-by: pechnikov2026-star <pechnikov2026-star@users.noreply.github.com> Co-authored-by: Brandon Thomas <echelon@gmail.com> |
||
|
|
58ae136267 |
Add Greek (el) interface localization (#994)
* feat(i18n): add Greek localization * Greek: same place in both language lists, covered by the font test; Μαΐ * Greek: rows for the labels that landed alongside this catalog; refresh hashes --------- Co-authored-by: George Athenakis <2944375+Camthalion77@users.noreply.github.com> Co-authored-by: Brandon Thomas <echelon@gmail.com> |
||
|
|
a0d68dafd2 |
M14: Slovenian (Slovenščina) interface language (#1091)
* Add Slovenian translation * Slovene: a date language for the date picker; drop two stale PRINTCRAFT_MODELS rows * Slovene: rows for the labels that landed alongside this catalog; refresh hashes --------- Co-authored-by: spacekomet <1369225+spacekomet@users.noreply.github.com> Co-authored-by: Brandon Thomas <echelon@gmail.com> |
||
|
|
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 <noreply@anthropic.com> * Sign tests from #1099: compare against the &'static str kinds * Camera tests: undo returns a Label now --------- Co-authored-by: Fernando Areán <soporte@codebit.es> Co-authored-by: Claude Opus 5 <noreply@anthropic.com> Co-authored-by: Brandon Thomas <echelon@gmail.com> |
||
|
|
315a0cc458 |
Issue #432: global Units preference (mm, cm, in, pt), defaulting to the system region (#463)
* Issue #432: global Units preference (mm, cm, in, pt), defaulting to the system region Preferences ▸ Documents and view ▸ Units sets the unit for page sizes (Document Properties, Print), Header & Footer margins, the start of Set Page Boxes and the Measure tool on pages without a viewport scale. On first start it follows the system region: AppleMeasurementUnits on macOS, Region ▸ Measurement system (iMeasure) on Windows, then LC_ALL / LC_MEASUREMENT / LANG; inches for US, LR and MM, millimetres elsewhere. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * Issue #432: macOS falls back to AppleLocale when the measurement system was never set AppleMeasurementUnits only exists in the global domain once someone changed Measurement system in System Settings, and apps started from the Finder get no LANG, so a German Mac with untouched settings landed on inches. The region of AppleLocale (always present) decides next; AppleMeasurementUnits still wins. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01REz7wTfQgnopyvN9eQJJVu * Issue #432: Units labels in the Arabic and Italian catalogs new on main Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01REz7wTfQgnopyvN9eQJJVu * Issue #432: the Print dialog's PDF page size follows the Units preference Main's new "PDF page: {w} × {h} in" label was always in inches. It now uses the preferred unit like the sheet size next to it; each catalog keeps its wording with the size in place of the inch values. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com> Co-authored-by: Tristan von Kries <tristan@MacBook-Pro-von-Tristan.local> Co-authored-by: Brandon Thomas <echelon@gmail.com> |
||
|
|
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 <echelon@gmail.com> |
||
|
|
0c1528f17d |
feat: Scale down large signature images instead of rejecting (#1075) (#1124)
* Fill & Sign: scale down large signature images instead of rejecting them (#1075) A 6025 x 2506 PNG signature failed with "Image size exceeds limit": imports were capped at 4096 px per side and 4 megapixels, and the decoder's limit error leaked through raw. Sources up to 8192 px per side and 32 megapixels are now decoded and scaled down to fit the old bounds; anything larger reports the normal size message. Reported-by: @HassanAlthaf Co-authored-by: mjohnson7cd <mjohnson7cd@users.noreply.github.com> * A placed image too large to preview gets its own message; scale with thumbnail, which needs far less memory --------- Co-authored-by: mjohnson7cd <mjohnson7cd@users.noreply.github.com> Co-authored-by: Brandon Thomas <echelon@gmail.com> |
||
|
|
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 <noreply@anthropic.com> * 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 <noreply@anthropic.com> * 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 <noreply@anthropic.com> * 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 <noreply@anthropic.com> * 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 <noreply@anthropic.com> * 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 <noreply@anthropic.com> * Camera level: when another edit broke the run, redo what was undone instead of warping the page again --------- Co-authored-by: magdale76 <16279778+magdale76@users.noreply.github.com> Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com> Co-authored-by: Brandon Thomas <echelon@gmail.com> |
||
|
|
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 <244287281+chiranjeevi-max@users.noreply.github.com> * 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 <244287281+chiranjeevi-max@users.noreply.github.com> --------- Signed-off-by: Chiranjeevi U <244287281+chiranjeevi-max@users.noreply.github.com> |
||
|
|
21d5323132 |
Add faux-redaction audit (paint-order and colour-aware, fail-closed) (#110)
* Rework faux-redaction audit per maintainer review - Paint-order aware: a rectangle counts as a cover only when painted strictly after the text (positions from the joined content stream). - Colour-aware: WCAG contrast between text and cover must be below 3.0, so page backgrounds, table shading and highlights are not flagged. - Fail closed: audit_redactions returns Err on undecodable content streams (decoded_strict) and malformed /Annots; the operator bound is an error, not a silent truncation. - Graphics state carries across a page's content streams (one joined walk); form XObjects still isolate their state at Do. - Audit-only: the sanitize-for-sharing half is dropped (upstream #162). - Parity entry marked partial, not shipped. * Audit: register its layer, add a README, fail closed past 32 nested forms, document what it doesn't check --------- Co-authored-by: Erick Barraza <ejbarraza1994@gmail.com> Co-authored-by: Brandon Thomas <echelon@gmail.com> |
||
|
|
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 <echelon@gmail.com> |
||
|
|
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 <noreply@openai.com> * 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 <noreply@openai.com> * 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 <noreply@openai.com> * 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 <noreply@openai.com> * 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 <noreply@openai.com> * marks_present and list_added read through scan_markers, so the parsing lives in one place --------- Co-authored-by: Codex <noreply@openai.com> Co-authored-by: Brandon Thomas <echelon@gmail.com> |
||
|
|
1651069e7a |
Viewer: Continuous scrolling is an independent checkbox over the base layout (#752) (#896)
* Viewer: touchpad scrolling tracks the finger 1:1 (#759) 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: Claude Opus 4.8 <noreply@anthropic.com> * Viewer: Continuous scrolling is an independent checkbox over the base layout (#752) Page display matches Acrobat: the base layout (Single page or Two-page view) is a radio choice, and Continuous scrolling is a separate checkbox, so every combination is reachable. This also adds the previously missing "Two-page view, one spread at a time" mode. `PageLayout` now has just `Single` and `TwoUp`; a separate `continuous` flag on the view (and on the default view preferences) decides whether it scrolls. A single `display_token` ("single" | "continuous" | "two-up" | "two-page") serialises both for settings and the control channel, keeping the earlier `continuous`/`single`/`two-up` names working and adding `two-page` for the new mode. The PDF's own page-layout request now maps to the right (layout, continuous) pair, including plain TwoUp. - View ▸ Page display and the rail menu show two radios plus a Continuous scrolling checkbox; Preferences gains a default Continuous checkbox. - `view.continuous` toggles the checkbox; `view.layout.single` / `view.layout.two_up` switch the base layout without touching it. The former `view.layout.continuous` command is gone (its state is Single + continuous); the `layout`/`default-layout` options and a new `continuous` option cover scripting. - Not scrolling, the wheel and the arrow/Page keys turn whole spreads in Two-page view, and only the current page or spread is laid out. Tests: new `notches_turn_two_page_spreads`, `two_page_view_shows_one_spread_at_a_time_until_continuous`, and updated layout/command/persistence tests; the `view()` helper takes `continuous`. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> * Parity: the continuous-scrolling feature points at view.continuous --------- Co-authored-by: Justin John <babyjjbabyjj549@gmail.com> Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com> Co-authored-by: Brandon Thomas <echelon@gmail.com> |
||
|
|
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 <echelon@gmail.com> |
||
|
|
56c034b0b3 |
M9: visible digital signatures can show a drawn, typed or image graphic (#193) (#934)
* M9: visible digital signatures can show a drawn, typed or image graphic (#193) A visible certificate signature can show a graphic where the signer's name is large, as Acrobat's Configure Signature Appearance does: the saved Fill & Sign signature (drawn, typed or image) or an imported PNG/JPEG. Sign as previews it and remembers the choice; sign_document takes `graphic` (image, text or strokes). pdfcraft-sign gains `Graphic` and `SignOptions::graphic`. Graphics are checked and size-capped before drawing; images keep alpha as a soft mask and are encrypted with the rest of the update in encrypted files. Refs #193 * Refresh catalog hashes --------- Co-authored-by: Brandon Thomas <echelon@gmail.com> |
||
|
|
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 <noreply@anthropic.com> |
||
|
|
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 <noreply@anthropic.com> * 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 <noreply@anthropic.com> * 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 <noreply@anthropic.com> * 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 <noreply@anthropic.com> --------- Co-authored-by: magdale76 <16279778+magdale76@users.noreply.github.com> Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com> Co-authored-by: Brandon Thomas <echelon@gmail.com> |
||
|
|
db26eb266d |
M14: Reuse form enumeration during document open (#1011)
Co-authored-by: Brandon Thomas <echelon@gmail.com> |
||
|
|
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 <noreply@anthropic.com> * 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 <noreply@anthropic.com> * 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 <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com> |
||
|
|
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 <noreply@anthropic.com> * 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 <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com> Co-authored-by: Brandon Thomas <echelon@gmail.com> |
||
|
|
2fbce7028c |
doc_create from text refuses characters Helvetica can't draw (#959) (#1030)
doc_create with from=text wrote every character outside WinAnsiEncoding (Korean, Chinese, Japanese, emoji) as `?` with no error. It now refuses such text with the same message page_add_text already gives, naming the first undrawable character and its code point. Line breaks, page breaks and tabs are layout and still pass. create_from_text itself is unchanged, so opening .txt files, comment summaries and compare reports behave as before. |
||
|
|
ecd9d158ea | UI: preserve shared document workspace on current main | ||
|
|
8ff27dc86e | UI.9: preserve PDFCraft panels and document lifecycle | ||
|
|
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 <173904641+Tevinhead@users.noreply.github.com> Co-authored-by: Codex <noreply@openai.com> Co-authored-by: Brandon Thomas <echelon@gmail.com> |
||
|
|
9ccf77bdf6 |
M4: traverse large bookmark trees lazily without dropping later items (#876)
* M4: traverse large bookmark trees lazily without dropping later items The outline walk stopped silently at 100,000 bookmarks, and opening a document built the whole nested outline with the same cap. Both walks also recursed once per level and overflowed the stack on a deep chain. organize walks the outline lazily and iteratively with no item cap, and lists it a page at a time (bookmark_page). render keeps a bounded window of the first 10,000 bookmarks, nested at most 33 levels deep, and reports when more exist. automation's bookmark_list pages with offset and limit, and doc_info reports outline_more. The bookmarks panel says when bookmarks are left out. Co-Authored-By: Claude Haiku 5.5 <noreply@anthropic.com> * M4: keep the bookmarks panel note in English The note was a tl! label, and the coverage tests require every tl! label to exist in each complete catalogue. This PR adds no translations, so the note is now a plain English label in every locale. The i18n catalogues are restored to origin/main. Co-Authored-By: Claude Haiku 5.5 <noreply@anthropic.com> * Bookmarks: translate the panel's limit notice; a numeric destination names a page in the document --------- Co-authored-by: Claude Haiku 5.5 <noreply@anthropic.com> Co-authored-by: Brandon Thomas <echelon@gmail.com> |
||
|
|
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 <echelon@gmail.com> |
||
|
|
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 <echelon@gmail.com>
|
||
|
|
603cefe213 |
M14: Add Polish interface language (#782)
* M14: Add Polish interface language * Polish catalog: attribution entry; format the Polish date names * Polish catalog: keyboard key names * Refresh catalog hashes after merging main --------- Co-authored-by: Brandon Thomas <echelon@gmail.com> |
||
|
+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 <noreply@anthropic.com> * 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 <noreply@anthropic.com> * 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 <noreply@anthropic.com> * 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 <noreply@anthropic.com> * 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 <noreply@opencode.ai> * 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 <noreply@anthropic.com> * 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 <noreply@opencode.ai> * 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 <noreply@opencode.ai> * 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 <noreply@opencode.ai> * 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 <tristan@MacBook-Pro-von-Tristan.local> Co-authored-by: Claude Fable 5.1 <noreply@anthropic.com> * 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 <noreply@opencode.ai> * 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 <noreply@anthropic.com> * 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 <noreply@opencode.ai> * 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 <echelon@gmail.com> * 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 <echelon@gmail.com> * fix(automation): retain batch results with identical filenames (#877) * fix(automation): retain batch results with identical filenames * Format --------- Co-authored-by: Brandon Thomas <echelon@gmail.com> * M13: preserve desktop Action Wizard batch results (#886) * M13: preserve desktop Action Wizard batch results Co-Authored-By: Codex <noreply@openai.com> * ROADMAP: date the desktop action outputs entry today --------- Co-authored-by: Tevinhead <173904641+Tevinhead@users.noreply.github.com> Co-authored-by: Codex <noreply@openai.com> Co-authored-by: Brandon Thomas <echelon@gmail.com> * 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 <noreply@opencode.ai> * 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 <DWAA1660@users.noreply.github.com> Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com> Co-authored-by: Brandon Thomas <echelon@gmail.com> * XFDF keeps a callout's leader, text-box inset, intent and arrowhead (#819) (#854) Co-authored-by: Brandon Thomas <echelon@gmail.com> * 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 <tristan@MacBook-Pro-von-Tristan.local> Co-authored-by: Claude Fable 5.1 <noreply@anthropic.com> * 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 <babyjjbabyjj549@gmail.com> Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com> * 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 <noreply@anthropic.com> * 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 <DWAA1660@users.noreply.github.com> Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com> * M10.2: scan JPEG metadata beyond frame headers (#764) Co-authored-by: Codex <noreply@openai.com> Co-authored-by: Brandon Thomas <echelon@gmail.com> * 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 <noreply@anthropic.com> * 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 <noreply@anthropic.com> * 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 <gyurix@users.noreply.github.com> * 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 <lloyd@stackworx.io> Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com> * 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 <noreply@anthropic.com> Co-authored-by: Brandon Thomas <echelon@gmail.com> * 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 <echelon@gmail.com> * 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 <noreply@anthropic.com> 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 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01REz7wTfQgnopyvN9eQJJVu --------- Co-authored-by: Tristan von Kries <tristan@MacBook-Pro-von-Tristan.local> Co-authored-by: Claude Fable 5.1 <noreply@anthropic.com> * 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 <noreply@anthropic.com> * 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 <noreply@anthropic.com> * M4: keep the inspector's 4 MiB page-label cap until its labels are lazy Commit |
||
|
|
65e89aefb2 |
M3, #740: column select (Alt/Option-drag, or Edit ▸ Column select) (#765)
As in Acrobat, holding Alt (Option on macOS) when a drag starts selects only the text inside the rectangle drawn, instead of everything between two points in reading order. It works with the Select tool, the text markup tools and Redact, so a column can be copied, highlighted or redacted. - render: PageText::glyphs_in (glyph centre inside the rectangle), glyph_rects (one box per run, never covering unselected text) and column_text: rows top to bottom, side-by-side pieces separated by a tab, right to left in right-to-left rows, reading order for vertical text. - ui-egui: a selection is a reading-order run or a column rectangle; the rectangle is outlined while dragging and the cursor is a crosshair. ⇧-click clears a column selection (it has no anchor glyph). - Edit ▸ Column select (edit.column_select, quick tool "column-select") does it with a plain drag. It is the way in on Linux desktops whose window manager takes Alt-drag for moving windows (Xfce, Cinnamon, MATE, KDE Plasma 5 by default), where the app never sees the drag. - automation: text_extract takes rect for the same extraction headless. - "Column select" translated in all 15 catalogs (Arabic included); their ATTRIBUTION.toml hashes updated. - parity: new view.column-select entry with platform notes and gaps (no selection across pages, no ⇧-click extension). Windows: winit hands an Alt release to the system menu loop only when the window has a native menu, which PdfCraft's does not, so Alt-drag cannot leave the window in menu mode. Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com> Co-authored-by: Brandon Thomas <echelon@gmail.com> |
||
|
|
86dd8d8283 |
Fill colour for rectangle, oval and polygon comments (#686) (#793)
* M5: fill colour for rectangle, oval and polygon comments (#686) Rectangles, ovals, polygons and clouds could only be given a border colour. They can now be filled, or have their fill removed: - the comment quick bar has a Fill colour control for those tools ("No fill" or a swatch), kept with the other per-tool styles; - Comment Properties > Appearance has a Fill colour row, applied in the same undo step as the other changes and carried over by Make Current Properties Default; - comment_edit takes `fill` (a colour, or "none"). pdfcraft-annot gains `set_fill` (writes or removes /IC and redraws the appearance; lines, ink and locked comments refuse it) and reports `fillable`/`fill` in the comment properties. The engine adds Edit::FillAnnotation. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_019XAJjko29kzKj7ULXoXSnN * M5: update ATTRIBUTION.toml hashes for the edited i18n catalogs (#686) The fill-colour strings changed 13 translation catalogs, so xtask's the_real_manifest_passes failed on ubuntu and windows CI. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> --------- Co-authored-by: DWAA1660 <DWAA1660@users.noreply.github.com> Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com> Co-authored-by: Brandon Thomas <echelon@gmail.com> |
||
|
|
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 <hadichokr@icloud.com> Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com> Co-authored-by: Brandon Thomas <echelon@gmail.com> |
||
|
|
5ad2ecfef9 |
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 <lloyd@stackworx.io> Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com> |
||
|
|
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 <noreply@opencode.ai> |
||
|
|
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 <noreply@anthropic.com> |
||
|
|
f3f4019fad | Autosave: snapshot a new unsaved document before its first edit (#815) (#850) | ||
|
|
9cd07e4793 |
M8: redact lists of words or phrases (protect.redact-word-lists) (#757)
* M8: redact lists of words or phrases Find text and redact gains "Multiple words or phrases": a list with one word or phrase per line, typed or imported from a .txt file (Import list…), whose every match is marked in one undo step. Lists are trimmed, deduplicated and bounded (1,000 entries of 256 characters; files up to 1 MB are read). redact_mark takes words and reports matched_words, the marks each entry made. protect.redact-word-lists is shipped; whole-word and case-sensitive matching and exporting the list are still missing. * M8: translate word-list labels into Arabic The Arabic catalog arrived on main after this branch, so the new Find text and redact strings were still English and failed arabic_covers_ui_literals. * Tests: drop sharp grid renders when the thumbnail will cover the cell Zooming the organize grid back out left the sharp page raster in place until a thumbnail texture existed, so the zoom test failed when that texture had not arrived yet. * M8: Czech rows for the word-list redaction labels Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> --------- Co-authored-by: Brandon Thomas <echelon@gmail.com> Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com> |
||
|
|
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 <noreply@anthropic.com> * M4: bookmarks from tagged headings (organize.bookmarks-from-structure) Bookmark options > New bookmarks from structure (command bookmark.from_structure, tool bookmark_from_structure) turns a tagged document's headings into bookmarks, as Acrobat's New Bookmarks From Structure does for headings: H1-H6 through the role map, a plain H by its Sect/Part/Art nesting, nested by level under a new first "Untitled" bookmark, each linked to its structure element (/SE, ISO 32000-2 §12.3.3). Titles come from /ActualText, the marked text drawn on the page (decoded through the font's ToUnicode or encoding), /Alt or /T. The structure walk is bounded (depth, visits, heading and title caps, seen-set) and survives self-referencing elements; an untagged document gets an actionable error; the edit is one undo step under the assemble permission. Not yet: choosing other element types, and destinations at the heading's position rather than its page. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * Tests: size the shared raster allowance from thumbnail demand The canvas test planted a 132×171 thumbnail whose tag did not match the request finish_render_frame computes. On FreeBSD that real render finished in the same frame, so the retained raster exceeded 132×171×4. Plant a thumbnail that already satisfies demand and compare against that size. * M4: a heading's text stops gathering once it is far past the title length A heading whose marked-content list repeats one mark up to the visit cap could otherwise collect about a gigabyte before the title is cut to 256 characters. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com> Co-authored-by: Brandon Thomas <echelon@gmail.com> |
||
|
|
49a8d04f24 |
M9: sign and timestamp encrypted documents (#753)
* M9: sign and timestamp encrypted documents Signing a password-protected PDF failed with "not supported: signing encrypted documents". The writer encrypted every string, including the signature's /Contents placeholder, which the signer patches in place after hashing. - cos: a signature dictionary's /Contents is never encrypted or decrypted (ISO 32000-2 §7.6.2), identified by /Type /Sig|/DocTimeStamp or by /Contents + /ByteRange. Existing signatures in encrypted files now read their real CMS and survive rewrites of their dictionary. - sign: the blanket refusal is replaced by the document's permissions (Table 22) when opened with the user password: existing fields and document timestamps need fill-forms, new fields need annotate + modify, certifying needs modify. Refusals name the permissions password. - Tests for all four algorithms, counter-signing, tampering, permissions, document timestamps and the sign_document tool; parity entry sign.encrypted-documents; ROADMAP log. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Signed-off-by: Hadi Chokr <hadichokr@icloud.com> * M9: signing an encrypted document waits for a pending security change to be saved Otherwise the incremental save becomes a full rewrite that applies or removes protection while signing. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> --------- Signed-off-by: Hadi Chokr <hadichokr@icloud.com> Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com> Co-authored-by: Brandon Thomas <echelon@gmail.com> |
||
|
|
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 <noreply@anthropic.com> * M4: a metadata edit reads the other Info entries as the full inspection does Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> --------- Co-authored-by: Claude Haiku 5.5 <noreply@anthropic.com> Co-authored-by: Brandon Thomas <echelon@gmail.com> |
||
|
|
1c33e6f851 |
M14: Arabic interface language (#692)
* M14: Arabic interface language A 2,136-entry Arabic catalog (six CLDR plural forms, Arabic month and weekday names), selected by `ar` and every `ar-*` locale. egui joins Arabic letters but places words left to right, so the catalog is put into display order when it loads (bidi::display_rtl); placeholders, keyboard shortcuts and caption colons stay in place. The command search matches a multi-word Arabic query word by word. The craft-fonts pin moves to the revision with Noto Sans Arabic. crates/fonts/build.rs now refuses to embed Adobe type designs, so the Noto Sans CJK SC in that revision is never embedded (AGENTS.md §1.1). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * M14: Arabic catalog follows the {key} shortcut labels and has Redaction code Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com> Co-authored-by: Brandon Thomas <echelon@gmail.com> |
||
|
|
ebc5ccd4c0 |
M14: View menu, tooltips and Keyboard shortcuts show Ctrl, not ⌘, off macOS (#695)
View ▸ Zoom and Page navigation were hand-written with "⌘1", "⌘[" … while
the registered items below them were formatted by Shortcut::label, so
Linux showed both ⌘1 and Ctrl+K in one menu. Ten tooltips and the view-key
rows of Help ▸ Keyboard shortcuts hard-coded ⌘ the same way.
- The keys the document view handles itself are Shortcut constants in
ui-egui commands.rs, written with the same Shortcut::label.
- Labels follow egui's detected OS (ctx.os()) instead of cfg!, so the web
build in a Linux or Windows browser shows Ctrl too, and tests can check
both platforms.
- Translated tooltips take a {key} placeholder (13 catalogs; only the
shortcut token changed, hashes updated in ATTRIBUTION.toml).
- Fit visible is listed once in View (in Zoom), not also in the generated
list; Ctrl+3 and the palette still run it.
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Co-authored-by: Brandon Thomas <echelon@gmail.com>
|