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 | //! Marks what has to come out of a statement before it is released.
//!
//! 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, which is what a long entry needs so
//! that the reason stays legible across it. The third shows nothing at all.
//!
//! Usage: `cargo run --example write_redaction -- tmp/redaction.pdf`
//! `HQF_PDF_LANG=fr cargo run --example write_redaction -- tmp/caviarde.pdf`
use std::env;
use std::fs;
use std::path::{Path, PathBuf};
use hqf_pdf::annotation::Redaction;
use hqf_pdf::content::Content;
use hqf_pdf::form::FieldAlign;
use hqf_pdf::layout::TextFlow;
use hqf_pdf::{Document, Font, FontHandle, Page, Rgb};
#[path = "shared/out.rs"]
mod out;
#[path = "shared/licence.rs"]
mod licence;
#[path = "shared/language.rs"]
mod language;
#[path = "shared/failure.rs"]
mod failure;
use language::Language;
/// The font the example draws with.
fn default_font() -> PathBuf {
Path::new(env!("CARGO_MANIFEST_DIR"))
.join("tests")
.join("fonts")
.join("DejaVuSans.ttf")
}
/// The margin the page is laid out inside.
const MARGIN: f64 = 56.0;
/// How far the page runs across, between the margins.
const WIDTH: f64 = 483.0;
/// How far the value of an entry stands from the margin.
const VALUE: f64 = 160.0;
/// The size the entries are set in.
const SIZE: f64 = 11.0;
/// The size the word in a gap is set in.
const SHOWN_SIZE: f64 = 9.0;
/// How far a mark reaches above and below the baseline of the line it covers.
const OVER: f64 = 3.0;
const UNDER: f64 = 3.0;
/// How many entries of the statement are struck out.
const STRUCK: usize = 3;
/// Where each entry's baseline sits, as released.
const ROWS: [f64; STRUCK] = [700.0, 676.0, 652.0];
/// Where each gap's baseline sits, in the half drawn to show what the marks ask
/// for.
const SHOWN_ROWS: [f64; STRUCK] = [512.0, 488.0, 464.0];
/// What each entry states. None of it is anybody's: the statement is written
/// for this page.
const ENTRIES: [&str; STRUCK] = [
"Claire Vasseur",
"14 rue des Lilas, 59000 Lille",
"FR76 3000 1007 9412 3456 7890 185",
];
/// The words the page is written in, one set per language.
#[derive(Debug)]
struct Words {
/// The line at the head of the page.
title: &'static str,
/// The heading over the statement as it is released.
released: &'static str,
/// The heading over the half drawn to show what the marks ask for.
applied: &'static str,
/// What each struck-out entry is called.
labels: [&'static str; STRUCK],
/// The word shown in the first gap, and repeated along the second.
shown: &'static str,
/// The paragraph the statement is released with.
body: &'static str,
/// The line under it all.
caption: &'static str,
}
impl Words {
/// The words the page is written in, in `language`.
fn of(language: Language) -> &'static Self {
language::pick(&WORDS, language)
}
}
/// The page in English.
const ENGLISH: Words = 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.
const FRENCH: Words = 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.
static WORDS: [(Language, &Words); 2] =
[(Language::English, &ENGLISH), (Language::French, &FRENCH)];
/// Writes a line of ordinary text at `(x, y)`.
fn write(
content: &mut Content,
font: &FontHandle,
x: f64,
y: f64,
size: f64,
text: &str,
) -> Result<(), hqf_pdf::Error> {
content.begin_text();
content.set_font(font, size)?;
content.text_origin(x, y)?;
content.show_glyphs(&font.glyphs(text));
content.end_text();
Ok(())
}
/// Writes a paragraph in a column the width of the page.
fn paragraph(
content: &mut Content,
font: &FontHandle,
size: f64,
top: f64,
color: Rgb,
text: &str,
) -> Result<(), hqf_pdf::Error> {
let flow = TextFlow::new(font, size).leading(size * 1.4).color(color);
let lines = flow.break_lines(text, WIDTH);
content.begin_text();
flow.draw(content, &lines, MARGIN, top, WIDTH)?;
content.end_text();
Ok(())
}
/// The most copies of a word one gap is filled with.
const MOST_COPIES: usize = 64;
/// Converts a count of copies to a float. A gap holds at most [`MOST_COPIES`]
/// of them, which is nowhere near where a `usize` loses precision as an `f64`.
#[expect(
clippy::cast_precision_loss,
reason = "a count of copies is far below f64's exact-integer range"
)]
const fn count_f64(n: usize) -> f64 {
n as f64
}
/// How many copies of a word `one` wide, set `step` apart, stand in `room`.
fn fitting(room: f64, one: f64, step: f64) -> usize {
(0..MOST_COPIES)
.take_while(|copy| count_f64(*copy).mul_add(step, one) <= room)
.count()
}
/// 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.
fn gap(
content: &mut Content,
font: &FontHandle,
y: f64,
width: f64,
shown: &str,
align: FieldAlign,
repeating: bool,
) -> Result<(), hqf_pdf::Error> {
content.save_state();
content.set_fill(Rgb::BLACK)?;
content.rect(MARGIN + VALUE, y - UNDER, width, SIZE + OVER + UNDER)?;
content.fill();
if !shown.is_empty() {
content.set_fill(Rgb::gray(1.0))?;
let one = font.measure(shown, SHOWN_SIZE);
let step = one + font.measure(" ", SHOWN_SIZE);
let left = MARGIN + VALUE;
let start = match align {
FieldAlign::Center => (width - one).mul_add(0.5, left),
FieldAlign::Right => left + width - one,
_ => left,
};
let room = left + width - start;
let times = if repeating {
fitting(room, one, step)
} else {
usize::from(one <= room)
};
for copy in 0..times {
let at = count_f64(copy).mul_add(step, start);
write(content, font, at, y, SHOWN_SIZE, shown)?;
}
}
content.restore_state();
Ok(())
}
fn main() -> std::process::ExitCode {
failure::reported(run())
}
fn run() -> Result<(), Box<dyn std::error::Error>> {
let language = Language::from_environment()?;
let words = Words::of(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/`.
let out = env::args()
.nth(1)
.unwrap_or_else(|| language.file_name(&out::default_path("redaction")));
let mut doc = Document::new();
doc.set_license(licence::licensed());
let font = doc.add_font(Font::parse(fs::read(default_font())?)?);
let widths: [f64; STRUCK] = std::array::from_fn(|struck| font.measure(ENTRIES[struck], SIZE));
let mut content = Content::new();
write(&mut content, &font, MARGIN, 780.0, 16.0, words.title)?;
content.save_state();
content.set_fill(Rgb::gray(0.35))?;
write(&mut content, &font, MARGIN, 736.0, 9.0, words.released)?;
write(&mut content, &font, MARGIN, 548.0, 9.0, words.applied)?;
content.restore_state();
for ((row, label), entry) in ROWS.iter().zip(words.labels).zip(ENTRIES) {
write(&mut content, &font, MARGIN, *row, SIZE, label)?;
write(&mut content, &font, MARGIN + VALUE, *row, SIZE, entry)?;
}
paragraph(&mut content, &font, 10.0, 618.0, Rgb::BLACK, words.body)?;
for (row, label) in SHOWN_ROWS.iter().zip(words.labels) {
write(&mut content, &font, MARGIN, *row, SIZE, label)?;
}
gap(
&mut content,
&font,
SHOWN_ROWS[0],
widths[0],
words.shown,
FieldAlign::Center,
false,
)?;
gap(
&mut content,
&font,
SHOWN_ROWS[1],
widths[1],
words.shown,
FieldAlign::Left,
true,
)?;
gap(
&mut content,
&font,
SHOWN_ROWS[2],
widths[2],
"",
FieldAlign::Left,
false,
)?;
paragraph(
&mut content,
&font,
8.5,
428.0,
Rgb::gray(0.35),
words.caption,
)?;
let mut page = Page::a4();
page.content = content.into_bytes();
// Each mark covers the words it hides and nothing else, so it is measured
// off the line it stands on.
let marks: [Redaction; STRUCK] = std::array::from_fn(|struck| {
Redaction::new(
MARGIN + VALUE,
ROWS[struck] - UNDER,
widths[struck],
SIZE + OVER + UNDER,
)
.fill(Rgb::BLACK)
});
let [named, lived, banked] = marks;
page.annotations.push(
named
.overlay_text(words.shown)
.aligned(FieldAlign::Center)
.into(),
);
page.annotations.push(
lived
.overlay_text(words.shown)
.repeating(true)
.aligned(FieldAlign::Left)
.into(),
);
page.annotations.push(banked.into());
doc.add_page(page)?;
let bytes = doc.to_bytes()?;
if let Some(parent) = Path::new(&out).parent() {
fs::create_dir_all(parent)?;
}
fs::write(&out, &bytes)?;
println!(
"wrote {out}: {} bytes, {STRUCK} entries struck out",
bytes.len()
);
Ok(())
}
#[cfg(test)]
mod tests {
use super::{ENTRIES, ROWS, SHOWN_ROWS, STRUCK, WORDS, language};
/// The lines two languages write the same way. There are none: every word
/// on the page is a word of the language it is in.
const SPARED: [&str; 0] = [];
#[test]
fn every_language_draws_the_page_in_its_own_words() {
let untranslated = language::untranslated_lines(&WORDS, &SPARED);
assert!(
untranslated.is_empty(),
"the page says these in more than one language: {untranslated:?}"
);
}
/// The entries stand far enough apart that no mark reaches the line above
/// it: a mark that overlapped its neighbour would cover a word nobody asked
/// to strike out. The same has to hold of the gaps drawn under them.
#[test]
fn no_two_marks_reach_each_other() {
assert_eq!(ROWS.len(), ENTRIES.len());
assert_eq!(STRUCK, ENTRIES.len());
let apart = super::SIZE + super::OVER + super::UNDER;
for rows in [ROWS, SHOWN_ROWS] {
for pair in rows.windows(2) {
assert!(pair[0] - pair[1] > apart);
}
}
}
}
|