Commit Graph
1451 Commits
Author SHA1 Message Date
alexanderalber 5b220eb886 M6: detect form fields on scanned pages (#1721) (#1722)
Field detection read only vector data: text-layer words and drawn
rectangles and lines. A scanned form is a single picture, so it found
nothing there.

A page that an image covers at least half of, and whose content draws
no lines, is now rendered at 144 dpi and its shapes are read from the
pixels (pdfcraft_forms::detect::raster): ink against a local mean,
long thin runs as lines, enclosed rectangular patches of paper as
boxes, the empty band of a box with a small printed label as its
writing area, and the open-ended horizontal lines as rules. Labels come
from the text layer, else from OCR when the models are installed;
without either the fields get default names. Detection works in the
displayed frame, so a scan stored sideways reads like any other, and
fields on a turned page get /MK /R so typing reads upright.

generated with Claude Code
2026-10-15 02:21:42 -04:00
659c8fcbd4 macOS: the standard About panel shows the website and Discord links (#1287) (#1718)
PdfCraft > About PdfCraft is AppKit's standard panel (winit's default menu),
which takes Credits.rtf from Contents/Resources. Ship one with the lines the
in-app Help > About has: the one-sentence description, the licence, and links
to getartcraft.com and the Discord.

Co-authored-by: Tristan von Kries <tristan@MacBook-Pro-von-Tristan.local>
Co-authored-by: Claude Fable 5.1 <noreply@anthropic.com>
2026-10-15 02:21:32 -04:00
4e06a01631 Edit text: the four-way arrow wherever a drag moves, and a sharp page while editing (#1717)
Moving a paragraph or image showed the text cursor over a closed paragraph
and a closed hand while dragging, so the drag by its box (and, since #1600,
by the frame of the open one) went unnoticed. Now the four-way arrow is
offered over a closed paragraph, on the frame band of the open one, over a
selected object group and throughout a move; edges keep the rewrap cursor and
the open paragraph's text keeps the text cursor.

While a paragraph is open, the page around the editor is drawn from one
raster capped at the 2048-pixel backdrop side, which went soft past about
1.2x on a Retina display. It is now asked for at the tiling threshold (4096),
sharp to about 2.5x on Letter or A4; a GPU with a smaller texture limit still
caps it.

Co-authored-by: Tristan von Kries <tristan@MacBook-Pro-von-Tristan.local>
Co-authored-by: Claude Fable 5.1 <noreply@anthropic.com>
2026-10-15 02:21:21 -04:00
db5b9b4364 M14: add Vietnamese interface and date localization (#1712)
* G14: Vietnamese catalog — menus, file and shell chrome

Add the Vietnamese (vi) TSV catalog header and translations for menus,
File/Edit/Help chrome, clipboard and common shell labels. PhotoCraft and
VectorCraft catalogs used only as translation memory when English source
and context matched. ATTRIBUTION records the asset hash.

Co-authored-by: Cursor <cursoragent@cursor.com>

* G14: Vietnamese catalog — forms, signatures and pages

Translate form/Fill & Sign/certificate wording and page organize/rotate/
extract/split labels. Keep product and format names unchanged.

Co-authored-by: Cursor <cursoragent@cursor.com>

* G14: Vietnamese catalog — comments, view, a11y and contexts

Translate comments/markup, view/zoom/preferences tools, accessibility and
preflight sentences, plus contextual rows (certificate, calendar, keys,
scroll speed, signature pad, action wizard, @plural one-form).

Co-authored-by: Cursor <cursoragent@cursor.com>

* G14: Vietnamese catalog — remaining dialogs and notices

Complete the Vietnamese catalog to every (context, English) key in the
bundled union (2,580 entries). No unapproved English UI fallbacks remain
outside the documented VI_KEEP_AS_IS list.

Co-authored-by: Cursor <cursoragent@cursor.com>

* G14: restore complete Vietnamese catalog key order

Replace the domain-appended catalog with the verified 2,580-entry union
in stable key order so coverage matches every bundled reference key.

Co-authored-by: Cursor <cursoragent@cursor.com>

* G14: register Vietnamese interface and date language

Add `vi` / Tiếng Việt to LANGUAGES and DATE_LANGUAGES with plural_none
and rtl false. Locale tags vi, vi-VN and vi_VN.UTF-8 resolve. Calendar
uses Tháng Một / tháng Một; Fill & Sign refuses non-WinAnsi month names.
Coverage tests require full command, literal, key and credits coverage.

Co-authored-by: Cursor <cursoragent@cursor.com>

* G14: document Vietnamese localization

Record Vietnamese at 100% catalog coverage in localization-parity and
ROADMAP, list Tiếng Việt in Preferences/docs, and note the G14 catalog
in the progress log. Engine/OS details, document contents, filenames,
CLI/MCP and external help remain out of scope by design.

Co-authored-by: Cursor <cursoragent@cursor.com>

* G14: use ASCII > in Vietnamese preference paths

Inter lacks U+25B8 (▸). Match other Latin catalogs and write preference
paths as "Tùy chọn > …" so the glyph coverage test stays green.

Co-authored-by: Cursor <cursoragent@cursor.com>

* G14: finalize Vietnamese terminology and current coverage

Disambiguate theme Appearance (Giao diện) from object look (Diện mạo),
normalize form Field to Trường, make redaction apply wording permanent,
use numeric calendar months, close the GoToR/digital-ID key gap, and
pin regression assertions. Refresh the vi.tsv attribution hash.

Co-authored-by: Cursor <cursoragent@cursor.com>

* G14: align localization docs with Vietnamese coverage

Recompute the twelve-language mean (≈59% → ≈68%), document 23 interface
languages, and remove stale claims that Vietnamese is missing.

Co-authored-by: Cursor <cursoragent@cursor.com>

* G14: cover Wayland title-bar strings in Vietnamese

Translate Minimize/Maximize/Restore and the system title-bar preference
added on upstream main, and refresh the catalog attribution hash and counts.

Co-authored-by: Cursor <cursoragent@cursor.com>

* G14: correct current union key count in localization docs

Co-authored-by: Cursor <cursoragent@cursor.com>

* G14: prepare Vietnamese localization for review

Trim the vi.tsv header to maintainer-oriented notes, record native-speaker
community review in attribution and localization docs, and refresh the
weighted readiness arithmetic (localization ≈68% → ≈50%).

Co-authored-by: Cursor <cursoragent@cursor.com>

---------

Co-authored-by: DUY Tran Nhat (SAE-KIDS) <duytn@sanancorp.com>
Co-authored-by: Cursor <cursoragent@cursor.com>
2026-10-15 02:21:11 -04:00
alexanderalber 00957b9f21 M5: Fill & Sign text reads across on rotated pages (#1710)
* M5: Fill & Sign text reads across on rotated pages

Typed text, dates and text boxes ignored the page's /Rotate, so on a
page stored sideways (common for scanned forms) the text came out
sideways. The box now follows the displayed axes, the annotation
records the turn in /PCTextRotate and its appearance is turned back
with a form /Matrix, as stamps and image signatures already do.
Callouts keep user-space axes. The headless fill_sign_add tool gets
the same layout.

generated with Claude Code

* chore: credit alexanderalber in the About window

generated with Claude Code
2026-10-15 02:21:01 -04:00
manchitmrandClaude Opus 5.5 089e13c074 Sinhala and Tamil interface: draw with an installed font (part of #1688) (#1706)
Sinhala and Tamil file names showed as boxes on the home screen, in Recent
and in document tabs: no embedded face covers either script, and the
installed-font fallback did not look for one.

Add sinhala_fallback() and tamil_fallback(), each probing for its script's
letter ka, and end every interface family with them:
- macOS: Sinhala Sangam MN / Sinhala MN, Tamil Sangam MN / Tamil MN
- Windows: Nirmala UI, then Iskoola Pota (Sinhala) or Latha (Tamil)
- Linux: Noto Sans Sinhala / Noto Sans Tamil, Lohit Tamil

The faces are read at runtime, never embedded or shipped, and
PDFCRAFT_SYSTEM_FONTS=0 still turns them off.

Co-authored-by: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-10-15 02:20:51 -04:00
null 00000111 b558ef7447 i18n(pt-br): fix 6 Brazilian Portuguese strings (#1694)
* i18n(pt-br): fix 5 Brazilian Portuguese strings

A subject-verb agreement slip, a hyphenated colour, and three terms
that disagreed with the catalog's own wording.

* i18n(pt-br): call the comb option "Caixas", as its warning does

Keeps the warning's "As caixas de caracteres" and renames the option
from the literal "Pente" to match it.
2026-10-15 02:20:41 -04:00
9c875656de fix(render, ui): form-heavy documents draw and fill fast (spot-colour images, widget placement, Fields panel) (#1691)
* fix(render): convert spot-colour images once per pixel value (vendored hayro-interpret patch 28)

An image whose pixels hold at most 16 bits of samples (an 8-bit Separation or
DeviceN image, a 1-, 2- or 4-bit gray or indexed one) is now converted to RGB
through a table of every possible pixel value instead of pixel by pixel.

A Separation or DeviceN tint transform is a PDF function, often a PostScript
calculator program, and the image decoder ran it once per pixel. Fillable
character sheets draw their background art as an 8-bit DeviceN image: one with
a 3304 x 4105 background took 3.8 s and 430 MB to draw each page (Poppler: 0.27 s),
when opening it and again after every field was filled. It now draws in 0.11 s
and 100 MB, pixel for pixel the same.

The table decodes each sample through the same `decode_sample` as the per-pixel
path, keeps `fix_image_length`'s rules for short and overlong data (the pixels
past the data are converted as before), and is used only when the image has
more pixels than the table has entries.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01KnNgjPEDfKJM1wj17Q8Za8

* fix(render): place widgets without /P in one pass over the pages

Opening a document, the inspector finds the page of a form field whose widget
has no /P by searching every page's /Annots for it, once per field: O(fields x
annotations). A form of 8,000 such fields took 1.17 s to open instead of
0.27 s. The pages' /Annots are now read once, on the first such field, into a
map from annotation to page.

A widget that several pages list is now placed on the first of them, as
pdfcraft-forms places it; the search took whichever page a HashMap visited
first.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01KnNgjPEDfKJM1wj17Q8Za8

* fix(ui): the Fields panel keeps up with large forms

Every frame the Fields panel searched the form for each row by name, once to
sort the rows into tab order and again to find what a click selects: O(fields²)
a frame. It also laid out, drew and built the tooltip of every row, scrolled
into view or not. With 8,000 fields the app used 13.4 s of CPU for 20 pointer
moves with the panel open (0.2 s with it closed).

The rows are now worked out by `field_rows`, which pairs each row with the
first form field of its name through one map a frame, as the engine pairs
them, and a row out of view is allocated and keeps its clicks, keyboard focus
and accessibility label but isn't drawn and builds no tooltip. The same 20
moves now take 0.86 s.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01KnNgjPEDfKJM1wj17Q8Za8

* docs: roadmap entries for faster form-heavy documents

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01KnNgjPEDfKJM1wj17Q8Za8

* fix(render): convert each lookup-table entry on its own (vendored hayro-interpret patch 28)

Patch 28 converted the whole table of possible pixel values in one batch.
Separation and DeviceN `convert_f32` concatenate each tint result, and a Type 4
program that returns the wrong number of components is only warned about, so a
malformed result for a value the image never uses shifted every later entry. A
32 x 32 image whose only sample (255) is red drew black because tint 0 returned
one component (PR #1691 review).

`convert_each` now converts the table one entry at a time, so a malformed entry
changes only itself. The largest table (65,536 entries, a two-ink DeviceN image
over DeviceCMYK) takes about 2 ms longer.

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

---------

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Co-authored-by: Brandon Thomas <echelon@gmail.com>
2026-10-15 02:20:31 -04:00
Marco ArnulfoandBrandon Thomas f48b76c146 Text selection: a drag after a double- or triple-click extends by whole words or lines (fixes #1685) (#1689)
Co-authored-by: Brandon Thomas <echelon@gmail.com>
2026-10-15 02:20:09 -04:00
Marco Arnulfo 44fbacfffd Edit text & images: a dragged image's pixels follow its frame instead of waiting for the release (part of #1671) (#1677) 2026-10-15 02:19:45 -04:00
Marco Arnulfo 740117620d Text selection: dragging to the top or bottom edge of the view scrolls it (part of #1670) (#1675) 2026-10-15 02:19:34 -04:00
FredApps 760cde5ade M3: keep the full final spread reachable with unequal page heights (#1672)
Related #187: this corrects the reproduced unreachable tall-left content, not the reported backward-page flash.

AI-assisted by Codex (GPT-6).
2026-10-15 02:19:24 -04:00
9bbd765ea4 Keep page labels and focused navigation consistent across reloads (#1667)
* M3: await remote-link refusal in the UI regression

Wait for the asynchronous notification before checking the existing no-tab and refusal-message assertions.

Co-authored-by: Codex <codex@openai.com>

* M3: keep page label input consistent across reloads

Co-authored-by: Codex <codex@openai.com>

---------

Co-authored-by: sanyamsingh001 <314051937+sanyamsingh001@users.noreply.github.com>
Co-authored-by: Codex <codex@openai.com>
Co-authored-by: Brandon Thomas <echelon@gmail.com>
2026-10-15 02:19:13 -04:00
FredApps 2fee7af241 Clamp extracted Indexed image samples to the declared palette range (#1581)
* M10: clamp extracted Indexed image samples to hival

Validate Indexed hival as an integer in 0..=255 and clamp raw palette samples before lookup. Preserve existing Decode, mask, and short-palette behavior.

Refs #1201 (item 9 only).

AI-generated by Codex (GPT-6).

* ci: re-run (unrelated flaky test)

* test: make reduce-size no-op regression deterministic
2026-10-15 02:18:44 -04:00
tylerstoltz 5105eb732c feat(ui): drop files on the Pages panel to insert pages; track the pointer during drags on Windows (#1547)
Files dragged from outside onto the Pages panel are inserted at the gap under the pointer (with a drop indicator) instead of opening as new documents. Files dropped beside the panel still open as before.

On Windows, winit sends no pointer moves while files are dragged over the window, so the cursor is read with GetPhysicalCursorPos (winsafe "user" feature). This also makes the page grid show and use the gap under the pointer during a drag on Windows. Linux/BSD still have no pointer during a drag and keep the existing fallback.

Tests: two new kittest cases in tests/editing.rs (drop on the panel inserts at the gap / at the end; drop beside the panel opens a document). The Windows cursor path was checked by hand.
2026-10-15 02:18:34 -04:00
cyfung1031andClaude Haiku 5.5 e770c564a8 COS: resolve xref entries on demand (#1505)
Co-authored-by: Claude Haiku 5.5 <noreply@anthropic.com>
2026-10-15 02:18:23 -04:00
Earl Co 0b732e90ff M12: mark leaf tags as artifacts (#1475)
* M12: mark leaf tags as artifacts

* chore: refresh translation asset hashes

* fix(a11y): retain text autotag exports after artifact merge
2026-10-15 02:18:13 -04:00
3d7336fc41 M5: add comment and reply submission shortcuts (#1457)
Submit highlight comment text and replies with Ctrl+Enter, retain Command+Enter
for floating composers, and let Shift+Enter insert reply newlines. Keep ordinary
Enter-to-post for replies and restrict submission to the focused editor.

Add UI regressions and parity evidence. Normalize existing lint and formatting
issues and pin the reload test's English labels so the required checks pass on
this Windows machine.

Co-authored-by: Armin Reisgys <Armin.Reisgys@de.bosch.com>
Co-authored-by: OpenAI Codex <codex@openai.com>
Co-authored-by: Luchsen <210128845+Luchsen@users.noreply.github.com>
Co-authored-by: Brandon Thomas <echelon@gmail.com>
2026-10-15 02:18:03 -04:00
Márk Bartalos b89cbd0044 M4: keep the last saved revision across undo (#1310, #1311, #1312) (#1435)
* M4: keep the last saved revision across undo (#1310, #1311, #1312)

Undo treated a return to the opened file as unsaved work, and after a save it followed the restored snapshot's older base. Revert and the next save now stay on the last saved file, so that revision remains in the PDF.

* Run a tool after a background open of the file it asked for

A tool chosen with no document opened the picked file on the worker, but only ran when that open finished inside the picker callback. Remember the command on the pending open, and let the command test wait for the worker.

* Wait for background opens in the command tests

Opening a picked file or a recent file finishes on the worker, so a fixed handful of frames can assert before the tab exists.

* Compare batch copies to the PDF that was written

A new text PDF stamps the current second, so a later pdf() call is a different file. The numbered copy and the source must match the bytes that were saved.

* Drop the unused batch fixture left by the merge

Main already keeps the written PDF in original, so the older source binding is unread and fails -D warnings.
2026-10-15 02:17:06 -04:00
Sir Clangington III.andLabDennis 8a04b6d5c2 M8: Bound and reuse repeated redaction overlays (#1426)
Consolidate the reviewed change and its follow-ups on current upstream.

Original change notes:
M8: Bound and reuse repeated redaction overlays

Prepare appearances within a shared 8 MiB budget before changing the document. Reuse one measured and encoded row, reject excessive geometry, and preserve document and undo state on refusal.

Fixes #1326

M8: Preserve upstream font metrics in bounded redaction overlays

M8: reuse ordered redaction mark groups (#1326)

Co-authored-by: LabDennis <341377186+LabDennis@users.noreply.github.com>
2026-10-15 02:16:56 -04:00
Earl Co fc601b89e1 Tests: use a 300 DPI scan for OCR searchability (#1197) 2026-10-15 02:16:45 -04:00
Jules Kreuer 039706c437 Clear image leftovers (#1151)
* M8: Remove image redaction leftovers

* M8: Move roadmap note
2026-10-15 02:16:35 -04:00
Alexis e75082c9d6 Printing: a layer that is off in the document stays off on the sheets (#1135)
The print-ready PDF that `pdfcraft_print::impose` builds starts from a new
catalog with only /Type and /Pages, so the document's /OCProperties was not
carried over. The pages' marked content still refers to its optional-content
groups, but nothing said a group was off, so a layer that is switched off in
the document (and not drawn on screen) was printed anyway.

Keep /OCProperties in the new catalog, so the sheets hide the same layers the
document hides. Test: a one-page document whose red square is in a layer; with
the layer off by default it is not drawn on screen and, now, not on the
printed sheet; with the layer on it is drawn in both.

Printing still follows the layers' default state only; applying the Print
event of /AS (a layer shown only on screen or only in print) is not done here.
2026-10-15 02:16:23 -04:00
dbef872ed7 M2: preserve precision when writing page transforms (#946)
Co-authored-by: sanyamsingh001 <314051937+sanyamsingh001@users.noreply.github.com>
Co-authored-by: Codex <codex@openai.com>
2026-10-15 02:16:13 -04:00
d3a91c5c5f M2: use fixed-size pixel chunks in oracle decoder (#2342)
Keep RGBA decoding unchanged while satisfying the current Clippy lint.
Include the separately verified Rustls 0.23.46 lock prerequisite.

Co-authored-by: sanyamsingh001 <314051937+sanyamsingh001@users.noreply.github.com>
Co-authored-by: Codex <codex@openai.com>
2026-10-15 05:44:02 +02:00
Tanish MankuandClaude Opus 5 ac89a11391 fix(xtask): the text oracle scored us zero for honouring a no-copy document
A PDF can forbid copying its text. `pdfcraft-cli text` honours that and refuses, naming the owner
password as the way through. pdftotext ignores the flag and extracts anyway, so the oracle was
comparing our refusal against its text and scoring 0.000 -- measuring obedience, not quality.

bug1815476.pdf is the case: a one-page Spanish form we render perfectly, whose text we decline to
extract and pdftotext hands over regardless. It sat at the bottom of the worst list looking like a
font bug, which is what sent me looking at it.

Those files are now skipped and counted, not scored, and the count is printed rather than passed
over in silence, on the same rule the oracle crate exists for: a comparison that did not happen
must not look like one that succeeded. Over the first 250 corpus files that moves the mean from
0.811 to 0.854 and drops one file from the sample, with the median unchanged at 0.991.

Permissions come from `Document::permissions`, so this is not a match on our own error text. A
file that cannot be opened at all counts as permitted, because that is a different problem and
dropping it quietly would hide it.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-10-14 15:51:32 -07:00
Tanish MankuandClaude Opus 5 f802257be6 docs(fonts): the README still called the standard-14 widths an estimate
F1 replaced the character-class approximation with the published standard-14 metrics, and
`helvetica_width` has delegated to `Std14::Helvetica` ever since. The README kept describing it as
PdfCraft's own estimate and, more importantly, said "No metrics file or font program from any
vendor is bundled" -- which stopped being true when the generated tables landed.

They are Adobe-authored non-visual technical data, the item AGENTS.md 1.1 permits, carried in
std14_metrics.rs with adobe_data = true in ATTRIBUTION.toml. A contributor reading the README
would have concluded there was no vendor data here at all, which is the one kind of error that
matters in a provenance claim. Says what is bundled now, and what still is not: widths and
encodings, never a font program or a typeface.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-10-14 15:32:36 -07:00
Tanish MankuandClaude Opus 5 e0c63546bb test(render): pin that every JBIG2 conformance fixture decodes to the same picture
The render oracle put four JBIG2 fixtures at the top of its disagreement list against Poppler, and
the obvious reading was that hayro-jbig2, which carries local patches, had a bug. Investigating it
found the reverse.

The 96 `bitmap-*.pdf` corpus files are a conformance suite: one small drawing encoded through every
corner of JBIG2 -- each generic-region template, custom AT pixels, TPGDON and TPGRON, refinement,
halftone with and without skip, symbol dictionaries with Huffman tables, MMR, the composition
operators, random access, unknown-height segments. Those features change how the bytes are
compressed, not what the picture is, so a correct decoder renders all 96 identically. That is a
real test with no reference image and no external oracle: the suite checks itself.

We render 1 distinct image across all 96. Poppler renders 8: it draws a blank page for the seven
halftone-skip and symbol fixtures, and its output changes when TPGRON is switched on, which a
correct decoder's cannot. So roughly a fifth of Poppler's JBIG2 output is wrong, the top of the
oracle's worst list is its bug rather than ours, and Poppler is not a usable oracle for JBIG2.

Watched failing by admitting one unrelated file to the set: the test fails and names it as the odd
one out rather than merely reporting a count.

The expectation holds of the pinned corpus. Whoever re-pins it to a newer pdf.js should re-check
this rather than assume it still holds, which the test's own documentation says.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-10-14 15:12:52 -07:00
Tanish MankuandClaude Opus 5 5e08aa8719 fix(oracle): say what the render-oracle score does and does not mean
Investigating the worst file of the first 250 turned the headline finding around. bug1552113.pdf
scores 84%, and the cause is Poppler: it paints the link's `/Border [0 0 112]` as a 112-unit band
across a 250x50 page, hiding the two lines of text. The fixture is named "this text should be
visible", and in our rendering it is. The oracle is not ground truth, and the top of its worst
list needs a person to look at both images before it is read as a bug of ours.

That led to a plausible change that the measurement rejected. Poppler synthesises appearances for
annotations that have none, where we draw only what the document carries -- `/AP`-less link borders
are already recorded as not covered under view.render-annotation-appearances -- so hiding
annotations looked like the way to measure page content alone. It is worse: it then scores Poppler
for every appearance we do draw. Over the same 250 files, shown gives mean 2.16%, p90 5.35% and 25
files within 0.1%; hidden gives 2.18%, 5.55% and 22, with annotation-tx3.pdf jumping to 45.64% and
annotation-tx2.pdf to 18.79%. The two biases nearly cancel.

So the default is unchanged and `--hide-annotations` is an opt-in for explaining a single file,
with the numbers written down so the experiment is not repeated. Verified that the default
reproduces the merged baseline exactly and that the flag moves it.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-10-14 14:59:26 -07:00
Tanish MankuandClaude Opus 5 f988cf87ed feat(oracle): measure rendering against Poppler, and turn the pixel test on
Gap 12 of docs/gaps.md: until rendering is measured, every claim about its quality is a judgement.
Text extraction had an oracle and written output had qpdf; rendering had nothing.

`cargo xtask render-oracle` renders page 1 of every corpus file with our CLI and with Poppler's
pdftoppm and reports how far apart they are. The metric is the point. A raw pixel comparison is
useless here -- two correct renderers disagree on every glyph edge, so it calls a perfect page
badly wrong and says nothing about whether a shading is missing. `pixels::compare` counts a pixel
as differing only when it differs from every pixel in the other image's 3x3 neighbourhood by more
than a tolerance, in both directions, taking the worse. A one-pixel shift is forgiven; absent or
miscoloured content is not.

First reading, 250 files at 72 dpi, tolerance 8: 229 compared, median 0.89%, mean 2.16%, p90
5.35%, 25 within 0.1%. It reports rather than gates -- the corpus is not on CI, and a first
reading of a borrowed renderer is a baseline to improve on, not a bar to clear. The worst results
cluster on JBIG2 `bitmap-*` files and on ZapfDingbats, which aims the renderer work at something
specific for the first time. 15 files could not be compared because the two renderers round the
page box to different pixel sizes; they are reported as unmeasurable rather than scored.

Verified against the knob's bounds end to end: tolerance 255 scores every file 0.00%, tolerance 0
takes the median to 0.95% and p90 to 26.36%.

Also turns on `extract_type3_shared_xobjects_matches_poppler_pixels`, which had never run anywhere.
Poppler is now installed on the Linux runner, and the test is version-robust by construction --
both sides of its comparison go through the same pdftoppm, so it is about our two documents rather
than about Poppler's output. Verified in all three states: runs and passes with pdftoppm present,
skips without it, fails without it when PDFCRAFT_ORACLES_REQUIRED is set.

`run_capture` keeps its exact behaviour, including leaving the buffer empty on non-UTF-8 output;
the byte-oriented capture an image needs is a second entry point on the same timeout logic.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-10-14 14:28:19 -07:00
Tanish MankuandClaude Opus 5 50629eb6bf feat(testkit): one way to write a synthetic PDF fixture
55 files hand-rolled the same serialiser -- number the objects, record each one's offset, emit a
classic cross-reference table and a trailer -- 111 times over. The object bodies are properly the
business of the crate under test; the envelope around them is not, and every copy is a place for a
fixture to be quietly wrong in a way that makes a test prove less than it appears to.

`pdfcraft-testkit` holds that envelope. It has no workspace dependencies on purpose, so the crates
at the bottom of the layering can use it without a cycle, and it returns bytes rather than a parsed
document for the same reason.

The builder is byte-identical to the serialiser it replaces, asserted against a verbatim copy of
the old code across versions and trailer variations, because a migration that changes a fixture's
bytes silently changes what the test using it proves. Watched failing by breaking the free-entry
generation: three tests caught it. The one intended difference is that an empty stream dictionary
gives `<< /Length N >>` rather than a doubled space.

Migrates cos, measure and redact: 111 copies in 55 files becomes 108 in 53. The rest are
follow-ups rather than one sweep -- ui-egui alone is 27 of those files and is the most contested
area in the repository. Some copies should stay: a test about a malformed cross-reference table
needs to write its own bytes.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-10-14 13:57:56 -07:00
Tanish MankuandClaude Opus 5 0754740fd9 feat(oracle): make a missing external oracle loud instead of a silent pass
Every test that wanted an external oracle probed for it by hand and chose for itself what to do
when it was missing. The common choice -- print a line and return -- reports as a pass, so a suite
with no oracles installed looked exactly like one in which every oracle agreed.

That was not hypothetical. No CI runner installed qpdf, so `qpdf_agrees` had never executed on a
runner, while README.md said saved output was "independently checked with qpdf". For R2-R4 that
test is the only check of owner-password authentication (Algorithm 7) that does not go through our
own code.

`pdfcraft-oracle` holds the probes and one rule: a missing oracle is a declared skip or a failure,
never a silent pass. `PDFCRAFT_ORACLES_REQUIRED` picks which, on the same convention as
`CRAFT_FONTS_REQUIRED`. CI installs qpdf and sets it, so a skip notice there is now a red build.
The crate has no panicking path of its own; `or_skip!` expands its assert into the caller, which
is test code.

Verified by running the qpdf tests against qpdf 12.4.2 for the first time -- they pass, so the
README claim is now both enforced and true. Both gate directions were watched: absent and
unrequired skips with a notice, absent and required fails with the install command in the message.

Only qpdf is installed in CI, because it is the only tool the test job uses. The Poppler pixel
comparison in crates/organize stays ignored until a change turns it on and verifies it there.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-10-14 13:56:00 -07:00
Tanish MankuandClaude Opus 5 727555ca29 fix(xtask): let a standalone crate dev-depend on test support
The standalone rule ran before the testkit exemption and returned early, so `cos`, `geom`,
`filters`, `crypt` and `arlington` could not use `pdfcraft-testkit` or `pdfcraft-oracle` even as a
dev-dependency. Both crates were already registered in the layering table as `Class::Testkit`, so
the first real use of either from a standalone crate would have been rejected.

A dev-dependency is not part of the graph a standalone crate publishes, which is what the rule
protects; a normal dependency on test support, and any dependency on a product crate, are still
rejected. The existing test covered neither direction of the standalone case.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-10-14 13:56:00 -07:00
Mex 893cb7430e M1: preserve output-limit errors in raw Flate fallback 2026-10-14 21:58:05 +02:00
Brandon ThomasandClaude Opus 5.5 9931329b9f Release 0.8.0
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
v0.8.0
2026-10-14 09:06:28 -04:00
Marco Arnulfo c1a41cc5c7 fix(save): validate PDF output before replacing files
Validate serialized PDF bytes and page count before replacing the destination in desktop and automation saves. Reject unreadable or page-losing output before file replacement and saved-state updates.

Keep staging writes synced before publication and retain the regression for a repaired page tree that cannot survive a full rewrite.

Fixes #1698
2026-10-14 00:25:22 -06:00
skylar taylor-barrick 3ef63cb4e7 M2: retain open regressions and deterministic test fixtures 2026-10-13 23:14:03 -06:00
hpcgridandClaude Fable 5.1 e4028b3c77 fix(ui): reject stale file watcher results
Carry the watched path and epoch through queued file reads, and reject stale results before changing the document. Preserve newer saved contents and undo history across Save and Save As while retaining ordinary external reloads.

Fixes #1802

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2026-10-13 22:40:52 -06:00
Sir Clangington III. dc4d86ba40 fix(automation): sync staged output before publication
Sync staged output after writing all bytes and before replacing its destination, closing the handle before rename. Return sync failures while keeping the old destination and ordinary save state available for retry.

Fixes #1804
2026-10-13 21:44:45 -06:00
Ibn ZwanckaertandClaude Opus 5.5 5e12f5fd6d feat(ui): reorder and cycle document tabs
Reorder document tabs by dragging while keeping the active document and its view state. Add Ctrl+Tab and Ctrl+Shift+Tab cycling in visible order, including Combine files, and translate the menu labels.

Keep timestamped batch test expectations tied to the originally generated PDF bytes.

Closes #1537
Closes #1542

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-13 20:06:20 -06:00
Tanish MankuandClaude d9607ec3be F3: one documented substitution chain, and report what it chose and why
A font the document does not embed was replaced by whatever the caller
happened to pick, and nothing told the user. `doc_info` said only
`embedded: false`, which is why "my Chinese PDF shows nothing" has been
a mystery rather than a sentence.

`crates/fonts/src/fallback.rs` applies one chain, in a documented order,
and returns what it chose with a reason:

1. embedded -> nothing substituted;
2. /BaseFont names a standard-14 face or a known alias (exact metrics);
3. craft-fonts holds that exact family;
4. /CIDSystemInfo /Ordering names a script we have a face for;
5. the ordering names a script we have no licensed face for -- report
   none, so nothing is drawn rather than the wrong glyphs;
6. otherwise the nearest of the fourteen by descriptor flags.

It delegates to the existing per-script selectors instead of restating
their order, so each keeps one source of truth. It answers the
dictionary question only; "which face has a glyph for this character?"
depends on the string being drawn and stays where the drawing happens.

`doc_info` gains `substitute` and `substitution` per font. Both null
means the document's own glyphs are shown; a null `substitute` with a
reason means no face was available at all. That is Acrobat's Actual
Font, minus the interface, which `parity/` now records as still missing.

Two findings on the way:

`Std14::from_base_font` matches loosely, so "SomeHouseSerif" resolved to
Times by its name containing "serif". The face is a fair guess but
reporting it as "names one of the standard 14" would have been untrue of
a font nobody has heard of. `Std14::recognise` now says whether a name
matched an alias or only a family hint, and the two report differently.
`from_base_font` is unchanged.

The generated table moved to `crates/fonts/src/std14_metrics.rs`, data
alone, and the ATTRIBUTION entry and AGENTS.md 1.1 line follow it. F1
put the hash on a file holding both the table and the code reading it,
so this commit's ordinary edits tripped the gate -- and a hash that
churns on unrelated changes gets re-attested reflexively, which is the
protection gone. Verified both directions after the split: editing
std14.rs passes the gate, changing one width in std14_metrics.rs fails
it.

Every new test was watched failing first: removing the script step
breaks the two unit tests and the doc_info integration test.

Co-Authored-By: Claude <noreply@anthropic.com>
2026-10-13 16:42:10 -07:00
3f044b6263 fix(print): adopt native Windows spooler
Adopt the canonical craft-print revision from craft-libs #28 so Windows printing uses the in-process Windows.Data.Pdf/XPS backend instead of PowerShell. Keep exact paper, orientation and scaling validation and the existing PDFCraft composition and permission checks.

Pin the reviewed winprint fork, scope its compile-time licence/advisory exceptions, and distinguish the earlier WDAC Sandbox exercise from the final pinned revision's Windows CI coverage. Physical printer output remains unverified.

Add worker-barrier regressions for Home-selected tool handoff and startup document ordering.

Fixes #1107

Co-authored-by: durtyrobbin <76844303+durtyrobbin@users.noreply.github.com>
Co-authored-by: Dimitar Klaturov <7221384+bulgariamitko@users.noreply.github.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-authored-by: skylar taylor-barrick <hi@skylar.cc>
2026-10-13 14:46:24 -06:00
skylar taylor-barrick b5467d93a0 fix(ui): preserve native open actions and password prompts
Carry selected tool actions through native file preparation, password retries and conversion, and defer startup intake until pending native opens finish.

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

M3; refs #1914.
2026-10-13 12:18:41 -06:00
MMexicannandCodex b85d519993 M6: preserve whitespace in XML and XFDF attributes
Encode tabs and line feeds as character references so XML normalization cannot change field names, comment identities or metadata. Add failing-before-fix saved-PDF round-trip regressions.

Co-Authored-By: Codex <noreply@openai.com>
2026-10-13 14:16:44 +00:00
MMexicannandCodex 2a218f3420 M4: resolve indirect page rotations before editing
Read the effective rotation through its PDF integer object so inherited and
page-local indirect values retain their orientation when rotated.

Co-Authored-By: Codex <noreply@openai.com>
2026-10-13 14:13:38 +00:00
7437bde378 M3: draw a GNOME-style title bar on Wayland (#553) (#1530)
GNOME's Wayland compositor draws no window frames (it offers no
zxdg_decoration_manager_v1), so winit drew its own fallback title bar,
which ignores GNOME's theme, dark mode and button layout. PdfCraft now
asks the compositor at launch (apps/pdfcraft/src/decorations.rs) and,
where it draws no frames, turns winit's frame off and makes the tab strip
the header bar, as in GNOME's own apps
(crates/ui-egui/src/window_controls.rs):

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

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

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

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

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

Co-authored-by: bmh129 <11478682+bmh129@users.noreply.github.com>
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Co-authored-by: Brandon Thomas <echelon@gmail.com>
2026-10-13 03:04:13 -04:00
RidwanandBrandon Thomas 7739685796 Issue #1396: remove an imported digital ID (#1515)
Digital IDs added through Sign with a Digital ID (imported .p12/.pfx files
or new self-signed ones) are saved with the app settings, but nothing ever
took one off the list, so an old or mistakenly imported certificate stayed
there for good (#1396).

Sign with a Digital ID now has Remove next to Configure New Digital ID. It
acts on the selected file-based ID, asks first ("Remove "<name>" from this
list? The file itself is not deleted."), and takes the entry off the saved
list. The .p12 file stays on disk. Keychain and Windows store IDs can't be
removed here: the OS manages them and they are listed again on every
signing.

- sign_ui: PdfCraftApp::remove_digital_id refuses OS store IDs and stale
  indexes, keeps the dialog's selection on the same ID (or the first
  usable one), and goes to Configure when no IDs are left.
- SignDraft::confirm_remove holds the entry awaiting confirmation; clicking
  another row cancels the question.
- i18n: the confirmation is translated in all 21 catalogs; ATTRIBUTION.toml
  hashes updated.
- Tests: a unit test for the list, the selection and persist/restore, and a
  real-shell test that imports a .p12, cancels, then removes it.

Co-authored-by: Brandon Thomas <echelon@gmail.com>
2026-10-13 02:49:38 -04:00
e88dd728cb M13: scan each directory once in CLI checks (#1501)
Co-authored-by: MMexicann <MMexicann@users.noreply.github.com>
Co-authored-by: OpenAI Codex <noreply@openai.com>
Co-authored-by: Brandon Thomas <echelon@gmail.com>
2026-10-13 02:41:41 -04:00
5ebb53a4a4 M4: preserve shared Type 3 glyph resources during extraction (#1500)
Co-authored-by: MMexicann <MMexicann@users.noreply.github.com>
Co-authored-by: OpenAI Codex <noreply@openai.com>
Co-authored-by: Brandon Thomas <echelon@gmail.com>
2026-10-13 02:34:12 -04:00
13a921ae4a M2: keep byte boundaries in packed TIFF-predictor rows (#1499)
Co-authored-by: MMexicann <MMexicann@users.noreply.github.com>
Co-authored-by: OpenAI Codex <noreply@openai.com>
Co-authored-by: Brandon Thomas <echelon@gmail.com>
2026-10-13 02:27:26 -04:00