* test: give faux-redaction audit its own workdir (parallel-test flake)
* hayro: ColorReplacement type and luminance mapping (reading colours, #897)
* fmt: engine dates (upstream Greek localization miss)
* hayro: apply ColorReplacement to solid text and line-art paints (#897)
* render: ReadingColors view setting flows into RenderSettings (#897)
* engine: per-document reading colours state (#897)
* automation: view_reading_colors tool; page_render honours it (#897)
* ui: Reading colours preference (Replace Document Colors, #897)
* ui: seed reading colours on revision and signed-version opens (#897)
* parity: a11y.replace-page-colors shipped (#897)
* render: map stencil-mask paints through reading colours (#897)
An ImageMask stencil's solid paint was baked into the raster bytes raw,
bypassing the reading-colours hook every other solid fill goes through:
under white-on-black, CCITT scans, JBIG2 text and stencil logos drew
black ink on the black mapped background and vanished. Route it through
replaced(paint, false) — stencils are line art in disguise — and cover
it with an ImageMask fixture test sampling mapped ink on a mapped
background.
* render: print previews render with reading colours off (#897)
Print previews went through the document's render pool, whose config now
carries the reading colours: a white-on-black user saw black sheets in
the Print dialog while the actual print stayed uncoloured. The request
tag is the per-request channel that already crosses the render boundary,
so PRINT_TAG now lives in pdfcraft_render and the renderer honours it by
rendering previews with reading colours off — a preview's job is
fidelity to the printout. The main view stays recoloured. Covered on
both render paths (inline PageRenderer and pool worker).
* ui: seed reading colours on created-document opens (#897)
open_created (comment summaries) and open_created_bytes (images, text,
blank pages, clipboard) pushed new DocViews without seeding the reading
colours, unlike every other open path. A created document now starts
with the app-wide preference too; the seed-on-open test drives both
paths.
* docs: reading-colours flag echo and display-fidelity parity notes (#897)
The view_reading_colors description now says a flag that differs from
the mode's preset default makes the echoed mode read custom, and the
a11y.replace-page-colors parity notes record that print previews and
view snapshots render with the document's true colours: reading colours
apply to the reading view only.
* Merge follow-ups: reading colours beside preview renders and the new catalogs
- Renderer::new carries both color_replacement and the new preview flag
- our raster tests construct RenderRequest with preview: false
- the preference dialog grew past the window: scroll the combo into view
and give the fixture a valid xref so no repair banner covers the dialog
- checkboxes use the crate's square-box widget (clippy disallows egui's)
- Kazakh and Turkish catalogs gain the ten reading-colours strings
* Reading colours: record the hayro patch in vendor/README.md; add missing translations
vendor/README.md gains hayro patch (13): `RenderSettings::color_replacement` /
`ColorReplacement`, the `render_pdf` signature change and the removed
`[dev-dependencies.log]`, with its tests. Adds the ten reading-colours labels
to el, es, pl, te and zh-hant.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
---------
Co-authored-by: Brandon Thomas <echelon@gmail.com>
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
* 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 <noreply@anthropic.com>
* 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 <noreply@anthropic.com>
* 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 <noreply@anthropic.com>
* 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 <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Co-authored-by: Brandon Thomas <echelon@gmail.com>
* M7: open local GoToR PDF links natively
* GoToR links: read only regular .pdf files under a size cap; add missing translations
A GoToR link in an untrusted PDF could name /dev/zero (unbounded read), a FIFO
(UI thread hangs) or a directory. Both the UI path and the `link_open` tool now
go through `pdfcraft_engine::read_remote_document`, which requires a `.pdf`
extension and a regular file (metadata checked before opening) of at most
1 GiB, and stops reading if the file grows past the cap. Regression tests cover
directories, a /dev/zero symlink and a FIFO (unix) in the engine, the tool and
the UI.
Adds the four link notifications to all 20 catalogs and "The existing action
will be preserved." to el, es, pl, te and zh-hant.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
---------
Co-authored-by: Brandon Thomas <echelon@gmail.com>
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
* 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 <noreply@anthropic.com>
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 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_019ojKsSCHWk2AuyDTFMHCd7
---------
Co-authored-by: Claude <noreply@anthropic.com>
Co-authored-by: Brandon Thomas <echelon@gmail.com>
* Expose tag parent and depth for hierarchy view
* Accessibility: add visual tag properties panel
* Show collapsible tag hierarchy in accessibility panel
* Open checker findings in the matching structure tag
* Translate Tags panel across supported languages
* Tags panel: walk the structure tree once per edit, not every frame
The Tags panel and checker findings that open a tag share a tag list cached per
document and edit generation.
---------
Co-authored-by: Brandon Thomas <echelon@gmail.com>
* fix: recognize paragraph drafts as unsaved work (Issue #811)
* fix(color): honour the /Lab colour space /WhitePoint (#1203)
The old path fed every Lab value to the bundled ICC transform as if the
space's white point were D50, so a /Lab space with any other /WhitePoint
(e.g. D65 [0.9505 1 1.089]) rendered off by several levels per channel
and a D65 blue came out 45 levels too red.
Map L*a*b* to XYZ in the space's own white coordinates (ISO 32000-2
§8.6.5.4), Bradford-adapt it to D65 and convert to sRGB directly, as
Poppler does. Missing or degenerate white points fall back to a flat
white and hostile components are clamped without panicking.
Add a render regression that checks the five reported flat patches of
the D65 white-point fixture against their CIE sRGB values.
* Lab: map normalised Indexed palette components onto L*/Range; drop unused LAB.icc
An /Indexed palette over Lab passes its entries as byte/255 (manual_scale),
which the new Lab conversion read as raw L*a*b* and drew near black. Map
them onto L* 0..100 and the a*/b* /Range, with a regression test. Record the
patch as hayro-interpret (25) in vendor/README.md, and remove LAB.icc (no
longer referenced) with its ATTRIBUTION, NOTICE and assets README entries.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* Main gates: rustfmt tests/viewer.rs; a type alias for the refresh-steps array (clippy type_complexity)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
---------
Co-authored-by: Brandon Thomas <echelon@gmail.com>
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
* 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>
* 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>
* 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>
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>
* 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 3aa539b5b6.
* 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>
* 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>
* 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>
* 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>
* 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>
* 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>
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>
* 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>
* 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>
* 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>
* 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>
* 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>
* 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>
* 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>
* M14: complete the Czech (cs) interface catalog
Czech covered the menus and a few later additions (167 keys). The catalog
now has every key the code uses (2,194): Home, All tools, panels, dialogs,
notices, diagnostics and history labels. Translated clean-room from the
meaning of the English labels; the 167 existing entries are unchanged.
- The @plural messages use the three Czech forms (one | few | other);
other counted messages use count-neutral wording ("Vybráno stránek: {n}").
- "calendar date" months are genitive ("9. října 2026"); "Undo {action}"
and "Redo {action}" are whole phrases ("Zpět: {action}").
- Labels the code joins with a leading or trailing space keep it.
- The key-name entries (context "key") are kept as they were.
Tests that relied on the incomplete catalog:
- czech_covers_every_menu_label keeps a short list of strings spelled the
same in Czech (product and format names, Text, Index, Metadata, Model,
templates such as "{0}: {1}"), compares leading and trailing spaces with
the source instead of forbidding them, and requires a catalog without
parse errors.
- date_format.rs expected the English fallback "Same as interface
(Čeština)" under the Czech interface; that label is now translated.
ATTRIBUTION.toml: new SHA-256 of cs.tsv.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* M14: check the Czech catalog like the other complete catalogs
Czech now has the same coverage gates as the complete catalogs:
- czech_covers_commands_and_catalogue: every registered command, menu
title and All tools group, section and item;
- czech_covers_ui_literals: every tl!("…") literal in the UI source;
- czech_plurals_dates_and_history_labels: 1/3/5 pages and 0 fields, the
genitive date ("9. října 2026"), and Undo/Redo/Change history labels
that keep captured names.
cs also joins the lists in missing_ocr_models_messages_are_translated and
bulk_field_properties_labels_are_in_every_full_catalog.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* i18n (cs): terminology review fixes
An independent review of the Czech catalog found 5 entries with a wrong
or misleading meaning and 44 that read unnaturally or inconsistently.
Meaning:
- US formats say so: "Čísla sociálního zabezpečení (USA)" (not the Czech
birth number) and "Telefonní číslo (USA)" for the field format.
- Mouse Exit/Enter triggers: "Odjetí kurzoru" / "Najetí kurzoru".
- Rich text in form fields is not RTF; Form XObjects are not forms.
Wording and consistency:
- Home: pinning folders refers to "úvodní obrazovka" (also in the existing
"Pin folder to Home…"); "úvodní stránka" reads as the first page.
- Callout is "Popisek se šipkou"; Self-signed is "Samopodepsaný" (fixes
the agreement in "Samopodepsaná ID").
- Reset (forms), Apply redactions, guided actions, the radio button and
checkbox styles, line endings (also the existing arrow entries), border
styles, "Omezit na … znaků", "Rozlišovat velikost písmen", grammar in
several messages, and the About window wording shared with the other
Crafting Apps.
History labels for the Creator and Producer document information keys
use the Document Properties wording: "Aplikace" and "Producent PDF".
ATTRIBUTION.toml: new SHA-256 of cs.tsv.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* contributors: add my own people.toml entry
Own entry only, as AGENTS.md describes: credited by the name on my public
GitHub profile (sync_github_name), otherwise by @username.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* i18n (cs): translate "Offset from centre" (watermark offset, #1076)
New upstream string added after the catalog was last synced.
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
* i18n (cs): follow the reworded Combine files strings (#1032)
Upstream generalised the Combine files dialog (images and text files, a
prompt for files that can't be read) and replaced six strings. The old
Czech entries are dropped and the ten new ones translated.
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
* i18n (cs): translate the new Enhance page / camera image strings (#1027)
Four strings added upstream after the catalog was last synced.
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
* i18n (cs): margin labels Top/Bottom read Nahoře/Dole
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* i18n: translate the Digital ID errors, the Signatures panel and key usages
The digital-ID dialog printed its validation errors, the Signatures panel
headings ("Rev. n: Signed by ...", "Unsigned signature field"), the
certificate issuer line of the signed-document bar and the nine key-usage
names in English. Translate them at the point where they are drawn, the way
title() and notify_fmt() already do, so engine and OS error texts that
share those paths pass through unchanged. Czech catalog entries added; other
languages fall back to English.
The signature appearance preview is left alone on purpose: it mirrors the
Helvetica text the engine bakes into the PDF ("Digitally signed by", "Reason:"),
which is document content, not interface.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* i18n: translate form action, print, panel layout, rendering and OCR language texts
Route the remaining plain-English messages through the translation lookup
where they are drawn: the Add-action validation hints, the Cut and stack
print error (preview and notice), the two panel-layout notices, the
"Rendering..." placeholders on unrendered pages, the page number under the
Page Boxes preview (reuses "Page {label}") and the OCR language name.
The messages of the docking validator and the engine and OS errors stay
English: they report internal invariants or text this code does not own.
"My stamps" stays too: it is stored as the category of the user's stamps, so
translating it would split a library when the language changes. The dynamic
stamp chip text mirrors the English text baked into the stamp and is left as is.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* i18n (cs): translate the Appearance preferences and the wheel scroll speed
The strings added upstream with the Appearance section and the mouse wheel
scroll speed setting were missing from the Czech catalog, so
czech_covers_ui_literals failed after the merge.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* i18n: use plural forms for counts built in code
The saved notice after Optimize ("3 images optimized", "2 items discarded"),
the pixel sizes in Export all images, the exact size in bytes (human_size and
the document properties) and the history label of a multi-field delete were
assembled from English words and a trailing "s". Use trn() for them, with
@plural entries in the Czech catalog (1 | 2-4 | 5+), and translate the
"Delete N fields" history label in action_label(). English output is unchanged;
other languages without a plural entry keep the English wording.
The Czech plural test now also checks bytes, optimized images and the
Delete N fields label.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* i18n: translate the text-box and organize toolbar tooltips
"Done adding text" and "Discard this text (Esc)" (the buttons under a new
text box) and the Rotate tooltips of the organize toolbar were passed to
icons::button as plain English. Translate them at the call site, as the
other icon buttons do.
The two text-box tooltips are tl!() literals, so every complete catalog
(ar, bg, cs, de, fr, hu, it, ja, pt-br, ru, uk, zh-hans) has to carry them;
the entries for the languages other than Czech are AI drafts that want a
native speaker's look. The Rotate tooltips already had translations.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
---------
Co-authored-by: Losminek <301412321+Losminek@users.noreply.github.com>
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Co-authored-by: Brandon Thomas <echelon@gmail.com>
* i18n: let the layers below hand their sentences over, so they can be translated
The accessibility findings, the PDF/A problems, the signature details, the
page-range mistakes and the edit labels were English in all ten languages,
because matching their wording from the UI with a tl! literal would break
silently whenever the engine changed it.
So each layer now hands the wording over instead, apart from the document's
own words: a WORDINGS list of every sentence it can say, with holes in it, and
said(&words), which looks the wording up through the closure it is given and
fills the holes in a single pass. in_english changes nothing, for the CLI and
the tests; the UI passes &i18n::words, a catalog lookup. A wording with no
entry comes back as it stands, which is the English, so the nine catalogs this
does not touch read exactly as before.
a11y::report_html takes a language tag and a words closure, so a saved report
reads the way the panel read, down to its lang attribute. print::PrintError
gained a variant per thing a person can be wrong about, with the numbers apart
from the words; faults rather than mistakes keep their own text.
Spanish translates all of it (107 entries). spanish_covers_what_the_engine_says
requires an entry per wording in the eight lists and
spanish_keeps_the_holes_in_what_the_engine_says requires the holes to survive
the translation, so the catalog cannot fall behind the engine unnoticed.
No new tl! literal, so the Chinese, French and Russian UI-literal gates are
unaffected.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
* Sign tests from #1099: compare against the &'static str kinds
* Camera tests: undo returns a Label now
---------
Co-authored-by: Fernando Areán <soporte@codebit.es>
Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
Co-authored-by: Brandon Thomas <echelon@gmail.com>
* Issue #432: global Units preference (mm, cm, in, pt), defaulting to the system region
Preferences ▸ Documents and view ▸ Units sets the unit for page sizes
(Document Properties, Print), Header & Footer margins, the start of Set
Page Boxes and the Measure tool on pages without a viewport scale. On
first start it follows the system region: AppleMeasurementUnits on macOS,
Region ▸ Measurement system (iMeasure) on Windows, then LC_ALL /
LC_MEASUREMENT / LANG; inches for US, LR and MM, millimetres elsewhere.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* Issue #432: macOS falls back to AppleLocale when the measurement system was never set
AppleMeasurementUnits only exists in the global domain once someone changed
Measurement system in System Settings, and apps started from the Finder get no
LANG, so a German Mac with untouched settings landed on inches. The region of
AppleLocale (always present) decides next; AppleMeasurementUnits still wins.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01REz7wTfQgnopyvN9eQJJVu
* Issue #432: Units labels in the Arabic and Italian catalogs new on main
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01REz7wTfQgnopyvN9eQJJVu
* Issue #432: the Print dialog's PDF page size follows the Units preference
Main's new "PDF page: {w} × {h} in" label was always in inches. It now uses
the preferred unit like the sheet size next to it; each catalog keeps its
wording with the size in place of the inch values.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Co-authored-by: Tristan von Kries <tristan@MacBook-Pro-von-Tristan.local>
Co-authored-by: Brandon Thomas <echelon@gmail.com>
A PDF protected with only a permissions (owner) password opens without
asking, restricted, and nothing let the user enter that password
afterwards: the security notice only led to a read-only Security tab and
the hint said to reopen the file with the password, which the app had no
way to do (#995, #1077).
Now the security notice and Document Properties > Security offer Unlock...,
and Change settings / Remove security on a restricted document ask for the
permissions password first and then carry on, as Acrobat does. The open
password is refused there.
- crypt: SecurityHandler::authenticate_owner accepts only the owner
password and refuses a different file key.
- cos: Document::unlock upgrades the open document in place; owner and user
passwords unlock the same file key, so nothing is decoded again.
- engine: Session::unlock (not an edit: no undo step, not marked unsaved)
also unlocks the undo and redo states and reopens saves as owner;
Document::can_unlock, EditError::WrongPassword, command protect.unlock.
- ui: the permissions password prompt shares the open prompt's modal; the
notice and Security tab get Unlock...; the disabled-command hints no
longer point at reopening the file.
- i18n: the new strings in all 15 full catalogs (catalog hashes updated).
Tests cover crypt, cos and engine (all four algorithms, wrong and open
passwords, undo, redo, save, removing security afterwards) and the UI
(notice flow, Remove security asking first).
Closes#995Closes#1077
Co-authored-by: Brandon Thomas <echelon@gmail.com>
* M10: enhance camera images: straighten a page from four corners (ocr.camera-perspective)
Four corners on a page (any order, a convex quadrilateral) are warped to an
upright rectangle that replaces what the page shows, read at the page
picture's own resolution (at most 600 dpi) and kept at that physical size.
One undoable step through Replace Pages, so comments, links and bookmarks
stay.
- geom: a pure 3x3 homography solve and corner ordering (no dependencies);
collinear, concave and non-finite input gives None.
- engine: camera::warp (bilinear, over white, capped at 40 MP) and
Session::enhance_camera_page.
- automation: page_enhance_camera (corners in top-left-origin points).
- parity: ocr.camera-perspective is partial; the UI handles, automatic edge
detection and contrast cleanup are still missing.
Refs #1008.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* M10: Enhance camera image: drag four corners on the page, then Enhance page (ocr.camera-perspective)
Scan & OCR ▸ Enhance camera image (ocr.enhance_camera) shows four corner
handles on the current page, inset 5% from its edges, with what will be cut
away dimmed. Enhance page straightens what they enclose through
Session::enhance_camera_page (one undoable step); Cancel and Esc leave the
page alone. The buttons float at the top right, clear of a centred page's
corners, and the instructions are a notice, as for Crop.
A command of its own, beside Enhance scanned file (ocr.enhance), which
stays for scan correction (#1012).
- ui-egui: camera_ui (handles, dimming, bar), QuickTool::EnhanceCamera,
quick_tool "enhance-camera" for ui.state and the shot example.
- Catalogs: the three new labels in every language with UI coverage
(plus es, pl, zh-hant); ATTRIBUTION.toml hashes updated.
- automation: page_enhance_camera is linked to the command.
- parity: ocr.camera-perspective lists the command and the UI test.
Refs #1008.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* M10: Enhance camera image finds the document's edges (ocr.camera-perspective)
The corners now start on the photographed document's edges when they can
be found, else inset 5% as before.
camera_edges::detect_document, pure Rust and deterministic: shrink to 400
px, blur, Sobel edges thinned and thresholded against the 98th percentile;
a Hough transform where each edge votes within 6 degrees of its own
normal; pairs of near-horizontal and near-vertical lines make convex
quadrilaterals (at least 12% of the picture), kept when every side lies on
a matching edge along at least 55% of its length and scored by area times
the square of the mean support; each side is then refitted through its
edge points (total least squares). None when nothing convinces.
- Session::detect_camera_corners (user space); page_enhance_camera finds
the corners itself when `corners` is omitted and reports the corners it
used and whether they were detected; the UI command starts from them.
- Tests on synthetic photos: plain table, text on the sheet, wood grain
with a shadow, strong perspective, a 30 degree turn, a sheet running off
the picture, no document, white on white, bad input; end to end through
the engine and the tool.
- Parity notes: what is still missing (turning the result upright, rounded
corners, clean-up).
Refs #1008.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* M10: after Enhance page, rotate, crop or organize the page (ocr.camera-perspective)
Enhance camera image no longer closes on Enhance page: the bar then offers
rotate counterclockwise and clockwise for the enhanced page (one undo step
each), Crop pages and Organize pages (which leave the tool for those
tools), and Done; Esc leaves too. A photo of a card often comes out a
quarter turn off until the page is turned upright automatically.
The two icon buttons are named for screen readers. Every label already
exists in the catalogs.
Refs #1008.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* M10: Enhance camera image cleans the photo up and chooses its look (#1034)
After Enhance page the bar offers a look (Original, Colour, Greyscale,
Black and white) and a Clean-up slider (0-100, default Colour at 50).
Choosing a look or letting go of the slider enhances the page again from
the straightened photo and turns it as before, so the history keeps one
enhancement (and one turn), never a pile of them.
camera_clean, pure Rust:
- clean: the paper's illumination is estimated per channel on a coarse
grid (each block's 90th percentile, lifted to the 75th percentile of the
5x5 blocks around it so a stamp or picture isn't taken for paper, then
smoothed) and divided out, so casts, shadows and darker edges go and the
paper turns white; a contrast stretch grows with the level, and from 60
isolated dark pixels are removed. Colour ink stays.
- apply(look): Original is the photo as taken; Greyscale is the cleaned
page in grey; Black and white evens the lighting fully, thresholds the
whole page (the level sets the threshold: higher keeps fainter strokes)
and despeckles. Black and white is stored as PNG (lossless): JPEG would
smear grey around every letter.
- Session::enhance_camera_page_at(look, level); page_enhance_camera takes
`mode` (original, colour, greyscale, bw) and `level` (0-100), refusing
anything else.
- "Clean-up", "Original", "Greyscale" and "Black and white" in every
catalog with UI coverage, plus es, pl and zh-hant.
- Tests: shadow and yellow cast become white paper with dark text, a blue
stamp stays blue, strength grows with the level, specks go but text
stays, the four looks (black and white is only black and white, the
threshold moves with the level), bad sizes; a shadowed sheet through the
engine; level and mode through the tool; the slider and the looks
re-enhancing from the photo and keeping the turn.
- Parity: ocr.background-removal, ocr.edge-shadow-removal and
ocr.despeckle are partial (camera images only).
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* M10: Enhance camera image cleans the photo up and chooses its look (#1034)
After Enhance page the bar offers a look (Original, Colour, Greyscale,
Black and white) and a Clean-up slider (0-100, default Colour at 50).
Choosing a look or letting go of the slider enhances the page again from
the straightened photo and turns it as before, so the history keeps one
enhancement (and one turn), never a pile of them.
camera_clean, pure Rust:
- clean: the paper's illumination is estimated per channel on a coarse
grid (each block's 90th percentile, lifted to the 75th percentile of the
5x5 blocks around it so a stamp or picture isn't taken for paper, then
smoothed) and divided out, so casts, shadows and darker edges go and the
paper turns white; a contrast stretch grows with the level, and from 60
isolated dark pixels are removed. Colour ink stays.
- apply(look): Original is the photo as taken; Greyscale is the cleaned
page in grey; Black and white evens the lighting fully, thresholds the
whole page (the level sets the threshold: higher keeps fainter strokes)
and despeckles. Black and white is stored as PNG (lossless): JPEG would
smear grey around every letter.
- Session::enhance_camera_page_at(look, level); page_enhance_camera takes
`mode` (original, colour, greyscale, bw) and `level` (0-100), refusing
anything else.
- "Clean-up", "Original", "Greyscale" and "Black and white" in every
catalog with UI coverage, plus es, pl and zh-hant.
- Tests: shadow and yellow cast become white paper with dark text, a blue
stamp stays blue, strength grows with the level, specks go but text
stays, the four looks (black and white is only black and white, the
threshold moves with the level), bad sizes; a shadowed sheet through the
engine; level and mode through the tool; the slider and the looks
re-enhancing from the photo and keeping the turn.
- Parity: ocr.background-removal, ocr.edge-shadow-removal and
ocr.despeckle are partial (camera images only).
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* Camera level: when another edit broke the run, redo what was undone instead of warping the page again
---------
Co-authored-by: magdale76 <16279778+magdale76@users.noreply.github.com>
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Co-authored-by: Brandon Thomas <echelon@gmail.com>
* M10.5: Restore architectural paper presets and PDF scale controls
Port the missing paper catalog and scale controls from PR #599 onto the merged Windows printing implementation. Keep the spooler and background discovery from #847/#1007. Document the feature-by-feature audit and verification limits.
Assisted-by: OpenAI Codex
* Drop the provenance write-up and the screenshot example (the kittest covers the dialog)
* Refresh catalog hashes
---------
Co-authored-by: Brandon Thomas <echelon@gmail.com>
* Engine: reload a document from new bytes, keeping its id
* Issue #431: reload a document when its file changes on disk
---------
Co-authored-by: Brandon Thomas <echelon@gmail.com>
* M4: document the directly accessible page overview
Part of the organize-page-overview change.
Co-authored-by: Codex <noreply@openai.com>
* M4: open Organize in the page grid with selection and per-page actions
Show the overview from the tool catalogue, Home, palette and control channel. Add checkbox selection and local rotate/delete actions; preserve thumbnail aspect ratios.
Co-authored-by: Codex <noreply@openai.com>
* M4: translate page selection and local actions
Co-authored-by: Codex <noreply@openai.com>
* M4: cover overview entry, selection and page actions
Co-authored-by: Codex <noreply@openai.com>
* M4: record the organizer workflow improvement
Co-authored-by: Codex <noreply@openai.com>
* M4: link page overview regression evidence
Co-authored-by: Codex <noreply@openai.com>
---------
Co-authored-by: Codex <noreply@openai.com>
Co-authored-by: Brandon Thomas <echelon@gmail.com>
* Issue #568: honour the document's copy permission
ISO 32000-2 §7.6.4.1 has readers restrict access to an encrypted file
according to its permissions, and Table 22 bit 5 covers "Copy or otherwise
extract text and graphics". PdfCraft reported the bit (Document Properties
shows "Content copying: Not allowed") but ⌘C copied selected text anyway.
- Engine: Document::allows_copying / check_copying, and Needs::Copying for
Take a snapshot, Export to image/text/Word/HTML/RTF and Export all images.
Save Image As refuses too.
- UI: ⌘C moves from canvas::shortcuts to the app, which knows the document.
Refused copies, snapshots and exports say why. The snapshot tool and the
exports check again when they act, since the tool stays selected across tabs.
- Automation: text_extract (and the MCP text resources), text_paragraphs,
doc_export_text/images/all_images, doc_export_office and image_save refuse.
Find, read aloud and the accessibility tools keep working: the spec exempts
assistive technology, and searching isn't copying. Page extraction stays with
the assemble bit, as before. Owners and unencrypted files are unaffected.
* Refresh catalog hashes
---------
Co-authored-by: Brandon Thomas <echelon@gmail.com>
* M9: visible digital signatures can show a drawn, typed or image graphic (#193)
A visible certificate signature can show a graphic where the signer's
name is large, as Acrobat's Configure Signature Appearance does: the
saved Fill & Sign signature (drawn, typed or image) or an imported
PNG/JPEG. Sign as previews it and remembers the choice; sign_document
takes `graphic` (image, text or strokes).
pdfcraft-sign gains `Graphic` and `SignOptions::graphic`. Graphics are
checked and size-capped before drawing; images keep alpha as a soft
mask and are encrypted with the rest of the update in encrypted files.
Refs #193
* Refresh catalog hashes
---------
Co-authored-by: Brandon Thomas <echelon@gmail.com>
Dancing Script, the typed-signature face, is Latin and Vietnamese only, so
script_outline skipped every Cyrillic or Greek character and a Serbian
Cyrillic name came out empty. Characters the script lacks are now drawn from
the bundled Inter Regular, slanted about 12 degrees and scaled to the
script's capital height. Characters neither face has are still skipped.
Inter's bytes are now exported as pdfcraft_fonts::INTER_REGULAR and the UI
uses that static, so the font is still embedded once.
Co-authored-by: DWAA1660 <DWAA1660@users.noreply.github.com>
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Co-authored-by: Brandon Thomas <echelon@gmail.com>
* UI: theme picker with light and dark theme cards
* UI: update the icon count and the theme test for the cycling theme button
---------
Co-authored-by: Brandon Thomas <echelon@gmail.com>
egui scrolls 40 points per wheel line on the desktop. winit reports one line
per notch on Windows and Linux, where browsers scroll about 100 pixels, so
the wheel felt slow there. Preferences ▸ Documents and view gets "Mouse wheel
scroll speed": Slow / Normal / Fast (0.5×, 1×, 2× of the platform's normal:
100 pt per line on Windows and Linux, 40 on macOS and 8 on the web, both
egui's defaults). It is saved, and `set_option("scroll_speed", …)` sets it
over the control channel.
- Applied in `raw_input_hook`, before egui reads the frame's input (and in
`logic` for hosts without the hook).
- ⌘/Ctrl-wheel zoom keeps egui's rate: those line deltas are scaled back,
since egui scales lines before turning them into zoom.
- Point deltas (most trackpads) keep their speed; Single page wheel paging
still turns one page per notch (its notch size is fixed).
- Labels translated (fr, ru, zh-hans, zh-hant, es, ja, bg, de), the speed names in
a "scroll speed" context; catalog hashes updated.
Claude-Session: https://claude.ai/code/session_01VNJqS2dEQawHJn1qTRJiKp
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Co-authored-by: Brandon Thomas <echelon@gmail.com>
* M10: enhance camera images: straighten a page from four corners (ocr.camera-perspective)
Four corners on a page (any order, a convex quadrilateral) are warped to an
upright rectangle that replaces what the page shows, read at the page
picture's own resolution (at most 600 dpi) and kept at that physical size.
One undoable step through Replace Pages, so comments, links and bookmarks
stay.
- geom: a pure 3x3 homography solve and corner ordering (no dependencies);
collinear, concave and non-finite input gives None.
- engine: camera::warp (bilinear, over white, capped at 40 MP) and
Session::enhance_camera_page.
- automation: page_enhance_camera (corners in top-left-origin points).
- parity: ocr.camera-perspective is partial; the UI handles, automatic edge
detection and contrast cleanup are still missing.
Refs #1008.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* M10: Enhance camera image: drag four corners on the page, then Enhance page (ocr.camera-perspective)
Scan & OCR ▸ Enhance camera image (ocr.enhance_camera) shows four corner
handles on the current page, inset 5% from its edges, with what will be cut
away dimmed. Enhance page straightens what they enclose through
Session::enhance_camera_page (one undoable step); Cancel and Esc leave the
page alone. The buttons float at the top right, clear of a centred page's
corners, and the instructions are a notice, as for Crop.
A command of its own, beside Enhance scanned file (ocr.enhance), which
stays for scan correction (#1012).
- ui-egui: camera_ui (handles, dimming, bar), QuickTool::EnhanceCamera,
quick_tool "enhance-camera" for ui.state and the shot example.
- Catalogs: the three new labels in every language with UI coverage
(plus es, pl, zh-hant); ATTRIBUTION.toml hashes updated.
- automation: page_enhance_camera is linked to the command.
- parity: ocr.camera-perspective lists the command and the UI test.
Refs #1008.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* M10: Enhance camera image finds the document's edges (ocr.camera-perspective)
The corners now start on the photographed document's edges when they can
be found, else inset 5% as before.
camera_edges::detect_document, pure Rust and deterministic: shrink to 400
px, blur, Sobel edges thinned and thresholded against the 98th percentile;
a Hough transform where each edge votes within 6 degrees of its own
normal; pairs of near-horizontal and near-vertical lines make convex
quadrilaterals (at least 12% of the picture), kept when every side lies on
a matching edge along at least 55% of its length and scored by area times
the square of the mean support; each side is then refitted through its
edge points (total least squares). None when nothing convinces.
- Session::detect_camera_corners (user space); page_enhance_camera finds
the corners itself when `corners` is omitted and reports the corners it
used and whether they were detected; the UI command starts from them.
- Tests on synthetic photos: plain table, text on the sheet, wood grain
with a shadow, strong perspective, a 30 degree turn, a sheet running off
the picture, no document, white on white, bad input; end to end through
the engine and the tool.
- Parity notes: what is still missing (turning the result upright, rounded
corners, clean-up).
Refs #1008.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* M10: after Enhance page, rotate, crop or organize the page (ocr.camera-perspective)
Enhance camera image no longer closes on Enhance page: the bar then offers
rotate counterclockwise and clockwise for the enhanced page (one undo step
each), Crop pages and Organize pages (which leave the tool for those
tools), and Done; Esc leaves too. A photo of a card often comes out a
quarter turn off until the page is turned upright automatically.
The two icon buttons are named for screen readers. Every label already
exists in the catalogs.
Refs #1008.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
---------
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>
The Save button's tooltip and accessibility name read "Save (M4)", a
leftover milestone tag, while Print and Document properties beside it
name their shortcuts. It now uses command_tip with file.save, so it reads
"Save (Ctrl+S)" on Windows/Linux and "Save (⌘S)" on macOS. The catalog
key becomes "Save ({key})" in the 15 catalogs that had it (ATTRIBUTION.toml
hashes updated). New test commands::the_top_bar_save_button_names_its_shortcut_not_a_milestone.
Claude-Session: https://claude.ai/code/session_01UngnNXzwMCF7ksQ8tNucDe
Co-authored-by: DWAA1660 <DWAA1660@users.noreply.github.com>
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Co-authored-by: Brandon Thomas <echelon@gmail.com>
* M10: refuse a PNG whose header claims more than 2 GiB of pixels before allocating
The png crate's limits don't cover the caller's frame buffer, which png_image sized from the
header alone: a few-KB PNG claiming 60000x60000 RGBA made Create allocate and fill 14.4 GB.
Apply the 2 GiB ceiling TIFF pages already have. Reached from Create, Insert pages and (with
#569) Combine files.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* M4, #569: Combine files takes images and text files
PNG, JPEG, TIFF, GIF, BMP, JPEG 2000 and .txt files become PDFs as they join the Combine files
list (Add files…, drops, folder scans), so each is an ordinary row or card with its pages,
thumbnails and page range. Session::as_pdf does the conversion; PDFs pass through unread
(Combine reads them itself, with their passwords), as does a file named .pdf without a header,
which the reader may still open. doc_combine accepts the same file types, and a converted
file's bookmark drops its extension.
A file that can't be read (a damaged image or PDF, a type that can't be converted) no longer
is refused at the door: it joins the list with why in its Warnings (a red card in the grid).
Combine then asks "Leave out files that can't be read?", naming them; Combine the rest goes on
without them, Cancel changes nothing. Password-protected files still block Combine until
unlocked.
The picker offers "PDF, images and text", and the empty state, folder tooltips and folder
notices say what is accepted; translated in every full catalog (catalog hashes updated).
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>
Add Watermark / Update Watermark can now move the mark from the page
centre by a horizontal and a vertical offset in points (right and up).
- edit: add_watermark refuses a non-finite offset instead of writing it
into the content stream; tests for the placement and the refusal.
- automation: doc_watermark takes offset_x and offset_y (clamped to
+-20,000 points); a test saves, reopens and measures the text moving.
- ui-egui: an "Offset from centre" row with Horizontal and Vertical
fields; the preview follows it for text and for picture watermarks.
- i18n: "Offset from centre" in the catalogs that have a coverage test
(ar, bg, de, fr, hu, it, ja, pt-br, ru, uk, zh-hans); hashes updated in
ATTRIBUTION.toml.
- parity and roadmap: edit.watermark-appearance stays partial (no offset
from the page edge, no print/screen-only variants, no repeat); image
and PDF watermarks no longer listed as missing; progress log line.
Co-authored-by: Brandon Thomas <echelon@gmail.com>
* M10: Windows printing prints at actual size, off the UI thread (follow-up to #847)
- Sheets print at 100% on the driver's paper closest to their size, landscape
when wide, instead of being shrunk into .NET's default 1-inch margins
(about 76% on Letter) and never rotated. Each sheet is capped at ~64 MP so a
poster-sized sheet can't exhaust memory.
- PowerShell runs with CREATE_NO_WINDOW: the release app has no console, so
Windows opened one over the app for the whole job.
- The Print dialog lists printers in the background (the desktop app's
printer_source) and spools jobs on a worker thread, so Ctrl+P and long jobs
no longer freeze the UI. A list arriving late preselects the default only if
the user hasn't picked a printer.
- Parity evidence named a test that #847 renamed; xtask parity passes again.
The raster-only limit is now stated in the parity notes and README.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* M10: refresh catalog hashes in ATTRIBUTION.toml for the new print strings
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Preserve source glyphs and artwork, resolve the group atomically, and expose generation-scoped automation tools with selection and rendering regressions.
Co-authored-by: Tevinhead <173904641+Tevinhead@users.noreply.github.com>
Co-authored-by: Codex <noreply@openai.com>
Co-authored-by: Brandon Thomas <echelon@gmail.com>