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
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564 | //! Adds a signature area, a date, a line of one's own and a page at the end to
//! an agreement somebody else sent, then a place to sign over the signature
//! area, without moving a byte of it.
//!
//! The agreement arrives as a PDF of two pages. What is added is created as a
//! document of its own: its first page is drawn over the agreement's last page,
//! and its second page is added after it. The framed, empty signature field
//! [`FIELD`] is then added over the signature area, in a section of its own,
//! through `IncrementalUpdate::to_bytes_with_signature_field`. The file that
//! comes out is the agreement, byte for byte, followed by the sections holding
//! what was added — an ordinary signature the agreement already carried still
//! covers the bytes it covered, and a reader that shows the file's earlier
//! versions still shows the agreement as it arrived. A signature certifying the
//! agreement against change does not survive: no permission ISO 32000-2 table
//! 257 gives `/DocMDP` allows a page to be added or drawn over, and nothing
//! here looks for one.
//!
//! The example creates the agreement itself first, so that it runs with nothing
//! handed to it. Every line both documents draw is held in `Words`, once per
//! language, and `HQF_PDF_LANG` picks which set is drawn.
//!
//! Usage: `cargo run --example write_added_to -- tmp/added_to.pdf`
//! `HQF_PDF_LANG=fr cargo run --example write_added_to -- tmp/ajout.pdf`
use std::env;
use std::fs;
use std::path::{Path, PathBuf};
use hqf_pdf::content::Content;
use hqf_pdf::{
Document, FieldBorder, Font, FontHandle, IncrementalUpdate, Page, Rgb, SignatureField,
};
#[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 lines of both documents are set in.
fn default_font() -> PathBuf {
Path::new(env!("CARGO_MANIFEST_DIR"))
.join("tests")
.join("fonts")
.join("DejaVuSans.ttf")
}
/// The margin every line starts from.
const MARGIN: f64 = 56.0;
/// The size of a title.
const TITLE_SIZE: f64 = 18.0;
/// The size of a line of the agreement and of the annex.
const BODY_SIZE: f64 = 11.0;
/// How far apart two lines of the agreement and of the annex stand.
const LEADING: f64 = 18.0;
/// The size of what is written in and under the signature area.
const SMALL_SIZE: f64 = 9.0;
/// The signature area: its left edge, its bottom, its width and its height.
const AREA: [f64; 4] = [320.0, 120.0, 219.0, 90.0];
/// The name of the place to sign added over the signature area.
const FIELD: &str = "approval";
/// The colour of the frame of the place to sign, and of the word written in the
/// corner of the signature area.
const FRAME: Rgb = Rgb::new(0.2, 0.35, 0.7);
/// The date the agreement is received on, written the same way in every
/// language.
const DATE: &str = "2026-09-12";
/// How many lines each page of the agreement holds.
const CLAUSES: usize = 5;
/// How many lines the annex holds.
const ANNEX_LINES: usize = 4;
/// The words both documents are written in, one set per language.
#[derive(Debug)]
struct Words {
/// The title of the agreement.
title: &'static str,
/// The lines of the agreement's first page.
first: [&'static str; CLAUSES],
/// The lines of the agreement's second page.
second: [&'static str; CLAUSES],
/// What is written in the corner of the signature area.
signature: &'static str,
/// What stands before the date under the signature area.
received_on: &'static str,
/// The line of one's own, written above the signature area.
approval: &'static str,
/// The title of the page added at the end.
annex_title: &'static str,
/// The lines of the page added at the end. `{bytes}` stands for how many
/// bytes the agreement arrived as.
annex: [&'static str; ANNEX_LINES],
}
impl Words {
/// The words the documents are written in, in `language`.
fn of(language: Language) -> &'static Self {
language::pick(&WORDS, language)
}
}
/// The documents in English.
const ENGLISH: Words = Words {
title: "Maintenance agreement",
first: [
"1. The supplier services the two lifts of the building every quarter.",
"2. A fault reported before noon is looked at the same working day.",
"3. Parts are charged at the price list in force on the day they are fitted.",
"4. The agreement runs for one year and renews itself unless ended.",
"5. Either party ends it by letter, three months before it renews.",
],
second: [
"6. The customer keeps the machine rooms clear and reachable.",
"7. The supplier's staff sign the logbook at every visit.",
"8. An invoice is paid within thirty days of the date it bears.",
"9. Disputes go before the courts where the building stands.",
"Signed for the supplier, and sent to the customer to sign.",
],
signature: "Signature",
received_on: "Received on",
approval: "Read and approved, subject to annex A.",
annex_title: "Annex A",
annex: [
"The lift in the north wing is serviced every month, not every quarter.",
"This page, the signature area, the date and the line above it were",
"added after the agreement arrived. The first {bytes} bytes of this file",
"are the agreement exactly as it was sent.",
],
};
/// The documents in French.
const FRENCH: Words = Words {
title: "Contrat de maintenance",
first: [
"1. Le prestataire entretient les deux ascenseurs chaque trimestre.",
"2. Une panne signalée avant midi est examinée le même jour ouvré.",
"3. Les pièces sont facturées au tarif en vigueur le jour de leur pose.",
"4. Le contrat court un an et se renouvelle sauf résiliation.",
"5. Chaque partie peut le résilier par lettre, trois mois avant l’échéance.",
],
second: [
"6. Le client maintient les salles des machines dégagées et accessibles.",
"7. Le personnel du prestataire signe le registre à chaque visite.",
"8. Une facture est réglée dans les trente jours suivant sa date.",
"9. Les litiges relèvent des tribunaux du lieu de l’immeuble.",
"Signé pour le prestataire, et envoyé au client pour signature.",
],
signature: "Signature",
received_on: "Reçu le",
approval: "Lu et approuvé, sous réserve de l’annexe A.",
annex_title: "Annexe A",
annex: [
"L’ascenseur de l’aile nord est entretenu chaque mois, et non chaque trimestre.",
"Cette page, la zone de signature, la date et la ligne au-dessus ont été",
"ajoutées après réception du contrat. Les {bytes} premiers octets de ce",
"fichier sont le contrat exactement tel qu’il a été envoyé.",
],
};
/// The documents in Dutch.
const DUTCH: Words = Words {
title: "Onderhoudscontract",
first: [
"1. De leverancier onderhoudt de twee liften van het gebouw elk kwartaal.",
"2. Een storing die vóór twaalf uur is gemeld, wordt nog dezelfde werkdag bekeken.",
"3. Onderdelen worden berekend volgens de prijslijst die geldt op de dag van montage.",
"4. Het contract loopt een jaar en wordt stilzwijgend verlengd.",
"5. Elke partij kan het per brief opzeggen, uiterlijk drie maanden voor de verlenging.",
],
second: [
"6. De klant houdt de machinekamers vrij en bereikbaar.",
"7. Het personeel van de leverancier tekent het logboek bij elk bezoek.",
"8. Een factuur wordt betaald binnen dertig dagen na de factuurdatum.",
"9. Geschillen worden voorgelegd aan de rechter van de plaats waar het gebouw staat.",
"Namens de leverancier ondertekend en ter ondertekening naar de klant gestuurd.",
],
signature: "Handtekening",
received_on: "Ontvangen op",
approval: "Gelezen en goedgekeurd, onder voorbehoud van bijlage A.",
annex_title: "Bijlage A",
annex: [
"De lift in de noordvleugel wordt elke maand onderhouden, niet elk kwartaal.",
"Deze pagina, het handtekeningvak, de datum en de regel erboven zijn",
"toegevoegd nadat het contract binnenkwam. De eerste {bytes} bytes van dit",
"bestand zijn het contract precies zoals het verstuurd is.",
],
};
/// The documents in Portuguese.
const PORTUGUESE: Words = Words {
title: "Contrato de manutenção",
first: [
"1. O fornecedor revê os dois elevadores do edifício todos os trimestres.",
"2. Uma avaria comunicada antes do meio-dia é atendida no mesmo dia útil.",
"3. As peças são cobradas pela tabela em vigor no dia da montagem.",
"4. O contrato dura um ano e renova-se automaticamente salvo denúncia.",
"5. Qualquer das partes pode denunciá-lo por carta, com três meses de antecedência.",
],
second: [
"6. O cliente mantém as casas das máquinas livres e acessíveis.",
"7. O pessoal do fornecedor assina o livro de registo em cada visita.",
"8. Uma fatura paga-se nos trinta dias seguintes à sua data.",
"9. Os litígios são submetidos ao tribunal da comarca onde fica o edifício.",
"Assinado pelo fornecedor e enviado ao cliente para assinar.",
],
signature: "Assinatura",
received_on: "Recebido em",
approval: "Lido e aprovado, salvo o anexo A.",
annex_title: "Anexo A",
annex: [
"O elevador da ala norte é revisto todos os meses, não a cada trimestre.",
"Esta página, a zona de assinatura, a data e a linha acima foram",
"acrescentadas depois de o contrato chegar. Os primeiros {bytes} bytes",
"deste ficheiro são o contrato tal como foi enviado.",
],
};
/// The documents in Italian.
const ITALIAN: Words = Words {
title: "Contratto di manutenzione",
first: [
"1. Il fornitore revisiona i due ascensori dell’edificio ogni trimestre.",
"2. Un guasto segnalato prima di mezzogiorno è esaminato nello stesso giorno.",
"3. I ricambi sono addebitati al listino in vigore il giorno del montaggio.",
"4. Il contratto dura un anno e si rinnova tacitamente salvo disdetta.",
"5. Ciascuna parte lo disdice per lettera, tre mesi prima del rinnovo.",
],
second: [
"6. Il cliente tiene i locali macchine sgombri e accessibili.",
"7. Il personale del fornitore firma il registro a ogni visita.",
"8. Una fattura si paga entro trenta giorni dalla sua data.",
"9. Le controversie vanno al foro del luogo in cui sorge l’edificio.",
"Firmato per il fornitore e inviato al cliente per la firma.",
],
signature: "Firma",
received_on: "Ricevuto il",
approval: "Letto e approvato, salvo l’allegato A.",
annex_title: "Allegato A",
annex: [
"L’ascensore dell’ala nord è revisionato ogni mese, non ogni trimestre.",
"Questa pagina, la zona della firma, la data e la riga sopra sono state",
"aggiunte dopo l’arrivo del contratto. I primi {bytes} byte di questo file",
"sono il contratto esattamente come è stato inviato.",
],
};
/// The documents in Spanish.
const SPANISH: Words = Words {
title: "Contrato de mantenimiento",
first: [
"1. El proveedor revisa los dos ascensores del edificio cada trimestre.",
"2. Una avería comunicada antes del mediodía se atiende el mismo día hábil.",
"3. Las piezas se cobran a la tarifa vigente el día de su montaje.",
"4. El contrato dura un año y se renueva solo salvo que se rescinda.",
"5. Cualquiera de las partes puede rescindirlo por carta con tres meses de antelación.",
],
second: [
"6. El cliente mantiene las salas de máquinas despejadas y accesibles.",
"7. El personal del proveedor firma el libro de registro en cada visita.",
"8. Cada factura se paga en los treinta días siguientes a su fecha.",
"9. Los litigios se someten a los tribunales del lugar donde se encuentra el edificio.",
"Firmado por el proveedor y enviado al cliente para su firma.",
],
signature: "Firma",
received_on: "Recibido el",
approval: "Leído y conforme, salvo el anexo A.",
annex_title: "Anexo A",
annex: [
"El ascensor del ala norte se revisa cada mes, no cada trimestre.",
"Esta página, la zona de firma, la fecha y la línea de arriba se",
"añadieron después de recibir el contrato. Los primeros {bytes} bytes",
"de este archivo son el contrato tal como se envió.",
],
};
/// The documents in German.
const GERMAN: Words = Words {
title: "Wartungsvertrag",
first: [
"1. Der Auftragnehmer wartet die beiden Aufzüge des Gebäudes jedes Quartal.",
"2. Eine vor zwölf Uhr gemeldete Störung wird am selben Werktag geprüft.",
"3. Ersatzteile werden zum Listenpreis am Tag des Einbaus berechnet.",
"4. Der Vertrag läuft ein Jahr und verlängert sich, sofern er nicht gekündigt wird.",
"5. Jede Partei kann ihn bis drei Monate vor der Verlängerung schriftlich kündigen.",
],
second: [
"6. Der Kunde hält die Maschinenräume frei und zugänglich.",
"7. Das Personal des Auftragnehmers unterschreibt bei jedem Besuch das Prüfbuch.",
"8. Eine Rechnung ist binnen dreißig Tagen ab ihrem Datum zu zahlen.",
"9. Gerichtsstand ist der Ort, an dem das Gebäude steht.",
"Für den Auftragnehmer unterschrieben und dem Kunden zur Unterschrift gesandt.",
],
signature: "Unterschrift",
received_on: "Erhalten am",
approval: "Gelesen und genehmigt, vorbehaltlich Anlage A.",
annex_title: "Anlage A",
annex: [
"Der Aufzug im Nordflügel wird jeden Monat gewartet, nicht jedes Quartal.",
"Diese Seite, das Unterschriftsfeld, das Datum und die Zeile darüber wurden",
"hinzugefügt, nachdem der Vertrag eingegangen war. Die ersten {bytes} Bytes",
"dieser Datei sind der Vertrag, genau so, wie er versandt wurde.",
],
};
/// 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); 7] = [
(Language::English, &ENGLISH),
(Language::French, &FRENCH),
(Language::German, &GERMAN),
(Language::Spanish, &SPANISH),
(Language::Italian, &ITALIAN),
(Language::Portuguese, &PORTUGUESE),
(Language::Dutch, &DUTCH),
];
/// A whole number with its thousands set apart by a space.
fn grouped(number: usize) -> String {
let digits = number.to_string();
let mut out = String::new();
for (rank, digit) in digits.chars().enumerate() {
if rank > 0 && (digits.len() - rank).is_multiple_of(3) {
out.push(' ');
}
out.push(digit);
}
out
}
/// Writes `text` at `x`, `y`, in `font` at `size`.
fn line(
content: &mut Content,
font: &FontHandle,
size: f64,
x: f64,
y: 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(())
}
/// A page holding a title and `lines` under it.
fn page_of_lines(font: &FontHandle, title: &str, lines: &[&str]) -> Result<Page, hqf_pdf::Error> {
let mut content = Content::new();
line(&mut content, font, TITLE_SIZE, MARGIN, 770.0, title)?;
let mut y = 730.0;
for text in lines {
line(&mut content, font, BODY_SIZE, MARGIN, y, text)?;
y -= LEADING;
}
let mut page = Page::a4();
page.content = content.into_bytes();
Ok(page)
}
/// The agreement as it is sent: two pages, created by somebody else.
fn agreement(words: &Words, font: &Font) -> Result<Vec<u8>, Box<dyn std::error::Error>> {
let mut doc = Document::new();
doc.set_license(licence::licensed());
let font = doc.add_font(font.clone());
doc.add_page(page_of_lines(&font, words.title, &words.first)?)?;
doc.add_page(page_of_lines(&font, words.title, &words.second)?)?;
Ok(doc.to_bytes()?)
}
/// What is added: a page drawn over the agreement's last page, holding the
/// signature area, the date and the line of one's own, and the annex added
/// after it.
fn additions(
words: &Words,
font: &Font,
received: usize,
) -> Result<Document, Box<dyn std::error::Error>> {
let mut doc = Document::new();
doc.set_license(licence::licensed());
let font = doc.add_font(font.clone());
let [x, y, width, height] = AREA;
let mut over = Content::new();
over.save_state();
over.set_fill(Rgb::new(0.93, 0.96, 1.0))?;
over.rect(x, y, width, height)?;
over.fill();
over.set_fill(FRAME)?;
line(
&mut over,
&font,
SMALL_SIZE,
x + 8.0,
y + height - 14.0,
words.signature,
)?;
over.restore_state();
let date = format!("{} {DATE}", words.received_on);
line(&mut over, &font, SMALL_SIZE, x, y - 16.0, &date)?;
line(&mut over, &font, BODY_SIZE, MARGIN, 240.0, words.approval)?;
let mut page = Page::a4();
page.content = over.into_bytes();
doc.add_page(page)?;
let count = grouped(received);
let annex: Vec<String> = words
.annex
.iter()
.map(|text| text.replace("{bytes}", &count))
.collect();
let annex: Vec<&str> = annex.iter().map(String::as_str).collect();
doc.add_page(page_of_lines(&font, words.annex_title, &annex)?)?;
Ok(doc)
}
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("added_to")));
let font = Font::parse(fs::read(default_font())?)?;
let received = agreement(words, &font)?;
let mut update = IncrementalUpdate::new(received.clone())?;
update.set_license(licence::licensed());
// The first page of the additions is drawn over the agreement's second
// page; the annex, which no entry names, comes after it.
let added = update.to_added_bytes(&additions(words, &font, received.len())?, &[1])?;
// The place to sign goes on the page drawn over, the second of the file.
let [x, y, width, height] = AREA;
let field =
SignatureField::new(FIELD, x, y, width, height).border(FieldBorder::solid(FRAME, 1.0));
let mut update = IncrementalUpdate::new(added)?;
update.set_license(licence::licensed());
let bytes = update.to_bytes_with_signature_field(1, &field)?;
if let Some(parent) = Path::new(&out).parent() {
fs::create_dir_all(parent)?;
}
fs::write(&out, &bytes)?;
println!(
"wrote {out}: {} bytes, the first {} of them the agreement as it arrived",
bytes.len(),
received.len()
);
Ok(())
}
#[cfg(test)]
mod tests {
use super::{
AREA, BODY_SIZE, Font, MARGIN, SMALL_SIZE, TITLE_SIZE, WORDS, default_font, grouped,
language,
};
use hqf_pdf::Document;
/// The width of an A4 page, in points.
const PAGE_WIDTH: f64 = 595.276;
/// The lines two languages are allowed to write the same way: the word
/// written in the corner of the signature area is one word in both.
const SPARED: [&str; 3] = [
"signature: \"Signature\"",
// Written the same way in Italian.
r#"signature: "Firma""#,
// Written the same way in Portuguese.
r#"annex_title: "Anexo A""#,
];
#[test]
fn every_language_creates_the_documents_in_its_own_words() {
let untranslated = language::untranslated_lines(&WORDS, &SPARED);
assert!(
untranslated.is_empty(),
"the documents say these in more than one language: {untranslated:?}"
);
}
#[test]
fn a_number_sets_its_thousands_apart_by_a_space() {
assert_eq!(grouped(7), "7");
assert_eq!(grouped(999), "999");
assert_eq!(grouped(1000), "1 000");
assert_eq!(grouped(46_512), "46 512");
assert_eq!(grouped(1_234_567), "1 234 567");
}
/// Every line fits the room it is drawn in: the page less its two margins
/// for a title and a line of the agreement or the annex, the area for what
/// is written inside and under it.
#[test]
fn every_line_fits_the_room_it_is_drawn_in() {
let mut doc = Document::new();
let font = doc.add_font(
Font::parse(std::fs::read(default_font()).expect("the font reads"))
.expect("the font parses"),
);
let page = MARGIN.mul_add(-2.0, PAGE_WIDTH);
let [_, _, area, _] = AREA;
for (language, words) in WORDS {
let code = language.code();
let annex = words.annex.map(|text| text.replace("{bytes}", "999 999"));
let body = words
.first
.iter()
.chain(&words.second)
.copied()
.chain(annex.iter().map(String::as_str))
.chain([words.approval]);
for text in body {
let width = font.measure(text, BODY_SIZE);
assert!(
width <= page,
"in {code}, {text:?} is {width} wide in {page}"
);
}
for text in [words.title, words.annex_title] {
let width = font.measure(text, TITLE_SIZE);
assert!(
width <= page,
"in {code}, {text:?} is {width} wide in {page}"
);
}
let date = format!("{} {}", words.received_on, super::DATE);
for text in [words.signature, date.as_str()] {
let width = font.measure(text, SMALL_SIZE);
assert!(
width + 8.0 <= area,
"in {code}, {text:?} is {width} wide in {area}"
);
}
}
}
}
|