Letters you draw yourself

A tick, a cross and a dash drawn by the program, added to the document as a font, and set among ordinary words.

Rust write_drawn_letters.rs 333 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
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
//! Sets text in a font whose letters are drawings rather than outlines read
//! from a file.
//!
//! Two fonts are built here, both out of content operators. The first paints
//! its own colours, which no font read from a file can do: a green tick, a red
//! cross and a grey dash, set inline among ordinary words. The second states
//! shapes and no colour, so each is painted in whatever colour the text around
//! it is in — the same square comes out red on one line and blue on the next.
//!
//! The letters still copy out of the file as the characters they stand for,
//! because the font carries the map back to them.
//!
//! Both sets of words are held in `Words`, once per language, and
//! `HQF_PDF_LANG` picks which set is drawn.
//!
//! Usage: `cargo run --example write_drawn_letters -- tmp/drawn.pdf [font.ttf]`
//!        `HQF_PDF_LANG=fr cargo run --example write_drawn_letters`

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

use hqf_pdf::content::Content;
use hqf_pdf::{
    Document, Font, FontHandle, Page, Rect, Rgb, RichText, Style, TextFlow, Type3Font, Type3Glyph,
};

#[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 its words with when none is given on the command
/// line: the one committed for the tests, so 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 page's title.
    title: &'static str,
    /// What the page is about.
    lead: &'static str,
    /// The heading over the letters that paint themselves.
    painted: &'static str,
    /// The line the coloured letters are set in, before each of them.
    checks: [&'static str; 3],
    /// The heading over the letters that take the colour around them.
    shaped: &'static str,
    /// What the two coloured lines say, before the square.
    tinted: [&'static str; 2],
    /// The closing note about copying the text out.
    note: &'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 {
    title: "Letters you draw yourself",
    lead: "The three marks below are not in any font file. Each one is a \
           drawing this program made, added to the document as a font, and set \
           in a line of text like any other letter. Copy the line out of this \
           file and the marks come back as the characters they stand for.",
    painted: "Marks that paint themselves",
    checks: ["Invoice sent", "Payment received", "Delivery note"],
    shaped: "Marks that take the colour around them",
    tinted: ["Overdue", "Settled"],
    note: "A mark of the first kind carries its own colour, which no font read \
           from a file can do. A mark of the second kind states a shape and \
           nothing else, so the colour of the words it sits among is the colour \
           it comes out in.",
};

/// The page in French.
const FRENCH: Words = Words {
    title: "Des lettres que vous dessinez vous-même",
    lead: "Les trois marques ci-dessous ne viennent d'aucun fichier de police. \
           Chacune est un dessin fait par ce programme, ajouté au document \
           comme une police, et posé dans une ligne de texte comme n'importe \
           quelle lettre. Copiez la ligne hors de ce fichier et les marques \
           reviennent sous la forme des caractères qu'elles représentent.",
    painted: "Des marques qui posent leur propre couleur",
    checks: ["Facture envoyée", "Paiement reçu", "Bon de livraison"],
    shaped: "Des marques qui prennent la couleur autour d'elles",
    tinted: ["En retard", "Réglé"],
    note: "Une marque de la première sorte porte sa couleur, ce qu'aucune \
           police lue dans un fichier ne sait faire. Une marque de la seconde \
           n'énonce qu'une forme, donc elle sort dans la couleur des mots \
           qu'elle accompagne.",
};

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

/// The matrix that puts a drawn font on the thousandth scale a font file uses,
/// so a width of 600 is six tenths of the size the text is set at.
const THOUSANDTH: [f64; 6] = [0.001, 0.0, 0.0, 0.001, 0.0, 0.0];

/// How wide every drawn glyph is, in glyph space: one em.
const GLYPH_WIDTH: f64 = 1000.0;

/// How thick a drawn stroke is, in glyph space.
const STROKE: f64 = 150.0;

/// The left edge of everything on the page.
const LEFT: f64 = 72.0;

/// How wide a block of text is.
const WIDTH: f64 = 450.0;

/// The character the tick is set with.
const TICK: char = '\u{2713}';
/// The character the cross is set with.
const CROSS: char = '\u{2717}';
/// The character the dash is set with.
const DASH: char = '\u{2013}';
/// The character the square is set with.
const SQUARE: char = '\u{25A0}';

/// The box every drawn glyph stands in, in glyph space.
const fn glyph_box() -> Rect {
    Rect::new(0.0, -200.0, GLYPH_WIDTH, 1000.0)
}

/// A tick, stroked in `ink`.
fn tick(ink: Rgb) -> Result<Content, Box<dyn std::error::Error>> {
    let mut pen = Content::new();
    pen.set_stroke(ink)?.set_line_width(STROKE)?;
    pen.move_to(140.0, 480.0)?
        .line_to(390.0, 230.0)?
        .line_to(870.0, 740.0)?;
    pen.stroke();
    Ok(pen)
}

/// A cross, stroked in `ink`.
fn cross(ink: Rgb) -> Result<Content, Box<dyn std::error::Error>> {
    let mut pen = Content::new();
    pen.set_stroke(ink)?.set_line_width(STROKE)?;
    pen.move_to(200.0, 230.0)?.line_to(810.0, 740.0)?;
    pen.move_to(810.0, 230.0)?.line_to(200.0, 740.0)?;
    pen.stroke();
    Ok(pen)
}

/// A dash, filled in `ink`.
fn dash(ink: Rgb) -> Result<Content, Box<dyn std::error::Error>> {
    let mut pen = Content::new();
    pen.set_fill(ink)?.rect(160.0, 400.0, 680.0, STROKE)?;
    pen.fill();
    Ok(pen)
}

/// A filled square, in no colour of its own.
fn square() -> Result<Content, Box<dyn std::error::Error>> {
    let mut pen = Content::new();
    pen.rect(200.0, 150.0, 600.0, 600.0)?;
    pen.fill();
    Ok(pen)
}

/// The font whose three marks paint themselves.
fn painted_font() -> Result<Type3Font, Box<dyn std::error::Error>> {
    let glyphs = vec![
        Type3Glyph::painted(TICK, GLYPH_WIDTH, tick(Rgb::new(0.15, 0.55, 0.25))?),
        Type3Glyph::painted(CROSS, GLYPH_WIDTH, cross(Rgb::new(0.75, 0.15, 0.15))?),
        Type3Glyph::painted(DASH, GLYPH_WIDTH, dash(Rgb::gray(0.55))?),
    ];
    Ok(Type3Font::new(THOUSANDTH, glyph_box(), glyphs)?)
}

/// The font whose one mark takes the colour of the text around it.
fn shaped_font() -> Result<Type3Font, Box<dyn std::error::Error>> {
    let bounds = Rect::new(200.0, 150.0, 600.0, 600.0);
    let glyphs = vec![Type3Glyph::shaped(SQUARE, GLYPH_WIDTH, bounds, square()?)];
    Ok(Type3Font::new(THOUSANDTH, glyph_box(), glyphs)?)
}

/// Sets a block of words with its first baseline at `top`, and hands back the
/// ordinate the block ends at.
fn block(
    c: &mut Content,
    handle: &FontHandle,
    size: f64,
    top: f64,
    text: &str,
) -> Result<f64, Box<dyn std::error::Error>> {
    let flow = TextFlow::new(handle, size);
    let lines = flow.break_lines(text, WIDTH);
    c.begin_text();
    flow.draw(c, &lines, LEFT, top, WIDTH)?;
    c.end_text();
    Ok(top - flow.height(&lines))
}

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("drawn_letters")));
    let font_path = args.next().map_or_else(default_font, PathBuf::from);

    let mut doc = Document::new();
    doc.set_license(licence::licensed());
    let text = doc.add_font(Font::parse(fs::read(&font_path)?)?);
    let marks = doc.add_type3_font(painted_font()?);
    let shapes = doc.add_type3_font(shaped_font()?);

    let mut c = Content::new();

    let mut top = 760.0;
    top = block(&mut c, &text, 18.0, top, words.title)? - 14.0;
    top = block(&mut c, &text, 10.0, top, words.lead)? - 26.0;

    top = block(&mut c, &text, 12.0, top, words.painted)? - 10.0;
    let label = Style::new(&text, 11.0);
    let mark = Style::new(&marks, 11.0);
    let mut line = RichText::new();
    for (index, check) in words.checks.iter().enumerate() {
        let drawn = [TICK, CROSS, DASH][index];
        // The gap after a mark is set in the words' own font: a font of
        // drawings holds the marks and nothing else, so a space in it would be
        // a space it cannot draw.
        line = line
            .push(drawn.to_string(), &mark)
            .push(format!("\u{00A0}{check}"), &label)
            .push(
                if index == words.checks.len() - 1 {
                    ""
                } else {
                    "    "
                },
                &label,
            );
    }
    let lines = line.break_lines(WIDTH);
    line.draw(&mut c, &lines, LEFT, top, WIDTH)?;
    top -= RichText::height(&lines) + 26.0;

    top = block(&mut c, &text, 12.0, top, words.shaped)? - 10.0;
    for (index, tinted) in words.tinted.iter().enumerate() {
        let ink = if index == 0 {
            Rgb::new(0.75, 0.15, 0.15)
        } else {
            Rgb::new(0.15, 0.35, 0.75)
        };
        let shape = Style::new(&shapes, 11.0).color(ink);
        let word = Style::new(&text, 11.0).color(ink);
        let row = RichText::new()
            .push(SQUARE.to_string(), &shape)
            .push(format!("\u{00A0}{tinted}"), &word);
        let drawn = row.break_lines(WIDTH);
        row.draw(&mut c, &drawn, LEFT, top, WIDTH)?;
        top -= RichText::height(&drawn) + 4.0;
    }

    top -= 22.0;
    block(&mut c, &text, 10.0, top, words.note)?;

    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::{CROSS, DASH, SQUARE, TICK, WORDS, language, painted_font, shaped_font};
    use hqf_pdf::GlyphId;

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

    #[test]
    fn the_painted_font_draws_the_three_marks_the_page_sets() {
        let font = painted_font().expect("the marks are drawn");

        assert_eq!(font.glyph_count(), 3);
        for mark in [TICK, CROSS, DASH] {
            assert!(font.glyph(mark).is_some(), "{mark} is drawn");
        }
    }

    #[test]
    fn the_shaped_font_draws_one_mark_and_gives_it_the_first_code() {
        let font = shaped_font().expect("the square is drawn");

        assert_eq!(font.glyph(SQUARE), Some(GlyphId(0)));
        assert_eq!(font.glyph_count(), 1);
    }
}