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.

Python write_drawn_letters.py 267 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
"""Sets text in a font whose letters are drawings rather than outlines read from a file.

The Python twin of the `write_drawn_letters` example in Rust. 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: python examples/write_drawn_letters.py [out.pdf] [font.ttf]
       HQF_PDF_LANG=fr python examples/write_drawn_letters.py
"""

from __future__ import annotations

from dataclasses import dataclass
from pathlib import Path

import _language
import _licence
import _out

import hqf_pdf


@dataclass(frozen=True)
class Words:
    """Every word the page draws, in one language."""

    # The page's title.
    title: str
    # What the page is about.
    lead: str
    # The heading over the letters that paint themselves.
    painted: str
    # The line the coloured letters are set in, before each of them.
    checks: tuple[str, str, str]
    # The heading over the letters that take the colour around them.
    shaped: str
    # What the two coloured lines say, before the square.
    tinted: tuple[str, str]
    # The closing note about copying the text out.
    note: str


# The page in English.
ENGLISH = 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.
FRENCH = 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.
WORDS = {_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.
THOUSANDTH = (0.001, 0.0, 0.0, 0.001, 0.0, 0.0)

# How wide every drawn glyph is, in glyph space: one em.
GLYPH_WIDTH = 1000.0

# How thick a drawn stroke is, in glyph space.
STROKE = 150.0

# The left edge of everything on the page.
LEFT = 72.0

# How wide a block of text is.
WIDTH = 450.0

# The character the tick is set with.
TICK = "✓"
# The character the cross is set with.
CROSS = "✗"
# The character the dash is set with.
DASH = "–"
# The character the square is set with.
SQUARE = "■"


def glyph_box() -> hqf_pdf.Rect:
    """The box every drawn glyph stands in, in glyph space."""
    return hqf_pdf.Rect(0.0, -200.0, GLYPH_WIDTH, 1000.0)


def tick(ink: hqf_pdf.Rgb) -> hqf_pdf.Content:
    """A tick, stroked in `ink`."""
    pen = hqf_pdf.Content()
    pen.set_stroke(ink)
    pen.set_line_width(STROKE)
    pen.move_to(140.0, 480.0)
    pen.line_to(390.0, 230.0)
    pen.line_to(870.0, 740.0)
    pen.stroke()
    return pen


def cross(ink: hqf_pdf.Rgb) -> hqf_pdf.Content:
    """A cross, stroked in `ink`."""
    pen = hqf_pdf.Content()
    pen.set_stroke(ink)
    pen.set_line_width(STROKE)
    pen.move_to(200.0, 230.0)
    pen.line_to(810.0, 740.0)
    pen.move_to(810.0, 230.0)
    pen.line_to(200.0, 740.0)
    pen.stroke()
    return pen


def dash(ink: hqf_pdf.Rgb) -> hqf_pdf.Content:
    """A dash, filled in `ink`."""
    pen = hqf_pdf.Content()
    pen.set_fill(ink)
    pen.rect(160.0, 400.0, 680.0, STROKE)
    pen.fill()
    return pen


def square() -> hqf_pdf.Content:
    """A filled square, in no colour of its own."""
    pen = hqf_pdf.Content()
    pen.rect(200.0, 150.0, 600.0, 600.0)
    pen.fill()
    return pen


def painted_font() -> hqf_pdf.Type3Font:
    """The font whose three marks paint themselves."""
    glyphs = [
        hqf_pdf.Type3Glyph.painted(TICK, GLYPH_WIDTH, tick(hqf_pdf.Rgb(0.15, 0.55, 0.25))),
        hqf_pdf.Type3Glyph.painted(
            CROSS, GLYPH_WIDTH, cross(hqf_pdf.Rgb(0.75, 0.15, 0.15))
        ),
        hqf_pdf.Type3Glyph.painted(DASH, GLYPH_WIDTH, dash(hqf_pdf.Rgb.gray(0.55))),
    ]
    return hqf_pdf.Type3Font(THOUSANDTH, glyph_box(), glyphs)


def shaped_font() -> hqf_pdf.Type3Font:
    """The font whose one mark takes the colour of the text around it."""
    bounds = hqf_pdf.Rect(200.0, 150.0, 600.0, 600.0)
    return hqf_pdf.Type3Font(
        THOUSANDTH,
        glyph_box(),
        [hqf_pdf.Type3Glyph.shaped(SQUARE, GLYPH_WIDTH, bounds, square())],
    )


def block(
    content: hqf_pdf.Content,
    handle: hqf_pdf.FontHandle,
    size: float,
    top: float,
    text: str,
) -> float:
    """Sets a block of words at `top`, and hands back the ordinate it ends at."""
    flow = hqf_pdf.TextFlow(handle, size)
    lines = flow.break_lines(text, WIDTH)
    # The binding opens the text object itself, so opening another here would write a
    # pair of operators its twin in Rust does not.
    flow.draw(content, lines, LEFT, top, WIDTH)
    return top - flow.height(lines)


def main() -> None:
    """Writes the page."""
    language = _language.from_environment()
    words = _language.words_of(WORDS, language)

    # A named file is written as named; the default one carries the language, so the two
    # languages do not overwrite each other in `tmp/`.
    out = _out.output_path(
        Path(_language.file_name("drawn_letters.pdf", language)).stem
    )

    doc = hqf_pdf.Document()
    doc.set_license(_licence.licensed())
    text = doc.add_font(hqf_pdf.Font.from_path(_out.font_path()))
    marks = doc.add_type3_font(painted_font())
    shapes = doc.add_type3_font(shaped_font())

    content = hqf_pdf.Content()

    top = 760.0
    top = block(content, text, 18.0, top, words.title) - 14.0
    top = block(content, text, 10.0, top, words.lead) - 26.0

    top = block(content, text, 12.0, top, words.painted) - 10.0
    label = hqf_pdf.Style(text, 11.0)
    mark = hqf_pdf.Style(marks, 11.0)
    line = hqf_pdf.RichText()
    for index, check in enumerate(words.checks):
        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, mark).push(f{check}", label)
        line = line.push("" if index == len(words.checks) - 1 else "    ", label)
    lines = line.break_lines(WIDTH)
    line.draw(content, lines, LEFT, top, WIDTH)
    top -= hqf_pdf.RichText.height(lines) + 26.0

    top = block(content, text, 12.0, top, words.shaped) - 10.0
    for index, tinted in enumerate(words.tinted):
        ink = hqf_pdf.Rgb(0.75, 0.15, 0.15) if index == 0 else hqf_pdf.Rgb(0.15, 0.35, 0.75)
        shape = hqf_pdf.Style(shapes, 11.0, color=ink)
        word = hqf_pdf.Style(text, 11.0, color=ink)
        row = hqf_pdf.RichText().push(SQUARE, shape).push(f{tinted}", word)
        drawn = row.break_lines(WIDTH)
        row.draw(content, drawn, LEFT, top, WIDTH)
        top -= hqf_pdf.RichText.height(drawn) + 4.0

    top -= 22.0
    block(content, text, 10.0, top, words.note)

    page = hqf_pdf.Page.a4()
    page.set_content(content)
    doc.add_page(page)

    written = doc.write(out)
    print(f"wrote {out}: {written} bytes")


if __name__ == "__main__":
    main()