Commit Graph
793 Commits
Author SHA1 Message Date
skylar taylor-barrick 78f7ee0454 M10.5: Keep the adoption current with main progress and viewer fixes 2026-10-12 01:28:15 +02:00
cyfung1031andBrandon Thomas eb32e3adf7 M2: keep doubled short text runs from interleaving (#1198)
* M2: split short whole-run redraw resets before placement

* M2: reject non-finite redraw prefix positions

---------

Co-authored-by: Brandon Thomas <[email protected]>
2026-10-11 19:21:24 -04:00
skylar taylor-barrick 00184a1006 M10.5: Preserve main duplex-driver warnings through craft-print 2026-10-12 01:20:52 +02:00
gyurixandBrandon Thomas f5452cf126 Linux: prefer server DPI over panel physical DPI in vendored winit (#1150)
* fix(x11): prefer server DPI over panel physical DPI in winit

Without Xft DPI, use the X server screen dimensions before the output physical density. Xorg can run a dense panel at 96 DPI; EDID sizes alone make only winit apps appear zoomed.

Supersedes #908: preserves Xft.dpi priority, configured HiDPI screens, and WINIT_X11_SCALE_FACTOR override (including randr). Falls back to RandR output density when server dimensions are missing.

Adds default_dpi_factor() with 4 unit tests plus vendored-copy guard test in apps/pdfcraft/tests/winit_x11_dpi.rs.

* vendor/README: record the winit X11 DPI patch as (11)

---------

Co-authored-by: Brandon Thomas <[email protected]>
2026-10-11 19:12:32 -04:00
492207ab2c i18n(es): add 84 missing rows, consistent tú register (#1134)
* i18n(es): add 84 missing rows, consistent tú register

Spanish lacked 84 labels that every other full catalog has (About window,
field alignment, redaction marks, certificate details, address warnings…),
so they showed in English. Five strings used formal usted; the field mask
hint was garbled; per-page rotate now matches the other rotate commands.

AI-assisted, native review welcome.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
Claude-Session: https://claude.ai/code/session_01LtFRDwSXTyqaPWCYnbBGnX

* Spanish: one row each for Run form script and Mark differences

---------

Co-authored-by: Diego Ospina <[email protected]>
Co-authored-by: Claude Opus 5.5 <[email protected]>
Co-authored-by: Brandon Thomas <[email protected]>
2026-10-11 19:09:32 -04:00
Earl CoandBrandon Thomas f94c4a6ffd Show annotation actions beside selected text (#1133)
* M5: show actions for selected page text

* Selection actions: literal labels (so catalogs can cover them), no unwrap or unreachable

* Catalogs: Add comment in every interface language; refresh hashes

---------

Co-authored-by: Brandon Thomas <[email protected]>
2026-10-11 19:04:58 -04:00
Earl CoandBrandon Thomas 377f8da36b M14: choose Tools panel side in Preferences (#1130)
* M14: choose Tools panel side in Preferences

* Catalogs: Tools panel side in every interface language; refresh hashes

* OCR dialog: the shared outlined checkbox (raw ui.checkbox is linted since #1173)

---------

Co-authored-by: Brandon Thomas <[email protected]>
2026-10-11 18:54:06 -04:00
634f3d1144 Issue #1028: OCR can read pictures on pages that already have text (#1184)
* Issue #1028: OCR can read pictures on pages that already have text

Recognize Text skipped every page with text, so a picture of text on a
page that also has real text never became searchable. The engine and
the ocr_recognize tool already had skip_text_pages, but the desktop
dialog never offered it, and turning it off read the page's own text
again and added it a second time, so search found every word twice.

- Engine: always collect the page's own glyph boxes; with
  skip_text_pages false, a recognised word at least half covered by
  them (an earlier OCR layer included) is left out. OcrPage counts
  those words as already_on_page. The default still skips pages with
  text.
- Dialog: "Also recognize pages that already have text", kept with the
  other choices and used by Recognize text in multiple files too.
- Tools: ocr_recognize reports already_on_page; ocr_recognize_files
  accepts skip_text_pages.
- The two new labels in the 15 catalogs that translate the OCR dialog.
- Synthetic tests (a picture of a sentence plus real text) for the
  engine, the tool and the dialog; the engine test fails with the
  filter turned off. Unit tests for the coverage check, including
  NaN, infinite, inverted and overlapping boxes.
- parity, docs/roadmap.md M10 row and the ROADMAP.md progress log.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
Claude-Session: https://claude.ai/code/session_01JqiZ658tGndQaHao5Q6mdm

* Issue #1028: Czech labels for the OCR text-pages option

The Czech catalog was completed (#1066) while #1028 was in review, and
czech_covers_ui_literals requires every tl! literal: the two new
Recognize Text labels were missing, so the test failed after the merge.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
Claude-Session: https://claude.ai/code/session_01JqiZ658tGndQaHao5Q6mdm

* Contributors: credit rp6186030 as Raj Patel in the About window

Co-Authored-By: Claude Opus 5.5 <[email protected]>
Claude-Session: https://claude.ai/code/session_01JqiZ658tGndQaHao5Q6mdm

---------

Co-authored-by: Claude Opus 5.5 <[email protected]>
Co-authored-by: copilot-swe-agent[bot] <[email protected]>
Co-authored-by: rp6186030 <[email protected]>
Co-authored-by: Brandon Thomas <[email protected]>
2026-10-11 18:45:36 -04:00
fb97ce958d M10: Windows two-sided binds on the chosen edge; note drivers that ignore Two-sided (#1154)
The print script mapped "Flip on long edge" to System.Drawing's Duplex.Horizontal and
"short edge" to Vertical. Those are DEVMODE's DMDUP_HORIZONTAL (short-edge binding) and
DMDUP_VERTICAL (long-edge binding), so every Windows two-sided job bound on the other edge:
a PCL driver received @PJL SET BINDING = TOP for long edge. They are swapped back.

Win32_Printer's Capabilities say whether a driver prints two-sided (3, Duplex Printing). The
printer list already comes from Win32_Printer, so it now carries that as Printer::two_sided
(None on CUPS), the printers tool reports it, and the Print dialog notes when the selected
printer's driver doesn't take two-sided settings and points to Properties…. Such drivers keep
their own setting even with Two-sided Off. No vendor-specific names are used.

Co-authored-by: Claude Opus 5.5 <[email protected]>
Co-authored-by: Brandon Thomas <[email protected]>
2026-10-11 18:41:02 -04:00
287fce8f9c M2: bound the total size of page-label prefixes (#1146)
Both page-label readers decode each range's prefix into a string of its
own: render's inspection for its range table, Organize for the ranges it
returns. Ranges may share one prefix object, so a small file could ask
for a copy per range: within the 200,000-entry tree limit, about 200 MiB
of copies of one 1,024-byte prefix. Decoded prefixes now take at most
4 MiB in all (MAX_PREFIX_TOTAL_BYTES, beside MAX_LABEL_TOTAL_BYTES). Past
that the tree is refused before any label is made, as the other tree
limits refuse it.

Tests: 5,000 ranges sharing a 1,000-byte prefix are refused in both
readers (relabelling such a tree changes nothing), and exactly 4 MiB of
prefixes is still read.

Co-authored-by: Claude Opus 5.5 <[email protected]>
Co-authored-by: Brandon Thomas <[email protected]>
2026-10-11 18:38:48 -04:00
f61a1e5465 M14: "Edit with PdfCraft" on the File Explorer menu of PDF files (#682) (#1180)
* M14: "Edit with PdfCraft" on the File Explorer menu of PDF files (#682)

The Windows MSI adds a classic shell verb under
HKLM\Software\Classes\SystemFileAssociations\.pdf\shell\PdfCraft.Edit that
runs "pdfcraft.exe" "%1" with the app's icon. It appears whichever app is
the default for .pdf and never changes that default. On Windows 11 it is
under "Show more options": a first-level entry needs an IExplorerCommand
handler and package identity, which an MSI doesn't have.

MultiSelectModel is Document: several selected PDFs (up to 15) start one
launch each, and the existing single-instance hand-off opens them as tabs
of one window.

The verb has its own component, conditioned on a new secure public
property INSTALLPDFCONTEXTMENU (default 1, a welcome-dialog checkbox,
INSTALLPDFCONTEXTMENU=0 for unattended installs), following
INSTALLDESKTOPSHORTCUT. Its menu value is the key path, so uninstall
removes every value. The portable zip still registers nothing.

test-msi.ps1 checks the compiled Registry, Component, Property and
CheckBox rows, and that the .pdf default is never written. Docs: README,
docs/releasing.md, parity notes, ROADMAP log.

Co-Authored-By: Claude Opus 5.5 <[email protected]>

* Leave ROADMAP.md to main (its log moved on); the change is described in the PR

---------

Co-authored-by: Claude Opus 5.5 <[email protected]>
Co-authored-by: Brandon Thomas <[email protected]>
2026-10-11 18:35:02 -04:00
0ef75d294c UI: checkboxes and radio buttons are outlined, and checkboxes are square (#1173)
* UI: checkboxes draw a rounded-square box, not a radio-button circle

The theme's 6 px widget radius made egui's 14 px checkbox box a circle,
so View > Page display's Continuous scrolling and Show cover page looked
like two more layout radio buttons. On the light theme an unticked box
had no outline and the panel's fill, so it couldn't be seen.

Every checkbox (81 call sites) now goes through widgets::checkbox /
checkbox_without_text, which draws the box with a 3 px radius and a 1 px
border outline, keeping an outline the caller already set (dialogs).
clippy.toml disallows egui::Ui::checkbox and egui::Checkbox::{new,
without_text} so new checkboxes use the helper too.

Co-Authored-By: Claude Opus 5.5 <[email protected]>

* UI: radio buttons are outlined and filled, so they show on the light theme

On the light theme the field colour is the panels' white and widgets
have no outline, so an unselected radio button (and an unticked
checkbox) was invisible outside dialogs: View > Page display showed only
the selected dot.

Every radio button (79 call sites) now goes through widgets::radio /
radio_value, sharing the checkbox helper's styling: a 1 px border
outline, text_muted on hover, and on the light theme the plain fill
(hover) as dialogs already had, keeping what the caller set. The dark
theme keeps its fills. clippy.toml also disallows egui::Ui::radio,
egui::Ui::radio_value and egui::RadioButton::new.

Co-Authored-By: Claude Opus 5.5 <[email protected]>

---------

Co-authored-by: Claude Opus 5.5 <[email protected]>
Co-authored-by: Brandon Thomas <[email protected]>
2026-10-11 18:34:16 -04:00
Krzysztof CieślikandKrzysztofC 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 <[email protected]>
2026-10-11 18:32:29 -04:00
Earl Co 036e00f2a1 Test PDF viewer trackpad pinch zoom (#1148) 2026-10-11 18:32:16 -04:00
9622f29d04 M5: preserve comment identities during partial XFDF and FDF imports (#1144)
Reuse a replaced named comment's object reference so omitted replies and
review statuses remain attached. Keep popup replacement behavior and let
the final duplicate record control its deferred reply relationship.
Resolve indirect names in existing PDF comments so updates match their
original objects instead of creating duplicate parents.

Cover existing-PDF updates, incremental and full saves, subsequent FDF
exchange, page-scoped names, unnamed additions, duplicate records and
indirect names with surviving reply and review-status references.

Co-authored-by: MMexicann <[email protected]>
Co-authored-by: Codex <[email protected]>
2026-10-11 18:32:03 -04:00
MarekSpodBiedry 75b113ac5b Raise workspace rust-version to 1.95 to match the UI crate (#1169) (#1188) 2026-10-11 18:30:25 -04:00
cyfung1031 3fa7a3a3e8 M14: keep structural page order through inspection (#1182)
* M14: keep structural page IDs in traversal order

* M14: reuse structural page order during inspection
2026-10-11 18:30:13 -04:00
Joel Tony 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.
2026-10-11 18:30:00 -04:00
Ufuk Efe 826941dcfc docs(contributors): add Ufuk Efe to people.toml (#1177) 2026-10-11 18:29:48 -04:00
ysrdevs 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.
2026-10-11 18:29:35 -04:00
Raj PatelandBrandon Thomas 29dca46089 Bound text search result allocation (#1164)
Co-authored-by: Brandon Thomas <[email protected]>
2026-10-11 18:27:48 -04:00
6091deb829 M7: preserve overlapping text when dragging paragraphs (#594) (#1052)
* M7 #594: preserve ordinary paragraph drags (source-only GGEO candidate) [skip ci]

Route body drags through source-preserving MoveObjects, bind gestures to
the document generation, and keep added-content input ownership with
the Edit panel open or closed. Add nine real-shell source regressions.

Source-only transfer to the owner's fork, explicitly authorized before
validation. Independently source-reviewed; formatting, builds, tests,
rendering and all gates remain pending on ggeo-ai02. No upstream PR.

AI-assisted with OpenAI.

* M7 #594: record validated ordinary paragraph drags

Format the source-reviewed candidate and record successful GGEO validation:
focused UI/engine tests, exact-base source loss, native drag/history/save/reopen,
independent Poppler checks, and all eight repository CI gates.

Credit: @FredApps

* M7: use generic validation wording

* Tests: pin the clock where two saves or combines are compared byte for byte

Co-Authored-By: Claude Opus 5.5 <[email protected]>

---------

Co-authored-by: FredApps <[email protected]>
Co-authored-by: Brandon Thomas <[email protected]>
Co-authored-by: Claude Opus 5.5 <[email protected]>
2026-10-11 18:25:25 -04:00
skylar taylor-barrick c5f1cb2d3f M10.5: Reconcile hosted CI and keep the Arabic golden in both font builds 2026-10-12 00:23:30 +02:00
8ee3673bd4 M7: support shaped Thai text in Add Text and UI fallback fonts (#767)
* M7: support shaped Thai text in Add Text and UI fallback fonts

Share the bounded shaping and Type 3 writer with Thai, preserving vowel and tone clusters and original Unicode through save/reopen. Keep the Thai face optional and update UI notices and their translations.

Constraint: Thai font assets remain in the companion craft-fonts repository.

Confidence: high

Scope-risk: moderate

Co-authored-by: Codex <[email protected]>

* M7: show Thai Add Text before and after the fix

Document the rejected input on the base commit and the saved Thai text on the fixed build using genuine UI captures.

Constraint: Synthetic content and licensed fonts only; system font fallback disabled.

Co-authored-by: Codex <[email protected]>

* M7: demonstrate Thai stress cases in the comparison screenshot

Show nine lines of stacked marks, both Sara Am encodings, Thai digits and mixed Latin text. Verify all 198 Unicode code points survive saving and extraction with both PdfCraft and Poppler.

Co-authored-by: Codex <[email protected]>

* CI and releases: craft-fonts pin to 3d9ab3a (adds Noto Sans Thai)

ci.yml, freebsd.yml and the craft-fonts-ref release.yml passes to craft-common-actions move
from 8dcdacd to storytold/craft-fonts 3d9ab3a, the revision with Noto Sans Thai (#18), so
Thai Add Text is built in and tested.

* ATTRIBUTION: drop the screenshot entry again (the image isn't committed)

---------

Co-authored-by: Codex <[email protected]>
Co-authored-by: Brandon Thomas <[email protected]>
2026-10-11 18:07:36 -04:00
Zeyu LiandBrandon Thomas b91d38c073 M14: text the fonts can't draw is kept and reported, never written as ? silently (#1112)
* M14: text the fonts can't draw is kept and reported, never written as ? silently

Form values and captions, text boxes, callouts, Fill & Sign text, measurement labels and stamp by-lines keep text the standard fonts can't draw, draw ? for it, mark the annotation (PCUndrawn) and report the edit (interface notice, tools' warnings); forms set /NeedAppearances (not in PDF/A). Flattening, redaction overlay text, typed and visible signatures and create-from-text refuse instead. Reports escape such characters as [U+XXXX].

* Catalogs: the two new can't-draw messages in Czech, Slovene, Kazakh, Turkish and Greek; refresh hashes

---------

Co-authored-by: Brandon Thomas <[email protected]>
2026-10-11 17:53:55 -04:00
d415749556 Heavy pages: cull off-tile images, cache decodes, preview backdrops, calmer queues (#1117)
* Heavy pages: cull off-tile images, cache decodes, preview backdrops, calmer queues

A large CAD sheet (page 24 of the benchmark file: ~5050 pt wide, 2104
images, 9 MB of content) cost a full page interpret per tile: 4.3 s for a
1024 px tile at 1000%, 10.5 s for the whole page, and a storm of renders on
every pan frame. Tiles now cost 0.6 s, the sheet is covered by a handful of
large tiles, and gestures no longer queue work faster than workers finish it.

- Vendored hayro patch (10): skip images whose stream-size box misses the
  canvas before decoding them, so a tile no longer decodes, resamples and
  rasterizes every image on its page. Pixel-identical; the skip regression
  test now covers images too.
- Vendored hayro-interpret patch (23): decoded image pixels shared across
  renders through the interpreter cache, keyed by stream and decode target
  size within a 64 MiB budget.
- Preview renders (hayro patch 11, RenderRequest::preview through to the
  device): backdrops, thumbnails, grid cells and signature-drag backgrounds
  skip images under 16 device px. Tiles, whole pages, print and export always
  draw everything. Previews may differ by those pixels; finals don't.
- Viewer: zoom/pan settle holds apply only to pages whose raster proved slow
  (slow_pages, 500 ms), so fast pages render with no added latency; tile sides
  grow with zoom and page size (giant sheets need ~8 tiles, not ~28), queued
  center-first and keyed by exact coordinates; Edit-text outlines capped.
- vendor/README.md records the new patches with their tests.

Co-Authored-By: Muse Spark <[email protected]>

* Decoded-image cache: key every decode input, survive a poisoned lock

The key was the dictionary's bytes and the target size, so two images with byte-identical
dictionaries (uncompressed, one size) shared pixels, and an image drawn under a transfer
function reused pixels decoded without it. DecodedImageKey now holds the dictionary's place
in the document (its data follows it), its bytes and object, mask/stencil/raster output,
interpolation, the target size and preview or final quality; images under a transfer
function and inline images (named colour spaces, dictionaries in content streams) are not
cached, and an image over the budget is never kept. A poisoned lock decodes uncached
instead of panicking. Soft masks: the per-renderer cache can't carry a preview mask into a
final render (documented). Regression tests for identical dictionaries, preview then final
through one cache, and transfer functions.

---------

Co-authored-by: Muse Spark <[email protected]>
Co-authored-by: Brandon Thomas <[email protected]>
2026-10-11 17:42:48 -04:00
tzagimandBrandon Thomas e609722b36 M7: Hebrew in Edit text (reading order, writing order, Hebrew glyphs) (#521)
* Hebrew fix

* Tests: the faux-redaction audit gets its own work folder (it shared "audit" with the space audit, so the two raced)

* Arabic golden test: compare bytes only in a build without an Arabic face (CI's Linux job builds with craft-fonts)

---------

Co-authored-by: Brandon Thomas <[email protected]>
2026-10-11 17:41:06 -04:00
c65f32a078 Content edits on heavy documents: narrow refresh, cached appearances, per-page invalidation (#1122)
* Content edits on heavy documents: narrow refresh, cached appearances, per-page invalidation

Every content edit (drag, resize, retype) on a large document ran a full
synchronous refresh on the UI thread: a whole-file re-inspection plus a
redraw of every missing annotation appearance, then dropped all page
rasters. On a 72 MB / 202-page CAD file with 27k appearance-less
annotations each commit cost ~1.5 s before anything re-rendered. Now ~0.3 s.

- New Scope::Content for edits that touch only page content, resources and
  added content/marks (move/resize/retype/images/added items/marks): the
  page tree, annotations, fields, links, outline and fonts are unchanged,
  so refresh_content rebuilds working bytes, display bytes and renderer and
  validates with a light check_pages (parse plus page count, keeping the
  roll-back guarantee) instead of a full inspect. added/marks are re-read
  only when the edit can change them; undo/redo re-read both.
- AppearanceCache in pdfcraft-annot: built display appearances are reused
  while no annotation edit runs (content edits leave every annotation
  dictionary alone); cleared on comment, structural and full refreshes.
  set_appearance is split into build/apply halves so a hit skips the draw.
- check_pages in pdfcraft-render: light post-edit validation.
- Viewer: single-page content edits invalidate only their page
  (page_changed); other pages keep tiles, text and thumbnails.
- XFA dataset sync now also runs for content edits.

Co-Authored-By: Muse Spark <[email protected]>

* Fonts added by a content edit show in Properties and doc_info (read again on demand); form edits clear cached appearances too

* M14: DATE_LANGUAGES is a static (21 entries trip clippy's large_const_arrays); rustfmt the Greek entry

Co-Authored-By: Claude Opus 5.5 <[email protected]>

* Preferences scroll in a short window; tests scroll to controls below the fold (#487's roomier rows); the highlighter test waits for its mark under load

Co-Authored-By: Claude Opus 5.5 <[email protected]>

* Select tool: a multi-click selects only on the frame egui reports it, so a cleared selection (or the highlighter's marked word) isn't selected again

The highlighter test now clicks at a human pace, with frames in between, which is what exposed it.

Co-Authored-By: Claude Opus 5.5 <[email protected]>

---------

Co-authored-by: Muse Spark <[email protected]>
Co-authored-by: Brandon Thomas <[email protected]>
Co-authored-by: Claude Opus 5.5 <[email protected]>
2026-10-11 17:28:30 -04:00
Ufuk EfeandBrandon Thomas 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 <[email protected]>
2026-10-11 16:34:25 -04:00
skylar taylor-barrick 981a2e57c8 M10.5: Integrate main and adopt the validated craft-print spooler 2026-10-11 22:29:54 +02:00
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 <[email protected]>
Co-authored-by: Brandon Thomas <[email protected]>
2026-10-11 16:22:16 -04:00
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 <[email protected]>
Co-authored-by: Brandon Thomas <[email protected]>
2026-10-11 16:16:40 -04:00
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 <[email protected]>
Co-authored-by: Brandon Thomas <[email protected]>
2026-10-11 16:09:24 -04:00
skylar taylor-barrick 95434e9115 M10.5: Preserve composed sheets with the shared scaling and duplex API 2026-10-11 22:05:01 +02:00
cc9392922b M14: Italian catalog review: meaning, agreement, consistency (47 entries) (#1025)
* M14: Italian catalog review: meaning, agreement, consistency (47 entries)

A native-speaker review of it.tsv, written from the English meaning (no
other product's strings consulted).

- Meaning: "Margins (in)" is inches (Margini (pollici)); Clear Recent Files
  empties the list rather than deleting files; the comb field ("Suddividi
  in", "Caselle per carattere"); rich text formatting is not RTF;
  checkboxes and style values read as options, not commands; the
  position fields measure from the bottom-left corner; "checked"
  properties are the ticked ones.
- Agreement: counts that read wrong with 1 ("1 selezionati", "Sbloccati 1
  file") become "Label: {n}"; sentences with {kind}/{tool}, which can be
  feminine, no longer use masculine pronouns; plural "revisioni", "per 5
  anni", subject agreement in two messages.
- Consistency: “ ” quotes and straight apostrophes throughout; "carattere"
  rather than "font"; "combinare" for Combine; "email"; "Contributori";
  "Adatta all'area visibile"; progress messages in the impersonal form;
  quoted option names match their labels; "Più pagine per foglio" for the
  print layout.

Co-Authored-By: Claude Opus 5.5 <[email protected]>

* Italian: one row for Unlocked {n} of {m} files (the review's wording)

---------

Co-authored-by: magdale76 <[email protected]>
Co-authored-by: Claude Opus 5.5 <[email protected]>
Co-authored-by: Brandon Thomas <[email protected]>
2026-10-11 15:59:47 -04:00
de8e0176ed M14: complete the Czech (cs) interface catalog (#1066)
* M14: complete the Czech (cs) interface catalog

Czech covered the menus and a few later additions (167 keys). The catalog
now has every key the code uses (2,194): Home, All tools, panels, dialogs,
notices, diagnostics and history labels. Translated clean-room from the
meaning of the English labels; the 167 existing entries are unchanged.

- The @plural messages use the three Czech forms (one | few | other);
  other counted messages use count-neutral wording ("Vybráno stránek: {n}").
- "calendar date" months are genitive ("9. října 2026"); "Undo {action}"
  and "Redo {action}" are whole phrases ("Zpět: {action}").
- Labels the code joins with a leading or trailing space keep it.
- The key-name entries (context "key") are kept as they were.

Tests that relied on the incomplete catalog:
- czech_covers_every_menu_label keeps a short list of strings spelled the
  same in Czech (product and format names, Text, Index, Metadata, Model,
  templates such as "{0}: {1}"), compares leading and trailing spaces with
  the source instead of forbidding them, and requires a catalog without
  parse errors.
- date_format.rs expected the English fallback "Same as interface
  (Čeština)" under the Czech interface; that label is now translated.

ATTRIBUTION.toml: new SHA-256 of cs.tsv.

Co-Authored-By: Claude Opus 5.5 <[email protected]>

* M14: check the Czech catalog like the other complete catalogs

Czech now has the same coverage gates as the complete catalogs:
- czech_covers_commands_and_catalogue: every registered command, menu
  title and All tools group, section and item;
- czech_covers_ui_literals: every tl!("…") literal in the UI source;
- czech_plurals_dates_and_history_labels: 1/3/5 pages and 0 fields, the
  genitive date ("9. října 2026"), and Undo/Redo/Change history labels
  that keep captured names.
cs also joins the lists in missing_ocr_models_messages_are_translated and
bulk_field_properties_labels_are_in_every_full_catalog.

Co-Authored-By: Claude Opus 5.5 <[email protected]>

* i18n (cs): terminology review fixes

An independent review of the Czech catalog found 5 entries with a wrong
or misleading meaning and 44 that read unnaturally or inconsistently.

Meaning:
- US formats say so: "Čísla sociálního zabezpečení (USA)" (not the Czech
  birth number) and "Telefonní číslo (USA)" for the field format.
- Mouse Exit/Enter triggers: "Odjetí kurzoru" / "Najetí kurzoru".
- Rich text in form fields is not RTF; Form XObjects are not forms.

Wording and consistency:
- Home: pinning folders refers to "úvodní obrazovka" (also in the existing
  "Pin folder to Home…"); "úvodní stránka" reads as the first page.
- Callout is "Popisek se šipkou"; Self-signed is "Samopodepsaný" (fixes
  the agreement in "Samopodepsaná ID").
- Reset (forms), Apply redactions, guided actions, the radio button and
  checkbox styles, line endings (also the existing arrow entries), border
  styles, "Omezit na … znaků", "Rozlišovat velikost písmen", grammar in
  several messages, and the About window wording shared with the other
  Crafting Apps.

History labels for the Creator and Producer document information keys
use the Document Properties wording: "Aplikace" and "Producent PDF".

ATTRIBUTION.toml: new SHA-256 of cs.tsv.

Co-Authored-By: Claude Opus 5.5 <[email protected]>

* contributors: add my own people.toml entry

Own entry only, as AGENTS.md describes: credited by the name on my public
GitHub profile (sync_github_name), otherwise by @username.

Co-Authored-By: Claude Opus 5.5 <[email protected]>

* i18n (cs): translate "Offset from centre" (watermark offset, #1076)

New upstream string added after the catalog was last synced.

Co-Authored-By: Claude Sonnet 5.5 <[email protected]>

* i18n (cs): follow the reworded Combine files strings (#1032)

Upstream generalised the Combine files dialog (images and text files, a
prompt for files that can't be read) and replaced six strings. The old
Czech entries are dropped and the ten new ones translated.

Co-Authored-By: Claude Sonnet 5.5 <[email protected]>

* i18n (cs): translate the new Enhance page / camera image strings (#1027)

Four strings added upstream after the catalog was last synced.

Co-Authored-By: Claude Sonnet 5.5 <[email protected]>

* i18n (cs): margin labels Top/Bottom read Nahoře/Dole

Co-Authored-By: Claude Opus 5.5 <[email protected]>

* i18n: translate the Digital ID errors, the Signatures panel and key usages

The digital-ID dialog printed its validation errors, the Signatures panel
headings ("Rev. n: Signed by ...", "Unsigned signature field"), the
certificate issuer line of the signed-document bar and the nine key-usage
names in English. Translate them at the point where they are drawn, the way
title() and notify_fmt() already do, so engine and OS error texts that
share those paths pass through unchanged. Czech catalog entries added; other
languages fall back to English.

The signature appearance preview is left alone on purpose: it mirrors the
Helvetica text the engine bakes into the PDF ("Digitally signed by", "Reason:"),
which is document content, not interface.

Co-Authored-By: Claude Opus 5.5 <[email protected]>

* i18n: translate form action, print, panel layout, rendering and OCR language texts

Route the remaining plain-English messages through the translation lookup
where they are drawn: the Add-action validation hints, the Cut and stack
print error (preview and notice), the two panel-layout notices, the
"Rendering..." placeholders on unrendered pages, the page number under the
Page Boxes preview (reuses "Page {label}") and the OCR language name.

The messages of the docking validator and the engine and OS errors stay
English: they report internal invariants or text this code does not own.
"My stamps" stays too: it is stored as the category of the user's stamps, so
translating it would split a library when the language changes. The dynamic
stamp chip text mirrors the English text baked into the stamp and is left as is.

Co-Authored-By: Claude Opus 5.5 <[email protected]>

* i18n (cs): translate the Appearance preferences and the wheel scroll speed

The strings added upstream with the Appearance section and the mouse wheel
scroll speed setting were missing from the Czech catalog, so
czech_covers_ui_literals failed after the merge.

Co-Authored-By: Claude Opus 5.5 <[email protected]>

* i18n: use plural forms for counts built in code

The saved notice after Optimize ("3 images optimized", "2 items discarded"),
the pixel sizes in Export all images, the exact size in bytes (human_size and
the document properties) and the history label of a multi-field delete were
assembled from English words and a trailing "s". Use trn() for them, with
@plural entries in the Czech catalog (1 | 2-4 | 5+), and translate the
"Delete N fields" history label in action_label(). English output is unchanged;
other languages without a plural entry keep the English wording.

The Czech plural test now also checks bytes, optimized images and the
Delete N fields label.

Co-Authored-By: Claude Opus 5.5 <[email protected]>

* i18n: translate the text-box and organize toolbar tooltips

"Done adding text" and "Discard this text (Esc)" (the buttons under a new
text box) and the Rotate tooltips of the organize toolbar were passed to
icons::button as plain English. Translate them at the call site, as the
other icon buttons do.

The two text-box tooltips are tl!() literals, so every complete catalog
(ar, bg, cs, de, fr, hu, it, ja, pt-br, ru, uk, zh-hans) has to carry them;
the entries for the languages other than Czech are AI drafts that want a
native speaker's look. The Rotate tooltips already had translations.

Co-Authored-By: Claude Opus 5.5 <[email protected]>

---------

Co-authored-by: Losminek <[email protected]>
Co-authored-by: Claude Opus 5.5 <[email protected]>
Co-authored-by: Brandon Thomas <[email protected]>
2026-10-11 15:55:21 -04:00
0f7ff1ce30 Improve PDF Navigation with Cursor-Anchored Zoom and Mouse Panning (#504)
* M3: add cursor zoom and temporary mouse-button navigation on Windows

Anchor Ctrl-wheel zoom to the cursor; share bounded, immediate ScrollArea pan between the middle button and Space + primary drag. Preserve ordinary scrolling and editing, cancel capture on focus/document changes, and initialize numeric zoom before the viewport exists.

Validation on Windows x64: formatting, application check and workspace Clippy pass; full workspace tests pass (928 passed, 4 ignored), including 53 UI unit tests and 27 control integration tests. No branding changes, new dependencies, machine-specific build configuration or local development notes.

* M3: add contributor display name for mhadidi06

---------

Co-authored-by: Brandon Thomas <[email protected]>
Co-authored-by: Claude Opus 5.5 <[email protected]>
2026-10-11 15:49:39 -04:00
513a1ecc3a i18n: let the layers below hand their sentences over, so they can be translated (#478)
* i18n: let the layers below hand their sentences over, so they can be translated

The accessibility findings, the PDF/A problems, the signature details, the
page-range mistakes and the edit labels were English in all ten languages,
because matching their wording from the UI with a tl! literal would break
silently whenever the engine changed it.

So each layer now hands the wording over instead, apart from the document's
own words: a WORDINGS list of every sentence it can say, with holes in it, and
said(&words), which looks the wording up through the closure it is given and
fills the holes in a single pass. in_english changes nothing, for the CLI and
the tests; the UI passes &i18n::words, a catalog lookup. A wording with no
entry comes back as it stands, which is the English, so the nine catalogs this
does not touch read exactly as before.

a11y::report_html takes a language tag and a words closure, so a saved report
reads the way the panel read, down to its lang attribute. print::PrintError
gained a variant per thing a person can be wrong about, with the numbers apart
from the words; faults rather than mistakes keep their own text.

Spanish translates all of it (107 entries). spanish_covers_what_the_engine_says
requires an entry per wording in the eight lists and
spanish_keeps_the_holes_in_what_the_engine_says requires the holes to survive
the translation, so the catalog cannot fall behind the engine unnoticed.

No new tl! literal, so the Chinese, French and Russian UI-literal gates are
unaffected.

Co-Authored-By: Claude Opus 5 <[email protected]>

* Sign tests from #1099: compare against the &'static str kinds

* Camera tests: undo returns a Label now

---------

Co-authored-by: Fernando Areán <[email protected]>
Co-authored-by: Claude Opus 5 <[email protected]>
Co-authored-by: Brandon Thomas <[email protected]>
2026-10-11 15:45:51 -04:00
Márk BartalosandBrandon Thomas 116fa51262 Give dialogs roomier text fields and padding (#487)
* Give dialogs roomier text fields and padding

Single-line fields in dialogs and side panels were only as tall as one line of text, and modals used the popup frame's tight margin. Fields are now 30 points with more inset, and every modal shares one padded card so titles and buttons sit clear of the edge.

* Keep long labels inside their rows and dialogs

Painted names ran past the edge of fixed rows, and a page hover tip was drawn above open dialogs. Labels now end in an ellipsis, tab rows wrap, and those tips stay hidden while a dialog is open.

---------

Co-authored-by: Brandon Thomas <[email protected]>
2026-10-11 15:38:14 -04:00
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 <[email protected]>

* 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 <[email protected]>
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 <[email protected]>
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 <[email protected]>

---------

Co-authored-by: Claude Opus 5.5 <[email protected]>
Co-authored-by: Tristan von Kries <[email protected]>
Co-authored-by: Brandon Thomas <[email protected]>
2026-10-11 15:33:54 -04:00
49217aec6c fix(theme): complete Linux System appearance tracking (#1017)
* fix(theme): complete Linux System appearance tracking

Follow up on #881 for #446 with the standard numeric appearance portal setting
and event-driven updates that wake idle windows. Keep manual modes independent,
cache their desktop updates without repainting, and cancel the worker on close.

Retain inherited native window appearance and populate both egui style branches
with the chosen palette. Preserve saved System, the Light default and unknown
source fallback, and avoid desktop session access in headless UI contexts.

Assisted-by: Codex (gpt-6.1-sol)

* Desktop theme: keep the GNOME colour-scheme fallback XFCE needs

The portal tracker read only org.freedesktop.appearance, which XFCE's portal
doesn't answer (it proxies the GSettings schemas). It now also reads and
subscribes to org.gnome.desktop.interface color-scheme and uses it when the
freedesktop setting states no preference, as main's reader did. KDE answers
only the freedesktop one, which wins when it states a preference.

Co-Authored-By: Claude Opus 5.5 <[email protected]>

---------

Co-authored-by: Brandon Thomas <[email protected]>
Co-authored-by: Claude Opus 5.5 <[email protected]>
2026-10-11 15:23:49 -04:00
cyfung1031andBrandon Thomas 881682c495 Tests: consolidate safe ui-egui integration targets (#992)
* tests: consolidate safe ui-egui integration targets

* Group the safe UI tests as tests/ui_group/ instead of turning off test discovery

With autotests = false, a test file added later builds only if someone also lists it in
Cargo.toml, so new tests could silently never run. A tests/ui_group/main.rs target groups the
same 15 files while Cargo keeps discovering every other file in tests/.

---------

Co-authored-by: Brandon Thomas <[email protected]>
2026-10-11 15:20:29 -04:00
RidwanandBrandon Thomas 5df8f3b33a Issue #995: unlock a restricted document with its permissions password (#1127)
A PDF protected with only a permissions (owner) password opens without
asking, restricted, and nothing let the user enter that password
afterwards: the security notice only led to a read-only Security tab and
the hint said to reopen the file with the password, which the app had no
way to do (#995, #1077).

Now the security notice and Document Properties > Security offer Unlock...,
and Change settings / Remove security on a restricted document ask for the
permissions password first and then carry on, as Acrobat does. The open
password is refused there.

- crypt: SecurityHandler::authenticate_owner accepts only the owner
  password and refuses a different file key.
- cos: Document::unlock upgrades the open document in place; owner and user
  passwords unlock the same file key, so nothing is decoded again.
- engine: Session::unlock (not an edit: no undo step, not marked unsaved)
  also unlocks the undo and redo states and reopens saves as owner;
  Document::can_unlock, EditError::WrongPassword, command protect.unlock.
- ui: the permissions password prompt shares the open prompt's modal; the
  notice and Security tab get Unlock...; the disabled-command hints no
  longer point at reopening the file.
- i18n: the new strings in all 15 full catalogs (catalog hashes updated).

Tests cover crypt, cos and engine (all four algorithms, wrong and open
passwords, undo, redo, save, removing security afterwards) and the UI
(notice flow, Remove security asking first).

Closes #995
Closes #1077

Co-authored-by: Brandon Thomas <[email protected]>
2026-10-11 15:18:01 -04:00
PiohrunandBrandon Thomas d576fe2b06 Forms: keep tiles and render unbaked field values during page re-render (#636)
* Forms: keep tiles and render unbaked field values during page re-render

In complex interactive forms, switching focus away from an edited field
caused its text to disappear for several seconds while background tiles
re-rendered.

- In DocView, retain existing tiles as stale rather than immediately
  purging them on page_changed so the canvas remains sharp without flashing.
- Prioritize visible tiles over backdrops in the render queue.
- Track unbaked form fields and paint an immediate egui widget preview
  for committed text, checkbox, and radio inputs until fresh raster
  tiles arrive.
- Scope page invalidation on form edits to pages containing the edited
  or calculated fields.
- Hoist fields lookup in recalculate_with and apply_changes to eliminate
  repeated parsing.
- Run JavaScript scripts via catch_unwind on the current thread instead of
  spawning a new thread per script invocation.
- Add contributor entry for Piohrun in contributors/people.toml.
- Add test switching_fields_keeps_committed_text_visible_immediately.

* Forms: address review feedback on tile stale tracking, hoist, and diff invalidation

- Revert crates/js to preserve dedicated 1 GiB stack thread for boa scripts.
- In DocView::page_changed, purge non-current-tag tiles and mark only current-tag tiles stale.
- Restore demand queue order in prepare_render_queue.
- Add unbaked overlay entries only on successful edit apply and only for fields whose values changed; fix font zoom calculation on 90°/270° pages.
- Re-read fields(doc) in recalculate_with only when script returns changes.
- Diff form before and after edit in apply_edit, invalidating pages with changed fields, falling back to document_changed for XFA or page-count changes.
- Add tests covering script recalculation side effects across pages, unbaked overlay cleanup on tile delivery/zoom/rejection, and checkbox recalc chaining.

---------

Co-authored-by: Brandon Thomas <[email protected]>
2026-10-11 15:15:48 -04:00
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 <[email protected]>

* A placed image too large to preview gets its own message; scale with thumbnail, which needs far less memory

---------

Co-authored-by: mjohnson7cd <[email protected]>
Co-authored-by: Brandon Thomas <[email protected]>
2026-10-11 15:10:03 -04:00
b642e327e7 Security: fix 10 verified audit findings in redaction, signature validation, and untrusted-input handling (#1099)
* fix(create): cap decoded size of imported PNGs

A PNG's declared dimensions sized an infallible allocation, so a few-byte file claiming huge dimensions aborted the process when the allocation failed. Cap the decoded size at the TIFF ceiling and reserve every input-sized buffer fallibly, returning CreateError::Image instead. (Security audit run-2: v3-024.)

Co-Authored-By: Claude Opus 4.8 <[email protected]>

* fix(xfa): bound total picture bytes kept during layout

Each laid-out picture kept its own copy, so one picture in a repeating subform multiplied without bound and could exhaust memory on open. Add a per-layout aggregate cap (256 MiB) that stops with XfaError::TooLarge. (Security audit run-2: v3-103.)

Co-Authored-By: Claude Opus 4.8 <[email protected]>

* fix(xfdf): bound XML nesting and protect redaction marks on import

Deeply nested XFDF/XML overflowed roxmltree's recursive parser and aborted the process; a non-recursive depth scan now rejects it before parsing. Imported comments could also replace a locked or redaction mark by /NM or add a hidden one; place() no longer replaces locked/Redact annotations and clears the hidden/invisible flags on imported redaction marks. (Security audit run-2: v3-105, v3-111.)

Co-Authored-By: Claude Opus 4.8 <[email protected]>

* fix(sign): classify post-signing changes by the signed revision's roles

A signed object's modification was classified from labels in the attacker-written new version, so relabelling a signed page /Type /Sig (or giving it /ModDate), or relabelling a drawn stream /ObjStm or /XRef, let a forged page validate as Valid even under DocMDP P=1. Decide an object's role (page content, signature, metadata, file plumbing) from what it was in the signed revision, not its new dictionary. (Security audit run-2: v3-082, v3-083.)

Co-Authored-By: Claude Opus 4.8 <[email protected]>

* fix(redact): remove redacted content from the saved file, fail closed otherwise

Apply Redactions and Remove Hidden Information left the originals behind: dropped or cleaned image/form XObjects kept their resource names (so the full rewrite saved the originals), an XFA form kept the redacted value in its datasets packet, and vertical-writing text under a mark was never hit by the horizontal removal geometry yet reported success. Prune replaced XObject names the page no longer draws, drop /XFA and /NeedsRendering when redacting an XFA form, and fail closed (RedactError::VerticalText) rather than silently leave vertical text. (Security audit run-2: v3-064, v3-067, v3-062.)

Co-Authored-By: Claude Opus 4.8 <[email protected]>

* fix(ui-egui): trust the displayed signer, not the document's embedded root

The Signatures panel's one-click 'Add to trusted certificates' trusted the top of the chain the document supplied, installing the author's self-signed root as an anchor instead of the signer shown. Trust the displayed signer certificate. (Security audit run-2: v3-098.)

Co-Authored-By: Claude Opus 4.8 <[email protected]>

* Use main's decoded-size ceiling for PNGs too; keep the depth tests on main's checker

---------

Co-authored-by: Claude Opus 4.8 <[email protected]>
Co-authored-by: Brandon Thomas <[email protected]>
2026-10-11 15:09:39 -04:00
8f8e0236aa M10: Enhance camera image cleans the photo up and chooses its look (part 4 of #1008, closes #1034) (#1038)
* M10: enhance camera images: straighten a page from four corners (ocr.camera-perspective)

Four corners on a page (any order, a convex quadrilateral) are warped to an
upright rectangle that replaces what the page shows, read at the page
picture's own resolution (at most 600 dpi) and kept at that physical size.
One undoable step through Replace Pages, so comments, links and bookmarks
stay.

- geom: a pure 3x3 homography solve and corner ordering (no dependencies);
  collinear, concave and non-finite input gives None.
- engine: camera::warp (bilinear, over white, capped at 40 MP) and
  Session::enhance_camera_page.
- automation: page_enhance_camera (corners in top-left-origin points).
- parity: ocr.camera-perspective is partial; the UI handles, automatic edge
  detection and contrast cleanup are still missing.

Refs #1008.

Co-Authored-By: Claude Opus 5.5 <[email protected]>

* M10: Enhance camera image: drag four corners on the page, then Enhance page (ocr.camera-perspective)

Scan & OCR ▸ Enhance camera image (ocr.enhance_camera) shows four corner
handles on the current page, inset 5% from its edges, with what will be cut
away dimmed. Enhance page straightens what they enclose through
Session::enhance_camera_page (one undoable step); Cancel and Esc leave the
page alone. The buttons float at the top right, clear of a centred page's
corners, and the instructions are a notice, as for Crop.

A command of its own, beside Enhance scanned file (ocr.enhance), which
stays for scan correction (#1012).

- ui-egui: camera_ui (handles, dimming, bar), QuickTool::EnhanceCamera,
  quick_tool "enhance-camera" for ui.state and the shot example.
- Catalogs: the three new labels in every language with UI coverage
  (plus es, pl, zh-hant); ATTRIBUTION.toml hashes updated.
- automation: page_enhance_camera is linked to the command.
- parity: ocr.camera-perspective lists the command and the UI test.

Refs #1008.

Co-Authored-By: Claude Opus 5.5 <[email protected]>

* M10: Enhance camera image finds the document's edges (ocr.camera-perspective)

The corners now start on the photographed document's edges when they can
be found, else inset 5% as before.

camera_edges::detect_document, pure Rust and deterministic: shrink to 400
px, blur, Sobel edges thinned and thresholded against the 98th percentile;
a Hough transform where each edge votes within 6 degrees of its own
normal; pairs of near-horizontal and near-vertical lines make convex
quadrilaterals (at least 12% of the picture), kept when every side lies on
a matching edge along at least 55% of its length and scored by area times
the square of the mean support; each side is then refitted through its
edge points (total least squares). None when nothing convinces.

- Session::detect_camera_corners (user space); page_enhance_camera finds
  the corners itself when `corners` is omitted and reports the corners it
  used and whether they were detected; the UI command starts from them.
- Tests on synthetic photos: plain table, text on the sheet, wood grain
  with a shadow, strong perspective, a 30 degree turn, a sheet running off
  the picture, no document, white on white, bad input; end to end through
  the engine and the tool.
- Parity notes: what is still missing (turning the result upright, rounded
  corners, clean-up).

Refs #1008.

Co-Authored-By: Claude Opus 5.5 <[email protected]>

* M10: after Enhance page, rotate, crop or organize the page (ocr.camera-perspective)

Enhance camera image no longer closes on Enhance page: the bar then offers
rotate counterclockwise and clockwise for the enhanced page (one undo step
each), Crop pages and Organize pages (which leave the tool for those
tools), and Done; Esc leaves too. A photo of a card often comes out a
quarter turn off until the page is turned upright automatically.

The two icon buttons are named for screen readers. Every label already
exists in the catalogs.

Refs #1008.

Co-Authored-By: Claude Opus 5.5 <[email protected]>

* M10: Enhance camera image cleans the photo up and chooses its look (#1034)

After Enhance page the bar offers a look (Original, Colour, Greyscale,
Black and white) and a Clean-up slider (0-100, default Colour at 50).
Choosing a look or letting go of the slider enhances the page again from
the straightened photo and turns it as before, so the history keeps one
enhancement (and one turn), never a pile of them.

camera_clean, pure Rust:
- clean: the paper's illumination is estimated per channel on a coarse
  grid (each block's 90th percentile, lifted to the 75th percentile of the
  5x5 blocks around it so a stamp or picture isn't taken for paper, then
  smoothed) and divided out, so casts, shadows and darker edges go and the
  paper turns white; a contrast stretch grows with the level, and from 60
  isolated dark pixels are removed. Colour ink stays.
- apply(look): Original is the photo as taken; Greyscale is the cleaned
  page in grey; Black and white evens the lighting fully, thresholds the
  whole page (the level sets the threshold: higher keeps fainter strokes)
  and despeckles. Black and white is stored as PNG (lossless): JPEG would
  smear grey around every letter.

- Session::enhance_camera_page_at(look, level); page_enhance_camera takes
  `mode` (original, colour, greyscale, bw) and `level` (0-100), refusing
  anything else.
- "Clean-up", "Original", "Greyscale" and "Black and white" in every
  catalog with UI coverage, plus es, pl and zh-hant.
- Tests: shadow and yellow cast become white paper with dark text, a blue
  stamp stays blue, strength grows with the level, specks go but text
  stays, the four looks (black and white is only black and white, the
  threshold moves with the level), bad sizes; a shadowed sheet through the
  engine; level and mode through the tool; the slider and the looks
  re-enhancing from the photo and keeping the turn.
- Parity: ocr.background-removal, ocr.edge-shadow-removal and
  ocr.despeckle are partial (camera images only).

Co-Authored-By: Claude Opus 5.5 <[email protected]>

* M10: Enhance camera image cleans the photo up and chooses its look (#1034)

After Enhance page the bar offers a look (Original, Colour, Greyscale,
Black and white) and a Clean-up slider (0-100, default Colour at 50).
Choosing a look or letting go of the slider enhances the page again from
the straightened photo and turns it as before, so the history keeps one
enhancement (and one turn), never a pile of them.

camera_clean, pure Rust:
- clean: the paper's illumination is estimated per channel on a coarse
  grid (each block's 90th percentile, lifted to the 75th percentile of the
  5x5 blocks around it so a stamp or picture isn't taken for paper, then
  smoothed) and divided out, so casts, shadows and darker edges go and the
  paper turns white; a contrast stretch grows with the level, and from 60
  isolated dark pixels are removed. Colour ink stays.
- apply(look): Original is the photo as taken; Greyscale is the cleaned
  page in grey; Black and white evens the lighting fully, thresholds the
  whole page (the level sets the threshold: higher keeps fainter strokes)
  and despeckles. Black and white is stored as PNG (lossless): JPEG would
  smear grey around every letter.

- Session::enhance_camera_page_at(look, level); page_enhance_camera takes
  `mode` (original, colour, greyscale, bw) and `level` (0-100), refusing
  anything else.
- "Clean-up", "Original", "Greyscale" and "Black and white" in every
  catalog with UI coverage, plus es, pl and zh-hant.
- Tests: shadow and yellow cast become white paper with dark text, a blue
  stamp stays blue, strength grows with the level, specks go but text
  stays, the four looks (black and white is only black and white, the
  threshold moves with the level), bad sizes; a shadowed sheet through the
  engine; level and mode through the tool; the slider and the looks
  re-enhancing from the photo and keeping the turn.
- Parity: ocr.background-removal, ocr.edge-shadow-removal and
  ocr.despeckle are partial (camera images only).

Co-Authored-By: Claude Opus 5.5 <[email protected]>

* Camera level: when another edit broke the run, redo what was undone instead of warping the page again

---------

Co-authored-by: magdale76 <[email protected]>
Co-authored-by: Claude Opus 5.5 <[email protected]>
Co-authored-by: Brandon Thomas <[email protected]>
2026-10-11 14:57:31 -04:00
durtyrobbinandBrandon Thomas f036d3841b Print: architectural paper presets and PDF scale controls (#599 follow-up) (#1123)
* M10.5: Restore architectural paper presets and PDF scale controls

Port the missing paper catalog and scale controls from PR #599 onto the merged Windows printing implementation. Keep the spooler and background discovery from #847/#1007. Document the feature-by-feature audit and verification limits.

Assisted-by: OpenAI Codex

* Drop the provenance write-up and the screenshot example (the kittest covers the dialog)

* Refresh catalog hashes

---------

Co-authored-by: Brandon Thomas <[email protected]>
2026-10-11 14:49:32 -04:00
Oussama Dalhi a3753c52f9 Fix text selection cursor on all tools toggle(View More - View Less Button) (#1121)
* Fix text selection cursor on all tools toggle

* Apply Rust formatting
2026-10-11 14:46:33 -04:00
Chiranjeevi U 9644aeb9e2 Cap unbounded editing and text caches; index form fields once (#1115)
* Cap editing and text-layer caches that grew without bound

The cos editing cache kept every touched object until save, so editing
a large document pinned its whole object graph. Bound it like the scan
reader: 1024 objects / 32 MB of owned bytes, 256 object streams /
32 MB, evicting to the newcomer. Returned objects outlive eviction, and
clones share the caches, so this only costs re-parsing.

The viewer text cache had the same shape: every visited page's text
stayed for the session. Retain visible, selected and matched pages plus
a 24-page prefetch window each frame; evicted pages re-extract on
revisit. Skipped while a search is active, where demand still wants
every page. Failures are never evicted: they cost nothing to remember.

cli info on a 1024-page/147 KB file (debug build, same host/command):
before 66.7 MB RSS, after 54.0 MB RSS (3 runs, tight).

Tests: editor count bound, byte bound, snapshot-clone reads (cos);
evict-far/keep-visible+matched+selection, search-active keep-all
(canvas). All mutation-checked: each bound goes red when disabled.

Signed-off-by: Chiranjeevi U <[email protected]>

* Index form fields once per refresh instead of once per field

The form-scope refresh ran a linear find over all fields for every
field, O(F^2) on each keystroke; forms reach 10k fields. Build one
name index up front, O(F). entry().or_insert() keeps the old
first-match semantics for repeated field names.

New test with two same-named widgets pins first-wins and goes red
under insert. 141 engine tests green, clippy warnings and perf lints
clean.

Signed-off-by: Chiranjeevi U <[email protected]>

---------

Signed-off-by: Chiranjeevi U <[email protected]>
2026-10-11 14:46:19 -04:00