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
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 <[email protected]>
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
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#1537Closes#1542
Co-Authored-By: Claude Opus 5.5 <[email protected]>
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 <[email protected]>
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 <[email protected]>
Co-authored-by: Dimitar Klaturov <[email protected]>
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Co-authored-by: skylar taylor-barrick <[email protected]>
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.
GNOME's Wayland compositor draws no window frames (it offers no
zxdg_decoration_manager_v1), so winit drew its own fallback title bar,
which ignores GNOME's theme, dark mode and button layout. PdfCraft now
asks the compositor at launch (apps/pdfcraft/src/decorations.rs) and,
where it draws no frames, turns winit's frame off and makes the tab strip
the header bar, as in GNOME's own apps
(crates/ui-egui/src/window_controls.rs):
- Minimize, maximize/restore and close in Adwaita's style (round buttons,
symbolic glyphs drawn in code, no new assets), in the order and on the
side named by org.gnome.desktop.wm.preferences button-layout, read
through gsettings; Fedora's default `appmenu:close` shows only close.
Hidden in full screen.
- Resize zones along the window's edges and L-shaped corners, above the
rest of the UI so a press there resizes instead of reaching a
scrollbar; a hairline outlines the window while it isn't maximized.
- Preferences > Use system title bar (shown only where the app draws the
frame) gives the window back winit's frame at once, without a restart
(part of #1001). Named as in VectorCraft and PhotoCraft, but GNOME draws
no title bars for apps, so on GNOME this is winit's basic frame, not
GNOME's own; the tooltip says so. Saved as `system_title_bar`;
`system-title-bar` on the control channel.
- PDFCRAFT_TITLEBAR=app|system overrides the detection.
KDE, Sway, Hyprland, COSMIC and X11 offer server-side decorations and
keep the frame their desktop draws; macOS and Windows are unchanged.
Not done: right-clicking the header bar doesn't open GNOME's window menu
(egui has no command for it), the window has no shadow or rounded
corners, the resize zones lie just inside the window rather than outside
it, and Windows still uses its system frame (the rest of #1001).
The new labels (Minimize, Maximize, Restore, Use system title bar and its
tooltip) were added to all 21 translation catalogs. These translations
have not been checked by anyone fluent in those languages and should be
reviewed. The catalogs' hashes in ATTRIBUTION.toml are updated.
Made with Claude Code (Claude Opus 5.5, medium effort). It passes the
workspace quality gates (fmt, clippy, cargo test --workspace, xtask
assets) and was checked by hand on Fedora 44 GNOME (light and dark,
maximized and not, the preference toggled live), but it has not been
exhaustively regression tested.
Co-authored-by: bmh129 <[email protected]>
Co-authored-by: Claude Opus 5.5 <[email protected]>
Co-authored-by: Brandon Thomas <[email protected]>
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 <[email protected]>
* fix(text): read a TrueType font without an encoding as WinAnsi (fixes#1468)
* fix(text): describe the WinAnsi reading by pdftotext's output
---------
Co-authored-by: Brandon Thomas <[email protected]>
A stamp placed on a page with /Rotate was drawn in user space, so the
label or picture turned with the page. Counterrotate the appearance so
it reads upright as the page is displayed.
Co-authored-by: Brandon Thomas <[email protected]>
- Create PDF detects BigTIFF (II+/MM+) as well as classic TIFF; the tiff
decoder already reads it, so these scans now convert instead of being
refused.
- Image detection accepts only GIF87a/GIF89a, the headers conversion
decodes, so a malformed GIF header is no longer called an image and then
rejected with a misleading error.
- file-format-parity.md: XDP split into XFA data import/export (shipped
under form.xfa-data) and opening .xdp as a document (none).
Tests: bigtiff_images_become_pages; malformed GIF headers in
source_kind_tells_pdfs_images_and_text_apart. Both fail without the fix.
Claude-Session: https://claude.ai/code/session_01F8DFYWkDyTnEcin9PNkUW5
Co-authored-by: Claude <[email protected]>
Co-authored-by: Brandon Thomas <[email protected]>
Flattening deleted a cloudy comment it could not draw, and a refused text-box restyle or resize left the new properties in the file. Arrow defaults were stored on plain lines. A custom match ending on a separator included the next glyph, and the vertical-text redaction error claimed an overlap it had not checked.
Co-authored-by: Brandon Thomas <[email protected]>
The declaration line was built from the PDF/A identification alone, so a
document that declares only PDF/UA-1 read "Declared conformance: none".
It now lists PDF/UA next to PDF/A, and still says none when neither is
declared. pdfa_dialog_shows_a_declared_pdfua fails before the change.
With the options dialog turned off, run_accessibility_check used the range
saved on an earlier, longer document as it was: a page past the end
selected no page, and the page rules came out Passed without looking at
anything. The range is now clamped to the active document's pages, as the
dialog already does. a_saved_page_range_past_the_document_still_checks_its_pages
fails before the change with Passed for Tagged content.
Layout::multiple(n) maps counts without a fixed grid to a near-square
grid (3 -> 2x2, 5 -> 3x2), but Layout::Multiple kept only the grid, so
layout() filled every cell: three per sheet put four pages on a sheet.
Layout::Multiple now carries per_sheet, capped by the grid; spare cells
stay blank and cut-stack piles use the same count. The doc_print tool
and the print dialog pass the count through.
Co-authored-by: Claude Opus 5.5 <[email protected]>
Follow-up to #1420.
#1420 compares the bytes of each entry to tell a file added twice from a
different file with the same name and size. An image or text file is
converted to PDF each time it is added, with its own date, so those bytes
never match and the same .txt or image added twice was no longer flagged.
Each entry now keeps the file as it was added (the same bytes for a PDF,
the image or text otherwise), and that is what is compared.
Adds the_same_image_or_text_file_added_twice_is_a_duplicate in
combine_ui.rs, which fails before this change.
Some programs start the PDF reader with the path unquoted
(`pdfcraft.exe P:\Archive\00042A26 Contract.PDF`), so Windows splits it
into `P:\Archive\00042A26` and `Contract.PDF` and neither opens
("Couldn't read 00042A26: os error 2"). Acrobat accepts this. Rejoin
consecutive arguments when one names no file but it and the ones after it,
joined with a space, do (longest run first); everything else is kept as
given. Done before the single-instance hand-off, so files sent to a running
PdfCraft are rejoined too.
Co-authored-by: magdale76 <[email protected]>
Co-authored-by: Claude Opus 5.5 <[email protected]>
ui.move refuses a coordinate that isn't finite once narrowed to a
pointer position; ui.click {x, y} and ui.drag did not, so 1e39 became
an infinite position and was queued. With debug assertions on, the
next idle frame panicked in emath comparing NaN vectors.
Both now refuse such a coordinate with an error, as ui.move does, and
ui.drag also refuses ends too far apart for the way between them to
stay finite.
Adds click_and_drag_refuse_coordinates_that_overflow_like_move in
tests/control.rs, which panics before this change.
Rules skipped in the accessibility results were kept in one set for
the whole app, and every check applied it. With the options dialog
turned off, skipping Tagged PDF in one document made the next
document's check report it as Skipped, its findings cleared, in the
panel and the saved report.
The set is now keyed by document: Skip Rule records the rule for the
document whose report is shown, and a check applies only that
document's skips. Start Checking in the options dialog still starts
over, for the document it checks only.
Adds a_rule_skipped_in_one_document_is_not_skipped_in_another in
tests/accessibility.rs, which fails before this change.
Use a session-wide byte-accounted LRU instead of retaining every visited
page. Share the renderer's PageText Arc without cloning all glyphs and
avoid retaining obsolete document bytes through the cache.
Preserve per-page extraction results so text_find can continue after a
failed page, reporting bounded warnings alongside readable matches and
normal continuation cursors. Other text consumers retain strict errors.
Add deterministic coverage for cache budgets across documents, eviction,
revision changes, close, partial searches and bounded warnings.
Fixes#1483.
Co-authored-by: LabDennis <[email protected]>
`set_info` changed only `/Info`. Files that carry XMP (LibreOffice and Word
exports, PDF/A) kept the old dc:title and dc:creator, so viewers that read
XMP first kept showing the old title, and PDF/A preflight reported the two
as different (6.6.3).
The matching XMP property (Title dc:title, Author dc:creator, Subject
dc:description, Keywords pdf:Keywords, Creator xmp:CreatorTool, Producer
pdf:Producer) is now replaced where it stands, added in a description of
its own, or removed with an empty value. The packet is edited as text and
the rest of it stays byte for byte; files without XMP get none, and a
packet that isn't UTF-8 is left as it was.
Co-authored-by: Claude Opus 5.5 <[email protected]>
Follow-up to #1259. Before a connection authenticates, the control server
read request lines with BufRead::lines, which buffers a whole line before
anything looks at it, so a client that never sends `auth` could make it
buffer an arbitrarily long line.
Read lines with a bounded read instead. Until `auth` succeeds a line may be
at most MAX_AUTH_LINE (4 KiB; the auth request is about a hundred bytes);
a longer one gets a -32600 error and the connection is closed, after
reading at most MAX_AUTH_LINE + 1 bytes of it. After `auth`, lines are read
whole as before. End of stream, read errors and non-UTF-8 input end the
connection as they did with BufRead::lines.
Tests: lines_are_read_up_to_the_limit, an_overlong_line_before_auth_is_refused,
long_lines_after_auth_are_still_read.
Co-authored-by: Claude Opus 5.5 <[email protected]>
* M8: redaction unlinks removed images and forms from inherited and shared resources
Apply Redactions and Remove Hidden Information give a rewritten page its own
pruned /Resources, but the dictionary the page drew through before could still
name the image or form that was removed:
- a /Resources inherited from a /Pages node;
- one resource dictionary object shared by several pages;
- the page's own dictionary, when a form without /Resources drew the image
through it.
A full-rewrite save keeps whatever is still referenced, so the removed image
samples and form text stayed in the saved file while the operation reported
success. Per-page resources (the case fixed in #1099) were not affected.
The interpreter now records the original object behind every XObject it drops
or replaces. After the page loop, each page-tree resource dictionary that still
names one loses that entry unless a page drawing through that dictionary still
draws the name (the same image on another page, or again outside a mark). A
dictionary that loses an entry is replaced by a private copy on its node, so a
dictionary object shared with other nodes is left alone for them. If a page's
use of a name cannot be told (unreadable content, a form that will not decode),
the operation fails closed.
Tests: redacted_xobjects_do_not_survive_through_inherited_or_shared_resources,
an_image_another_page_still_draws_through_the_same_resources_is_kept,
an_image_a_form_drew_through_the_pages_resources_does_not_survive,
a_hidden_layers_image_does_not_survive_through_inherited_resources.
Not covered here: originals named by a form XObject's own /Resources, by an
annotation appearance or by a pattern.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
* M9: a cross-reference entry the reader sets aside is never an unchanged signed object
The reader that judges post-signing changes and the renderer that draws the
page resolve some cross-reference entries differently. A revision appended
after signing could redefine a page's content stream and give the object an
entry the reader sets aside while the renderer follows it:
- an in-use entry at offset 0 (the reader skips it; the renderer fails to read
the object there, rebuilds its index and takes the last definition);
- a second entry for the same object in one section (the reader keeps the
first, the renderer the last).
The classifier compared the signed object with itself, found nothing changed
and reported the signature Valid while the page showed the new content.
pdfcraft-cos now records the entries it does not take at face value
(Document::xref_oddities: offset-0 in-use entries other than object 0,
duplicate entries that differ, reserved cross-reference stream types, and a
hybrid /XRefStm entry that disagrees with the table), and can say whether an
entry locates the object itself (Document::xref_entry_is_exact). What it
resolves is unchanged.
classify_changes treats any such entry added after the signed revision as a
change that is never permitted (page content when the object was a signed page
or its content, other changes otherwise). An object whose content compares
equal at a new place is unchanged only when its entry is exact: where the
reader had to search for the object, another reader may arrive elsewhere.
Tests: a_cross_reference_entry_the_reader_sets_aside_does_not_hide_new_page_content,
a_cross_reference_entry_that_misses_its_object_is_not_an_unchanged_object,
cross_reference_entries_that_are_set_aside_are_recorded,
an_entry_that_misses_its_object_is_found_by_searching_but_is_not_exact.
This closes the two demonstrated triggers and their near variants. It does not
prove that the two parsers agree on every file: other differences in what each
tolerates may remain.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
* M8: Apply Redactions is refused when the page still shows text under a mark
Redaction decides what lies under a mark from the positions its own
interpreter computes, and its verify pass reuses them. Where those differ from
the renderer's, a mark drawn over the text as displayed lies over glyphs the
interpreter places elsewhere, or does not see at all: they stayed in the file
and the operation reported success.
The case the test pins: a font and size set through an ExtGState /Font entry
(`gs`), which the renderer honours and the interpreter does not model. The same
kind of gap was observed for a font a form XObject takes from its caller, for
some predefined CMaps and for a rotated Type 3 font matrix. The standard-14
width error that first showed it is fixed at its root by d0d1ad81.
The engine now asks the renderer, which is what the user saw when placing the
marks. After redaction runs, the result is written as a save would write it
(without the marks' own overlay), its text is extracted, and a glyph still at
least half inside a mark fails the edit with the existing "could not be
verified" error. The edit runs on a copy, so the document is unchanged. A page
whose text cannot be read back fails the same way.
This makes such mismatches fail closed; it does not make those documents
redactable. Teaching the interpreter each of them is separate work.
Test: redaction_is_refused_when_the_page_still_shows_text_under_a_mark (the
line whose font is set by `gs` is refused and left unchanged; the same line set
with `Tf` is redacted).
Co-Authored-By: Claude Opus 5.5 <[email protected]>
---------
Co-authored-by: Claude Opus 5.5 <[email protected]>
wgpu validates indirect draw and dispatch arguments with a compute shader it
compiles when the device is created. Metal on an Intel Iris Pro (MacBookPro11,3,
macOS 12.7.6) fails to compile it ("Compiler encountered an internal error" in
MTLCompilerService), the device is lost, and the window falls back to OpenGL.
PdfCraft draws and dispatches nothing indirectly, so leave the check out of the
instance flags. WGPU_VALIDATION_INDIRECT_CALL=1 turns it back on. VectorCraft
does the same (storytold/vectorcraft#651).
Co-authored-by: Claude Opus 5.5 <[email protected]>
On macOS sync_all is F_FULLFSYNC, which SMB and FUSE volumes such as a mounted Cryptomator vault refuse with ENOTSUP or ENOTTY, so every save there failed. Those two errors now fall back to fsync; any other flush error still fails the save.
* fix(edit): move existing text by its frame, and selections with the arrow keys (fixes#1520)
* docs(parity): arrow-key nudging and the paragraph frame are no longer gaps (#1520)
Two follow-ups the maintainer asked for after #1355.
The script session's worker now says when the engine is ready for the
script it was sent (its JavaScript context built; FormCalc at once),
and the script's time limit runs from then, with the engine's own
start limit before it, as the one-shot path always did: building a
context is not charged to the script, which matters on a busy machine.
A field inside an area shares its data node with a field of the same
name outside it (areas are left out of data paths). The in-place
refresh of the live form now follows the data path through the tree
and gives every field bound to the node the value the tree builder
would, each by its own template field (an exclusion group there means
a rebuild), and the engine's value setter, with an index of widgets by
data node, gives every widget bound to the node the value by its own
kind, hidden fields included, writing the data whenever the node's
text differs. The script's time now also starts after its host and
globals are set up. A stale parity note (choosing a picture for an
image field, delivered in #1175) is removed.