Commit Graph
198 Commits
Author SHA1 Message Date
skylar taylor-barrick b5467d93a0 fix(ui): preserve native open actions and password prompts
Carry selected tool actions through native file preparation, password retries and conversion, and defer startup intake until pending native opens finish.

Await observable completion in native opening, password, recovery and linked-PDF tests while retaining their content, identity, dirty-state, cancellation and destination checks. Verify the expected validation error and preserve the original document after refused consent targets.

M3; refs #1914.
2026-10-13 12:18:41 -06:00
7437bde378 M3: draw a GNOME-style title bar on Wayland (#553) (#1530)
GNOME's Wayland compositor draws no window frames (it offers no
zxdg_decoration_manager_v1), so winit drew its own fallback title bar,
which ignores GNOME's theme, dark mode and button layout. PdfCraft now
asks the compositor at launch (apps/pdfcraft/src/decorations.rs) and,
where it draws no frames, turns winit's frame off and makes the tab strip
the header bar, as in GNOME's own apps
(crates/ui-egui/src/window_controls.rs):

- Minimize, maximize/restore and close in Adwaita's style (round buttons,
  symbolic glyphs drawn in code, no new assets), in the order and on the
  side named by org.gnome.desktop.wm.preferences button-layout, read
  through gsettings; Fedora's default `appmenu:close` shows only close.
  Hidden in full screen.
- Resize zones along the window's edges and L-shaped corners, above the
  rest of the UI so a press there resizes instead of reaching a
  scrollbar; a hairline outlines the window while it isn't maximized.
- Preferences > Use system title bar (shown only where the app draws the
  frame) gives the window back winit's frame at once, without a restart
  (part of #1001). Named as in VectorCraft and PhotoCraft, but GNOME draws
  no title bars for apps, so on GNOME this is winit's basic frame, not
  GNOME's own; the tooltip says so. Saved as `system_title_bar`;
  `system-title-bar` on the control channel.
- PDFCRAFT_TITLEBAR=app|system overrides the detection.

KDE, Sway, Hyprland, COSMIC and X11 offer server-side decorations and
keep the frame their desktop draws; macOS and Windows are unchanged.

Not done: right-clicking the header bar doesn't open GNOME's window menu
(egui has no command for it), the window has no shadow or rounded
corners, the resize zones lie just inside the window rather than outside
it, and Windows still uses its system frame (the rest of #1001).

The new labels (Minimize, Maximize, Restore, Use system title bar and its
tooltip) were added to all 21 translation catalogs. These translations
have not been checked by anyone fluent in those languages and should be
reviewed. The catalogs' hashes in ATTRIBUTION.toml are updated.

Made with Claude Code (Claude Opus 5.5, medium effort). It passes the
workspace quality gates (fmt, clippy, cargo test --workspace, xtask
assets) and was checked by hand on Fedora 44 GNOME (light and dark,
maximized and not, the preference toggled live), but it has not been
exhaustively regression tested.

Co-authored-by: bmh129 <[email protected]>
Co-authored-by: Claude Opus 5.5 <[email protected]>
Co-authored-by: Brandon Thomas <[email protected]>
2026-10-13 03:04:13 -04:00
e88dd728cb M13: scan each directory once in CLI checks (#1501)
Co-authored-by: MMexicann <[email protected]>
Co-authored-by: OpenAI Codex <[email protected]>
Co-authored-by: Brandon Thomas <[email protected]>
2026-10-13 02:41:41 -04:00
5ebb53a4a4 M4: preserve shared Type 3 glyph resources during extraction (#1500)
Co-authored-by: MMexicann <[email protected]>
Co-authored-by: OpenAI Codex <[email protected]>
Co-authored-by: Brandon Thomas <[email protected]>
2026-10-13 02:34:12 -04:00
13a921ae4a M2: keep byte boundaries in packed TIFF-predictor rows (#1499)
Co-authored-by: MMexicann <[email protected]>
Co-authored-by: OpenAI Codex <[email protected]>
Co-authored-by: Brandon Thomas <[email protected]>
2026-10-13 02:27:26 -04:00
4cb803038d M3: confirm before opening absolute GoToR paths (#1364) (#1489)
* M3: confirm before opening absolute GoToR paths (#1364)

* fix(i18n): translate GoToR confirmation dialog labels

---------

Co-authored-by: srt8hellcat2-ui <[email protected]>
Co-authored-by: Brandon Thomas <[email protected]>
2026-10-13 02:16:21 -04:00
Yousef MojahidandBrandon Thomas 0dd5fdd5bd M7: keep Arabic ActualText ligatures in their word (#1473)
Normalize single-glyph RTL replacements before assembling editable lines. Preserve authoritative multi-glyph spans and cover lam-alef variants, marks, CID codes, replacement, copy/search and save/reopen.

Assisted-by: Codex

Co-authored-by: Brandon Thomas <[email protected]>
2026-10-13 01:52:22 -04:00
Márk BartalosandBrandon Thomas fff985f30d M5: keep stamps upright on rotated pages (#1398) (#1460)
A stamp placed on a page with /Rotate was drawn in user space, so the
label or picture turned with the page. Counterrotate the appearance so
it reads upright as the page is displayed.

Co-authored-by: Brandon Thomas <[email protected]>
2026-10-13 01:42:57 -04:00
dfa43beb9f M10: BigTIFF and strict GIF detection in Create PDF; XDP data in format parity (#1453)
- Create PDF detects BigTIFF (II+/MM+) as well as classic TIFF; the tiff
  decoder already reads it, so these scans now convert instead of being
  refused.
- Image detection accepts only GIF87a/GIF89a, the headers conversion
  decodes, so a malformed GIF header is no longer called an image and then
  rejected with a misleading error.
- file-format-parity.md: XDP split into XFA data import/export (shipped
  under form.xfa-data) and opening .xdp as a document (none).

Tests: bigtiff_images_become_pages; malformed GIF headers in
source_kind_tells_pdfs_images_and_text_apart. Both fail without the fix.


Claude-Session: https://claude.ai/code/session_01F8DFYWkDyTnEcin9PNkUW5

Co-authored-by: Claude <[email protected]>
Co-authored-by: Brandon Thomas <[email protected]>
2026-10-13 01:19:48 -04:00
Márk BartalosandBrandon Thomas 9ed3abe490 Keep undrawn comments and accurate search hits (#1317, #1327, #1302, #1315, #1340) (#1442)
Flattening deleted a cloudy comment it could not draw, and a refused text-box restyle or resize left the new properties in the file. Arrow defaults were stored on plain lines. A custom match ending on a separator included the next glyph, and the vertical-text redaction error claimed an overlap it had not checked.

Co-authored-by: Brandon Thomas <[email protected]>
2026-10-13 01:09:36 -04:00
Earl CoandBrandon Thomas 890b83c4c4 Add reviewable text-only PDF auto-tagging (#1440)
Co-authored-by: Brandon Thomas <[email protected]>
2026-10-13 01:02:05 -04:00
13fbb2f6fb M0: label local builds in --version, About and the package release (#1529)
A build compiled with PDFCRAFT_BUILD_LABEL=<label> shows "0.7.1 (<label>)"
in `pdfcraft --version`, `pdfcraft-cli --version`, Help > About and the
first log line (pdfcraft_engine::build_info). The version itself is
unchanged, so the update check, the MCP server info and the User-Agent
still carry the release version.

packaging/linux/package.sh puts the label in the file names
(pdfcraft-0.7.1-<label>-linux-x86_64.rpm) and makes the .rpm/.deb release
`1.<label>` (nfpm `release: ${RELEASE}`, 1 when unlabelled). rpm sorts
0.7.1-1.<label> after 0.7.1-1 and before 0.7.2-1, so a locally built
package upgrades over the installed release and the next release replaces
it. packaging/env.sh accepts only letters, digits and inner dots, which
are valid in both rpm and deb release fields.

Unlabelled builds are unchanged.

Made with Claude Code (Claude Opus 5.5, medium effort). It passes the
workspace quality gates (fmt, clippy, cargo test --workspace, xtask assets)
and was checked by hand on Fedora 44, but it has not been exhaustively
regression tested.

Co-authored-by: bmh129 <[email protected]>
Co-authored-by: Claude Opus 5.5 <[email protected]>
2026-10-13 00:07:32 -04:00
Márk Bartalos c6128c039a M7: keep a neighbouring line when replacing text (#1316) (#1427)
Glyph boxes of tightly spaced lines overlap, so replacement treated the next line as a duplicate and removed it. A second drawing is removed only when it also shares the edited line's baseline.
2026-10-13 00:01:50 -04:00
FredAppsandCodex fcc1d9ead0 M2: adapt Lab Bradford white points in LMS (#1417)
Follow up #1237 by deriving Bradford scaling ratios from the source and
D65 white points in LMS. Preserve CalRGB and the merged Indexed/Range
handling. Add independent neutral/chromatic, identity, asymmetric Range,
and degenerate-cone render regressions.

Co-authored-by: Codex <[email protected]>
2026-10-12 23:59:51 -04:00
057e453a16 Retire inherited resources (#1348)
* M8: Retire inherited redaction resources

* Redaction: pin inherited resources only on redacted pages, shared by reference

localize_resources rewrote every page in the document on every apply and copied a direct
inherited /Resources dictionary into each one, so a large document grew by N x the dict.
Now only redacted pages that inherit resources are pinned; a direct inherited dictionary is
made one indirect object and shared by reference. The parent's /Resources is retired when no
other page still inherits it (the leak fix), and kept (through the shared reference) when an
unredacted page still draws with it. A /Parent chain that can't be followed or disagrees with
the page tree retires nothing. Tests: an unredacted page is left untouched and the rewrite is
bounded; redacting every inheriting page retires the parent's resources. ROADMAP entry moved
into the revision-history table.

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-12 23:49:57 -04:00
Márk BartalosandBrandon Thomas 87b448b664 M1: write a valid xref when an incremental save has no objects (#1339) (#1424)
An unchanged classic-table file used to append xref immediately followed by trailer, which qpdf treats as a damaged file. A zero-length subsection keeps trailer-only updates without rewriting any object entry.

Co-authored-by: Brandon Thomas <[email protected]>
2026-10-12 08:57:37 -04:00
4bfd3c2729 M14: add Lithuanian interface and date localization (#1401)
* M14: add Lithuanian interface localization

Register Lithuanian UI and date languages with three integer plural forms
and a 2,572-entry contributor-original catalog. Cover dynamic dialog labels,
contextual calendar and certificate wording, commands, tools, and every key
in the other bundled catalogs. Add real-shell calendar, inspector and
password-dialog regressions; preserve PDF values and document state.

Update localization documentation and asset attribution. Native-speaker/VLKK
terminology review remains outstanding.

Validation: cargo xtask ci passed; cargo-deny was skipped because it is not
installed. Five rendered dialogs and contributor manual testing checked UI.

Co-Authored-By: OpenAI gpt-6.1-sol <[email protected]>

* M14: preserve multilingual localization documentation

Restore the existing language descriptions instead of replacing general
localization guidance with Lithuanian-specific coverage. List every
registered language and control-channel code; keep Lithuanian details
alongside the existing catalog descriptions.

Include the previously omitted Kazakh, Polish, Slovenian, Greek and Turkish
catalogs in the shipped-language table without changing the historical
coverage snapshot or existing assessments. Keep Lithuanian roadmap notes
concise and additive.

Validation: all 22 registered languages appear in documentation; every
existing descriptive paragraph is retained. cargo xtask ci passed, with
cargo-deny skipped because it is not installed.

Co-Authored-By: OpenAI gpt-6.1-sol <[email protected]>

* M14: record completed Lithuanian native-speaker review

The contributor confirms they are a native Lithuanian speaker and have
reviewed the translation. Remove obsolete pending-review labels from the
asset manifest and documentation; regenerate ATTRIBUTION.md. Remove the
redundant generated-message disclaimer from the Lithuanian description,
keeping the existing general localization documentation unchanged.

Validation: cargo xtask ci passed; cargo-deny was skipped because it is
not installed. Catalog content and its SHA-256 are unchanged.

Co-Authored-By: OpenAI gpt-6.1-sol <[email protected]>

* Lithuanian: cover main's Copy Image labels, list lt last, check its glyphs

Adds the six Copy Image rows from #1363, moves lt to the end of LANGUAGES
and DATE_LANGUAGES (after tr), adds lt.tsv to the primary-font coverage
test, writes the JavaScript preferences path with '>' as the other
catalogs do (Inter has no ▸), and refreshes counts and hashes.

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

---------

Co-authored-by: OpenAI gpt-6.1-sol <[email protected]>
Co-authored-by: Brandon Thomas <[email protected]>
Co-authored-by: Claude Opus 5.5 <[email protected]>
2026-10-12 08:54:15 -04:00
2839152af1 M12: preview text structure proposals for auto-tagging (#1383)
* M12: preview text structure proposals for auto-tagging

* Auto-tag preview: no unregistered command, allowed on copy-restricted files like a11y.check

The preview is read-only with no UI command yet, like render_preview, so it no
longer names the Planned a11y.autotag catalog id (tool_table_is_well_formed).

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

* M12: preview text structure proposals for auto-tagging

* Keep read-only auto-tag preview separate from unavailable command

---------

Co-authored-by: Brandon Thomas <[email protected]>
Co-authored-by: Claude Opus 5.5 <[email protected]>
2026-10-12 08:20:13 -04:00
5cce1b247f M10: edit OCR text on scanned pages so the change shows (#1397)
* M10: edit OCR text on scanned pages so the change shows

Editing a recognised word on a scan rewrote it as invisible OCR text, so
the page still showed the old word, and the whole OCR layer grouped into
one paragraph that rewrapped when one word changed.

- An edited invisible paragraph is drawn visibly, over a cover in the
  paper colour sampled around it, in the scan's sampled ink colour
  (or the chosen colour).
- OCR words group into phrases: a table cell or a tab stop splits them
  and each scanned line stays apart; a phrase grows rightwards.
- Styled rewrites measure with the text matrix's horizontal scale, so
  stretched OCR words keep their widths instead of overlapping.
- The inline editor covers the scanned word and grows like the result.

* M10: size OCR text from each word's letters

A word box from recognition is tight around the ink, but the text layer
assumed every box spans Helvetica's ascender-descender height. A
capitals-only word such as "DT" spans only the cap height, so its text
was set about 23% too small and its baseline floated up; editing it
then drew the new word visibly smaller than the scanned one.

The em and baseline now come from the word's letters: cap height or
x-height on top, descender or baseline at the bottom. On a real scanned
form, a rewritten "DT" -> "DSI" now matches the scanned word's height
and baseline. The engine OCR-edit tests build their words the way a scan
would box them.

* Edit OCR text: sample colours only for invisible blocks, from the current bytes

Session::apply looked up every edited block with a fresh text_blocks pass and
re-serialized the whole document to render the sampling tile. Text blocks are
now cached per page on the editor (keyed on the unmodified document's source
identity, so any edit, undo or save invalidates them), visible blocks skip
sampling entirely, and the tile renders from the document's current bytes.
Adds a visible stretched-text regression test and documents that editing any
invisible text makes it visible over a painted cover.

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

* Edit OCR text: name the cached text-blocks tuple (clippy), rustfmt

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-12 08:16:42 -04:00
2a004529dd M14: Preserve recovery snapshots without readable metadata (#1378)
* M14: Preserve recovery snapshots without readable metadata

Recovery writes the PDF before its metadata sidecar. A failed sidecar write
or interrupted save could leave a usable snapshot that the next listing
silently deleted.

List PDF snapshots without deleting recovery files. When metadata is
missing, unreadable, corrupt or refers to another key, retain the snapshot
and recover it without an original path so saving requires a destination.
Keep the existing password flow for encrypted snapshots.

Add regression coverage for failed metadata writes, repeated listing,
missing/corrupt/mismatched metadata and encrypted recovery. Update the M14
progress documentation.

Validation: the failed-write regression fails before the fix; all 11
recovery tests pass after it. Formatting and the 31-crate WASM check pass.
The full workspace and strict Clippy gates remain red; see the PR body
for the failing tests and diagnostics.

* Recovery: replace damaged metadata once its snapshot is recovered; one progress-log entry

Recovering a snapshot whose sidecar was missing, unreadable or for another
key now writes fresh metadata for it (recording whether it is encrypted), so
the damaged file doesn't linger. clear() also removes sidecars whose PDF is
gone; snapshots themselves are never removed by listing. The header and M14
milestone-row edits are replaced by a single progress-log entry.

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

---------

Co-authored-by: LabDennis <[email protected]>
Co-authored-by: Brandon Thomas <[email protected]>
Co-authored-by: Claude Opus 5.5 <[email protected]>
2026-10-12 07:13:35 -04:00
88d96c93a8 M2: snap shrunk interpolated images to whole pixels so scans stay sharp (#1377)
* M2: snap shrunk interpolated images to whole pixels so scans stay sharp

A scanned page (one 300 dpi image with /Interpolate true) was shrunk to a
ceil-sized raster and then sampled a second time at a fractional scale and
offset, which softened every edge next to Acrobat. An axis-aligned shrunk
interpolated image is now shrunk once to its rounded device rectangle and
drawn 1:1 at the rounded corner. Rotated, skewed, flipped, Type 3 and
non-interpolated images are unchanged.

On a synthetic scan page the mean error against an area-average reference
falls from 3.4 to 1.6 grey levels at 96 dpi (3.0 to 1.5 at 144 dpi).

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

* vendor: list the hayro snap patch as (14) in the Patches and Tests columns; correct gaps.md patch counts

The patch description had been appended to the Tests column of one of the
two hayro rows. gaps.md now counts the distinct patches vendor/README.md
lists (26 hayro-interpret, 15 hayro, 3 hayro-jbig2, 17 hayro-syntax).

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

---------

Co-authored-by: Claude Sonnet 5.5 <[email protected]>
Co-authored-by: Brandon Thomas <[email protected]>
2026-10-12 07:12:02 -04:00
186c1dc767 M7: preserve Arabic text order in copy, search and editing (#1369)
* M7: preserve Arabic text order when copying and editing

Record per-line ActualText for shaped added text and preserve its logical order in Edit text. Cover mixed scripts, marks, copy/search, retyping and save/reopen; retain the drawing golden.

Assisted-by: Codex

* Edit text: keep only right-to-left ActualText out of line joining and bidi recovery

Word and Chrome wrap Latin ligatures in /ActualText spans mid-word; splitting
lines at every span broke such words into pieces in Edit Text. Only spans
whose ActualText has strong right-to-left characters keep their own line and
typing order now. Regression: a Latin fi ligature span stays on its line.

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-12 07:11:23 -04:00
d2d1b16b21 M5: adjust Fill and Sign text size (#1285)
Co-authored-by: Brandon Thomas <[email protected]>
Co-authored-by: Claude Opus 5.5 <[email protected]>
2026-10-12 06:13:48 -04:00
Jules Kreuer 47d612817a M8: Clean indirect and form tag references (#1351) 2026-10-12 05:43:12 -04:00
51982db121 M3: match Greek final sigma in case-insensitive search (#1349)
* M3: use the macOS Finder label in the tab-menu regression

The UI already uses Reveal in Finder on macOS, but the regression test
queried Reveal in File Explorer on every platform. Select the platform's
label while preserving the disabled-action and inactive-tab assertions.

Co-Authored-By: Codex <[email protected]>

* M3: match Greek final sigma in case-insensitive search

Fold capital, normal and final sigma to the same character so Greek words
match in either case. Cover exact search, whole words, glyph ranges and
bounded matches without changing the existing Turkish-I behavior.

Record the search fix and the macOS test portability follow-up in the
roadmap. Workspace tests, all-target Clippy, formatting, wasm, layering
and a synthetic CLI before/after comparison pass on macOS.

Co-Authored-By: Codex <[email protected]>

---------

Co-authored-by: nevicia2010-hub <[email protected]>
Co-authored-by: Codex <[email protected]>
2026-10-12 05:42:27 -04:00
0e55d07b5f M12: say when a form's scripts were turned off (#1251)
* M12: say when a form's scripts were turned off

When an XFA script runs past its time limit it is abandoned and the
form's scripts are turned off for the document. The only trace was an
error line in the JavaScript console, so calculations and buttons just
stopped working.

- Engine: Document::xfa_scripts_off() reports the state (set on open and
  on an edit), so a viewer shows it without reading error text. doc_info
  reports it to agents as xfa_scripts_off.
- App: when it happens (at open or after a click) a toast says so once,
  and the notice bar says so from then on, ahead of its other notices,
  with a JavaScript Console button. A notice bar the user had closed
  comes back for it; closed again, it stays closed.
- The sentence is translated in the 17 catalogs that are complete
  (hashes in ATTRIBUTION.toml updated).

Tests: two kittest UI tests (runaway at open; runaway click after the
bar was closed), which fail without the app change; engine assertions
for both states; a doc_info tool test.

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

* Catalogs: the scripts-off notice in Greek, Polish and Telugu; refresh hashes

---------

Co-authored-by: Claude Opus 5.5 <[email protected]>
Co-authored-by: Brandon Thomas <[email protected]>
2026-10-12 03:42:56 -04:00
fcf6e39d46 M14: tune Windows low-DPI JetBrains Mono hinting (#1264)
* M14: tune Windows low-DPI mono hinting

* Theme: ignore the raster-sharpness probe test

`windows_hinting_improves_low_dpi_mono_raster_without_changing_layout_or_coverage`
asserts exact egui rasterisation deltas that move with egui upgrades; it is now
`#[ignore]` (run with --ignored when retuning). The tweak and its policy test
stay.

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-12 03:25:25 -04:00
8cacb44728 M3: follow Windows' Animation effects setting; add an Animation effects preference (#1235)
* M3: follow Windows' Animation effects setting; Smooth scrolling preference

With Settings > Accessibility > Visual effects > Animation effects turned
off, the wheel still eased: egui spreads a coarse wheel step over about a
quarter of a second, for scrolling and for Ctrl-wheel zooming, and has no
switch for it.

- crates/ui-egui/src/motion.rs reads each frame's wheel input the way egui
  does, minus the easing, and hands it to the scroll areas and the views
  (zoom_delta). A test sums what egui eases and compares it with what is
  read at once, and fails if egui stops easing.
- With the system asking for no animations, egui's own animations take no
  time and the progress card's bar steps without its sweep.
- The desktop app asks WinRT's UISettings.AnimationsEnabled at launch and
  every two seconds of use, so a change applies without a restart.
- Preferences has Smooth scrolling: Use system setting (default), On, Off.
  On or Off decides the wheel's scrolling and zooming, and scrolling to a
  place, whatever the system says. Saved, settable as smooth_scrolling on
  the control channel, translated in all 20 catalogs.

How far a notch scrolls or zooms is unchanged. macOS, Linux and the
browser aren't asked for their setting yet; Off works everywhere.

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

* Credit Jeremy Yao by name in About (contributors/people.toml)

My own entry: real name and display name "Jeremy Yao".

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

* M3: rename Smooth scrolling to Animation effects; it decides every animation

The preference also governs Ctrl-wheel zooming, so "Smooth scrolling"
described only part of it. As "Animation effects" (Use system setting, On,
Off) it now decides every animation in the window, the way the Windows
setting of the same name does:

- the wheel's scrolling and zooming, and scrolling to a place (as before);
- egui's own animations, the progress card's sweep, and the chrome's hover
  and press washes (new on main, with durations of their own), which used
  to follow the system alone or not at all.

Saved and set on the control channel as animation_effects (system, on,
off); the unreleased smooth_scrolling name is gone. The three labels are
retranslated in all 20 catalogs.

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

* Tests: the app's two real-GPU tests take turns (FreeBSD SIGSEGV)

FreeBSD CI died with SIGSEGV in the pdfcraft binary's tests on this branch
(d4b928f1). The log shows a_gpu_error_while_a_window_is_set_up... (started
first, still running) and pipeline_creation_error... (started shortly
before) both in flight when the process died: two test threads inside the
graphics libraries at once. Nothing else still running was native code.

Both now take one lock for the whole test, as the UI tests already do for
the same reason (crates/ui-egui/tests/view.rs). Each takes about two
seconds with a device, which is why they can overlap.

The FreeBSD job has been failing at the same step with the same exit code
on main (84a75eae, 4a1e205f, 3d2bdfe7: every completed run since #1042
added the second real-GPU test) and on unrelated PRs. Their logs weren't
available to confirm it is the same crash.

Not reproduced locally (no FreeBSD machine here); the next FreeBSD run is
the check.

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-12 02:46:40 -04:00
c1d98946a9 M7: open local GoToR PDF links natively (#1265)
* M7: open local GoToR PDF links natively

* GoToR links: read only regular .pdf files under a size cap; add missing translations

A GoToR link in an untrusted PDF could name /dev/zero (unbounded read), a FIFO
(UI thread hangs) or a directory. Both the UI path and the `link_open` tool now
go through `pdfcraft_engine::read_remote_document`, which requires a `.pdf`
extension and a regular file (metadata checked before opening) of at most
1 GiB, and stops reading if the file grows past the cap. Regression tests cover
directories, a /dev/zero symlink and a FIFO (unix) in the engine, the tool and
the UI.

Adds the four link notifications to all 20 catalogs and "The existing action
will be preserved." to el, es, pl, te and zh-hant.

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

---------

Co-authored-by: Brandon Thomas <[email protected]>
Co-authored-by: Claude Opus 5.5 <[email protected]>
2026-10-12 02:34:19 -04:00
2aea7dcfeb M10: print from the web build through the browser's print dialog (gap 4) (#1243)
* Gates: fix two clippy failures on main

- ocr_ui: "Also recognize pages that already have text" used egui's own
  checkbox, which clippy.toml disallows (#1184 landed beside #1173);
  use widgets::checkbox like every other box.
- camera_edges: Rust 1.97's manual_range_contains; the rewrite keeps the
  old answer for a NaN angle (false) with an is_finite check.

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

* M10: print from the web build through the browser's print dialog (gap 4)

The web build had no printer: spool::submit refuses on wasm, so Print
could only download the print-ready PDF. Now its Print dialog offers
"Browser's print dialog" (the default there), which reaches every
printer the operating system knows.

- web_print: PdfCraft draws the print-ready PDF's sheets itself
  (engine Exporter), one sheet a frame since wasm has no threads:
  300 dpi up to 100 sheets, 200 up to 300, 150 up to 1,000, at most
  64 MP a sheet and 1 GiB of PNGs a job. The PNGs become blob URLs
  and print from a hidden srcdoc iframe, one sheet per page, margin 0.
- Chromium prints every page at the default @page size, so a sheet of
  the paper's size turned sideways is rotated 90 degrees CCW onto it
  (as drivers do); other sizes get named @page rules.
- Copies, two-sided and colour are greyed out (the browser's dialog
  sets them). If print() is refused (e.g. a sandboxed frame), the
  print-ready PDF downloads instead, with the reason.
- pdfcraft-print stays free of web APIs; six new strings in all 20
  catalogs (ATTRIBUTION hashes refreshed); docs and parity updated,
  gap 4 closed.

Verified in headless Chromium against a trunk build: three sheets at
300 dpi, the landscape sheet turned, no blank trailing page, and the
download fallback. Firefox and Safari not yet tried.

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

---------

Co-authored-by: Claude <[email protected]>
Co-authored-by: Brandon Thomas <[email protected]>
2026-10-12 02:31:36 -04:00
13637b8440 M7: transform private image crops across nested states and streams (#1269)
Co-authored-by: OpenAI Codex <[email protected]>
Co-authored-by: Brandon Thomas <[email protected]>
2026-10-12 01:38:32 -04:00
Raj PatelandClaude Opus 5.5 d0975b974d M5: quick-bar colour recolours the selected comment (#1191) (#1255)
* Fix selected comment quick-bar color (#1191)

* M5: skip quick-bar recolour when commenting is forbidden (#1191)

The quick-bar colour swatch now checks allows_annotation() before
restyling the selected comment, matching the canvas and context-menu
gating. Without it, a restricted document showed a "Change comment
properties failed" toast on every swatch pick; the engine already
rejected the edit. Adds a regression test on an encrypted, print-only
fixture that fails without the guard.

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

* M5: quick-bar colour also recolours a selected arrow (#1191)

Arrows are saved as Line annotations with an ending, so tool_for()
reports them as the Line tool and the Arrow tool's swatch never matched
the selected arrow. comments::made_by() treats the Line and Arrow tools
as the same kind. Adds a regression test that fails without the fix.

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

---------

Co-authored-by: Claude <[email protected]>
2026-10-12 01:32:40 -04:00
Earl CoandBrandon Thomas 05a862995d Accessibility: show figure findings on the page (#1280)
* Highlight locatable accessibility figure findings

* M12: locate annotation and form accessibility findings

* M12: keep one contributor profile entry

---------

Co-authored-by: Brandon Thomas <[email protected]>
2026-10-12 01:29:25 -04:00
Raj PatelandClaude Opus 5.5 1970935587 M14: edits let go of the previous working file before writing the next (gap 13) (#1279)
* M0: keep the camera edge test NaN-safe under clippy's manual_range_contains

Clippy 1.97 flags `d < 0.35 || d > PI - 0.35` in the camera edge hit test. Its suggested
`!(0.35..=PI - 0.35).contains(&d)` would count a NaN direction as a hit, which the two
comparisons did not, so the NaN check is kept explicitly. Behaviour is unchanged.

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

* M14: edits let go of the previous working file before writing the next (gap 13)

Every edit writes a new working file: the original plus one appended revision. The previous
one stayed alive in the working bytes, the display bytes and the renderer while the next was
written, so after a few edits a 1 GiB file peaked at three copies.

Each refresh path (full, comments/form, content, undo, redo, after save) now releases them
first: the working and display bytes fall back to the original (already held by the editor)
and the renderer is replaced by an empty one until the refresh rebuilds it. A refresh that
still fails after rollback draws the original instead of nothing. Save still writes from the
editor, never from these.

Measured on a synthetic 1 GiB file (release build, Linux): five edits peak at 2.1 GiB instead
of 3.1 GiB, in 8.5 s instead of 16 s. Saving still peaks at three copies; reading from disk on
demand stays blocked on the renderer (hayro parses one contiguous buffer, gap 2). gaps.md,
the parity notes and the roadmap record both.

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

---------

Co-authored-by: Claude <[email protected]>
2026-10-12 01:28:01 -04:00
DWAA1660andBrandon Thomas 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 <[email protected]>
2026-10-12 01:25:38 -04:00
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 <[email protected]>
Co-authored-by: Brandon Thomas <[email protected]>
2026-10-11 21:16:34 -04:00
7caaa35a81 Fix Word invoice layout and pagination (#773) (#1126)
* M10: keep invoice overlays out of Word body flow (#773)

Export inferred tables without visible borders. Keep text and images
positioned on pages with artwork, rotated or large text, and retain
each source page's size, crop rotation and units.

Cover invoice conversion through the automation tools, and handle
unavailable Windows symlink setup in the confinement test.

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

* M10: add @lollinoo's chosen contributor credit (#773)

The contributor requested their public GitHub profile name while preparing the #773 PR.

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

* Word export: float a page only when text overlaps an image or watermark

positioned() turned every page with any image, or with text twice the body
size, into absolutely positioned VML text boxes, so a page with a logo or a
big title lost its flowing paragraphs, headings and tables. Now a page floats
only when it has rotated text, or when an image or a watermark-sized text
block actually overlaps other text (bounded pairwise test). Other pages flow
with images inline. Adds a regression test for a page whose image and title
sit clear of the text, and updates the docs and parity notes.

---------

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

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

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

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

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

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

* Revert "M0: remove upstream trademark artwork from contributor fork"

This reverts commit 3aa539b5b6.

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

---------

Co-authored-by: Kamoliddin Fatkhullaev <[email protected]>
Co-authored-by: OpenAI Codex <[email protected]>
Co-authored-by: Brandon Thomas <[email protected]>
Co-authored-by: Claude Opus 5.5 <[email protected]>
2026-10-11 20:27:55 -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
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
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
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
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
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
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