Commit Graph

225 Commits

Author SHA1 Message Date
Brandon Thomas 20842c72e3 Never crash: Type 1 font programs can't hang or overflow the stack (skrifa 0.47)
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>
2026-10-05 10:47:39 -04:00
Brandon Thomas 3870bea079
Merge pull request #45 from storytold/pr/freebsd
FreeBSD: build and test the workspace in a FreeBSD VM
2026-10-05 09:44:57 -04:00
Brandon Thomas 49d2a2040c FreeBSD CI: git safe.directory, 4 vCPUs, pinned vmactions/freebsd-vm
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-05 09:21:23 -04:00
Brandon Thomas 6fafe67653 FreeBSD CI: install git in the VM (xtask's asset test lists tracked files)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-05 08:46:32 -04:00
Brandon Thomas 6e90710e4b Never crash: JBIG2 images can't decode billions of pixels
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>
2026-10-05 08:28:52 -04:00
Brandon Thomas 1e32ac276c
docs: point agents to the shared test corpora (#44)
Real-file test oracles live in storytold/photocraft-corpus, explained in
craftrules standards/test-corpora.md. Never commit large binary fixtures
here; fetch them pinned and sha256-verified.

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-05 08:24:34 -04:00
Brandon Thomas 9147bd946f FreeBSD: build and test the workspace in a FreeBSD VM
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-05 08:23:31 -04:00
Justin John e59c9a793e
Fix text editing layout and preserve source PDF font styles (#36)
* Comments: a text box's rectangle follows its edited text

Editing a text box (or typewriter or callout) rewrote /Contents but
kept the old /Rect: the appearance clipped the new text to the old box
and silently dropped lines that no longer fit, while shorter text left
an oversized box. set_contents now re-measures the text with the same
Helvetica widths as at creation and re-fits the box: the wrap width and
top edge stay, the height fits the wrapped lines. A callout's leader
line keeps its place and re-attaches to the moved box (that geometry
moved from set_rect into a shared apply_text_box). A locked box keeps
its text editable, so it re-fits too.

Covered by annot tests (grow and shrink with nothing clipped, callout
leader re-attach, locked box) and a comment_edit round trip through the
automation tools; parity entries cite them.

Co-Authored-By: GLM <noreply@z.ai>

* Polish edit text and images tool

* feat(edit-text): select source font style

* fix(edit-text): preserve detected PDF font styles

* feat(edit-text): support Japanese replacements

* fix(forms): escape non-ASCII format script strings

* Forms: unwrap_or instead of map_or with identity (clippy map_or_identity)

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

---------

Co-authored-by: Brandon Thomas <echelon@gmail.com>
Co-authored-by: GLM <noreply@z.ai>
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-05 07:48:23 -04:00
Brandon Thomas 2f364665b8 Never crash: the fuzz job's eleven hangs finish (parser retries, inline images, Type 3 fan-out)
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>
2026-10-05 07:35:38 -04:00
Brandon Thomas f97684f0d8 Docs: honest parity assessment, gap list and direction (ROADMAP, README, CLAUDE.md)
ROADMAP gains an honest assessment by dimension (features, effort,
foundations, robustness, fidelity, product readiness) and by area, what
'shipped' does and doesn't mean, and a ranked list of where we're lacking
and where we're going. Numbers refreshed (804 features, 49.5% shipped,
effort ≈ 30–35%). README's stale 'What's next' becomes an honest status
summary; CLAUDE.md points every agent session at both sections.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-05 06:19:06 -04:00
Brandon Thomas de9b9e82bc Updates: check only when asked (no check at start)
By the owner's decision there is no update check at start and no in-place
install for the foreseeable future. The startup option, its setting and the
first-frame trigger are gone; Help ▸ Check for updates is the only request.
Settings written by a build that had the option are ignored.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-05 06:07:11 -04:00
Brandon Thomas 2ea5846a16
Merge pull request #33 from storytold/pr/release-0.2.0
Release: PrintCraft v0.2.0
2026-10-05 02:41:04 -04:00
Brandon Thomas df3457ed92 Release: PrintCraft v0.2.0
Bump the workspace version 0.1.1 -> 0.2.0 (cargo xtask version set 0.2.0).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-05 02:40:53 -04:00
Brandon Thomas 85a8652de1 Issue #28: Help ▸ Check for updates, with an opt-in check at start
PrintCraft asks GitHub for the latest release and, when it is newer, offers
its release page to download from. Nothing is downloaded or installed
automatically (installing in place needs signed release artifacts).

- ui-egui: updates module (version comparison, background check, dialog);
  no network code: the desktop app supplies the source, and without one
  (web build, tests) the command opens the releases page.
- desktop: one HTTPS request (ureq over rustls with the OS's trusted roots,
  10 s timeout, 1 MiB cap). Only a page under the releases list is offered.
  No bundled root list: webpki-roots/webpki-root-certs are CDLA-licensed.
- The check at start is opt-in and silent unless there is a newer version.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-05 01:40:27 -04:00
Brandon Thomas 27f7ef96fe Issue #8: prefer the integrated GPU, and the save prompt answers to the keyboard
- On hybrid-graphics laptops wgpu picked the discrete GPU (egui-wgpu defaults
  to HighPerformance). NVIDIA's can lose or corrupt its memory across suspend
  and screen lock, which matches the illegible window in #8. PrintCraft now
  asks for the low-power adapter unless WGPU_POWER_PREF is set; one-GPU
  machines are unaffected, and it saves battery.
- "Save changes?" had no keyboard: Enter saves, Escape cancels, and Don't save
  takes Cmd/Ctrl+D (macOS) or Alt+D / Alt+N (the mnemonics the reporter tried).
  Its keys are read before the canvas (which took Escape), and no other
  shortcut runs underneath it (Cmd+D opened Document properties).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-05 01:17:31 -04:00
Brandon Thomas a5d830bf25 Never crash: predictor rows and stroke widths from the input can't exhaust memory
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>
2026-10-05 01:06:45 -04:00
Brandon Thomas 6058068266 Never crash: absurd image-mask and CCITT sizes are refused before decoding
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>
2026-10-05 00:10:17 -04:00
Brandon Thomas 1449b71a19 Never crash: bound the allocations behind the nightly fuzz runner's out-of-memory deaths
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>
2026-10-04 17:59:25 -04:00
Brandon Thomas 330a4f59cf Fuzz: run the CLI cargo built, refuse to run without it, and cap child memory on Linux
- xtask looked for target/release/printcraft-cli even under CARGO_TARGET_DIR
  (which parallel agents use), and a child that failed to spawn counted as a
  pass: such runs fuzzed nothing and reported 0 crashes. The binary now comes
  from CARGO_TARGET_DIR (gates' corpus check and text oracle too), and fuzz
  stops if it is missing.
- On Linux each child runs under a 4 GiB address-space limit, so an input that
  allocates without bound becomes a crash finding instead of exhausting the
  machine: the nightly fuzz job's runner was killed this way every night,
  losing the findings. Allocation failures are kept as the finding's message.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-04 17:59:25 -04:00
Brandon Thomas 1ca576309e Git: no line-ending conversion for any file, so Windows checkouts keep asset hashes
Windows runners check out with core.autocrlf=true, which turned the SVG icons'
LF into CRLF, so their SHA-256 no longer matched ATTRIBUTION.toml and
xtask's the_real_manifest_passes failed there (hidden until now behind the
perf-test failure, since cargo test stops at the first failing binary).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-04 15:55:57 -04:00
Brandon Thomas 9ecfd0afbc Tests: frame budgets take turns and keep the best of three rounds
The two budget tests ran in parallel, each with its own render workers, and
a single preempted stretch on a shared CI runner pushed the average over the
40 ms budget (41-52 ms on Ubuntu and Windows). They now hold a lock while
measuring, and a round over budget is measured again (up to three rounds):
noise only adds time, so the best round is the UI's own cost and a real
regression still fails every round. The budget is unchanged.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-04 15:55:57 -04:00
Brandon Thomas f5e3255b80
Merge pull request #25 from storytold/pr/never-crash-edit-xfdf
Never crash: edit and xfdf have no expect calls
2026-10-04 14:49:48 -04:00
Brandon Thomas 342593af46
Merge pull request #24 from storytold/pr/never-crash-attrs
Never crash: deny the panic lints in crates that are already clean
2026-10-04 14:49:29 -04:00
Brandon Thomas fb0ea34a01
Merge pull request #23 from storytold/pr/never-crash-create
Never crash: create has no expect calls
2026-10-04 14:49:10 -04:00
Brandon Thomas cec677cd64
Merge pull request #22 from storytold/pr/never-crash-ui
Never crash: UI and app shell have no expect/unreachable
2026-10-04 14:48:48 -04:00
Brandon Thomas b8f40d0c04
Merge pull request #21 from storytold/pr/never-crash-automation
Never crash: the automation (MCP/script) tools have no expect/unreachable
2026-10-04 14:48:27 -04:00
Brandon Thomas 0772e99cde
Merge pull request #20 from storytold/pr/never-crash-engine
Never crash: engine denies the panic lints
2026-10-04 14:48:05 -04:00
Brandon Thomas 66d127bb73
Merge pull request #19 from storytold/pr/never-crash-render
Never crash: render has no expect/unreachable
2026-10-04 14:47:45 -04:00
Brandon Thomas 0133cb7454
Merge pull request #18 from storytold/pr/never-crash-organize
Never crash: organize has no expect/unreachable
2026-10-04 14:47:26 -04:00
Brandon Thomas e373293926
Merge pull request #17 from storytold/pr/never-crash-sign
Never crash: sign has no expect/unreachable
2026-10-04 14:47:06 -04:00
Brandon Thomas 414060a3ff
Merge pull request #16 from storytold/pr/never-crash-small
Never crash: filters, fonts, js, optimize and a11y deny the panic lints
2026-10-04 14:46:43 -04:00
Brandon Thomas 7da7ec6148
Merge pull request #15 from storytold/pr/never-crash-crypt
Never crash: crypt has no panic paths
2026-10-04 14:46:22 -04:00
Brandon Thomas a76263a9a7
Merge pull request #14 from storytold/pr/never-crash-guard
Never crash: last-resort guard around edits, opens, saves and UI commands
2026-10-04 14:46:00 -04:00
Brandon Thomas 5c4988aed6
Merge pull request #13 from storytold/pr/never-crash-js
Never crash: hostile form scripts can't overflow the stack
2026-10-04 14:45:39 -04:00
Brandon Thomas ff8c75f46f
Merge pull request #12 from storytold/pr/never-crash-forms
Never crash: forms scripts can't overflow the stack or the date arithmetic
2026-10-04 14:35:21 -04:00
Brandon Thomas 3dc6a81d31
Merge pull request #11 from storytold/pr/never-crash-steering
Never crash: agent rules, clippy.toml, forbid unsafe
2026-10-04 14:34:59 -04:00
Brandon Thomas ac222e818b Never crash: edit and xfdf have no expect calls
- edit: the substitute font name is a `&str` constant, so there's no `from_utf8(..).expect`.
  Paragraph grouping uses `let … else` / `if let` chains, and the reuse-font path takes the
  metrics with `filter` instead of `expect("checked")`.
- xfdf: a comment key that vanished between listing and lookup is skipped.

Both crates now deny the panic lints. Adds the root clippy.toml (identical to #11) so tests may
still unwrap.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-04 14:23:06 -04:00
storybored379 e7ba974fe1
Edit text: a rewritten paragraph keeps its place in the text object it shares (#7)
* Edit text: a rewritten paragraph keeps its place in the text object it shares

A rewritten paragraph was drawn again just before the BT of its text object. When earlier
paragraphs shared that BT … ET (as in text-only pages), the paragraph moved ahead of them in
the content: reading order changed (screen readers, copy, `pdftotext -raw`) and text_paragraphs
renumbered, so after text_edit paragraph 2 a following text_edit paragraph 1 rewrote the
already edited paragraph instead of the first one. The text object is now split where the
paragraph was, and the second half resumes from the same line matrix and text state.

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

* Edit text: read the text before a paragraph without slicing by input positions (never-panic rule)

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

* Edit text: test a recoloured paragraph in a shared text object (split + q/Q together)

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

---------

Co-authored-by: storybored379 <storybored379@users.noreply.github.com>
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Co-authored-by: Brandon Thomas <echelon@gmail.com>
2026-10-04 14:00:31 -04:00
storybored379 95af23d3a5
Forms: multi-select list values survive XML, CSV and text import (#5)
Export data writes a multi-select list's values into one cell joined by ", " ("A, C"), but
import passed the cell on as a single choice, which the list refused, so the selection was
silently lost on an export → import round trip (the field was only counted as rejected).
A multi-select list now splits such a cell into its options, unless the whole cell is itself
one of them.

Co-authored-by: storybored379 <storybored379@users.noreply.github.com>
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-04 14:00:23 -04:00
Brandon Thomas 59f74ea386 Root clippy.toml: tests may unwrap, expect, panic and index
The same file as the steering PR (#11), so this PR stands on its own. The crate-level
`#![deny(clippy::unwrap_used, …)]` applies to production code only.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-04 13:35:01 -04:00
Brandon Thomas 4e74f9ed6a Root clippy.toml: tests may unwrap, expect, panic and index
The same file as the steering PR (#11), so this PR stands on its own. The crate-level
`#![deny(clippy::unwrap_used, …)]` applies to production code only.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-04 13:35:00 -04:00
Brandon Thomas e96a89c871 Root clippy.toml: tests may unwrap, expect, panic and index
The same file as the steering PR (#11), so this PR stands on its own. The crate-level
`#![deny(clippy::unwrap_used, …)]` applies to production code only.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-04 13:35:00 -04:00
Brandon Thomas f688415504 Root clippy.toml: tests may unwrap, expect, panic and index
The same file as the steering PR (#11), so this PR stands on its own. The crate-level
`#![deny(clippy::unwrap_used, …)]` applies to production code only.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-04 13:35:00 -04:00
Brandon Thomas 0e2f8a2fb3 Root clippy.toml: tests may unwrap, expect, panic and index
The same file as the steering PR (#11), so this PR stands on its own. The crate-level
`#![deny(clippy::unwrap_used, …)]` applies to production code only.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-04 13:35:00 -04:00
Brandon Thomas 6ea62a0f6b Root clippy.toml: tests may unwrap, expect, panic and index
The same file as the steering PR (#11), so this PR stands on its own. The crate-level
`#![deny(clippy::unwrap_used, …)]` applies to production code only.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-04 13:35:00 -04:00
Brandon Thomas dd0eed7425 Root clippy.toml: tests may unwrap, expect, panic and index
The same file as the steering PR (#11), so this PR stands on its own. The crate-level
`#![deny(clippy::unwrap_used, …)]` applies to production code only.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-04 13:35:00 -04:00
Brandon Thomas 926171569e Root clippy.toml: tests may unwrap, expect, panic and index
The same file as the steering PR (#11), so this PR stands on its own. The crate-level
`#![deny(clippy::unwrap_used, …)]` applies to production code only.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-04 13:35:00 -04:00
Brandon Thomas cfe84da351 Root clippy.toml: tests may unwrap, expect, panic and index
The same file as the steering PR (#11), so this PR stands on its own. The crate-level
`#![deny(clippy::unwrap_used, …)]` applies to production code only.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-04 13:35:00 -04:00
Brandon Thomas 9902ff3043 Root clippy.toml: tests may unwrap, expect, panic and index
The same file as the steering PR (#11), so this PR stands on its own. The crate-level
`#![deny(clippy::unwrap_used, …)]` applies to production code only.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-04 13:35:00 -04:00
Brandon Thomas b834c6c063 Root clippy.toml: tests may unwrap, expect, panic and index
The same file as the steering PR (#11), so this PR stands on its own. The crate-level
`#![deny(clippy::unwrap_used, …)]` applies to production code only.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-04 13:34:59 -04:00