write_english_book.rs

The Rust file of the “A whole novel in English” example. A public-domain English novel read into its twelve chapters and laid out across sixty-two numbered pages, behind a contents you can click, each chapter opening a page of its own and the engine choosing every line break.

Rust 145 lines

What this example is for

Most examples on this site are one page long, which says nothing about what happens at page sixty. Here the whole of Lewis Carroll's Alice's Adventures in Wonderland, published in 1865, goes in exactly as the plain text file holds it and comes out as one PDF: a title page, a contents, twelve chapters, sixty-two numbered pages. The program is told one thing and one thing only about that text — that a line reading CHAPTER I opens a chapter — and every heading, every running head and every page number follows from it.

What this example shows

  1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
//! Lays a whole English novel out and writes it as one PDF.
//!
//! It is what a document's weight really looks like. A novel draws the whole
//! alphabet in both cases, the digits and the punctuation, so the embedded font
//! carries far more outlines than an invoice does — and its metrics, which used
//! to be stated for every glyph of the face, are stated only for the glyphs the
//! book draws.
//!
//! Its twin, `write_french_book`, lays another novel out through the same code.
//! Neither is a translation of the other: each is a book of its own, and each
//! says how its own file marks a chapter.
//!
//! Usage: `cargo run --example write_english_book -- tmp/english_book.pdf
//! [font.ttf]`

use std::env;
use std::fs;
use std::path::{Path, PathBuf};

use hqf_pdf::{Document, Font};

#[path = "shared/out.rs"]
mod out;

#[path = "shared/licence.rs"]
mod licence;

#[path = "shared/book.rs"]
mod book;

#[path = "shared/failure.rs"]
mod failure;

/// The font the example draws with when none is given on the command line.
fn default_font() -> PathBuf {
    Path::new(env!("CARGO_MANIFEST_DIR"))
        .join("tests")
        .join("fonts")
        .join("DejaVuSans.ttf")
}

/// The novel this lays out, carried in full. Its file opens a chapter on a line
/// that reads `CHAPTER I.`, and it holds twelve of them.
const BOOK: book::Book = book::Book {
    title: "Alice's Adventures in Wonderland",
    author: "Lewis Carroll",
    file: "wonderland.txt",
    mark_before: "CHAPTER ",
    mark_after: ".",
    chapters: 12,
    contents_title: "Contents",
    // The file already sets its titles the way English does.
    titles: &[],
    // English sets its marks tight against the word they close, and the file
    // already draws its dashes.
    typography: book::Typography {
        space_before: &[],
        space_after: &[],
        dash: None,
    },
};

fn main() -> std::process::ExitCode {
    failure::reported(run())
}

fn run() -> Result<(), Box<dyn std::error::Error>> {
    let mut args = env::args().skip(1);
    let out = args
        .next()
        .unwrap_or_else(|| out::default_path("english_book"));
    let font_path = args.next().map_or_else(default_font, PathBuf::from);

    let mut doc = Document::new();
    doc.set_license(licence::licensed());
    let handle = doc.add_font(Font::parse(fs::read(&font_path)?)?);

    let text = fs::read_to_string(BOOK.path())?;
    book::lay_out(&mut doc, &handle, &BOOK, &text)?;

    let bytes = doc.to_bytes()?;
    if let Some(parent) = Path::new(&out).parent() {
        fs::create_dir_all(parent)?;
    }
    fs::write(&out, &bytes)?;
    println!("wrote {out}: {} bytes", bytes.len());
    Ok(())
}

#[cfg(test)]
mod tests {
    use hqf_pdf::{Document, Font, FontHandle};

    use super::{BOOK, book, default_font};

    /// The font every measurement here is taken in, which is the one the
    /// example draws with when the command line says nothing.
    fn font(document: &mut Document) -> FontHandle {
        document.add_font(
            Font::parse(std::fs::read(default_font()).expect("the committed font is there"))
                .expect("the committed font parses"),
        )
    }

    #[test]
    fn every_chapter_of_the_novel_is_headed_and_carries_prose() {
        book::check_every_chapter_is_headed(&BOOK);
    }

    #[test]
    fn no_chapter_is_headed_in_capitals_throughout() {
        book::check_no_title_is_set_in_capitals(&BOOK);
    }

    #[test]
    fn the_titles_drawn_are_the_ones_the_file_carries() {
        book::check_the_titles_only_change_case(&BOOK);
    }

    /// This book holds no mark off the word beside it, and is drawn with no
    /// no-break space anywhere: what the other book sets does not reach it.
    #[test]
    fn the_marks_are_spaced_the_way_this_book_sets_them() {
        assert!(BOOK.typography.space_before.is_empty());
        assert!(BOOK.typography.space_after.is_empty());

        book::check_the_marks_are_spaced_the_way_the_book_sets_them(&BOOK);
    }

    #[test]
    fn the_contents_points_at_every_chapter() {
        let mut document = Document::new();
        let handle = font(&mut document);

        book::check_the_contents_points_at_every_chapter(&handle, &BOOK);
    }

    #[test]
    fn the_running_heads_stay_on_one_line() {
        let mut document = Document::new();
        let handle = font(&mut document);

        book::check_the_running_heads_stay_on_one_line(&BOOK, &handle);
    }
}