write_paper_sizes.py

Le fichier Python de l’exemple « Chaque format de papier, sur sa page ». Une page pour chaque format de papier que la bibliothèque connaît, de l’A0 à l’A6 ainsi que les formats américains, chacune avec sa taille en millimètres et en pouces ; puis des pages couchées ou à l’envers, et une banderole de dix mètres de long.

Python 357 lignes

À quoi sert cet exemple

Une affiche, une lettre pour un client américain, une carte postale : chacune demande sa propre taille de page, et un programme ne devrait pas avoir à chercher combien de points mesure une feuille A3. La bibliothèque connaît les formats courants par leur nom. Ici, chacun a sa page, de l’affiche A0 à la carte A6, puis B4, B5, Letter, Legal, Tabloid et Ledger, et chaque page affiche sa taille en millimètres et en pouces, dans un cadre tracé à un centimètre du bord.

Ce que montre cet exemple

  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
"""Writes one page of each paper size the library names, and a few pages told how a
reader is to show them.

The Python twin of the `write_paper_sizes` example in Rust: the same pages, through the
binding rather than through the library directly.

Each page states its own name and its size, measured off the page in millimetres and in
inches, and is framed a centimetre in from its edges, so a reader that fits it to the
screen shows the whole sheet. The sizes run from A0 to A6, B4 and B5, then the four the
North American trade uses.

After them come an A5 page laid on its side; pages the reader is told to turn upside
down and a quarter to the left; pages that ask for the order a reader tabs through what
they carry; and a banner ten metres long, larger than a page may be, drawn on a page
whose unit is ten points.

The words are held in `Words`, once per language, and `HQF_PDF_LANG` picks which set the
pages are written in.

Usage: python examples/write_paper_sizes.py [out.pdf]
       HQF_PDF_LANG=fr python examples/write_paper_sizes.py
"""

from __future__ import annotations

from dataclasses import dataclass
from pathlib import Path

import _language
import _licence
import _out

import hqf_pdf

# The font the pages are written in.
DEFAULT_FONT = _out.ROOT / "crates" / "hqf-pdf" / "tests" / "fonts" / "DejaVuSans.ttf"

# How many points make a millimetre.
POINTS_PER_MM = 72.0 / 25.4

# How far in from each edge the frame is drawn, in points: one centimetre.
FRAME = 10.0 * POINTS_PER_MM

# How long the banner is, in millimetres: ten metres.
BANNER_MM = 10_000.0

# How many points one unit of the banner's page stands for.
BANNER_UNIT = 10.0


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

    # What the file says of itself.
    title: str
    # What a page says of its size; `{name}`, `{mm}` and `{inches}` stand for them.
    size: str
    # What the A5 page laid on its side is called.
    side: str
    # What the three turned pages say: upside down, a quarter to the left, upright.
    turns: tuple[str, str, str]
    # What the pages asking for a tab order say, one per order: structure, annotations,
    # widgets.
    tabs: tuple[str, str, str]
    # What the banner says; `{metres}` and `{unit}` stand for its length and the points
    # one unit stands for.
    banner: str
    # The character separating the whole part of a figure from its tenths.
    decimal: str


# The pages in English.
ENGLISH = Words(
    title="Every paper size, and pages told how to be shown",
    size="{name}: {mm} mm, {inches} in",
    side="A5 on its side",
    turns=(
        "This page is to be shown upside down.",
        "This page is to be shown turned a quarter to the left.",
        "This page is to be shown as it was drawn.",
    ),
    tabs=(
        "A reader tabs through this page in the order its structure reads it.",
        "A reader tabs through this page in the order it lists its annotations.",
        "A reader tabs through this page's fields first, then the rest.",
    ),
    banner=(
        "A banner {metres} metres long: each unit of this page stands for {unit} "
        "points."
    ),
    decimal=".",
)

# The pages in French.
FRENCH = Words(
    title="Tous les formats de papier, et des pages qui disent comment les afficher",
    size="{name} : {mm} mm, {inches} po",
    side="A5 à l’italienne",
    turns=(
        "Cette page doit être affichée à l’envers.",
        "Cette page doit être affichée tournée d’un quart vers la gauche.",
        "Cette page doit être affichée telle qu’elle a été composée.",
    ),
    tabs=(
        "Avec la touche Tab, on parcourt cette page dans l’ordre de sa structure.",
        "Avec la touche Tab, on parcourt cette page dans l’ordre de ses annotations.",
        (
            "Avec la touche Tab, on parcourt d’abord les champs de cette page, puis le "
            "reste."
        ),
    ),
    banner=(
        "Une banderole de {metres} mètres : chaque unité de cette page vaut "
        "{unit} points."
    ),
    decimal=",",
)

# The pages in Dutch.
DUTCH = Words(
    title="Elk papierformaat, en pagina’s die te horen krijgen hoe ze getoond worden",
    size="{name}: {mm} mm, {inches} in",
    side="A5 liggend",
    turns=(
        "Deze pagina moet ondersteboven getoond worden.",
        "Deze pagina moet een kwartslag naar links gedraaid getoond worden.",
        "Deze pagina moet getoond worden zoals ze getekend is.",
    ),
    tabs=(
        "Een lezer tabt door deze pagina in de volgorde waarin haar structuur haar leest.",
        "Een lezer tabt door deze pagina in de volgorde waarin ze haar annotaties opsomt.",
        "Een lezer tabt eerst door de velden van deze pagina, dan door de rest.",
    ),
    banner=(
        "Een spandoek van {metres} meter: elke eenheid van deze pagina staat voor "
        "{unit} punten."
    ),
    decimal=".",
)

# The pages in Portuguese.
PORTUGUESE = Words(
    title="Todos os formatos de papel, e páginas às quais se diz como devem ser mostradas",
    size="{name}: {mm} mm, {inches} in",
    side="A5 ao baixo",
    turns=(
        "Esta página deve ser mostrada de pernas para o ar.",
        "Esta página deve ser mostrada rodada um quarto de volta para a esquerda.",
        "Esta página deve ser mostrada tal como foi desenhada.",
    ),
    tabs=(
        "Um leitor percorre esta página com o tabulador pela ordem da sua estrutura.",
        "Um leitor percorre esta página com o tabulador pela ordem em que lista as suas anotações.",
        "Um leitor percorre com o tabulador primeiro os campos desta página, depois o resto.",
    ),
    banner=(
        "Uma faixa de {metres} metros: cada unidade desta página vale {unit} pontos."
    ),
    decimal=".",
)

# The pages in Italian.
ITALIAN = Words(
    title="Ogni formato di carta, e pagine a cui si dice come mostrarsi",
    size="{name}: {mm} mm, {inches} in",
    side="A5 orizzontale",
    turns=(
        "Questa pagina va mostrata capovolta.",
        "Questa pagina va mostrata ruotata di un quarto a sinistra.",
        "Questa pagina va mostrata come è stata disegnata.",
    ),
    tabs=(
        "Un lettore percorre questa pagina col tasto Tab nell’ordine della sua struttura.",
        "Un lettore percorre questa pagina col tasto Tab nell’ordine in cui elenca le sue annotazioni.",
        "Un lettore percorre col tasto Tab prima i campi di questa pagina, poi il resto.",
    ),
    banner=(
        "Uno striscione di {metres} metri: ogni unità di questa pagina vale {unit} "
        "punti."
    ),
    decimal=".",
)

# The pages in Spanish.
SPANISH = Words(
    title="Todos los tamaños de papel, y páginas a las que se dice cómo mostrarse",
    size="{name}: {mm} mm, {inches} in",
    side="A5 apaisado",
    turns=(
        "Esta página debe mostrarse cabeza abajo.",
        "Esta página debe mostrarse girada un cuarto a la izquierda.",
        "Esta página debe mostrarse tal como se dibujó.",
    ),
    tabs=(
        "Un lector recorre esta página con el tabulador en el orden en que la lee su estructura.",
        "Un lector recorre esta página con el tabulador en el orden en que enumera sus anotaciones.",
        "Un lector recorre primero los campos de esta página con el tabulador, y luego el resto.",
    ),
    banner=(
        "Una pancarta de {metres} metros: cada unidad de esta página vale {unit} "
        "puntos."
    ),
    decimal=".",
)

# The pages in German.
GERMAN = Words(
    title="Jedes Papierformat und Seiten, denen gesagt wird, wie sie anzuzeigen sind",
    size="{name}: {mm} mm, {inches} Zoll",
    side="A5 quer",
    turns=(
        "Diese Seite ist auf dem Kopf anzuzeigen.",
        "Diese Seite ist um eine Vierteldrehung nach links gedreht anzuzeigen.",
        "Diese Seite ist so anzuzeigen, wie sie gezeichnet wurde.",
    ),
    tabs=(
        "Ein Leser springt mit der Tabulatortaste in der Reihenfolge ihrer Struktur durch diese Seite.",
        "Ein Leser springt mit der Tabulatortaste in der Reihenfolge ihrer Anmerkungen durch diese Seite.",
        "Ein Leser springt mit der Tabulatortaste zuerst durch die Felder dieser Seite, dann durch den Rest.",
    ),
    banner=(
        "Ein Banner von {metres} Metern Länge: Jede Einheit dieser Seite steht für "
        "{unit} Punkt."
    ),
    decimal=".",
)

# 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, _language.GERMAN: GERMAN, _language.SPANISH: SPANISH, _language.ITALIAN: ITALIAN, _language.PORTUGUESE: PORTUGUESE, _language.DUTCH: DUTCH}


def sizes() -> list[tuple[str, hqf_pdf.Page]]:
    """The sizes the library names, each with what it is called."""
    page = hqf_pdf.Page
    return [
        ("A0", page.a0()),
        ("A1", page.a1()),
        ("A2", page.a2()),
        ("A3", page.a3()),
        ("A4", page.a4()),
        ("A5", page.a5()),
        ("A6", page.a6()),
        ("B4", page.b4()),
        ("B5", page.b5()),
        ("Letter", page.letter()),
        ("Legal", page.legal()),
        ("Tabloid", page.tabloid()),
        ("Ledger", page.ledger()),
    ]


def figure(value: float, places: int, words: Words) -> str:
    """`value` to `places` decimals, its decimal separator the language's own."""
    return f"{value:.{places}f}".replace(".", words.decimal)


def size_line(name: str, page: hqf_pdf.Page, words: Words) -> str:
    """What `page`, called `name`, says of its size."""
    width, height = page.width, page.height
    millimetres = (
        f"{figure(width / POINTS_PER_MM, 0, words)} × "
        f"{figure(height / POINTS_PER_MM, 0, words)}"
    )
    inches = f"{figure(width / 72.0, 1, words)} × {figure(height / 72.0, 1, words)}"
    return (
        words.size.replace("{name}", name)
        .replace("{mm}", millimetres)
        .replace("{inches}", inches)
    )


def framed(page: hqf_pdf.Page, font, lines) -> hqf_pdf.Page:
    """`page` framed a centimetre in from its edges, with `lines` set in its top left
    corner at a size that grows with the sheet, each broken to the frame."""
    width, height = page.width, page.height
    size = max(min(width, height) / 24.0, 10.0)
    content = hqf_pdf.Content()
    content.set_line_width(1.0)
    content.rect(FRAME, FRAME, width - FRAME - FRAME, height - FRAME - FRAME)
    content.stroke()
    flow = hqf_pdf.TextFlow(font, size, leading=size * 1.4)
    room = width - FRAME - FRAME - size - size
    top = height - FRAME - size
    for line in lines:
        broken = flow.break_lines(line, room)
        flow.draw(content, broken, FRAME + size, top, room)
        top -= flow.height(broken)
    page.set_content(content)
    return page


def pages(font, words: Words) -> list[hqf_pdf.Page]:
    """Every page of the document, in order."""
    result = []
    for name, page in sizes():
        result.append(framed(page, font, [size_line(name, page, words)]))

    side = hqf_pdf.Page.a5().on_its_side()
    result.append(framed(side, font, [size_line(words.side, side, words)]))

    turns = (
        hqf_pdf.PageTurn.UpsideDown,
        hqf_pdf.PageTurn.Left,
        hqf_pdf.PageTurn.Upright,
    )
    for turn, said in zip(turns, words.turns):
        page = framed(hqf_pdf.Page.a5(), font, [said])
        page.set_turn(turn)
        result.append(page)

    orders = (
        hqf_pdf.TabOrder.Structure,
        hqf_pdf.TabOrder.Annotations,
        hqf_pdf.TabOrder.Widgets,
    )
    for order, said in zip(orders, words.tabs):
        page = framed(hqf_pdf.Page.a5(), font, [said])
        page.set_tab_order(order)
        result.append(page)

    long = BANNER_MM * POINTS_PER_MM / BANNER_UNIT
    tall = 500.0 * POINTS_PER_MM / BANNER_UNIT
    metres = figure(BANNER_MM / 1000.0, 0, words)
    said = words.banner.replace("{metres}", metres).replace(
        "{unit}", figure(BANNER_UNIT, 0, words)
    )
    banner = framed(hqf_pdf.Page(long, tall), font, [said])
    banner.set_user_unit(BANNER_UNIT)
    result.append(banner)
    return result


def main() -> None:
    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("paper_sizes.pdf", language)).stem)

    document = hqf_pdf.Document()
    document.set_license(_licence.licensed())
    # The two last tab orders are read from PDF 2.0 on.
    document.set_version(hqf_pdf.Version.V2_0)
    document.set_info("Title", words.title)
    font = document.add_font(hqf_pdf.Font.from_path(DEFAULT_FONT))
    for page in pages(font, words):
        document.add_page(page)

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


if __name__ == "__main__":
    main()