Recovering an encrypted snapshot parked its metadata in `pending_recovered`
until the password prompt was answered. Cancel left it there, and the next
encrypted document unlocked in the foreground took the snapshot's original
path and recovery key: Save would overwrite the other file and delete its
snapshot.
The association now travels with the prompt (`PasswordPrompt::recovered`).
Cancel puts the entry back on offer and leaves the store alone; a wrong
password keeps it on the re-raised prompt; the correct one finishes the
recovery of the tab that was just opened. `pending_recovered` is gone.
Test: crash with an encrypted snapshot of A, Recover, Cancel, open encrypted
B with its password. B keeps its own path and is clean, A's snapshot stays
in the store, also after closing B. Fails on main at "B keeps its own path".
Co-authored-by: Tristan von Kries <tristan@MacBook-Pro-von-Tristan.local>
Co-authored-by: Claude Fable 5.1 <noreply@anthropic.com>
Export wrote polygon and polyline comments without their points and import
had nowhere to read them from, so an XFDF round trip left the comments with
no /Vertices and no appearance. Export now writes the points as XFDF
<vertices>x,y;x,y;…</vertices>, and import reads them back (whole x,y pairs
only) before the appearance is drawn.
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>
Case-insensitive search folded the query and the page with
str::to_lowercase, which follows no locale: U+0130 (İ) becomes "i" plus a
combining dot above (two characters) and U+0131 (ı) has no case pair. So
"istanbul" never found "İSTANBUL", and "ırmak"/"kış" never found
"IRMAK"/"KIŞ" (or the reverse). The same PageText::find serves Find,
Search & Redact (phrases and word lists) and the automation text search,
so Turkish words could also be left unredacted.
Fold I, i, İ and ı to "i" and every other character with its own
lowercase, one character for one character. Case-sensitive search is
unchanged.
find_urls case-folded a copy of the page text and then sliced the original
characters with offsets taken from that copy. Case folding can change a
string's length — U+0130 (İ) lowercases to "i" plus a combining dot — so the
offsets drifted past the end of the source and `chars[i..j]` panicked. The CLI
exited 101 before it could create the link; MCP reported an internal tool error
but kept serving.
Match the ASCII prefixes case-insensitively against the original characters
instead, so every offset stays in the source's own index space, and use get()
so no lookup can index past the end.
A new test reproduces the reported panic on the old code (range start index 41
out of range for slice of length 39) and passes with the fix. pdfcraft-annot's
tests, clippy and rustfmt are green.
Fixes#816.
Co-authored-by: Big Pickle <noreply@opencode.ai>
* view: check Shift-Command-G before Command-G for find navigation (#810)
* fix: recognize paragraph drafts as unsaved work (Issue #811)
---------
Co-authored-by: Brandon Thomas <echelon@gmail.com>
* view: check Shift-Command-G before Command-G for find navigation (#810)
* Tests: the find shortcut test waits for find by a deadline
(cherry picked from commit eaf3180bd8ebb353b3d8c5ca7b9ef7548ed55c37)
---------
Co-authored-by: Brandon Thomas <echelon@gmail.com>
The comment summary gathered a comment's direct replies only, matching /IRT against the comment itself. A reply to a reply points at the direct reply, so its text never reached the generated summary even though the stored discussion kept it.
Replies are now walked to any depth, with a bound, and indented one level per reply. The existing output for direct replies is unchanged.
The new test builds a rectangle, a reply and a reply to that reply, then checks the summary carries both texts with the nested one indented under its parent. It omitted the nested reply before the fix.
Co-authored-by: Big Pickle <noreply@opencode.ai>
The ocrs crate's default alphabet has an E where its recognition model was
trained with € (ocrs-models DEFAULT_ALPHABET), so a scanned "€250" was
recognised as "E250". pdfcraft-ocr now gives ocrs the alphabet the model was
trained with.
the_alphabet_is_the_trained_one checks the alphabet without the models;
recognize_text_reads_the_euro_sign reads a scan end to end when they are
installed.
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Page numbering adds the start number to the position of the page in the run. At the top of the u32 range that addition wrapped, so a release build saved zero for the second page and still reported success, and a debug build hit a caught overflow error instead.
Check the whole run before drawing anything. When the last page number would pass the u32 limit the operation returns an actionable error and the document keeps no header, rather than a half applied run.
Fixes#804.
Co-authored-by: Big Pickle <noreply@opencode.ai>
Acrobat reports an encrypted PDF as "damaged and could not be repaired", before
asking for the password, when the document catalog sits in an object stream.
Every other reader tried (qpdf, pypdf, poppler, PDFKit, PdfCraft) accepts such
files, and qpdf keeps the catalog standalone too. Full saves of encrypted
documents now write the catalog as its own object; unencrypted output is
unchanged. Verified with Acrobat DC on macOS: the same file fails with the
catalog packed and opens with it standalone, in AES-256 and RC4-128.
Claude-Session: https://claude.ai/code/session_01REz7wTfQgnopyvN9eQJJVu
Co-authored-by: Tristan von Kries <tristan@MacBook-Pro-von-Tristan.local>
Co-authored-by: Claude Fable 5.1 <noreply@anthropic.com>
comment_list collected only the replies whose /IRT pointed at the root comment, so a reply to a reply was listed nowhere at all. Roots and the root count were already right; the nested reply was simply dropped from its thread.
Follow the /IRT chain to any depth, with a bound, so every reply under a comment is listed in document order. A reply to a reply now stays discoverable through the tool that promises every comment and its replies.
The new test builds a root note, a direct reply and a reply to that reply, then checks both texts are listed and the two notes stay roots. It listed only the direct reply before the fix.
Co-authored-by: Big Pickle <noreply@opencode.ai>
Viewers draw notes, FreeText, ink and stamps from their dictionary when the file stores no /AP /N. flatten only drew annotations that already had an appearance and then removed the rest, so a supported note disappeared and the saved page came back blank.
Build the appearance a viewer would show for every selected, visible, non-widget annotation that has none, then draw it as usual. A subtype this build cannot render still fails to build and is handled exactly as before.
The new synthetic test draws the note icon into the page and asserts it is baked in; without the fix flatten reports 0 drawn and the icon is lost.
Co-authored-by: Big Pickle <noreply@opencode.ai>
The phrase search collapsed runs of whitespace in the query but not in the page text it was matching against, so a document drawn with two space glyphs in a row like the issue example never matched the phrase. Single words still matched, which is why it looked like only some files were affected.
Collapse whitespace while the text is flattened, the same way the query is normalised. The regression test fails on the old code because the phrase find returns an empty range, and it also covers tabs and single word queries.
Fixes#814.
Co-authored-by: Big Pickle <noreply@opencode.ai>
ocr_recognize_files turned a path outside the automation root into a per-file
error and returned success, so no_tool_escapes_the_root reported 9 leaks once
the OCR models were installed. Without them, loading the models failed first
and hid it, and CI never installs them. Every path is now checked before any
work starts, and one outside the root refuses the whole call, like the other
tools. A missing file inside the root stays a per-file error. Nothing was ever
read from outside the root.
ocr_recognize_files_refuses_paths_outside_the_root checks this without the
models.
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Page rotation only turns in quarter steps, but the degree value was read with an integer division by 90 that threw the remainder away. Asking for 45 degrees saved an unrotated page, 91 and 135 saved a 90 degree turn, and the command still reported success.
Reject the value before touching the document instead, so every caller gets an actionable error and the pages are left alone. The automation tool already rejected it; the single edit command did not.
Fixes#801.
Co-authored-by: Big Pickle <noreply@opencode.ai>
Progress docs per craftrules standards/progress-docs.md: target-app parity, gaps, roadmap with alpha gate, readiness by audience (full / mainstream / essentials) with Opus 5.5 hours, and feature, format, PDF-spec, hardware, UI and localization parity docs.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* M3: gate watchdog tests without timed sleeps
* test(render): replace watchdog timing delays with gates
* Render tests: the panic test doesn't wait on the gate's finished barrier
Only an abandoned render reaches it, so waiting there hung under load.
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>
* M8: redact lists of words or phrases
Find text and redact gains "Multiple words or phrases": a list with one word
or phrase per line, typed or imported from a .txt file (Import list…), whose
every match is marked in one undo step. Lists are trimmed, deduplicated and
bounded (1,000 entries of 256 characters; files up to 1 MB are read).
redact_mark takes words and reports matched_words, the marks each entry made.
protect.redact-word-lists is shipped; whole-word and case-sensitive matching
and exporting the list are still missing.
* M8: translate word-list labels into Arabic
The Arabic catalog arrived on main after this branch, so the new Find
text and redact strings were still English and failed
arabic_covers_ui_literals.
* Tests: drop sharp grid renders when the thumbnail will cover the cell
Zooming the organize grid back out left the sharp page raster in place
until a thumbnail texture existed, so the zoom test failed when that
texture had not arrived yet.
* M8: Czech rows for the word-list redaction labels
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>
* Parity: merge the duplicate form.field-list evidence and notes
#649 and #650 each added `evidence` and `notes` to the same entry, so the
TOML has duplicate keys and `cargo xtask parity` fails on main.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* M4: bookmarks from tagged headings (organize.bookmarks-from-structure)
Bookmark options > New bookmarks from structure (command
bookmark.from_structure, tool bookmark_from_structure) turns a tagged
document's headings into bookmarks, as Acrobat's New Bookmarks From
Structure does for headings: H1-H6 through the role map, a plain H by its
Sect/Part/Art nesting, nested by level under a new first "Untitled"
bookmark, each linked to its structure element (/SE, ISO 32000-2
§12.3.3). Titles come from /ActualText, the marked text drawn on the page
(decoded through the font's ToUnicode or encoding), /Alt or /T.
The structure walk is bounded (depth, visits, heading and title caps,
seen-set) and survives self-referencing elements; an untagged document
gets an actionable error; the edit is one undo step under the assemble
permission. Not yet: choosing other element types, and destinations at the
heading's position rather than its page.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* Tests: size the shared raster allowance from thumbnail demand
The canvas test planted a 132×171 thumbnail whose tag did not match the
request finish_render_frame computes. On FreeBSD that real render finished
in the same frame, so the retained raster exceeded 132×171×4. Plant a
thumbnail that already satisfies demand and compare against that size.
* M4: a heading's text stops gathering once it is far past the title length
A heading whose marked-content list repeats one mark up to the visit cap could otherwise collect
about a gigabyte before the title is cut to 256 characters.
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>
* Extract pages: a File name field names the extracted files (#737)
The Extract pages dialog had no way to name the result: separate files
were always "<document> (page N).pdf" in the chosen folder, and a new
tab was "<document> (extract).pdf".
The dialog now has a File name field, filled in with the document's
name when it opens. Separate files are "<name> (page N).pdf"; a single
renamed page is "<name>.pdf"; extracting into a new tab names the tab
(and so the Save As suggestion) "<name>.pdf". The name is made safe for
every platform (separators, reserved and control characters, trailing
dots and spaces, any-case ".pdf"), and falls back to the document's
name when nothing usable is left.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* Extract pages: Windows device names get a prefix, and the name is capped at 200 bytes
"NUL.pdf" writes nowhere on Windows, and 200 CJK characters overflow the 255-byte name limit.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
---------
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>
* M9: sign and timestamp encrypted documents
Signing a password-protected PDF failed with "not supported: signing
encrypted documents". The writer encrypted every string, including the
signature's /Contents placeholder, which the signer patches in place
after hashing.
- cos: a signature dictionary's /Contents is never encrypted or
decrypted (ISO 32000-2 §7.6.2), identified by /Type /Sig|/DocTimeStamp
or by /Contents + /ByteRange. Existing signatures in encrypted files
now read their real CMS and survive rewrites of their dictionary.
- sign: the blanket refusal is replaced by the document's permissions
(Table 22) when opened with the user password: existing fields and
document timestamps need fill-forms, new fields need annotate +
modify, certifying needs modify. Refusals name the permissions
password.
- Tests for all four algorithms, counter-signing, tampering,
permissions, document timestamps and the sign_document tool; parity
entry sign.encrypted-documents; ROADMAP log.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Signed-off-by: Hadi Chokr <hadichokr@icloud.com>
* M9: signing an encrypted document waits for a pending security change to be saved
Otherwise the incremental save becomes a full rewrite that applies or removes protection while
signing.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
---------
Signed-off-by: Hadi Chokr <hadichokr@icloud.com>
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Co-authored-by: Brandon Thomas <echelon@gmail.com>
* Parity: merge the duplicate form.field-list evidence and notes
#649 and #650 each added `evidence` and `notes` to the same entry, so the
TOML has duplicate keys and `cargo xtask parity` fails on main.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* M9: the signer's certificate must allow document signing (key usage, EKU, critical extensions)
A trusted signer whose certificate may not sign documents was reported
Valid. Validation now checks the signer certificate (RFC 5280 §4.2.1.3,
§4.2.1.12, §4.2): a keyUsage needs digitalSignature or nonRepudiation, an
extended key usage needs a document-signing purpose (any, emailProtection,
codeSigning, RFC 9336 documentSigning, Adobe Authentic Documents, Microsoft
document signing), and no critical extension may be one PdfCraft doesn't
process. Otherwise the verdict is Unknown and the details say why.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* M9: a critical qcStatements extension doesn't make a qualified certificate unusable
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>
* M4: keep the render pool across metadata edits
Metadata edits (SetInfo) no longer rebuild the render pool or the display
bytes. The engine refreshes the document-information fields, the file size,
the signatures (when present) and the working bytes, and keeps the renderer.
Undo and redo of metadata edits reuse the same pool.
Comment and form edits still rebuild the renderer and their view data.
Tests:
- metadata_edits_and_history_reuse_the_render_pool
- field_and_comment_edits_still_rebuild_the_render_pool
- metadata_edit_keeps_the_renderer_without_changing_page_content (the
metadata invalidation audit; its assertions now expect the kept renderer)
Supersedes #711 and #712.
Co-Authored-By: Claude Haiku 5.5 <noreply@anthropic.com>
* M4: a metadata edit reads the other Info entries as the full inspection does
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
---------
Co-authored-by: Claude Haiku 5.5 <noreply@anthropic.com>
Co-authored-by: Brandon Thomas <echelon@gmail.com>
* M14: Arabic interface language
A 2,136-entry Arabic catalog (six CLDR plural forms, Arabic month and
weekday names), selected by `ar` and every `ar-*` locale. egui joins
Arabic letters but places words left to right, so the catalog is put
into display order when it loads (bidi::display_rtl); placeholders,
keyboard shortcuts and caption colons stay in place. The command
search matches a multi-word Arabic query word by word.
The craft-fonts pin moves to the revision with Noto Sans Arabic.
crates/fonts/build.rs now refuses to embed Adobe type designs, so the
Noto Sans CJK SC in that revision is never embedded (AGENTS.md §1.1).
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* M14: Arabic catalog follows the {key} shortcut labels and has Redaction code
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>
View ▸ Zoom and Page navigation were hand-written with "⌘1", "⌘[" … while
the registered items below them were formatted by Shortcut::label, so
Linux showed both ⌘1 and Ctrl+K in one menu. Ten tooltips and the view-key
rows of Help ▸ Keyboard shortcuts hard-coded ⌘ the same way.
- The keys the document view handles itself are Shortcut constants in
ui-egui commands.rs, written with the same Shortcut::label.
- Labels follow egui's detected OS (ctx.os()) instead of cfg!, so the web
build in a Linux or Windows browser shows Ctrl too, and tests can check
both platforms.
- Translated tooltips take a {key} placeholder (13 catalogs; only the
shortcut token changed, hashes updated in ATTRIBUTION.toml).
- Fit visible is listed once in View (in Zoom), not also in the generated
list; Ctrl+3 and the palette still run it.
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Co-authored-by: Brandon Thomas <echelon@gmail.com>
* M8: redaction code sets (U.S. FOIA, Privacy Act) as overlay text
Redaction properties offers "Custom text" or "Redaction code" under "Use overlay
text". Picking the U.S. FOIA (5 U.S.C. 552(b)) or U.S. Privacy Act (552a) set and
ticking codes makes new marks carry them as overlay text, in the set's order.
redact_mark takes code_set and codes and rejects unknown sets or codes with the
allowed list.
Also merges a duplicated evidence/notes pair in form.field-list that made
parity/acrobat-features.toml fail to parse on main.
* M8: say why the default redaction code set can't fail
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* Tests: pin the clock in form_bulk (saves a second apart differed in /ModDate on Windows CI)
---------
Co-authored-by: Brandon Thomas <echelon@gmail.com>
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
* Windows: open files in the running PdfCraft instead of a new window each time (#282, #317)
Outlook attachments, Explorer double-clicks and Open With start pdfcraft.exe with the file,
so every file got its own process and window. Now the first PdfCraft listens on a Unix domain
socket in %LOCALAPPDATA%\PdfCraft (or a portable copy's data folder), one per Windows session.
A later launch that only names files sends their absolute paths, waits for "ok" and exits; the
running app opens them as tabs through the existing OsEvent::Open path (the one macOS Apple
events use) and comes to the front.
Anything else keeps today's behaviour: --new-window, any other option, a launch without files,
or any failure to hand over within 5 seconds opens a window of its own. Other platforms are
unchanged.
The socket lives in the user's profile, so other accounts can't create, replace or reach it.
uds_windows (already in the lockfile through zbus) provides it: std has no Unix sockets on
Windows and the workspace forbids unsafe. A socket left by a crash is replaced on the next start.
* Windows: launches started together share one window (#282, #317)
Several files selected in Explorer and opened at once start one process
per file at the same moment. A launch could then find the socket bound
by another launch that was not listening yet: connecting failed, clearing
the socket failed with "Access is denied", and the launch opened a window
of its own.
Launches now take turns through a lock on a file beside the socket, held
only while a launch finds out whether it is the first. A socket that
cannot be cleared because another launch holds it is tried again.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016es5tszobbb1yZ7JeozJXL
* Windows: a portable copy doesn't hand files to another instance
Its data folder may be writable by other accounts, so a socket there could leak or inject file
paths. Builds on art804's single-instance work in #602.
Co-authored-by: art804 <7828385+art804@users.noreply.github.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
---------
Co-authored-by: art804 <7828385+art804@users.noreply.github.com>
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Co-authored-by: Brandon Thomas <echelon@gmail.com>
* PDF/A: no sRGB output intent for CMYK documents, and verify checks the intent's colour space (#667)
pdfa_convert always added an sRGB output intent when there was none,
even for documents whose only content is DeviceCMYK, and pdfa_verify
then called them compliant: it only checked that some PDF/A output
intent existed. ISO 19005-2 6.2.4.3 allows DeviceCMYK only with a CMYK
output intent, and DeviceRGB only with an RGB one.
- verify reads the PDF/A output intent's destination profile (/N, else
the ICC header's data colour space) and reports DeviceCMYK under a
non-CMYK intent and DeviceRGB under a non-RGB one (6.2.4.3, not
fixable).
- convert adds the generated sRGB intent only when the document has no
DeviceCMYK; a CMYK document keeps the existing "needs a CMYK output
intent (not added automatically)" issue instead of a wrong intent.
- Device colour detection tells gray, RGB and CMYK apart (gray works
under any intent).
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* PDF/A: DefaultRGB/DefaultCMYK resources satisfy the output-intent colour check
ISO 19005-2 6.2.4.3 accepts device colour drawn through a default colour space under any output
intent.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
---------
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>
* render: cancel obsolete in-flight requests
* #623: recheck cancellation after busy publication
* #623: record rendering session in roadmap
* M2: a request cancelled during the parse doesn't leave its worker unable to render
The abort was cached as the worker's parse result, so every later request on that worker failed as
unparsable. Also drops ROADMAP edits that described other PRs.
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>
* M2: text turned at any angle reads as lines along its own baseline
The text layer sorted every glyph into the nearest of four quarter turns
and laid it out by its axis-aligned box. Text at 30-60 degrees lost its
word gaps, split into one line per word in reverse order, or interleaved
two neighbouring lines letter by letter; text set along a curve came out
in fragments.
Each glyph now keeps its drawn em box as a quad and its writing
direction. Glyphs within the layout's word-gap tolerance (sin 0.15) of a
quarter turn take the existing path unchanged. Other glyphs are laid out
in a frame along their own baseline, one flow per direction; runs that
continue each other while turning (stamps, seals) are unrolled into one
line. Turned text keeps the place in the reading order that its first
glyph had before.
Horizontal, quarter-turn, skewed and near-axis text is bit-identical
(text, rect, line_of, space_before) over pdf_engine_test.pdf, the
crates' test PDFs and 300 synthetic pages.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* M2: look up a turned glyph's direction among directions only, not every curved run
Curved runs can number about half the glyphs, which made grouping quadratic on hostile pages.
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>
* M2: pages with /UserUnit are shown at their size in points, as in Acrobat
/UserUnit gives the size of a page's user-space unit in points; PdfCraft
ignored it everywhere except the measuring tools, so a /UserUnit 2 page
was shown, extracted, printed and edited at half its size, and the
measuring tools disagreed with the rest of the app.
The value is now read as Acrobat reads it (the page's own entry, a number
from 1 up, at most 75,000; anything else counts as 1) in two places:
- vendored hayro-syntax Page (patch 13): render_dimensions and
initial_transform give the page in points, so rendering, tiles,
PageInfo and the text layer follow; a page without it keeps exactly
the transform it had.
- pdfcraft-model Page: user_unit(), and display_size/view_matrix in
points, so print, header/footer, watermark, background, added content
and measuring agree with the screen. The measure crate drops its own
arithmetic. Added items record the /UserUnit they were written for;
items without the record (written before) read back converted.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* M2: an added item's recorded /UserUnit is clamped like the page's
An infinite or huge recorded /UserUnit made the scale factor 0 and collapsed the item.
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>
* Sign: decide Windows store keys in code every platform builds
#646 fixed the Windows build after #386 with `_` arms in the two
matches in windows.rs. That module is cfg(windows), so Linux and macOS
CI never compile it, and the next PublicKey variant will either break
the Windows build again or, now, fall into `_` without anyone choosing.
The store's two decisions move into keys.rs, which every platform
builds, with every variant named and no `_` arm:
- PublicKey::store_signing_key: RSA, or ECDSA with 256/384 bits, or
None for the kinds PdfCraft only verifies with (P-521, brainpool,
Ed25519, Unsupported).
- PublicKey::store_signature_der: CNG's raw r||s as DER (RSA as is);
the verify-only kinds return SignError::Unsupported, as since #646.
windows.rs calls both, with no `_` arms left. Behaviour is unchanged.
tests/store_keys.rs runs everywhere: every variant's answer (with an
exhaustive match, so a new variant must be added to it), the P-256 and
P-384 conversion round-tripped through PublicKey::verify (code that
used to compile only on Windows), and malformed CNG signatures erroring
rather than panicking.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* Sign: smart cards and tokens in the Windows store ask for their PIN (#694)
Signing with a Windows store identity whose key lives on a smart card or
USB token failed with NTE_SILENT_CONTEXT (0x80090022) and no PIN dialog.
`identities()` acquires each key with CRYPT_ACQUIRE_SILENT_FLAG so that
listing never prompts, and signing reused that handle after
`set_silent(false)`. That only drops NCRYPT_SILENT_FLAG from
NCryptSignHash: a provider handle that was opened silently stays silent,
so a card that needs its PIN refuses instead of asking.
`WindowsKey` now keeps the certificate context and acquires a fresh,
non-silent key handle when it signs, so the provider can show its own
dialog. Listing is unchanged and still silent.
Reproduced and checked by hand with a qualified certificate on a SafeNet
token (SafeNet Smart Card Key Storage Provider, RSA-2048): 0.5.0 fails
with 80090022, this build shows the SafeNet PIN dialog and signs. The
software-key round trip (`signing_with_windows_store_identities`,
--ignored) still passes on Windows 11.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* Sign: a Windows store identity signs with its issuers
A digital ID file brings its certificate chain and the signature embeds
it; a Windows store identity signed with its own certificate only. A
certificate issued by an intermediate CA (the usual case on a token)
then validates only where the reader already has that CA.
`windows::find`, which fetches the identity to sign with, now fills
`chain` with the certificates Windows chains it to, without the root.
`identities()` still builds no chains, so listing stays fast and local.
A chain that can't be built leaves `chain` empty, as before.
Test `a_windows_store_identity_signs_with_its_issuers` (--ignored, like
the other store test): a temporary root, issuer and signer; the
signature validates against the root alone. The cleanup guard now also
removes the public copy New-SelfSignedCertificate leaves in Intermediate
Certification Authorities, which the existing test left behind.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
---------
Co-authored-by: James Locke <336218001+jcl-bit@users.noreply.github.com>
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Co-authored-by: mmmaarian03-crypto <253881360+mmmaarian03-crypto@users.noreply.github.com>
Co-authored-by: Brandon Thomas <echelon@gmail.com>
A resizable egui panel stores the width its content used at the end of
every frame and starts the next frame there, so a row wider than the panel
widens it on every repaint (any pointer move) until it reaches 520 px.
- Bookmarks: the search row reserved 32 px for an 8 px gap and a 26 px
clear button, overflowing by 2 px. The field now reserves the button's
size plus the theme's item spacing.
- Comments: the reply row reserved 56 px for the gap and "Post", which only
fits English. The field now fills its line and Post sits right-aligned
under it, keeping the field first in Tab order.
- Tests read egui's PanelState and hold each panel's width across 30
repaints (Bookmarks with a bookmark; Comments with a reply open in
Russian with Post hovered). On main they grow 358 → 418 and to 520 px.
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Create from images (doc_create, and image_xobject for stamps, signatures and
inserted images) dropped a JPEG's or PNG's embedded ICC profile, so a CMYK
JPEG became untagged /DeviceCMYK.
- JPEG: the APP2 ICC_PROFILE chunks are joined by sequence number; a profile
with a missing or repeated chunk is ignored.
- PNG: the iCCP profile is read.
- A profile whose header is sound (acsp) and whose colour space matches the
image (GRAY/RGB/CMYK for 1/3/4 components) becomes [/ICCBased <stream>]
with /N and the device space as /Alternate. Anything else keeps the device
space, as before. JPEG data and an Adobe CMYK /Decode are unchanged.
- Images in one document that carry the same profile share one profile
object.
TIFF profiles are not read yet.
Co-authored-by: lloyd <lloyd@stackworx.io>
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
#646 fixed the Windows build after #386 with `_` arms in the two
matches in windows.rs. That module is cfg(windows), so Linux and macOS
CI never compile it, and the next PublicKey variant will either break
the Windows build again or, now, fall into `_` without anyone choosing.
The store's two decisions move into keys.rs, which every platform
builds, with every variant named and no `_` arm:
- PublicKey::store_signing_key: RSA, or ECDSA with 256/384 bits, or
None for the kinds PdfCraft only verifies with (P-521, brainpool,
Ed25519, Unsupported).
- PublicKey::store_signature_der: CNG's raw r||s as DER (RSA as is);
the verify-only kinds return SignError::Unsupported, as since #646.
windows.rs calls both, with no `_` arms left. Behaviour is unchanged.
tests/store_keys.rs runs everywhere: every variant's answer (with an
exhaustive match, so a new variant must be added to it), the P-256 and
P-384 conversion round-tripped through PublicKey::verify (code that
used to compile only on Windows), and malformed CNG signatures erroring
rather than panicking.
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>