`comment_add` converted a note's or attachment's `at` (the icon's top-left
as displayed) as a single point through `PageInfo::view_to_user`, and the
engine's `rect_for` extends the icon square from `at` toward +x/-y in user
space. On an unrotated page the displayed and user-space top-left are the
same corner; under `/Rotate` they are different corners of the square, so
the icon landed one icon width (20 pt) away from the requested point.
`icon_anchor` converts the whole displayed square to user space and takes
its user-space top-left, so the `/Rect` covers the requested square under
any rotation. `PageInfo::view_rect_to_user` is the render-side counterpart
of `view_rect_to_quad`, and `NOTE_SIZE` is re-exported from the engine.
Test: `note_icons_anchor_at_the_requested_corner_on_rotated_pages` adds a
note and an attachment on pages rotated 0/90/180/270 and asserts the
`comment_list` rectangles; before the fix the 90° page reports
[52, 72, 72, 92] for a note asked for at [72, 72].
Co-authored-by: wstock <1394687+wstock@users.noreply.github.com>
Co-authored-by: Claude Fable 5.1 <noreply@anthropic.com>
Co-authored-by: Brandon Thomas <echelon@gmail.com>
An A4 page (595.28×841.89 pt) rendered 595×841 at 72 dpi, where
pdftoppm gives 596×842: the whole-page render passed hayro no size, and
hayro floors the scaled page size. Content touching the right or bottom
edge lost its last partial pixel at every resolution.
- render: new device_pixels(pt, scale) rounds up (ignoring float noise
up to 1/100 px, so Letter at 150 dpi stays 1275 px). The untiled
render passes it to hayro as the width and height, still within
MAX_SIDE. The partial edge pixels are anti-aliased against the white
page, as the clip to the crop box already does.
- ui-egui: the canvas tile grid uses the same helper instead of
round(), so tiled pages get tiles for the partial row and column too.
- No change to vendored hayro.
Regression test: a fractional MediaBox renders 596×842 with partly
covered edge pixels, 2481 px wide at 300 dpi, and a corner tile matches
the whole-page render. It failed (595×841) before the fix.
Co-authored-by: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Co-authored-by: Brandon Thomas <echelon@gmail.com>
hayro-syntax recovered a stream with a wrong /Length by scanning ahead
for the bytes `stream`. An indirect appearance-state dictionary
(`<< /Yes … /Off … >>`, as Quartz writes a checkbox's `/AP /N`) was
read as whatever stream came later in the file. That dictionary has no
`/BBox`, so the appearance was dropped and the widget was skipped.
`stream` now has to be the next token. A wrong `/Length` still
recovers, and the renderer already selects the `/AS` state once `/N`
is a dictionary.
Fixes#180
Co-authored-by: Eri Bastos <eb@fluxmail.xyz>
Co-authored-by: Brandon Thomas <echelon@gmail.com>
The app, crates, binaries, bundle id, packaging, icons, screenshots and docs are now
PdfCraft / pdfcraft (repo storytold/pdfcraft). On first launch the desktop app moves the
PrintCraft settings and crash-recovery folders to the new name, and both apps read settings
saved under the old storage key, so upgrades keep recent files, preferences and unsaved work.
The signing test fixtures keep their PrintCraft subject names (they are binary).
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
XFA forms aren't read yet, and opening one gave no hint: a dynamic XFA form
showed only its "Please wait…" placeholder page, and a static one looked like
an ordinary form though Acrobat shows its XFA data in preference to the
fields that get filled.
- render: DocInfo.xfa (Static / Dynamic) from /AcroForm /XFA; dynamic when
/NeedsRendering is true or there are no fields. Exported for doc_info
(automation and CLI) as "xfa".
- ui: the notice bar explains each case (above the form-fields notice); a
static form keeps its Highlight fields button.
- Tests: tests/xfa.rs (dynamic with and without /NeedsRendering, static,
ordinary forms, a malformed /XFA). Parity notes for form.xfa-static and
form.xfa-dynamic (still planned: this explains XFA, it doesn't support it).
Reported by @Sudachi-dev.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The nightly fuzz job found a Type 1 font that hung hayro: read-fonts 0.39
(through skrifa 0.42) looked ahead after every integer by parsing the next
token, which looked ahead again, recursing through a whole run of numbers.
A long run hung; 20,000 numbers overflowed the stack and aborted the
process. Upstream fixed the tokenizer; the vendored hayro-interpret now uses
skrifa 0.47 (read-fonts 0.44), with its one API change. The finding finishes
in 0.5 s. Synthetic regression test (it aborts with the old skrifa).
Corpus sweep: 0 regressions; text oracle unchanged (median F1 0.980, mean
0.866, every per-file score identical).
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The nightly fuzz job found a JBIG2 symbol dictionary that decoded for
minutes: each bitmap side may be 65535, and up to 65535 symbols are decoded
pixel by pixel. Vendored hayro-jbig2 0.3.0 with two bounds: at most 2^28
pixels per bitmap (also the page bitmap, halftone grid and grayscale image),
and at most 2^26 symbol pixels per dictionary. The finding now finishes in
about a second. Synthetic regression test (a 65535 x 65535 page and region
behind a small /Width and /Height); the corpus sweep shows 0 regressions.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Every hang the nightly fuzz job found (11 findings, 8 source files) now
finishes in 0.1-2 s; each cause has a synthetic regression test that times
out without its fix.
- cos (7 findings, our own parser): a stream whose /Length pointed at
itself, with no endstream, made every parse attempt fail and each attempt
reload the same object: ~2^32 parses. Objects being loaded are a seen-set,
/Length values are memoised per load, and the shifted-header retry only
runs when the header is shifted.
- hayro-syntax (3): deciding where an inline image's data ends re-parsed the
rest of the stream, nesting through every inline image; 50 images ran for
over ten minutes. The look-ahead no longer nests and is bounded (256).
- hayro-interpret (1): a Type 3 glyph showing eight glyphs of its own font,
sixteen deep. Nested paints share a per-page budget (50,000).
The pdf.js corpus sweep matches its baseline (963 ok, same 20 known).
xtask check writes its report under CARGO_TARGET_DIR too.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The second full nightly fuzz run on main found two more:
- hayro-syntax computed a Flate/LZW predictor row as columns × bits in
wrapping arithmetic: /Columns 9223372036854775807 asked for 2^61 bytes and
aborted on any machine. Row widths are now saturating usize (colors × bits
also overflowed u8), a zero width is refused (it divided by zero), and a
row longer than the data is not allocated (empty output, as before).
- hayro stroked /LW 9223372036854775807 as given, and stroke expansion
allocated 10 GB. Widths are clamped to 4 × (canvas width + height) device
pixels, which covers the same pixels.
Both findings now run in 8 MB. Synthetic regression test in render.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The first full nightly fuzz run on main (now that the runner survives) found
two 4 GiB allocations, caught by the 4 GiB child cap:
- an inline stencil mask claiming /W 4294967295 was decoded in hayro-interpret
before the renderer's MAX_IMAGE_PIXELS check could skip it (locally 9.6 GB,
42 s). Vendored patch image_size_ok: images and masks over 2^28 pixels are
not decoded.
- a CCITT image whose /DecodeParms claimed /Columns 4294967295 (with a small
/Width) sized the decoder's line buffers (locally 4.3 GB). Vendored patch
ccitt_size_ok in hayro-syntax: at most 2^20 columns and 2^28 pixels.
Both findings now run in 7 MB. Synthetic regression test in render.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Fuzzed inputs drove one check-one process to 2-6 GB (the 16 GB CI runner
running two of them was killed every night). Three causes, each with a test:
- lopdf (bootstrap inspector) sized PNG predictor rows from /Columns before
reading data: /Columns 4294967295 allocated two 4 GiB rows. Vendored lopdf
0.45.0 refuses rows longer than the data (and uses saturating row widths);
its load-time stream decoding also gets a 256 MiB limit (it had none).
- hayro sized a tiling pattern's cell pixmap from /XStep x scale, bounded only
by u16: a step far beyond /BBox allocated 2 GB. The vendored patch keeps
the cell within 3000 px a side, lowering the resolution instead.
- cos decoded cross-reference streams up to the general 1 GiB limit; they now
stop at what 8,388,607 objects' rows can hold (~200 MB at most), and the
section is reconstructed (logged) instead.
On the nightly's seed (41) the worst captured input fell from 6.2 GB to 6 MB,
and a 300 s fuzz run had no process above 300 MB (before: many at 2-5 GB).
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The text reading order uses `let … else`, `while let` and `get_mut` instead of `expect`, and
named destinations bind their key with an or-pattern instead of `unreachable!`. The crate now
denies the panic lints.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Stable clippy 1.99 denies chunks_exact/chunks_exact_mut with a constant
size (32 sites), which failed the test job on main and every open PR.
Behaviour unchanged; MSRV 1.90 has as_chunks.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>