* feat(ui): add PDF tab context menu (#1233)
* Tab menu: reveal through D-Bus off the UI thread
`dbus-send --print-reply --reply-timeout=2000` (and the xdg-open fallback)
could block the frame for 2 s or more; on Linux and FreeBSD the reveal now runs
on a `pdfcraft-reveal` thread and a failure is logged. The existence check stays
on the UI thread and is still reported; Windows and macOS launches are
unchanged. Refreshes the catalog hashes after the main merge.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
---------
Co-authored-by: srt8hellcat2-ui <[email protected]>
Co-authored-by: Brandon Thomas <[email protected]>
Co-authored-by: Claude Opus 5.5 <[email protected]>
* Comments: "Other colour…" picker and recent custom colours under the swatches (#1193)
The annotation tools offered only the ten fixed swatches. The shared swatch
grid (comments' quick bar, right-click menu and Comment Properties, and the
other colour choosers that use it) now ends with "Other colour…", a full
colour picker that starts from the current colour; Apply commits it once.
Applied custom colours are kept as a "Recent colours" row (newest first,
five at most, swatches not repeated) for the session.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Claude-Session: https://claude.ai/code/session_01CVtZHu998NDhf4Z8QvS5pd
* Comments: import kittest's Queryable in the Other colour test (#1193)
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Claude-Session: https://claude.ai/code/session_01CVtZHu998NDhf4Z8QvS5pd
* Comments: "Other colour…" is a "+" swatch so the grid keeps its height (#1193)
A row under every swatch grid made dense dialogs (Redaction Tool
Properties has two grids) taller, pushing their lower controls out of
reach. The picker now opens from a swatch-sized "+" at the end of the
first row; the Recent colours row appears only once a custom colour has
been applied.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Claude-Session: https://claude.ai/code/session_01CVtZHu998NDhf4Z8QvS5pd
* Comments: each colour grid keeps its own "Other colour…" state
Two swatch grids drawn in one parent (Redaction properties, Prepare a form) shared the
picker's open state, so "+" opened both. The state and the swatch grid are now keyed on
each grid's own place in its parent.
* Comments: test that two colour grids in one parent open separately
* ATTRIBUTION.toml: catalog hashes after merging main
---------
Co-authored-by: ngrafton2006 <[email protected]>
Co-authored-by: Claude Opus 5.5 <[email protected]>
Co-authored-by: Brandon Thomas <[email protected]>
* Make Arabic output golden independent of Flate backend
* Arabic golden: keep /Filter and /DecodeParms in the hashed bytes
Normalise only the raw stream bytes (and so /Length, which the writer
takes from them), so a change that stops compressing generated streams or
switches filters still fails the golden. Fingerprint recomputed (same with
and without craft-fonts).
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]>
* test: give faux-redaction audit its own workdir (parallel-test flake)
* hayro: ColorReplacement type and luminance mapping (reading colours, #897)
* fmt: engine dates (upstream Greek localization miss)
* hayro: apply ColorReplacement to solid text and line-art paints (#897)
* render: ReadingColors view setting flows into RenderSettings (#897)
* engine: per-document reading colours state (#897)
* automation: view_reading_colors tool; page_render honours it (#897)
* ui: Reading colours preference (Replace Document Colors, #897)
* ui: seed reading colours on revision and signed-version opens (#897)
* parity: a11y.replace-page-colors shipped (#897)
* render: map stencil-mask paints through reading colours (#897)
An ImageMask stencil's solid paint was baked into the raster bytes raw,
bypassing the reading-colours hook every other solid fill goes through:
under white-on-black, CCITT scans, JBIG2 text and stencil logos drew
black ink on the black mapped background and vanished. Route it through
replaced(paint, false) — stencils are line art in disguise — and cover
it with an ImageMask fixture test sampling mapped ink on a mapped
background.
* render: print previews render with reading colours off (#897)
Print previews went through the document's render pool, whose config now
carries the reading colours: a white-on-black user saw black sheets in
the Print dialog while the actual print stayed uncoloured. The request
tag is the per-request channel that already crosses the render boundary,
so PRINT_TAG now lives in pdfcraft_render and the renderer honours it by
rendering previews with reading colours off — a preview's job is
fidelity to the printout. The main view stays recoloured. Covered on
both render paths (inline PageRenderer and pool worker).
* ui: seed reading colours on created-document opens (#897)
open_created (comment summaries) and open_created_bytes (images, text,
blank pages, clipboard) pushed new DocViews without seeding the reading
colours, unlike every other open path. A created document now starts
with the app-wide preference too; the seed-on-open test drives both
paths.
* docs: reading-colours flag echo and display-fidelity parity notes (#897)
The view_reading_colors description now says a flag that differs from
the mode's preset default makes the echoed mode read custom, and the
a11y.replace-page-colors parity notes record that print previews and
view snapshots render with the document's true colours: reading colours
apply to the reading view only.
* Merge follow-ups: reading colours beside preview renders and the new catalogs
- Renderer::new carries both color_replacement and the new preview flag
- our raster tests construct RenderRequest with preview: false
- the preference dialog grew past the window: scroll the combo into view
and give the fixture a valid xref so no repair banner covers the dialog
- checkboxes use the crate's square-box widget (clippy disallows egui's)
- Kazakh and Turkish catalogs gain the ten reading-colours strings
* Reading colours: record the hayro patch in vendor/README.md; add missing translations
vendor/README.md gains hayro patch (13): `RenderSettings::color_replacement` /
`ColorReplacement`, the `render_pdf` signature change and the removed
`[dev-dependencies.log]`, with its tests. Adds the ten reading-colours labels
to el, es, pl, te and zh-hant.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
---------
Co-authored-by: Brandon Thomas <[email protected]>
Co-authored-by: Claude Opus 5.5 <[email protected]>
* 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]>
* 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]>
* 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]>
* Expose tag parent and depth for hierarchy view
* Accessibility: add visual tag properties panel
* Show collapsible tag hierarchy in accessibility panel
* Open checker findings in the matching structure tag
* Translate Tags panel across supported languages
* Tags panel: walk the structure tree once per edit, not every frame
The Tags panel and checker findings that open a tag share a tag list cached per
document and edit generation.
---------
Co-authored-by: Brandon Thomas <[email protected]>
* Save accessibility review decisions against PDF revision
* Accessibility review sidecars: read them with a 1 MiB cap
The sidecar path is the caller's and may name any file, so reading it
whole was an unbounded allocation. Stop past 1 MiB and refuse an
oversized file as not a sidecar (load fails, save leaves it alone), like
malformed content. Tests cover malformed, binary and oversized files.
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]>
* fix: recognize paragraph drafts as unsaved work (Issue #811)
* fix(color): honour the /Lab colour space /WhitePoint (#1203)
The old path fed every Lab value to the bundled ICC transform as if the
space's white point were D50, so a /Lab space with any other /WhitePoint
(e.g. D65 [0.9505 1 1.089]) rendered off by several levels per channel
and a D65 blue came out 45 levels too red.
Map L*a*b* to XYZ in the space's own white coordinates (ISO 32000-2
§8.6.5.4), Bradford-adapt it to D65 and convert to sRGB directly, as
Poppler does. Missing or degenerate white points fall back to a flat
white and hostile components are clamped without panicking.
Add a render regression that checks the five reported flat patches of
the D65 white-point fixture against their CIE sRGB values.
* Lab: map normalised Indexed palette components onto L*/Range; drop unused LAB.icc
An /Indexed palette over Lab passes its entries as byte/255 (manual_scale),
which the new Lab conversion read as raw L*a*b* and drew near black. Map
them onto L* 0..100 and the a*/b* /Range, with a regression test. Record the
patch as hayro-interpret (25) in vendor/README.md, and remove LAB.icc (no
longer referenced) with its ATTRIBUTION, NOTICE and assets README entries.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
* Main gates: rustfmt tests/viewer.rs; a type alias for the refresh-steps array (clippy type_complexity)
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]>
The desktop app started `reg.exe query "HKCU\Control Panel\International" /v iMeasure` at every launch to pick the default Units (#463). On managed PCs reg.exe is a process EDR and WDAC/AppLocker setups watch, and a Sandbox process-creation audit showed it as the one child process left at startup. Read the value in-process with winsafe's safe RegGetValue (advapi feature) instead. Behaviour is unchanged: "1" is inches, "0" is millimetres, anything missing or unreadable falls through to the locale check. The value-to-unit mapping is a pure function with a unit test, plus a Windows test that the registry read yields a known value.
Co-authored-by: Claude Opus 5.5 <[email protected]>
Use the queue component of a validated CUPS destination to locate its PPD,
while preserving the full queue/instance name when querying lpoptions.
Cover instance defaults and command arguments with synthetic PPDs and a
stand-in process, retaining path validation and the existing public API.
Co-authored-by: srt8hellcat2-ui <[email protected]>
control::serve documents that the first request on a connection must be
`auth` and that anything else closes the connection. A first line that
wasn't valid JSON got a parse error but left the connection open, unlike a
wrong token or a non-auth request. Close it in that case too; after a
successful auth a malformed line still just gets the parse error.
Adds control::transport_tests::malformed_input_before_auth_closes_the_connection,
which fails before this change.
Co-authored-by: Claude Opus 5.5 <[email protected]>
* 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]>
On upright horizontal text of unrotated pages the caret's tip goes to the
nearer character edge on the baseline, sized to the text; elsewhere it stays
at the click. The size is mapped to user space, so /UserUnit pages match.
Parity stays partial: snapping is UI-only.
AI-assisted (Claude Opus 5.5), reviewed by GPT-5.6-terra (Codex CLI).
Native review welcome.
Co-authored-by: Diego Ospina <[email protected]>
Co-authored-by: Claude Opus 5.5 <[email protected]>
Parse the committed parity manifest in the existing xtask unit-test suite
so duplicate TOML keys fail cargo test --workspace.
Co-authored-by: srt8hellcat2-ui <[email protected]>
When an annotation was added or edited, page_changed cleared search matches for the page and reset f.current to None. When the background text reload finished, refresh_find_page saw f.current.is_none() and invoked flash_match(), setting view.goto and causing the canvas viewport to jump after every drawn element.
- Preserve search state in page_changed instead of prematurely clearing matches and current match index.
- In refresh_find_page, gate flash_match() so it only jumps when no match was previously selected (e.g. initial search query), avoiding viewport jumps during background text reloads.
- Add regression test marking_while_searching_does_not_jitter_or_reset_search_state.
Fixes#1254
* 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]>
* #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]>
* 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 <[email protected]>
---------
Co-authored-by: KrzysztofC <[email protected]>
Co-authored-by: Brandon Thomas <[email protected]>
Co-authored-by: Claude Opus 5.5 <[email protected]>
* 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 <[email protected]>
* 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 <[email protected]>
---------
Co-authored-by: Fabio Lussi <[email protected]>
Co-authored-by: Claude Opus 5.5 <[email protected]>
Co-authored-by: Brandon Thomas <[email protected]>
* 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 <[email protected]>
---------
Co-authored-by: Brandon Thomas <[email protected]>
Co-authored-by: Claude Opus 5.5 <[email protected]>
* 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 <[email protected]>
---------
Co-authored-by: Brandon Thomas <[email protected]>
Co-authored-by: Claude Opus 5.5 <[email protected]>
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]>
* 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]>
- 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 <[email protected]>
* 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 <[email protected]>
* 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]>
* 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 <[email protected]>
* 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 <[email protected]>
* fix(x11): prefer server DPI over panel physical DPI in winit
Without Xft DPI, use the X server screen dimensions before the output physical density. Xorg can run a dense panel at 96 DPI; EDID sizes alone make only winit apps appear zoomed.
Supersedes #908: preserves Xft.dpi priority, configured HiDPI screens, and WINIT_X11_SCALE_FACTOR override (including randr). Falls back to RandR output density when server dimensions are missing.
Adds default_dpi_factor() with 4 unit tests plus vendored-copy guard test in apps/pdfcraft/tests/winit_x11_dpi.rs.
* vendor/README: record the winit X11 DPI patch as (11)
---------
Co-authored-by: Brandon Thomas <[email protected]>
* i18n(es): add 84 missing rows, consistent tú register
Spanish lacked 84 labels that every other full catalog has (About window,
field alignment, redaction marks, certificate details, address warnings…),
so they showed in English. Five strings used formal usted; the field mask
hint was garbled; per-page rotate now matches the other rotate commands.
AI-assisted, native review welcome.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Claude-Session: https://claude.ai/code/session_01LtFRDwSXTyqaPWCYnbBGnX
* Spanish: one row each for Run form script and Mark differences
---------
Co-authored-by: Diego Ospina <[email protected]>
Co-authored-by: Claude Opus 5.5 <[email protected]>
Co-authored-by: Brandon Thomas <[email protected]>
* M5: show actions for selected page text
* Selection actions: literal labels (so catalogs can cover them), no unwrap or unreachable
* Catalogs: Add comment in every interface language; refresh hashes
---------
Co-authored-by: Brandon Thomas <[email protected]>
* M14: choose Tools panel side in Preferences
* Catalogs: Tools panel side in every interface language; refresh hashes
* OCR dialog: the shared outlined checkbox (raw ui.checkbox is linted since #1173)
---------
Co-authored-by: Brandon Thomas <[email protected]>