write_english_book.rs

Le fichier Rust de l'exemple « Un roman entier en anglais ». Un roman anglais du domaine public lu en ses douze chapitres et mis en page sur soixante-deux pages numérotées, derrière une table des matières où l'on peut cliquer, chaque chapitre ouvrant sa propre page et le moteur choisissant chaque coupure de ligne.

Rust 145 lignes

À quoi sert cet exemple

La plupart des exemples de ce site tiennent sur une page, ce qui ne dit rien de ce qui se passe à la page soixante. Ici, tout Alice au pays des merveilles de Lewis Carroll, publié en 1865, entre tel que le fichier texte le contient et ressort en un seul PDF : une page de titre, un sommaire, douze chapitres, soixante-deux pages numérotées. Le programme reçoit une seule information sur ce texte — une ligne qui dit CHAPTER I ouvre un chapitre — et tous les titres, tous les en-têtes courants et tous les numéros de page en découlent.

Ce que montre cet exemple

  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);
    }
}