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 | """Stamps a page with what it is: DRAFT, COPY, PAID.
The Python twin of the `write_stamp` example in Rust. A stamp is scaled to the box it
is given rather than set in a size, so the same line stamps a whole page, a table
cell and a form field without a single size being worked out by the caller. It is
turned first and scaled second, so that what stays inside the box is the line as the
reader sees it. The first page draws four of them across boxes of very different
shapes.
The stamp takes whatever colour it is given and nothing else: it is text, and a
caller that wants it under the page's own ink draws it first.
Under each caption, a second line says how far the stamp turned its line.
The three pages after it carry watermarks instead, handed to the document once rather
than drawn page by page. The first runs from page two to the end, even pages only, set
at 96 points whatever the page measures and painted at 35 per cent opacity. The second
runs from page three, odd pages only, under the page's own ink, fitted to half the page
and a shadow behind it. The third lies flat over every page from the fourth on, small
and pale. Each later page says how many of the three it carries, and page three how far
its shadow is offset. The last page carries a stamp of another kind besides: APPROVED
pressed above the page rather than drawn into it, which a reader can hide, delete or
leave off the paper.
The stamped words, the title, the four captions, the watermark and the line heading
each later page are held in `Words`, once per language, and `HQF_PDF_LANG` picks which
set is drawn. Each set keeps its last word the longest, because the last box is the one
that shows a long word coming out smaller rather than cut.
Usage: python examples/write_stamp.py [out.pdf]
HQF_PDF_LANG=fr python examples/write_stamp.py
"""
from __future__ import annotations
import math
from dataclasses import dataclass
from pathlib import Path
import _language
import _licence
import _out
import hqf_pdf
MARGIN = 56.0
# The size the watermark is set in, in points, on every page it covers.
WATERMARK_SIZE = 96.0
# The share of its colour the watermark is painted with.
WATERMARK_ALPHA = 0.35
# The size the first page's title is set in, in points.
TITLE_SIZE = 14.0
# The size the line heading each later page is set in, in points: a sentence rather than
# a title, kept inside the margins in every language.
LATER_SIZE = 11.0
# The colour the stamp drawn above the last page reads in.
APPROVED_GREEN = hqf_pdf.Rgb(0.1, 0.5, 0.2)
@dataclass(frozen=True)
class Stamped:
"""One box to stamp: where it is, how big, and which way the line runs."""
x: float
y: float
width: float
height: float
slant: hqf_pdf.Slant
color: hqf_pdf.Rgb
@dataclass(frozen=True)
class Words:
"""Every word the page draws, in one language.
The stamped words are language: a reader stamps a document in the language they read
it in. Each set keeps its last word the longest, because the last box is the one
that shows a long word coming out smaller rather than cut.
"""
# The line at the head of the page.
title: str
# What each box is stamped with, in the order the boxes are laid out.
stamps: tuple[str, str, str, str]
# What is written under each box.
captions: tuple[str, str, str, str]
# The watermark laid on the even pages after the first.
watermark: str
# The watermark laid under the odd pages from the third.
under: str
# The watermark laid flat over every page from the fourth.
flat: str
# How far a stamp turned its line; `{degrees}` stands for the angle.
turned: str
# How many watermarks a page carries; `{count}` stands for them.
carried: str
# How far the shadow of the second watermark is offset; `{dx}` and `{dy}` stand for
# the offsets.
shadowed: str
# The line at the head of each page after the first.
later: tuple[str, str, str]
# The word the stamp drawn above the last page reads.
approved: str
# What the last page says that stamp is, the first of the two lines a reader that
# cannot see the page is told it says.
above: str
# What the last page says a reader can do with that stamp, the second of the two
# lines.
offered: str
# The page in English.
ENGLISH = Words(
title="One stamp, four boxes",
stamps=(
"DRAFT",
"COPY",
"PAID",
"CANCELLED",
),
captions=(
"A wide box: the line runs up its diagonal, which is nearly flat.",
"A tall box, stamped the other way.",
"Flat across the middle, and wide enough that the width is what binds.",
"A longer word in a shorter box: the size follows, nothing is cut.",
),
watermark="SPECIMEN",
under="INTERNAL",
flat="ARCHIVED",
turned="Turned {degrees}°.",
carried="Watermarks on this page: {count}.",
shadowed="The shadow is offset by ({dx}, {dy}) pt.",
later=(
"Page two is even: the first watermark lies across it, at 96 points.",
"Page three is odd: the second lies under it, fitted to half the page.",
"Page four is even again: the first once more, and the third over both.",
),
approved="APPROVED",
above="The green stamp “Approved” lies above the page.",
offered="It can be hidden, deleted or left off the paper.",
)
# The page in French.
FRENCH = Words(
title="Un tampon, quatre boîtes",
stamps=(
"BROUILLON",
"COPIE",
"PAYÉ",
"ANNULATION",
),
captions=(
"Une boîte large : le mot monte le long de sa diagonale, presque à plat.",
"Une boîte haute, tamponnée dans l’autre sens.",
"À plat au milieu, et assez large pour que ce soit la largeur qui limite.",
(
"Un mot plus long dans une boîte plus basse : la taille suit, rien n’est "
"coupé."
),
),
watermark="SPÉCIMEN",
under="INTERNE",
flat="ARCHIVÉ",
turned="Tourné de {degrees}°.",
carried="Filigranes sur cette page : {count}.",
shadowed="L’ombre est décalée de ({dx} ; {dy}) pt.",
later=(
"La page deux est paire : le premier filigrane la traverse, en corps 96.",
"La page trois est impaire : le deuxième passe dessous, à demi-page.",
"La page quatre est paire : le premier à nouveau, et le troisième par-dessus.",
),
approved="APPROUVÉ",
above="Le tampon vert « Approuvé » est posé sur la page.",
offered="On peut le masquer, l’effacer ou ne pas l’imprimer.",
)
# The page in Dutch.
DUTCH = Words(
title="Eén stempel, vier kaders",
stamps=(
"CONCEPT",
"KOPIE",
"BETAALD",
"GEANNULEERD",
),
captions=(
"Een breed kader: de regel loopt langs de diagonaal, die bijna vlak is.",
"Een hoog kader, de andere kant op gestempeld.",
"Vlak door het midden, en breed genoeg dat de breedte beslist.",
"Een langer woord in een korter kader: de grootte volgt, er wordt niets afgesneden.",
),
watermark="SPECIMEN",
under="INTERN",
flat="GEARCHIVEERD",
turned="Gedraaid {degrees}°.",
carried="Watermerken op deze pagina: {count}.",
shadowed="De schaduw is verschoven met ({dx}, {dy}) pt.",
later=(
"Pagina twee is even: het eerste watermerk ligt eroverheen, op 96 punten.",
"Pagina drie is oneven: het tweede ligt eronder, passend op een halve pagina.",
"Pagina vier is weer even: nog eens het eerste, en het derde over beide.",
),
approved="GOEDGEKEURD",
above="De groene stempel ‘Goedgekeurd’ ligt over de pagina heen.",
offered="Je kunt hem verbergen, verwijderen of buiten de afdruk laten.",
)
# The page in Portuguese.
PORTUGUESE = Words(
title="Um carimbo, quatro caixas",
stamps=(
"RASCUNHO",
"CÓPIA",
"PAGO",
"CANCELADO",
),
captions=(
"Uma caixa larga: a linha sobe pela diagonal, que é quase plana.",
"Uma caixa alta, carimbada no outro sentido.",
"Plano pelo meio, e tão largo que é a largura que manda.",
"Uma palavra mais longa numa caixa mais curta: o tamanho acompanha, nada é cortado.",
),
watermark="ESPÉCIME",
under="INTERNO",
flat="ARQUIVADO",
turned="Rodado {degrees}°.",
carried="Marcas de água nesta página: {count}.",
shadowed="A sombra está deslocada ({dx}, {dy}) pt.",
later=(
"A página dois é par: a primeira marca de água atravessa-a, a 96 pontos.",
"A página três é ímpar: a segunda fica por baixo, ajustada a meia página.",
"A página quatro volta a ser par: de novo a primeira, e a terceira por cima de ambas.",
),
approved="APROVADO",
above="O carimbo verde «Aprovado» fica por cima da página.",
offered="Um leitor de PDF pode ocultá-lo, apagá-lo ou deixá-lo fora da impressão.",
)
# The page in Italian.
ITALIAN = Words(
title="Un timbro, quattro riquadri",
stamps=(
"BOZZA",
"COPIA",
"PAGATO",
"ANNULLATO",
),
captions=(
"Un riquadro largo: la riga sale lungo la diagonale, quasi piatta.",
"Un riquadro alto, timbrato nell’altro senso.",
"Piatto al centro, e largo abbastanza da essere vincolato dalla larghezza.",
"Una parola più lunga in un riquadro più corto: il corpo segue, niente è tagliato.",
),
watermark="CAMPIONE",
under="INTERNO",
flat="ARCHIVIATO",
turned="Ruotato di {degrees}°.",
carried="Filigrane su questa pagina: {count}.",
shadowed="L’ombra è spostata di ({dx}, {dy}) pt.",
later=(
"La pagina due è pari: la prima filigrana la attraversa, a 96 punti.",
"La pagina tre è dispari: la seconda sta sotto, adattata a mezza pagina.",
"La pagina quattro è di nuovo pari: ancora la prima, e la terza sopra entrambe.",
),
approved="APPROVATO",
above="Il timbro verde «Approvato» è sovrapposto alla pagina.",
offered="Lo si può nascondere, eliminare o escludere dalla stampa.",
)
# The page in Spanish.
SPANISH = Words(
title="Un sello, cuatro cajas",
stamps=(
"BORRADOR",
"COPIA",
"PAGADO",
"CANCELADO",
),
captions=(
"Una caja ancha: la línea sube por su diagonal, que es casi plana.",
"Una caja alta, sellada en el otro sentido.",
"Plano por el centro, y tan ancho que lo que manda es el ancho.",
"Una palabra más larga en una caja más corta: el tamaño se adapta, nada se corta.",
),
watermark="MUESTRA",
under="INTERNO",
flat="ARCHIVADO",
turned="Girado {degrees}°.",
carried="Marcas de agua en esta página: {count}.",
shadowed="La sombra está desplazada ({dx}, {dy}) pt.",
later=(
"La página dos es par: la primera marca de agua la cruza, a 96 puntos.",
"La página tres es impar: la segunda va debajo, ajustada a media página.",
"La página cuatro vuelve a ser par: otra vez la primera, y la tercera sobre ambas.",
),
approved="APROBADO",
above="Sobre la página hay un sello verde «Aprobado».",
offered="Un lector de PDF puede ocultarlo, eliminarlo o excluirlo de la impresión.",
)
# The page in German.
GERMAN = Words(
title="Ein Stempel, vier Rahmen",
stamps=(
"ENTWURF",
"KOPIE",
"BEZAHLT",
"STORNIERT",
),
captions=(
"Ein breiter Rahmen: Die Zeile läuft seine fast flache Diagonale hinauf.",
"Ein hoher Rahmen, andersherum gestempelt.",
"Flach durch die Mitte und so breit, dass die Breite die Größe bestimmt.",
"Ein längeres Wort in einem kürzeren Rahmen: Die Größe folgt, nichts wird abgeschnitten.",
),
watermark="MUSTER",
under="INTERN",
flat="ARCHIVIERT",
turned="Um {degrees}° gedreht.",
carried="Wasserzeichen auf dieser Seite: {count}.",
shadowed="Der Schatten ist um ({dx}, {dy}) pt versetzt.",
later=(
"Seite zwei ist gerade: Das erste Wasserzeichen liegt quer darüber, in 96 Punkt.",
"Seite drei ist ungerade: Das zweite liegt darunter, auf die halbe Seite eingepasst.",
"Seite vier ist wieder gerade: noch einmal das erste, und das dritte über beiden.",
),
approved="GENEHMIGT",
above="Der grüne Stempel „Genehmigt“ liegt über der Seite.",
offered="Man kann ihn ausblenden, löschen oder beim Drucken weglassen.",
)
# 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 boxes() -> list[Stamped]:
"""The boxes, from the top of the page down."""
return [
Stamped(
MARGIN,
606.0,
483.0,
150.0,
hqf_pdf.Slant.Up,
hqf_pdf.Rgb(0.75, 0.16, 0.16),
),
Stamped(
MARGIN,
300.0,
150.0,
260.0,
hqf_pdf.Slant.Down,
hqf_pdf.Rgb(0.25, 0.35, 0.7),
),
Stamped(
268.0,
300.0,
271.0,
260.0,
hqf_pdf.Slant.Flat,
hqf_pdf.Rgb(0.1, 0.45, 0.25),
),
Stamped(
MARGIN,
130.0,
483.0,
110.0,
hqf_pdf.Slant.Up,
hqf_pdf.Rgb(0.45, 0.45, 0.45),
),
]
def outline(content: hqf_pdf.Content, stamped: Stamped) -> None:
"""Draws the outline of a box, so that what the stamp was fitted to can be seen."""
content.save_state()
content.set_stroke(hqf_pdf.Rgb.gray(0.8))
content.set_line_width(0.5)
content.rect(stamped.x, stamped.y, stamped.width, stamped.height)
content.stroke()
content.restore_state()
def caption(
content: hqf_pdf.Content,
font: hqf_pdf.FontHandle,
stamped: Stamped,
row: float,
text: str,
) -> None:
"""Writes line `row` of the caption under a box, the first one at row zero."""
content.save_state()
content.set_fill(hqf_pdf.Rgb.gray(0.35))
content.draw_text(font, 8.5, stamped.x, stamped.y - 14.0 - 11.0 * row, text)
content.restore_state()
def watermarks(font, words, translucent, shadow) -> tuple[hqf_pdf.Watermark, ...]:
"""The three watermarks the pages after the first carry: the first on the even pages
from page two, the second under the odd pages from page three with `shadow` behind
it, the third flat over every page from page four. The first and the third are
drawn under `translucent`."""
return (
hqf_pdf.Watermark(
font,
words.watermark,
first=1,
parity=hqf_pdf.PageParity.Even,
size=WATERMARK_SIZE,
color=hqf_pdf.Rgb(0.75, 0.16, 0.16),
state=translucent,
),
hqf_pdf.Watermark(
font,
words.under,
first=2,
parity=hqf_pdf.PageParity.Odd,
stacking=hqf_pdf.Stacking.Under,
span=0.5,
slant=hqf_pdf.Slant.Down,
color=hqf_pdf.Rgb.gray(0.88),
shadow=shadow,
),
hqf_pdf.Watermark(
font,
words.flat,
first=3,
parity=hqf_pdf.PageParity.Every,
stacking=hqf_pdf.Stacking.Over,
size=28.0,
slant=hqf_pdf.Slant.Flat,
color=hqf_pdf.Rgb(0.2, 0.35, 0.65),
state=translucent,
),
)
def press_approved(page, content, font, words: Words, baseline: float) -> None:
"""Writes the two lines saying what the stamp above the last page is, from `baseline`
down, and presses that stamp under them, above the page."""
for text in (words.above, words.offered):
content.draw_text(font, 10.0, MARGIN, baseline, text)
baseline -= 14.0
page.add_annotation(
hqf_pdf.StampAnnotation(MARGIN, baseline - 56.0, 200.0, 56.0)
.icon(hqf_pdf.StampIcon.Approved)
.contents(f"{words.above} {words.offered}")
.words(
hqf_pdf.Stamp(
font,
words.approved,
slant=hqf_pdf.Slant.Flat,
color=APPROVED_GREEN,
)
)
)
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("stamp.pdf", language)).stem)
document = hqf_pdf.Document()
document.set_license(_licence.licensed())
font = document.add_font(hqf_pdf.Font.from_path(_out.DEFAULT_FONT))
translucent = document.add_ext_gstate(hqf_pdf.ExtGState(fill_alpha=WATERMARK_ALPHA))
shadow = hqf_pdf.Shadow(3.0, -3.0, hqf_pdf.Rgb.gray(0.7))
marks = watermarks(font, words, translucent, shadow)
carried = [
sum(1 for mark in marks if mark.covers(index))
for index in range(1, 1 + len(words.later))
]
for mark in marks:
document.add_watermark(mark)
content = hqf_pdf.Content()
content.draw_text(font, TITLE_SIZE, MARGIN, 780.0, words.title)
stamped_boxes = boxes()
for stamped, text, caption_text in zip(stamped_boxes, words.stamps, words.captions):
outline(content, stamped)
stamp = hqf_pdf.Stamp(font, text, slant=stamped.slant, color=stamped.color)
stamp.draw(content, stamped.x, stamped.y, stamped.width, stamped.height)
caption(content, font, stamped, 0.0, caption_text)
angle = stamp.angle(stamped.width, stamped.height)
turned = words.turned.replace("{degrees}", f"{math.degrees(angle):.0f}")
caption(content, font, stamped, 1.0, turned)
page = hqf_pdf.Page.a4()
page.set_content(content)
document.add_page(page)
for index, line in enumerate(words.later):
content = hqf_pdf.Content()
content.draw_text(font, LATER_SIZE, MARGIN, 780.0, line)
said = [words.carried.replace("{count}", str(carried[index]))]
if index == 1:
said.append(
words.shadowed.replace("{dx}", f"{shadow.dx:.0f}").replace(
"{dy}", f"{shadow.dy:.0f}"
)
)
baseline = 760.0
for text in said:
content.draw_text(font, 10.0, MARGIN, baseline, text)
baseline -= 14.0
page = hqf_pdf.Page.a4()
if index == len(words.later) - 1:
press_approved(page, content, font, words, baseline)
page.set_content(content)
document.add_page(page)
written = document.write(out)
pages = 1 + len(words.later)
print(f"wrote {out}: {written} bytes, {len(stamped_boxes)} stamps, {pages} pages")
if __name__ == "__main__":
main()
|