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
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582 | """Writes a two-story newsletter whose reading order is not its page order.
The Python twin of the `write_articles` example in Rust. Each story begins in a column
of the front page and carries on in a column of the second — and the two swap sides on
the way, so the column below the one being read is the other story. Nothing drawn on the
page says which carries on which. The articles say it: each names the patches of page it
is read through, in order, and reading software that follows one goes from the foot of a
patch to the head of the next wherever it falls.
Both stories are held in `Words`, once per language, and `HQF_PDF_LANG` picks
which set is set. The grid is not language: the same four columns hold whichever
words are poured into them.
The pages open two columns wide, the odd pages on the left, so the first spread shows
the first two pages side by side.
Each article also names the newsletter as its subject and the keywords it is filed
under, which reading software's panel shows beside its title and author.
Usage: python examples/write_articles.py [out.pdf] [font.ttf]
HQF_PDF_LANG=fr python examples/write_articles.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 Story:
"""One story of the newsletter."""
# What the reader sees over it, and what the article is called in reading software's
# panel.
heading: str
# Who wrote it.
author: str
# The passage poured through the story's two columns.
passage: str
@dataclass(frozen=True)
class Words:
"""Every word the newsletter draws, in one language."""
# What stands across the head of the front page.
masthead: str
# The keywords every story is filed under, which reading software's panel shows
# beside its title and author.
keywords: str
# The line under it, which says what the file is showing.
standfirst: str
# The first story: its heading and the passage it is told in.
first: Story
# The second story, told beside it.
second: Story
# What stands after a heading where a story is taken up again.
continued: str
def stories(self) -> tuple[Story, Story]:
"""The two stories, in the order they are laid out."""
return (self.first, self.second)
# The newsletter in English.
ENGLISH = Words(
masthead="The Thursday Sheet",
keywords="newsletter, workshop, weekly",
standfirst="Two stories, four columns, and a reading order of their own.",
first=Story(
heading="The bridge reopens",
author="Claire Vasseur",
passage=(
"The bridge reopened on Tuesday, eleven months after the river took the "
"middle of it. The county surveyor walked the deck at dawn and declared "
"it sound, which is the only ceremony anyone asked for. Traffic crossed "
"within the hour. What took the eleven months was not the deck but the "
"two piers under it, each rebuilt from the bed up while the water ran "
"round them. The contractor counted nineteen days lost to the weather "
"and none at all to the design, which is a rarer thing than it sounds. "
"The old approach road stays shut until the spring, so the lorries still "
"go the long way round by the mill. The parish council has asked for a "
"footway on the upstream side and been told it will wait "
"its turn. Meanwhile the bus takes four minutes off its run and the "
"school on the far bank has moved its bell back to where it stood before "
"the flood. Nobody has yet said what becomes of the temporary crossing "
"downstream, which cost more to keep open than to build and which the "
"anglers would like left where it is."
),
),
second=Story(
heading="The harvest, county by county",
author="Martin Aubert",
passage=(
"The wheat came in ten days early and heavier than anyone had allowed "
"for, and the barns of three parishes were full before the second week "
"of August. The dry June is what did it, say the growers, and the wet "
"April before that. Not every field agrees: the high ground east of the "
"ridge lost a third of what it stood to a wind that came through on a "
"single afternoon. The merchants have held their price so far, which "
"nobody expects to last past the sales, and the millers are already "
"asking what the winter sowing will look like on ground this hard. The "
"straw is the other half of the story: there is a great deal of it, it "
"is dry, and the men who bale it have been working the fields two "
"counties away as well as their own. Two of the larger farms have put "
"their second cut off a fortnight in the hope of rain. The weighbridge "
"at the co-operative has been open since six every morning and the queue "
"at seven is longer than it was last year at nine."
),
),
continued="(continued)",
)
# The newsletter in French.
FRENCH = Words(
masthead="La Feuille du jeudi",
keywords="lettre d’information, atelier, hebdomadaire",
standfirst=("Deux articles, quatre colonnes et un ordre de lecture qui leur est "
"propre."),
first=Story(
heading="Le pont rouvre",
author="Claire Vasseur",
passage=(
("Le pont a rouvert mardi, onze mois après que la rivière en a emporté le "
"milieu. L’ingénieur des routes a parcouru le tablier à l’aube et l’a "
"déclaré sûr : c’est la seule cérémonie que l’on ait réclamée. La "
"circulation a repris dans l’heure. Ce qui a pris onze mois, ce n’est pas"
" le tablier mais les deux piles qui le soutiennent, refaites depuis le "
"lit pendant que l’eau courait autour. L’entreprise a compté dix-neuf "
"jours perdus à cause du temps et pas un à cause des plans, ce qui est "
"plus rare qu’il n’y paraît. L’ancienne route d’accès reste fermée "
"jusqu’au printemps : les camions font donc encore le détour par le "
"moulin. Le conseil municipal a demandé un trottoir côté amont ; on lui a"
" répondu que ce projet attendrait son tour. En attendant, le car gagne "
"quatre minutes sur son trajet et l’école de l’autre rive a remis sa "
"sonnerie à l’heure qu’elle avait avant la crue. Personne n’a encore dit "
"ce que deviendra la passerelle provisoire en aval, qui a coûté plus cher"
" à entretenir qu’à monter et que les pêcheurs aimeraient voir rester en "
"place.")
),
),
second=Story(
heading="La moisson, canton par canton",
author="Martin Aubert",
passage=(
("Le blé est rentré dix jours en avance et plus lourd que prévu, et les "
"granges de trois communes étaient pleines avant la deuxième semaine "
"d’août. C’est la sécheresse de juin qui l’explique, disent les "
"cultivateurs, et avant elle les pluies d’avril. Toutes les parcelles "
"n’en disent pas autant : les hauteurs à l’est de la crête ont perdu un "
"tiers de leur récolte sur pied sous un coup de vent d’un seul "
"après-midi. Les négociants tiennent leur prix pour l’instant, ce qui, de"
" l’avis général, ne durera pas au-delà des ventes, et les meuniers se "
"demandent déjà ce que donneront les semis d’hiver sur une terre aussi "
"dure. La paille est l’autre moitié de l’affaire : il y en a beaucoup, "
"elle est sèche, et ceux qui la mettent en balles travaillent jusqu’à "
"deux cantons plus loin en plus de leurs propres champs. Deux des plus "
"grosses fermes ont repoussé leur seconde coupe de quinze jours dans "
"l’espoir qu’il pleuve. Le pont-bascule de la coopérative ouvre chaque "
"matin à six heures, et la file de sept heures est plus longue que celle "
"de neuf heures l’an dernier.")
),
),
continued="(suite)",
)
# The newsletter in Dutch.
DUTCH = Words(
masthead="Het Donderdagblad",
keywords="nieuwsbrief, werkplaats, wekelijks",
standfirst="Twee verhalen, vier kolommen en een eigen leesvolgorde.",
first=Story(
heading="De brug gaat weer open",
author="Claire Vasseur",
passage=(
"De brug is dinsdag weer opengegaan, elf maanden nadat de rivier het "
"middenstuk had meegenomen. De provinciale landmeter liep bij dageraad "
"over het dek en verklaarde het veilig, de enige plechtigheid waar iemand "
"om had gevraagd. Binnen een uur reed het verkeer eroverheen. Wat de elf "
"maanden kostte, was niet het dek maar de twee pijlers eronder, elk vanaf "
"de bedding opnieuw gebouwd terwijl het water eromheen stroomde. De "
"aannemer telde negentien dagen verlies door het weer en geen enkele door "
"het ontwerp. De oude toegangsweg blijft tot het voorjaar dicht, dus de "
"vrachtwagens rijden nog om via de molen. De gemeenteraad vroeg om een "
"voetpad aan de stroomopwaartse kant en moet op zijn beurt wachten. "
"Intussen wint de bus vier minuten en heeft de school aan de overkant de "
"bel teruggehangen waar hij vóór de overstroming hing. Niemand heeft nog "
"gezegd wat er gebeurt met de noodoversteek stroomafwaarts, die meer "
"kostte om open te houden dan om te bouwen en die de vissers graag zouden "
"laten liggen waar hij ligt."
),
),
second=Story(
heading="De oogst, streek per streek",
author="Martin Aubert",
passage=(
"De tarwe kwam tien dagen vroeg binnen en zwaarder dan iemand had "
"verwacht, en de schuren van drie parochies zaten vol vóór de tweede week "
"van augustus. Het was de droge juni, zeggen de telers, en de natte april "
"daarvoor. Niet elk veld is het daarmee eens: het hoge land ten oosten "
"van de heuvelrug verloor een derde van wat er stond door een wind die op "
"één middag langskwam. De handelaren hebben hun prijs tot nu toe "
"vastgehouden, wat niemand na de veilingen nog verwacht, en de molenaars "
"vragen zich al af hoe de wintertarwe op zulke harde grond zal staan. Het "
"stro is de andere helft van het verhaal: er is veel van, het is droog, "
"en de mannen die het persen hebben naast hun eigen velden ook die van "
"twee streken verderop gedaan. Twee van de grotere boerderijen hebben hun "
"tweede snede twee weken uitgesteld in de hoop op regen. De weegbrug van "
"de coöperatie is elke ochtend vanaf zes uur open en de rij om zeven is "
"langer dan vorig jaar om negen."
),
),
continued="(vervolg)",
)
# The newsletter in Portuguese.
PORTUGUESE = Words(
masthead="A Folha de Quinta",
keywords="boletim, oficina, semanal",
standfirst="Duas notícias, quatro colunas e uma ordem de leitura própria.",
first=Story(
heading="A ponte reabre",
author="Claire Vasseur",
passage=(
"A ponte reabriu na terça-feira, onze meses depois de o rio lhe ter "
"levado o tramo central. O engenheiro do concelho percorreu o tabuleiro "
"de madrugada e declarou-o sólido, a única cerimónia que alguém pediu. O "
"trânsito passou em menos de uma hora. O que levou os onze meses não foi "
"o tabuleiro mas os dois pilares por baixo, refeitos a partir do leito "
"enquanto a água corria à volta. O empreiteiro contou dezanove dias "
"perdidos com o tempo e nenhum com o projeto, coisa mais rara do que "
"parece. A antiga estrada de acesso continua fechada até à primavera, por "
"isso os camiões continuam a dar a volta pelo moinho. A junta pediu um "
"passeio do lado de montante e ouviu que terá de esperar a sua vez. "
"Entretanto o autocarro ganha quatro minutos no percurso e a escola da "
"outra margem pôs o sino de volta onde estava antes da cheia. Ninguém "
"disse ainda o que será da passagem provisória a jusante, que custou mais "
"a manter aberta do que a construir e que os pescadores gostariam de "
"deixar onde está."
),
),
second=Story(
heading="A colheita, concelho a concelho",
author="Martin Aubert",
passage=(
"O trigo chegou dez dias mais cedo e mais pesado do que se previa, e os "
"celeiros de três freguesias estavam cheios antes da segunda semana de "
"agosto. Foi o junho seco, dizem os agricultores, e o abril chuvoso antes "
"dele. Nem todos os campos concordam: as terras altas a leste da serra "
"perderam um terço do que tinham com um vento que passou numa só tarde. "
"Os comerciantes mantiveram o preço até agora, coisa que ninguém espera "
"que dure para lá das vendas, e os moleiros já perguntam como será a "
"sementeira de inverno numa terra tão dura. A palha é a outra metade da "
"história: há muita, está seca, e quem a enfarda tem trabalhado, além dos "
"seus, os campos de dois concelhos vizinhos. Duas das maiores herdades "
"adiaram quinze dias o segundo corte à espera de chuva. A báscula da "
"cooperativa abre às seis todas as manhãs e a fila às sete é mais longa "
"do que no ano passado às nove."
),
),
continued="(continua)",
)
# The newsletter in Italian.
ITALIAN = Words(
masthead="Il Foglio del giovedì",
keywords="notiziario, laboratorio, settimanale",
standfirst="Due articoli, quattro colonne e un ordine di lettura tutto loro.",
first=Story(
heading="Il ponte riapre",
author="Claire Vasseur",
passage=(
"Il ponte ha riaperto martedì, undici mesi dopo che il fiume se ne era "
"portato via la parte centrale. Il geometra della provincia ha percorso "
"l’impalcato all’alba e lo ha dichiarato sicuro, unica cerimonia che "
"qualcuno avesse chiesto. Il traffico è passato entro un’ora. Gli undici "
"mesi non se li è presi l’impalcato ma le due pile sottostanti, "
"ricostruite dal letto del fiume mentre l’acqua scorreva loro intorno. "
"L’impresa ha contato diciannove giorni persi per il maltempo e nessuno "
"per il progetto, cosa più rara di quanto sembri. La vecchia strada "
"d’accesso resta chiusa fino alla primavera, così i camion fanno ancora "
"il giro lungo dal mulino. Il consiglio comunale ha chiesto un "
"marciapiede sul lato a monte e si è sentito dire che aspetterà il suo "
"turno. Intanto l’autobus guadagna quattro minuti sul percorso e la "
"scuola dell’altra sponda ha rimesso la campana dov’era prima "
"dell’alluvione. Nessuno ha ancora detto che ne sarà del passaggio "
"provvisorio a valle: tenerlo aperto è costato più che costruirlo, e i "
"pescatori vorrebbero lasciarlo dov’è."
),
),
second=Story(
heading="Il raccolto, provincia per provincia",
author="Martin Aubert",
passage=(
"Il grano è arrivato con dieci giorni di anticipo e più pesante del "
"previsto, e i granai di tre paesi erano pieni prima della seconda "
"settimana di agosto. È stato il giugno secco, dicono i coltivatori, e "
"l’aprile piovoso prima. Non tutti i campi sono d’accordo: le terre alte "
"a est del crinale hanno perso un terzo di quanto avevano per un vento "
"passato in un solo pomeriggio. I commercianti hanno tenuto il prezzo "
"finora, cosa che nessuno si aspetta duri oltre le vendite, e i mugnai "
"già si chiedono come sarà la semina invernale su una terra così dura. La "
"paglia è l’altra metà della storia: ce n’è molta, è asciutta, e chi la "
"imballa ha lavorato i campi di due province più in là oltre ai propri. "
"Due delle aziende più grandi hanno rinviato di quindici giorni il "
"secondo taglio sperando nella pioggia. La pesa della cooperativa apre "
"alle sei ogni mattina e la coda alle sette è più lunga di quella "
"dell’anno scorso alle nove."
),
),
continued="(continua)",
)
# The newsletter in Spanish.
SPANISH = Words(
masthead="La Hoja del Jueves",
keywords="boletín, taller, semanal",
standfirst="Dos artículos, cuatro columnas y un orden de lectura propio.",
first=Story(
heading="El puente reabre",
author="Claire Vasseur",
passage=(
"El puente reabrió el martes, once meses después de que el río se llevara "
"su tramo central. El aparejador del condado recorrió el tablero al "
"amanecer y lo declaró sólido, única ceremonia que nadie pidió. El "
"tráfico cruzó en menos de una hora. Lo que costó los once meses no fue "
"el tablero sino las dos pilas de debajo, rehechas desde el lecho "
"mientras el agua corría a su alrededor. El contratista contó diecinueve "
"días perdidos por el tiempo y ninguno por el proyecto, cosa más rara de "
"lo que parece. La antigua carretera de acceso sigue cerrada hasta la "
"primavera, así que los camiones siguen dando el rodeo por el molino. El "
"ayuntamiento pidió una acera en el lado de aguas arriba y le dijeron que "
"esperaría su turno. Mientras tanto el autobús ahorra cuatro minutos en "
"su recorrido y la escuela de la otra orilla ha vuelto a poner su campana "
"donde estaba antes de la riada. Nadie ha dicho aún qué será del paso "
"provisional de aguas abajo, que costó más mantener abierto que construir "
"y que los pescadores querrían dejar donde está."
),
),
second=Story(
heading="La cosecha, comarca por comarca",
author="Martin Aubert",
passage=(
"El trigo llegó diez días antes y más pesado de lo previsto, y los "
"graneros de tres parroquias estaban llenos antes de la segunda semana de "
"agosto. Fue el junio seco, dicen los agricultores, y el abril lluvioso "
"de antes. No todos los campos opinan igual: las tierras altas al este de "
"la loma perdieron un tercio de lo que tenían por un viento que pasó en "
"una sola tarde. Los comerciantes han mantenido su precio por ahora, cosa "
"que nadie espera que dure más allá de las ventas, y los molineros ya "
"preguntan cómo será la siembra de invierno en una tierra tan dura. La "
"paja es la otra mitad de la historia: hay mucha, está seca, y los que la "
"empacan han trabajado los campos de dos comarcas más allá además de los "
"suyos. Dos de las fincas mayores han retrasado quince días su segundo "
"corte con la esperanza de que llueva. La báscula de la cooperativa abre "
"a las seis cada mañana y la cola a las siete es más larga que el año "
"pasado a las nueve."
),
),
continued="(continúa)",
)
# The newsletter in German.
GERMAN = Words(
masthead="Das Donnerstagsblatt",
keywords="Wöchentlicher Rundbrief der Werkstatt",
standfirst="Zwei Berichte, vier Spalten und eine eigene Lesereihenfolge.",
first=Story(
heading="Die Brücke ist wieder offen",
author="Claire Vasseur",
passage=(
"Die Brücke ist seit Dienstag wieder offen, elf Monate nachdem der Fluss "
"ihr Mittelstück mitriss. Der Kreisbaumeister schritt im Morgengrauen die "
"Fahrbahn ab und erklärte sie für tragfähig; mehr Feier hatte niemand "
"verlangt. Binnen einer Stunde rollte der Verkehr. Die elf Monate "
"steckten nicht in der Fahrbahn, sondern in den zwei Pfeilern darunter, "
"vom Flussbett an neu gebaut, während das Wasser um sie strömte. Die "
"Baufirma zählte neunzehn Tage Verlust durch das Wetter und keinen durch "
"den Entwurf. Die alte Zufahrt bleibt bis zum Frühjahr gesperrt, also "
"fahren die Lastwagen weiter den Umweg über die Mühle. Der Gemeinderat "
"will einen Fußweg auf der Seite flussaufwärts und muss warten, bis er an "
"der Reihe ist. Der Bus spart vier Minuten, und die Schule am anderen "
"Ufer läutet ihre Glocke wieder wie vor dem Hochwasser. Was aus dem "
"Behelfsübergang flussabwärts wird, hat noch niemand gesagt; die Angler "
"hätten ihn gern behalten."
),
),
second=Story(
heading="Die Ernte, Kreis für Kreis",
author="Martin Aubert",
passage=(
"Der Weizen kam zehn Tage früher und schwerer als erwartet, und die "
"Scheunen dreier Gemeinden waren vor der zweiten Augustwoche voll. Das "
"habe der trockene Juni bewirkt, sagen die Bauern, und davor der nasse "
"April. Nicht jedes Feld bestätigt das: Die Höhen östlich des Kamms "
"verloren ein Drittel ihrer Ernte an einen Sturm, der an einem einzigen "
"Nachmittag durchzog. Die Händler halten ihren Preis bisher, was nach den "
"Auktionen niemand mehr erwartet hatte, und die Müller fragen schon nach "
"der Wintersaat auf so hartem Boden. Das Stroh ist die andere Hälfte der "
"Ernte: Es gibt viel davon, es ist trocken, und die Pressen arbeiten auch "
"zwei Kreise weiter. Zwei größere Höfe haben ihren zweiten Schnitt um "
"vierzehn Tage verschoben, in der Hoffnung auf Regen. Die Waage der "
"Genossenschaft öffnet jeden Morgen um sechs, und die Schlange um sieben "
"ist länger als im Vorjahr um neun."
),
),
continued="(Fortsetzung)",
)
# 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}
# The left edge of the column on the left of a page.
LEFT = 70.0
# The left edge of the column on the right of a page.
RIGHT = 310.0
# The width of one column.
COLUMN = 215.0
# The top edge of the two columns of the front page, which the masthead stands above.
FRONT_TOP = 700.0
# The top edge of the two columns of the second page.
INSIDE_TOP = 760.0
# How far down from its top edge a column reaches.
HEIGHT = 190.0
# The size the passages are set at, in points.
SIZE = 9.0
# The distance from one baseline to the next, in points.
LEADING = 12.5
# The size a story's heading is set at, in points, or less where the heading would not
# fit across its column.
HEADING_SIZE = 12.0
# How much of a column's height the heading and the line under it take.
HEADING_ROOM = 26.0
@dataclass(frozen=True)
class Patch:
"""One patch of page a story is read through: which page, and where on it."""
# The page it is on, counting from zero.
page: int
# The left edge of the column, in points.
x: float
# The top edge of the column, in points up from the foot of the sheet.
top: float
def text_top(self) -> float:
"""Where the passage starts, under the room the heading takes."""
return self.top - HEADING_ROOM
# How much of a column the passage has, once the heading has had its room.
TEXT_HEIGHT = HEIGHT - HEADING_ROOM
# The two patches the first story is read through: the left of the front page, then the
# right of the second.
FIRST = (Patch(0, LEFT, FRONT_TOP), Patch(1, RIGHT, INSIDE_TOP))
# The two patches the second story is read through, which cross the first story's on the
# way: the right of the front page, then the left of the second.
SECOND = (Patch(0, RIGHT, FRONT_TOP), Patch(1, LEFT, INSIDE_TOP))
# The patches each story is read through, in the order the stories are laid out.
PATCHES = (FIRST, SECOND)
def heading(words: Words, story: Story, first: bool) -> str:
"""The heading one patch carries: the story's own on the patch it begins in, and the
story's own with a word after it on every later patch."""
return story.heading if first else f"{story.heading} {words.continued}"
def masthead(
content: hqf_pdf.Content, font: hqf_pdf.FontHandle, words: Words
) -> None:
"""Draws the masthead across the head of the front page."""
content.draw_text(font, 22.0, LEFT, 780.0, words.masthead)
content.draw_text(font, SIZE, LEFT, 762.0, words.standfirst)
content.set_line_width(1.0)
content.move_to(LEFT, 752.0)
content.line_to(RIGHT + COLUMN, 752.0)
content.stroke()
def draw_patch(
content: hqf_pdf.Content,
font: hqf_pdf.FontHandle,
flow: hqf_pdf.TextFlow,
patch: Patch,
title: str,
lines: hqf_pdf.Lines,
) -> int:
"""Draws one patch: its frame, its heading, and the slice of the passage it holds.
Returns how many lines it took."""
content.save_state()
content.set_stroke(hqf_pdf.Rgb.gray(0.75))
content.set_line_width(0.5)
content.rect(patch.x, patch.top - HEIGHT, COLUMN, HEIGHT)
content.stroke()
content.restore_state()
size = font.fit(title, COLUMN - 12.0, HEADING_SIZE)
content.draw_text(font, size, patch.x + 6.0, patch.top - 16.0, title)
taken = flow.fit(lines, TEXT_HEIGHT, 0)
flow.draw(
content,
lines.sub(0, taken),
patch.x + 6.0,
patch.text_top(),
COLUMN - 12.0,
)
return taken
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("articles.pdf", language)).stem)
document = hqf_pdf.Document()
document.set_license(_licence.licensed())
document.set_info("Title", words.masthead)
font = document.add_font(hqf_pdf.Font.from_path(_out.font_path()))
flow = hqf_pdf.TextFlow(font, SIZE, leading=LEADING, align=hqf_pdf.Align.Justify)
# One content stream per page, drawn into as each story reaches it.
sheets = [hqf_pdf.Content(), hqf_pdf.Content()]
masthead(sheets[0], font, words)
for story, patches in zip(words.stories(), PATCHES):
broken = flow.break_lines(story.passage, COLUMN - 12.0)
start = 0
for index, patch in enumerate(patches):
taken = draw_patch(
sheets[patch.page],
font,
flow,
patch,
heading(words, story, index == 0),
broken.sub(start, len(broken)),
)
start += taken
article = hqf_pdf.Article(
story.heading,
author=story.author,
subject=words.masthead,
keywords=words.keywords,
)
for patch in patches:
article = article.on_page(
patch.page, patch.x, patch.top - HEIGHT, COLUMN, HEIGHT
)
print(
f"{article.title}: {article.patches} patches, keywords {article.keywords or ''}"
)
document.add_article(article)
for content in sheets:
page = hqf_pdf.Page.a4()
page.set_content(content)
document.add_page(page)
document.set_page_layout(hqf_pdf.PageLayout.TwoColumnsOddLeft)
written = document.write(out)
print(f"wrote {out}: {written} bytes")
if __name__ == "__main__":
main()
|