Skip to content

0.110 → 0.111 — the PDF spine checks what it was trusting

Documents in storage are untouched, stored blobs re-encode byte for byte, and no rendered output moves for a quill whose assets already met the contract. This is a fix release: what moves is the line between input the engine accepts and input it refuses, plus two answers it was computing wrongly.

The one break that can stop a render is a pdfform background that was never stripped. form.pdf is documented as carrying no form of its own, and the backend wrote a fresh /AcroForm onto it without looking. A base that still had one got a second /AcroForm key on the catalog — a dict the spec does not define — with the old form's widgets still live in the page /Annots the update preserves. That is refused now.

Break Surface Action
A base PDF already carrying an /AcroForm is refused (pdf::existing_acroform) pdfform quills, quillmark_pdf::stamp Strip the form from form.pdf. PDF output only; SVG and PNG draw values and are unaffected
A non-finite widget rect is refused (pdf::bad_rect) pdfform quills, stamp / flatten Fix the form.json geometry; the PDF it wrote instead was unparseable
LineOp::SetKind refuses a heading level outside 1..=6 (ApplyError::BadHeadingLevel) Rust Keep the level in range. ApplyError is #[non_exhaustive], so an existing match still compiles
A path naming no directory fails as quill::load_failed, not quill::missing_file Rust, Python, CLI Route on the code: a missing bundle and a missing path are now different answers
tree_from_path errors on a path that is not a directory Rust It returned an empty tree; a caller relying on that gets an Err
A plaintext field declared inline: true keeps the flag on the schema echo Rust, WASM, Python, CLI schema Re-baseline a snapshot over quill.schema(). The constraint was being dropped, including across a serde round-trip
A Typst diagnostic's column counts characters Rust, WASM, Python None, unless a consumer read the column as a byte offset

Additive: mapMarks on WASM and Content::map_marks in Rust.

The stripped background is now checked

The rule has not changed — the pdfform backend (acroform from 0.113) writes the AcroForm fresh from form.json and never reconciles one it finds — only its enforcement. Producing a clean form.pdf stays the job of a qualification layer outside the engine; the engine checks that layer's result, not how it was reached.

A quill that rendered PDF and now fails with pdf::existing_acroform was shipping a double-form PDF whose behavior was the reader's choice. Re-strip the background and the render returns.

Where a mark lands, answered instead of predicted

Every channel of a ChangeBundle that moves text (delta, islandOps, lineOps) rebases the marks already in the field by one assoc rule: a range's start takes after and its end before, so an insertion at either edge grows text outside the span; a zero-width mark takes before, so an insertion at its own position leaves it put. That last case is the only position where the two assocs differ, and where an anchor most often sits.

An editor deciding which markOps to emit had to reimplement that rule to know what it was diffing against. mapMarks(content, bundle) runs it instead:

const predicted = mapMarks(doc.main.body, bundle)
const markOps = yourDiff(predicted, intended) // only the difference
doc.applyChange({}, { ...bundle, markOps })

The answer is normalized, as the store's is, so a bundle carrying no markOps names the marks the field will hold. bundle.markOps are ignored, and a bundle applyChange would refuse throws here rather than answering.

mapPos is unchanged and answers a different question: a position the caller holds, through one delta, on the side assoc names.