write_table_borders.rs

Die Rust-Datei des Beispiels „Außenabstand, Rahmen, Innenabstand und Füllung einer Zelle“. Was eine Zelle mit ihrem eigenen Rechteck tun kann: der Außenabstand, der Rahmen, der Innenraum, der Hintergrund.

Rust 413 Zeilen

Wofür dieses Beispiel da ist

Fast jede Beschwerde über eine von einem Programm erzeugte Tabelle ist in Wahrheit eine Beschwerde über den Raum in ihren Zellen: Zahlen, die die Linie darüber berühren, eine an ihren Rahmen gedrängte Überschrift, Spalten, die ungleich wirken, weil der Innenabstand nicht überall gleich ist. Nichts davon zeigt sich im Code, und alles zeigt sich auf dem Papier.

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
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
//! Shows what a cell can do with its own box: the margin around it, the border
//! on each of its sides, and the padding within it.
//!
//! The table declares no rule of its own, so every line on the page is drawn by
//! the cell it belongs to. That is the whole point of a cell border: it is the
//! cell's, not the grid's, and a cell can carry one on a single side.
//!
//! Every word the sheet draws is held in `Words`, once per language, and
//! `HQF_PDF_LANG` picks which one is drawn.
//!
//! Usage: `cargo run --example write_table_borders -- tmp/borders.pdf
//! [font.ttf]`
//!        `HQF_PDF_LANG=fr cargo run --example write_table_borders --
//! tmp/bordures.pdf`

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

use hqf_pdf::content::Content;
use hqf_pdf::layout::{
    Border, Cell, ColumnWidth, Columns, Margin, Padding, Row, Stroke, Table, TableFrame, VAlign,
};
use hqf_pdf::{Align, Document, Font, Page, Rgb};

#[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.
fn default_font() -> PathBuf {
    Path::new(env!("CARGO_MANIFEST_DIR"))
        .join("tests")
        .join("fonts")
        .join("DejaVuSans.ttf")
}

/// The margin around the table, in points.
const MARGIN: f64 = 56.0;
/// The top of the table.
const TOP: f64 = 780.0;
/// A4's width, which the table is fitted across.
const PAGE_WIDTH: f64 = 595.276;
/// The width left to the table between the margins.
const TABLE_WIDTH: f64 = PAGE_WIDTH - 2.0 * MARGIN;

/// How many samples the sheet shows.
const SAMPLE_COUNT: usize = 13;

/// The words the sheet is written in, one set per language.
#[derive(Debug)]
struct Words {
    /// The line set over the sheet.
    title: &'static str,
    /// The word drawn inside every demonstration cell.
    cell: &'static str,
    /// What each sample demonstrates, in the order `samples` builds them.
    samples: [&'static str; SAMPLE_COUNT],
}

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

/// The sheet in English.
const ENGLISH: Words = Words {
    title: "Borders, margins and padding",
    cell: "Cell",
    samples: [
        "The top, and nothing else",
        "The bottom, and nothing else",
        "The left",
        "The right",
        "The top and the bottom",
        "All four sides",
        "A 0.25 pt rule",
        "A 2.5 pt rule",
        "A weight of its own on each side",
        "A rule in colour",
        "A margin holds the cell off its column",
        "A negative margin runs it past its column instead",
        "Padding holds the text off the rule",
    ],
};

/// The sheet in French.
const FRENCH: Words = Words {
    title: "Bordures, marges et retraits",
    cell: "Cellule",
    samples: [
        "Le haut, et rien d’autre",
        "Le bas, et rien d’autre",
        "La gauche",
        "La droite",
        "Le haut et le bas",
        "Les quatre côtés",
        "Un filet de 0.25 pt",
        "Un filet de 2.5 pt",
        "Une épaisseur par côté",
        "Un filet en couleur",
        "Une marge écarte la cellule de sa colonne",
        "Une marge négative la fait déborder de sa colonne",
        "Un retrait écarte le texte du filet",
    ],
};

/// The sheet in Dutch.
const DUTCH: Words = Words {
    title: "Randen, marges en opvulling",
    cell: "Cel",
    samples: [
        "Boven, en verder niets",
        "Onder, en verder niets",
        "Links",
        "Rechts",
        "Boven en onder",
        "Alle vier de zijden",
        "Een lijn van 0,25 pt",
        "Een lijn van 2,5 pt",
        "Een eigen dikte aan elke zijde",
        "Een lijn in kleur",
        "Een marge houdt de cel van haar kolom af",
        "Een negatieve marge laat haar juist voorbij haar kolom lopen",
        "Opvulling houdt de tekst van de lijn af",
    ],
};

/// The sheet in Portuguese.
const PORTUGUESE: Words = Words {
    title: "Contornos, margens e espaçamento interno",
    cell: "Célula",
    samples: [
        "Em cima, e mais nada",
        "Em baixo, e mais nada",
        "À esquerda",
        "À direita",
        "Em cima e em baixo",
        "Os quatro lados",
        "Um filete de 0,25 pt",
        "Um filete de 2,5 pt",
        "Uma espessura própria em cada lado",
        "Um filete a cores",
        "Uma margem afasta a célula da sua coluna",
        "Uma margem negativa fá-la sair da coluna",
        "O espaçamento interno afasta o texto do filete",
    ],
};

/// The sheet in Italian.
const ITALIAN: Words = Words {
    title: "Bordi, margini e spaziatura interna",
    cell: "Cella",
    samples: [
        "Sopra, e nient’altro",
        "Sotto, e nient’altro",
        "A sinistra",
        "A destra",
        "Sopra e sotto",
        "I quattro lati",
        "Un filetto da 0,25 pt",
        "Un filetto da 2,5 pt",
        "Uno spessore proprio per ogni lato",
        "Un filetto colorato",
        "Un margine stacca la cella dalla sua colonna",
        "Un margine negativo la fa uscire dalla sua colonna",
        "La spaziatura interna stacca il testo dal filetto",
    ],
};

/// The sheet in Spanish.
const SPANISH: Words = Words {
    title: "Bordes, márgenes y relleno",
    cell: "Celda",
    samples: [
        "Arriba, y nada más",
        "Abajo, y nada más",
        "A la izquierda",
        "A la derecha",
        "Arriba y abajo",
        "Los cuatro lados",
        "Un filete de 0,25 pt",
        "Un filete de 2,5 pt",
        "Un grosor propio en cada lado",
        "Un filete en color",
        "Un margen separa la celda de su columna",
        "Un margen negativo la hace salir de su columna",
        "El relleno separa el texto del filete",
    ],
};

/// The sheet in German.
const GERMAN: Words = Words {
    title: "Rahmen, Außenabstände und Innenabstände",
    cell: "Zelle",
    samples: [
        "Oben, und sonst nichts",
        "Unten, und sonst nichts",
        "Links",
        "Rechts",
        "Oben und unten",
        "Alle vier Seiten",
        "Eine Linie von 0,25 pt",
        "Eine Linie von 2,5 pt",
        "Eine eigene Stärke auf jeder Seite",
        "Eine farbige Linie",
        "Ein Außenabstand hält die Zelle von ihrer Spalte fern",
        "Ein negativer Außenabstand lässt sie stattdessen über ihre Spalte hinauslaufen",
        "Der Innenabstand hält den Text von der Linie fern",
    ],
};

/// 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),
];

/// One line of the sheet: the cell that shows a sample, and how it is built.
struct Sample {
    /// The border the demonstration cell carries.
    border: Border,
    /// The margin holding that cell off its column.
    margin: Margin,
    /// The padding holding its text off its border.
    padding: Padding,
    /// The background the cell paints, if any.
    fill: Option<Rgb>,
}

/// The samples, in the order `Words::samples` names them.
fn samples() -> Vec<Sample> {
    let rule = Stroke::black(0.8);
    let hairline = Stroke::black(0.25);
    let heavy = Stroke::black(2.5);
    let ink = Stroke::new(1.0, Rgb::new(0.78, 0.14, 0.12));

    let plain = |border| Sample {
        border,
        margin: Margin::symmetric(0.0, 3.0),
        padding: Padding::symmetric(6.0, 4.0),
        fill: None,
    };

    vec![
        plain(Border::none().top(&rule)),
        plain(Border::none().bottom(&rule)),
        plain(Border::none().left(&rule)),
        plain(Border::none().right(&rule)),
        plain(Border::none().top(&rule).bottom(&rule)),
        plain(Border::uniform(&rule)),
        plain(Border::uniform(&hairline)),
        plain(Border::uniform(&heavy)),
        plain(
            Border::none()
                .top(&hairline)
                .bottom(&heavy)
                .left(&rule)
                .right(&rule),
        ),
        plain(Border::uniform(&ink)),
        Sample {
            border: Border::uniform(&rule),
            margin: Margin::uniform(10.0),
            padding: Padding::symmetric(6.0, 4.0),
            fill: Some(Rgb::gray(0.92)),
        },
        Sample {
            border: Border::uniform(&rule),
            margin: Margin::symmetric(-10.0, 3.0),
            padding: Padding::symmetric(6.0, 4.0),
            fill: Some(Rgb::gray(0.92)),
        },
        Sample {
            border: Border::uniform(&rule),
            margin: Margin::symmetric(0.0, 3.0),
            padding: Padding::uniform(14.0),
            fill: Some(Rgb::gray(0.92)),
        },
    ]
}

/// The sheet: a line per sample, its name on the left and the cell that shows
/// it on the right.
fn border_sheet<'a>(
    text: &'a hqf_pdf::FontHandle,
    words: &'static Words,
) -> Result<Table<'a>, hqf_pdf::Error> {
    // No rule is declared on this table: every line the page shows is a cell's
    // own border.
    let columns = Columns::new(
        vec![ColumnWidth::Fraction(1.0), ColumnWidth::Points(210.0)],
        TABLE_WIDTH,
    )?;
    let mut table = Table::new(columns);

    for (sample, label) in samples().into_iter().zip(words.samples) {
        let mut demonstration = Cell::new(text, 9.0, words.cell)
            .align(Align::Center)
            .valign(VAlign::Middle)
            .border(sample.border.clone())
            .margin(sample.margin)
            .padding(sample.padding);
        if let Some(fill) = sample.fill {
            demonstration = demonstration.fill(fill);
        }

        table.push(
            Row::new()
                .cell(
                    Cell::new(text, 9.0, label)
                        .valign(VAlign::Middle)
                        .padding(Padding::symmetric(0.0, 4.0)),
                )
                .cell(demonstration)
                .min_height(34.0),
        );
    }

    Ok(table)
}

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("table_borders")));
    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 text = doc.add_font(font);

    let table = border_sheet(&text, words)?;

    let mut content = Content::new();
    content.begin_text();
    content.set_font(&text, 14.0)?;
    content.text_origin(MARGIN, TOP + 24.0)?;
    content.show_glyphs(&text.glyphs(words.title));
    content.end_text();

    let placed = table.fit(TableFrame::new(MARGIN, TOP, TOP - MARGIN), 0)?;
    placed.draw(&mut content)?;

    let mut page = Page::a4();
    page.content = content.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, 1 page, {} rows",
        bytes.len(),
        table.row_count()
    );
    Ok(())
}

#[cfg(test)]
mod tests {
    use super::{WORDS, language};

    /// The lines two languages are allowed to write the same way, where two
    /// languages spell a word alike. Every word is a word of the language it is
    /// written in.
    const SPARED: [&str; 2] = [
        // Written the same way in Dutch.
        r#""Links""#,
        r#""Rechts""#,
    ];

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

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