Commit Graph

510 Commits

Author SHA1 Message Date
Ikki Nishihara cc31d11227
i18n: complete the Japanese catalog (105 missing strings) and check it like fr/de/ru/zh-hans (#587)
* 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>
2026-10-09 18:45:11 -04:00
Alexis 80e6bc9d98
M14: complete the French (fr) catalog (#579)
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>
2026-10-09 18:44:56 -04:00
Thorsten Hirsch 56f85f76df
M9: more signature validation algorithms (SHA-3, RIPEMD-160, Ed25519, P-521, brainpool, PSS, rsa_sha1) (#386)
* 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>
2026-10-09 18:44:38 -04:00
Thorsten Hirsch c957288d3b
M9: read BER signatures (DocuSign, Documenso); classify later changes narrowly (#144)
* M9: read BER signatures; later changes are classified narrowly

Real-world signatures (DocuSign, Telekom QES, Documenso) are BER: indefinite
lengths and segmented OCTET STRINGs, which the strict DER reader turned away
("DER signature wrong"). Behind that, later revisions that add DSS data or
rewrite the XMP were classified as tampering.

- cms: the CMS structure is read with `Tlv::parse_ber` (the reader #453 added
  for PKCS#12); certificates and signed attributes are still verified over
  their own exact bytes, signed attributes with an indefinite length are
  re-encoded as DER first (RFC 5652 §5.4), and the details say the signature
  was BER. High tag numbers are not read.
- pdf: validation data added after signing is a permitted change, but only
  objects reached through /DSS /Certs|CRLs|OCSPs|VRI by the keys the standard
  defines, with the shape of their role, and new or only grown since signing.
  A /DSS entry naming other objects (a page's contents) excuses nothing.
- pdf: only the signed catalog's /Metadata is the document's XMP. A
  /Type /Metadata or /Type /DSS label on any other dictionary or stream, even
  one it already had when signed (a page's Form XObject), makes nothing
  permitted. Negative tests at every DocMDP level.

Six of the new tests fail on the previous code: the BER signature, nested
segments, the store-growth rules, and a dictionary or stream carrying a
/Type /Metadata or /Type /DSS label. The last one let a drawn Form XObject be
rewritten under a Valid signature.

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

* M9: comparing a grown document security store is linear for references and bounded otherwise

only_grew compared arrays element by element, so a crafted /Certs of ~100k entries could hang
validation. Reference arrays are compared as sets; other arrays only up to 10,000 elements.

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

---------

Co-authored-by: Claude Sonnet 5.5 <noreply@anthropic.com>
Co-authored-by: Brandon Thomas <echelon@gmail.com>
2026-10-09 18:43:21 -04:00
Thorsten Hirsch 0079bead14
M9: validate a document timestamp that sits in a signature field (#621)
* M9: validate a document timestamp that sits in a signature field

Documenso (and others) store the document timestamp as an AcroForm signature
field (`Timestamp_1`) whose /V is a /Type /DocTimeStamp dictionary. Field
values only went through the signature validator, which read the RFC 3161
token as a CMS signature, compared its TSTInfo with the document digest and
reported "altered or corrupted since the signature was applied" for an
untouched file.

- A field value with /SubFilter /ETSI.RFC3161 is validated as a timestamp.
- Standalone timestamps are found with /Type /DocTimeStamp (ISO 32000-2
  12.8.5) as well as the /Type /Sig older writers used; our own
  timestamp_document now writes /DocTimeStamp. The discovery cache (#419)
  selects its candidates with the same test, so it sees both.
- The discovery test that lists a signature field holding a timestamp used to
  assert `doc_timestamp == false`, which is the bug above; it asserts true now.
  What that test is about (the field is listed once, not also as a standalone
  timestamp) is unchanged.

Regression tests build the same structure with a test TSA; no third-party
file is needed, and they fail without the fix.

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

* M9: a trusted TSA whose certificate wasn't valid when it stamped leaves a document timestamp Unknown, never Valid

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

---------

Co-authored-by: Claude <noreply@anthropic.com>
Co-authored-by: Brandon Thomas <echelon@gmail.com>
2026-10-09 18:42:37 -04:00
Brandon Thomas d90b3f897f
Refs #103: ship the OCR models in the desktop release packages (#630)
Release builds said "Text recognition isn't installed: its models are
missing" because no packaging script or release job fetched the OCR
models: only `cargo xtask models` does, into the source tree.

- packaging/env.sh gains stage_models DEST, which runs
  `cargo xtask models DEST` (every ATTRIBUTION.toml kind = "model" file,
  SHA-256 verified, with its licence text). No model file is named, so
  the models PRs #433/#572 add ship without packaging changes.
- `cargo xtask models` also writes ATTRIBUTION.txt crediting each model
  (CC-BY-SA-4.0 requires attribution when redistributed).
- macOS: PdfCraft.app/Contents/Resources/models, before signing.
- Windows: models\ beside pdfcraft.exe in the MSI (WiX v5 <Files>
  harvest) and the portable zip; test-msi.ps1 checks the folder.
- Linux deb/rpm/tar.gz/AppImage, Flatpak bundle and FreeBSD tarball:
  share/pdfcraft/models beside bin/. Models::search_dirs now also looks
  in <exe dir>/../share/pdfcraft/models (Models::dirs_beside_exe); a
  test builds every package layout on disk and finds the models in each.
  release.yml and freebsd.yml check the packages contain them.
- The missing-models messages no longer tell end users to run cargo,
  and the 11 catalogs that keyed an old PRINTCRAFT_MODELS wording (or
  the previous one) now key the current English text; ATTRIBUTION
  hashes updated. A test checks the translations match.

The web (wasm) build, which the issue reports, still has no models; this
is the desktop half only.

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-09 18:42:23 -04:00
Thorsten Hirsch 8b04e3cba4
M9: chain building checks CA constraints, key usage, path length and validity (#620)
* M9: chain building checks CA constraints, key usage, path length and validity

build_chain linked certificates by name and signature only. An issuer must
also be allowed to issue: basicConstraints CA:TRUE (an old v1 self-signed
root without constraints still anchors), keyCertSign when there is a key
usage, a pathLenConstraint that allows the CAs below it, and validity at the
signing time (or the trusted timestamp / token time) when it is known.

Without this, an ordinary end-entity certificate of a trusted CA, embedded in
a PDF, could "issue" a certificate for any identity and chain to the trusted
root. The signature details now say why an issuer was refused.

Tests issue certificates in-process and cover each refusal, plus a signed PDF
whose signer chains through an end-entity certificate (Unknown, not Valid).

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01LXAstazhuAFyakty2m93Af

* M9: unreadable keyUsage, pathLen or basicConstraints fail closed; only v1/v2 self-signed certificates count as old roots

- a present keyUsage that doesn't parse allows no usage (it was unrestricted)
- a present pathLenConstraint that doesn't parse allows no CA below (it was unlimited)
- basicConstraints counts as present before it is parsed, so a garbled one can't make a
  certificate look like an old root
- the self-signed-without-basicConstraints exception applies to v1/v2 certificates only
- restores the load_certificates doc comment

Tests build v1 certificates when they have no extensions, as real old roots are.

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

* M9: clippy: slice from a reference in the chain test

---------

Co-authored-by: Claude <noreply@anthropic.com>
Co-authored-by: Brandon Thomas <echelon@gmail.com>
2026-10-09 18:40:12 -04:00
Márk Bartalos b0548e64a7
M14: correct Hungarian Windows Store and saved-signature labels (#595)
* M14: correct Hungarian Windows Store and saved-signature labels

Follow-up to the Magyar catalog: the certificate list keeps the name Windows Store, saved signature and initials labels take the substituted noun, and the squiggly comment type names the underline.

* hu: keep "Windows-tároló" — "Windows store" is the certificate store, not Microsoft's app store

---------

Co-authored-by: Brandon Thomas <echelon@gmail.com>
2026-10-09 18:39:31 -04:00
Dmytro 445673fe1b
M14: clarify Ukrainian wording and restore CI (#584)
* M14: clarify Ukrainian certificate and accessibility wording

Distinguish the certificate holder from the document topic, translate
fixed certificate types, and clarify fingerprints, keyboard access,
table sections and equal field sizes. Validate the complete catalog
with VESUM 6.8.6 and review technical-word exceptions in context.

Fixes #503
Follow-up to #509

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

* M14: restore CI after merged test changes

Keep one byte-identical copy of each appearance regression test instead
of five definitions, preserving all three test bodies and assertions.
Correct MCP resource parity evidence to the existing passing test name.

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

* ROADMAP: drop the CI follow-up sentence (main fixed the duplicated tests and MCP evidence in #605/#608)

---------

Co-authored-by: Codex <noreply@openai.com>
Co-authored-by: Brandon Thomas <echelon@gmail.com>
2026-10-09 18:38:18 -04:00
feraremag2680 c3a717a093
tests: pin the interface language in the harnesses added since #502 (#593)
* tests: pin the interface language in the harnesses added since #502

#502 pinned it in every harness there was. These five arrived after its
branch point, so the suite is red again on a machine whose display
language has a catalog:

- `commands.rs`, Clear Recent Files from the menu (#557).
- `editing.rs`, `session_harness` (#462), which clicks "Don't save".
- `stamps.rs`, the rotated-stamp test (#473), which clicks "OK".
- `ui.rs`, removing one recent file from Home (#557), and the narrow tab
  strip (#416), which reads "Display theme:".

The eight other `PdfCraftApp::new()` without the line never draw the
shell, so `i18n::set_current` is never called and the thread-local stays
on English. They compare saved state or a default, and are left alone —
including the one that asserts on a `notify_tr` toast, which reads
English for the same reason.

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

* tests: pin the interface language in the date-format and Pages-panel harnesses too

---------

Co-authored-by: Fernando Areán <soporte@codebit.es>
Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
Co-authored-by: Brandon Thomas <echelon@gmail.com>
2026-10-09 18:37:24 -04:00
Books e7a15783f3
Export to Word/HTML/RTF: read text and images drawn by form XObjects (fixes #314) (#406)
* Export to Word/HTML/RTF: read text and images drawn by form XObjects (fixes #314)

The office export built its paragraphs from text_blocks and its images from
page_images. Both read only the page's own content streams (they back Edit
text and Edit image, which rewrite those streams), so a page drawn through
`/Fm1 Do`, as many print-to-PDF tools emit, exported as an empty document.

Add read-only reading_blocks and reading_images to pdfcraft-edit. They
follow Do into form XObjects (nested ones too), apply each form's /Matrix,
use its own /Resources (or the caller's when it has none), and start it
from the graphics state at the Do. Depth is capped at MAX_FORM_DEPTH and a
form already being read is skipped, so self-referencing and mutually
recursive forms are read once. export_pages uses them; editing is
unchanged and still offers only the page's own text and images.

* Export: a page's walk through form XObjects has a total visit budget, so forms that fan out can't hang it

* Export: keep the image walk within clippy's argument limit

---------

Co-authored-by: Brandon Thomas <echelon@gmail.com>
2026-10-09 18:22:07 -04:00
Brandon Thomas 72d640f9f0
Fix #155: edit text across content stream pieces (#628)
A page's /Contents array is one content stream in pieces that writers may
split between any two tokens (ISO 32000-2 §7.8.2). Edit text parsed each
piece on its own, so an operator whose operands ended one piece and whose
keyword started the next arrived with no operands: a `[(After) -20 (wards)]`
/ `TJ` split made the line vanish from text_lines, text_paragraphs and
text_edit, and from the Edit text tool.

The joined-parse logic the redaction interpreter used (#153) moves into
pdfcraft-content as `Pieces`: join the pieces, parse them as one stream,
map positions back to pieces, and splice joined byte ranges back into
per-piece bytes (an edit replacing or removing an operator that spans
pieces rewrites every piece it touches; untouched pieces keep their
objects). text_lines now interprets the joined operators, and replace_line
and rewrite_block splice through `Pieces`. Redaction uses the same helper.

Lines and paragraphs are grouped by the piece their first operator starts
in, as before, so pages whose pieces are separate drawings don't merge.

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-09 17:57:02 -04:00
Brandon Thomas 275cbcf323
Refs #169: no form bar for push-button-only documents; Screen is not a comment (#615)
LaTeX `animate` PDFs store each frame as a read-only push button and drive
them from a Screen annotation. PdfCraft showed "This document contains N
interactive form fields." because the bar counted any field, and listed
the Screen annotation in the Comments panel because only Link, Widget and
Popup were filtered out.

- The form-fields bar now needs at least one field that isn't a push
  button (something to fill in).
- Non-markup annotations (ISO 32000-2 §12.5.6: Screen, Movie, RichMedia,
  3D, PrinterMark, TrapNet, Watermark) are no longer comments, both when
  a document is inspected and when the comment list is rebuilt after an
  edit (new pdfcraft_annot::is_comment_subtype).

Playing the animation itself (page actions, timers) is out of scope here.

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-09 17:49:54 -04:00
Brandon Thomas 90bb2f14ad
Refs #142: decode outline titles with BOMs and raw UTF-8 consistently (#614)
The bookmarks panel decoded /Title through lopdf::decode_text_string,
which keeps a PDF 2.0 UTF-8 BOM as a visible U+FEFF, drops the whole
title when the bytes after a UTF-8 BOM are not valid UTF-8, and reads
BOM-less raw UTF-8 (written by some producers) as PDFDocEncoding
mojibake. Outline editing in pdfcraft-organize used a second decoder,
PdfString::to_text, so the two could disagree.

PdfString::to_text now decodes UTF-8 after a BOM lossily, reads
BOM-less strings that are valid non-ASCII UTF-8 as UTF-8, and strips
stray leading U+FEFF. The inspector's text strings (titles, name-tree
keys, field values) go through the same function.

The reporter's file decodes correctly on main, so this is hardening of
the decoder rather than a reproduction of that file.

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-09 16:52:07 -04:00
Brandon Thomas d483d6d475
Fix #423: publish a standalone Linux CLI archive (#613)
On Linux, pdfcraft-cli shipped only inside the desktop packages (about
55 MB), while macOS already publishes pdfcraft-cli-<v>-macos-universal.zip.
Servers, CI and agent sandboxes only need the headless CLI.

packaging/linux/package.sh gains a `cli` format, on by default, that writes
pdfcraft-cli-<v>-linux-<arch>.tar.gz: the stripped pdfcraft-cli from the
shared staging tree plus the licences/README via copy_docs. copy_docs now
also copies NOTICE (every package benefits). The release job already
uploads and checksums every file in dist/release; its package check now
lists the archive and runs the extracted binary. README and
docs/releasing.md list the new artifact and state the glibc 2.35 baseline.

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-09 16:45:13 -04:00
Brandon Thomas efb3439cb9
Parity: cite the MCP resources test by its merged name (#608)
#451 renamed conventions_resources_and_modern_results to
conventions_resources before merging, but the misc.mcp-resources evidence
kept the old name, so `cargo xtask parity` failed on main.

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-09 15:45:38 -04:00
Brandon Thomas f34ee9388c
Fix main CI: drop four duplicate copies of the forms redraw tests (#605)
Four squash merges in a row (#450, #414, #451, #351) each re-added the
same 206-line block from #417 to crates/forms/src/tests.rs, so the crate's
tests no longer compiled (E0428: three test functions defined five times)
and main went red on every platform. Keep the first copy. The same merges
repeated one ROADMAP log line four times; keep one.

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-09 15:42:10 -04:00
Tanish Manku cecbfad0ae
D2, D3: fuzz every tool's arguments, and prove root confinement (#351)
* XFA: choice lists, signature, password, image and barcode fields; PNG and GIF pictures

The xfa crate lays out the field kinds that were left blank or shown as
text fields:

- Choice lists become combo boxes (editable with textEntry) or list
  boxes (open="always"; multi-select with open="multiSelect"), with the
  shown items and the saved values of <items save="1"> as /Opt pairs,
  /V, /I and /DV as saved values, several on separate lines in the
  data. Field::saved_value maps shown text to saved values for the
  layout and the script tree alike, so scripts read what is saved.
- Password fields are password text fields; signature fields are /Sig
  fields the app can sign.
- Image fields show the picture the data (base64) or the template
  holds; barcode fields show their value as text, with a warning that
  no bars are drawn.
- PNG and GIF pictures (fields and draws) are decoded to Flate image
  XObjects with a soft mask for transparency; JPEGs stay as they were.
  Sizes come from the headers, so layout never decodes; decoding is
  capped at 40 million pixels and failures are warnings, not errors.

Engine: multi-select list values from scripts and data are one per
line. README and parity notes updated (form.xfa-dynamic).

Hardened after an adversarial review pass: picture sizes refused from
the header past 20 million pixels or 65,535 a side, the GIF decoder
given limits (a frame far larger than its screen is refused before
allocation), template pictures read whole up to 32 MB, pictures
decoded once and remembered in the catalog by content hash so
re-layouts reuse them, data pictures that aren't images reported by
field; choice values matched against saved values first, several
values deduplicated and capped, /I ascending and unique, /DV through
the same mapping, <value> children read, the item cap reported; the
reopen path takes another viewer's choice values (several per line);
CMYK JPEGs inverted only with Adobe APP14.

* D5: README for cos and engine, and an honest cos status line

Thirteen crates have no README, although the session protocol tells every session to read the README of the crate it is touching. These are the two biggest: cos is where every hostile file lands first, and engine is the whole API surface a frontend gets.

Each states its layer and the rules that bind it, sketches the API against the real signatures, and ends with what is deliberately not done. The cos README spells out the clone-is-a-snapshot property and why both writers return bytes rather than taking a sink; the engine README spells out the four-step editing model that makes what you see what Save writes.

Also corrects the module doc on cos/src/lib.rs, which still said encryption and object-stream output for full saves were not done. Both shipped: security.rs handles R2-R6 with crypt filters and /EFF, and SaveOptions::object_streams defaults to true.

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

* D2, D3: fuzz every tool's arguments, and prove root confinement

AGENTS.md §3 and §4 treat tool arguments as untrusted, but nothing fed hostile arguments to the tool table: xtask fuzz mutates PDFs only. And tools::check_args validates shape alone (object, no unknown keys, required present), not types or ranges, so a string where an integer belongs or i64::MIN where a page number belongs reaches the handler.

Two tests. The first calls every tool with a zoo of wrong types, numeric extremes, empty and 100,000-character strings, NUL and right-to-left-override names, mistyped and deeply nested containers: 19,299 calls over 132 tools, each required to return Ok or a readable ToolError rather than panic. The second is the D3 audit mechanized: every path argument is tried with .. traversal, absolute, drive and UNC paths, and a planted symbolic link, and must be refused; 451 attempts, all refused.

Reading the code says every handler resolves its own paths and that no tool dispatches another internally, so the PhotoCraft hole does not exist here. A test keeps that true as tools are added; a written finding would not.

Both were proved to fail before passing. An out-of-bounds index in page_rotate is reported as: page_rotate panicked on {"degrees":-1,"doc":1}. Disabling resolve() is reported as 82 leaks naming the tool and path, including doc_save writing through ../ and through the symbolic link.

The first draft of the confinement test did not catch a fully disabled resolve(): almost every tool failed on a non-existent document id before it ever looked at a path, and a file-listing diff cannot see a read. It now opens a real document first so handlers reach their path arguments, and asserts each call is refused rather than only diffing the filesystem.

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

* Preserve alternate appearances and indirect push-button values

Keep rollover, down and custom appearance entries when normal appearances are regenerated, without modifying shared dictionaries. Resolve indirect button caption and layout values. Cover full and incremental saved output, and wait for raster readiness in the existing Linux scroll test.

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

* M13: common MCP tools through the confined automation path

Preserve task tools and compact mode, add complete hints and strict argument-key errors, and catch escaped tool panics.

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

* M13: session resources and version-aware MCP result hints

Keep per-document resources and legacy response shapes; add document/command resources and private cache hints for modern clients.

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

* M7: select and edit grouped Form XObject figures

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

* M14: let neutral Windows locales fall through to display language

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

* pdfcraft-cli --help lists `mcp` only when the build has it

Follow-up to #540: a build without the `mcp` feature (`--no-default-features`) still listed
`pdfcraft-cli mcp` in its usage, and `pdfcraft-cli mcp` then said "unknown command".

USAGE is now put together with `concat!`, and its `mcp` lines come from a macro that is empty
without the feature. In such a build `pdfcraft-cli mcp` says the MCP server was left out instead
of calling it an unknown command.

- tests/help.rs: usage_lists_mcp_only_when_built_with_it (runs in both feature sets) and
  mcp_without_the_feature_says_it_was_left_out (without the feature)

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

* engine README: the execution plan is local-only, so don't link a path that doesn't exist

* Merge land/450 into land/414; XFA pictures are found again by a SHA-256 on their XObject

The image cache was a /PCXfaImages map in the catalog, keyed by DefaultHasher
(not stable across Rust releases) and read back from the file. Each image
XObject now records the SHA-256 of its picture's bytes, and a re-layout finds
the XObjects on the pages it replaces by that hash; nothing goes in the catalog.

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

* Merge land/414 into land/451; keep main's MCP protocol versions, cap command_batch at 1000 steps

The 2026-07-28 revision and its resultType/ttlMs/cacheScope fields are dropped:
main doesn't advertise them and they couldn't be checked against the spec.
command_batch refuses more than 1000 steps. docs/mcp.md says that unknown
argument keys are now refused with -32602 (a behaviour change for clients that
sent extra keys).

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

* Merge land/451 into land/351; escape targets stay in the test's temp dir, and out/image paths are tried

Absolute escape targets are now paths under the test's own temp base instead of
/etc/passwd, C:\Windows\… and a UNC share, so a confinement bug can only touch
files the test made. String 'out' and 'image' properties (and any described as a
file path) are tried too. The backslash traversal runs only on Windows: elsewhere
it is an ordinary file name inside the root.

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

---------

Co-authored-by: Yuvraj Singh <singh.yuvraj31@icloud.com>
Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
Co-authored-by: sanyamsingh001 <314051937+sanyamsingh001@users.noreply.github.com>
Co-authored-by: Codex <codex@openai.com>
Co-authored-by: storybored379 <storybored379@users.noreply.github.com>
Co-authored-by: OpenAI Codex <noreply@openai.com>
Co-authored-by: Flowershangfromthebranches <152056395+Flowershangfromthebranches@users.noreply.github.com>
Co-authored-by: Streetblock <github@streetblock.de>
Co-authored-by: lloyd <lloyd@stackworx.io>
Co-authored-by: Brandon Thomas <echelon@gmail.com>
2026-10-09 14:46:42 -04:00
storybored379 db1b3d065d
MCP: common command tools, session resources and client hints (#451)
* XFA: choice lists, signature, password, image and barcode fields; PNG and GIF pictures

The xfa crate lays out the field kinds that were left blank or shown as
text fields:

- Choice lists become combo boxes (editable with textEntry) or list
  boxes (open="always"; multi-select with open="multiSelect"), with the
  shown items and the saved values of <items save="1"> as /Opt pairs,
  /V, /I and /DV as saved values, several on separate lines in the
  data. Field::saved_value maps shown text to saved values for the
  layout and the script tree alike, so scripts read what is saved.
- Password fields are password text fields; signature fields are /Sig
  fields the app can sign.
- Image fields show the picture the data (base64) or the template
  holds; barcode fields show their value as text, with a warning that
  no bars are drawn.
- PNG and GIF pictures (fields and draws) are decoded to Flate image
  XObjects with a soft mask for transparency; JPEGs stay as they were.
  Sizes come from the headers, so layout never decodes; decoding is
  capped at 40 million pixels and failures are warnings, not errors.

Engine: multi-select list values from scripts and data are one per
line. README and parity notes updated (form.xfa-dynamic).

Hardened after an adversarial review pass: picture sizes refused from
the header past 20 million pixels or 65,535 a side, the GIF decoder
given limits (a frame far larger than its screen is refused before
allocation), template pictures read whole up to 32 MB, pictures
decoded once and remembered in the catalog by content hash so
re-layouts reuse them, data pictures that aren't images reported by
field; choice values matched against saved values first, several
values deduplicated and capped, /I ascending and unique, /DV through
the same mapping, <value> children read, the item cap reported; the
reopen path takes another viewer's choice values (several per line);
CMYK JPEGs inverted only with Adobe APP14.

* D5: README for cos and engine, and an honest cos status line

Thirteen crates have no README, although the session protocol tells every session to read the README of the crate it is touching. These are the two biggest: cos is where every hostile file lands first, and engine is the whole API surface a frontend gets.

Each states its layer and the rules that bind it, sketches the API against the real signatures, and ends with what is deliberately not done. The cos README spells out the clone-is-a-snapshot property and why both writers return bytes rather than taking a sink; the engine README spells out the four-step editing model that makes what you see what Save writes.

Also corrects the module doc on cos/src/lib.rs, which still said encryption and object-stream output for full saves were not done. Both shipped: security.rs handles R2-R6 with crypt filters and /EFF, and SaveOptions::object_streams defaults to true.

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

* Preserve alternate appearances and indirect push-button values

Keep rollover, down and custom appearance entries when normal appearances are regenerated, without modifying shared dictionaries. Resolve indirect button caption and layout values. Cover full and incremental saved output, and wait for raster readiness in the existing Linux scroll test.

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

* M13: common MCP tools through the confined automation path

Preserve task tools and compact mode, add complete hints and strict argument-key errors, and catch escaped tool panics.

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

* M13: session resources and version-aware MCP result hints

Keep per-document resources and legacy response shapes; add document/command resources and private cache hints for modern clients.

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

* M7: select and edit grouped Form XObject figures

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

* M14: let neutral Windows locales fall through to display language

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

* pdfcraft-cli --help lists `mcp` only when the build has it

Follow-up to #540: a build without the `mcp` feature (`--no-default-features`) still listed
`pdfcraft-cli mcp` in its usage, and `pdfcraft-cli mcp` then said "unknown command".

USAGE is now put together with `concat!`, and its `mcp` lines come from a macro that is empty
without the feature. In such a build `pdfcraft-cli mcp` says the MCP server was left out instead
of calling it an unknown command.

- tests/help.rs: usage_lists_mcp_only_when_built_with_it (runs in both feature sets) and
  mcp_without_the_feature_says_it_was_left_out (without the feature)

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

* engine README: the execution plan is local-only, so don't link a path that doesn't exist

* Merge land/450 into land/414; XFA pictures are found again by a SHA-256 on their XObject

The image cache was a /PCXfaImages map in the catalog, keyed by DefaultHasher
(not stable across Rust releases) and read back from the file. Each image
XObject now records the SHA-256 of its picture's bytes, and a re-layout finds
the XObjects on the pages it replaces by that hash; nothing goes in the catalog.

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

* Merge land/414 into land/451; keep main's MCP protocol versions, cap command_batch at 1000 steps

The 2026-07-28 revision and its resultType/ttlMs/cacheScope fields are dropped:
main doesn't advertise them and they couldn't be checked against the spec.
command_batch refuses more than 1000 steps. docs/mcp.md says that unknown
argument keys are now refused with -32602 (a behaviour change for clients that
sent extra keys).

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

---------

Co-authored-by: Yuvraj Singh <singh.yuvraj31@icloud.com>
Co-authored-by: Tanish Manku <tmanku.dev@gmail.com>
Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
Co-authored-by: sanyamsingh001 <314051937+sanyamsingh001@users.noreply.github.com>
Co-authored-by: Codex <codex@openai.com>
Co-authored-by: storybored379 <storybored379@users.noreply.github.com>
Co-authored-by: OpenAI Codex <noreply@openai.com>
Co-authored-by: Flowershangfromthebranches <152056395+Flowershangfromthebranches@users.noreply.github.com>
Co-authored-by: Streetblock <github@streetblock.de>
Co-authored-by: lloyd <lloyd@stackworx.io>
Co-authored-by: Brandon Thomas <echelon@gmail.com>
2026-10-09 14:46:24 -04:00
ysrdevs 1ba8b194b2
XFA: choice lists, signature, password, image and barcode fields; PNG and GIF pictures (#414)
* XFA: choice lists, signature, password, image and barcode fields; PNG and GIF pictures

The xfa crate lays out the field kinds that were left blank or shown as
text fields:

- Choice lists become combo boxes (editable with textEntry) or list
  boxes (open="always"; multi-select with open="multiSelect"), with the
  shown items and the saved values of <items save="1"> as /Opt pairs,
  /V, /I and /DV as saved values, several on separate lines in the
  data. Field::saved_value maps shown text to saved values for the
  layout and the script tree alike, so scripts read what is saved.
- Password fields are password text fields; signature fields are /Sig
  fields the app can sign.
- Image fields show the picture the data (base64) or the template
  holds; barcode fields show their value as text, with a warning that
  no bars are drawn.
- PNG and GIF pictures (fields and draws) are decoded to Flate image
  XObjects with a soft mask for transparency; JPEGs stay as they were.
  Sizes come from the headers, so layout never decodes; decoding is
  capped at 40 million pixels and failures are warnings, not errors.

Engine: multi-select list values from scripts and data are one per
line. README and parity notes updated (form.xfa-dynamic).

Hardened after an adversarial review pass: picture sizes refused from
the header past 20 million pixels or 65,535 a side, the GIF decoder
given limits (a frame far larger than its screen is refused before
allocation), template pictures read whole up to 32 MB, pictures
decoded once and remembered in the catalog by content hash so
re-layouts reuse them, data pictures that aren't images reported by
field; choice values matched against saved values first, several
values deduplicated and capped, /I ascending and unique, /DV through
the same mapping, <value> children read, the item cap reported; the
reopen path takes another viewer's choice values (several per line);
CMYK JPEGs inverted only with Adobe APP14.

* D5: README for cos and engine, and an honest cos status line

Thirteen crates have no README, although the session protocol tells every session to read the README of the crate it is touching. These are the two biggest: cos is where every hostile file lands first, and engine is the whole API surface a frontend gets.

Each states its layer and the rules that bind it, sketches the API against the real signatures, and ends with what is deliberately not done. The cos README spells out the clone-is-a-snapshot property and why both writers return bytes rather than taking a sink; the engine README spells out the four-step editing model that makes what you see what Save writes.

Also corrects the module doc on cos/src/lib.rs, which still said encryption and object-stream output for full saves were not done. Both shipped: security.rs handles R2-R6 with crypt filters and /EFF, and SaveOptions::object_streams defaults to true.

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

* Preserve alternate appearances and indirect push-button values

Keep rollover, down and custom appearance entries when normal appearances are regenerated, without modifying shared dictionaries. Resolve indirect button caption and layout values. Cover full and incremental saved output, and wait for raster readiness in the existing Linux scroll test.

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

* M7: select and edit grouped Form XObject figures

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

* M14: let neutral Windows locales fall through to display language

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

* pdfcraft-cli --help lists `mcp` only when the build has it

Follow-up to #540: a build without the `mcp` feature (`--no-default-features`) still listed
`pdfcraft-cli mcp` in its usage, and `pdfcraft-cli mcp` then said "unknown command".

USAGE is now put together with `concat!`, and its `mcp` lines come from a macro that is empty
without the feature. In such a build `pdfcraft-cli mcp` says the MCP server was left out instead
of calling it an unknown command.

- tests/help.rs: usage_lists_mcp_only_when_built_with_it (runs in both feature sets) and
  mcp_without_the_feature_says_it_was_left_out (without the feature)

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

* engine README: the execution plan is local-only, so don't link a path that doesn't exist

* Merge land/450 into land/414; XFA pictures are found again by a SHA-256 on their XObject

The image cache was a /PCXfaImages map in the catalog, keyed by DefaultHasher
(not stable across Rust releases) and read back from the file. Each image
XObject now records the SHA-256 of its picture's bytes, and a re-layout finds
the XObjects on the pages it replaces by that hash; nothing goes in the catalog.

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

---------

Co-authored-by: Tanish Manku <tmanku.dev@gmail.com>
Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
Co-authored-by: sanyamsingh001 <314051937+sanyamsingh001@users.noreply.github.com>
Co-authored-by: Codex <codex@openai.com>
Co-authored-by: Flowershangfromthebranches <152056395+Flowershangfromthebranches@users.noreply.github.com>
Co-authored-by: Codex <noreply@openai.com>
Co-authored-by: Streetblock <github@streetblock.de>
Co-authored-by: lloyd <lloyd@stackworx.io>
Co-authored-by: Brandon Thomas <echelon@gmail.com>
2026-10-09 14:46:07 -04:00
Flowershangfromthebranches 37076662a7
M7: select and edit grouped PDF figures (Form XObjects) (#450)
* D5: README for cos and engine, and an honest cos status line

Thirteen crates have no README, although the session protocol tells every session to read the README of the crate it is touching. These are the two biggest: cos is where every hostile file lands first, and engine is the whole API surface a frontend gets.

Each states its layer and the rules that bind it, sketches the API against the real signatures, and ends with what is deliberately not done. The cos README spells out the clone-is-a-snapshot property and why both writers return bytes rather than taking a sink; the engine README spells out the four-step editing model that makes what you see what Save writes.

Also corrects the module doc on cos/src/lib.rs, which still said encryption and object-stream output for full saves were not done. Both shipped: security.rs handles R2-R6 with crypt filters and /EFF, and SaveOptions::object_streams defaults to true.

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

* Preserve alternate appearances and indirect push-button values

Keep rollover, down and custom appearance entries when normal appearances are regenerated, without modifying shared dictionaries. Resolve indirect button caption and layout values. Cover full and incremental saved output, and wait for raster readiness in the existing Linux scroll test.

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

* M7: select and edit grouped Form XObject figures

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

* M14: let neutral Windows locales fall through to display language

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

* pdfcraft-cli --help lists `mcp` only when the build has it

Follow-up to #540: a build without the `mcp` feature (`--no-default-features`) still listed
`pdfcraft-cli mcp` in its usage, and `pdfcraft-cli mcp` then said "unknown command".

USAGE is now put together with `concat!`, and its `mcp` lines come from a macro that is empty
without the feature. In such a build `pdfcraft-cli mcp` says the MCP server was left out instead
of calling it an unknown command.

- tests/help.rs: usage_lists_mcp_only_when_built_with_it (runs in both feature sets) and
  mcp_without_the_feature_says_it_was_left_out (without the feature)

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

* engine README: the execution plan is local-only, so don't link a path that doesn't exist

---------

Co-authored-by: Tanish Manku <tmanku.dev@gmail.com>
Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
Co-authored-by: sanyamsingh001 <314051937+sanyamsingh001@users.noreply.github.com>
Co-authored-by: Codex <codex@openai.com>
Co-authored-by: Codex <noreply@openai.com>
Co-authored-by: Streetblock <github@streetblock.de>
Co-authored-by: lloyd <lloyd@stackworx.io>
Co-authored-by: Brandon Thomas <echelon@gmail.com>
2026-10-09 14:45:49 -04:00
sanyamsingh001 9a529a4fbe
Preserve alternate appearances and indirect push-button values on redraw (#417)
* D5: README for cos and engine, and an honest cos status line

Thirteen crates have no README, although the session protocol tells every session to read the README of the crate it is touching. These are the two biggest: cos is where every hostile file lands first, and engine is the whole API surface a frontend gets.

Each states its layer and the rules that bind it, sketches the API against the real signatures, and ends with what is deliberately not done. The cos README spells out the clone-is-a-snapshot property and why both writers return bytes rather than taking a sink; the engine README spells out the four-step editing model that makes what you see what Save writes.

Also corrects the module doc on cos/src/lib.rs, which still said encryption and object-stream output for full saves were not done. Both shipped: security.rs handles R2-R6 with crypt filters and /EFF, and SaveOptions::object_streams defaults to true.

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

* Preserve alternate appearances and indirect push-button values

Keep rollover, down and custom appearance entries when normal appearances are regenerated, without modifying shared dictionaries. Resolve indirect button caption and layout values. Cover full and incremental saved output, and wait for raster readiness in the existing Linux scroll test.

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

* M14: let neutral Windows locales fall through to display language

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

* pdfcraft-cli --help lists `mcp` only when the build has it

Follow-up to #540: a build without the `mcp` feature (`--no-default-features`) still listed
`pdfcraft-cli mcp` in its usage, and `pdfcraft-cli mcp` then said "unknown command".

USAGE is now put together with `concat!`, and its `mcp` lines come from a macro that is empty
without the feature. In such a build `pdfcraft-cli mcp` says the MCP server was left out instead
of calling it an unknown command.

- tests/help.rs: usage_lists_mcp_only_when_built_with_it (runs in both feature sets) and
  mcp_without_the_feature_says_it_was_left_out (without the feature)

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

* engine README: the execution plan is local-only, so don't link a path that doesn't exist

---------

Co-authored-by: Tanish Manku <tmanku.dev@gmail.com>
Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
Co-authored-by: sanyamsingh001 <314051937+sanyamsingh001@users.noreply.github.com>
Co-authored-by: Codex <codex@openai.com>
Co-authored-by: Streetblock <github@streetblock.de>
Co-authored-by: lloyd <lloyd@stackworx.io>
Co-authored-by: Brandon Thomas <echelon@gmail.com>
2026-10-09 14:45:32 -04:00
Tanish Manku 2d40d85450
D5: READMEs for cos and engine (#332)
* D5: README for cos and engine, and an honest cos status line

Thirteen crates have no README, although the session protocol tells every session to read the README of the crate it is touching. These are the two biggest: cos is where every hostile file lands first, and engine is the whole API surface a frontend gets.

Each states its layer and the rules that bind it, sketches the API against the real signatures, and ends with what is deliberately not done. The cos README spells out the clone-is-a-snapshot property and why both writers return bytes rather than taking a sink; the engine README spells out the four-step editing model that makes what you see what Save writes.

Also corrects the module doc on cos/src/lib.rs, which still said encryption and object-stream output for full saves were not done. Both shipped: security.rs handles R2-R6 with crypt filters and /EFF, and SaveOptions::object_streams defaults to true.

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

* M14: let neutral Windows locales fall through to display language

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

* pdfcraft-cli --help lists `mcp` only when the build has it

Follow-up to #540: a build without the `mcp` feature (`--no-default-features`) still listed
`pdfcraft-cli mcp` in its usage, and `pdfcraft-cli mcp` then said "unknown command".

USAGE is now put together with `concat!`, and its `mcp` lines come from a macro that is empty
without the feature. In such a build `pdfcraft-cli mcp` says the MCP server was left out instead
of calling it an unknown command.

- tests/help.rs: usage_lists_mcp_only_when_built_with_it (runs in both feature sets) and
  mcp_without_the_feature_says_it_was_left_out (without the feature)

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

* engine README: the execution plan is local-only, so don't link a path that doesn't exist

---------

Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
Co-authored-by: Streetblock <github@streetblock.de>
Co-authored-by: Codex <codex@openai.com>
Co-authored-by: lloyd <lloyd@stackworx.io>
Co-authored-by: Brandon Thomas <echelon@gmail.com>
2026-10-09 14:44:37 -04:00
Streetblock 025b79b515
M14: honor Windows display language with neutral launcher locales (#567)
* M14: let neutral Windows locales fall through to display language

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

* pdfcraft-cli --help lists `mcp` only when the build has it

Follow-up to #540: a build without the `mcp` feature (`--no-default-features`) still listed
`pdfcraft-cli mcp` in its usage, and `pdfcraft-cli mcp` then said "unknown command".

USAGE is now put together with `concat!`, and its `mcp` lines come from a macro that is empty
without the feature. In such a build `pdfcraft-cli mcp` says the MCP server was left out instead
of calling it an unknown command.

- tests/help.rs: usage_lists_mcp_only_when_built_with_it (runs in both feature sets) and
  mcp_without_the_feature_says_it_was_left_out (without the feature)

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

---------

Co-authored-by: Codex <codex@openai.com>
Co-authored-by: lloyd <lloyd@stackworx.io>
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Co-authored-by: Brandon Thomas <echelon@gmail.com>
2026-10-09 14:44:24 -04:00
LloydNicholson fce60eacb4
pdfcraft-cli --help lists `mcp` only when the build has it (#574)
Follow-up to #540: a build without the `mcp` feature (`--no-default-features`) still listed
`pdfcraft-cli mcp` in its usage, and `pdfcraft-cli mcp` then said "unknown command".

USAGE is now put together with `concat!`, and its `mcp` lines come from a macro that is empty
without the feature. In such a build `pdfcraft-cli mcp` says the MCP server was left out instead
of calling it an unknown command.

- tests/help.rs: usage_lists_mcp_only_when_built_with_it (runs in both feature sets) and
  mcp_without_the_feature_says_it_was_left_out (without the feature)

Co-authored-by: lloyd <lloyd@stackworx.io>
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Co-authored-by: Brandon Thomas <echelon@gmail.com>
2026-10-09 14:44:11 -04:00
skylar taylor-barrick dbd0748177
Reduce large-PDF memory growth (#307) (#419)
* M14: bound large-PDF memory use (#307)

Inspect large documents lazily, share parser/resource metadata, bound full-save
and raster retention, and cache signature candidate discovery safely.

Preserve upstream rendering deduplication, pixel alignment, signature previews,
and organizer insertion behavior, with regression coverage for their interaction.

Validated with 1,336 workspace tests, 193 parser tests, strict workspace Clippy,
30 WASM crate checks, 1x/2x GUI protocols, and same-PDF workflow comparisons.

* cos: drop the writer cache-policy probe and its test-only hooks in production paths

* cos: the temporary reader keeps parsed objects until a count or owned-bytes budget

Since #552 a stream read from the file is a view into the input, so clearing the whole parse
cache after every read saved little memory and cost most of the full-save slowdown (every
object parsed twice, indirect /Length objects re-parsed per stream). Clear only past
OBJECTS entries or STREAM_BYTES of data of the cache's own (decrypted streams, strings).

* cos: the writers refuse a document opened with a stream limit (read-only inspection)

* cos: a full save fails rather than writing null when an object reads differently the second time

* render: parsing is not timed by the watchdog; timing tests with wide margins

A slow first parse (a huge or damaged file) made the whole pool fail every page for its
lifetime. Parsing, shared or private, is now its own untimed stage, as on main before the
shared parser; rendering is timed from when parsing ends. The init-timeout tests become one
deterministic test (a 2 s parse against a 10 ms limit, shared and private). Watchdog tests
use limits and injected delays ~10x apart instead of 25-50 ms against real renders. Also:
saturating retired-worker count, main's stop-flag test runs on private parsers (the first
shared timeout is a private retry), set_queue documents that unlisted results are dropped,
and retire() documents the permanent per-worker-parser fallback.

* render: page numbers in the lazy page tree use checked conversion

* render: the lazy inspector is compared with lopdf on named destinations and object streams too

* ui: on-screen pages always render; pages turned back to stay cached; stable thumbnail tags

- Visible pages and tiles are always admitted and uploaded (the viewport bounds them); the
  byte and count limits decide only how much prefetch fits. A HiDPI two-up view or a 5K
  viewport left pages on 'Rendering...' for good.
- Pages no longer demanded stay cached, nearest the current page first, while the 128 MiB
  allowance and the 24-page count have room: paging back no longer re-renders.
- Thumbnail density follows the demand count rounded up to a power of two, so rows scrolling
  in and out of an organize grid don't change every thumbnail's tag and re-render it.
- A suspended (background) tab drains its pool's finished results instead of holding them.
- Organize drag-and-drop ends on the primary button only.
- Tests: the stale-result test uses valid pixels and checks nothing was uploaded; the
  organizer wheel test runs 40 steps; regressions for the first three fixes.

* vendor: mark the hayro-syntax lock and SegmentList patches and list them as (8) and (9); clippy 1.99 as_chunks in a raster test

* render: clippy 1.99 — contains over iter().any() in a raster test

---------

Co-authored-by: Brandon Thomas <echelon@gmail.com>
2026-10-09 14:38:01 -04:00
Dimitar 75b31fb04d
M10: Print ▸ Properties… opens the printer driver's own settings (#482)
* M5.7: date format and date language for Fill & Sign in the engine

* M5: flatten Fill & Sign marks when saving

Saving can now bake typed text, marks and signatures into the page, from Preferences or doc_save. The option defaults off, so a normal save still leaves them editable, and other comments and form fields stay.

* M4: zoom the Organize page grid

The page grid draws its pages at 50% to 300%: toolbar buttons with a
percentage that resets to 100%, trackpad pinch or Ctrl/⌘ + wheel about
the page under the pointer, and ⌘+ / ⌘− / ⌘0 (which no longer change the
hidden page view's zoom while the grid shows).

Thumbnails are re-rendered sharper in a few steps as the grid grows,
capped so one document's thumbnails stay within 256 MB. The page under
the pointer (or the first one in view) stays where it is across a zoom.

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

* M4: zoomed grid pages are rendered at the size drawn

A zoomed page grid stretched 132 pt thumbnails, and the stepped detail
that was meant to help was capped by a memory budget, so a long document
stayed blurry at any zoom.

Pages drawn larger than their thumbnails now get a render at exactly the
size drawn while they are in view; the thumbnail stands in until it
arrives. Only pages in view keep one, so memory no longer depends on the
page count, and thumbnails go back to their one fixed size.

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

* M5.7: fill_sign_date_format tool; format and language on fill_sign_add

* M5.7: Preferences ▸ Date format for Fill & Sign dates (refs #158)

* M5.7: refuse Fill & Sign dates the PDF text font can't hold

* M5.7: Spanish labels for Preferences ▸ Date format

* M10: Print ▸ Properties… opens the printer driver's own settings

CUPS: the queue's PPD job options (with lpoptions defaults), grouped in the
Print dialog and sent with this job as lp -o key=value; installable options
and what the dialog sets itself are left out. Windows: the driver's Printing
Preferences window through printui (no unsafe FFI), saved as the printer's
per-user defaults. Headless: printer_options tool and doc_print options.

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

* View: the middle button pans instead of drawing with markup tools

egui's drag responses accept every pointer button, so on Windows and macOS a
middle-button drag reached the page tools: with Draw (ink) selected it drew a
stroke, and other tools cropped, selected or moved. Linux was unaffected
because its middle-click auto-scroll already blocks page input.

Outside Linux a middle-button drag that starts in the viewport (document or
Organize Pages) now pans 1:1 while the button is held, with a Grabbing
cursor, and page input is blocked from the press through the release. Linux
keeps its latched auto-scroll.

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

* M3: a short page gone to from the Pages panel stays current

The current page was whichever page showed most of itself. Going to a short
page (a cheque) above a tall one scrolls it to the top, where the tall page
below shows more, so the current page and the Pages panel's highlight moved
to the tall page instead of the one clicked. The page navigated to now stays
current while it is wholly on screen.

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

* M4: page operations from the Pages panel: context menu and drag to reorder

Right-clicking a thumbnail offers Extract pages, Cut, Copy and Paste after, on
the selection or on the page clicked (which becomes the selection), as the
organize grid does. Dragging thumbnails moves the selection, or the page
grabbed, to the gap it is dropped on: a bar marks the gap, the page count
follows the pointer, and the panel scrolls near its edges. The move is one
undoable step; the moved pages stay selected and the current page follows
its page. Documents that forbid assembly keep click-only thumbnails.

Parity: view.thumbnail-context-ops is shipped, with what it still lacks.

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

* Pages panel: only the primary button drags thumbnails; a drag left over after the panel closed or the document changed is dropped

* Fill & Sign dates: German, Bulgarian, Hungarian and Ukrainian month and weekday names, matching the interface languages

* uk: keep the author's wording for the flatten setting

---------

Co-authored-by: KissT <github@kisst.net>
Co-authored-by: Mark Bartalos <bartalos86@gmail.com>
Co-authored-by: samehelhawary <basio20044@gmail.com>
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Co-authored-by: Cody Vogt <Cody.Vogt@caltechsurveys.com>
Co-authored-by: William Max & Co. <info@williammax.com>
Co-authored-by: Brandon Thomas <echelon@gmail.com>
2026-10-09 14:27:02 -04:00
kisst d592dbb9c4
Fill & Sign: date format and date language in Preferences (refs #158) (#505)
* M5.7: date format and date language for Fill & Sign in the engine

* M5: flatten Fill & Sign marks when saving

Saving can now bake typed text, marks and signatures into the page, from Preferences or doc_save. The option defaults off, so a normal save still leaves them editable, and other comments and form fields stay.

* M4: zoom the Organize page grid

The page grid draws its pages at 50% to 300%: toolbar buttons with a
percentage that resets to 100%, trackpad pinch or Ctrl/⌘ + wheel about
the page under the pointer, and ⌘+ / ⌘− / ⌘0 (which no longer change the
hidden page view's zoom while the grid shows).

Thumbnails are re-rendered sharper in a few steps as the grid grows,
capped so one document's thumbnails stay within 256 MB. The page under
the pointer (or the first one in view) stays where it is across a zoom.

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

* M4: zoomed grid pages are rendered at the size drawn

A zoomed page grid stretched 132 pt thumbnails, and the stepped detail
that was meant to help was capped by a memory budget, so a long document
stayed blurry at any zoom.

Pages drawn larger than their thumbnails now get a render at exactly the
size drawn while they are in view; the thumbnail stands in until it
arrives. Only pages in view keep one, so memory no longer depends on the
page count, and thumbnails go back to their one fixed size.

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

* M5.7: fill_sign_date_format tool; format and language on fill_sign_add

* M5.7: Preferences ▸ Date format for Fill & Sign dates (refs #158)

* M5.7: refuse Fill & Sign dates the PDF text font can't hold

* M5.7: Spanish labels for Preferences ▸ Date format

* View: the middle button pans instead of drawing with markup tools

egui's drag responses accept every pointer button, so on Windows and macOS a
middle-button drag reached the page tools: with Draw (ink) selected it drew a
stroke, and other tools cropped, selected or moved. Linux was unaffected
because its middle-click auto-scroll already blocks page input.

Outside Linux a middle-button drag that starts in the viewport (document or
Organize Pages) now pans 1:1 while the button is held, with a Grabbing
cursor, and page input is blocked from the press through the release. Linux
keeps its latched auto-scroll.

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

* M3: a short page gone to from the Pages panel stays current

The current page was whichever page showed most of itself. Going to a short
page (a cheque) above a tall one scrolls it to the top, where the tall page
below shows more, so the current page and the Pages panel's highlight moved
to the tall page instead of the one clicked. The page navigated to now stays
current while it is wholly on screen.

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

* M4: page operations from the Pages panel: context menu and drag to reorder

Right-clicking a thumbnail offers Extract pages, Cut, Copy and Paste after, on
the selection or on the page clicked (which becomes the selection), as the
organize grid does. Dragging thumbnails moves the selection, or the page
grabbed, to the gap it is dropped on: a bar marks the gap, the page count
follows the pointer, and the panel scrolls near its edges. The move is one
undoable step; the moved pages stay selected and the current page follows
its page. Documents that forbid assembly keep click-only thumbnails.

Parity: view.thumbnail-context-ops is shipped, with what it still lacks.

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

* Pages panel: only the primary button drags thumbnails; a drag left over after the panel closed or the document changed is dropped

* Fill & Sign dates: German, Bulgarian, Hungarian and Ukrainian month and weekday names, matching the interface languages

---------

Co-authored-by: Mark Bartalos <bartalos86@gmail.com>
Co-authored-by: samehelhawary <basio20044@gmail.com>
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Co-authored-by: Cody Vogt <Cody.Vogt@caltechsurveys.com>
Co-authored-by: William Max & Co. <info@williammax.com>
Co-authored-by: Brandon Thomas <echelon@gmail.com>
2026-10-09 14:26:29 -04:00
Márk Bartalos 8e3782d5ca
Flatten Fill & Sign marks when saving (#464)
* M5: flatten Fill & Sign marks when saving

Saving can now bake typed text, marks and signatures into the page, from Preferences or doc_save. The option defaults off, so a normal save still leaves them editable, and other comments and form fields stay.

* M4: zoom the Organize page grid

The page grid draws its pages at 50% to 300%: toolbar buttons with a
percentage that resets to 100%, trackpad pinch or Ctrl/⌘ + wheel about
the page under the pointer, and ⌘+ / ⌘− / ⌘0 (which no longer change the
hidden page view's zoom while the grid shows).

Thumbnails are re-rendered sharper in a few steps as the grid grows,
capped so one document's thumbnails stay within 256 MB. The page under
the pointer (or the first one in view) stays where it is across a zoom.

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

* M4: zoomed grid pages are rendered at the size drawn

A zoomed page grid stretched 132 pt thumbnails, and the stepped detail
that was meant to help was capped by a memory budget, so a long document
stayed blurry at any zoom.

Pages drawn larger than their thumbnails now get a render at exactly the
size drawn while they are in view; the thumbnail stands in until it
arrives. Only pages in view keep one, so memory no longer depends on the
page count, and thumbnails go back to their one fixed size.

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

* View: the middle button pans instead of drawing with markup tools

egui's drag responses accept every pointer button, so on Windows and macOS a
middle-button drag reached the page tools: with Draw (ink) selected it drew a
stroke, and other tools cropped, selected or moved. Linux was unaffected
because its middle-click auto-scroll already blocks page input.

Outside Linux a middle-button drag that starts in the viewport (document or
Organize Pages) now pans 1:1 while the button is held, with a Grabbing
cursor, and page input is blocked from the press through the release. Linux
keeps its latched auto-scroll.

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

* M3: a short page gone to from the Pages panel stays current

The current page was whichever page showed most of itself. Going to a short
page (a cheque) above a tall one scrolls it to the top, where the tall page
below shows more, so the current page and the Pages panel's highlight moved
to the tall page instead of the one clicked. The page navigated to now stays
current while it is wholly on screen.

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

* M4: page operations from the Pages panel: context menu and drag to reorder

Right-clicking a thumbnail offers Extract pages, Cut, Copy and Paste after, on
the selection or on the page clicked (which becomes the selection), as the
organize grid does. Dragging thumbnails moves the selection, or the page
grabbed, to the gap it is dropped on: a bar marks the gap, the page count
follows the pointer, and the panel scrolls near its edges. The move is one
undoable step; the moved pages stay selected and the current page follows
its page. Documents that forbid assembly keep click-only thumbnails.

Parity: view.thumbnail-context-ops is shipped, with what it still lacks.

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

* Pages panel: only the primary button drags thumbnails; a drag left over after the panel closed or the document changed is dropped

* M5: translate Fill & Sign flatten labels into Ukrainian

The coverage test failed because the new preference strings had no Ukrainian entries.

* 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>

* uk: the author's wording for the flatten setting

---------

Co-authored-by: samehelhawary <basio20044@gmail.com>
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Co-authored-by: Cody Vogt <Cody.Vogt@caltechsurveys.com>
Co-authored-by: William Max & Co. <info@williammax.com>
Co-authored-by: Brandon Thomas <echelon@gmail.com>
2026-10-09 14:25:08 -04:00
Sameh Elhawary f28cbbcdd9
M4: zoom the Organize page grid (#468)
* M4: zoom the Organize page grid

The page grid draws its pages at 50% to 300%: toolbar buttons with a
percentage that resets to 100%, trackpad pinch or Ctrl/⌘ + wheel about
the page under the pointer, and ⌘+ / ⌘− / ⌘0 (which no longer change the
hidden page view's zoom while the grid shows).

Thumbnails are re-rendered sharper in a few steps as the grid grows,
capped so one document's thumbnails stay within 256 MB. The page under
the pointer (or the first one in view) stays where it is across a zoom.

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

* M4: zoomed grid pages are rendered at the size drawn

A zoomed page grid stretched 132 pt thumbnails, and the stepped detail
that was meant to help was capped by a memory budget, so a long document
stayed blurry at any zoom.

Pages drawn larger than their thumbnails now get a render at exactly the
size drawn while they are in view; the thumbnail stands in until it
arrives. Only pages in view keep one, so memory no longer depends on the
page count, and thumbnails go back to their one fixed size.

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

* View: the middle button pans instead of drawing with markup tools

egui's drag responses accept every pointer button, so on Windows and macOS a
middle-button drag reached the page tools: with Draw (ink) selected it drew a
stroke, and other tools cropped, selected or moved. Linux was unaffected
because its middle-click auto-scroll already blocks page input.

Outside Linux a middle-button drag that starts in the viewport (document or
Organize Pages) now pans 1:1 while the button is held, with a Grabbing
cursor, and page input is blocked from the press through the release. Linux
keeps its latched auto-scroll.

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

* M3: a short page gone to from the Pages panel stays current

The current page was whichever page showed most of itself. Going to a short
page (a cheque) above a tall one scrolls it to the top, where the tall page
below shows more, so the current page and the Pages panel's highlight moved
to the tall page instead of the one clicked. The page navigated to now stays
current while it is wholly on screen.

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

* M4: page operations from the Pages panel: context menu and drag to reorder

Right-clicking a thumbnail offers Extract pages, Cut, Copy and Paste after, on
the selection or on the page clicked (which becomes the selection), as the
organize grid does. Dragging thumbnails moves the selection, or the page
grabbed, to the gap it is dropped on: a bar marks the gap, the page count
follows the pointer, and the panel scrolls near its edges. The move is one
undoable step; the moved pages stay selected and the current page follows
its page. Documents that forbid assembly keep click-only thumbnails.

Parity: view.thumbnail-context-ops is shipped, with what it still lacks.

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

* Pages panel: only the primary button drags thumbnails; a drag left over after the panel closed or the document changed is dropped

---------

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Co-authored-by: Cody Vogt <Cody.Vogt@caltechsurveys.com>
Co-authored-by: William Max & Co. <info@williammax.com>
Co-authored-by: Brandon Thomas <echelon@gmail.com>
2026-10-09 14:24:45 -04:00
Max Abramowitz d1b7e98698
Pages panel: keep a short page current; context menu and drag to reorder (#573)
* View: the middle button pans instead of drawing with markup tools

egui's drag responses accept every pointer button, so on Windows and macOS a
middle-button drag reached the page tools: with Draw (ink) selected it drew a
stroke, and other tools cropped, selected or moved. Linux was unaffected
because its middle-click auto-scroll already blocks page input.

Outside Linux a middle-button drag that starts in the viewport (document or
Organize Pages) now pans 1:1 while the button is held, with a Grabbing
cursor, and page input is blocked from the press through the release. Linux
keeps its latched auto-scroll.

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

* M3: a short page gone to from the Pages panel stays current

The current page was whichever page showed most of itself. Going to a short
page (a cheque) above a tall one scrolls it to the top, where the tall page
below shows more, so the current page and the Pages panel's highlight moved
to the tall page instead of the one clicked. The page navigated to now stays
current while it is wholly on screen.

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

* M4: page operations from the Pages panel: context menu and drag to reorder

Right-clicking a thumbnail offers Extract pages, Cut, Copy and Paste after, on
the selection or on the page clicked (which becomes the selection), as the
organize grid does. Dragging thumbnails moves the selection, or the page
grabbed, to the gap it is dropped on: a bar marks the gap, the page count
follows the pointer, and the panel scrolls near its edges. The move is one
undoable step; the moved pages stay selected and the current page follows
its page. Documents that forbid assembly keep click-only thumbnails.

Parity: view.thumbnail-context-ops is shipped, with what it still lacks.

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

* Pages panel: only the primary button drags thumbnails; a drag left over after the panel closed or the document changed is dropped

---------

Co-authored-by: Cody Vogt <Cody.Vogt@caltechsurveys.com>
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Co-authored-by: William Max & Co. <info@williammax.com>
Co-authored-by: Brandon Thomas <echelon@gmail.com>
2026-10-09 14:24:32 -04:00
codyvogt a48e1a3ffe
View: the middle button pans instead of drawing with markup tools (#565)
egui's drag responses accept every pointer button, so on Windows and macOS a
middle-button drag reached the page tools: with Draw (ink) selected it drew a
stroke, and other tools cropped, selected or moved. Linux was unaffected
because its middle-click auto-scroll already blocks page input.

Outside Linux a middle-button drag that starts in the viewport (document or
Organize Pages) now pans 1:1 while the button is held, with a Grabbing
cursor, and page input is blocked from the press through the release. Linux
keeps its latched auto-scroll.

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Co-authored-by: Brandon Thomas <echelon@gmail.com>
2026-10-09 14:24:18 -04:00
Brandon Thomas 86ec14291c
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>
2026-10-09 14:13:58 -04:00
cyfung1031 a70c2b4938
View: keep earlier tiles on screen while a zoom's new tiles render (#415)
* View: keep earlier tiles on screen while a zoom's new tiles render

Zooming a tiled page (larger than 4096 device pixels) dropped every tile
of the previous scale at once, so each zoom step fell back to the
2048 px backdrop until the new tiles arrived: the page went blurry on
every frame of a pinch. Tiles are now keyed by scale; those of earlier
zooms stay (nearest scale on top, at most 64) and are drawn stretched
under the current ones until those cover the page. Entering the tiled
range also keeps a whole-page raster sharper than the backdrop instead
of replacing it with the backdrop.

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

* hayro-syntax: fix float mantissa overflow on high-precision coordinates (#199)

jsPDF outputs double-precision floating-point coordinates with 16+ decimal
places in /MediaBox. In hayro-syntax, read_inner accumulated float mantissa
digits into u64 using wrapping arithmetic. Integer mantissas exceeding
u64::MAX wrapped modulo 2^64, severely corrupting page dimensions
(e.g., 3874.96 pt became 185.61 pt, rendering as a warped ribbon).

Accumulation now uses checked arithmetic: trailing fractional digits
exceeding u64 capacity are safely ignored without incrementing decimal_shift.
This preserves full IEEE-754 f64 precision while preventing overflow for
numbers with arbitrary numbers of decimal places.

Includes:
- Unit test in hayro-syntax with reported values and a 50-digit fraction.
- Synthetic PDF regression test in pdfcraft-render verifying inspect().
- Vendored patch (6) documentation in vendor/README.md.

Fixes #199

* M2: bound page-label work and output before applying labels

Share bounded PDF Roman and repeated-letter formatters between inspection
and editing. Bound tree traversal, raw and decoded prefixes, individual
labels and aggregate label output, with checked numbering arithmetic.

Stage inspection labels before applying them and retain physical page
numbers with a warning when optional labels cannot be processed. Reject
unsupported label edits before changing the document tree.

Add synthetic limit, cycle, Unicode, numbering and atomic-edit regressions.

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

* Never crash: mesh shadings can't exhaust memory or run for minutes

`cargo xtask fuzz` (with the dash, JBIG2 and decompression fixes in
place) found a page whose Coons patch shading (type 6), a 147-byte
stream with 1-bit coordinates decoded onto +-40000, took the renderer
past 3 GB. hayro-interpret samples mesh shadings (types 4-7) pixel by
pixel over each triangle's device-space bounding box into a map, with
nothing but the u16 cast bounding it: up to 65536 x 65536 entries for
one triangle. A synthetic single patch passed 6 GB in six seconds; ten
thousand triangles that each cover half a 1000 x 1000 page ran for
minutes. Two rounds of review of earlier versions found more: every
patch was cut into 722 triangles (about 150 KB) before anything limited
them; a lattice mesh with `/VerticesPerRow 0` pushed empty rows forever,
and one with 4294967295 read the whole stream into one row; a mesh
painted a thousand times was triangulated a thousand times; and the
first caps cut content without a word.

Vendored hayro-interpret patch (18):
- `ShadingPattern::encode_within(area)`: the vendored hayro renderer
  passes the area it will actually draw (the path's bounding box on the
  canvas, which it already computes) plus a pixel for texels at a
  fractional edge, and triangles are sampled only inside it, so the map
  holds at most that area's pixels.
- `TriangleSampler`: at most 8 visits per pixel of that area plus 4 per
  triangle in it. A triangle's bounding box is about twice its area and a
  sub-pixel triangle's is at most 2 x 2 pixels, so tiling meshes and
  small dense ones (a patch in a few pixels is 722 triangles) still
  shade completely; overlapping ones stop.
- Coons and tensor-product patches are triangulated one at a time.
- Meshes hold at most `MAX_MESH_PATCHES` (2^16; a 200 x 200 gradient
  mesh is 40 thousand) patches and `MAX_MESH_TRIANGLES` (2^20)
  triangles, and a lattice needs two (ISO 32000-2 8.7.4.5.6) to 2^19
  vertices per row.
- A page samples at most `MAX_PAGE_MESH_TRIANGLES` (2^25; the 200 x 200
  mesh is 29 million) mesh triangles in all, reset per page like the
  existing paint budget.
- Each of these cuts logs a warning.
`encode()` keeps its signature; with no area it samples within a fixed
2^24 visits.

Synthetic regression tests in render:
- `mesh_shadings_far_off_the_page_render`: a Coons patch and a triangle
  mesh decoded onto +-40000 render and the rest of the page draws; a
  patch on the page, the part of the huge one that covers it, and a
  patch filling a 4 x 4 pt rectangle as a pattern are all shaded.
- `many_overlapping_mesh_triangles_finish`: 10,000 triangles each
  covering half of a 1000 x 1000 page finish, and are drawn.
- `mesh_shadings_with_many_patches_or_no_rows_finish`: 40,000 patches
  beside the page, and a lattice with `/VerticesPerRow 0`.
Red on main (out of memory, killed at 3.9 GB). Each part is pinned by a
mutation: no visit budget stalls past 60 s; no clip leaves the covering
patch unshaded; no per-triangle allowance leaves the small patch
unshaded; triangulating all patches first runs out of memory (killed at
5.8 GB); no rows check runs out of memory.

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

* Never crash: decompression bombs can't exhaust the renderer's memory

The renderer's parser (vendored hayro-syntax) decoded Flate, LZW and
RunLength streams with no output limit. A 2 MB page whose content
stream inflates to 2 GB, or whose 8 x 8 image does, took `pdfcraft-cli
render` and `text` past 3 GB in under a second, and a few more MB of
file scale it further. On the desktop that is the first-page render of
an emailed PDF, and the allocation-failure abort takes every open
document with it. Our own layers already bound single streams (lopdf's
load-time decoding at 256 MiB, cos at 1 GiB), so `info` stays at a few
MB; the renderer path didn't. Random mutation almost never makes a
high-ratio stream, so `cargo xtask fuzz` doesn't find this; found by
hand while triaging the fuzz run.

Vendored hayro-syntax patch (6):
- `decode_limit` gives each filter an output limit, and Flate (both
  flate2 and the fallback inflater), LZW and RunLength never write past
  it (the buffer can still grow to under twice it):
  `MAX_DECODED_STREAM`, 256 MiB, lopdf's figure; or, for the last filter
  of an image whose size, bits per component and components are known,
  twice its pixel bytes (a PNG predictor tag byte per byte at most),
  capped at 1 GiB. Earlier filters in a chain produce compressed data
  and get the general limit.
- `Stream::decoded_within(max)` for callers that budget.
- A page's `/Contents` array shares one `MAX_DECODED_STREAM` (naming one
  bomb twenty times decoded 5 GB), reserving exactly what it appends.

Vendored hayro-interpret patch (18):
- The content a page holds at once (its contents, and the forms, tiling
  patterns and Type 3 glyph procedures being painted) stays within
  `MAX_PAGE_CONTENT`, 512 MiB, reset per page like the existing paint
  budget. Forms nest fifty deep and each level kept its content while
  the next ran: a form painting itself around a 256 MiB stream would
  have held over 12 GB. Each level gives its bytes back when it is
  dropped, so content painted again and again keeps drawing, and a
  stream without filters (borrowed from the file) costs nothing.
- Type 0 sampled functions read every sample into a 4-byte value and
  then a table entry: 256 MiB of 1-bit samples asked for over 8 GB. They
  now take at most 2^21 table points and 2^23 values (the largest real
  tables, DeviceN tint transforms such as 33^4 points x 3 outputs, are
  1.2 million and 3.6 million), counted with checked arithmetic, and
  read no further than `/Size` asks. `/Size 0` no longer underflows
  building the default `/Encode`.

Three rounds of independent review found the aggregate cases, buffer
doubling past the limit, a chain/predictor shape the first image limit
would have truncated, the Type 0 amplifier, and a first, cumulative
budget that would have dropped legitimately repeated content, and a
first Type 0 cap low enough to refuse real tint transforms; all fixed
here.

Measured in a release build (render; OS peak commit):
- content stream inflating to 2 GB: 389 MB, 0.3 s
- 8 x 8 image inflating to 2 GB: 4 MB, 0.2 s
- /Contents naming it twenty times: 516 MB, 0.3 s
- a form painting itself around it: 644 MB, 0.4 s

Synthetic regression tests in render (flate2 is a new dev-dependency
there, already in the workspace):
- `decompression_bombs_stop_at_the_stream_limit`: 288 MiB bombs through
  flate2, the fallback inflater (a stored block with a wrong length
  check, which flate2 refuses and the fallback accepts), LZW and
  RunLength each decode exactly 256 MiB, and at most 128 bytes as an
  8 x 8 image.
- `page_contents_and_nested_forms_share_one_budget`: three /Contents
  references decode one stream's worth; a self-painting form finishes.
- `content_painted_again_and_again_keeps_drawing`: a 2 MiB form painted
  300 times draws the 300th time.
- `sampled_functions_with_billions_of_samples_are_refused`.
Ten mutations each turn them red: no limit at all; no limit in flate2,
the fallback, LZW, RunLength or for images; an unshared /Contents limit;
no page budget; no refund on drop; no Type 0 sample cap. (Taking a form
out of the budget no longer compiles: its content is the budget guard.)

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

* M2: normalize JPEG2000 samples without overflowing

Use bounded integer sample scaling for 1-32-bit JPX images and reject empty dimensions before chunking. Cover public decoding and continued page rendering with synthetic regressions.

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

* M14: use adapter texture limits for HiDPI surfaces (#577)

Preserve the existing egui-wgpu device descriptor while enabling the
selected adapter's supported 2D texture extent. Cover the observed
8600-pixel Outlook startup surface and downlevel limit preservation.

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

* Never crash: decompression cut at its limit is logged, not silent; vendor README renumbered after #575

---------

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Co-authored-by: philkwek <philkwek03@gmail.com>
Co-authored-by: Streetblock <github@streetblock.de>
Co-authored-by: OpenAI Codex <codex@openai.com>
Co-authored-by: James Locke <336218001+jcl-bit@users.noreply.github.com>
Co-authored-by: msaulc <103976208+msaulc@users.noreply.github.com>
Co-authored-by: Codex <noreply@openai.com>
Co-authored-by: Brandon Thomas <echelon@gmail.com>
2026-10-09 14:12:43 -04:00
Streetblock bd4736d39c
M2: bound page-label traversal and output (#480)
* hayro-syntax: fix float mantissa overflow on high-precision coordinates (#199)

jsPDF outputs double-precision floating-point coordinates with 16+ decimal
places in /MediaBox. In hayro-syntax, read_inner accumulated float mantissa
digits into u64 using wrapping arithmetic. Integer mantissas exceeding
u64::MAX wrapped modulo 2^64, severely corrupting page dimensions
(e.g., 3874.96 pt became 185.61 pt, rendering as a warped ribbon).

Accumulation now uses checked arithmetic: trailing fractional digits
exceeding u64 capacity are safely ignored without incrementing decimal_shift.
This preserves full IEEE-754 f64 precision while preventing overflow for
numbers with arbitrary numbers of decimal places.

Includes:
- Unit test in hayro-syntax with reported values and a 50-digit fraction.
- Synthetic PDF regression test in pdfcraft-render verifying inspect().
- Vendored patch (6) documentation in vendor/README.md.

Fixes #199

* M2: bound page-label work and output before applying labels

Share bounded PDF Roman and repeated-letter formatters between inspection
and editing. Bound tree traversal, raw and decoded prefixes, individual
labels and aggregate label output, with checked numbering arithmetic.

Stage inspection labels before applying them and retain physical page
numbers with a warning when optional labels cannot be processed. Reject
unsupported label edits before changing the document tree.

Add synthetic limit, cycle, Unicode, numbering and atomic-edit regressions.

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

* Never crash: mesh shadings can't exhaust memory or run for minutes

`cargo xtask fuzz` (with the dash, JBIG2 and decompression fixes in
place) found a page whose Coons patch shading (type 6), a 147-byte
stream with 1-bit coordinates decoded onto +-40000, took the renderer
past 3 GB. hayro-interpret samples mesh shadings (types 4-7) pixel by
pixel over each triangle's device-space bounding box into a map, with
nothing but the u16 cast bounding it: up to 65536 x 65536 entries for
one triangle. A synthetic single patch passed 6 GB in six seconds; ten
thousand triangles that each cover half a 1000 x 1000 page ran for
minutes. Two rounds of review of earlier versions found more: every
patch was cut into 722 triangles (about 150 KB) before anything limited
them; a lattice mesh with `/VerticesPerRow 0` pushed empty rows forever,
and one with 4294967295 read the whole stream into one row; a mesh
painted a thousand times was triangulated a thousand times; and the
first caps cut content without a word.

Vendored hayro-interpret patch (18):
- `ShadingPattern::encode_within(area)`: the vendored hayro renderer
  passes the area it will actually draw (the path's bounding box on the
  canvas, which it already computes) plus a pixel for texels at a
  fractional edge, and triangles are sampled only inside it, so the map
  holds at most that area's pixels.
- `TriangleSampler`: at most 8 visits per pixel of that area plus 4 per
  triangle in it. A triangle's bounding box is about twice its area and a
  sub-pixel triangle's is at most 2 x 2 pixels, so tiling meshes and
  small dense ones (a patch in a few pixels is 722 triangles) still
  shade completely; overlapping ones stop.
- Coons and tensor-product patches are triangulated one at a time.
- Meshes hold at most `MAX_MESH_PATCHES` (2^16; a 200 x 200 gradient
  mesh is 40 thousand) patches and `MAX_MESH_TRIANGLES` (2^20)
  triangles, and a lattice needs two (ISO 32000-2 8.7.4.5.6) to 2^19
  vertices per row.
- A page samples at most `MAX_PAGE_MESH_TRIANGLES` (2^25; the 200 x 200
  mesh is 29 million) mesh triangles in all, reset per page like the
  existing paint budget.
- Each of these cuts logs a warning.
`encode()` keeps its signature; with no area it samples within a fixed
2^24 visits.

Synthetic regression tests in render:
- `mesh_shadings_far_off_the_page_render`: a Coons patch and a triangle
  mesh decoded onto +-40000 render and the rest of the page draws; a
  patch on the page, the part of the huge one that covers it, and a
  patch filling a 4 x 4 pt rectangle as a pattern are all shaded.
- `many_overlapping_mesh_triangles_finish`: 10,000 triangles each
  covering half of a 1000 x 1000 page finish, and are drawn.
- `mesh_shadings_with_many_patches_or_no_rows_finish`: 40,000 patches
  beside the page, and a lattice with `/VerticesPerRow 0`.
Red on main (out of memory, killed at 3.9 GB). Each part is pinned by a
mutation: no visit budget stalls past 60 s; no clip leaves the covering
patch unshaded; no per-triangle allowance leaves the small patch
unshaded; triangulating all patches first runs out of memory (killed at
5.8 GB); no rows check runs out of memory.

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

* Never crash: decompression bombs can't exhaust the renderer's memory

The renderer's parser (vendored hayro-syntax) decoded Flate, LZW and
RunLength streams with no output limit. A 2 MB page whose content
stream inflates to 2 GB, or whose 8 x 8 image does, took `pdfcraft-cli
render` and `text` past 3 GB in under a second, and a few more MB of
file scale it further. On the desktop that is the first-page render of
an emailed PDF, and the allocation-failure abort takes every open
document with it. Our own layers already bound single streams (lopdf's
load-time decoding at 256 MiB, cos at 1 GiB), so `info` stays at a few
MB; the renderer path didn't. Random mutation almost never makes a
high-ratio stream, so `cargo xtask fuzz` doesn't find this; found by
hand while triaging the fuzz run.

Vendored hayro-syntax patch (6):
- `decode_limit` gives each filter an output limit, and Flate (both
  flate2 and the fallback inflater), LZW and RunLength never write past
  it (the buffer can still grow to under twice it):
  `MAX_DECODED_STREAM`, 256 MiB, lopdf's figure; or, for the last filter
  of an image whose size, bits per component and components are known,
  twice its pixel bytes (a PNG predictor tag byte per byte at most),
  capped at 1 GiB. Earlier filters in a chain produce compressed data
  and get the general limit.
- `Stream::decoded_within(max)` for callers that budget.
- A page's `/Contents` array shares one `MAX_DECODED_STREAM` (naming one
  bomb twenty times decoded 5 GB), reserving exactly what it appends.

Vendored hayro-interpret patch (18):
- The content a page holds at once (its contents, and the forms, tiling
  patterns and Type 3 glyph procedures being painted) stays within
  `MAX_PAGE_CONTENT`, 512 MiB, reset per page like the existing paint
  budget. Forms nest fifty deep and each level kept its content while
  the next ran: a form painting itself around a 256 MiB stream would
  have held over 12 GB. Each level gives its bytes back when it is
  dropped, so content painted again and again keeps drawing, and a
  stream without filters (borrowed from the file) costs nothing.
- Type 0 sampled functions read every sample into a 4-byte value and
  then a table entry: 256 MiB of 1-bit samples asked for over 8 GB. They
  now take at most 2^21 table points and 2^23 values (the largest real
  tables, DeviceN tint transforms such as 33^4 points x 3 outputs, are
  1.2 million and 3.6 million), counted with checked arithmetic, and
  read no further than `/Size` asks. `/Size 0` no longer underflows
  building the default `/Encode`.

Three rounds of independent review found the aggregate cases, buffer
doubling past the limit, a chain/predictor shape the first image limit
would have truncated, the Type 0 amplifier, and a first, cumulative
budget that would have dropped legitimately repeated content, and a
first Type 0 cap low enough to refuse real tint transforms; all fixed
here.

Measured in a release build (render; OS peak commit):
- content stream inflating to 2 GB: 389 MB, 0.3 s
- 8 x 8 image inflating to 2 GB: 4 MB, 0.2 s
- /Contents naming it twenty times: 516 MB, 0.3 s
- a form painting itself around it: 644 MB, 0.4 s

Synthetic regression tests in render (flate2 is a new dev-dependency
there, already in the workspace):
- `decompression_bombs_stop_at_the_stream_limit`: 288 MiB bombs through
  flate2, the fallback inflater (a stored block with a wrong length
  check, which flate2 refuses and the fallback accepts), LZW and
  RunLength each decode exactly 256 MiB, and at most 128 bytes as an
  8 x 8 image.
- `page_contents_and_nested_forms_share_one_budget`: three /Contents
  references decode one stream's worth; a self-painting form finishes.
- `content_painted_again_and_again_keeps_drawing`: a 2 MiB form painted
  300 times draws the 300th time.
- `sampled_functions_with_billions_of_samples_are_refused`.
Ten mutations each turn them red: no limit at all; no limit in flate2,
the fallback, LZW, RunLength or for images; an unshared /Contents limit;
no page budget; no refund on drop; no Type 0 sample cap. (Taking a form
out of the budget no longer compiles: its content is the budget guard.)

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

* M2: normalize JPEG2000 samples without overflowing

Use bounded integer sample scaling for 1-32-bit JPX images and reject empty dimensions before chunking. Cover public decoding and continued page rendering with synthetic regressions.

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

* Never crash: decompression cut at its limit is logged, not silent; vendor README renumbered after #575

---------

Co-authored-by: philkwek <philkwek03@gmail.com>
Co-authored-by: OpenAI Codex <codex@openai.com>
Co-authored-by: James Locke <336218001+jcl-bit@users.noreply.github.com>
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Co-authored-by: Brandon Thomas <echelon@gmail.com>
2026-10-09 14:12:16 -04:00
Phil Kwek 1399d38ea3
Fix: High-precision float mantissa overflow in hayro-syntax, causing warped display of some PDFs (#199) (#427)
* hayro-syntax: fix float mantissa overflow on high-precision coordinates (#199)

jsPDF outputs double-precision floating-point coordinates with 16+ decimal
places in /MediaBox. In hayro-syntax, read_inner accumulated float mantissa
digits into u64 using wrapping arithmetic. Integer mantissas exceeding
u64::MAX wrapped modulo 2^64, severely corrupting page dimensions
(e.g., 3874.96 pt became 185.61 pt, rendering as a warped ribbon).

Accumulation now uses checked arithmetic: trailing fractional digits
exceeding u64 capacity are safely ignored without incrementing decimal_shift.
This preserves full IEEE-754 f64 precision while preventing overflow for
numbers with arbitrary numbers of decimal places.

Includes:
- Unit test in hayro-syntax with reported values and a 50-digit fraction.
- Synthetic PDF regression test in pdfcraft-render verifying inspect().
- Vendored patch (6) documentation in vendor/README.md.

Fixes #199

* Never crash: mesh shadings can't exhaust memory or run for minutes

`cargo xtask fuzz` (with the dash, JBIG2 and decompression fixes in
place) found a page whose Coons patch shading (type 6), a 147-byte
stream with 1-bit coordinates decoded onto +-40000, took the renderer
past 3 GB. hayro-interpret samples mesh shadings (types 4-7) pixel by
pixel over each triangle's device-space bounding box into a map, with
nothing but the u16 cast bounding it: up to 65536 x 65536 entries for
one triangle. A synthetic single patch passed 6 GB in six seconds; ten
thousand triangles that each cover half a 1000 x 1000 page ran for
minutes. Two rounds of review of earlier versions found more: every
patch was cut into 722 triangles (about 150 KB) before anything limited
them; a lattice mesh with `/VerticesPerRow 0` pushed empty rows forever,
and one with 4294967295 read the whole stream into one row; a mesh
painted a thousand times was triangulated a thousand times; and the
first caps cut content without a word.

Vendored hayro-interpret patch (18):
- `ShadingPattern::encode_within(area)`: the vendored hayro renderer
  passes the area it will actually draw (the path's bounding box on the
  canvas, which it already computes) plus a pixel for texels at a
  fractional edge, and triangles are sampled only inside it, so the map
  holds at most that area's pixels.
- `TriangleSampler`: at most 8 visits per pixel of that area plus 4 per
  triangle in it. A triangle's bounding box is about twice its area and a
  sub-pixel triangle's is at most 2 x 2 pixels, so tiling meshes and
  small dense ones (a patch in a few pixels is 722 triangles) still
  shade completely; overlapping ones stop.
- Coons and tensor-product patches are triangulated one at a time.
- Meshes hold at most `MAX_MESH_PATCHES` (2^16; a 200 x 200 gradient
  mesh is 40 thousand) patches and `MAX_MESH_TRIANGLES` (2^20)
  triangles, and a lattice needs two (ISO 32000-2 8.7.4.5.6) to 2^19
  vertices per row.
- A page samples at most `MAX_PAGE_MESH_TRIANGLES` (2^25; the 200 x 200
  mesh is 29 million) mesh triangles in all, reset per page like the
  existing paint budget.
- Each of these cuts logs a warning.
`encode()` keeps its signature; with no area it samples within a fixed
2^24 visits.

Synthetic regression tests in render:
- `mesh_shadings_far_off_the_page_render`: a Coons patch and a triangle
  mesh decoded onto +-40000 render and the rest of the page draws; a
  patch on the page, the part of the huge one that covers it, and a
  patch filling a 4 x 4 pt rectangle as a pattern are all shaded.
- `many_overlapping_mesh_triangles_finish`: 10,000 triangles each
  covering half of a 1000 x 1000 page finish, and are drawn.
- `mesh_shadings_with_many_patches_or_no_rows_finish`: 40,000 patches
  beside the page, and a lattice with `/VerticesPerRow 0`.
Red on main (out of memory, killed at 3.9 GB). Each part is pinned by a
mutation: no visit budget stalls past 60 s; no clip leaves the covering
patch unshaded; no per-triangle allowance leaves the small patch
unshaded; triangulating all patches first runs out of memory (killed at
5.8 GB); no rows check runs out of memory.

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

* Never crash: decompression bombs can't exhaust the renderer's memory

The renderer's parser (vendored hayro-syntax) decoded Flate, LZW and
RunLength streams with no output limit. A 2 MB page whose content
stream inflates to 2 GB, or whose 8 x 8 image does, took `pdfcraft-cli
render` and `text` past 3 GB in under a second, and a few more MB of
file scale it further. On the desktop that is the first-page render of
an emailed PDF, and the allocation-failure abort takes every open
document with it. Our own layers already bound single streams (lopdf's
load-time decoding at 256 MiB, cos at 1 GiB), so `info` stays at a few
MB; the renderer path didn't. Random mutation almost never makes a
high-ratio stream, so `cargo xtask fuzz` doesn't find this; found by
hand while triaging the fuzz run.

Vendored hayro-syntax patch (6):
- `decode_limit` gives each filter an output limit, and Flate (both
  flate2 and the fallback inflater), LZW and RunLength never write past
  it (the buffer can still grow to under twice it):
  `MAX_DECODED_STREAM`, 256 MiB, lopdf's figure; or, for the last filter
  of an image whose size, bits per component and components are known,
  twice its pixel bytes (a PNG predictor tag byte per byte at most),
  capped at 1 GiB. Earlier filters in a chain produce compressed data
  and get the general limit.
- `Stream::decoded_within(max)` for callers that budget.
- A page's `/Contents` array shares one `MAX_DECODED_STREAM` (naming one
  bomb twenty times decoded 5 GB), reserving exactly what it appends.

Vendored hayro-interpret patch (18):
- The content a page holds at once (its contents, and the forms, tiling
  patterns and Type 3 glyph procedures being painted) stays within
  `MAX_PAGE_CONTENT`, 512 MiB, reset per page like the existing paint
  budget. Forms nest fifty deep and each level kept its content while
  the next ran: a form painting itself around a 256 MiB stream would
  have held over 12 GB. Each level gives its bytes back when it is
  dropped, so content painted again and again keeps drawing, and a
  stream without filters (borrowed from the file) costs nothing.
- Type 0 sampled functions read every sample into a 4-byte value and
  then a table entry: 256 MiB of 1-bit samples asked for over 8 GB. They
  now take at most 2^21 table points and 2^23 values (the largest real
  tables, DeviceN tint transforms such as 33^4 points x 3 outputs, are
  1.2 million and 3.6 million), counted with checked arithmetic, and
  read no further than `/Size` asks. `/Size 0` no longer underflows
  building the default `/Encode`.

Three rounds of independent review found the aggregate cases, buffer
doubling past the limit, a chain/predictor shape the first image limit
would have truncated, the Type 0 amplifier, and a first, cumulative
budget that would have dropped legitimately repeated content, and a
first Type 0 cap low enough to refuse real tint transforms; all fixed
here.

Measured in a release build (render; OS peak commit):
- content stream inflating to 2 GB: 389 MB, 0.3 s
- 8 x 8 image inflating to 2 GB: 4 MB, 0.2 s
- /Contents naming it twenty times: 516 MB, 0.3 s
- a form painting itself around it: 644 MB, 0.4 s

Synthetic regression tests in render (flate2 is a new dev-dependency
there, already in the workspace):
- `decompression_bombs_stop_at_the_stream_limit`: 288 MiB bombs through
  flate2, the fallback inflater (a stored block with a wrong length
  check, which flate2 refuses and the fallback accepts), LZW and
  RunLength each decode exactly 256 MiB, and at most 128 bytes as an
  8 x 8 image.
- `page_contents_and_nested_forms_share_one_budget`: three /Contents
  references decode one stream's worth; a self-painting form finishes.
- `content_painted_again_and_again_keeps_drawing`: a 2 MiB form painted
  300 times draws the 300th time.
- `sampled_functions_with_billions_of_samples_are_refused`.
Ten mutations each turn them red: no limit at all; no limit in flate2,
the fallback, LZW, RunLength or for images; an unshared /Contents limit;
no page budget; no refund on drop; no Type 0 sample cap. (Taking a form
out of the budget no longer compiles: its content is the budget guard.)

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

* M2: normalize JPEG2000 samples without overflowing

Use bounded integer sample scaling for 1-32-bit JPX images and reject empty dimensions before chunking. Cover public decoding and continued page rendering with synthetic regressions.

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

* Never crash: decompression cut at its limit is logged, not silent; vendor README renumbered after #575

---------

Co-authored-by: James Locke <336218001+jcl-bit@users.noreply.github.com>
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Co-authored-by: Streetblock <github@streetblock.de>
Co-authored-by: OpenAI Codex <codex@openai.com>
Co-authored-by: Brandon Thomas <echelon@gmail.com>
2026-10-09 14:11:59 -04:00
jcl-bit f5c99ce059
Never crash: mesh shadings can't exhaust memory or run for minutes (#412)
* Never crash: mesh shadings can't exhaust memory or run for minutes

`cargo xtask fuzz` (with the dash, JBIG2 and decompression fixes in
place) found a page whose Coons patch shading (type 6), a 147-byte
stream with 1-bit coordinates decoded onto +-40000, took the renderer
past 3 GB. hayro-interpret samples mesh shadings (types 4-7) pixel by
pixel over each triangle's device-space bounding box into a map, with
nothing but the u16 cast bounding it: up to 65536 x 65536 entries for
one triangle. A synthetic single patch passed 6 GB in six seconds; ten
thousand triangles that each cover half a 1000 x 1000 page ran for
minutes. Two rounds of review of earlier versions found more: every
patch was cut into 722 triangles (about 150 KB) before anything limited
them; a lattice mesh with `/VerticesPerRow 0` pushed empty rows forever,
and one with 4294967295 read the whole stream into one row; a mesh
painted a thousand times was triangulated a thousand times; and the
first caps cut content without a word.

Vendored hayro-interpret patch (18):
- `ShadingPattern::encode_within(area)`: the vendored hayro renderer
  passes the area it will actually draw (the path's bounding box on the
  canvas, which it already computes) plus a pixel for texels at a
  fractional edge, and triangles are sampled only inside it, so the map
  holds at most that area's pixels.
- `TriangleSampler`: at most 8 visits per pixel of that area plus 4 per
  triangle in it. A triangle's bounding box is about twice its area and a
  sub-pixel triangle's is at most 2 x 2 pixels, so tiling meshes and
  small dense ones (a patch in a few pixels is 722 triangles) still
  shade completely; overlapping ones stop.
- Coons and tensor-product patches are triangulated one at a time.
- Meshes hold at most `MAX_MESH_PATCHES` (2^16; a 200 x 200 gradient
  mesh is 40 thousand) patches and `MAX_MESH_TRIANGLES` (2^20)
  triangles, and a lattice needs two (ISO 32000-2 8.7.4.5.6) to 2^19
  vertices per row.
- A page samples at most `MAX_PAGE_MESH_TRIANGLES` (2^25; the 200 x 200
  mesh is 29 million) mesh triangles in all, reset per page like the
  existing paint budget.
- Each of these cuts logs a warning.
`encode()` keeps its signature; with no area it samples within a fixed
2^24 visits.

Synthetic regression tests in render:
- `mesh_shadings_far_off_the_page_render`: a Coons patch and a triangle
  mesh decoded onto +-40000 render and the rest of the page draws; a
  patch on the page, the part of the huge one that covers it, and a
  patch filling a 4 x 4 pt rectangle as a pattern are all shaded.
- `many_overlapping_mesh_triangles_finish`: 10,000 triangles each
  covering half of a 1000 x 1000 page finish, and are drawn.
- `mesh_shadings_with_many_patches_or_no_rows_finish`: 40,000 patches
  beside the page, and a lattice with `/VerticesPerRow 0`.
Red on main (out of memory, killed at 3.9 GB). Each part is pinned by a
mutation: no visit budget stalls past 60 s; no clip leaves the covering
patch unshaded; no per-triangle allowance leaves the small patch
unshaded; triangulating all patches first runs out of memory (killed at
5.8 GB); no rows check runs out of memory.

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

* Never crash: decompression bombs can't exhaust the renderer's memory

The renderer's parser (vendored hayro-syntax) decoded Flate, LZW and
RunLength streams with no output limit. A 2 MB page whose content
stream inflates to 2 GB, or whose 8 x 8 image does, took `pdfcraft-cli
render` and `text` past 3 GB in under a second, and a few more MB of
file scale it further. On the desktop that is the first-page render of
an emailed PDF, and the allocation-failure abort takes every open
document with it. Our own layers already bound single streams (lopdf's
load-time decoding at 256 MiB, cos at 1 GiB), so `info` stays at a few
MB; the renderer path didn't. Random mutation almost never makes a
high-ratio stream, so `cargo xtask fuzz` doesn't find this; found by
hand while triaging the fuzz run.

Vendored hayro-syntax patch (6):
- `decode_limit` gives each filter an output limit, and Flate (both
  flate2 and the fallback inflater), LZW and RunLength never write past
  it (the buffer can still grow to under twice it):
  `MAX_DECODED_STREAM`, 256 MiB, lopdf's figure; or, for the last filter
  of an image whose size, bits per component and components are known,
  twice its pixel bytes (a PNG predictor tag byte per byte at most),
  capped at 1 GiB. Earlier filters in a chain produce compressed data
  and get the general limit.
- `Stream::decoded_within(max)` for callers that budget.
- A page's `/Contents` array shares one `MAX_DECODED_STREAM` (naming one
  bomb twenty times decoded 5 GB), reserving exactly what it appends.

Vendored hayro-interpret patch (18):
- The content a page holds at once (its contents, and the forms, tiling
  patterns and Type 3 glyph procedures being painted) stays within
  `MAX_PAGE_CONTENT`, 512 MiB, reset per page like the existing paint
  budget. Forms nest fifty deep and each level kept its content while
  the next ran: a form painting itself around a 256 MiB stream would
  have held over 12 GB. Each level gives its bytes back when it is
  dropped, so content painted again and again keeps drawing, and a
  stream without filters (borrowed from the file) costs nothing.
- Type 0 sampled functions read every sample into a 4-byte value and
  then a table entry: 256 MiB of 1-bit samples asked for over 8 GB. They
  now take at most 2^21 table points and 2^23 values (the largest real
  tables, DeviceN tint transforms such as 33^4 points x 3 outputs, are
  1.2 million and 3.6 million), counted with checked arithmetic, and
  read no further than `/Size` asks. `/Size 0` no longer underflows
  building the default `/Encode`.

Three rounds of independent review found the aggregate cases, buffer
doubling past the limit, a chain/predictor shape the first image limit
would have truncated, the Type 0 amplifier, and a first, cumulative
budget that would have dropped legitimately repeated content, and a
first Type 0 cap low enough to refuse real tint transforms; all fixed
here.

Measured in a release build (render; OS peak commit):
- content stream inflating to 2 GB: 389 MB, 0.3 s
- 8 x 8 image inflating to 2 GB: 4 MB, 0.2 s
- /Contents naming it twenty times: 516 MB, 0.3 s
- a form painting itself around it: 644 MB, 0.4 s

Synthetic regression tests in render (flate2 is a new dev-dependency
there, already in the workspace):
- `decompression_bombs_stop_at_the_stream_limit`: 288 MiB bombs through
  flate2, the fallback inflater (a stored block with a wrong length
  check, which flate2 refuses and the fallback accepts), LZW and
  RunLength each decode exactly 256 MiB, and at most 128 bytes as an
  8 x 8 image.
- `page_contents_and_nested_forms_share_one_budget`: three /Contents
  references decode one stream's worth; a self-painting form finishes.
- `content_painted_again_and_again_keeps_drawing`: a 2 MiB form painted
  300 times draws the 300th time.
- `sampled_functions_with_billions_of_samples_are_refused`.
Ten mutations each turn them red: no limit at all; no limit in flate2,
the fallback, LZW, RunLength or for images; an unshared /Contents limit;
no page budget; no refund on drop; no Type 0 sample cap. (Taking a form
out of the budget no longer compiles: its content is the budget guard.)

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

* M2: normalize JPEG2000 samples without overflowing

Use bounded integer sample scaling for 1-32-bit JPX images and reject empty dimensions before chunking. Cover public decoding and continued page rendering with synthetic regressions.

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

* Never crash: decompression cut at its limit is logged, not silent; vendor README renumbered after #575

---------

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Co-authored-by: Streetblock <github@streetblock.de>
Co-authored-by: OpenAI Codex <codex@openai.com>
Co-authored-by: Brandon Thomas <echelon@gmail.com>
2026-10-09 14:11:43 -04:00
jcl-bit e88def3465
Never crash: decompression bombs can't exhaust the renderer's memory (#411)
* Never crash: decompression bombs can't exhaust the renderer's memory

The renderer's parser (vendored hayro-syntax) decoded Flate, LZW and
RunLength streams with no output limit. A 2 MB page whose content
stream inflates to 2 GB, or whose 8 x 8 image does, took `pdfcraft-cli
render` and `text` past 3 GB in under a second, and a few more MB of
file scale it further. On the desktop that is the first-page render of
an emailed PDF, and the allocation-failure abort takes every open
document with it. Our own layers already bound single streams (lopdf's
load-time decoding at 256 MiB, cos at 1 GiB), so `info` stays at a few
MB; the renderer path didn't. Random mutation almost never makes a
high-ratio stream, so `cargo xtask fuzz` doesn't find this; found by
hand while triaging the fuzz run.

Vendored hayro-syntax patch (6):
- `decode_limit` gives each filter an output limit, and Flate (both
  flate2 and the fallback inflater), LZW and RunLength never write past
  it (the buffer can still grow to under twice it):
  `MAX_DECODED_STREAM`, 256 MiB, lopdf's figure; or, for the last filter
  of an image whose size, bits per component and components are known,
  twice its pixel bytes (a PNG predictor tag byte per byte at most),
  capped at 1 GiB. Earlier filters in a chain produce compressed data
  and get the general limit.
- `Stream::decoded_within(max)` for callers that budget.
- A page's `/Contents` array shares one `MAX_DECODED_STREAM` (naming one
  bomb twenty times decoded 5 GB), reserving exactly what it appends.

Vendored hayro-interpret patch (18):
- The content a page holds at once (its contents, and the forms, tiling
  patterns and Type 3 glyph procedures being painted) stays within
  `MAX_PAGE_CONTENT`, 512 MiB, reset per page like the existing paint
  budget. Forms nest fifty deep and each level kept its content while
  the next ran: a form painting itself around a 256 MiB stream would
  have held over 12 GB. Each level gives its bytes back when it is
  dropped, so content painted again and again keeps drawing, and a
  stream without filters (borrowed from the file) costs nothing.
- Type 0 sampled functions read every sample into a 4-byte value and
  then a table entry: 256 MiB of 1-bit samples asked for over 8 GB. They
  now take at most 2^21 table points and 2^23 values (the largest real
  tables, DeviceN tint transforms such as 33^4 points x 3 outputs, are
  1.2 million and 3.6 million), counted with checked arithmetic, and
  read no further than `/Size` asks. `/Size 0` no longer underflows
  building the default `/Encode`.

Three rounds of independent review found the aggregate cases, buffer
doubling past the limit, a chain/predictor shape the first image limit
would have truncated, the Type 0 amplifier, and a first, cumulative
budget that would have dropped legitimately repeated content, and a
first Type 0 cap low enough to refuse real tint transforms; all fixed
here.

Measured in a release build (render; OS peak commit):
- content stream inflating to 2 GB: 389 MB, 0.3 s
- 8 x 8 image inflating to 2 GB: 4 MB, 0.2 s
- /Contents naming it twenty times: 516 MB, 0.3 s
- a form painting itself around it: 644 MB, 0.4 s

Synthetic regression tests in render (flate2 is a new dev-dependency
there, already in the workspace):
- `decompression_bombs_stop_at_the_stream_limit`: 288 MiB bombs through
  flate2, the fallback inflater (a stored block with a wrong length
  check, which flate2 refuses and the fallback accepts), LZW and
  RunLength each decode exactly 256 MiB, and at most 128 bytes as an
  8 x 8 image.
- `page_contents_and_nested_forms_share_one_budget`: three /Contents
  references decode one stream's worth; a self-painting form finishes.
- `content_painted_again_and_again_keeps_drawing`: a 2 MiB form painted
  300 times draws the 300th time.
- `sampled_functions_with_billions_of_samples_are_refused`.
Ten mutations each turn them red: no limit at all; no limit in flate2,
the fallback, LZW, RunLength or for images; an unshared /Contents limit;
no page budget; no refund on drop; no Type 0 sample cap. (Taking a form
out of the budget no longer compiles: its content is the budget guard.)

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

* M2: normalize JPEG2000 samples without overflowing

Use bounded integer sample scaling for 1-32-bit JPX images and reject empty dimensions before chunking. Cover public decoding and continued page rendering with synthetic regressions.

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

* Never crash: decompression cut at its limit is logged, not silent; vendor README renumbered after #575

---------

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Co-authored-by: Streetblock <github@streetblock.de>
Co-authored-by: OpenAI Codex <codex@openai.com>
Co-authored-by: Brandon Thomas <echelon@gmail.com>
2026-10-09 14:10:50 -04:00
Streetblock a5202e11fc
M2: normalize JPEG2000 samples without overflowing (#575)
* M2: normalize JPEG2000 samples without overflowing

Use bounded integer sample scaling for 1-32-bit JPX images and reject empty dimensions before chunking. Cover public decoding and continued page rendering with synthetic regressions.

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

* ROADMAP: keep the JPX entry with this session's entries

---------

Co-authored-by: OpenAI Codex <codex@openai.com>
Co-authored-by: Brandon Thomas <echelon@gmail.com>
2026-10-09 14:09:56 -04:00
Hal Goff f1af1e2e79
Issue #125: page text the standard fonts can't draw is refused, not written as ? (#510)
Add text, headers/footers and watermarks wrote every character outside
WinAnsiEncoding as `?` and reported success. They now refuse it with a
specific error ("the standard fonts can't draw "中" (U+4E2D) ...") before
changing anything, as the maintainer asked on #327. Form fields and
text-box comments are unchanged (they keep the value).

- fonts: win_ansi_encodable / first_non_win_ansi, defined through
  win_ansi itself so a fuller table widens them too; the edited-text
  Type 3 decision uses the same check (same character set as before).
- edit: one drawable() check, fed the lines exactly as each tool draws
  them, so a CR that would become `?` is refused and CRLF in headers is not.
- ui: the Add-text editor keeps such text open with an inline warning.
  Done, a click elsewhere or a double-click on another item don't take
  or replace it; it counts as unsaved work; Save stops and shows it; a
  deleted page takes its draft with it.
- Arabic (#403): one public check, first_undrawable, follows the drawing:
  Arabic shaped with the craft-fonts face is drawable, and only what's left
  to the standard font counts. Without the face, #403 still decides for the
  Arabic letters (new items refused with what's missing, existing ones stay
  movable), but nothing else in the item may become `?`. The editor uses
  the same check, cached per draft.
- #403's odd_arabic_text_never_panics: its U+2029/U+0085 input, which was
  drawn as `?`, now expects a refusal naming the character.
- The warning is translated (fr, ru, zh-hans, zh-hant, es, ja, bg, de); the
  catalogs' SHA-256 entries in ATTRIBUTION.toml are updated.


Claude-Session: https://claude.ai/code/session_01VNJqS2dEQawHJn1qTRJiKp

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Co-authored-by: Brandon Thomas <echelon@gmail.com>
2026-10-09 13:20:47 -04:00
Márk Bartalos 3af93c7585
M5: draw all ten PDF line-ending styles (#520)
* M5: draw all ten PDF line-ending styles

Imported lines, polylines and callouts kept Square, Circle, Diamond, Butt, Slash and reverse arrows in /LE but would not redraw them. Those styles now draw, and comment_add and Comment properties can set them.

* hu, uk: Line ending

---------

Co-authored-by: Brandon Thomas <echelon@gmail.com>
2026-10-09 13:15:44 -04:00
Márk Bartalos c9d0619226
Forms: rotate field widgets with /MK /R (#516)
* M6: rotate form field widgets with /MK /R

Filling or restyling a rotated field redrew its text upright. Appearances now follow /MK /R, and Field Properties and form_set_props can set the rotation.

* Forms: a rotation refused as too small is checked before any other property is written; no indexing by widget number

* hu, uk: Rotation:

---------

Co-authored-by: Brandon Thomas <echelon@gmail.com>
2026-10-09 13:13:12 -04:00
Dimitar 2d24de1c16
M7: edit text: Cyrillic/Greek fallback letters are spaced proportionally (#528)
Replacement text that the line's own (subset) font can't show and WinAnsi can't either goes
to the Type 3 fallback. For serif sources that was always Shippori Mincho, which has no
Cyrillic or Greek, so e.g. retyping a Bulgarian word in a Garamond line failed with
"Japanese fallback font has no glyph for U+0421". BIZ UDMincho and BIZ UDPGothic have the
letters. document_font_for_text keeps the style's face when it shows every character and
otherwise picks the closest Japanese face that does (Mincho first for serif, then weight).

Those faces draw Cyrillic and Greek full-width (1 em, JIS X 0208) with no palt metrics, so
fallback glyphs in those blocks are respaced to their ink width plus a 0.05 em side bearing.

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Co-authored-by: Brandon Thomas <echelon@gmail.com>
2026-10-09 13:06:33 -04:00
Chen Tao 0375bd1e28
M6.2: form fields in Unicode CID fonts show Japanese instead of "?" (#535)
* M6.2: form fields in Unicode CID fonts show Japanese instead of "?"

Japanese forms often give their fields a /DA font that /DR defines as a Type0
font with a predefined Unicode CMap, such as a non-embedded HeiseiMin-W3 with
/UniJIS-UCS2-H. Field appearances accepted only simple fonts, so they fell back
to Helvetica and WinAnsi, and every Japanese character was drawn as "?".

Appearances now use such fonts and encode the text in the CMap's form (UCS-2,
UTF-16, UTF-8 or UTF-32), measuring full-width characters as one em so centring,
auto size, comb cells and multiline wrapping fit. Identity-H and embedded CMaps
still fall back to Helvetica.

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

* M6.2: vertical Unicode CMaps fall back to Helvetica, since field appearances are horizontal

---------

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Co-authored-by: Brandon Thomas <echelon@gmail.com>
2026-10-09 12:59:33 -04:00
Chen Tao f78f74164c
M6.2: check boxes with non-UTF-8 state names show their mark when clicked (#536)
Japanese forms often name a check box's on state in Shift-JIS (「はい」 is
/#82#CD#82#A2). State names were read with from_utf8_lossy, so ticking such a
box wrote a different name to /AS and /V, no appearance matched, and the box
was drawn with no mark (or not at all).

Names now map to text losslessly (forms::name_text / name_bytes: UTF-8 as
itself, other bytes and `#` as #XX) wherever states and values are read and
written: /V, /DV and /AS, PdfCraft's own state appearances, and FDF import and
export, so such values also survive an FDF or XFDF round trip.

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Co-authored-by: Brandon Thomas <echelon@gmail.com>
2026-10-09 12:58:38 -04:00
Dmytro e6df6b1384
i18n: add complete Ukrainian interface translation (#509)
* M14: add complete Ukrainian interface translation

Add the uk registry entry and original catalog, Ukrainian integer plurals,
calendar month inflection, attribution and localization documentation.
Fit Home recommendation cards and the search hint to translated text.
Cover locale detection, persistence, catalogs, bundled glyphs and UI control.

Fixes #503
Related to #348

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

* uk: add the recent-files labels, labels that reach tl!() through a variable, and 'Inserted text'; fix 'мають мати'

* uk: Home / End are key-cap names

---------

Co-authored-by: Codex <codex@openai.com>
Co-authored-by: Brandon Thomas <echelon@gmail.com>
2026-10-09 12:55:46 -04:00
Streetblock f626db25cc
M14: refine German labels and prevent translated UI text overlap (#515)
* M14: refine German labels and keep translated text within UI bounds

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

* Tool names inside notices stay lowercase in English; German keeps its capitalised nouns

---------

Co-authored-by: Codex <codex@openai.com>
Co-authored-by: Brandon Thomas <echelon@gmail.com>
2026-10-09 12:39:57 -04:00
cyfung1031 63363ec438
M3: rasters reach egui without a copy on the UI thread (#555)
* M3: rasters reach egui without a copy on the UI thread

Every page, tile and thumbnail raster was copied pixel by pixel on the UI
thread (`ColorImage::from_rgba_premultiplied`), because egui's `Color32` is
4-byte aligned and a `Vec<u8>` isn't. That is 0.5 ms per 1024² tile, 5.4 ms
for a 33 MB page and 36 ms for a 197 MB page, against a 16.7 ms frame.

hayro, vendored patch 7: `render_size` and `render_into` render into a buffer
the caller allocates. `render` is `render_into` on a new pixmap, so its output
is unchanged. pdfcraft-render renders straight into `Pixels`, the same
premultiplied RGBA bytes kept in one 4-byte word per pixel. `Pixels`
dereferences to `[u8]`, so every reader of `RenderedPage::rgba` compiles
unchanged. The canvas reinterprets the words as `Color32`s and hands them to
egui as they are.

Pixels are identical: all 33 renders of #260's test file hash the same. The
UI-thread copy is gone, and rendering a 6182×8000 page takes 67 ms instead of
71–76 ms.

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

* M3: render_into leaves a buffer of the wrong length untouched instead of reaching vello's assert

* M3: regression test for render_into with a wrong-length buffer

---------

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Co-authored-by: Brandon Thomas <echelon@gmail.com>
2026-10-09 12:28:46 -04:00
cyfung1031 eb00280eba
M1: stream data read from a file points into the file instead of being copied (#552)
* M1: stream data read from a file points into the file instead of being copied

Every stream COS loaded was copied out of the document's bytes
(`data[start..end].to_vec()`) and kept in the object cache until the document
closed. Anything that reads every object therefore left a second copy of all
stream data behind. That includes a full save, which edits needing a rewrite
(repaired files, security changes, redaction) run on every change. A full save
of a 208 MB document left 208 MB cached on it.

`Stream::raw` is now `Bytes`: owned bytes, or a range of a larger shared
buffer. Streams parsed from a document's own bytes are ranges of that buffer,
which the document keeps anyway. Decrypted and newly built streams own their
bytes as before. A stream added to a document from another document's file gets
its own copy of just its bytes (`Document::set`/`add`), so it never keeps
another whole file alive.

Results, measured with an exact heap counter:
- A full save of a 208 MB document leaves 0.2 MB cached instead of 208.2 MB.
- The first full save of a freshly opened document is about twice as fast:
  10 → 5 ms at 40 MB, 38 → 21 ms at 200 MB.

Output is identical: 68 incremental and full saves of five originals,
encrypted ones included, hash the same before and after.

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

* M1: the full-save capacity test (#523) builds its stream from Bytes

---------

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Co-authored-by: Brandon Thomas <echelon@gmail.com>
2026-10-09 12:28:33 -04:00
Márk Bartalos 95120862fc
M14: Hungarian interface catalog (hu) (#545)
* M14: Hungarian interface catalog (hu)

Magyar joins the interface languages as a complete catalog, so Preferences and hu locales show commands, dialogs, panels and notices in Hungarian. Wording uses informal "te" and nouns for commands; the strings were written from the meaning of the English labels.

* Small grammar correction

* hu: restore trailing spaces in run-on labels; add the recent-files labels and labels that reach tl!() through a variable

---------

Co-authored-by: Brandon Thomas <echelon@gmail.com>
2026-10-09 12:28:06 -04:00