Breaking a paragraph up, and aligning it

A paragraph broken into lines, set four ways: left, right, centred, justified.

Rust write_flow.rs 157 lines
  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
146
147
148
149
150
151
152
153
154
155
156
157
//! Lays a paragraph out four ways and writes the result, to see the alignment.
//!
//! The paragraph is held in `Words`, once per language, and `HQF_PDF_LANG`
//! picks which one is laid out.
//!
//! Usage: `cargo run --example write_flow -- tmp/flow.pdf [font.ttf]`
//!        `HQF_PDF_LANG=fr cargo run --example write_flow -- tmp/flux.pdf`

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

use hqf_pdf::content::Content;
use hqf_pdf::layout::Align;
use hqf_pdf::{Document, Font, Page, TextFlow};

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

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

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

use language::Language;

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

/// The words the page is written in, one set per language.
#[derive(Debug)]
struct Words {
    /// The paragraph the four alignments are shown on.
    paragraph: &'static str,
}

impl Words {
    /// The words the page is written in, in `language`.
    fn of(language: Language) -> &'static Self {
        language::pick(&WORDS, language)
    }
}

/// The page in English.
const ENGLISH: Words = Words {
    paragraph: "This library lays text out by breaking a paragraph into lines \
that fit a box, then placing each line according to an alignment. The same words \
are shown below flush left, flush right, centred, and justified, so the \
difference between them is plain to see at a glance.",
};

/// The page in French.
const FRENCH: Words = Words {
    paragraph: "Cette bibliothèque met le texte en page en découpant un \
paragraphe en lignes qui tiennent dans une boîte, puis en plaçant chaque ligne \
selon un alignement. Les mêmes mots sont repris ci-dessous alignés à gauche, \
alignés à droite, centrés, puis justifiés, pour que la différence entre eux se \
voie d'un seul coup d'œil.",
};

/// Every language the example is written in. A language is added by writing its
/// own set of words and naming it here.
static WORDS: [(Language, &Words); 2] =
    [(Language::English, &ENGLISH), (Language::French, &FRENCH)];

fn main() -> Result<(), Box<dyn std::error::Error>> {
    let language = Language::from_environment()?;
    let words = Words::of(language);

    let mut args = env::args().skip(1);
    // A named file is written as named; the default one carries the language,
    // so the two languages do not overwrite each other in `tmp/`.
    let out = args
        .next()
        .unwrap_or_else(|| language.file_name(&out::default_path("flow")));
    let font_path = args.next().map_or_else(default_font, PathBuf::from);

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

    let mut c = Content::new();
    let box_width = 400.0;
    let x = 80.0;
    let mut top = 780.0;

    for (label, align) in [
        ("Left", Align::Left),
        ("Right", Align::Right),
        ("Center", Align::Center),
        ("Justify", Align::Justify),
    ] {
        let flow = TextFlow::new(&handle, 11.0).align(align).leading(15.0);
        let lines = flow.break_lines(words.paragraph, box_width);

        c.begin_text();
        flow.draw(&mut c, &lines, x, top, box_width)?;
        c.end_text();

        // A frame around the text column, inset from the glyphs on every side.
        let height = flow.height(&lines);
        let pad = 8.0;
        c.save_state();
        c.set_stroke_rgb(0.8, 0.8, 0.8)?
            .set_line_width(0.5)?
            .rect(
                x - pad,
                top - height - pad,
                pad.mul_add(2.0, box_width),
                pad.mul_add(2.0, height),
            )?
            .stroke();
        c.restore_state();

        let _ = label;
        top -= height + 30.0;
    }

    let mut page = Page::a4();
    page.content = c.into_bytes();
    doc.add_page(page)?;

    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 super::{WORDS, language};

    /// The lines two languages are allowed to write the same way. There are
    /// none: every word is a word of the language it is written in.
    const SPARED: [&str; 0] = [];

    #[test]
    fn every_language_lays_the_page_out_in_its_own_words() {
        let untranslated = language::untranslated_lines(&WORDS, &SPARED);

        assert!(
            untranslated.is_empty(),
            "the page says these in more than one language: {untranslated:?}"
        );
    }
}