Commit Graph

808 Commits

Author SHA1 Message Date
DWAA1660 c52072b678
Add editable page-range selection to Organize Pages (#1092) (#1174)
* #1092: Add editable Organize page-range selection

Generated with Codex.

* #1092: Leave overlapping main CI cleanup to PR #1167

Generated with Codex.

---------

Co-authored-by: Brandon Thomas <echelon@gmail.com>
2026-10-12 01:25:38 -04:00
Krzysztof Cieślik 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>
2026-10-11 21:55:25 -04:00
Fabio Lussi 0a447ee1ab
Comments panel: show Persian and Arabic comments in reading order (#1079) (#1138)
* Comments panel: show Persian and Arabic comments in reading order

Comment bodies and replies with right-to-left script were laid out with
their words in logical order, left to right. They now wrap through
bidi::wrap: each paragraph takes its direction from its first strong
character, lines break in reading order, each line is reordered (UAX #9)
and right-to-left lines sit against the right edge. Author names get the
same treatment on one line. Persian and Arabic-Indic digits, which
egui's shaper drew reversed, now read left to right, file names
included.

Fixes #1079

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* RTL comments: cache wrapped lines between frames

wrapped_text measured every candidate line of every right-to-left comment on every frame.
The wrapped lines are now kept in egui's temp data, keyed by a hash of (text, font, width,
pixels per point, collapsed); the cache starts over at 256 entries, so it stays bounded.
A collapsed comment wraps only its first 1024 characters (it shows one line), still with
the ellipsis when the text goes on. Test: second frame reuses the wrap, the cache stays
bounded, a very long collapsed comment is one line.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

---------

Co-authored-by: Fabio Lussi <198260595+itrycodinganditsbad@users.noreply.github.com>
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Co-authored-by: Brandon Thomas <echelon@gmail.com>
2026-10-11 21:22:12 -04:00
Earl Co 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>
2026-10-11 21:20:13 -04:00
Eduardo Soares 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>
2026-10-11 21:18:21 -04:00
Bjorneseth 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>
2026-10-11 21:16:34 -04:00
lollinoo 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>
2026-10-11 21:10:32 -04:00
krinry 4701a00831
Edit text: support Devanagari system font fallback and Indic pre-base matra reordering (#1033)
- UI font fallback: add Devanagari probe (U+0915) and candidate font paths (Noto Sans Devanagari, Lohit, Nirmala, Mangal, Devanagari MT, Kohinoor) in system_fonts.rs, and register SYSTEM_DEVANAGARI_FALLBACK across all family stacks in theme.rs so Hindi text renders clearly in the inline editor and chrome.
- Text editing & extraction: reorder visual-order Indic pre-base vowel signs (e.g. Devanagari 'ि' U+093F) to standard Unicode logical order upon extraction in text_lines and reading_lines, and provide a visual-order fallback during font reuse in replace_line and rewrite_block.
- Add unit tests for Devanagari font fallback coverage in crates/ui-egui/tests/fonts.rs and Indic visual/logical reordering round-trip in crates/edit/src/tests.rs.
- Credit krinry with sync_github_name in contributors/people.toml.

Co-authored-by: Brandon Thomas <echelon@gmail.com>
2026-10-11 21:03:40 -04:00
Márk Bartalos a786c8fb02
Polish chrome hover, press, and selection (#457)
* Ease hover and press feedback on chrome controls.

* Polish chrome alignment, selection, and borders.

* Drop the unused mut on page-thumbnail responses.

The context menu borrows the response immutably, so the hand-cursor binding does not need mut.

* Format the icon button wash

* Test the shared icon button for pointer parity only

Its painting now differs from the legacy path on purpose (eased wash, selected
outline, sinking glyph), so the frozen-shapes comparison no longer applies.

---------

Co-authored-by: Brandon Thomas <echelon@gmail.com>
2026-10-11 21:01:36 -04:00
komo1iddin 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 3aa539b5b69e97419bbab83e6cca3d38835b8de8.

* Test: give the text layer up to 4 s before copying (it flaked on a loaded machine)

---------

Co-authored-by: Kamoliddin Fatkhullaev <komoliddinfatxullayev@MacBook-Pro-Komoliddin.local>
Co-authored-by: OpenAI Codex <codex@openai.com>
Co-authored-by: Brandon Thomas <echelon@gmail.com>
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-11 20:27:55 -04:00
skylar taylor-barrick f3571c591f
Merge pull request #1015 from storytold/codex/craft-print
M10.5: Adopt craft-print spooler with Windows printing fixes
2026-10-11 18:12:40 -06:00
Earl Co 4a1e205f81
Accessibility: export before-and-after review record (#1204)
* Add accessibility before-and-after review record tool

* Parity: leave out the review record (the parity list tracks Acrobat features; this is a PdfCraft extension)

---------

Co-authored-by: Brandon Thomas <echelon@gmail.com>
2026-10-11 19:37:00 -04:00
Earl Co 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>
2026-10-11 19:36:00 -04:00
skylar taylor-barrick 78f7ee0454 M10.5: Keep the adoption current with main progress and viewer fixes 2026-10-12 01:28:15 +02:00
Earl Co fe2eadb211
Accessibility: identify structure objects in checker findings (#1207)
* Accessibility: identify structure objects in checker findings

* Include object generation in accessibility findings
2026-10-11 19:24:30 -04:00
Earl Co 0824f44daf
docs: sync Earl Co contributor name from GitHub (#1199)
Co-authored-by: Brandon Thomas <echelon@gmail.com>
2026-10-11 19:24:13 -04:00
cyfung1031 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 <echelon@gmail.com>
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
gyurix 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 <echelon@gmail.com>
2026-10-11 19:12:32 -04:00
Diego Ospina 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 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01LtFRDwSXTyqaPWCYnbBGnX

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

---------

Co-authored-by: Diego Ospina <diegozpy@users.noreply.github.com>
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Co-authored-by: Brandon Thomas <echelon@gmail.com>
2026-10-11 19:09:32 -04:00
Earl Co 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 <echelon@gmail.com>
2026-10-11 19:04:58 -04:00
Earl Co 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 <echelon@gmail.com>
2026-10-11 18:54:06 -04:00
Raj Patel 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>
2026-10-11 18:45:36 -04:00
Dimitar 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 <noreply@anthropic.com>
Co-authored-by: Brandon Thomas <echelon@gmail.com>
2026-10-11 18:41:02 -04:00
ymote 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 <noreply@anthropic.com>
Co-authored-by: Brandon Thomas <echelon@gmail.com>
2026-10-11 18:38:48 -04:00
jcl-bit 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 <noreply@anthropic.com>

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

---------

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Co-authored-by: Brandon Thomas <echelon@gmail.com>
2026-10-11 18:35:02 -04:00
Todd Pederson 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 <noreply@anthropic.com>

* 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 <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Co-authored-by: Brandon Thomas <echelon@gmail.com>
2026-10-11 18:34:16 -04:00
Krzysztof Cieślik 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>
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
Mex 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 <MMexicann@users.noreply.github.com>
Co-authored-by: Codex <noreply@openai.com>
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 Patel 29dca46089
Bound text search result allocation (#1164)
Co-authored-by: Brandon Thomas <echelon@gmail.com>
2026-10-11 18:27:48 -04:00
FredApps 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>
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
Tanapat Eiam-arj 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 <noreply@openai.com>

* 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 <noreply@openai.com>

* 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 <noreply@openai.com>

* 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 <noreply@openai.com>
Co-authored-by: Brandon Thomas <echelon@gmail.com>
2026-10-11 18:07:36 -04:00
Zeyu Li 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>
2026-10-11 17:53:55 -04:00
Maksim Popov 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 <noreply@meta.com>

* 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 <noreply@meta.com>
Co-authored-by: Brandon Thomas <echelon@gmail.com>
2026-10-11 17:42:48 -04:00
tzagim 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 <echelon@gmail.com>
2026-10-11 17:41:06 -04:00
Maksim Popov 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>
2026-10-11 17:28:30 -04:00
Ufuk Efe 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>
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
pechnikov2026-star 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>
2026-10-11 16:22:16 -04:00
Γιῶργος (George) 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>
2026-10-11 16:16:40 -04:00
spacekomet 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>
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
magdale76 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 <noreply@anthropic.com>

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

---------

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>
2026-10-11 15:59:47 -04:00