write_flow.rs

Die Rust-Datei des Beispiels „Einen Absatz umbrechen und ausrichten“. Ein in Zeilen umbrochener Absatz, dann auf vier Arten gesetzt: links, rechts, zentriert, Blocksatz.

Rust 190 Zeilen

Wofür dieses Beispiel da ist

Text, der länger ist als eine Beschriftung, muss in Zeilen umbrochen werden. Seine Länge schwankt, die Spalte hat eine feste Breite, und die Bibliothek entscheidet, wo jede Zeile endet. Der Umbruch wird Block für Block eingestellt: Die gewählte Spaltenbreite und Ausrichtung gelten für jede Zeile dieses Blocks, auf jeder Seite.

Was dieses Beispiel zeigt

  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
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
//! 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;

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

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 column, 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 colonne, 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.",
};

/// The page in Dutch.
const DUTCH: Words = Words {
    paragraph: "Deze bibliotheek zet tekst door een alinea te breken in regels die in een kolom passen en elke regel daarna volgens een uitlijning te plaatsen. Dezelfde woorden staan hieronder links uitgelijnd, rechts uitgelijnd, gecentreerd en uitgevuld, zodat het verschil in één oogopslag te zien is.",
};

/// The page in Portuguese.
const PORTUGUESE: Words = Words {
    paragraph: "Esta biblioteca compõe o texto partindo um parágrafo em linhas que cabem numa coluna e colocando depois cada linha segundo um alinhamento. As mesmas palavras aparecem abaixo alinhadas à esquerda, à direita, centradas e justificadas, para que a diferença se veja num relance.",
};

/// The page in Italian.
const ITALIAN: Words = Words {
    paragraph: "Questa libreria compone il testo spezzando un paragrafo in righe che stanno in una colonna, poi collocando ogni riga secondo un allineamento. Le stesse parole sono mostrate qui sotto allineate a sinistra, a destra, centrate e giustificate, così la differenza si vede a colpo d’occhio.",
};

/// The page in Spanish.
const SPANISH: Words = Words {
    paragraph: "Esta biblioteca compone el texto cortando un párrafo en líneas que caben en una columna y colocando luego cada línea según una alineación. Las mismas palabras se muestran abajo alineadas a la izquierda, a la derecha, centradas y justificadas, para que la diferencia se vea de un vistazo.",
};

/// The page in German.
const GERMAN: Words = Words {
    paragraph: "Diese Bibliothek setzt Text, indem sie einen Absatz in Zeilen umbricht, die in eine Spalte passen, und dann jede Zeile nach einer Ausrichtung platziert. Dieselben Wörter stehen unten linksbündig, rechtsbündig, zentriert und im Blocksatz, sodass der Unterschied zwischen ihnen auf einen Blick deutlich wird.",
};

/// 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); 7] = [
    (Language::English, &ENGLISH),
    (Language::French, &FRENCH),
    (Language::German, &GERMAN),
    (Language::Spanish, &SPANISH),
    (Language::Italian, &ITALIAN),
    (Language::Portuguese, &PORTUGUESE),
    (Language::Dutch, &DUTCH),
];

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

fn run() -> 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 align in [Align::Left, Align::Right, Align::Center, 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();

        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:?}"
        );
    }
}