DEV Community

Cover image for The Page Order Math Behind Saddle-Stitch Booklets Is Weirder Than You Think. So I Automated It. [Devlog #8]
hiyoyo
hiyoyo

Posted on

The Page Order Math Behind Saddle-Stitch Booklets Is Weirder Than You Think. So I Automated It. [Devlog #8]

All tests run on an 8-year-old MacBook Air.

Print pages 1, 2, 3, 4 in order, fold the paper — and the result is wrong.

Booklet printing requires a completely different page ordering. For an 8-page booklet, you print [8, 1] on the front of sheet 1, [2, 7] on the back. This is called imposition, and it's a paid feature in InDesign and Acrobat.

I built it in Rust.


The math

For a saddle-stitch booklet, total pages must be a multiple of 4 (blank pages pad the remainder). Each sheet carries 4 logical pages: front-left, front-right, back-left, back-right.

pub fn compute_imposition(total_pages: u32) -> Vec<(u32, u32, u32, u32)> {
    // Round up to nearest multiple of 4
    let padded = ((total_pages + 3) / 4) * 4;
    let sheets = padded / 4;
    let mut layout: Vec<(u32, u32, u32, u32)> = Vec::new();

    for i in 0..sheets {
        let front_right = i + 1;
        let front_left = padded - i;
        let back_left = i + 2;
        let back_right = padded - i - 1;

        // (front_left, front_right, back_left, back_right)
        layout.push((front_left, front_right, back_left, back_right));
    }

    layout
}
Enter fullscreen mode Exit fullscreen mode

That layout then drives a lopdf reordering pass to produce the print-ready PDF:

pub fn build_imposition_pdf(
    original: &Document,
    layout: &[(u32, u32, u32, u32)],
) -> Result {
    let mut new_doc = Document::with_version("1.5");

    for (front_left, front_right, back_left, back_right) in layout {
        add_sheet(&mut new_doc, original, *front_left, *front_right)?;
        add_sheet(&mut new_doc, original, *back_left, *back_right)?;
    }

    Ok(new_doc)
}
Enter fullscreen mode Exit fullscreen mode

Auto TOC

The second Publisher feature: automatic table of contents generation.

Heuristic heading detection — short lines that don't end with sentence-ending punctuation get flagged as headings:

pub fn detect_headings(doc: &Document) -> Vec<(u32, String)> {
    let mut headings: Vec<(u32, String)> = Vec::new();

    for (page_num, _) in doc.get_pages() {
        if let Ok(text) = doc.extract_text(&[page_num]) {
            let first_line = text.lines().next().unwrap_or("").trim();

            if first_line.len() > 2
                && first_line.len() < 60
                && !first_line.ends_with('。')
                && !first_line.ends_with('.')
            {
                headings.push((page_num, first_line.to_string()));
            }
        }
    }

    headings
}
Enter fullscreen mode Exit fullscreen mode

The detected headings become a linked TOC page prepended to the PDF via lopdf.


Where I got stuck

Blank page sizing — when page count isn't a multiple of 4, blank pages fill the gap. They need to match the source PDF's page dimensions exactly or the print layout breaks.

Gutter margins — the fold point needs extra inner margin or text near the spine disappears into the crease. Required an automatic margin offset during the sheet assembly step.


Next devlog

Sanctuary Viewer — opening PDFs without leaving a trace anywhere on macOS. No recent files, no cache, no history. The file was never opened.


Hiyoko PDF Vault → https://hiyokoko.gumroad.com/l/HiyokoPDFVault
X → @hiyoyok

Top comments (0)