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>
* Parity: document the Page Display resolution gap at fixed zoom
view.zoom-to-percent and view.zoom-actual-size are shipped with empty
notes. Record that PdfCraft's 100% is its fixed 96-dpi basis, that
Acrobat's fixed-percentage display also depends on its Page Display
Resolution preference, and that PdfCraft does not offer that preference
yet (tracked with misc.prefs-page-display). Status, commands, tools and
evidence are unchanged. No runtime behavior changed.
Co-Authored-By: Claude Haiku 5.5 <noreply@anthropic.com>
* Parity: state the Acrobat fixed-zoom formula in the zoom notes
The zoom percentage and Actual Size notes now give the measured rule
(logical px per PDF point = ppi / 72 × zoom), name the Custom
resolutions checked black-box, and say that Use System Setting is not
reproduced. Status, commands, tools and evidence are unchanged. No
runtime behavior changed.
Co-Authored-By: Claude Haiku 5.5 <noreply@anthropic.com>
---------
Co-authored-by: Claude Haiku 5.5 <noreply@anthropic.com>
* macOS: IME text committed after the marked text is cleared isn't lost (winit#4650)
winit 0.30.13 reports insertText as Ime::Commit only while marked text is
left, and after any commit it swallows the next key. Real paths where that
loses text:
- input methods that end a composition with unmarkText, then insertText
(rust-windowing/winit#3925): the committed text was dropped;
- Apple Korean 2-Set, for the key typed after a syllable: on 한 then 5 it
calls setMarkedText("한"), insertText("한"), setMarkedText(""),
insertText("5"), so the 5 (or ;, or any key that ends the syllable) was
dropped;
- a commit made outside a key press (a candidate clicked with the mouse,
dictation): no key follows to reset the state, so the next key (for
example Backspace) was swallowed.
vendor/winit now commits when a composition was started (pending_commit,
set in setMarkedText, cleared by the commit and when IME is disallowed);
other insertText calls still reach the app as KeyboardInput. A commit
outside keyDown clears the marked text and returns to the ground state.
validAttributesForMarkedText lists the underline and marked-clause
attributes, which some input methods need before they compose.
This is rust-windowing/winit#4650, the 0.30 backport of #4651 (merged on
winit master), plus the part of #4748 that fixes#4651's out-of-key-press
case (#4748's other parts are not included). One correction: upstream
spells the underline attribute "NSUnderlineStyle", but AppKit's
NSUnderlineStyleAttributeName is "NSUnderline" (printed on macOS 14), so
the AppKit value is used.
Checked with the real input methods on macOS 14.7.1 (a scratch winit app fed
key events through AppKit): with the patch 한+5 and 한+; commit the 5 and ;,
unmarkText-then-insertText commits, and Backspace after a commit outside a
key press is delivered; ABC typing, held keys, Japanese (日本, あ), Cangjie
(日) and Korean composition give the same events as before. A test fails if
a re-vendored winit loses the patch.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* macOS: the first Apple Korean key in a fresh PdfCraft composes (winit#4744)
On the first key after PdfCraft starts, Apple Korean 2-Set can answer
interpretKeyEvents with insertText and the raw compatibility jamo instead of
setMarkedText. winit sends that key to the app as an ordinary key, and the
next keys compose without it: g k s Space typed ㅎㅏㄴ instead of 한 (Search,
form fields, comments; rust-windowing/winit#3095).
vendor/winit now ports rust-windowing/winit#4744: keyDown interprets that
one key a second time, synchronously, before any KeyboardInput is sent, and
only when every condition holds: Apple Korean, IME allowed and not yet
composing, no pending commit or marked text, no Command/Control/Option, not
a repeat, the key window's first responder, the only callback is one
insertText of that same single jamo, and focus and input source unchanged
afterwards. The decision is ImeStartup (ime_startup.rs, copied with its
seven tests); no timer, no key queue, no text rewriting. It sits on top of
the previous macOS IME change (pending_commit, in_key_down); the retry runs
inside in_key_down. Command maps to ModifiersState::SUPER in winit 0.30
(META on winit master).
Checked with the real input method on macOS 14.7.1 (scratch winit app, fresh
process each time): without the patch g k s Space gave ㅎ as a key, then
ㅏ and ㄴ committed separately, 3 runs out of 3; with it, ㅎ → 하 → 한 → 한
committed, 3 out of 3. All other traces (Japanese, Cangjie, ABC, held
keys, later Korean keys) are unchanged. ImeStartup's tests pass with
rustc --test. A test fails if a re-vendored winit loses the patch.
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>
* macOS: IME text committed after the marked text is cleared isn't lost (winit#4650)
winit 0.30.13 reports insertText as Ime::Commit only while marked text is
left, and after any commit it swallows the next key. Real paths where that
loses text:
- input methods that end a composition with unmarkText, then insertText
(rust-windowing/winit#3925): the committed text was dropped;
- Apple Korean 2-Set, for the key typed after a syllable: on 한 then 5 it
calls setMarkedText("한"), insertText("한"), setMarkedText(""),
insertText("5"), so the 5 (or ;, or any key that ends the syllable) was
dropped;
- a commit made outside a key press (a candidate clicked with the mouse,
dictation): no key follows to reset the state, so the next key (for
example Backspace) was swallowed.
vendor/winit now commits when a composition was started (pending_commit,
set in setMarkedText, cleared by the commit and when IME is disallowed);
other insertText calls still reach the app as KeyboardInput. A commit
outside keyDown clears the marked text and returns to the ground state.
validAttributesForMarkedText lists the underline and marked-clause
attributes, which some input methods need before they compose.
This is rust-windowing/winit#4650, the 0.30 backport of #4651 (merged on
winit master), plus the part of #4748 that fixes#4651's out-of-key-press
case (#4748's other parts are not included). One correction: upstream
spells the underline attribute "NSUnderlineStyle", but AppKit's
NSUnderlineStyleAttributeName is "NSUnderline" (printed on macOS 14), so
the AppKit value is used.
Checked with the real input methods on macOS 14.7.1 (a scratch winit app fed
key events through AppKit): with the patch 한+5 and 한+; commit the 5 and ;,
unmarkText-then-insertText commits, and Backspace after a commit outside a
key press is delivered; ABC typing, held keys, Japanese (日本, あ), Cangjie
(日) and Korean composition give the same events as before. A test fails if
a re-vendored winit loses the patch.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* macOS: Space in Apple Korean with no syllable in progress types one space (winit#4478)
With Apple Korean 2-Set on macOS 14, Space with no syllable in progress
(a second space, or a space after a digit or punctuation) makes AppKit
call setMarkedText(" "), insertText(" "), then setMarkedText(""). winit
reports the insertText as Ime::Commit(" "), but the trailing empty
setMarkedText returns its state to the ground state, so keyDown also sends
the key as KeyboardInput with text " ". egui inserts both, and PdfCraft's
text fields got two spaces (the double space of alacritty/alacritty#8079).
vendor/winit now records that the key being handled committed IME text and
then doesn't also send it as KeyboardInput, unless AppKit forwards it as a
command through doCommandBySelector (Enter after a syllable still commits
the syllable and sends Enter). keyDown consumes the record on every key,
so keys typed after IME is disallowed are unaffected.
rust-windowing/winit#4478 aims at the same double space, but its changes
(clearing the marked text on commit, forwarding a later insertText, and
dropping doCommandBySelector's guard) don't reach this call order, and the
key loss it also fixes is already fixed by the previous change
(pending_commit). This is the narrow form for the call order observed
here; it should go upstream with #4478.
Checked with the real input methods on macOS 14.7.1 (scratch winit app, on top
of the previous change): the duplicate KeyboardInput Space is gone and
nothing else changes (Korean syllables, 한+Enter, 한+5, Japanese, Cangjie,
ABC, held keys, keys after IME is disallowed). A test fails if a
re-vendored winit loses the patch.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* Keep #678's emoji-picker branch: view.rs is main's plus this PR's Space fix (54b3d3c)
---------
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Co-authored-by: Brandon Thomas <echelon@gmail.com>
* macOS: IME text committed after the marked text is cleared isn't lost (winit#4650)
winit 0.30.13 reports insertText as Ime::Commit only while marked text is
left, and after any commit it swallows the next key. Real paths where that
loses text:
- input methods that end a composition with unmarkText, then insertText
(rust-windowing/winit#3925): the committed text was dropped;
- Apple Korean 2-Set, for the key typed after a syllable: on 한 then 5 it
calls setMarkedText("한"), insertText("한"), setMarkedText(""),
insertText("5"), so the 5 (or ;, or any key that ends the syllable) was
dropped;
- a commit made outside a key press (a candidate clicked with the mouse,
dictation): no key follows to reset the state, so the next key (for
example Backspace) was swallowed.
vendor/winit now commits when a composition was started (pending_commit,
set in setMarkedText, cleared by the commit and when IME is disallowed);
other insertText calls still reach the app as KeyboardInput. A commit
outside keyDown clears the marked text and returns to the ground state.
validAttributesForMarkedText lists the underline and marked-clause
attributes, which some input methods need before they compose.
This is rust-windowing/winit#4650, the 0.30 backport of #4651 (merged on
winit master), plus the part of #4748 that fixes#4651's out-of-key-press
case (#4748's other parts are not included). One correction: upstream
spells the underline attribute "NSUnderlineStyle", but AppKit's
NSUnderlineStyleAttributeName is "NSUnderline" (printed on macOS 14), so
the AppKit value is used.
Checked with the real input methods on macOS 14.7.1 (a scratch winit app fed
key events through AppKit): with the patch 한+5 and 한+; commit the 5 and ;,
unmarkText-then-insertText commits, and Backspace after a commit outside a
key press is delivered; ABC typing, held keys, Japanese (日本, あ), Cangjie
(日) and Korean composition give the same events as before. A test fails if
a re-vendored winit loses the patch.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* macOS: emoji from the emoji picker reach text fields (winit#4749)
The macOS emoji picker (Fn/Globe-E, Control-Command-Space) inserts its text
with insertText:replacementRange: outside any key press and without a
composition. winit 0.30.13 reports insertText only as part of a key press or
a composition, so an emoji picked into Search, a form field or a comment
was dropped.
vendor/winit now reports text inserted outside a key press, with IME
allowed and no composition, as Ime::Commit, as rust-windowing/winit#4749
(the 0.30 backport of #4748) does. Builds on the pending_commit and
in_key_down state of the previous macOS IME change.
#4748's selectedRange change, which lets dictation start, is left out:
checked on macOS 14.7.1, a valid selection switches on press-and-hold for
every held key in a winit window, so holding e kept sending e key events
under an accent menu, and the accent picked with 1 arrived as a separate
commit after "eeee…1". Without it, held keys repeat as before.
Checked with a scratch winit app on macOS 14.7.1: an insertText outside a key
press now arrives as Ime::Commit; held keys, ABC, Japanese, Cangjie and
Korean typing give the same events as before. A test fails if a re-vendored
winit loses the patch or gains the selectedRange change.
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>
egui disallows IME when a text field loses focus, for example when the user
clicks the page while composing. winit 0.30.13 then clears its own marked
text but never tells the input method, which keeps the composition and
continues it once IME is allowed again. Checked with the real input methods
on macOS 14.7.1: Korean 하, IME off and on, then r k gave 학, then 하 and 가
(하 committed a second time if the app already kept it); Japanese か, IME
off and on, then s a gave 傘.
vendor/winit now calls NSTextInputContext::discardMarkedText when IME is
disallowed with marked text present, before its existing reset, as
rust-windowing/winit#4745 does on winit master. With the patch the same
sequences give 가 and さ. AppKit may call unmarkText from
discardMarkedText while IME is still enabled, so its Preedit("") comes
before Disabled; the RefCell borrow in the check ends before the call.
Everything else in the scratch app's traces (ABC, Japanese, Cangjie,
Korean) is unchanged. A test fails if a re-vendored winit loses the patch.
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Co-authored-by: Brandon Thomas <echelon@gmail.com>
winit 0.30.13 reports insertText as Ime::Commit only while marked text is
left, and after any commit it swallows the next key. Real paths where that
loses text:
- input methods that end a composition with unmarkText, then insertText
(rust-windowing/winit#3925): the committed text was dropped;
- Apple Korean 2-Set, for the key typed after a syllable: on 한 then 5 it
calls setMarkedText("한"), insertText("한"), setMarkedText(""),
insertText("5"), so the 5 (or ;, or any key that ends the syllable) was
dropped;
- a commit made outside a key press (a candidate clicked with the mouse,
dictation): no key follows to reset the state, so the next key (for
example Backspace) was swallowed.
vendor/winit now commits when a composition was started (pending_commit,
set in setMarkedText, cleared by the commit and when IME is disallowed);
other insertText calls still reach the app as KeyboardInput. A commit
outside keyDown clears the marked text and returns to the ground state.
validAttributesForMarkedText lists the underline and marked-clause
attributes, which some input methods need before they compose.
This is rust-windowing/winit#4650, the 0.30 backport of #4651 (merged on
winit master), plus the part of #4748 that fixes#4651's out-of-key-press
case (#4748's other parts are not included). One correction: upstream
spells the underline attribute "NSUnderlineStyle", but AppKit's
NSUnderlineStyleAttributeName is "NSUnderline" (printed on macOS 14), so
the AppKit value is used.
Checked with the real input methods on macOS 14.7.1 (a scratch winit app fed
key events through AppKit): with the patch 한+5 and 한+; commit the 5 and ;,
unmarkText-then-insertText commits, and Backspace after a commit outside a
key press is delivered; ABC typing, held keys, Japanese (日本, あ), Cangjie
(日) and Korean composition give the same events as before. A test fails if
a re-vendored winit loses the patch.
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Co-authored-by: Brandon Thomas <echelon@gmail.com>
egui calls Window::set_ime_allowed(true) when a text field gains focus and
set_ime_allowed(false) when it loses it. On X11, winit 0.30.13 handles each
change by destroying the window's X input context and creating a new one, and
leaves focusing the new context to the next FocusIn. PdfCraft's window keeps
the focus while the user moves between Search, form fields and the canvas, so
no FocusIn comes and the new context stays unfocused: the input method
(IBus, fcitx5) doesn't follow the field until the window loses and regains
focus.
vendor/winit's Ime::set_ime_allowed now returns create_context's result, and
the event processor, after draining the queued IME requests, focuses the last
context replaced for the active window, once, as
rust-windowing/winit#4727 does on winit master. No public API changes. A
test fails if a re-vendored winit loses the patch.
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Co-authored-by: Brandon Thomas <echelon@gmail.com>
eframe creates PdfCraft's window hidden, restores the saved state
(app.ron, persist_window) and shows the window after the first frame is
drawn. For a window closed maximized, winit 0.30.13 applies the maximized
state with ShowWindow(SW_MAXIMIZE), which shows a hidden window: PdfCraft
appeared unpainted (white), was hidden again, then shown with SW_SHOW and
SW_MAXIMIZE once more. rust-windowing/winit#4586 also shows the window's
focus state going wrong on this path.
vendor/winit now skips the maximize while the window is hidden and shows a
hidden window that is maximized with one SW_MAXIMIZE, as
rust-windowing/winit#4587 (approved upstream) does on winit master. That is
the later, focus-safe form of #4575; #4575's other change (re-sending
SW_MINIMIZE on every flag change while minimized) is not taken. Transitions
between two visible states make the same ShowWindow calls as before. A test
fails if a re-vendored winit loses the patch.
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Co-authored-by: Brandon Thomas <echelon@gmail.com>
When a composition has no target clause, winit 0.30.13 reads the cursor with
ImmGetCompositionStringW(GCS_CURSORPOS) without checking that the
WM_IME_COMPOSITION lParam has GCS_CURSORPOS set. The Microsoft Korean IME
never sets it (lParam 0x6018), so the read gives 0 and winit reports
Ime::Preedit("한", Some((0, 0))). egui 0.36.2 recognises that exact report on
Windows and drops the range, so PdfCraft draws no caret while Korean is being
composed.
vendor/winit now reads GCS_CURSORPOS only when the flag says the IME set it
and otherwise puts the cursor at the end of the composition, as
rust-windowing/winit#4746 does on winit master (Firefox and SDL do the same).
IMEs that report a cursor or a target clause (Japanese, Chinese) are
unchanged. A test fails if a re-vendored winit loses the patch.
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Co-authored-by: Brandon Thomas <echelon@gmail.com>
winit 0.30.13 destroys a seat's zwp_text_input_v3 in
SeatHandler::remove_capability, whichever capability went away (a touch
screen or tablet unplugged, a remote-desktop session ending), and only
new_capability recreates it. A seat that loses a capability without gaining
one leaves PdfCraft without a text input: IME (fcitx5, IBus) stops working
until PdfCraft is restarted.
The text input belongs to the seat, so vendor/winit now destroys it in
remove_seat only, as rust-windowing/winit#4747 does on winit master. A test
fails if a re-vendored winit loses the patch.
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Co-authored-by: Brandon Thomas <echelon@gmail.com>
* 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>
* 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>
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>
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>
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>
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>
* 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>
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>
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>
* 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>
* 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>
* 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>
* 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>
* 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>
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>
* Fix#378: draw on the GPU that drives the primary display on Windows
The low-power preference picked the AMD Ryzen integrated GPU on a desktop
whose monitors all hang off an NVIDIA card. Presenting from the headless
iGPU to a window on another GPU took the AMD display driver down, blacking
out every monitor for two minutes before PdfCraft exited.
Windows now gets the display-aware adapter choice Linux already had:
EnumDisplayDevices (through the safe `winsafe` crate, MIT) says which
adapter drives each desktop display and which is primary; `pick_adapter`
matches that to the wgpu adapters by PCI ids. The GPU driving the primary
display wins, as the built-in panel does on Linux (#8). The log names the
GPU chosen, the first question when a window stays black.
Synthetic tests cover the Windows device-id parser and the primary-display
rule; a live test checks the enumeration on the running machine. Verified
on a Ryzen 7000 + RTX 3090 desktop: the app now starts on the 3090.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01XBMGuLLMhs1h8orSxcJXER
* 631: the winsafe feature comment says user (and kernel)
---------
Co-authored-by: Claude Fable 5.1 <noreply@anthropic.com>
Co-authored-by: Brandon Thomas <echelon@gmail.com>
pdfcraft-cli.exe had no VERSIONINFO, so Explorer's Details tab,
(Get-Item pdfcraft-cli.exe).VersionInfo and inventory tools showed no
version or publisher for it. A build.rs like the app's embeds one with
winresource (already a build dependency of pdfcraft): the same product
name and copyright, its own description and file names, and the package
version. PDFCRAFT_REQUIRE_WINRES=1, which the Windows packaging already
sets, makes a missing resource compiler an error; otherwise it's a
warning, and other targets are unaffected.
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Co-authored-by: Brandon Thomas <echelon@gmail.com>
* Security: pdfcraft-cli ui refuses a control file another user could plant
`pdfcraft-cli ui --control FILE` sent the token and every command, typed
text included, to whatever port FILE named, without checking who owned it.
The README suggested `/tmp/pc.json`; in a shared /tmp another local user
can create that file first, so the CLI talked to their listener.
The CLI now refuses a symbolic link and anything but a regular file, a file
over 64 KiB, and on Unix a file owned by another user or with any group or
other permission bits (the app writes 600). The owner and mode are read from
the opened file, and its device and inode must match the path's, so a swap
after the check is refused too. std has no geteuid and unsafe is forbidden,
so the effective uid is the owner of a file the CLI creates in the temp
folder and removes. The README now uses a per-user path.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* Security: stop the app when --control can't be published
A control file that couldn't be written was only logged, and the app ran
on with a listener nobody could reach. The failure can mean another user
already created a file at a shared path to receive the commands, and a
script started with --control would otherwise drive nothing without being
told. The app now exits with the error, before any document opens, so no
work is lost. The README and the app's usage comment say so.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* CLI: pdfcraft-cli ui refuses a control-file port above 65535
`ui` truncated the control file's port with `as u16`, so 65536 + N reached
port N, and the token and command went to whatever listened there. A port
that doesn't fit is now refused, before connecting.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* CLI: learn the effective uid from rustix, not a temp-file probe
Review on #597: the probe made `pdfcraft-cli ui` depend on a writable temp
folder to judge a control file somewhere else, created and removed a file
on every call, and could leave one behind. rustix's `geteuid` is safe and
can't fail, and rustix is already in Cargo.lock; it is a Unix-only
dependency of the CLI, so Windows builds don't compile it.
A control file of your own now works with TMPDIR pointing at a folder that
doesn't exist; the probe refused it.
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>
* 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>
* Windows: vendor egui-wgpu 0.36.2 as published (#577)
The crate as released on crates.io, plus egui's LICENSE-APACHE and
LICENSE-MIT from the 0.36.2 tag (the published crate leaves them out).
Not used yet: the next commit patches it and adds it to [patch.crates-io].
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* Windows: a window larger than the GPU's texture limit can't close PdfCraft (#577)
egui-wgpu 0.36.2 configures a window's surface at the window's own size.
Beyond the device's max_texture_dimension_2d, Surface::configure fails
validation and wgpu's default error handler panics. eframe configures the
surface before the app is created, so no app code can intercept it
(emilk/egui#8361). #578 raised the device's limit to the adapter's; this
removes the crash for windows larger than even that.
vendor/egui-wgpu: `winit::surface_fit` shrinks both sides by one factor so
neither exceeds the limit (the longer side lands exactly on it), and egui
draws at that factor into the surface. On DX12, PdfCraft's Windows default,
the swapchain is stretched over the window (DXGI_SCALING_STRETCH), so the
window stays complete and lined up with the pointer, only softer; wgpu's GL
backend copies it unscaled into a corner instead. It applies wherever a
surface is sized: install, resize (start-up restore, maximise, DPI change,
fullscreen) and a recreated surface, which starts again from the window's
own size. Depth and MSAA textures follow. A surface logs a warning when
it becomes fitted.
Why only an Outlook launch hit it (read from the code, not observed):
winit creates the window at CW_USEDEFAULT, so Windows places it, and a
launcher that hands ShellExecute its own window gets new windows on its
monitor. winit then converts the restored logical size at that monitor's
scale: 3440 points became 8600 pixels on a 250% panel. eframe's clamp
compares points against the largest monitor, so it let 3440 through.
Tests: surface_fit with the issue's 8600 x 3423 and edge sizes (zero,
u32::MAX, a zero limit, the longer side exactly on the limit); egui-wgpu
resolves to vendor/egui-wgpu and the fit stays wired into the paths that
size a surface.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* Windows: a GPU that can't set up the window falls back to OpenGL (#519)
On a 2015 Intel GPU with no D3D12 driver, wgpu picked its GL adapter
over WARP and Surface::configure failed (GpuWaitTimeout). wgpu's default
error handler panics, and eframe configures the root surface before the
app is created, so the OpenGL retry from #530 never ran.
Vendored egui-wgpu (PdfCraft patch #519): winit::catch_errors runs a
closure inside wgpu error scopes (validation, out of memory, internal)
and returns the error. Painter::add_surface configures each new window's
surface inside it and returns a failure as WgpuError::ConfigureSurface,
so eframe returns Error::Wgpu and main retries with glow. Later
reconfigures are unchanged.
Tests: a real device returns a wgpu validation error from catch_errors
instead of panicking (red with the scopes removed), and add_surface
keeps the wiring (red with it removed). Docs: vendor/README.md patch 2,
docs/development.md "Renderer fallback" and PDFCRAFT_RENDERER, ROADMAP.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* vendor/egui-wgpu: drop the stray .cargo-ok marker
* Tests: the GPU error-scope test skips on a machine without a GPU adapter
---------
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Co-authored-by: Brandon Thomas <echelon@gmail.com>
* Windows: vendor egui-wgpu 0.36.2 as published (#577)
The crate as released on crates.io, plus egui's LICENSE-APACHE and
LICENSE-MIT from the 0.36.2 tag (the published crate leaves them out).
Not used yet: the next commit patches it and adds it to [patch.crates-io].
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* Windows: a window larger than the GPU's texture limit can't close PdfCraft (#577)
egui-wgpu 0.36.2 configures a window's surface at the window's own size.
Beyond the device's max_texture_dimension_2d, Surface::configure fails
validation and wgpu's default error handler panics. eframe configures the
surface before the app is created, so no app code can intercept it
(emilk/egui#8361). #578 raised the device's limit to the adapter's; this
removes the crash for windows larger than even that.
vendor/egui-wgpu: `winit::surface_fit` shrinks both sides by one factor so
neither exceeds the limit (the longer side lands exactly on it), and egui
draws at that factor into the surface. On DX12, PdfCraft's Windows default,
the swapchain is stretched over the window (DXGI_SCALING_STRETCH), so the
window stays complete and lined up with the pointer, only softer; wgpu's GL
backend copies it unscaled into a corner instead. It applies wherever a
surface is sized: install, resize (start-up restore, maximise, DPI change,
fullscreen) and a recreated surface, which starts again from the window's
own size. Depth and MSAA textures follow. A surface logs a warning when
it becomes fitted.
Why only an Outlook launch hit it (read from the code, not observed):
winit creates the window at CW_USEDEFAULT, so Windows places it, and a
launcher that hands ShellExecute its own window gets new windows on its
monitor. winit then converts the restored logical size at that monitor's
scale: 3440 points became 8600 pixels on a 250% panel. eframe's clamp
compares points against the largest monitor, so it let 3440 through.
Tests: surface_fit with the issue's 8600 x 3423 and edge sizes (zero,
u32::MAX, a zero limit, the longer side exactly on the limit); egui-wgpu
resolves to vendor/egui-wgpu and the fit stays wired into the paths that
size a surface.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* vendor/egui-wgpu: drop the stray .cargo-ok marker
---------
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Co-authored-by: Brandon Thomas <echelon@gmail.com>
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>
Limit function construction and calculator work, detect path cycles, and handle input-derived integer and stack errors through the existing optional failure path.
Co-authored-by: OpenAI Codex <codex@openai.com>
Co-authored-by: Brandon Thomas <echelon@gmail.com>
* M5: name line endings in the comment properties combo
The menu showed PDF tokens such as ROpenArrow. It now uses translated labels, and the stored /LE name is unchanged.
* Fix main CI: move the rotated-field render test out of pdfcraft-forms (#591)
#516 added pdfcraft-render as a dev-dependency of pdfcraft-forms for one
rendering test. Both crates are layer 3, so `cargo xtask layers` (run in
CI) rejects the sideways edge and main went red. The test now lives in
crates/engine/tests/form_rotation.rs, where both crates are already
dependencies, and forms drops the dev-dependency. The parity evidence
points at the new location.
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
* ja: 平ら for the Butt line ending
---------
Co-authored-by: Brandon Thomas <echelon@gmail.com>
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
* Refs #519: tell the user when PdfCraft draws without a GPU
When wgpu's adapter is a software one (WARP on Windows, llvmpipe on
Linux), show a notice at launch that PdfCraft may be slow. egui-wgpu
only logged it, so the #519 reporter had to find it in pdfcraft.log.
The OpenGL fallback isn't covered: glow reports its renderer only
through unsafe calls. The string is translated in the ten catalogs that
carry the portable-mode notice, with their ATTRIBUTION.toml hashes.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* zh-hant: the drawing-without-a-GPU notice
---------
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Co-authored-by: Brandon Thomas <echelon@gmail.com>
* i18n: complete the Japanese catalog and check it like the other languages
Japanese lacked 105 strings that other languages already have: the
open-link and open-email confirmations, DPI settings, the script sandbox
note, XFA form notes, signing, table and page-mode settings, and the
Contributors / Models statistics. Each now has a Japanese row, so the
interface no longer falls back to English for them.
`japanese_covers_ui_literals` joins the French, German, Russian and
Simplified Chinese checks: every `tl!("…")` literal in the UI source needs
a Japanese row. ATTRIBUTION.toml carries the catalog's new SHA-256.
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
* ja: the Fill & Sign flatten setting main gained meanwhile
---------
Co-authored-by: Claude Sonnet 5.5 <noreply@anthropic.com>
Co-authored-by: Brandon Thomas <echelon@gmail.com>
Add the 47 labels and messages that had no French translation (redaction
marks, undo/redo labels, file-dialog and script warnings, SHA digests,
contextual Start/Subject/Edit/Type, ...). Written from the plain meaning of
the English text, clean-room, following the existing fr.tsv vocabulary
(vouvoiement, "biffer" for redact).
Update the fr.tsv SHA-256 in ATTRIBUTION.toml and add a credit entry for
the contributor in contributors/people.toml.
Co-authored-by: Brandon Thomas <echelon@gmail.com>
* M9: more signature validation algorithms
Validation now reads what real signers write, and says when it is weak:
- Digests SHA-224, SHA-512/224, SHA-512/256, SHA3-224/256/384/512 and
RIPEMD-160, beside SHA-1 and SHA-2.
- RSA PKCS#1 v1.5 for every digest, in `rsa_pad`: the one expected encoding
is built and compared byte for byte, with or without the DigestInfo's NULL
(RFC 8017 App. B.1). A signature over the bare digest, which doesn't say
which hash it uses, is refused. The signature value must be below the
modulus (RFC 8017 §5.2.2); s and s + n both verified before.
- RSA-PSS with the hash, MGF1 hash and salt length the signature declares.
- ECDSA as DER, non-minimal DER or raw r||s on P-256, P-384, P-521 and
brainpoolP256r1/P384r1/P512r1 (BSI "plain" ECDSA OIDs too); brainpoolP512r1
is hand-written on crypto-bigint because no RustCrypto crate has it.
- Ed25519 (RFC 8419), and the legacy adbe.x509.rsa_sha1 format.
- An algorithm PdfCraft can't check is Unknown, never Invalid.
- SHA-1 and RIPEMD-160 signatures validate, but the signature's details say
the algorithm is weak (wording pinned by tests).
- Encoding irregularities that were tolerated are listed in the details
(`verify_noting`; `verify` keeps its signature).
Found while testing, each with a regression test that failed before the fix:
a brainpoolP512r1 public key with a coordinate >= p panicked inside
crypto-bigint's `sub_mod` (the certificate comes from the PDF); it is refused
now, and the point at infinity is an invalid signature, not an error.
Vectors were made with OpenSSL (tests/data/README.md has the commands).
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
* Update comment to clarify MAC digest restrictions
Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>
* M9: a legacy x509.rsa_sha1 signature whose key the verifier rejects is Invalid; only unsupported keys are "can't check yet"
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
---------
Co-authored-by: Claude Sonnet 5.5 <noreply@anthropic.com>
Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>
Co-authored-by: Brandon Thomas <echelon@gmail.com>