pdfluent 1.2.0

Pure-Rust PDF SDK with PDF/A, digital signatures, WASM, and experimental (feature-gated) XFA support.
Documentation
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
// Copyright (c) 2026 Innovation Trigger B.V.
//
// PDFluent is available under two licences, at your option: the GNU AGPLv3, or
// the PDFluent Commercial Licence. See the LICENSE file in this repository --
// that file travels with the copy you received, which a URL does not.

//! De codevoorbeelden van pdfluent.com, als code die bouwt.
//!
//! De documentatiepagina droeg vier Rust-voorbeelden die **niet compileerden**.
//! Ze verwezen naar een `pdfluent::Sdk` die niet bestaat, naar
//! `Sdk::init_with_license("license.json")`, naar `doc.import_xfa_data(...)`
//! dat nergens voorkomt, en naar `PdfaLevel` waar het type `PdfAProfile` heet.
//! Wie het eerste voorbeeld kopieerde kreeg vijf compilatiefouten op vier
//! regels code -- als eerste indruk van de SDK.
//!
//! Niemand had ze ooit gebouwd. Was dat gebeurd, dan was het er meteen
//! uitgekomen. Zie #164.
//!
//! Dit bestand is de bron. Elk blok tussen `// site:<naam>` en `// site:end`
//! is een voorbeeld dat op de site hoort, en dit bestand compileert in CI --
//! dus een voorbeeld dat niet meer klopt, breekt de build in plaats van de
//! eerste indruk van een bezoeker.
//!
//! De voorbeelden gebruiken geen licentiesleutel. Dat is geen verzuim maar het
//! besluit van 24-08-2026: de SDK staat open, alleen ondertekenen weigert
//! zonder licentie.

use std::path::Path;

// Elk blok draagt zijn eigen `use`-regels, binnen de markering: een fragment
// zonder imports is niet te plakken, en dat was juist de klacht (#164, #247).

/// Tekst uit een document halen.
fn open_and_extract(path: &Path) -> pdfluent::Result<()> {
    // site:open-and-extract
    use pdfluent::PdfDocument;

    let doc = PdfDocument::open("invoice.pdf")?;

    // The full text, in reading order.
    let text = doc.extract_text()?;
    println!("{text}");

    // Or per page.
    for n in 0..doc.page_count() {
        println!("--- page {} ---", n + 1);
        println!("{}", doc.page(n)?.text()?);
    }
    // site:end
    let _ = (path, doc);
    Ok(())
}

/// Een XFA-formulier invullen en afvlakken.
fn fill_xfa(path: &Path) -> pdfluent::Result<()> {
    // site:fill-xfa
    use pdfluent::PdfDocument;

    let mut doc = PdfDocument::open("tax_return.pdf")?;

    // The form model: one row per logical field.
    let model = doc.xfa_form_model()?;
    for field in &model.fields {
        println!("{} = {:?}", field.name, field.value);
    }

    // Fill by name.
    doc.set_xfa_field_value("form1.name", pdfluent::xfa::XfaFieldValue::Text("Smith"))?;
    // site:end
    let _ = path;
    Ok(())
}

/// Toetsen of een document aan PDF/A voldoet.
fn validate_pdfa(path: &Path) -> pdfluent::Result<()> {
    // site:validate-pdfa
    use pdfluent::{PdfAProfile, PdfDocument};

    let doc = PdfDocument::open("legacy.pdf")?;

    let report = doc.validate_pdfa(PdfAProfile::A2b)?;
    if report.is_compliant() {
        println!("conforms to PDF/A-2b");
    } else {
        for violation in &report.violations {
            println!("{violation:?}");
        }
    }
    // site:end
    let _ = path;
    Ok(())
}

fn main() {
    // Dit bestand bestaat om te compileren, niet om te draaien: de genoemde
    // bestanden staan er niet. Wat het bewijst is dat de voorbeelden op de
    // site kloppen met de API die er werkelijk is.
    let pad = Path::new("voorbeeld.pdf");
    if std::env::var("PDFLUENT_SITE_SNIPPETS_RUN").is_ok() {
        let _ = open_and_extract(pad);
        let _ = fill_xfa(pad);
        let _ = validate_pdfa(pad);
        let _ = sign_pkcs12(pad);
        let _ = verify_signatures(pad);
        let _ = after_chrome(pad);
        smoke_page_count();
        let _ = open_and_count(pad);
        quickstart_extract_text();
        let _ = structured_extraction(pad);
        let _ = text_and_merge(pad);
        let _ = xfa_fill_and_flatten(pad);
        let _ = pdfa_validate_and_convert(pad);
        let _ = sign_and_verify(pad);
        let _ = render_pages_to_png(pad);
    } else {
        println!(
            "site_snippets: gebouwd, niet gedraaid (zet PDFLUENT_SITE_SNIPPETS_RUN om te draaien)"
        );
    }
}

/// Een document ondertekenen met een PKCS#12-bestand.
///
/// De site beschreef `PdfSigner::from_pkcs12("cert.p12", "password")` en deed
/// vervolgens `let signed = doc.sign(...)` gevolgd door `signed.save(...)`.
/// Drie dingen kloppen daar niet: de constructor heet `Pkcs12Signer::from_pfx_file`,
/// `sign` wijzigt het document en levert niets op, en het profiel dat
/// `SignOptions::new()` kiest is `LongTerm` -- dat weigert deze build, omdat er
/// geen route is om een document security store te schrijven (#176).
///
/// Een voorbeeld dat het standaardprofiel laat staan, faalt dus bij de eerste
/// uitvoering. Het kiest hier expliciet `BasicSignature`.
fn sign_pkcs12(path: &Path) -> pdfluent::Result<()> {
    // site:sign-pkcs12
    use pdfluent::signer::{PadesProfile, Pkcs12Signer, SignOptions};
    use pdfluent::PdfDocument;

    let mut doc = PdfDocument::open("contract.pdf")?;
    let signer = Pkcs12Signer::from_pfx_file("cert.p12", "password")?;

    // SignOptions::new() is PAdES B-B. For B-T or B-LT, use
    // doc.sign_timestamped(..), which signs and then hands your timestamp
    // authority the signature to stamp.
    let opts = SignOptions::new()
        .profile(PadesProfile::BasicSignature)
        .reason("Approved");

    doc.sign(&signer, opts)?;
    doc.save("contract-signed.pdf")?;
    // site:end
    let _ = path;
    Ok(())
}

/// Handtekeningen verifiëren.
///
/// De site beschreef hier een `TrustStore` en `SignatureVerifier::with_trust_store()`.
/// Geen van beide bestaat. Erger dan de verkeerde naam is wat het beloofde: dat je
/// je eigen CA kon vastpinnen. Dat kan niet — de keten wordt gebouwd uit de
/// certificaten die in het document zitten, zonder vertrouwensanker.
fn verify_signatures(path: &Path) -> pdfluent::Result<()> {
    // site:verify-signatures
    use pdfluent::signer::SignatureStatus;
    use pdfluent::PdfDocument;

    let doc = PdfDocument::open("signed.pdf")?;
    let report = doc.verify_signatures()?;
    for validation in report.validations() {
        match &validation.status {
            SignatureStatus::Valid => println!("Signature is valid"),
            SignatureStatus::Invalid { reason } => println!("Signature is invalid: {reason}"),
            SignatureStatus::Unknown { reason } => {
                println!("Signature status is unknown: {reason}")
            }
            // SignatureStatus is #[non_exhaustive]: new outcomes can be added
            // without a breaking change, so a match on it needs a catch-all.
            _ => println!("Signature status not recognised by this build"),
        }
    }
    // site:end
    let _ = (path, doc);
    Ok(())
}

/// Wat PDFluent doet met een PDF die headless Chrome heeft gemaakt.
///
/// De site beschreef hier `pdfluent::HtmlToPdf` met een "browser bridge" —
/// een hele API die niet bestaat, voor iets dat we op 19-08-2026 hebben
/// besloten niet te bouwen. Wat wél klopt is de tweede helft: de conversie doe
/// je met Chrome, en alles daarna met PDFluent.
///
/// Dit blok bestaat omdat een taalmodel de vervangende code niet kon schrijven
/// zonder hem te verzinnen: het stelde `doc.compress().add_watermark("DRAFT")`
/// voor, en geen van die twee heeft die vorm. De compiler wist dat wel.
fn after_chrome(path: &Path) -> pdfluent::Result<()> {
    // site:after-chrome
    use pdfluent::{CompressOptions, PdfAProfile, PdfDocument, WatermarkOptions};

    // Chrome wrote the PDF; everything after that is PDFluent.
    let mut doc = PdfDocument::open("out.pdf")?;

    doc.add_watermark("DRAFT", WatermarkOptions::centered())?;
    let report = doc.compress(CompressOptions::default())?;
    println!("{} streams compressed", report.streams_compressed);

    // convert_to_pdfa returns a new document rather than changing this one.
    let archived = doc.convert_to_pdfa(PdfAProfile::A2b)?;
    archived.save("invoice-archived.pdf")?;
    // site:end
    let _ = path;
    Ok(())
}

/// De installatiepagina's "does it work" -- een heel programma, niet een fragment.
///
/// Dit blok en de twee hieronder stonden als sjabloonliteral in TSX
/// (`docsChannels.ts`, `DownloadPage.tsx`, `SdkPage.tsx`). Ze klopten toevallig,
/// maar niets hield dat vast: acht Rust-blokken op de site droegen hun eigen
/// tekst en geen enkele was ooit gebouwd (#247).
///
/// De geneste `fn main` is met opzet: wat de lezer op de installatiepagina ziet
/// is een compleet programma om te plakken, en dat is dus ook wat hier
/// compileert.
fn smoke_page_count() {
    // site:smoke-page-count
    use pdfluent::PdfDocument;

    fn main() -> pdfluent::Result<()> {
        let doc = PdfDocument::open("invoice.pdf")?;
        println!("{} pages", doc.page_count());
        Ok(())
    }
    // site:end
    let _ = main();
}

/// Hetzelfde, zonder programma-omhulsel: de downloadpagina toont het fragment.
fn open_and_count(path: &Path) -> pdfluent::Result<()> {
    // site:open-and-count
    use pdfluent::PdfDocument;

    let doc = PdfDocument::open("invoice.pdf")?;
    println!("{} pages", doc.page_count());
    // site:end
    let _ = (path, doc);
    Ok(())
}

/// De SDK-pagina: openen en de tekst eruit, als heel programma.
fn quickstart_extract_text() {
    // site:quickstart-extract-text
    use pdfluent::PdfDocument;

    fn main() -> pdfluent::Result<()> {
        let doc = PdfDocument::open("invoice.pdf")?;
        let text = doc.extract_text()?;
        println!("{text}");
        Ok(())
    }
    // site:end
    let _ = main();
}

/// De RAG-pagina: de structuur eruit, als markdown en als blokken.
fn structured_extraction(path: &Path) -> pdfluent::Result<()> {
    // site:structured-extraction
    use pdfluent::prelude::*;

    let doc = PdfDocument::open("report.pdf")?;

    // The whole document as markdown: headings, lists and tables, in reading
    // order, with a comment where the page changes.
    let markdown = doc.extract_markdown(&ExtractOptions::default())?;
    std::fs::write("report.md", &markdown).ok();

    // Or the blocks themselves, to chunk and cite with.
    for block in doc.extract_blocks(&ExtractOptions::default())? {
        if let BlockKind::Heading { level } = block.kind {
            println!("h{level} p{} {}", block.page + 1, block.text);
        }
    }
    // site:end
    let _ = (path, doc);
    Ok(())
}

/// Tekst met plaatsaanduiding, en documenten samenvoegen.
fn text_and_merge(path: &Path) -> pdfluent::Result<()> {
    // site:text-and-merge
    use pdfluent::{PdfDocument, PdfMerger};

    let doc = PdfDocument::open("report.pdf")?;

    // Text with its bounding box, block by block.
    for block in doc.text_with_layout()? {
        let [x, y, _, _] = block.bbox;
        println!("p{} [{x:.0},{y:.0}] {}", block.page, block.text);
    }

    // Merge multiple PDFs.
    let merged = PdfMerger::new()
        .add(PdfDocument::open("part1.pdf")?)
        .add(PdfDocument::open("part2.pdf")?)
        .build()?;
    merged.save("combined.pdf")?;
    // site:end
    let _ = path;
    Ok(())
}

/// Een XFA-formulier lezen en invullen.
///
/// De site schreef hier `doc.flatten_xfa()?`. Die methode bestaat niet, en een
/// naamcontrole ziet dat niet -- `flatten` bestaat immers. De lezer kreeg E0599
/// op de laatste stap van de belangrijkste demonstratie op die pagina.
///
/// Wat er niet voor in de plaats komt is `flatten_forms`. Dat compileert, en
/// dat is precies de val: het is AcroForm-only, en in 1.0 geeft het altijd
/// `Error::MissingDependency` terug (#1223). Een voorbeeld dat bouwt en bij
/// elke uitvoering faalt is erger dan een voorbeeld dat niet bouwt -- dezelfde
/// vorm als `SignOptions::new()` hierboven. Het blok stopt dus na het invullen,
/// en toont wat er wél gebeurt: de waarde gaat de datasets-packet in.
fn xfa_fill_and_flatten(path: &Path) -> pdfluent::Result<()> {
    // site:xfa-fill-and-flatten
    use pdfluent::{PdfDocument, XfaFieldValue};

    let mut doc = PdfDocument::open("tax_return.pdf")?;

    // The form model: every logical field, with its value and its type.
    let model = doc.xfa_form_model()?;
    for f in &model.fields {
        println!("{} = {:?} ({:?})", f.name, f.value, f.field_type);
    }

    // Fill by name. The value is written back into the datasets packet, so
    // saving keeps it.
    let value = XfaFieldValue::Text("Alice Smith");
    let outcome = doc.set_xfa_field_value("form1.applicant.name", value)?;
    println!("persisted: {}", outcome.persisted_to_datasets);
    doc.save("filed_return.pdf")?;
    // site:end
    let _ = path;
    Ok(())
}

/// PDF/A toetsen en daarna omzetten.
fn pdfa_validate_and_convert(path: &Path) -> pdfluent::Result<()> {
    // site:pdfa-validate-and-convert
    use pdfluent::{PdfAProfile, PdfDocument};

    let doc = PdfDocument::open("legacy.pdf")?;

    // Validate, then read the findings.
    let report = doc.validate_pdfa(PdfAProfile::A2b)?;
    if !report.is_compliant() {
        for v in &report.violations {
            eprintln!("[{}] {} ({:?})", v.rule, v.message, v.severity);
        }
    }

    // convert_to_pdfa returns a new document rather than changing this one.
    let archived = doc.convert_to_pdfa(PdfAProfile::A2b)?;
    archived.save("archived.pdf")?;
    // site:end
    let _ = path;
    Ok(())
}

/// Ondertekenen en daarna verifiëren, in één blok.
///
/// De site liet `SignOptions::new()` staan. Dat vraagt PAdES B-LT, en deze build
/// weigert dat: er is geen route om een document security store te schrijven
/// (#176). Zo'n voorbeeld compileert en faalt pas bij de eerste uitvoering --
/// het slechtste van beide. Het profiel staat er nu expliciet in.
fn sign_and_verify(path: &Path) -> pdfluent::Result<()> {
    // site:sign-and-verify
    use pdfluent::{PadesProfile, PdfDocument, Pkcs12Signer, SignOptions};

    let mut doc = PdfDocument::open("contract.pdf")?;

    let signer = Pkcs12Signer::from_pfx_file("cert.p12", "password")?;
    doc.sign(
        &signer,
        SignOptions::new()
            // PAdES B-B. B-T and B-LT need a timestamp over the signature, so
            // they go through doc.sign_timestamped; B-LTA is not implemented.
            .profile(PadesProfile::BasicSignature)
            .reason("Approved by legal")
            .location("Amsterdam, NL"),
    )?;
    doc.save("signed_contract.pdf")?;

    // Verify the signatures already on a document.
    let report = doc.verify_signatures()?;
    for v in report.validations() {
        println!("{}: {:?}", v.info.signer_name, v.status);
    }
    // site:end
    let _ = path;
    Ok(())
}

/// Elke bladzijde als PNG.
///
/// De benchmarkpagina toonde `use pdfluent::{Document, RenderOptions};` -- twee
/// namen die geen van beide bestaan, op de pagina die rendersnelheid claimt.
/// Rasteren gaat via `to_images`, dat de bestanden zelf wegschrijft en de paden
/// teruggeeft. De vorm klopte ook niet: `ToImagesOptions` is `#[non_exhaustive]`
/// en neemt geen structliteraal aan, dus zelfs met de juiste naam was
/// `RenderOptions { dpi: 150, ..Default::default() }` niet te bouwen (E0639).
fn render_pages_to_png(path: &Path) -> pdfluent::Result<()> {
    // site:render-pages-to-png
    use pdfluent::{ImageFormat, PdfDocument, ToImagesOptions};

    let doc = PdfDocument::open("document.pdf")?;

    // The page number goes in as `{page}`. Without that marker `to_images`
    // appends `_N` before the extension instead, so a pattern is never wrong
    // in a way the compiler can see.
    //
    // ToImagesOptions is #[non_exhaustive]: it takes the builder, not a struct
    // literal, so that a new option is not a breaking change.
    let report = doc.to_images(
        "page_{page}.png",
        ToImagesOptions::new()
            .with_dpi(150)
            .with_format(ImageFormat::Png),
    )?;
    println!("{} pages rendered", report.paths.len());
    // site:end
    let _ = path;
    Ok(())
}