* Tests: UI tests wait for background work by time, not frame count (#4186)
A frame count is not a time: on a busy runner the worker had not finished
the open, or the page text had not arrived, within 30 frames or 50 hover
rounds. support::step_until (#[must_use]) steps frames until a condition
holds or 30 s pass, sleeping briefly between frames, and says whether it
held; each test asserts on that with its own message.
- control_open_replies_pending_while_the_worker_opens_and_focuses_once_it_has
and mode_tabs_keep_the_hand_over_page_text_scrolled_under_them wait with it,
and so does control.rs call() for its reply.
- support::settle waits for every pending render and fails if the pages are
not rendered within 30 s. It replaces all the render-pending settle loops
(40 to 3000 frames) and the local settle/wait_for_pages wrappers, except
text_select's double-click wait, which is held to 8 frames on purpose to
stay inside egui's multi-click window, and view.rs's loops that already
wait by a deadline.
- wait_until is built on step_until, keeping its diagnostic message.
- tabbing_back_to_the_same_field_then_saving_keeps_the_committed_value already
waited 30 s for its file. It failed because its "same-frame" case shared
its scratch directory with typing_and_saving_in_the_same_frame_saves_the_last_keystrokes,
which runs in the same test process (since ee1ee704, #1045) and deleted
the directory mid-save. forms' and pickers' scratch() now give every call
a directory of its own.
Fixes#4186
* M14: update ctutils to 0.4.5 for RUSTSEC-2026-0385
The dependency-advisory gate rejects ctutils 0.4.2 (non-constant-time
mask generation and u128 comparisons, fixed in 0.4.4). It comes in
through crypto-bigint for the signing crates. Only its version and
checksum change in Cargo.lock.
(cherry picked from commit 1f8811761a)
The release a desktop grab kept from egui was appended to the next input,
so when that input carried the press of a second resize or move the
button ended the frame up and the drag stopped. It now goes before the
input's first primary press; only a release ahead of any press counts as
the kept one, since a quick click's release belongs to its own press.
Found by CodeRabbit in review of #3970.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017rPQFoRmQbw8vWhGgGVrRs
ViewportCommand::BeginResize and StartDrag hand the pointer to the
compositor, which keeps the button's release on Wayland; winit reports
only that the pointer left, and egui doesn't count that as a release. egui
then still thought the primary button held, so the next press on an edge
or the tab strip didn't start a drag until a click inside the window.
window_controls::desktop_grab notes each hand-over, and
release_after_desktop_grab (called first in raw_input_hook) adds the
missed release to the next input, once, unless that input brings one.
Ported from WordCraft 1581c288, confirmed by hand there for repeated edge
resizes on GNOME/Wayland.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Wrwtfrnom6Eg1Jt7xZ7VA8
Two stray `|` separators inside the Test column (before (44) in the
hayro-interpret row, before (22) in the hayro row) made those rows six
cells wide in a five-column table, so the trailing test references fell
outside the rendered table (markdownlint MD056).
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0175FWj4xFhMhSH3UpMU8DSC
Type0Font::new built its FallbackFontQuery from the Type 0 font
dictionary, which has no /FontDescriptor (ISO 32000-2 §9.7.6.1,
Table 119); the descriptor belongs to the descendant CIDFont (§9.7.4.1,
Table 115). Every request for a non-embedded CID font therefore said
not serif, not italic, weight 400, and no resolver could tell a Song or
Mincho font from a Hei or Gothic one, or see /FontWeight 700 (§9.8,
Tables 120, 121).
FallbackFontQuery::with_descriptor takes the name from the font
dictionary and the style from the given descriptor; new() calls it with
the dictionary's own descriptor, so simple fonts are unchanged. The CID
path passes the descendant's descriptor it already reads.
Vendored hayro-interpret patch (58). Tests in
crates/render/tests/it/cid_fallback_query.rs record the request a
font_resolver receives. The CID-fonts parity note now says the Japanese
face also follows the Serif flag and /FontWeight.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0175FWj4xFhMhSH3UpMU8DSC
The print filter read /Subtype with a direct-only lookup, so a standard
annotation whose /Subtype is an indirect reference (ISO 32000-2 §7.3.10)
looked like an unknown type and an Invisible flag dropped it from print
(Table 167 applies only to non-standard types, Table 171). The subtype is now
resolved through the document first.
Also removes a stray cell separator in the hayro-interpret row of
vendor/README.md that pushed the test references for patches (44)-(56) out
of the table.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0175FWj4xFhMhSH3UpMU8DSC
A simple font that a document names but doesn't embed (Arial,Bold, ArialMT, ...) is
drawn with a standard-font stand-in, and each glyph was scaled horizontally to its
/Widths advance, narrower or wider. With 600-unit widths, Helvetica-Bold's i and l
(278 units) were widened 2.16 times into solid blocks, and the line read as a heavy
fixed-pitch face. The advance already places the next glyph (ISO 32000-2 §9.2.4;
Widths, Table 109), so a substituted glyph is still narrowed to a narrower advance
but no longer widened. Exact standard-14 fonts are unchanged.
Vendored hayro-interpret patch 57 (StandardKind::outline_glyph), with tests in
crates/render/tests/it/substitute_widths.rs.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0175FWj4xFhMhSH3UpMU8DSC
An annotation whose Invisible flag (bit 1) is set and whose /Subtype is none
of the standard types of ISO 32000-2 Table 171 is no longer drawn (Table 167)
or printed (Table 167: "do not print it even if the Print flag is set"). A
/Popup annotation's /AP is no longer painted as page content: per §12.5.6.14 a
popup has no appearance stream of its own and is shown by the viewer as a
window for its parent. Printing already left popups out.
Link appearances are painted as before (§12.5.5).
hayro-interpret patch (56), recorded in vendor/README.md.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0175FWj4xFhMhSH3UpMU8DSC
cargo deny refuses crypto-bigint 0.7.5, which pdfcraft-sign uses through
rsa and crypto-primes. 0.7.6 is a semver-compatible patch release with the
fix.
The on-screen drop-down button for combo boxes drawn display-only, the
narrowed text area beside it, and the Print as Image copy without it
(with_printed_field_appearances, printed_display_bytes) are removed.
Open PR #3567 draws the arrow for every combo box in the page view.
Check boxes, radio buttons, push buttons and comb lines are unchanged.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0175FWj4xFhMhSH3UpMU8DSC
A form that leaves its appearances to the viewer had its text and choice
fields drawn for display (#3475), but not its buttons. Now, also for
display only, without changing the file:
- Check boxes and radio buttons with no normal appearance for their
state show their /MK background and border (Table 192), and when on,
the /MK /CA caption as a standard-14 ZapfDingbats character
(12.7.5.2.3), 4 or l by default, in the /DA colour, auto-sized for
0 Tf.
- Push buttons with none show their /MK background, their /BS border
(beveled per Table 168) and their centred caption.
- Comb fields with a border colour draw the MaxLen - 1 lines between
their positions (Table 228), up to 500 cells. This also applies to the
appearances PdfCraft saves for comb fields.
- A combo box drawn for display shows a drop-down button on screen, with
its text kept clear of it. Saved appearances and Print as Image leave
it out (with_printed_field_appearances, printed_display_bytes).
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0175FWj4xFhMhSH3UpMU8DSC
Main took hayro-syntax patches 26 and 27 (xref streams), so the patch
that keeps up to 256 operands per instruction moves to 28 in
vendor/README.md and in every comment that cites it.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0175FWj4xFhMhSH3UpMU8DSC
ISO 32000-2 §8.6.6.5: "DeviceN colour spaces (PDF 1.3) may contain an
arbitrary number of colour components", and scn takes one operand per
component. The content parser (vendored hayro-syntax) held ten operands
per instruction; pushing the eleventh failed, and the failure ended the
whole content stream. A twelve-ink background fill therefore drew
nothing, and nothing after it was drawn either: a blank page, no error.
hayro-syntax patch (26): an instruction keeps up to 256 operands (enough
for the largest DeviceN space the interpreter reads, 255 inks, plus a
pattern name). Past that the oldest are dropped, in order, rather than
the stream ending: operators already read the operands nearest to them,
so this is how stray operands are treated below the cap too.
hayro-interpret patch (55): a Separation or DeviceN tint transform that
can't be read no longer makes the colour space unreadable (its paints
were drawn in DeviceGray from the first tint, white for a full tint);
it keeps the space, and like a transform that fails when evaluated it
paints that colour in the alternate space's initial colour. Those
fallbacks, and a cs/CS colour space that can't be read, were silent:
they are reported once per content stream as
InterpreterWarning::ColorFallback, which pdfcraft-render records as
RenderWarning::ColourFallback and pdfcraft-cli render prints.
Tests, each seen failing with its fix reverted: the report's twelve-ink
page paints black and draws what follows; a four-ink control; 300 stray
operands before rg still draw; a calculator tint transform that pops
too much, and an unknown function type, fall back with one warning and
the rest of the page drawn; hayro-syntax keeps all twelve scn operands
and, past the cap, the latest 256 in order (fails without the reorder).
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0175FWj4xFhMhSH3UpMU8DSC
* chore: resolve SonarCloud maintainability issues
* fix(packaging): use verify_sha256 helper function
* style: cargo fmt
* fix: restore wildcard imports and fix method-reference type errors
Revert the glob-import expansions (they missed SomIndices and left unused
imports), revert char::is_ascii_digit where the iterator yields char by
value, and pass the content slice to prune_doomed_xobjects as &[Vec<u8>].
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* Keep XObject decodability check without binding content in interp
---------
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Co-authored-by: Dylan Taylor <dylan@craftmaker.dev>
Comments that arrive without /AP are drawn from their dictionaries by
appearance::build, which lost three entries:
- /Border [hr vr w [dash]] (Table 166): the dash array made the whole
array unreadable, so the width fell back to 1 and the border was solid.
The width is now element 2 whatever follows, and without /BS the
/Border dash array is drawn. /BS, when present, wins for both (Table
166: "the Border entry is ignored"), so a /BS without /W means 1.
- Dash arrays are drawn only when finite, non-negative, not all zero
(§8.4.3.6) and at most 64 long; anything else draws solid.
- Ink applies its /BS dash pattern (Table 168, Table 182), like the
other stroked shapes.
- A caret with /Sy /P (Table 183) shows a ¶ (Helvetica, WinAnsi 0xB6) to
its right in its colour, inside /Rect: in the room /RD leaves, or by
narrowing the caret. /Sy /None draws as before.
The Helvetica font resource, written out three times, is one helper.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0175FWj4xFhMhSH3UpMU8DSC
* fix(render): bound xref streams, JBIG2 referred-to counts, saved graphics states and /W2 runs
Input bounds and crash fixes in the vendored hayro crates, ported from
the snapshot craft-libs carries (storytold/craft-libs#176, #239, #241):
- hayro-syntax: an xref stream subsection must be backed by its decoded
data. All-zero /W widths and a count longer than the data are refused
before any entry is inserted; a tiny stream with a large /Size or
/Index count inserted that many entries.
- hayro-syntax: a subsection running past object number u32::MAX is
refused; start + i overflowed.
- hayro-syntax: the xref stream type field is read big-endian at its
full /W[0] width, and /W[0] over 8 bytes is refused like /W[1].
- hayro-jbig2: the long-form referred-to segment count is capped at
u16::MAX before its retention bytes are read; the 29-bit count went
straight to Vec::with_capacity (about 2 GiB) and aborted the process.
- hayro-interpret: at most 4096 saved graphics states holding at most
64 MiB together. A q past either limit is counted but not copied, and
its Q still removes the clips added since.
- hayro-interpret: a /W2 run whose first CID is past MAX_CID stops there,
as /W does; a first CID of 4294967295 discarded the whole /W2 table.
Each patch is marked in the source, has a regression test in its crate,
and is listed in vendor/README.md.
* fix(render): a binary usecmap counts against the CMap nesting limit; /W2 checks the CID first
- hayro-cmap: a binary CMap's usecmap passed its own depth to the nested
parse, so a self-referencing binary CMap recursed until the stack
overflowed. It now counts like a text usecmap (ledger hayro-cmap 2).
- hayro-interpret: the /W2 first-CID check runs before the run's metrics
are read, so an incomplete run past MAX_CID no longer discards the rest
of the table (ledger 52).
* fix(render): refuse xref-stream object numbers past i32::MAX
ObjectIdentifier stores an i32; a number between i32::MAX and u32::MAX
was cast to a negative object number (ledger hayro-syntax 26).
Review follow-up on #2195: the last view stored ViewDefaults::zoom_name(),
which rounds 62.5% to 63%. A fixed zoom is now written with its fraction
(with_zoom already reads it back); the fit tokens are unchanged.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0175FWj4xFhMhSH3UpMU8DSC
CodeRabbit on #2195: a file opened while full screen was on entered the presentation
first, and the remembered view then replaced its one fitted page, while leaving full
screen went back to the display from before the restore; and a tab closed (or settings
saved) in full screen remembered full screen's temporary fit and display. The remembered
view is now applied before the presentation starts, and the view remembered is the
normal display (DocView::normal_display: the one presenting replaced) at the page
reached.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Catalan, Finnish, Hindi, Indonesian, Korean, Romanian and Albanian have coverage
tests the new preference failed after rebasing onto main; Norwegian Bokmål and Thai
get it too. AI-assisted, marked as not reviewed by a native speaker. Lao and Central
Kurdish fall back to English for now.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
A file opened again shows the page, zoom and page display it was last
shown with, as Acrobat's "Restore last view settings when reopening
documents" does: remembered by path (the last 100 files) when its tab
closes or settings are saved with it open, applied over the file's own
initial view, on by default and switched off in Preferences ▸ Documents
and view.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
An AP-less stamp whose /Name has no drawing got its placeholder frame in
the annotation's /C colour (/C [1 0 0] drew a red frame), while the doc
comment and parity note promise a neutral grey one. The frame now always
strokes PLACEHOLDER_GREY; the fourteen Table 184 names keep /C.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0175FWj4xFhMhSH3UpMU8DSC
dialog hides Two-sided while Booklet is chosen and prints the booklet two-sided itself
(Using Acrobat X Pro, Print dialog box options: "Acrobat automatically enables duplex
printing, if available, for booklet printing"; later manuals and the current help no
longer say so, and on a current Acrobat the Two-sided control is shown for Size and
hidden for Booklet with the same duplex-capable printer). print::booklet_duplex gives a
booklet's setting: both sides flip on the short edge, the fold of a 2-up sheet turned
sideways; front or back only is one pass of a manual duplex and prints one-sided. The
Print dialog hides Two-sided while Booklet is chosen and sends that setting, leaving the
Two-sided choice as it was for the other layouts (and as #2001 remembers it); doc_print
applies the same rule. This replaces the first version, which rewrote Two-sided to the
short edge when Booklet was chosen.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
A rubber stamp from another program with no normal appearance was not
drawn at all. The display copy now gives it one:
- the fourteen standard names of Table 184 draw that stamp with
PdfCraft's stamp drawing (eight from the palette, six with labels of
their own), fitted to /Rect, in /C or the stamp's colour;
- no /Name draws Draft, the table's default, unless /IT says the stamp
is an image or a snapshot;
- any other name (names match byte for byte, §7.3.5) draws a thin grey
frame with both diagonals, so the stamp stays visible and selectable.
The drawing is display-only, like the other comments without an
appearance: Save writes the file as it was, and build() still declines
these stamps, so restyling and flattening never write it into the file.
A stamp with its own /AP /N is never touched. stamp() now takes the
label and the sign-here flag instead of a StampKind.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0175FWj4xFhMhSH3UpMU8DSC
The calendar under a date field had no way to pick today: from another
month the only way back was the arrows. A Today button now sits under
the days, centred under the grid. It picks today from whichever month
the calendar shows, exactly as a click on today's cell would: written
with the field's own format through format_date, then the same commit
path, so the field's scripts treat it as they treat a clicked day, the
pick is one undoable edit, and the field and its calendar close. A year the field's date can't hold offers
no button. Hovering it shows the full date. One UI string in all 33
catalogues, marked AI-assisted.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0175FWj4xFhMhSH3UpMU8DSC