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 | """Marks what has to come out of a statement before it is released.
The Python twin of the `write_redaction` example in Rust. A redaction is a mark, and
only a mark. It states which part of the page must go, what is to be shown in its
place, and what colour fills the gap — and all three describe the page as it will be
once a reading software has applied the mark and written the document out again.
Until then the words are still in the file under the box, and a search finds them.
That is the mistake this page is laid out to make visible.
The upper half is the statement as it is released: the entries readable, each under a
mark that covers exactly the words it hides, because every box is measured off the
line it stands on rather than guessed.
The lower half is drawn by this example, in its own ink, to show what those three
marks ask for. The first shows a word in the middle of the gap. The second repeats it
along the whole width. The third shows nothing at all.
Usage: python examples/write_redaction.py [out.pdf]
HQF_PDF_LANG=fr python examples/write_redaction.py
"""
from __future__ import annotations
from dataclasses import dataclass
from pathlib import Path
import _language
import _licence
import _out
import hqf_pdf
# The margin the page is laid out inside.
MARGIN = 56.0
# How far the page runs across, between the margins.
WIDTH = 483.0
# How far the value of an entry stands from the margin.
VALUE = 160.0
# The size the entries are set in.
SIZE = 11.0
# The size the word in a gap is set in.
SHOWN_SIZE = 9.0
# How far a mark reaches above and below the baseline of the line it covers.
OVER = 3.0
UNDER = 3.0
# The most copies of a word one gap is filled with.
MOST_COPIES = 64
# Where each entry's baseline sits, as released.
ROWS = (700.0, 676.0, 652.0)
# Where each gap's baseline sits, in the half drawn to show what the marks ask for.
SHOWN_ROWS = (512.0, 488.0, 464.0)
# What each entry states. None of it is anybody's: the statement is written for this
# page.
ENTRIES = (
"Claire Vasseur",
"14 rue des Lilas, 59000 Lille",
"FR76 3000 1007 9412 3456 7890 185",
)
@dataclass(frozen=True)
class Words:
"""Every word the page draws, in one language."""
# The line at the head of the page.
title: str
# The heading over the statement as it is released.
released: str
# The heading over the half drawn to show what the marks ask for.
applied: str
# What each struck-out entry is called.
labels: tuple[str, str, str]
# The word shown in the first gap, and repeated along the second.
shown: str
# The paragraph the statement is released with.
body: str
# The line under it all.
caption: str
# The page in English.
ENGLISH = Words(
title="Witness statement, released in part",
released="As it is released",
applied="As a reading software shows it once it has applied the marks",
labels=("Name", "Address", "Account"),
shown="REDACTED",
body="The statement itself is released whole. What identifies the witness "
"is struck out above: the name shows the reason in the middle of its "
"gap, the address repeats it along the length of a longer one, and "
"the account is covered without a word.",
caption="A mark says what must go. It does not take it out. Until a reading "
"software applies these marks and writes the document out again, "
"the words are still under the boxes and a search finds them — "
"which is why the lower half is drawn here in this example's own "
"ink, and is not what the file states.",
)
# The page in French.
FRENCH = Words(
title="Déposition de témoin, communiquée en partie",
released="Telle qu'elle est communiquée",
applied="Telle qu'un logiciel de lecture la montre une fois les marques appliquées",
labels=("Nom", "Adresse", "Compte"),
shown="CAVIARDÉ",
body="La déposition elle-même est communiquée entière. Ce qui identifie le "
"témoin est caviardé ci-dessus : le nom montre le motif au milieu de "
"son emplacement, l'adresse le répète sur toute la longueur d'un "
"emplacement plus large, et le compte est couvert sans un mot.",
caption="Une marque dit ce qui doit partir. Elle ne l'enlève pas. Tant "
"qu'un logiciel de lecture n'a pas appliqué ces marques et réécrit "
"le document, les mots sont toujours sous les boîtes et une "
"recherche les trouve — c'est pourquoi la moitié basse est tracée "
"ici par l'exemple lui-même, et n'est pas ce que le fichier dit.",
)
# 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 fitting(room: float, one: float, step: float) -> int:
"""How many copies of a word `one` wide, set `step` apart, stand in `room`."""
copies = 0
while copies < MOST_COPIES and copies * step + one <= room:
copies += 1
return copies
def paragraph(
content: hqf_pdf.Content,
font: hqf_pdf.FontHandle,
size: float,
top: float,
color: hqf_pdf.Rgb,
text: str,
) -> None:
"""Writes a paragraph in a column the width of the page."""
flow = hqf_pdf.TextFlow(font, size, leading=size * 1.4, color=color)
lines = flow.break_lines(text, WIDTH)
flow.draw(content, lines, MARGIN, top, WIDTH)
def gap(
content: hqf_pdf.Content,
font: hqf_pdf.FontHandle,
y: float,
width: float,
shown: str,
align: hqf_pdf.FieldAlign,
repeating: bool,
) -> None:
"""Draws the gap a mark asks for: the colour it states, and the words it shows in
it, set where its alignment puts them and repeated if it asks for that."""
content.save_state()
content.set_fill(hqf_pdf.Rgb.gray(0.0))
content.rect(MARGIN + VALUE, y - UNDER, width, SIZE + OVER + UNDER)
content.fill()
if shown:
content.set_fill(hqf_pdf.Rgb.gray(1.0))
one = font.measure(shown, SHOWN_SIZE)
step = one + font.measure(" ", SHOWN_SIZE)
left = MARGIN + VALUE
if align == hqf_pdf.FieldAlign.Center:
start = (width - one) * 0.5 + left
elif align == hqf_pdf.FieldAlign.Right:
start = left + width - one
else:
start = left
room = left + width - start
times = fitting(room, one, step) if repeating else int(one <= room)
for copy in range(times):
content.draw_text(font, SHOWN_SIZE, copy * step + start, y, shown)
content.restore_state()
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("redaction.pdf", language)).stem)
document = hqf_pdf.Document()
document.set_license(_licence.licensed())
font = document.add_font(hqf_pdf.Font.from_path(_out.DEFAULT_FONT))
widths = tuple(font.measure(entry, SIZE) for entry in ENTRIES)
content = hqf_pdf.Content()
content.draw_text(font, 16.0, MARGIN, 780.0, words.title)
content.save_state()
content.set_fill(hqf_pdf.Rgb.gray(0.35))
content.draw_text(font, 9.0, MARGIN, 736.0, words.released)
content.draw_text(font, 9.0, MARGIN, 548.0, words.applied)
content.restore_state()
for row, label, entry in zip(ROWS, words.labels, ENTRIES):
content.draw_text(font, SIZE, MARGIN, row, label)
content.draw_text(font, SIZE, MARGIN + VALUE, row, entry)
paragraph(content, font, 10.0, 618.0, hqf_pdf.Rgb.gray(0.0), words.body)
for row, label in zip(SHOWN_ROWS, words.labels):
content.draw_text(font, SIZE, MARGIN, row, label)
gap(
content,
font,
SHOWN_ROWS[0],
widths[0],
words.shown,
hqf_pdf.FieldAlign.Center,
False,
)
gap(
content,
font,
SHOWN_ROWS[1],
widths[1],
words.shown,
hqf_pdf.FieldAlign.Left,
True,
)
gap(content, font, SHOWN_ROWS[2], widths[2], "", hqf_pdf.FieldAlign.Left, False)
paragraph(content, font, 8.5, 428.0, hqf_pdf.Rgb.gray(0.35), words.caption)
page = hqf_pdf.Page.a4()
page.set_content(content)
# Each mark covers the words it hides and nothing else, so it is measured off the
# line it stands on.
marks = [
hqf_pdf.RedactionAnnotation(
MARGIN + VALUE, ROWS[struck] - UNDER, widths[struck], SIZE + OVER + UNDER
).fill(hqf_pdf.Rgb.gray(0.0))
for struck in range(len(ENTRIES))
]
page.add_annotation(
marks[0].overlay_text(words.shown).aligned(hqf_pdf.FieldAlign.Center)
)
page.add_annotation(
marks[1]
.overlay_text(words.shown)
.repeating(True)
.aligned(hqf_pdf.FieldAlign.Left)
)
page.add_annotation(marks[2])
document.add_page(page)
written = document.write(out)
print(f"wrote {out}: {written} bytes, {len(ENTRIES)} entries struck out")
if __name__ == "__main__":
main()
|