Commit Graph

1189 Commits

Author SHA1 Message Date
DWAA1660 0bc619cd02 Extract pages: a File name field names the extracted files (#737) (#751)
* 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>
2026-10-10 08:20:08 -04:00
Silver 49a8d04f24 M9: sign and timestamp encrypted documents (#753)
* 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>
2026-10-10 08:19:45 -04:00
Márk Bartalos 7a33d4bf25 M9: the signer's certificate must allow document signing (key usage, EKU, critical extensions) (#749)
* 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>
2026-10-10 08:14:55 -04:00
cyfung1031 e4e39f4bef M4: keep the render pool across metadata edits (#750)
* 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>
2026-10-10 08:10:32 -04:00
Abdelhaq El Amraoui 1c33e6f851 M14: Arabic interface language (#692)
* 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>
2026-10-10 06:36:54 -04:00
Todd Pederson ebc5ccd4c0 M14: View menu, tooltips and Keyboard shortcuts show Ctrl, not ⌘, off macOS (#695)
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>
2026-10-10 06:32:06 -04:00
Márk Bartalos bfa65a9bfc M8: redaction code sets (U.S. FOIA, Privacy Act) as overlay text (#741)
* 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>
2026-10-10 06:27:26 -04:00
DWAA1660 e4cd9e2557 PDF/A: no sRGB output intent for CMYK documents, and pdfa_verify checks the intent's colour space (#667) (#732)
* 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>
2026-10-10 06:22:47 -04:00
cyfung1031 742c42ab63 M2: cancel obsolete in-flight requests (#702)
* 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>
2026-10-10 06:22:24 -04:00
cyfung1031 7fafadc3a2 M2: text turned at any angle reads as lines along its own baseline (#735)
* 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>
2026-10-10 06:19:54 -04:00
cyfung1031 d94da2038a M2: pages with /UserUnit are shown at their size in points, as in Acrobat (#722)
* 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>
2026-10-10 06:19:31 -04:00
mmmaarian03-crypto 99c7dd9311 Sign: smart cards and tokens in the Windows store ask for their PIN (#694) (#742)
* 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>
2026-10-10 06:19:07 -04:00
Todd Pederson c5d886488c M14: side panels keep their width after a resize (#704)
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>
2026-10-10 06:15:06 -04:00
LloydNicholson 06a7b9a7ca Issue #666: images made into a PDF keep their ICC profile (#729)
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>
2026-10-10 06:14:51 -04:00
jcl-bit d5b5687f79 Sign: decide Windows store keys in code every platform builds (#690)
#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>
2026-10-10 06:14:36 -04:00
cyfung1031 fb548a7064 fix(annot): preserve strokes with malformed interior colors (#708) 2026-10-10 06:14:21 -04:00
cyfung1031 997b82d49c test(render): cover pattern recursion cycles (#710) 2026-10-10 06:14:05 -04:00
cyfung1031 7a8445376e M3: compare stitched render partitions with whole page (#697) 2026-10-10 06:13:50 -04:00
cyfung1031 f109676f77 test(render): cover form recursion cycles (#709) 2026-10-10 06:13:12 -04:00
Oussama Dalhi f52f93bcd2 Close the command palette on outside click (#696) 2026-10-10 06:12:57 -04:00
Krzysztof Cieślik 627d822709 create: lift TIFF decoding buffer limit to allow print-sized images (#665) (#681)
* create: lift TIFF decoding buffer limit to allow print-sized images (#665)

By default, the tiff crate limits decoded buffer size to 256 MiB.
Set Decoder::with_limits(Limits::unlimited()) to match PNG decoding
behavior and prevent 'decoder limits exceeded' on large images.

* create: cap the TIFF decode buffer at 2 GiB instead of lifting it entirely (#665)

A few bytes claiming enormous dimensions must still be refused, not allocated.

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>
2026-10-10 06:12:10 -04:00
cyfung1031 a211e334a0 fix(render): key soft masks by inherited context (#716)
Co-authored-by: Brandon Thomas <echelon@gmail.com>
2026-10-10 06:11:45 -04:00
mmmaarian03-crypto f8c463416a Sign: long digital ID names stay inside the picker (#743)
The rows of Sign with a Digital ID painted the name and the "Issued by"
line unclipped, so a long subject or issuer (a Windows store
certificate named after a GUID, for example) ran past the dialog's
right edge.

Both lines are now cut with "…" at the row's edge, and a cut row shows
its whole text on hover.

Test `long_digital_id_names_stay_inside_the_picker` fails before the
change.

Co-authored-by: mmmaarian03-crypto <253881360+mmmaarian03-crypto@users.noreply.github.com>
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-10 06:04:32 -04:00
cyfung1031 de94e95994 #419: derive raster allowance test from demand (#723) 2026-10-10 06:04:17 -04:00
cyfung1031 e2b71e3e82 M5: unify annotation flag parsing (#698) 2026-10-10 06:04:03 -04:00
cyfung1031 0695fd6f16 fix(forms): reject out-of-domain field flags (#701) 2026-10-10 06:03:49 -04:00
cyfung1031 18e5db5e6d test(render): cover inherited form resource contexts (#707) 2026-10-10 06:03:36 -04:00
cyfung1031 d84d6c859a test(render): pin output across worker counts (#705) 2026-10-10 06:03:24 -04:00
cyfung1031 3d45b598ee test(engine): audit edit history side effects (#713) 2026-10-10 06:02:55 -04:00
cyfung1031 916e0bc4d5 #419: cover suspended raster resource ownership (#733) 2026-10-10 06:02:41 -04:00
DWAA1660 c02fe8ca53 About dialog test: wait for each control to stop moving before clicking it (#671) (#703)
The About dialog is a modal centred on its own size. Switching to the
Contributors tab grows it, and for a few frames it recentres: here the
Table toggle sits at y=378, then 355, then 341. The test waited a fixed
four frames (#656) before clicking Table. Since the 0.5.0 contributors
refresh that is not always enough (deterministic on some Debian
machines, intermittent on the GitHub runners), so the click landed where
the button had been and the table never opened: "No nodes found matching
the query: By { label: \"PRs\" }" at links.rs:78.

Clicking after one frame reproduces exactly that failure. A `settled`
helper now runs frames until the target node exists and its rect is the
same on two frames in a row, so the test no longer depends on how many
frames the modal needs. The dialog itself is fine: a real click comes
long after the two or three frames the modal takes to settle.

Co-authored-by: DWAA1660 <DWAA1660@users.noreply.github.com>
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-10 06:02:28 -04:00
cyfung1031 e5e6ded35b M5: constrain PDF font descriptor flags (#700) 2026-10-10 06:02:15 -04:00
Eduardo Soares ade5dc4dcb Print: list driverless CUPS destinations, not only permanent queues (#658)
* Print: list driverless CUPS destinations, not only permanent queues

The Print dialog's printer list came from `lpstat -p -d`, which lists only
permanent queues. A driverless IPP printer discovered on the network — the
standard setup on modern Linux desktops — has no permanent queue: only
`lpstat -e` (CUPS >= 1.7) names it, and CUPS builds a temporary queue when
`lp` sends it a job. Until now such printers were invisible and the dialog
showed only Save as PDF.

Enumerate `lpstat -e` alongside `lpstat -p -d` and merge the answers: queues
first, deduplicated, default still from `-d`; a spooler without `-e` keeps
today's behaviour and loses only the driverless names. The .deb/.rpm now
recommend cups-client so a machine without a print stack is not silently
printerless.

Fixes #652

Co-authored-by: ZCode (GLM-5.3) <noreply@z.ai>

* Print: fix the off-unix build and a driverless default's marker

`lpstat_e_command` is called only from `printers`' unix block, so off unix
it was dead code and CI's Windows clippy gate (`-D warnings`) failed to
build the crate. Gate it like its caller.

The default destination can itself be driverless — `lpoptions -d` names a
destination no queue exists for — and the merge appended it from
`lpstat -e` with `default: false`, so the dialog preselected the first
queue instead of the user's default. The `-d` default now marks the
appended entry too; the test covers it, and CRLF in `-e` output.

Co-authored-by: ZCode (GLM-5.3) <noreply@z.ai>

---------

Co-authored-by: ZCode (GLM-5.3) <noreply@z.ai>
Co-authored-by: Brandon Thomas <echelon@gmail.com>
2026-10-09 22:39:36 -04:00
cyfung1031 306bbcedf9 M3: harden render and CLI failure surfaces (#669)
* M3: harden render and CLI failure surfaces

Make content truncation observable, reject unknown CLI options, expose deterministic render work counters, and scope render failures to exact requests. Refuse silently reduced whole-page export rasters while preserving tiled rendering, and add heavy-vector and CLI regression coverage.

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

* Render: remember a failed render by page and kind again

A key with scale and tag made every zoom level and tile a new key: a
pathological page tied up a worker for the watchdog limit per request
(exhausting worker replacements) and was re-parsed each time. The
repeated-parsing check asks for a new generation and scale again.

* Export: pages too large at the chosen dpi export at the most the renderer allows, and say so

#669 made every Exporter refuse oversized whole-page renders, which
reached File > Export to image, doc_export_* and OCR: at 600 dpi any page
side over 13.65 in (Legal, A3, Tabloid) failed instead of exporting. The
exporter clamps again; Exporter::dpi_used reports the resolution a page
is drawn at, which the Export dialog's message and doc_export_png's
lower_dpi list mention. A source with reject_oversize still refuses
(CLI render keeps its strict config).

* doc_export_images: describe lower_dpi

* Render: ContentTruncated is reported once per page; the hayro-interpret patch is recorded

Nested forms, Type 3 glyphs and patterns are interpreted again and each
reported the exhausted budget; pdfcraft-render now keeps one. The warning
is part of hayro-interpret patch (18) (the page content budget): marked
in the code and described in vendor/README.md.

* CLI render: print render warnings (skipped content) to stderr

* CLI: the option check refuses unknown options only; commands report missing values themselves

text's own --page parsing (and its test on main) says what a page value
must be; the generic "needs a value" pre-empted it.

---------

Co-authored-by: Codex <noreply@openai.com>
Co-authored-by: Brandon Thomas <echelon@gmail.com>
2026-10-09 22:32:48 -04:00
cyfung1031 a5e378fd4b M3: tiles skip paths that can't paint them (item 5 of #623) (#670)
A tile of a large page renders the whole page with an offset, and hayro
stroked and filled every path on it before vello discarded what fell
outside the tile. Vendored hayro patch (9), `may_paint_canvas`, skips a
fill or stroke whose box misses the canvas: the box of the path's points
and control points (a curve stays inside their hull), grown by the
stroke's reach (half the width times the larger of the miter limit and
sqrt 2), by 0.5 user units for vello's and kurbo's approximation
tolerances, and by 2 device pixels for antialiasing. Empty paths and
non-finite bounds or miter limits are always drawn.

Renders are byte-identical with and without skipping
(`skipping_paths_off_the_canvas_changes_no_pixel`; the hidden per-thread
`set_skip_offscreen_paths` exists for that test). On #623's sample at
scale 2, a 1024 px tile takes about 38% less CPU time; whole pages are
unchanged.

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Co-authored-by: Brandon Thomas <echelon@gmail.com>
2026-10-09 22:15:31 -04:00
cyfung1031 ef9b553f55 M2: kanji that Adobe-Japan1 maps to a variation sequence draw in non-embedded fonts (#662)
Adobe-Japan1-UCS2 maps the JIS X 0208 forms of the kanji that
JIS X 0213:2004 redrew to a variation sequence (CID 1247 is 噂 U+E0100).
hayro only looked up single characters in a substitute font, so a
non-embedded Japanese font drew 噂, 逢, 溢, 祇, 箸 and over 200 other
common kanji as unrelated glyphs. A character with a variation selector,
from Adobe-Japan1-UCS2 or a /ToUnicode, now draws the substitute's glyph
for that form when its cmap has one, else its glyph for the character.

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Co-authored-by: Brandon Thomas <echelon@gmail.com>
2026-10-09 22:05:06 -04:00
cyfung1031 d63436ed10 M2: a character a substitute font lacks draws .notdef, not an unrelated glyph (#661)
A Type 0 font that isn't embedded is drawn with a substitute (a craft-fonts
Japanese face for Adobe-Japan1). When hayro couldn't find a CID's character
in the substitute by Unicode, it took the CID for a glyph index of the
substitute, which numbers its glyphs its own way: 𠮷 (CID 13706) drew as
"Q" in BIZ UDPGothic, ㈠ and the squared katakana words as unrelated
kanji. A CID of an Adobe character collection that the substitute can't
find now draws .notdef, unless the substitute is a CID-keyed CFF font,
which is looked up by CID as before.

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Co-authored-by: Brandon Thomas <echelon@gmail.com>
2026-10-09 22:04:26 -04:00
cyfung1031 d67e79dd15 M2: a ligature stays one glyph in the text layer, in every writing direction (#660)
A glyph whose ToUnicode is several characters ("ffi", Arabic lam-alef, a
letter with its marks) was split into one glyph per character, with the
glyph box cut into equal parts along view x. Turned text and vertical
columns put the parts across the line, so "office" at 90° read
"o ce\nf\nf\ni"; narrow ligatures put two "f"s close enough to be dropped
as fake bold ("of ice"); right-to-left runs reversed the characters inside
the ligature ("سالم" for "سلام").

The glyph now keeps its text and its own box, as PDFium reports every
character of a ligature with the whole glyph box.

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Co-authored-by: Brandon Thomas <echelon@gmail.com>
2026-10-09 22:04:11 -04:00
Tevinhead a99b39396f M6: bulk form-field properties and selection (#95) (#650)
* M6: bulk-edit shared form field properties (#95)

Add opt-in shared properties, partial appearance patches and atomic CLI/MCP
batches. Complete current-page, rectangle and Fields-panel selection, with
20 synthetic regressions for preservation, rollback and dialog lifecycle.

Co-Authored-By: Codex <noreply@openai.com>

* Bulk field properties: translated messages, a catalog check, and in-place /DA patches

- The bulk dialog's flag label, errors and notices are in all 13 full catalogs; the missing-field error is a template (fmt + tl!) instead of format!. New test bulk_field_properties_labels_are_in_every_full_catalog.
- patch_da rewrites the last Tf and fill colour in place (appending only when there is none), so /DA doesn't grow per edit; sizes clamp to 300 like parse_da.
- A size- or font-only edit leaves widgets untouched; an indirect /MK or /BS is updated where it lives instead of inlined into each widget.
- Draft::new says why: a widgetless field gets its own message, not 'could not be found'.

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

* Tests: pin the session clock in the bulk-fields harness (save_bytes stamps /ModDate)

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

---------

Co-authored-by: Tevinhead <173904641+Tevinhead@users.noreply.github.com>
Co-authored-by: Codex <noreply@openai.com>
Co-authored-by: Brandon Thomas <echelon@gmail.com>
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-09 21:56:17 -04:00
Eduardo Soares a2f0fb7363 Forms: fields listed only as page widgets join the form (#649)
A PDF whose AcroForm /Fields array is empty or missing listed no
fields, so none were fillable even though every widget sat on a page
with its name and value: Acrobat and the browsers fill those. The page
widgets are now adopted as fields — after the tree-listed ones, through
each widget's topmost unlisted /Parent so a field split across several
widgets stays one — and the leniency is recorded, never silent.

- forms: fields() walks /Fields as before, then adopts the unlisted
  page widgets; adopted_page_fields() counts them for the note.
- render: the inspector's field list adopts them the same way, so the
  Fields panel, the field-count notice and menu gating follow.
- engine: opening such a file notes "the form's /Fields list omits N
  fields; they were read from the page annotations" in the repair log
  (Document Properties ▸ Advanced).

Co-authored-by: GLM-5.3 <noreply@z.ai>
Co-authored-by: Brandon Thomas <echelon@gmail.com>
2026-10-09 21:43:09 -04:00
Tanish Manku 0a8b230ca0 D1: pin and verify the pdf.js corpus (#339)
xtask corpus cloned mozilla/pdf.js at HEAD with no commit pin and no content check, and the corpus tests returned early when the directory was missing. So the corpus sweep measured a moving target, xtask/baselines/pdfjs.json was compared against whatever upstream happened to be that day, and a damaged or tampered checkout produced green results. AGENTS.md §2 asks for fixtures fetched pinned by commit and sha256-verified.

Verification is two layers. The commit fixes which tree we ask for; the manifest digest fixes what landed on disk: one '<sha256>  <path>' line per file, sorted by path, then one sha256 over that text. Git object ids are SHA-1, so the manifest is what makes this a sha256 check. The pin is 5d923c9 with 1455 files, which matches what the GitHub tree API reports for test/pdfs.

The checkout sets core.autocrlf=false and core.eol=lf. This repository has autocrlf=true globally, so without that git would rewrite line endings on checkout and the same commit would hash differently on Windows than on Linux; the pin has to mean the same thing everywhere.

A successful verification writes corpus/pdfjs/.pdfcraft-corpus.json. The corpus tests now skip only when nothing is fetched, and fail loudly when a checkout exists without that stamp. xtask check verifies before sweeping, so the baseline is never compared against an unknown tree.

Checked end to end: the unrecorded-pin guard refuses to fetch; --update-pin fetches and prints the constants; appending one byte to one of the 1455 files is detected and repaired by re-fetching; removing the stamp makes the corpus tests fail with a message saying what to run. Against the verified corpus the tests pass: 964 files ok and 7 password cases.

Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
Co-authored-by: Brandon Thomas <echelon@gmail.com>
2026-10-09 21:26:54 -04:00
Márk Bartalos c5d12ad1d7 View: draw page previews at the resolution they are shown (#529)
* Implemented full resolution pdf rendering

* View: walk nearby print sheets without a range index

Clippy rejects the preview neighbour loop because the index only selects sheets.

* Forms: drop duplicated appearance redraw tests

A merge copied three tests four extra times, so pdfcraft-forms failed to compile.

* 529: keep previews sharp within main's texel-for-texel rule and bounded budgets

Drops whole-page 2x supersampling (SUPERSAMPLE_LIMIT, whole_page_scale)
and the 4096 BASE_SIDE: #260 keeps whole-page rasters texel for texel,
and #419's budgets assume 2048-side backdrops. Print-preview rasters
are capped to min(max_texture_side, 4096) px on a page's long side, and
collected nearest sheet first within 128 MiB (the current sheet always).

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

* 529: struct-update syntax in the preview tests (clippy)

---------

Co-authored-by: Brandon Thomas <echelon@gmail.com>
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-09 21:26:40 -04:00
Mawfyy b8b6fb2f45 Never crash & persist: Make Current Properties Default and page-display choices survive a restart (#340, #266) (#365)
* Never crash & persist: Make Current Properties Default and page display choices survive a restart

#340:
- Expose per-tool comment styles via CommentPrefs::styles().
- persist() writes a `comment_styles` array (tool, color, opacity, width, fill).
- restore() reads each entry through the clamping setters; a new rgb_of helper
  at the read boundary refuses malformed colors whole (string, wrong length,
  null, non-finite) and clamps finite out-of-range values into 0..=1, so a
  hostile settings file can't smuggle a NaN or saturated color.

#266:
- The page display chosen in a document (view.layout.*, view.fit_one_page,
  view.fit_width_scrolling) now mirrors into view_defaults, which the existing
  default-layout/default-zoom keys already persist. New documents and the next
  launch open in the persisted display; transient zooms do not rewrite the
  default (matches Acrobat).

Tests:
- make_current_properties_default: persist + fresh app reuses the style.
- the_page_display_you_choose_becomes_the_default: choice mirrors, persists,
  new document opens in the persisted display; Acrobat fit modes persist too.
- comment_style_settings_clamp_and_refuse_hostile_values: hostile JSON is
  clamped or refused.

cargo fmt --check and cargo clippy -p pdfcraft-ui-egui --all-targets are clean.

* Fix clippy: chunks_exact(4) -> as_chunks::<4>() in signature_image.rs

* 365: keep the comment-style persistence (#340) only; a saved no-fill clears a default fill

The #266 half (every View choice rewriting the default page display)
is dropped: Preferences already sets that default explicitly. rgb_triple
moves out of the module declarations with one doc comment.

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

* Fill & Sign dates: Italian names, since Italian is now an interface language (#494)

date_languages_are_the_interface_languages requires every interface
language to have date names; the unknown-language example in the
session test moves from it to xx.

---------

Co-authored-by: Mawfyy <mawfyy@users.noreply.github.com>
Co-authored-by: Brandon Thomas <echelon@gmail.com>
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-09 21:26:23 -04:00
Eduardo Soares 49c6e2ec54 i18n: complete Brazilian Portuguese (pt-BR) interface catalog (#604)
* i18n: complete Brazilian Portuguese (pt-BR) interface catalog

Catalog by André Cardoso (andrecards) from #288, unioned with the
strings merged after the catalog was cut. Asset hash refreshed.

* i18n: translate the pt-BR strings added since the #288 catalog and gate coverage

* i18n: pt-br review fixes (line ending, redact/sanitize/page-boxes terminology)

* i18n: credit the pt-BR translator and document full coverage

Name André Cardoso (andrecards) as the pt-br catalog's translator in
ATTRIBUTION.toml: the ~1,826-entry gist posted in issue #288, with the
labels added since and the wording review as this change's contribution.
ATTRIBUTION.md is regenerated from it.

docs/localization.md moves Brazilian Portuguese from the menus-only
clause into the full-coverage list, states the 1,997-entry count next
to the provenance (following the Ukrainian paragraph's pattern), and
lists pt-br with the languages whose coverage tests gate every
command, All tools label and tl!() literal (added with the catalog).

people.toml is unchanged: PR #165's translator has no entry there, and
its policy is that only one's own entry may be added.

* i18n: pt-br writes '>' like the other Latin catalogs (Inter has no ▸)

* i18n: pt-BR final review fixes (toast grammar, contextual entries, menu gate)

* pt-BR: add the labels main gained while this catalog was completed (98) and drop stale OCR keys

---------

Co-authored-by: Brandon Thomas <echelon@gmail.com>
2026-10-09 20:56:13 -04:00
Will 94f9ce69c6 i18n: add Italian (it) interface translation (#494)
* i18n: add Italian (it) interface translation

- Complete it catalog (about 2,000 entries); it and it_* locales use it
- Fonts test covers the Italian catalog; ▸ written as > like fr/es/ru, since Inter has no ▸
- Docs, attribution and ROADMAP

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

* it: add the labels main gained since this catalog (83), drop stale OCR keys, and coverage tests

---------

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Co-authored-by: Brandon Thomas <echelon@gmail.com>
2026-10-09 20:55:59 -04:00
Dammy196 a016f88a90 Fix non-exhaustive match in Windows signing path (#646)
* Fix non-exhaustive match in Windows signing path

The PublicKey enum gained P521, BrainpoolP256, BrainpoolP384, BrainpoolP512, and Ed25519 variants, but the Windows-specific match statements in windows.rs were not updated, causing a compile error on Windows builds.

Add wildcard arms that return a clean error for unsupported key types.

* Windows store: a key type it can't sign with is Unsupported, not a crypto failure

---------

Co-authored-by: Brandon Thomas <echelon@gmail.com>
2026-10-09 20:55:40 -04:00
Max Abramowitz c110c579a2 M4: delete pages from the Pages panel (#607)
Right-clicking a thumbnail offers Delete pages, and Delete or Backspace with the
pointer over the panel deletes too, as in the organize grid: the selection, or
the current page. A document keeps at least one page, so with every page
picked the item is off and the keys do nothing. The selection goes and the
current page moves to the page after the deleted ones; one undoable step.

Parity: view.thumbnail-context-ops notes and evidence include Delete pages.

Co-authored-by: William Max & Co. <info@williammax.com>
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Co-authored-by: Brandon Thomas <echelon@gmail.com>
2026-10-09 20:29:26 -04:00
mrme2tiri-hash 6e13a5fc54 Fill & Sign: remove white background from imported signature images (#637) 2026-10-09 20:29:12 -04:00
Max Abramowitz 4fc99e4845 Tab strip: shrink tabs before scrolling, keep Open in sight; ignore about:blank titles (#612)
* M3: tabs shrink to fit before they scroll, and Open stays in sight

With more documents open than fit, the tabs kept their full width: they ran
under the buttons on the right (#416 made them scroll) and the Open button
scrolled away with them. Now the tabs shrink first, as browsers do: narrow
tabs keep their width, wider ones share the rest equally and end their names
in "…" (the tooltip has the whole name). Only once they are at their narrowest
does the strip scroll. Open sits after the tabs, outside the scrolling area,
so it is always in sight next to the controls on the right.

Refs #397.

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

* M3: a placeholder title like about:blank leaves the file name on the tab

A browser's "Save as PDF" of a blank page or pop-up titles the document
"about:blank" and asks viewers to show the title, so its tab and window said
"about:blank". A one-word browser address (about:, blob:, data:) or a bare
"Untitled" now counts as no title, and the file name is shown. A tab that
does show a document title names its file on hover.

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

* M3: arrows step through tabs that don't fit, and tabs keep most of their name

Tabs shrank to 110 points before the strip scrolled, so with many open they
became a few characters each and nothing showed that more were hidden. Tabs
now stop at 180 points (most of a name), and once even that doesn't fit,
arrows appear either side of them: each steps about one tab, and an arrow is
dimmed at its end, so it is clear whether more tabs lie that way. The mouse
wheel still scrolls them, and the active tab is still brought into view.

New labels "Scroll tabs left" / "Scroll tabs right" are translated in every
catalog (machine-written; native review welcome).

Refs #397.

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

---------

Co-authored-by: William Max & Co. <info@williammax.com>
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Co-authored-by: Brandon Thomas <echelon@gmail.com>
2026-10-09 18:56:45 -04:00
Streetblock 1f0466fcae M2: bound image resampling buffers before allocation (#592)
Check source, destination, convolution and filter-plan sizes before allocation. Preserve original image geometry when a resize exceeds the policy, while allowing valid wide sources to shrink.

Co-authored-by: OpenAI Codex <codex@openai.com>
Co-authored-by: Brandon Thomas <echelon@gmail.com>
2026-10-09 18:53:56 -04:00