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 | """Writes a document whose every string and every stream is ciphertext.
The Python twin of the `write_protected` example in Rust: the same document,
through the binding rather than through the library directly.
The file is locked with AES under a two-hundred-and-fifty-six-bit key. Its user
password is empty, so any reader opens it without asking for one, which is what most
protected files in the world do; the author's password is set, and the page states it
so that the file can be opened both ways.
What the file grants is printing and reading aloud. Copying, changing and taking pages
out are withheld — as requests a reader honours, not as locks: a reader that ignores
them opens the document all the same, and the page says so rather than letting the file
be taken for a safe.
The page is written in the language `HQF_PDF_LANG` names. The password is not: it is a
string typed into a reader, and a translated password opens nothing.
Usage: python examples/write_protected.py [out.pdf] [font.ttf]
HQF_PDF_LANG=fr python examples/write_protected.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 heading sits, in points up from the foot of the sheet.
HEADING_BASELINE = 760.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 = 716.0
# How far one line of the body sits below the one before it, in points.
LEADING = 18.0
# The size the heading over the closing note is set at, in points.
NOTE_HEADING_SIZE = 13.0
# Where the baseline of that heading sits, in points up from the foot of the sheet.
NOTE_HEADING_BASELINE = 536.0
# Where the baseline of the first line of the note sits, in points up from the foot of
# the sheet.
NOTE_BASELINE = 508.0
# The author's password, which the page states so that the file can be opened both ways.
# It stands outside the words: a password is typed into a reader, and a translated one
# opens nothing.
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 password is not among them: it is typed into a reader, not translated.
"""
# What the document is called, both at the head of the page and in what the file
# says of itself.
title: str
# The body of the page, one line to a line.
body: tuple[str, ...]
# What stands before the author's password.
password_label: str
# What stands over the closing note.
note_heading: str
# The closing note, which says where the key comes from.
note: tuple[str, ...]
# The page in English.
ENGLISH = Words(
title="A document written protected",
body=(
"Every string and every stream in this file is ciphertext, locked",
"with AES under a two-hundred-and-fifty-six-bit key.",
"This copy opens without a password: its user password is empty,",
"which is what most protected files in the world do.",
"The reader is asked to allow printing and reading aloud, and to",
"withhold copying, changing and taking pages out. Those are",
"requests a reader honours, not locks: one that ignores them opens",
"the document all the same.",
),
password_label="The author's password lifts them:",
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(
title="Un document écrit protégé",
body=(
"Chaque chaîne et chaque flux de ce fichier est chiffré, sous une",
"clé AES de deux cent cinquante-six bits.",
"Cette copie s'ouvre sans mot de passe : son mot de passe",
"utilisateur est vide, comme la plupart des fichiers protégés.",
"Le lecteur est prié d'autoriser l'impression et la lecture à voix",
"haute, et de refuser la copie, la modification et le retrait de",
"pages. Ce sont des demandes qu'un lecteur honore, pas des",
"verrous : celui qui les ignore ouvre le document quand même.",
),
password_label="Le mot de passe de l'auteur les lève :",
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, the last line of which states the author's password."""
return [*words.body, f"{words.password_label} {OWNER_PASSWORD}"]
def encryption() -> hqf_pdf.Encryption:
"""What the document is locked with: the author's password, and what is allowed."""
return (
hqf_pdf.Encryption(SEED)
.owner_password(OWNER_PASSWORD)
.permissions(hqf_pdf.Permissions().printing().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 = [(words.title, HEADING_SIZE, HEADING_BASELINE)]
baseline = FIRST_BASELINE
for line in lines(words):
drawn.append((line, BODY_SIZE, baseline))
baseline -= LEADING
drawn.append((words.note_heading, NOTE_HEADING_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, words: Words) -> 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 drawing(words):
content.draw_text(handle, size, LEFT, baseline, line)
result = hqf_pdf.Page.a4()
result.set_content(content)
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("protected.pdf", language)).stem)
document = hqf_pdf.Document()
document.set_license(_licence.licensed())
document.set_info("Title", words.title)
handle = document.add_font(hqf_pdf.Font.from_path(_out.font_path()))
document.add_page(page(handle, words))
document.protect(encryption())
written = document.write(out)
print(f"wrote {out} ({written} bytes)")
if __name__ == "__main__":
main()
|