mirror of
https://github.com/storytold/pdfcraft.git
synced 2026-10-25 18:59:48 +00:00
refactor/shared-pdf-reader
216 Commits
| Author | SHA1 | Message | Date | |
|---|---|---|---|---|
|
|
4cb803038d |
M3: confirm before opening absolute GoToR paths (#1364) (#1489)
* M3: confirm before opening absolute GoToR paths (#1364) * fix(i18n): translate GoToR confirmation dialog labels --------- Co-authored-by: srt8hellcat2-ui <225485253+srt8hellcat2-ui@users.noreply.github.com> Co-authored-by: Brandon Thomas <echelon@gmail.com> |
||
|
|
fff985f30d |
M5: keep stamps upright on rotated pages (#1398) (#1460)
A stamp placed on a page with /Rotate was drawn in user space, so the label or picture turned with the page. Counterrotate the appearance so it reads upright as the page is displayed. Co-authored-by: Brandon Thomas <echelon@gmail.com> |
||
|
|
890b83c4c4 |
Add reviewable text-only PDF auto-tagging (#1440)
Co-authored-by: Brandon Thomas <echelon@gmail.com> |
||
|
|
759cfa65f4 |
Security: fix three audit findings in redaction and signature validation (#1631)
* M8: redaction unlinks removed images and forms from inherited and shared resources
Apply Redactions and Remove Hidden Information give a rewritten page its own
pruned /Resources, but the dictionary the page drew through before could still
name the image or form that was removed:
- a /Resources inherited from a /Pages node;
- one resource dictionary object shared by several pages;
- the page's own dictionary, when a form without /Resources drew the image
through it.
A full-rewrite save keeps whatever is still referenced, so the removed image
samples and form text stayed in the saved file while the operation reported
success. Per-page resources (the case fixed in #1099) were not affected.
The interpreter now records the original object behind every XObject it drops
or replaces. After the page loop, each page-tree resource dictionary that still
names one loses that entry unless a page drawing through that dictionary still
draws the name (the same image on another page, or again outside a mark). A
dictionary that loses an entry is replaced by a private copy on its node, so a
dictionary object shared with other nodes is left alone for them. If a page's
use of a name cannot be told (unreadable content, a form that will not decode),
the operation fails closed.
Tests: redacted_xobjects_do_not_survive_through_inherited_or_shared_resources,
an_image_another_page_still_draws_through_the_same_resources_is_kept,
an_image_a_form_drew_through_the_pages_resources_does_not_survive,
a_hidden_layers_image_does_not_survive_through_inherited_resources.
Not covered here: originals named by a form XObject's own /Resources, by an
annotation appearance or by a pattern.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* M9: a cross-reference entry the reader sets aside is never an unchanged signed object
The reader that judges post-signing changes and the renderer that draws the
page resolve some cross-reference entries differently. A revision appended
after signing could redefine a page's content stream and give the object an
entry the reader sets aside while the renderer follows it:
- an in-use entry at offset 0 (the reader skips it; the renderer fails to read
the object there, rebuilds its index and takes the last definition);
- a second entry for the same object in one section (the reader keeps the
first, the renderer the last).
The classifier compared the signed object with itself, found nothing changed
and reported the signature Valid while the page showed the new content.
pdfcraft-cos now records the entries it does not take at face value
(Document::xref_oddities: offset-0 in-use entries other than object 0,
duplicate entries that differ, reserved cross-reference stream types, and a
hybrid /XRefStm entry that disagrees with the table), and can say whether an
entry locates the object itself (Document::xref_entry_is_exact). What it
resolves is unchanged.
classify_changes treats any such entry added after the signed revision as a
change that is never permitted (page content when the object was a signed page
or its content, other changes otherwise). An object whose content compares
equal at a new place is unchanged only when its entry is exact: where the
reader had to search for the object, another reader may arrive elsewhere.
Tests: a_cross_reference_entry_the_reader_sets_aside_does_not_hide_new_page_content,
a_cross_reference_entry_that_misses_its_object_is_not_an_unchanged_object,
cross_reference_entries_that_are_set_aside_are_recorded,
an_entry_that_misses_its_object_is_found_by_searching_but_is_not_exact.
This closes the two demonstrated triggers and their near variants. It does not
prove that the two parsers agree on every file: other differences in what each
tolerates may remain.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* M8: Apply Redactions is refused when the page still shows text under a mark
Redaction decides what lies under a mark from the positions its own
interpreter computes, and its verify pass reuses them. Where those differ from
the renderer's, a mark drawn over the text as displayed lies over glyphs the
interpreter places elsewhere, or does not see at all: they stayed in the file
and the operation reported success.
The case the test pins: a font and size set through an ExtGState /Font entry
(`gs`), which the renderer honours and the interpreter does not model. The same
kind of gap was observed for a font a form XObject takes from its caller, for
some predefined CMaps and for a rotated Type 3 font matrix. The standard-14
width error that first showed it is fixed at its root by
|
||
|
|
29f4edf5be |
XFA: a script's time starts when its context is ready, and a value reaches every field bound to its data node (#1589)
Two follow-ups the maintainer asked for after #1355. The script session's worker now says when the engine is ready for the script it was sent (its JavaScript context built; FormCalc at once), and the script's time limit runs from then, with the engine's own start limit before it, as the one-shot path always did: building a context is not charged to the script, which matters on a busy machine. A field inside an area shares its data node with a field of the same name outside it (areas are left out of data paths). The in-place refresh of the live form now follows the data path through the tree and gives every field bound to the node the value the tree builder would, each by its own template field (an exclusion group there means a rebuild), and the engine's value setter, with an index of widgets by data node, gives every widget bound to the node the value by its own kind, hidden fields included, writing the data whenever the node's text differs. The script's time now also starts after its host and globals are set up. A stale parity note (choosing a picture for an image field, delivered in #1175) is removed. |
||
|
|
13fbb2f6fb |
M0: label local builds in --version, About and the package release (#1529)
A build compiled with PDFCRAFT_BUILD_LABEL=<label> shows "0.7.1 (<label>)"
in `pdfcraft --version`, `pdfcraft-cli --version`, Help > About and the
first log line (pdfcraft_engine::build_info). The version itself is
unchanged, so the update check, the MCP server info and the User-Agent
still carry the release version.
packaging/linux/package.sh puts the label in the file names
(pdfcraft-0.7.1-<label>-linux-x86_64.rpm) and makes the .rpm/.deb release
`1.<label>` (nfpm `release: ${RELEASE}`, 1 when unlabelled). rpm sorts
0.7.1-1.<label> after 0.7.1-1 and before 0.7.2-1, so a locally built
package upgrades over the installed release and the next release replaces
it. packaging/env.sh accepts only letters, digits and inner dots, which
are valid in both rpm and deb release fields.
Unlabelled builds are unchanged.
Made with Claude Code (Claude Opus 5.5, medium effort). It passes the
workspace quality gates (fmt, clippy, cargo test --workspace, xtask assets)
and was checked by hand on Fedora 44, but it has not been exhaustively
regression tested.
Co-authored-by: bmh129 <11478682+bmh129@users.noreply.github.com>
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
|
||
|
|
801a2f1488 |
fix(ui): dragged added text follows the pointer before release (#1508)
* fix(ui): dragged added text follows the pointer before release (fixes #1488) * fix(ui): keep a moved item in place until the page is redrawn (fixes #1488) |
||
|
|
55f76ab767 |
Desktop: prepare large PDFs off the UI thread (#1493)
* #307: split document open into prepare and commit * #307: move desktop open preparation off the frame thread |
||
|
|
4bfd3c2729 |
M14: add Lithuanian interface and date localization (#1401)
* M14: add Lithuanian interface localization Register Lithuanian UI and date languages with three integer plural forms and a 2,572-entry contributor-original catalog. Cover dynamic dialog labels, contextual calendar and certificate wording, commands, tools, and every key in the other bundled catalogs. Add real-shell calendar, inspector and password-dialog regressions; preserve PDF values and document state. Update localization documentation and asset attribution. Native-speaker/VLKK terminology review remains outstanding. Validation: cargo xtask ci passed; cargo-deny was skipped because it is not installed. Five rendered dialogs and contributor manual testing checked UI. Co-Authored-By: OpenAI gpt-6.1-sol <noreply@openai.com> * M14: preserve multilingual localization documentation Restore the existing language descriptions instead of replacing general localization guidance with Lithuanian-specific coverage. List every registered language and control-channel code; keep Lithuanian details alongside the existing catalog descriptions. Include the previously omitted Kazakh, Polish, Slovenian, Greek and Turkish catalogs in the shipped-language table without changing the historical coverage snapshot or existing assessments. Keep Lithuanian roadmap notes concise and additive. Validation: all 22 registered languages appear in documentation; every existing descriptive paragraph is retained. cargo xtask ci passed, with cargo-deny skipped because it is not installed. Co-Authored-By: OpenAI gpt-6.1-sol <noreply@openai.com> * M14: record completed Lithuanian native-speaker review The contributor confirms they are a native Lithuanian speaker and have reviewed the translation. Remove obsolete pending-review labels from the asset manifest and documentation; regenerate ATTRIBUTION.md. Remove the redundant generated-message disclaimer from the Lithuanian description, keeping the existing general localization documentation unchanged. Validation: cargo xtask ci passed; cargo-deny was skipped because it is not installed. Catalog content and its SHA-256 are unchanged. Co-Authored-By: OpenAI gpt-6.1-sol <noreply@openai.com> * Lithuanian: cover main's Copy Image labels, list lt last, check its glyphs Adds the six Copy Image rows from #1363, moves lt to the end of LANGUAGES and DATE_LANGUAGES (after tr), adds lt.tsv to the primary-font coverage test, writes the JavaScript preferences path with '>' as the other catalogs do (Inter has no ▸), and refreshes counts and hashes. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> --------- Co-authored-by: OpenAI gpt-6.1-sol <noreply@openai.com> Co-authored-by: Brandon Thomas <echelon@gmail.com> Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com> |
||
|
|
a6d48707f7 |
feat(preflight): verify image resolution and bleed without rewriting the file (#1408)
* feat(preflight): report image resolution and bleed without rewriting the file * feat(preflight): report image resolution and bleed without rewriting the file * feat(preflight): report image resolution and bleed without rewriting the file * feat(preflight): report image resolution and bleed without rewriting the file * feat(preflight): report image resolution and bleed without rewriting the file * feat(preflight): report image resolution and bleed without rewriting the file * feat(preflight): report image resolution and bleed without rewriting the file * feat(preflight): report image resolution and bleed without rewriting the file * feat(preflight): report image resolution and bleed without rewriting the file * feat(preflight): report image resolution and bleed without rewriting the file * feat(preflight): report image resolution and bleed without rewriting the file * feat(preflight): report image resolution and bleed without rewriting the file * test upload session tests only * Include the print-readiness module from the OCR module so the crate compiles. * Record the one-line print-readiness patch so the dispatch can be restored exactly. * Restore the print-readiness dispatch from the original sources. * M3: select an image on the page and copy it (#1363) * M3: select an image on the page and copy it Select tool: a click on a raster image selects it (also one drawn through Form artwork, or with text over it such as an OCR layer); ⌘C copies it as a file in its original format (a JPEG as stored in the PDF, otherwise a lossless PNG). Right-click ▸ Copy Image copies its pixels at full resolution, and Copy Image in Original Format the file; Edit text & images has both for the selected image. Both respect the document's copy permission. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01GWUuTDPKL5wivnSJuCkC8Q * M3: translate the copy-image strings into Czech, Kazakh, Slovene and Turkish Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01GWUuTDPKL5wivnSJuCkC8Q * Copy image: keep the clipboard file in a private per-user folder Copy Image in Original Format wrote into the shared temporary folder (/tmp/pdfcraft-clipboard) and deleted everything in it first, so another user on the machine could plant a link or folder there and have PdfCraft delete or overwrite their target. The file now goes in PdfCraft's per-user cache folder (or the portable data folder), which is checked to be a real folder and set to 0700 on Unix (refused when it isn't ours). Only earlier copies (regular files named '<stem> p<N> img<M>.jpg|png') are removed, links are never followed, and the new file is created with create_new so it can't be redirected. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * Tests: escape Windows paths in synthetic GoToR file specifications canonicalize() returns \\?\C:\... on Windows; unescaped backslashes in a PDF literal string become control characters, so link_open refused the target and main's Windows CI failed. Taken from #1359's helper fix. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> --------- Co-authored-by: Claude <noreply@anthropic.com> Co-authored-by: Brandon Thomas <echelon@gmail.com> * fix(cli): edit refuses move/insert destinations past the end (#1330) (#1400) Co-authored-by: umesh429 <3474974+umesh429@users.noreply.github.com> Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com> * fix(sign): refuse to sign a field whose /V is not a signature (#1328) (#1402) Follow-up to #1374. Signing an existing field refused a field whose /V is a signature dictionary and signed one whose /V is missing or null, but a /V that is neither (an empty string, a number, a name, an array) was treated as unsigned and overwritten with the new signature. Signing such a field by name now fails with "the value of the field ... is not a signature" and leaves the document as it is. A missing or null /V, direct or indirect, still signs, and a signed field is still refused as already signed. The signature list is unchanged: it still shows such a field as unsigned, since it carries no signature. Adds a_field_whose_value_is_not_a_signature_is_refused_by_name, which fails before this change. * fix(forms): keep a whitespace-only value in a text field without a format (#1323) (#1403) The commit-time Keystroke check returned an empty value for any whitespace-only text before looking at the field's format, so setting a space, a tab or a non-breaking space in a plain text field reported success and removed the field's value. Text and XFDF imports go through the same setter and lost the value the same way. Blank input now clears only a field with a format (number, percent, date, time, special or mask), as before; a field without one stores the text as given. An empty value still clears any field. Adds tests::whitespace_only_text_is_kept_in_a_field_without_a_format and a keystroke assertion in af::tests::numbers_format_like_acrobat, both of which fail before this change. * fix(content): skip an inline image with an unreadable key to its EI (#1318) (#1404) When a key in an inline image dictionary was not a name (`BI /W 4 H 4 ID ... EI`), the parser ended the image at the end of the stream and counted nothing as skipped, so every operator after the image was lost: text_lines found no text on a page whose text follows such an image. An image whose parameters can't be read is now skipped up to the EI that ends it, the skipped bytes are counted, and parsing resumes after it. Without an EI the image still runs to the end of the stream, as before. Adds tests::an_unreadable_inline_image_is_skipped_to_its_end, which fails before this change. * fix(automation): doc_save reports a required full rewrite as not incremental (#1329) (#1405) doc_save reported "incremental": !full, from the arguments alone. When an in-place save had to be a full rewrite (after doc_protect adds or removes encryption, or after Sanitize), write_incremental fell back to write_full, but the result still said the save was incremental. The result now says whether the written file starts with the file it was saved over, read from the new Document::saved_bytes. An ordinary in-place save still reports true and an explicit full save false. Adds an_in_place_save_reports_a_required_rewrite_as_not_incremental, which fails before this change for both doc_protect and doc_remove_hidden. * fix(audit): an identity cm between re and fill no longer hides a cover (#1333) (#1410) Co-authored-by: umesh429 <3474974+umesh429@users.noreply.github.com> Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com> * fix(ui): Current page prints the viewed page, not a page labelled with its number (#1308) (#1411) The Print dialog turned Current page into the range "N" for the page on screen, and ranges read numbers as page labels first. In a document labelled i, ii, iii, 1, 2, viewing the first page printed the fourth, whose label is "1". Current page is now passed as a position, the way Selected pages is, through print::select_listed. A typed range still reads labels first. Adds print_ui::tests::current_page_is_the_viewed_page_not_a_label, which fails before this change. * fix(render): draw Turkish Ğ İ Ş ğ ş in non-embedded fonts (vendored hayro-interpret patch 26) (#1409) A PDF whose fonts aren't embedded is drawn with PDFium's Foxit stand-ins for the standard 14 fonts. Those carry only the 230-odd glyphs of the Standard, WinAnsi and Mac Roman encodings, so a letter a font names beyond them in its /Differences mapped to .notdef and drew nothing. iText 2 writes Turkish exactly this way (Times New Roman left unembedded, /Gbreve /Idotaccent /Scedilla /gbreve /scedilla over Ð Ý Þ ð þ), and such letters showed blanks for Ğ, İ, Ş, ğ and ş while ı, ç, ö and ü drew. StandardKind now builds a Latin Extended-A letter the stand-in lacks from the base letter and accent it does have, as Type 1 seac composites and the standard-14 AFM composites are built: the accent is centred on the letter, raised over capitals and letters with ascenders (and moved along the slant in italics), cedillas hang below and ogoneks from the right edge. Composite glyph ids sit above any font's glyph count, one per character code; their width comes from the standard metrics of the name. Test: raster::tests::accented_letters_the_standard_stand_ins_lack_are_built_from_their_parts (fails without the patch: Gbreve drew no pixel). Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com> * M10: edit OCR text on scanned pages so the change shows (#1397) * M10: edit OCR text on scanned pages so the change shows Editing a recognised word on a scan rewrote it as invisible OCR text, so the page still showed the old word, and the whole OCR layer grouped into one paragraph that rewrapped when one word changed. - An edited invisible paragraph is drawn visibly, over a cover in the paper colour sampled around it, in the scan's sampled ink colour (or the chosen colour). - OCR words group into phrases: a table cell or a tab stop splits them and each scanned line stays apart; a phrase grows rightwards. - Styled rewrites measure with the text matrix's horizontal scale, so stretched OCR words keep their widths instead of overlapping. - The inline editor covers the scanned word and grows like the result. * M10: size OCR text from each word's letters A word box from recognition is tight around the ink, but the text layer assumed every box spans Helvetica's ascender-descender height. A capitals-only word such as "DT" spans only the cap height, so its text was set about 23% too small and its baseline floated up; editing it then drew the new word visibly smaller than the scanned one. The em and baseline now come from the word's letters: cap height or x-height on top, descender or baseline at the bottom. On a real scanned form, a rewritten "DT" -> "DSI" now matches the scanned word's height and baseline. The engine OCR-edit tests build their words the way a scan would box them. * Edit OCR text: sample colours only for invisible blocks, from the current bytes Session::apply looked up every edited block with a fresh text_blocks pass and re-serialized the whole document to render the sampling tile. Text blocks are now cached per page on the editor (keyed on the unmodified document's source identity, so any edit, undo or save invalidates them), visible blocks skip sampling entirely, and the tile renders from the document's current bytes. Adds a visible stretched-text regression test and documents that editing any invisible text makes it visible over a painted cover. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * Edit OCR text: name the cached text-blocks tuple (clippy), rustfmt 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> * Edit text: support per-font visual-order Indic text and nukta reordering (#1380) * Edit text: support per-font visual-order Indic text and nukta reordering - Detect legacy visual-order Indic fonts via Metrics.visual_order (fonts with Indic codepoints without OpenType GSUB tables, or legacy visual font names). - Implement Indic visual-to-logical and logical-to-visual pre-base vowel reordering in crates/edit/src/indic.rs, with support for decomposed/precomposed nuktas and conjuncts. - Apply reordering during extraction and source font reuse only when visual_order is set, leaving modern logical-order text untouched. - Add unit tests for nuktas, conjuncts, and round-trips in crates/edit/src/indic.rs and font visual_order tests in crates/edit/src/tests.rs. * Visual-order Indic: positive evidence only, ActualText kept logical, no font decoding - A font is visual order only when it is a known legacy Indic family (subset tag allowed; aps-/dv- as prefixes) with an Indic Unicode mapping. A missing GSUB is no evidence (bare CFF and subset fonts drop it), so Metrics::from_dict no longer decodes every embedded font program on every call. - Lines with /ActualText text are already logical: skip indic::to_logical, so "बढ़िया" no longer becomes "बढ़यिा". - reading_lines no longer runs bidi recovery on every line (it changed RTL reading_blocks output for all documents); only the Indic recovery remains. - indic.rs indexes through get(). - Tests use a ToUnicode fixture (not visual-order ActualText), cover an edit round-trip, logical ActualText in a legacy font, and a subset Noto font. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> --------- Co-authored-by: Brandon Thomas <echelon@gmail.com> Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com> * M12: preview text structure proposals for auto-tagging (#1383) * M12: preview text structure proposals for auto-tagging * Auto-tag preview: no unregistered command, allowed on copy-restricted files like a11y.check The preview is read-only with no UI command yet, like render_preview, so it no longer names the Planned a11y.autotag catalog id (tool_table_is_well_formed). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * M12: preview text structure proposals for auto-tagging * Keep read-only auto-tag preview separate from unavailable command --------- Co-authored-by: Brandon Thomas <echelon@gmail.com> Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com> * M10: Test current-page print previews and saved output (#1415) Extend the coverage of #1411 with preview raster selection and a dialog-driven PDF save test. Reopen and render the saved PDF to verify the viewed physical page when labels restart. Cover duplicate labels and selection guards without changing production code. Refs #1308, #1411 Co-authored-by: LabDennis <341377186+LabDennis@users.noreply.github.com> * fix(content): count an unreadable inline image with no EI as skipped (#1318) (#1418) Follow-up to #1404. An inline image whose parameters can't be read and that has no EI after them ran to the end of the stream as a BI op with no data, and those bytes were not counted. It is now skipped like one with an EI, its bytes counted in Parsed::skipped. A stream that ends inside the parameters (BI /W 1) still keeps its BI op. * feat(ui): add command palette arrow-key navigation (#1419) * M4: Compare and cache actual Combine file duplicates (#1420) Different equal-sized PDFs with the same name no longer receive duplicate warnings. Share exact-content detection between Grid and List, and cache the result using the existing list revision to remove the per-redraw all-pairs scan. Cover distinct contents, actual duplicates, source parts, cache reuse, removal, undo, redo and closing the tab. Both UI regressions fail before the fix. Fixes #1303 Co-authored-by: LabDennis <341377186+LabDennis@users.noreply.github.com> * feat(preflight): verify image resolution and bleed without rewriting the file (#1408) Squashed from the PR branch (its history added and removed a workflow file; the final tree has no .github changes). * printcheck: proper engine module, one TrimBox note, count unmeasurable images Declares printcheck in engine/src/lib.rs instead of a #[path] module inside ocr.rs. Pages without a TrimBox get one informational document-level finding instead of a bleed warning per page; bleed is only measured against a real TrimBox. Placements with a non-finite or degenerate matrix are counted as resolution unknown (info finding, text line and JSON field) rather than passing every check. Adds a print_readiness test through Automation::call. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> --------- Co-authored-by: Francesco Clemeno <checcomeno@gmail.com> Co-authored-by: Claude <noreply@anthropic.com> Co-authored-by: Brandon Thomas <echelon@gmail.com> Co-authored-by: umesh429 <umesh.ghanti@gmail.com> Co-authored-by: umesh429 <3474974+umesh429@users.noreply.github.com> Co-authored-by: Marco Arnulfo <103679419+marcoarnulfo@users.noreply.github.com> Co-authored-by: TheLastRoadRunner <99679216+TheLastRoadRunner@users.noreply.github.com> Co-authored-by: Tangu Achilis <43493154+TanguAchilis@users.noreply.github.com> Co-authored-by: krinry <121806532+krinry@users.noreply.github.com> Co-authored-by: Earl Co <earlvanze@gmail.com> Co-authored-by: LabDennis <dennis@leistenschneider.pro> Co-authored-by: LabDennis <341377186+LabDennis@users.noreply.github.com> Co-authored-by: Pratham Bhanushali <134723366+Morpheus004@users.noreply.github.com> |
||
|
|
2839152af1 |
M12: preview text structure proposals for auto-tagging (#1383)
* M12: preview text structure proposals for auto-tagging * Auto-tag preview: no unregistered command, allowed on copy-restricted files like a11y.check The preview is read-only with no UI command yet, like render_preview, so it no longer names the Planned a11y.autotag catalog id (tool_table_is_well_formed). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * M12: preview text structure proposals for auto-tagging * Keep read-only auto-tag preview separate from unavailable command --------- Co-authored-by: Brandon Thomas <echelon@gmail.com> Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com> |
||
|
|
5cce1b247f |
M10: edit OCR text on scanned pages so the change shows (#1397)
* M10: edit OCR text on scanned pages so the change shows Editing a recognised word on a scan rewrote it as invisible OCR text, so the page still showed the old word, and the whole OCR layer grouped into one paragraph that rewrapped when one word changed. - An edited invisible paragraph is drawn visibly, over a cover in the paper colour sampled around it, in the scan's sampled ink colour (or the chosen colour). - OCR words group into phrases: a table cell or a tab stop splits them and each scanned line stays apart; a phrase grows rightwards. - Styled rewrites measure with the text matrix's horizontal scale, so stretched OCR words keep their widths instead of overlapping. - The inline editor covers the scanned word and grows like the result. * M10: size OCR text from each word's letters A word box from recognition is tight around the ink, but the text layer assumed every box spans Helvetica's ascender-descender height. A capitals-only word such as "DT" spans only the cap height, so its text was set about 23% too small and its baseline floated up; editing it then drew the new word visibly smaller than the scanned one. The em and baseline now come from the word's letters: cap height or x-height on top, descender or baseline at the bottom. On a real scanned form, a rewritten "DT" -> "DSI" now matches the scanned word's height and baseline. The engine OCR-edit tests build their words the way a scan would box them. * Edit OCR text: sample colours only for invisible blocks, from the current bytes Session::apply looked up every edited block with a fresh text_blocks pass and re-serialized the whole document to render the sampling tile. Text blocks are now cached per page on the editor (keyed on the unmodified document's source identity, so any edit, undo or save invalidates them), visible blocks skip sampling entirely, and the tile renders from the document's current bytes. Adds a visible stretched-text regression test and documents that editing any invisible text makes it visible over a painted cover. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * Edit OCR text: name the cached text-blocks tuple (clippy), rustfmt 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> |
||
|
|
77c3f2003a |
fix(automation): doc_save reports a required full rewrite as not incremental (#1329) (#1405)
doc_save reported "incremental": !full, from the arguments alone. When an in-place save had to be a full rewrite (after doc_protect adds or removes encryption, or after Sanitize), write_incremental fell back to write_full, but the result still said the save was incremental. The result now says whether the written file starts with the file it was saved over, read from the new Document::saved_bytes. An ordinary in-place save still reports true and an explicit full save false. Adds an_in_place_save_reports_a_required_rewrite_as_not_incremental, which fails before this change for both doc_protect and doc_remove_hidden. |
||
|
|
0cb311d21c |
M3: select an image on the page and copy it (#1363)
* M3: select an image on the page and copy it Select tool: a click on a raster image selects it (also one drawn through Form artwork, or with text over it such as an OCR layer); ⌘C copies it as a file in its original format (a JPEG as stored in the PDF, otherwise a lossless PNG). Right-click ▸ Copy Image copies its pixels at full resolution, and Copy Image in Original Format the file; Edit text & images has both for the selected image. Both respect the document's copy permission. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01GWUuTDPKL5wivnSJuCkC8Q * M3: translate the copy-image strings into Czech, Kazakh, Slovene and Turkish Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01GWUuTDPKL5wivnSJuCkC8Q * Copy image: keep the clipboard file in a private per-user folder Copy Image in Original Format wrote into the shared temporary folder (/tmp/pdfcraft-clipboard) and deleted everything in it first, so another user on the machine could plant a link or folder there and have PdfCraft delete or overwrite their target. The file now goes in PdfCraft's per-user cache folder (or the portable data folder), which is checked to be a real folder and set to 0700 on Unix (refused when it isn't ours). Only earlier copies (regular files named '<stem> p<N> img<M>.jpg|png') are removed, links are never followed, and the new file is created with create_new so it can't be redirected. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * Tests: escape Windows paths in synthetic GoToR file specifications canonicalize() returns \\?\C:\... on Windows; unescaped backslashes in a PDF literal string become control characters, so link_open refused the target and main's Windows CI failed. Taken from #1359's helper fix. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> --------- Co-authored-by: Claude <noreply@anthropic.com> Co-authored-by: Brandon Thomas <echelon@gmail.com> |
||
|
|
9d6552291b |
Issue #1296: hayro-syntax applies SASLprep to AES-256 passwords (PdfCraft patch 16) (#1345)
Revisions 5-6 of the standard security handler key the file on the
password processed with SASLprep (ISO 32000-2 7.6.4.3.3, Algorithm 2.A
step a). pdfcraft-crypt writes and reads it so, but the vendored
hayro-syntax skipped the step ("TODO: Do the above.") and used the UTF-8
bytes as typed. A password that SASLprep changes (a decomposed "cafe" +
U+0301, a no-break space, fullwidth letters typed by an IME in full-width
mode) was wrong for the renderer: Protect with such an open password was
rolled back ("the password is incorrect"), and an AES-256 file with such a
password did not open in a session with it, although pdfcraft-cos
accepted it.
- hayro-syntax (vendored patch 16) tries an R5/R6 password in its SASLprep
form (as pdfcraft-crypt computes it: as given if SASLprep refuses it,
UTF-8, at most 127 bytes), then, when that differs, as given.
stringprep, already in the tree through pdfcraft-crypt, is an optional
dependency enabled by the std feature; without std nothing changes.
- Tests: hayro-syntax crypto::pdfcraft_password_tests (candidate list and
R5/R6 dictionaries built from prepared passwords) and two engine tests
(Protect and reopen, and pdfcraft-cos-encrypted files) for the three
cases, with precomposed "caf\u{E9}" as the control. The dictionary test
and both engine tests fail without the fix.
Fixes #1296.
|
||
|
|
4b45fc9cb4 |
XFA: one script thread per event, and the live form kept up to date in place (#1355)
* XFA: one script thread per event, and the live form kept up to date in place A form with hundreds of calculate scripts used to spend its open-time budget on set-up: every script got a thread of its own, and the engine dropped its live form after every value a script settled, rebuilding the whole tree for the next script. The js crate gains xfa_session::XfaSession: the scripts of one event run on one thread (with the script stack), each in a fresh JavaScript context or FormCalc interpreter on a copy of the form as the engine holds it then, so scripts share nothing, a script that fails halfway leaves nothing behind, and what a script changed reaches the next one only through the engine. A script past its time limit is abandoned on the thread and the session is dead from then on; a fault in the engine (a panic) loses only that script, the thread is replaced. On wasm the scripts run inline, as before. The engine's runner keeps one session per event and, for a value, now refreshes the one field in its live form in place (xfa::refresh_value, computing the value as the tree builder would: saved values, template defaults) instead of rebuilding the tree; rows, presence, access, resets, relayouts and data rewrites still rebuild it. Under test, the live form is compared with a fresh build before every script and at the end of every event, and a difference ends the test binary (a panic would be caught by the engine's guard and turned into a script error). 210 calculate scripts (one per row of 10 sections x 20 rows, plus a sum per section), debug build, instrumented inside the engine: the script event on open 495 ms -> 204 ms (of which live-form rebuilds 217 ms -> 0), on a change 300 ms -> 172 ms; through the tools, layout included, open 1.27 s -> 0.97 s and a change 0.44 s -> 0.22 s. The rebuild cost removed grew with form size times script count. * CI: the GoToR tests' case lists are only grown on Unix, so Windows must not warn on their mut |
||
|
|
0e55d07b5f |
M12: say when a form's scripts were turned off (#1251)
* M12: say when a form's scripts were turned off When an XFA script runs past its time limit it is abandoned and the form's scripts are turned off for the document. The only trace was an error line in the JavaScript console, so calculations and buttons just stopped working. - Engine: Document::xfa_scripts_off() reports the state (set on open and on an edit), so a viewer shows it without reading error text. doc_info reports it to agents as xfa_scripts_off. - App: when it happens (at open or after a click) a toast says so once, and the notice bar says so from then on, ahead of its other notices, with a JavaScript Console button. A notice bar the user had closed comes back for it; closed again, it stays closed. - The sentence is translated in the 17 catalogs that are complete (hashes in ATTRIBUTION.toml updated). Tests: two kittest UI tests (runaway at open; runaway click after the bar was closed), which fail without the app change; engine assertions for both states; a doc_info tool test. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * Catalogs: the scripts-off notice in Greek, Polish and Telugu; refresh hashes --------- Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com> Co-authored-by: Brandon Thomas <echelon@gmail.com> |
||
|
|
7d419a6c73 |
feat(edit): remove all links command and context menu (#539) (#1213)
* feat(edit): remove all links command and context menu (#539) - Expose 'edit.remove_links' and 'edit.remove_all_links' commands to remove all links across the document or on specified pages. - Add right-click context menu options for links: Properties, Delete, and Remove all links in both Link tool and Select tool modes. - Extend automation tool 'links_remove' with optional 'pages' parameter and removal count. - Add notification feedback in UI for link deletion. - Mark 'edit.remove-all-links' as shipped in Parity with evidence tests. - Add unit and integration tests covering automation, engine, and UI undo/redo. * Remove all links: one command id, catalog rows, link menu only on a link - Drop the duplicate edit.remove_all_links command (same label as the existing edit.remove_links, so it showed twice); edit.remove_links now runs remove_all_links with its toast. - Add the toast strings (No links to remove, Removed 1 link, Removed {n} links) to all 20 catalogs. - The canvas context menu shows the link items only when the right-click hit a link (LinkView::context, set per right-click), so a link left selected no longer takes over the comments menu, and the Link tool no longer replaces the comments menu away from links. Regression test. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * Restore the PR's links_remove pages test (lost with the workdir conflict) 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> |
||
|
|
4541974ef9 |
Reading colours: replace document colours for reading comfort (#897) (#1270)
* test: give faux-redaction audit its own workdir (parallel-test flake) * hayro: ColorReplacement type and luminance mapping (reading colours, #897) * fmt: engine dates (upstream Greek localization miss) * hayro: apply ColorReplacement to solid text and line-art paints (#897) * render: ReadingColors view setting flows into RenderSettings (#897) * engine: per-document reading colours state (#897) * automation: view_reading_colors tool; page_render honours it (#897) * ui: Reading colours preference (Replace Document Colors, #897) * ui: seed reading colours on revision and signed-version opens (#897) * parity: a11y.replace-page-colors shipped (#897) * render: map stencil-mask paints through reading colours (#897) An ImageMask stencil's solid paint was baked into the raster bytes raw, bypassing the reading-colours hook every other solid fill goes through: under white-on-black, CCITT scans, JBIG2 text and stencil logos drew black ink on the black mapped background and vanished. Route it through replaced(paint, false) — stencils are line art in disguise — and cover it with an ImageMask fixture test sampling mapped ink on a mapped background. * render: print previews render with reading colours off (#897) Print previews went through the document's render pool, whose config now carries the reading colours: a white-on-black user saw black sheets in the Print dialog while the actual print stayed uncoloured. The request tag is the per-request channel that already crosses the render boundary, so PRINT_TAG now lives in pdfcraft_render and the renderer honours it by rendering previews with reading colours off — a preview's job is fidelity to the printout. The main view stays recoloured. Covered on both render paths (inline PageRenderer and pool worker). * ui: seed reading colours on created-document opens (#897) open_created (comment summaries) and open_created_bytes (images, text, blank pages, clipboard) pushed new DocViews without seeding the reading colours, unlike every other open path. A created document now starts with the app-wide preference too; the seed-on-open test drives both paths. * docs: reading-colours flag echo and display-fidelity parity notes (#897) The view_reading_colors description now says a flag that differs from the mode's preset default makes the echoed mode read custom, and the a11y.replace-page-colors parity notes record that print previews and view snapshots render with the document's true colours: reading colours apply to the reading view only. * Merge follow-ups: reading colours beside preview renders and the new catalogs - Renderer::new carries both color_replacement and the new preview flag - our raster tests construct RenderRequest with preview: false - the preference dialog grew past the window: scroll the combo into view and give the fixture a valid xref so no repair banner covers the dialog - checkboxes use the crate's square-box widget (clippy disallows egui's) - Kazakh and Turkish catalogs gain the ten reading-colours strings * Reading colours: record the hayro patch in vendor/README.md; add missing translations vendor/README.md gains hayro patch (13): `RenderSettings::color_replacement` / `ColorReplacement`, the `render_pdf` signature change and the removed `[dev-dependencies.log]`, with its tests. Adds the ten reading-colours labels to el, es, pl, te and zh-hant. 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> |
||
|
|
c1d98946a9 |
M7: open local GoToR PDF links natively (#1265)
* M7: open local GoToR PDF links natively * GoToR links: read only regular .pdf files under a size cap; add missing translations A GoToR link in an untrusted PDF could name /dev/zero (unbounded read), a FIFO (UI thread hangs) or a directory. Both the UI path and the `link_open` tool now go through `pdfcraft_engine::read_remote_document`, which requires a `.pdf` extension and a regular file (metadata checked before opening) of at most 1 GiB, and stops reading if the file grows past the cap. Regression tests cover directories, a /dev/zero symlink and a FIFO (unix) in the engine, the tool and the UI. Adds the four link notifications to all 20 catalogs and "The existing action will be preserved." to el, es, pl, te and zh-hant. 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> |
||
|
|
b1784fc2d9 |
Accessibility: add visual tag properties panel (#1216)
* Expose tag parent and depth for hierarchy view * Accessibility: add visual tag properties panel * Show collapsible tag hierarchy in accessibility panel * Open checker findings in the matching structure tag * Translate Tags panel across supported languages * Tags panel: walk the structure tree once per edit, not every frame The Tags panel and checker findings that open a tag share a tag list cached per document and edit generation. --------- Co-authored-by: Brandon Thomas <echelon@gmail.com> |
||
|
|
bde9dc52fe |
honour the /Lab colour space /WhitePoint (#1203) (#1237)
* fix: recognize paragraph drafts as unsaved work (Issue #811) * fix(color): honour the /Lab colour space /WhitePoint (#1203) The old path fed every Lab value to the bundled ICC transform as if the space's white point were D50, so a /Lab space with any other /WhitePoint (e.g. D65 [0.9505 1 1.089]) rendered off by several levels per channel and a D65 blue came out 45 levels too red. Map L*a*b* to XYZ in the space's own white coordinates (ISO 32000-2 §8.6.5.4), Bradford-adapt it to D65 and convert to sRGB directly, as Poppler does. Missing or degenerate white points fall back to a flat white and hostile components are clamped without panicking. Add a render regression that checks the five reported flat patches of the D65 white-point fixture against their CIE sRGB values. * Lab: map normalised Indexed palette components onto L*/Range; drop unused LAB.icc An /Indexed palette over Lab passes its entries as byte/255 (manual_scale), which the new Lab conversion read as raw L*a*b* and drew near black. Map them onto L* 0..100 and the a*/b* /Range, with a regression test. Record the patch as hayro-interpret (25) in vendor/README.md, and remove LAB.icc (no longer referenced) with its ATTRIBUTION, NOTICE and assets README entries. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * Main gates: rustfmt tests/viewer.rs; a type alias for the refresh-steps array (clippy type_complexity) 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> |
||
|
|
5a91d1b7d0 |
fix(i18n): refine Greek translations and May abbreviation (#1262)
Co-authored-by: George Athenakis <2944375+Camthalion77@users.noreply.github.com> Co-authored-by: Brandon Thomas <echelon@gmail.com> |
||
|
|
829afbc985 | Move sibling structure tags in reading order (#1229) | ||
|
|
1970935587 |
M14: edits let go of the previous working file before writing the next (gap 13) (#1279)
* M0: keep the camera edge test NaN-safe under clippy's manual_range_contains Clippy 1.97 flags `d < 0.35 || d > PI - 0.35` in the camera edge hit test. Its suggested `!(0.35..=PI - 0.35).contains(&d)` would count a NaN direction as a hit, which the two comparisons did not, so the NaN check is kept explicitly. Behaviour is unchanged. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_011AsuqvnFXhY9ASL1bA2jtm * M14: edits let go of the previous working file before writing the next (gap 13) Every edit writes a new working file: the original plus one appended revision. The previous one stayed alive in the working bytes, the display bytes and the renderer while the next was written, so after a few edits a 1 GiB file peaked at three copies. Each refresh path (full, comments/form, content, undo, redo, after save) now releases them first: the working and display bytes fall back to the original (already held by the editor) and the renderer is replaced by an empty one until the refresh rebuilds it. A refresh that still fails after rollback draws the original instead of nothing. Save still writes from the editor, never from these. Measured on a synthetic 1 GiB file (release build, Linux): five edits peak at 2.1 GiB instead of 3.1 GiB, in 8.5 s instead of 16 s. Saving still peaks at three copies; reading from disk on demand stays blocked on the renderer (hayro parses one contiguous buffer, gap 2). gaps.md, the parity notes and the roadmap record both. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_011AsuqvnFXhY9ASL1bA2jtm --------- Co-authored-by: Claude <noreply@anthropic.com> |
||
|
|
84a75eae1b |
M10: Presentation (.pptx) export (#1137) (#1162)
* feat: presentation (.pptx) export (#1137) - Implement OpenXML PresentationML (.pptx) generator in crates/export/src/pptx.rs: - Generate [Content_Types].xml, _rels/.rels, core properties, presentation.xml - Build slide master, blank slide layout, and DrawingML theme - Render slides per page: largest text block as title shape, remaining paragraphs as body text box with font size, bold, italic, and color formatting - Embed images with proper DrawingML picture shapes and relationships - Generate DrawingML table shapes with column grid and span merging - Wire OfficeFormat::Pptx into crates/engine/src/compare.rs and export_office - Register export.pptx command and mark Presentation (.pptx) Ready in catalog - Support pptx in automation doc_export_office tool - Wire export.pptx into UI commands and file save dialog - Add unit and integration tests across crates/export, engine, automation, and ui-egui - Update create.export-pptx parity feature evidence and docs * pptx export: saturating slide positions, matching cell-border tags, theme ea/cs fonts, package test - Heights and the running vertical position use clamped/saturating arithmetic, so a hostile text size or image rectangle (1e300, NaN, inf) can't overflow i64; the item loop uses get(). - The table cells' a:lnR and a:lnT borders were closed with </a:lnL>, which made every slide with a table malformed XML; they now close with their own tags. - The theme's major/minor fonts carry the schema-required a:ea and a:cs children. - New test walks the produced zip entry by entry (sizes, inflation, central directory), checks every XML part's tags balance, the required parts, content-type overrides, relationship targets and that no placeholders are used; a hostile-sizes test covers the overflow. - Test literals gain main's Block/Image rotation and size fields. - Parity notes say what is still unverified. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> --------- Co-authored-by: KrzysztofC <krzysztofc@localhost-live.lan> Co-authored-by: Brandon Thomas <echelon@gmail.com> Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com> |
||
|
|
a8940f0003 |
M10: Create separate PDFs from multiple files (#1183)
* M10: Create separate PDFs from multiple files
* Separate PDFs: translatable notices
"Left out: {names}" and the per-file reasons (already a PDF, no unused output name, no
output folder was chosen) go through tl!; the browser's download notice is a plural
("Downloaded {n} PDF" / "Downloaded {n} PDFs") via trn. Rows in all 20 catalogs, with
each language's plural forms; catalog hashes refreshed.
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>
|
||
|
|
012b34a2a5 |
M4: rename colliding form field names when combining (organize.field-name-conflicts) (#1155)
* M4: rename colliding form field names when pages are imported or combined
Insert Pages from File, Combine Files and the grouped combine registered
incoming form fields under names the destination already used, silently
blurring them into one field (fields of one fully-qualified name are one
field, so their values mix). Importing now walks both sides' fully
qualified names (parent.T.child) — including names that hang only off
page widgets, which /AcroForm never lists — and renames a colliding
field's own /T to the first free variant (Text1-1, Text1-2, …), so its
children keep their place in the tree and the second of two colliding
incoming fields is renamed too.
A field_conflicts policy (rename by default; merge keeps the older
behaviour) and reporting — ImportOutcome::renamed_fields, the combine
outcomes, and a field_name_conflicts(dst, src) pre-scan — let callers
choose and prepare; engine, tool and Combine-tab wiring follow.
* M4: engine and tool wiring for renamed field names
Insert Pages from File carries a field_conflicts policy (rename by
default) and reports what it renamed: a repair-log note names each
old → new pair and warns that scripts referring to the old names were
not rewritten, and the undo step's label counts the renames, so nothing
is slipped in silently. Undo restores the original names, as ever.
The session's combine paths gain reporting variants —
combine_unlocked_reporting, combine_grouped_reporting — returning the
bytes and the renames, and doc_combine takes field_conflicts
("rename" | "merge") and lists renamed_fields [[old, new], …] in its
result when fields were renamed, so agents can find what their scripts
must call the field now.
* M4: the Combine tab warns before combining that field names collide
The Warnings column previews, per file, the form fields whose names
collide with earlier files' and will be renamed (Text1 → Text1-1, …),
computed once per list by a new in-order pre-scan (organize
combine_field_conflicts over the whole list, engine wrapper over the
tab's byte sources; a file that cannot be opened previews as nothing,
as its row already says why). The message is plural-aware and
translated in all sixteen catalogs.
* M4: parity for renamed field names on combine and insert
organize.field-name-conflicts ships: rename by fully-qualified name
(or merge, on request), reported everywhere it happens — repair log
and undo label on insert, doc_combine's result, the Combine tab's
warning column. The notes say what is still missing: the tab's preview
ignores narrowed page ranges, the UI offers no merge choice,
Create ▸ Multiple files renames silently, and scripts and XFA bindings
that reference old names are never rewritten.
* M4: review fixes for the field-name conflict work
Four findings from the PR's code review:
- Clippy: the -D warnings gate failed on the new engine tests (a
format! with no interpolation, two drop(doc) no-ops on a
reference); the branch could not pass its own CI.
- The Combine tab's preview warned that a file split into parts
collides with itself: the preview made one source per row, while
the combine groups parts of one file and imports it once. The
preview now dedupes by group (each group's first entry stands for
the file, as the combine does), and every part shows the file's
preview.
- Collision detection compared /T bytes, so one name written in two
encodings (PDFDocEncoding, UTF-16BE) did not collide — the exact
silent blur this work exists to prevent — and renaming a UTF-16BE
name rewrote it as from_utf8_lossy mojibake. Names are now compared,
suffixed and written as decoded text (PdfString::to_text /
PdfString::text), which is also how the forms crate reads them.
- The engine's combine_unlocked/combine_grouped duplicated their
_reporting twins line for line; they now delegate with the Rename
policy, as organize's own wrappers already did.
* Field-name conflicts: only Combine and Insert from File rename
import_pages (Duplicate, Replace) and extract_pages (Extract, Split) go back to Merge, so
copies of a document's own fields keep their names and a source with two same-named field
roots isn't split apart. Pasting pages into the document they were copied from also merges;
pasting into another document renames as Insert from File does. Regression tests for
duplicate, replace, extract/split (organize), duplicate (engine) and paste (ui-egui); the
PR's rename tests call import_pages_with(Rename) explicitly.
Merge fix-up: main made edit labels a translatable Label, so the rename suffix is now two
wordings in EDIT_LABELS ("Insert pages from {name} — 1 field renamed" / "— fields
renamed"), translated in all 20 catalogs. The Combine warning's @plural row is added to
el, kk, sl and tr. Catalog hashes refreshed.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
---------
Co-authored-by: Brandon Thomas <echelon@gmail.com>
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
|
||
|
|
09287e547f |
M5.7: Fill & Sign Scan tab for photographed signatures (#1110) (#1111)
Add a Scan tab to Fill & Sign ▸ Add signature. A phone photo of a signature on paper is framed automatically (all words; table, sheet edge and dust ignored; a sideways signature turned upright). The frame can be moved, resized or redrawn, the photo turned in quarter turns, and an Ink sensitivity slider picks up faint ink. A morphological paper estimate (van Herk/Gil-Werman closing) removes shadows and uneven light; the trimmed ink in its own colour on transparency is saved and reused like an image signature. - engine: new signature_scan module (SignaturePhoto, PhotoRect, Turn, ScanError), bounded decoding, no new dependencies. - Image tab: accepts full-resolution photos (up to 40 MiB, 50 MP) and scales them down to the saved-signature limits instead of refusing them. - Photos and images can be dropped on the Image and Scan tabs. - fill_sign_add: `scan` and `sensitivity` with `path`. - Translations for all catalogs, parity entry, roadmap log. Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com> Co-authored-by: Brandon Thomas <echelon@gmail.com> |
||
|
|
7caaa35a81 |
Fix Word invoice layout and pagination (#773) (#1126)
* M10: keep invoice overlays out of Word body flow (#773) Export inferred tables without visible borders. Keep text and images positioned on pages with artwork, rotated or large text, and retain each source page's size, crop rotation and units. Cover invoice conversion through the automation tools, and handle unavailable Windows symlink setup in the confinement test. Co-authored-by: Codex <noreply@openai.com> * M10: add @lollinoo's chosen contributor credit (#773) The contributor requested their public GitHub profile name while preparing the #773 PR. Co-authored-by: Codex <noreply@openai.com> * Word export: float a page only when text overlaps an image or watermark positioned() turned every page with any image, or with text twice the body size, into absolutely positioned VML text boxes, so a page with a logo or a big title lost its flowing paragraphs, headings and tables. Now a page floats only when it has rotated text, or when an image or a watermark-sized text block actually overlaps other text (bounded pairwise test). Other pages flow with images inline. Adds a regression test for a page whose image and title sit clear of the text, and updates the docs and parity notes. --------- Co-authored-by: Codex <noreply@openai.com> Co-authored-by: Brandon Thomas <echelon@gmail.com> |
||
|
|
6d843a8e8b |
Preserve source fonts in text editing and resize clipped artwork (#1116)
* M0: remove upstream trademark artwork from contributor fork
Use the existing document icon and a plain project label. Remove trademark
asset entries and regenerate attribution as required for modified forks.
Co-authored-by: OpenAI Codex <codex@openai.com>
* M7: group compatible PDF words and hide source text during editing
Join compatible words across text objects and subset-font spaces, recognize
component colours, and reuse subset fonts with numeric space positioning.
Render a read-only background with the selected glyphs removed while the
inline editor is open. Keep one frame and source line breaks, and reset
the layout width when switching paragraphs. Expose the same preview through page_render.
Add synthetic extraction, save/reopen, preview and UI regressions. Record
the remaining mixed-style paragraph and installed-font limitations.
Co-authored-by: OpenAI Codex <codex@openai.com>
* M7: preserve rich PDF regions and preview text edits in place
Group mixed source runs into complete visual regions, retain run styles and source encodings, and use validated embedded source faces in the inline editor. Preserve source baselines and show a single live frame as text grows or shrinks, backed by a read-only draft geometry tool.
Validation: strict workspace Clippy, workspace tests, all 30 WASM crates, asset and parity gates, licensed synthetic edit/save/reopen and desktop checks, and an opt-in four-page private-document regression.
Co-authored-by: OpenAI Codex <codex@openai.com>
* M7: keep cropped artwork from blocking text editing
Track page/path clipping separately from full image placement, use visible selection bounds, and prioritize text hit testing over overlapping artwork. Preserve fixed clips during image transformations and expose visible bounds through the headless tools.
Validation: strict workspace Clippy, workspace tests, 30 WASM crates, asset/parity gates, synthetic engine/tool/UI and native desktop checks, both private-document UI regressions, and all-page local edit/save/reopen/undo on a four-page cropped-artwork PDF.
Co-authored-by: OpenAI Codex <codex@openai.com>
* M7: resize cropped images with their clipping paths
Keep the visible frame and image pixels under the same transform. Preserve the
opposite corner and respond to diagonal, horizontal-only and vertical-only
corner drags without jumping from an off-centre grip. Refuse shared clipping
scopes that would move unrelated content.
Add synthetic engine/tool/UI regressions and opt-in private-file coverage
for 80 corner gestures and 10 grow/shrink save/reopen/undo render checks.
Co-authored-by: OpenAI Codex <codex@openai.com>
* M7: complete source font subsets and preserve clipped editing
Add explicit document fonts, CFF embedding, range formatting and bounded contextual shaping; isolate shared image clips and verify private Chinese edits on all pages.
Co-authored-by: OpenAI Codex <codex@openai.com>
* Revert "M0: remove upstream trademark artwork from contributor fork"
This reverts commit
|
||
|
|
b65b7adb01 |
Accessibility: edit structure tag properties through tools (#1210)
* Accessibility: edit structure tag properties through tools * Change tag properties: listed in EDIT_LABELS and translated in every catalog; refresh hashes --------- Co-authored-by: Brandon Thomas <echelon@gmail.com> |
||
|
|
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> |