write_pdfa1a.rs

The Rust file of the “A Tagged PDF/A-1a Deed of Deposit” example. The first part of the archival standard at its highest level: the page also says which line is the heading, which blocks are paragraphs and what is mere decoration.

Rust 456 lines

What This Example Is For

A deed kept for thirty years will be read by people nobody has met yet, some of them through software that reads the page aloud. A page that only looks right gives that software nothing to go on: it reads the ink in the order it was laid down, page number and decoration included.

What This Example Shows

  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
//! Writes a deed of deposit claiming the first part of the archival standard,
//! at level A.
//!
//! Level A is the one that asks the page to say what it is made of: which run
//! of words is the heading, which are paragraphs, and which marks are no part
//! of the reading at all. A screen reader follows that tree; a page that only
//! looks right has nothing for it to follow.
//!
//! The first part adds what the later editions dropped: the sheet is built on
//! PDF 1.4, nothing on it is painted so that what lies behind it shows through,
//! no part of it can be switched off, and no file travels inside it.
//!
//! The rule and the page number at the foot are marked as what they are —
//! furniture, not text — so nothing reading the document aloud says them.
//!
//! Whether the result really is PDF/A-1A is not for us to say. `veraPDF` says,
//! and `scripts/check_pdfa.sh` asks it.
//!
//! The words of the page and of its metadata are held in `Words`, once per
//! language, and `HQF_PDF_LANG` picks which set the record is written in. The
//! tree names the same language, so a screen reader reads the page in it.
//!
//! Usage: `cargo run --example write_pdfa1a -- tmp/pdfa1a.pdf [font.ttf]`
//!        `HQF_PDF_LANG=fr cargo run --example write_pdfa1a -- tmp/pdfa1a_fr.pdf`

use std::env;
use std::fs;
use std::path::{Path, PathBuf};

use hqf_pdf::content::{Artifact, Content};
use hqf_pdf::cos::Name;
use hqf_pdf::layout::FirstBaseline;
use hqf_pdf::metadata::xmp::{Metadata, PdfA};
use hqf_pdf::structure::{Marks, StructElement, StructureTree};
use hqf_pdf::{Document, Error, Font, FontHandle, Page, TextFlow, Version};

#[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 when none is given on the command line.
fn default_font() -> PathBuf {
    Path::new(env!("CARGO_MANIFEST_DIR"))
        .join("tests")
        .join("fonts")
        .join("DejaVuSans.ttf")
}

/// When the record was drawn up. The library never reads the clock, so the
/// caller says when.
const ISSUED: &str = "2026-01-19T11:30:00+01:00";

/// Who the file says wrote it, and what produced it, in every language alike.
const AUTHOR: &str = "HQF Development";
const PRODUCER: &str = "hqf-pdf-server";

/// The words the deed is written in, one set per language.
#[derive(Debug)]
struct Words {
    /// What the file says it is, in its metadata.
    title: &'static str,
    /// What the file says it is about, in its metadata.
    subject: &'static str,
    /// The language the tree says the page is written in, which a screen reader
    /// reads it in.
    lang: &'static str,
    /// The heading, tagged `H1`.
    heading: &'static str,
    /// The two paragraphs under the heading, each tagged `P`.
    paragraphs: [&'static str; 2],
    /// The page number at the foot, marked as furniture.
    foot: &'static str,
}

impl Words {
    /// The words the deed is written in, in `language`.
    fn of(language: Language) -> &'static Self {
        language::pick(&WORDS, language)
    }
}

/// The deed in English.
const ENGLISH: Words = Words {
    title: "Deed of deposit, January 2026",
    subject: "A record that says what it is made of, under the first edition",
    lang: "en-GB",
    heading: "Deed of deposit",
    paragraphs: [
        "This deed claims the first part of the archival standard at its highest level. On top \
            of the promise that the page keeps its looks, and on top of every mark of ink saying \
            which letter it stands for, the file says what it is made of: this line is a \
            heading, that block is a paragraph, and the rule across the foot is furniture that \
            says nothing at all.\nSomething reading the deed aloud follows the order the tree \
            gives rather than the order the ink was laid down in, and reading software on a \
            telephone has something to reflow.",
        "The papers listed above were deposited on 19 January 2026 and are held for thirty \
            years. The first part of the standard is what the depositary asks for by name, so \
            the sheet carries nothing the part closes: no see-through paint, no part of the page \
            that can be switched off, and no file inside it.",
    ],
    foot: "Page 1 of 1",
};

/// The deed in French.
const FRENCH: Words = Words {
    title: "Acte de dépôt, janvier 2026",
    subject: "Un acte qui dit de quoi il est fait, selon la première édition de la norme",
    lang: "fr-FR",
    heading: "Acte de dépôt",
    paragraphs: [
        "Cet acte déclare suivre la première partie de la norme d’archivage, à son \
niveau le plus exigeant. En plus de la promesse que la page garde son \
apparence, et de chaque trace d’encre qui dit quelle lettre elle représente, \
le fichier dit de quoi il est fait : cette ligne est un titre, ce bloc est un \
paragraphe, et le filet en pied de page est un ornement qui ne dit rien.\nUn \
logiciel qui lit l’acte à voix haute suit l’ordre que donne l’arbre de \
structure, et non celui dans lequel l’encre a été posée ; sur un téléphone, \
une application de lecture a de quoi recomposer le texte.",
        "Les pièces décrites ci-dessus ont été déposées le 19 janvier 2026 et sont \
conservées trente ans. Le dépositaire exige nommément la première partie de la \
norme ; la feuille ne contient donc rien de ce qu’elle exclut : aucune \
transparence, aucun élément que l’on puisse masquer, aucun fichier joint.",
    ],
    foot: "Page 1 sur 1",
};

/// The deed in German.
const GERMAN: Words = Words {
    title: "Hinterlegungsurkunde, Januar 2026",
    subject: "Ein Dokument nach dem ersten Teil der Norm, das angibt, woraus es besteht",
    lang: "de-DE",
    heading: "Hinterlegungsurkunde",
    paragraphs: [
        "Diese Urkunde erklärt Konformität mit dem ersten Teil der Archivnorm, auf \
dessen höchster Stufe. Zugesagt ist, dass die Seite ihr Aussehen behält und \
jede Druckspur angibt, für welchen Buchstaben sie steht; darüber hinaus gibt \
die Datei an, woraus sie besteht: Diese Zeile ist eine Überschrift, jener \
Block ein Absatz, und die Linie am Fuß ist Zierrat ohne jede Aussage.\nEin \
Vorleseprogramm folgt der Ordnung des Strukturbaums statt der Reihenfolge, in \
der die Tinte aufgetragen wurde, und eine Lese-App auf dem Smartphone kann den \
Text neu umbrechen.",
        "Die oben genannten Unterlagen wurden am 19. Januar 2026 hinterlegt und werden dreißig \
            Jahre lang aufbewahrt. Der Verwahrer verlangt ausdrücklich den ersten Teil der Norm, \
            daher enthält das Blatt nichts, was dieser Teil ausschließt: keine durchscheinende \
            Farbe, keinen ausblendbaren Teil der Seite und keine eingebettete Datei.",
    ],
    foot: "Seite 1 von 1",
};

/// The deed in Spanish.
const SPANISH: Words = Words {
    title: "Acta de depósito, enero de 2026",
    subject: "Un documento que dice de qué está hecho, según la primera edición",
    lang: "es-ES",
    heading: "Acta de depósito",
    paragraphs: [
        "Esta acta declara cumplir la primera parte de la norma de archivo en su nivel \
más alto. Además de prometer que la página conserva su aspecto y que cada \
trazo de tinta dice qué letra representa, el fichero dice de qué está hecho: \
esta línea es un título, ese bloque es un párrafo y el filete del pie es un \
adorno que no dice nada.\nUn programa que lea el acta en voz alta sigue el \
orden que da el árbol, no el orden en que se puso la tinta, y una aplicación \
de lectura en el móvil tiene con qué recomponer el texto.",
        "Los documentos citados arriba se depositaron el 19 de enero de 2026 y se \
conservan durante treinta años. El depositario exige expresamente la primera \
parte de la norma, así que la hoja no contiene nada de lo que esa parte \
excluye: ni transparencias, ni partes de la página que puedan ocultarse, ni \
ficheros adjuntos.",
    ],
    foot: "Página 1 de 1",
};

/// The deed in Italian.
const ITALIAN: Words = Words {
    title: "Atto di deposito, gennaio 2026",
    subject: "Un documento che dice di che cosa è fatto, secondo la prima edizione",
    lang: "it-IT",
    heading: "Atto di deposito",
    paragraphs: [
        "Quest’atto dichiara di rispettare la prima parte della norma di archiviazione \
al suo livello più alto. Oltre alla promessa che la pagina conservi il suo \
aspetto e che ogni segno d’inchiostro dica quale lettera rappresenta, il file \
dice di che cosa è fatto: questa riga è un titolo, quel blocco è un paragrafo \
e il filetto a piè di pagina è un ornamento che non dice nulla.\nUna sintesi \
vocale legge l’atto seguendo l’ordine dato dall’albero della struttura, non \
quello in cui è stato steso l’inchiostro, e un’app di lettura sul telefono può \
reimpaginare il testo.",
        "I documenti indicati sopra sono stati depositati il 19 gennaio 2026 e sono conservati \
            per trent’anni. Il depositario chiede espressamente la prima parte della norma, \
            perciò il foglio non contiene nulla di ciò che quella parte esclude: niente colore \
            trasparente, nessuna parte della pagina che si possa nascondere, nessun file \
            allegato.",
    ],
    foot: "Pagina 1 di 1",
};

/// The deed in Portuguese.
const PORTUGUESE: Words = Words {
    title: "Auto de depósito, janeiro de 2026",
    subject: "Um documento que diz de que é feito, segundo a primeira edição",
    lang: "pt-PT",
    heading: "Auto de depósito",
    paragraphs: [
        "Este auto declara cumprir a primeira parte da norma de arquivo, no seu nível \
mais alto. Além da promessa de que a página mantém o seu aspeto, e de que cada \
traço de tinta diz que letra representa, o ficheiro diz de que é feito: esta \
linha é um título, aquele bloco é um parágrafo e o filete do rodapé é um \
ornamento que nada diz.\nUm programa que lê o auto em voz alta segue a ordem \
dada pela árvore, e não a ordem em que a tinta foi posta, e uma aplicação de \
leitura no telemóvel tem com que recompor o texto.",
        "Os documentos acima referidos foram depositados a 19 de janeiro de 2026 e ficam \
            guardados durante trinta anos. O depositário exige expressamente a primeira parte da \
            norma, por isso a folha não contém nada do que essa parte exclui: nem tinta \
            transparente, nem partes da página que se possam ocultar, nem ficheiros anexos.",
    ],
    foot: "Página 1 de 1",
};

/// The deed in Dutch.
const DUTCH: Words = Words {
    title: "Akte van depot, januari 2026",
    subject: "Een document dat zegt waaruit het bestaat, volgens de eerste editie",
    lang: "nl-NL",
    heading: "Akte van depot",
    paragraphs: [
        "Deze akte verklaart te voldoen aan het eerste deel van de archiefnorm, op het \
hoogste niveau. Naast de belofte dat de pagina haar uiterlijk behoudt en dat \
elk inktspoor aangeeft voor welke letter het staat, vertelt het bestand \
waaruit het bestaat: deze regel is een kop, dat blok is een alinea en de lijn \
onderaan is opsmuk die niets zegt.\nEen voorleesprogramma houdt de volgorde \
van de boom aan en niet die waarin de inkt is neergezet, en een leesapp op een \
telefoon kan de tekst herschikken.",
        "De hierboven genoemde stukken zijn op 19 januari 2026 gedeponeerd en worden \
dertig jaar bewaard. De bewaarnemer vraagt uitdrukkelijk om het eerste deel \
van de norm; daarom bevat het blad niets wat dat deel uitsluit: geen \
transparantie, geen deel van de pagina dat kan worden uitgeschakeld en geen \
bijgevoegd bestand.",
    ],
    foot: "Pagina 1 van 1",
};

/// 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),
];

/// Where each block of what the page says goes, in the order it is read: the
/// tag it stands under, the size it is set at, the box it goes in, and the
/// leading where it states one. The blocks say the heading and the two
/// paragraphs of `Words`, in that order.
type Placed = (&'static str, f64, f64, f64, f64, Option<f64>);

/// The heading, then the two paragraphs.
const READ: [Placed; 3] = [
    ("H1", 20.0, 72.0, 782.0, 451.0, None),
    ("P", 11.0, 72.0, 730.0, 451.0, Some(16.0)),
    ("P", 11.0, 72.0, 580.0, 451.0, Some(16.0)),
];

/// The rule at the foot of the page, and the box of the line under it. Neither
/// is part of the reading, so both are marked as furniture.
const RULE: (f64, f64, f64, f64) = (72.0, 70.0, 451.0, 1.0);
const FOOT: (f64, f64, f64, f64) = (9.0, 72.0, 68.0, 451.0);

fn main() -> std::process::ExitCode {
    failure::reported(run())
}

/// Writes the file, and says where it went.
fn run() -> Result<(), Box<dyn std::error::Error>> {
    let language = Language::from_environment()?;
    let words = Words::of(language);

    let mut args = env::args().skip(1);
    // A named file is written as named; the default one carries the language,
    // so the languages do not overwrite each other in `tmp/`.
    let path = args
        .next()
        .unwrap_or_else(|| language.file_name(&out::default_path("pdfa1a")));
    let font_path = args.next().map_or_else(default_font, PathBuf::from);

    let mut doc = Document::new();
    doc.set_license(licence::licensed());
    doc.set_version(Version::V1_4);
    doc.set_conformance(PdfA::A1A);

    let mut facts = Metadata::default();
    facts.title = Some(words.title.to_owned());
    facts.author = Some(AUTHOR.to_owned());
    facts.subject = Some(words.subject.to_owned());
    facts.producer = Some(PRODUCER.to_owned());
    facts.created = Some(ISSUED.to_owned());
    doc.set_metadata(facts);

    let font = doc.add_font(Font::parse(fs::read(&font_path)?)?);
    let (page, sect) = sheet(&font, words)?;
    doc.add_page(page)?;
    doc.set_structure(
        StructureTree::new()
            .lang(words.lang)
            .child(StructElement::new("Document").child(sect)),
    );

    let bytes = doc.to_bytes()?;
    if let Some(parent) = Path::new(&path).parent() {
        fs::create_dir_all(parent)?;
    }
    fs::write(&path, &bytes)?;
    println!("wrote {path}: {} bytes", bytes.len());
    Ok(())
}

/// The one page, and the branch of the tree that says what is on it.
fn sheet(font: &FontHandle, words: &Words) -> Result<(Page, StructElement), Error> {
    let mut c = Content::new();
    let mut marks = Marks::new();
    let mut sect = StructElement::new("Sect");

    let said = [words.heading, words.paragraphs[0], words.paragraphs[1]];
    for ((tag, size, left, top, width, leading), text) in READ.into_iter().zip(said) {
        let mut flow = TextFlow::new(font, size).first_baseline(FirstBaseline::Points(0.0));
        if let Some(leading) = leading {
            flow = flow.leading(leading);
        }
        let lines = flow.break_lines(text, width);
        c.begin_text();
        let runs = flow.draw_tagged(
            &mut c,
            &lines,
            left,
            top,
            width,
            &Name::new(tag),
            &mut marks,
        )?;
        c.end_text();

        let mut element = StructElement::new(tag);
        for run in runs {
            element = element.content(0, run);
        }
        sect = sect.child(element);
    }

    foot(&mut c, font, words.foot)?;

    let mut page = Page::a4();
    page.content = c.into_bytes();
    Ok((page, sect))
}

/// The rule and the page number at the foot, both marked as furniture so that
/// nothing reading the document aloud says them.
fn foot(c: &mut Content, font: &FontHandle, text: &str) -> Result<(), Error> {
    let (left, low, width, height) = RULE;
    c.begin_artifact(Artifact::Footer);
    c.save_state();
    c.set_fill_rgb(0.0, 0.0, 0.0)?;
    c.rect(left, low, width, height)?;
    c.fill();
    c.restore_state();
    c.end_marked();

    let (size, left, top, width) = FOOT;
    let flow = TextFlow::new(font, size).first_baseline(FirstBaseline::Points(0.0));
    let lines = flow.break_lines(text, width);
    c.begin_artifact(Artifact::Footer);
    c.begin_text();
    flow.draw(c, &lines, left, top, width)?;
    c.end_text();
    c.end_marked();
    Ok(())
}

#[cfg(test)]
mod tests {
    use hqf_pdf::{Document, Font, TextFlow};

    use super::{FOOT, READ, RULE, WORDS, default_font, language};

    /// The lines two languages are allowed to write the same way: Spanish and
    /// Portuguese number a page with the same words.
    const SPARED: [&str; 1] = [r#"foot: "Página 1 de 1""#];

    #[test]
    fn every_language_writes_the_record_in_its_own_words() {
        let untranslated = language::untranslated_lines(&WORDS, &SPARED);

        assert!(
            untranslated.is_empty(),
            "the record says these in more than one language: {untranslated:?}"
        );
    }

    /// The heading and the page number each hold one line, and each paragraph
    /// ends a line above what stands under it: the second paragraph, then the
    /// rule at the foot. A paragraph one line too long is drawn over the next
    /// block, and nothing else on the page says so.
    #[test]
    fn every_language_fits_the_boxes_the_page_sets_it_in() {
        let mut doc = Document::new();
        let font = doc.add_font(
            Font::parse(std::fs::read(default_font()).expect("the committed font is there"))
                .expect("the committed font parses"),
        );
        let (_, heading_size, _, _, heading_width, _) = READ[0];
        let (foot_size, _, _, foot_width) = FOOT;
        let floors = [READ[2].3, RULE.1];

        for (named, words) in WORDS {
            let code = named.code();
            for (size, width, text) in [
                (heading_size, heading_width, words.heading),
                (foot_size, foot_width, words.foot),
            ] {
                let lines = TextFlow::new(&font, size).break_lines(text, width).len();
                assert_eq!(
                    lines, 1,
                    "the {code} record breaks {text:?} onto {lines} lines"
                );
            }

            for ((placed, text), floor) in READ[1..].iter().zip(words.paragraphs).zip(floors) {
                let (_, size, _, top, width, leading) = *placed;
                let leading = leading.expect("a paragraph states its leading");
                let lines = TextFlow::new(&font, size)
                    .leading(leading)
                    .break_lines(text, width)
                    .len();
                let count = f64::from(u32::try_from(lines).expect("a paragraph is a few lines"));
                assert!(
                    count * leading <= top - floor,
                    "the {code} record sets a paragraph on {lines} lines from {top}, \
                     and it has down to {floor}"
                );
            }
        }
    }
}