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 | """Writes a document a reader shows nothing of until a password is typed in.
The Python twin of the `write_locked` example in Rust: the same document, through the
binding rather than through the library directly.
Every string and every stream in the file is ciphertext, locked with AES under a
two-hundred-and-fifty-six-bit key. The user password is set, so a reader asks for it
before it shows a single page, and the password is what the key is worked out from: a
reader given the wrong one shows nothing at all. The page states the password, so that
the file can be opened by whoever is handed it.
Once it is open, the file grants printing, at the resolution the page was drawn at, and
reading aloud; it withholds copying, changing and taking pages out — as requests a
reader honours, not as locks. The author's password lifts them and opens the file as
well, and the page states that one too.
The document that opens with nothing typed is `write_restricted`, and this page names it
in words rather than by that name.
The page is written in the language `HQF_PDF_LANG` names. The passwords are not: they
are strings typed into a reader, and a translated password opens nothing.
Usage: python examples/write_locked.py [out.pdf] [font.ttf]
HQF_PDF_LANG=fr python examples/write_locked.py
"""
from __future__ import annotations
from dataclasses import dataclass
from pathlib import Path
import _language
import _licence
import _out
import hqf_pdf
# How far in from the left edge of the sheet every line is set, in points.
LEFT = 72.0
# The size the heading is set at, in points.
HEADING_SIZE = 18.0
# Where the baseline of the first line of the heading sits, in points up from the foot
# of the sheet.
HEADING_BASELINE = 760.0
# How far one line of the heading sits below the one before it, in points.
HEADING_LEADING = 22.0
# The size the body is set at, in points.
BODY_SIZE = 11.0
# Where the baseline of the first line of the body sits, in points up from the foot of
# the sheet.
FIRST_BASELINE = 700.0
# How far one line of the body sits below the one before it, in points.
LEADING = 18.0
# The size the heading over each closing section is set at, in points.
SECTION_SIZE = 13.0
# Where the baseline of the heading over the other document sits, in points up from the
# foot of the sheet.
COMPANION_HEADING_BASELINE = 520.0
# Where the baseline of the first line about the other document sits, in points up from
# the foot of the sheet.
COMPANION_BASELINE = 492.0
# Where the baseline of the heading over the closing note sits, in points up from the
# foot of the sheet.
NOTE_HEADING_BASELINE = 420.0
# Where the baseline of the first line of the note sits, in points up from the foot of
# the sheet.
NOTE_BASELINE = 392.0
# The password a reader asks for before it shows a page, which the page states so that
# the file can be opened at all. It stands outside the words: a password is typed into a
# reader, and a translated one opens nothing.
USER_PASSWORD = "the reader"
# The author's password, which the page states so that what the file withholds can be
# lifted. It stands outside the words for the same reason the other one does.
OWNER_PASSWORD = "the owner"
# The thirty-two bytes the file key is built from.
#
# They are fixed here, so that the example writes the same file on every run and one
# build can be compared with the last. A program takes them from its operating system —
# `os.urandom(32)`. A file locked under a seed anybody can read is a file anybody opens.
SEED = bytes.fromhex(
"00112233445566778899aabbccddeeff0f1e2d3c4b5a69788796a5b4c3d2e1f0"
)
@dataclass(frozen=True)
class Words:
"""Every word the page is written in, in one language.
The passwords are not among them: they are typed into a reader, not translated.
"""
# What the document is called, set over two lines at the head of the page and joined
# by a space in what the file says of itself.
heading: tuple[str, ...]
# The body of the page, one line to a line.
body: tuple[str, ...]
# What stands before the password the file asks for.
user_password_label: str
# What stands before the author's password.
password_label: str
# What stands over the lines about the other document.
companion_heading: str
# What the other document is, and how it differs from this one.
companion: tuple[str, ...]
# What stands over the closing note.
note_heading: str
# The closing note, which says where the key comes from.
note: tuple[str, ...]
def title(self) -> str:
"""What the file says of itself: the two lines of the heading, in a row."""
return " ".join(self.heading)
# The page in English.
ENGLISH = Words(
heading=("A document that asks for a password", "before it opens"),
body=(
"This file shows nothing until a password is typed in. Type the",
"wrong one and no page appears at all.",
"Everything written in it is scrambled, under a key of two",
"hundred and fifty-six bits, and the password is what that key",
"is worked out from.",
"Once it is open, it asks the reading software to let it be",
"printed and read out loud, but not copied or changed.",
),
user_password_label="The password this document asks for:",
password_label="The author's password opens it too, and lifts the request:",
companion_heading="The other document of this pair",
companion=(
"A second document was written beside this one. That one opens",
"on its own, with nothing to type in, and only asks that it not",
"be copied or changed.",
),
note_heading="Where the key comes from",
note=(
"The thirty-two bytes the key is built from are fixed in this",
"example, so that it writes the same file on every run and one",
"build can be compared with the last. A program takes them from",
"its operating system: a seed anybody can read locks nothing.",
),
)
# The page in French.
FRENCH = Words(
heading=(
"Un document qui demande un mot de passe",
"pour s'ouvrir",
),
body=(
"Ce fichier ne montre rien tant qu'un mot de passe n'a pas été",
"tapé. Tapez le mauvais et aucune page n'apparaît.",
"Tout ce qu'il contient est brouillé, sous une clé de deux cent",
"cinquante-six bits, et c'est le mot de passe qui permet de",
"retrouver cette clé.",
"Une fois ouvert, il demande au logiciel de lecture de laisser",
"imprimer et lire à voix haute, mais ni copier ni modifier.",
),
user_password_label="Le mot de passe que ce document demande :",
password_label="Le mot de passe de l'auteur l'ouvre aussi, et lève la demande :",
companion_heading="L'autre document de la paire",
companion=(
"Un second document a été écrit à côté de celui-ci. Lui s'ouvre",
"tout seul, sans rien à taper, et demande seulement qu'on ne le",
"copie ni ne le modifie.",
),
note_heading="D'où vient la clé",
note=(
"Les trente-deux octets dont la clé est tirée sont figés dans cet",
"exemple, pour qu'il écrive le même fichier à chaque fois et qu'une",
"version se compare à la précédente. Un programme les prend à son",
"système : une graine que tout le monde peut lire ne ferme rien.",
),
)
# 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}
def lines(words: Words) -> list[str]:
"""What the body says.
Its last two lines state the password the file asks for and the author's.
"""
return [
*words.body,
f"{words.user_password_label} {USER_PASSWORD}",
f"{words.password_label} {OWNER_PASSWORD}",
]
def encryption() -> hqf_pdf.Encryption:
"""What the document is locked with.
The password a reader asks for, the author's, and what is allowed once it is open:
printing at the resolution the page was drawn at, and reading aloud.
"""
return (
hqf_pdf.Encryption(SEED)
.user_password(USER_PASSWORD)
.owner_password(OWNER_PASSWORD)
.permissions(
hqf_pdf.Permissions()
.printing()
.printing_at_full_resolution()
.extracting_for_accessibility()
)
)
def drawing(words: Words) -> list[tuple[str, float, float]]:
"""Every line the page draws, in the order they are drawn.
Each is what it says, the size it is set at, and where its baseline sits in points
up from the foot of the sheet.
"""
drawn = []
baseline = HEADING_BASELINE
for line in words.heading:
drawn.append((line, HEADING_SIZE, baseline))
baseline -= HEADING_LEADING
baseline = FIRST_BASELINE
for line in lines(words):
drawn.append((line, BODY_SIZE, baseline))
baseline -= LEADING
drawn.append(
(words.companion_heading, SECTION_SIZE, COMPANION_HEADING_BASELINE)
)
baseline = COMPANION_BASELINE
for line in words.companion:
drawn.append((line, BODY_SIZE, baseline))
baseline -= LEADING
drawn.append((words.note_heading, SECTION_SIZE, NOTE_HEADING_BASELINE))
baseline = NOTE_BASELINE
for line in words.note:
drawn.append((line, BODY_SIZE, baseline))
baseline -= LEADING
return drawn
def page(
handle: hqf_pdf.FontHandle, drawn: list[tuple[str, float, float]]
) -> hqf_pdf.Page:
"""The page: every line set by its own origin, each in an object of its own."""
content = hqf_pdf.Content()
for line, size, baseline in drawn:
content.draw_text(handle, size, LEFT, baseline, line)
result = hqf_pdf.Page.a4()
result.set_content(content)
return result
def document(words: Words, face: Path) -> hqf_pdf.Document:
"""The document and its one page, before it is locked."""
result = hqf_pdf.Document()
result.set_license(_licence.licensed())
result.set_info("Title", words.title())
handle = result.add_font(hqf_pdf.Font.from_path(face))
result.add_page(page(handle, drawing(words)))
return result
def written(path: Path, doc: hqf_pdf.Document) -> None:
"""Writes `doc` to `path`, making the directory it goes in if it is not there."""
path.parent.mkdir(parents=True, exist_ok=True)
print(f"wrote {path} ({doc.write(path)} bytes)")
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("locked.pdf", language)).stem)
doc = document(words, _out.font_path())
doc.protect(encryption())
written(Path(out), doc)
if __name__ == "__main__":
main()
|