crates/nornir-docbook/src/svgz.rs
//! `.svgz` — the compressed storage form of a rendered book page.
//!
//! A typst-rendered page is enormous and enormously redundant. typst outlines
//! every glyph once into a per-page `<defs>` of `<symbol>` paths and then places
//! each occurrence as a `<use xlink:href="#g…">`, four-space-indented, with the
//! coordinates printed straight off `f64` — so a page is thousands of near-identical
//! lines carrying values like `x="32.592000000000006"`. Measured over korp's real
//! 84-page manual on 2026-09-02:
//!
//! | stage | bytes |
//! |----------------------------------------------|----------|
//! | typst as emitted | 35.21 MB |
//! | + [`optimize_svg`] (indentation + f64 noise) | 24.17 MB |
//! | + [`to_svgz`] (gzip) | **4.49 MB** |
//!
//! 7.8×. That is the difference between a manual that can be embedded in a shipped
//! binary and published on crates.io, and one that cannot.
//!
//! Both steps live HERE, in the shared render engine, and not in any one repo's
//! xtask — every book (dwarves', korp's, any future one) inherits the same bytes
//! from the same writer, and a committed page can never disagree with the emitter
//! that produced it.
//!
//! `.svgz` is not an invention of this crate: it is the standard gzip-compressed
//! SVG container that every SVG consumer already understands. A reader needs a
//! stock gzip decoder and nothing from this crate — which is the point, because the
//! GUI that renders the pages must not link the typst stack to read them.
use std::io::Write as _;
use std::path::Path;
/// The file extension of a compressed page. Pair it with the page-number stem the
/// caller already uses (`page-001.svgz`).
pub const SVGZ_EXTENSION: &str = "svgz";
/// Fractional digits [`optimize_svg`] keeps.
///
/// A book page is ~595 pt wide and is rasterized at roughly 1–3 px/pt, so 1e-4 pt
/// is ~3e-4 px — four orders of magnitude below the sub-pixel grid a rasterizer
/// antialiases on. That argument is *not* the proof; the proof is
/// `tests::optimize_svg_does_not_move_a_single_pixel`, which renders a real typst
/// page before and after through resvg and compares the RGBA buffers pixel by
/// pixel. Lowering this constant to 0 makes that test fail, which is how we know
/// it can see a moved glyph at all.
const KEPT_DECIMALS: usize = 4;
/// Losslessly shrink one typst-emitted SVG page, in the two ways its emitter is
/// wasteful. Nothing here changes the rendered image: the transform is text-level
/// and structure-preserving — no element is dropped, merged, reordered or renamed.
///
/// 1. **Inter-element indentation.** typst pretty-prints with four-space nesting.
/// A typst page has NO `<text>` node — every glyph is a `<use>` of a `<symbol>`
/// outline — so whitespace between a `>` and the next `<` is XML-insignificant
/// throughout the file. (The single newline after the final `</svg>` is not
/// between elements and is left alone.)
///
/// **That is a precondition, not a description**, and 0.1.2 gave it an edge to
/// fall off: [`crate::export::TextLayer`] puts real `<text>` elements over a
/// column render, where the space between two words IS significant. The layer
/// is therefore added *after* this pass and never fed through it. Anything
/// else that grows a `<text>` node must do the same, or this function will
/// quietly delete the spaces between the words.
/// 2. **`f64` print noise.** Coordinates are printed straight off `f64`, so a glyph
/// advance arrives as `x="32.592000000000006"` and a page height as
/// `841.8897637795276`. A literal carrying more than [`KEPT_DECIMALS`] fractional
/// digits is re-printed rounded, with trailing zeros trimmed.
///
/// A literal is rewritten only when it is a whole numeric token — the byte before it
/// is neither a digit nor a `.` — and only when the shortened form is strictly
/// shorter. typst's `#g<32 hex>` glyph ids contain no `.`, so they can never match;
/// `tests::optimize_svg_leaves_glyph_ids_alone` states that rather than trusting it.
///
/// The function is **idempotent**: `optimize_svg(optimize_svg(x)) == optimize_svg(x)`.
/// That is what lets a committed page be checked against this emitter without
/// keeping the pre-image around — inflate it, re-optimize, and the bytes must not move.
pub fn optimize_svg(src: &str) -> String {
optimize_svg_owned(src.to_owned())
}
/// The same pass, taking the page **by value and rewriting it in its own
/// buffer** — no second copy of the page is ever live.
///
/// This is the whole of [`optimize_svg`]; the borrowing door copies once and
/// calls here, so there is one implementation of the transform and not two.
///
/// # Why it can be done in place at all
///
/// Both rewrites are **strictly shrinking**: whitespace between two elements is
/// deleted, and a numeric literal is replaced only when the rounded form is
/// *strictly shorter* than what was there. So the write cursor is never ahead of
/// the read cursor and no byte is overwritten before it has been read. That is a
/// property of the transform, not an assumption about the input, and
/// `tests::the_in_place_pass_is_byte_for_byte_the_pass_it_replaced` compares it
/// to 0.1.4's copying version on a real page rather than trusting the argument.
///
/// # Why it exists
///
/// MEASURED 2026-09-12 on oden, one 3 MB markdown document through
/// [`crate::export_svg_column`]: `typst_svg` hands over a **512 MB** page and
/// the optimizer answered with a **313 MB** copy of it, so 825 MB of the render
/// was two spellings of the same page. `examples/mem.rs` is the instrument and
/// carries the table.
pub fn optimize_svg_owned(svg: String) -> String {
let mut buf = svg.into_bytes();
let len = shrink_in_place(&mut buf);
buf.truncate(len);
// Only whole ASCII tokens were replaced by ASCII, so UTF-8 boundaries are intact.
String::from_utf8(buf).expect("the transform only rewrites ASCII numeric tokens")
}
/// Apply both rewrites to `buf` in place and answer the new length.
///
/// `w <= i` is the invariant that makes this sound: every branch writes at most
/// as many bytes as it consumed. A byte at `i - 1` — which `starts_token` reads
/// — is therefore always still the ORIGINAL byte, because the writer has only
/// reached `w <= i` and anything at or past `w` that is below `i` is untouched.
fn shrink_in_place(buf: &mut [u8]) -> usize {
let n = buf.len();
let (mut i, mut w) = (0usize, 0usize);
while i < n {
let b = buf[i];
// (1) `>` whitespace… `<` → `><`
if b == b'>' {
let mut j = i + 1;
while j < n && buf[j].is_ascii_whitespace() {
j += 1;
}
buf[w] = b'>';
w += 1;
i = if j > i + 1 && j < n && buf[j] == b'<' { j } else { i + 1 };
continue;
}
// (2) a numeric token with runaway fractional digits
if b == b'-' || b.is_ascii_digit() {
let prev = if i == 0 { None } else { Some(buf[i - 1]) };
let starts_token = !matches!(prev, Some(p) if p.is_ascii_digit() || p == b'.');
if starts_token {
if let Some(end) = noisy_number_end(buf, i) {
if let Some(short) = shorter_form(&buf[i..end]) {
buf[w..w + short.len()].copy_from_slice(short.as_bytes());
w += short.len();
i = end;
continue;
}
}
}
}
buf[w] = b;
w += 1;
i += 1;
}
w
}
/// The rounded spelling of a scanned numeric token, when it is strictly
/// shorter than the token itself. `None` leaves the token alone.
fn shorter_form(token: &[u8]) -> Option<String> {
// Every byte scanned by `noisy_number_end` is ASCII `-`, digit or `.`.
let text = std::str::from_utf8(token).expect("ascii numeric token");
let short = trim_fraction(text.parse::<f64>().expect("scanned literal parses"));
(short.len() < text.len()).then_some(short)
}
/// If a `[-]?<digits>.<digits>` literal starts at `i` and its fraction is longer than
/// [`KEPT_DECIMALS`], return the index one past its last digit. `None` otherwise —
/// integers included, since they carry no noise to trim.
fn noisy_number_end(src: &[u8], i: usize) -> Option<usize> {
let mut j = i;
if src[j] == b'-' {
j += 1;
}
let int_start = j;
while j < src.len() && src[j].is_ascii_digit() {
j += 1;
}
if j == int_start || j >= src.len() || src[j] != b'.' {
return None;
}
j += 1;
let frac_start = j;
while j < src.len() && src[j].is_ascii_digit() {
j += 1;
}
(j - frac_start > KEPT_DECIMALS).then_some(j)
}
/// `v` printed with at most [`KEPT_DECIMALS`] fractional digits, trailing zeros and a
/// bare trailing `.` removed: `841.8897637795276` → `841.8898`,
/// `32.592000000000006` → `32.592`, `1.000004` → `1`.
fn trim_fraction(v: f64) -> String {
let mut s = format!("{v:.KEPT$}", KEPT = KEPT_DECIMALS);
if s.contains('.') {
while s.ends_with('0') {
s.pop();
}
if s.ends_with('.') {
s.pop();
}
}
s
}
/// One rendered page as the bytes a `.svgz` file is written with: [`optimize_svg`]
/// then gzip — **byte-stable across runs, machines and clocks**.
///
/// Determinism is not decoration here. These pages are COMMITTED (korp's manual is
/// 84 blobs in git and inside the shipped binary), so a gzip header carrying the
/// build clock would make every re-render of unchanged documentation a full-book
/// diff. The two header fields that are not a function of the input are pinned:
/// MTIME to 0 and the OS byte to `0xFF` ("unknown").
///
/// The result is an ordinary gzip stream. A consumer inflates it with any stock
/// gzip reader — it does not need this crate, and a GUI must not link typst to read
/// a page.
pub fn to_svgz(svg: &str) -> Vec<u8> {
gzip(&optimize_svg(svg))
}
/// The pinned gzip stream of an ALREADY optimized page.
///
/// [`to_svgz`] is this plus the optimizer, which is how the header stays pinned
/// in one place. It is separate so that a caller holding the optimized page —
/// [`SvgzBook::write`], which needs its length for the stats line — can compress
/// what it is already holding instead of re-running the pass. It ran the pass
/// TWICE per page until 2026-09-12: once inside `to_svgz` and once for
/// `optimized_bytes`, for a whole second copy of every page and a whole second
/// scan of it.
fn gzip(optimized: &str) -> Vec<u8> {
let mut enc = flate2::GzBuilder::new()
.mtime(0)
.operating_system(255)
.write(Vec::new(), flate2::Compression::best());
enc.write_all(optimized.as_bytes()).expect("gzip into a Vec cannot fail");
enc.finish().expect("gzip into a Vec cannot fail")
}
/// Inflate a `.svgz` page back to SVG text — the inverse of [`to_svgz`], for tools
/// that already depend on this crate (a reader that only renders pages should use a
/// stock gzip decoder instead of taking the whole typst stack as a dependency).
pub fn from_svgz(gz: &[u8]) -> anyhow::Result<String> {
use std::io::Read as _;
let mut svg = String::new();
flate2::read::GzDecoder::new(gz)
.read_to_string(&mut svg)
.map_err(|e| anyhow::anyhow!("not a readable .svgz page: {e}"))?;
Ok(svg)
}
/// A committed `.svgz` page set on disk — the ONE door between a render and the
/// bytes that land in git.
///
/// Both directions live here, and that is the whole point:
///
/// * [`SvgzBook::write`] is how pages are produced (optimize + deterministic gzip,
/// stale files swept), so a re-render always yields the committed shape;
/// * [`SvgzBook::check`] renders nothing and writes nothing — it answers whether
/// what is on disk is *exactly* what `write` would produce for the pages it is
/// handed. That is the gate a docs door runs, and it is only possible because
/// [`to_svgz`] pins the gzip header (see its notes): with a clock-stamped MTIME
/// every page would compare stale one second after it was written.
///
/// Ordinary builds never come near either one. They embed the committed bytes; only
/// the docs verb renders, and only the docs verb checks.
pub struct SvgzBook<'a> {
/// Directory holding `page-NNN.svgz`.
pub dir: &'a Path,
/// Zero-padding width of the page number (korp's book uses 3 → `page-001.svgz`).
pub digits: usize,
/// Sidecar file names in `dir` that are NOT pages and must be left alone
/// (`toc.tsv`, `manifest.json`, per-page `.txt` search sidecars…).
pub keep: &'a [&'a str],
}
/// What [`SvgzBook::check`] found. `is_empty()` is the green.
#[derive(Debug, Default, PartialEq, Eq)]
pub struct SvgzMismatch {
/// Pages the render produced that are not on disk at all.
pub missing: Vec<String>,
/// Pages on disk whose bytes differ from what the render produces — the doc was
/// edited and the book was not re-rendered (or was edited by hand).
pub stale: Vec<String>,
/// Files on disk the render does not produce: pages past the end of a shortened
/// book, and anything not named in [`SvgzBook::keep`].
pub extra: Vec<String>,
/// Uncompressed `*.svg` pages sitting in the book directory. Never a warning:
/// they are a second copy of a page riding into every binary and crate that
/// embeds the folder.
pub uncompressed: Vec<String>,
}
impl SvgzMismatch {
/// True when the committed book is exactly what the emitter produces.
pub fn is_empty(&self) -> bool {
self.missing.is_empty()
&& self.stale.is_empty()
&& self.extra.is_empty()
&& self.uncompressed.is_empty()
}
}
impl std::fmt::Display for SvgzMismatch {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
let mut section = |label: &str, items: &[String]| -> std::fmt::Result {
if items.is_empty() {
return Ok(());
}
writeln!(f, " {} ({}): {}", label, items.len(), items.join(", "))
};
section("stale", &self.stale)?;
section("missing", &self.missing)?;
section("extra", &self.extra)?;
section("uncompressed", &self.uncompressed)
}
}
/// What [`SvgzBook::write`] wrote, in bytes — the numbers a docs verb prints.
#[derive(Debug, Default, Clone, Copy)]
pub struct SvgzStats {
pub pages: usize,
/// Total size of the pages as typst emitted them.
pub raw_bytes: usize,
/// Total size after [`optimize_svg`], before gzip.
pub optimized_bytes: usize,
/// Total size actually committed.
pub svgz_bytes: usize,
}
impl<'a> SvgzBook<'a> {
/// The file name page `i` (0-based) is stored under.
pub fn page_name(&self, i: usize) -> String {
format!("page-{:0width$}.{SVGZ_EXTENSION}", i + 1, width = self.digits)
}
/// Write the page set, sweeping every stale page first — including any
/// uncompressed `*.svg` left by an older render, which would otherwise sit next
/// to its own `.svgz` as a silent duplicate.
pub fn write(&self, pages: &[String]) -> anyhow::Result<SvgzStats> {
self.sweep()?;
std::fs::create_dir_all(self.dir)?;
let mut stats = SvgzStats { pages: pages.len(), ..Default::default() };
for (i, page) in pages.iter().enumerate() {
// ONE optimizer pass per page, and the compressor is handed what it
// produced. `to_svgz(page)` plus `optimize_svg(page).len()` ran the
// pass twice and held a second full copy of every page to do it.
let opt = optimize_svg(page);
let gz = gzip(&opt);
stats.raw_bytes += page.len();
stats.optimized_bytes += opt.len();
stats.svgz_bytes += gz.len();
drop(opt);
std::fs::write(self.dir.join(self.page_name(i)), &gz)?;
}
Ok(stats)
}
/// Answer whether the committed book already IS this page set. Renders nothing,
/// writes nothing, touches nothing.
pub fn check(&self, pages: &[String]) -> anyhow::Result<SvgzMismatch> {
let mut out = SvgzMismatch::default();
let expected: Vec<String> = (0..pages.len()).map(|i| self.page_name(i)).collect();
for (name, page) in expected.iter().zip(pages) {
match std::fs::read(self.dir.join(name)) {
Err(_) => out.missing.push(name.clone()),
Ok(on_disk) if on_disk != to_svgz(page) => out.stale.push(name.clone()),
Ok(_) => {}
}
}
for name in self.other_files()? {
if name.ends_with(".svg") {
out.uncompressed.push(name);
} else if !expected.contains(&name) {
out.extra.push(name);
}
}
out.missing.sort();
out.stale.sort();
out.extra.sort();
out.uncompressed.sort();
Ok(out)
}
/// Every file in `dir` that is not a `keep` sidecar.
fn other_files(&self) -> anyhow::Result<Vec<String>> {
if !self.dir.exists() {
return Ok(Vec::new());
}
let mut names = Vec::new();
for entry in std::fs::read_dir(self.dir)?.flatten() {
let name = entry.file_name().to_string_lossy().into_owned();
if !self.keep.contains(&name.as_str()) {
names.push(name);
}
}
Ok(names)
}
/// Delete every page (compressed or not) so a shortened render leaves no orphans.
fn sweep(&self) -> anyhow::Result<()> {
for name in self.other_files()? {
if name.ends_with(".svg") || name.ends_with(".svgz") {
let _ = std::fs::remove_file(self.dir.join(name));
}
}
Ok(())
}
}
#[cfg(test)]
mod tests {
use super::*;
use std::io::Read as _;
/// A REAL typst page as this crate emits it — korp's `assets/manual-svg/page-001.svg`
/// captured 2026-09-02, before the book started shipping compressed. It is the
/// pre-image the optimizer is measured against. Re-capture it from a fresh
/// uncompressed render if typst's output shape ever changes.
fn raw_page() -> String {
let p = std::path::Path::new(env!("CARGO_MANIFEST_DIR"))
.join("tests/fixtures/typst-page-raw.svg");
std::fs::read_to_string(&p)
.unwrap_or_else(|e| panic!("fixture {} must exist: {e}", p.display()))
}
/// Rasterize a page through resvg — the SAME engine facett-docview gives every
/// shipped Manual tab — and return its straight-RGBA pixels.
fn pixels(svg: &str) -> ((u32, u32), Vec<[u8; 4]>) {
let opts = usvg::Options::default();
let tree = usvg::Tree::from_data(svg.as_bytes(), &opts).expect("page parses");
let size = tree.size();
// 2 px/pt: enough that a glyph is tens of pixels wide, so a moved outline
// shows up rather than vanishing into a coarse grid.
let (w, h) = ((size.width() * 2.0) as u32, (size.height() * 2.0) as u32);
let mut pixmap = tiny_skia::Pixmap::new(w, h).expect("pixmap");
resvg::render(&tree, tiny_skia::Transform::from_scale(2.0, 2.0), &mut pixmap.as_mut());
let px = pixmap
.pixels()
.iter()
.map(|p| {
let c = p.demultiply();
[c.red(), c.green(), c.blue(), c.alpha()]
})
.collect();
((w, h), px)
}
/// **The lossless proof.** Not "it still parses", not "it got smaller" — the
/// optimized page must rasterize to a bit-identical image. A changed glyph is a
/// red, and the only thing that can see a changed glyph is the pixels.
///
/// The comparison is guarded against being hollow two ways: the BEFORE raster
/// must carry thousands of dark pixels (two blank sheets also compare equal),
/// and setting `KEPT_DECIMALS` to 0 makes this test fail — verified 2026-09-02,
/// so it is known to be able to go red.
#[test]
fn optimize_svg_does_not_move_a_single_pixel() {
let raw = raw_page();
let opt = optimize_svg(&raw);
assert!(opt.len() < raw.len(), "the optimizer must actually remove bytes");
let (before_size, before) = pixels(&raw);
let (after_size, after) = pixels(&opt);
assert_eq!(before_size, after_size, "the page keeps its size");
let ink = before.iter().filter(|p| p[3] > 0 && p[0] < 200).count();
assert!(
ink > 1_000,
"the BEFORE raster must carry real ink or this comparison proves nothing: \
{ink} dark px in {before_size:?}",
);
let moved = before.iter().zip(&after).filter(|(a, b)| a != b).count();
assert_eq!(
moved,
0,
"{moved} of {} pixels changed ({} B raw -> {} B optimized)",
before.len(),
raw.len(),
opt.len(),
);
}
/// **The in-place pass is the pass it replaced.** 0.1.4 built the optimized
/// page into a second buffer; 0.1.5 rewrites it in the page's own. That is a
/// change to how every committed `.svgz` in the constellation is produced,
/// so it is not checked against itself.
///
/// `reference_optimize_svg` below is 0.1.4's function, verbatim, kept here
/// as the ORACLE — the one case where a second implementation is the point
/// rather than a duplicate. Compared on a real typst page, byte for byte.
///
/// Seen RED 2026-09-12 by making `shrink_in_place` skip its `w += 1` on the
/// `>` branch — every `>` in the page is then eaten by the byte after it.
/// 15 tests went red with it, the pixel proof included.
#[test]
fn the_in_place_pass_is_byte_for_byte_the_pass_it_replaced() {
let raw = raw_page();
let reference = reference_optimize_svg(&raw);
assert!(
reference.len() < raw.len() && reference.len() > raw.len() / 4,
"the oracle must actually have optimized something: {} B -> {} B",
raw.len(),
reference.len(),
);
assert_eq!(optimize_svg_owned(raw.clone()), reference, "the owned door");
assert_eq!(optimize_svg(&raw), reference, "the borrowing door");
}
/// 0.1.4's `optimize_svg`, unchanged, as the oracle for the in-place pass.
/// Do not "tidy" this into a call to the real one — that is the whole point
/// of it.
fn reference_optimize_svg(src: &str) -> String {
let src = src.as_bytes();
let mut out: Vec<u8> = Vec::with_capacity(src.len());
let mut i = 0usize;
while i < src.len() {
let b = src[i];
if b == b'>' {
let mut j = i + 1;
while j < src.len() && src[j].is_ascii_whitespace() {
j += 1;
}
out.push(b'>');
i = if j > i + 1 && j < src.len() && src[j] == b'<' { j } else { i + 1 };
continue;
}
if b == b'-' || b.is_ascii_digit() {
let prev = if i == 0 { None } else { Some(src[i - 1]) };
let starts_token = !matches!(prev, Some(p) if p.is_ascii_digit() || p == b'.');
if starts_token {
if let Some(end) = noisy_number_end(src, i) {
let text = std::str::from_utf8(&src[i..end]).expect("ascii numeric token");
let short =
trim_fraction(text.parse::<f64>().expect("scanned literal parses"));
if short.len() < text.len() {
out.extend_from_slice(short.as_bytes());
i = end;
continue;
}
}
}
}
out.push(b);
i += 1;
}
String::from_utf8(out).expect("ascii only")
}
/// The optimizer is a fixed point. This is what lets a committed `.svgz` be
/// audited against the emitter with no pre-image: inflate, re-optimize, compare.
#[test]
fn optimize_svg_is_idempotent() {
let once = optimize_svg(&raw_page());
assert_eq!(optimize_svg(&once), once);
}
/// Both wasteful shapes are actually gone — stated in bytes, not as a feeling.
#[test]
fn optimize_svg_removes_indentation_and_float_noise() {
let raw = raw_page();
assert!(raw.contains(">\n "), "the fixture must still be indented emitter output");
assert!(raw.contains("32.592000000000006\""), "the fixture must still carry f64 noise");
let opt = optimize_svg(&raw);
let b = opt.as_bytes();
let mut between = 0usize;
for i in 0..b.len() {
if b[i] != b'>' {
continue;
}
let mut j = i + 1;
while j < b.len() && b[j].is_ascii_whitespace() {
j += 1;
}
if j > i + 1 && j < b.len() && b[j] == b'<' {
between += 1;
}
}
assert_eq!(between, 0, "no whitespace survives between two elements");
assert_eq!(noisy_literals(&opt), 0, "no literal keeps more than {KEPT_DECIMALS} decimals");
assert!(noisy_literals(&raw) > 100, "the fixture had noise to remove");
}
/// Count the `[-]?d+.d{KEPT+1,}` literals [`optimize_svg`] targets.
fn noisy_literals(src: &str) -> usize {
let b = src.as_bytes();
(0..b.len())
.filter(|&i| {
if b[i] != b'-' && !b[i].is_ascii_digit() {
return false;
}
let prev = if i == 0 { None } else { Some(b[i - 1]) };
if matches!(prev, Some(p) if p.is_ascii_digit() || p == b'.') {
return false;
}
noisy_number_end(b, i).is_some()
})
.count()
}
/// A glyph id is never mistaken for a coordinate. typst's `#g<32 hex>` ids share
/// the digit alphabet with numbers; the `.`-plus-5-decimals requirement is what
/// keeps them apart, and this states it rather than trusting it.
#[test]
fn optimize_svg_leaves_glyph_ids_alone() {
let src = r##"<use xlink:href="#g6C14D6BACFB5D2F655FD69CF7DA72632" x="17.164000000000001"/>"##;
let out = optimize_svg(src);
assert!(out.contains("#g6C14D6BACFB5D2F655FD69CF7DA72632"), "id intact: {out}");
assert!(out.contains(r##"x="17.164""##), "coordinate trimmed: {out}");
}
/// The committed bytes must be REPRODUCIBLE: two encodes of one page are the same
/// file, and the header fields that would otherwise vary — MTIME and the OS byte —
/// are pinned. Without this, every re-render churns the whole book in git.
#[test]
fn svgz_is_byte_stable_across_runs() {
let page = raw_page();
let a = to_svgz(&page);
assert_eq!(a, to_svgz(&page), "two encodes of the same page must be identical");
assert_eq!(&a[0..2], b"\x1f\x8b", "gzip magic");
assert_eq!(&a[4..8], &[0, 0, 0, 0], "MTIME is pinned to 0, not the build clock");
assert_eq!(a[9], 0xFF, "the OS byte is pinned to `unknown`, not the builder's OS");
}
/// A `.svgz` is a real gzip stream that inflates to the OPTIMIZED page — and it
/// is what a stock decoder reads, not only what [`from_svgz`] reads.
#[test]
fn svgz_round_trips_through_a_stock_gzip_reader() {
let page = raw_page();
let gz = to_svgz(&page);
let mut stock = String::new();
flate2::read::GzDecoder::new(&gz[..]).read_to_string(&mut stock).unwrap();
assert_eq!(stock, optimize_svg(&page), "a stock reader gets the optimized page");
assert_eq!(from_svgz(&gz).unwrap(), stock, "and `from_svgz` agrees with it");
assert!(stock.contains("<svg"), "which is SVG");
}
/// Garbage is refused rather than handed on as a "page".
#[test]
fn from_svgz_rejects_something_that_is_not_a_page() {
assert!(from_svgz(b"<svg>not compressed at all</svg>").is_err());
assert!(from_svgz(&[0x1f, 0x8b, 0x08, 0x00, 0x00]).is_err(), "a truncated stream");
}
/// The whole point, as one number on a real page.
#[test]
fn a_real_page_ships_much_smaller() {
let raw = raw_page();
let gz = to_svgz(&raw);
assert!(
raw.len() >= gz.len() * 5,
"{} B raw -> {} B .svgz is only {:.1}x",
raw.len(),
gz.len(),
raw.len() as f64 / gz.len() as f64,
);
}
// ── the GATE: `SvgzBook::check` ─────────────────────────────────────────────
//
// The owner, 2026-09-02: "a build-time or test-time GATE keeps them honest … plant
// one uncompressed page and one stale page and prove each fails." Every case
// below plants the fault and asserts the SPECIFIC field that must name it — a
// check that merely returned "not ok" would pass a much weaker test than these.
fn book(dir: &std::path::Path) -> SvgzBook<'_> {
SvgzBook { dir, digits: 3, keep: &["toc.tsv"] }
}
/// Two real pages, so the gate is exercised on genuine typst output rather than
/// on a stub whose every byte is the same.
fn two_pages() -> Vec<String> {
let p = raw_page();
vec![p.clone(), p.replace("841.8897637795276", "841.8897637795277")]
}
/// A freshly written book checks clean, and it is written COMPRESSED under the
/// expected names. (The green half — without it, every red below could be a
/// check that never returns clean.)
#[test]
fn a_freshly_written_book_checks_clean() {
let tmp = tempfile::tempdir().unwrap();
let b = book(tmp.path());
let pages = two_pages();
let stats = b.write(&pages).unwrap();
assert_eq!(stats.pages, 2);
assert!(stats.svgz_bytes * 5 < stats.raw_bytes, "the write actually compressed");
assert!(tmp.path().join("page-001.svgz").exists(), "zero-padded to `digits`");
let m = b.check(&pages).unwrap();
assert!(m.is_empty(), "a book just written must be in sync:\n{m}");
}
/// **RED 1 — a STALE page.** One page's source changed and nobody re-rendered.
/// The gate must name that page, and only that page.
#[test]
fn the_gate_catches_a_stale_page() {
let tmp = tempfile::tempdir().unwrap();
let b = book(tmp.path());
let mut pages = two_pages();
b.write(&pages).unwrap();
pages[1] = pages[1].replace("841.8897637795277", "800.1234567890123");
let m = b.check(&pages).unwrap();
assert!(!m.is_empty(), "an edited doc with an un-re-rendered book is a RED");
assert_eq!(m.stale, vec!["page-002.svgz".to_string()], "the changed page is named");
assert!(m.missing.is_empty() && m.extra.is_empty() && m.uncompressed.is_empty());
}
/// **RED 2 — an UNCOMPRESSED page.** A `page-NNN.svg` planted next to the book is
/// a second copy of a page riding into every binary that embeds the folder.
#[test]
fn the_gate_catches_an_uncompressed_page() {
let tmp = tempfile::tempdir().unwrap();
let b = book(tmp.path());
let pages = two_pages();
b.write(&pages).unwrap();
std::fs::write(tmp.path().join("page-001.svg"), raw_page()).unwrap();
let m = b.check(&pages).unwrap();
assert!(!m.is_empty(), "an uncompressed page in the book is a RED");
assert_eq!(m.uncompressed, vec!["page-001.svg".to_string()]);
assert!(m.stale.is_empty(), "the compressed page itself is still fine");
}
/// **RED 3 — a page the render no longer produces.** The book got shorter; the
/// orphan must be named, not silently embedded forever.
#[test]
fn the_gate_catches_an_orphan_page() {
let tmp = tempfile::tempdir().unwrap();
let b = book(tmp.path());
let pages = two_pages();
b.write(&pages).unwrap();
let m = b.check(&pages[..1]).unwrap();
assert_eq!(m.extra, vec!["page-002.svgz".to_string()], "the orphan is named");
assert!(m.stale.is_empty() && m.missing.is_empty());
}
/// **RED 4 — a MISSING page.** A page was deleted (or never written).
#[test]
fn the_gate_catches_a_missing_page() {
let tmp = tempfile::tempdir().unwrap();
let b = book(tmp.path());
let pages = two_pages();
b.write(&pages).unwrap();
std::fs::remove_file(tmp.path().join("page-002.svgz")).unwrap();
let m = b.check(&pages).unwrap();
assert_eq!(m.missing, vec!["page-002.svgz".to_string()]);
}
/// The gate is not fooled by the CLOCK. This is the reason [`to_svgz`] pins the
/// gzip MTIME: a check that compares bytes would call every page stale a second
/// after it was written if the header carried the build time.
#[test]
fn the_gate_is_not_a_timestamp_comparison() {
let tmp = tempfile::tempdir().unwrap();
let b = book(tmp.path());
let pages = two_pages();
b.write(&pages).unwrap();
// Re-write the same pages: byte-identical files, so still clean.
b.write(&pages).unwrap();
assert!(b.check(&pages).unwrap().is_empty(), "re-rendering unchanged docs is a no-op");
}
/// `write` sweeps what `check` would have called an orphan — the two halves of
/// the door agree, so a re-render never leaves a book the gate then rejects.
#[test]
fn write_sweeps_stale_and_uncompressed_pages() {
let tmp = tempfile::tempdir().unwrap();
let b = book(tmp.path());
let pages = two_pages();
b.write(&pages).unwrap();
std::fs::write(tmp.path().join("page-009.svg"), raw_page()).unwrap();
std::fs::write(tmp.path().join("toc.tsv"), "0\tOne\n").unwrap();
b.write(&pages[..1]).unwrap();
assert!(b.check(&pages[..1]).unwrap().is_empty(), "the sweep left a clean book");
assert!(!tmp.path().join("page-002.svgz").exists(), "the orphan is gone");
assert!(!tmp.path().join("page-009.svg").exists(), "the uncompressed page is gone");
assert_eq!(
std::fs::read_to_string(tmp.path().join("toc.tsv")).unwrap(),
"0\tOne\n",
"a `keep` sidecar is never swept",
);
}
}