lightweight-pdfv0.3.0

Showcase

Each example shows real output produced by lightweight-pdf itself, from files in the project repository. Input on the left, output on the right.

Invoice

examples/demo_invoice.rsDetail: Invoice
//! Example: an invoice — sender/recipient block (DIN-5008-style window-
//! envelope layout), an "Inquiries to" contact box paired with invoice
//! metadata, a position table with an indented detail line under each
//! item, a right-aligned Subtotal/VAT/Total summary, and a four-column
//! footer (company / contact / owner+VAT-ID / bank details). The
//! letterhead uses a placeholder logo image (`Image` element, baseline
//! JPEG — no `png` feature needed to run this example) instead of a text
//! wordmark.
//!
//! All names, addresses, amounts and bank data below are fictional demo
//! data — this file exists purely to demonstrate layout, not to reproduce
//! any real document.
//!
//! Run: `cargo run -p lightweight-pdf --example demo_invoice`

use lightweight_pdf::*;

/// PDF points per millimeter (72pt / 25.4mm).
const MM: f32 = 72.0 / 25.4;

/// Dummy logo: white "LOGO" lettering on a silver-gray rectangle, baseline
/// JPEG (560x160px, 3.5:1) so it embeds without the optional `png` feature.
const LOGO_JPEG: &[u8] = include_bytes!("assets/logo.jpg");

struct LineItem {
    description: &'static str,
    detail: &'static str,
    qty: u32,
    unit: &'static str,
    vat_percent: u32,
    unit_price_cents: i64,
}

/// English-style amount formatting without a currency symbol, e.g.
/// `1,234.56` — the library only ships `format_currency_de` (German comma-
/// decimal), so an English demo needs its own small formatter.
fn amount(cents: i64) -> String {
    let sign = if cents < 0 { "-" } else { "" };
    let abs = cents.unsigned_abs();
    let whole = abs / 100;
    let frac = abs % 100;
    format!("{sign}{}.{frac:02}", group_thousands(whole))
}

fn group_thousands(n: u64) -> String {
    let digits = n.to_string();
    let len = digits.len();
    let mut out = String::with_capacity(len + len / 3);
    for (i, c) in digits.chars().enumerate() {
        if i > 0 && (len - i).is_multiple_of(3) {
            out.push(',');
        }
        out.push(c);
    }
    out
}

fn main() {
    let items = [
        LineItem {
            description: "Content Onboarding",
            detail: "Creation of sample pages from supplied content (demo data)",
            qty: 5,
            unit: "hrs",
            vat_percent: 19,
            unit_price_cents: 5_000,
        },
        LineItem {
            description: "Maintenance & Support",
            detail: "Updates and security patches / on-site visits (demo data)",
            qty: 4,
            unit: "hrs",
            vat_percent: 19,
            unit_price_cents: 5_000,
        },
    ];

    let net_total: i64 = items.iter().map(|i| i.qty as i64 * i.unit_price_cents).sum();
    let vat_total: i64 = items
        .iter()
        .map(|i| i.qty as i64 * i.unit_price_cents * i.vat_percent as i64 / 100)
        .sum();
    let gross_total = net_total + vat_total;

    let top_margin = 20.0 * MM;
    let mut doc = Document::new(PageFormat::A4)
        .margin(Margin::symmetric(20.0 * MM, top_margin))
        .footer(Footer::new(62.0, |_ctx| {
            Column::new()
                .gap(4.0)
                .child(Line::new())
                .child(
                    Row::new()
                        .gap(16.0)
                        .child(
                            Column::new()
                                .gap(1.0)
                                .flex(1.0)
                                .child(Text::new("Sample Design Studio").bold().size(8.0))
                                .child(Text::new("John Doe").size(8.0))
                                .child(Text::new("1 Sample Street").size(8.0))
                                .child(Text::new("12345 Sampletown").size(8.0)),
                        )
                        .child(
                            Column::new()
                                .gap(1.0)
                                .flex(1.0)
                                .child(Text::new("Phone: +1 555 0100").size(8.0))
                                .child(Text::new("Email: hello@sample-design.example").size(8.0))
                                .child(Text::new("Web: www.sample-design.example").size(8.0)),
                        )
                        .child(
                            Column::new()
                                .gap(1.0)
                                .flex(1.0)
                                .child(Text::new("Owner:").size(8.0))
                                .child(Text::new("John Doe").size(8.0))
                                .child(Text::new("VAT ID:").size(8.0))
                                .child(Text::new("EU123456789").size(8.0)),
                        )
                        .child(
                            Column::new()
                                .gap(1.0)
                                .flex(1.0)
                                .child(Text::new("Bank: Sample Bank").size(8.0))
                                .child(Text::new("Account Holder: John Doe").size(8.0))
                                .child(Text::new("IBAN: DE12 3456 7890 1234 5678 90").size(8.0))
                                .child(Text::new("BIC/SWIFT: SMPLUS33").size(8.0)),
                        ),
                )
                .into()
        }));

    // --- letterhead: logo, right-aligned --------------------------------
    doc.add(
        Column::new()
            .align(Align::End)
            .child(Image::new(LOGO_JPEG).expect("valid demo logo JPEG").width(120.0).height(34.3)),
    );
    doc.add(Spacer::new(10.0 * MM));

    doc.add(
        Row::new()
            .gap(20.0)
            .child(
                Column::new()
                    .gap(2.0)
                    .flex(1.0)
                    .child(Text::new("John Doe \u{b7} 1 Sample Street \u{b7} 12345 Sampletown").size(7.0))
                    .child(Spacer::new(8.0))
                    .child(Text::new("Sample Trading Ltd."))
                    .child(Text::new("Sample Trading Ltd."))
                    .child(Text::new("42 Example Road"))
                    .child(Text::new("54321 Exampleville")),
            )
            .child(
                Column::new()
                    .gap(2.0)
                    .width(190.0)
                    .child(Text::new("Inquiries to:").bold())
                    .child(Text::new("Sample Design Studio"))
                    .child(Text::new("+1 555 0100"))
                    .child(Text::new("hello@sample-design.example"))
                    .child(Spacer::new(8.0))
                    .child(
                        Row::new()
                            .child(Text::new("Invoice No.:").bold().flex(1.0))
                            .child(Text::new("INV-DEMO-0001")),
                    )
                    .child(
                        Row::new()
                            .child(Text::new("Customer No.:").bold().flex(1.0))
                            .child(Text::new("C-0001")),
                    )
                    .child(
                        Row::new()
                            .child(Text::new("Invoice Date:").bold().flex(1.0))
                            .child(Text::new("02/02/2026")),
                    )
                    .child(
                        Row::new()
                            .child(Text::new("Service Period:").bold().flex(1.0))
                            .child(Text::new("Jan 2026")),
                    )
                    .child(
                        Row::new()
                            .child(Text::new("Due Date:").bold().flex(1.0))
                            .child(Text::new("02/16/2026")),
                    ),
            ),
    );
    doc.add(Spacer::new(14.0 * MM));

    doc.add(Text::new("Invoice").heading3());
    doc.add(Spacer::new(6.0));
    doc.add(Text::new("Project: SMP-001 Website Maintenance (Demo)"));
    doc.add(Spacer::new(10.0));

    doc.add(
        Table::new()
            .columns([
                TableColumn::fixed(32.0),
                TableColumn::flex(1.0),
                TableColumn::fixed(45.0).align(Align::End),
                TableColumn::fixed(35.0),
                TableColumn::fixed(40.0).align(Align::End),
                TableColumn::fixed(85.0).align(Align::End),
                TableColumn::fixed(65.0).align(Align::End),
            ])
            .header(["No.", "Description", "Qty", "Unit", "VAT", "Unit Price", "Total"])
            .rows(items.iter().enumerate().map(|(i, item)| {
                let total = item.qty as i64 * item.unit_price_cents;
                vec![
                    Element::from(Text::new((i + 1).to_string())),
                    Element::from(
                        Column::new()
                            .gap(2.0)
                            .child(Text::new(item.description))
                            .child(Text::new(item.detail).size(8.0).color(Color::rgb(0x66, 0x66, 0x66))),
                    ),
                    Element::from(Text::new(item.qty.to_string()).align(Align::End)),
                    Element::from(Text::new(item.unit)),
                    Element::from(Text::new(format!("{}%", item.vat_percent)).align(Align::End)),
                    Element::from(Text::new(amount(item.unit_price_cents)).align(Align::End)),
                    Element::from(Text::new(amount(total)).bold().align(Align::End)),
                ]
            })),
    );

    doc.add(Spacer::new(14.0));

    // --- summary block (Subtotal/VAT/Total), right-aligned -------------
    doc.add(
        Column::new()
            .align(Align::End)
            .child(Column::new().gap(2.0).width(200.0).children(vec![
                Element::from(
                    Row::new()
                        .child(Text::new("Subtotal:").flex(1.0))
                        .child(Text::new(format!("EUR {}", amount(net_total)))),
                ),
                Element::from(
                    Row::new()
                        .child(Text::new("VAT (19%):").flex(1.0))
                        .child(Text::new(format!("EUR {}", amount(vat_total)))),
                ),
                Element::from(Line::new()),
                Element::from(
                    Row::new()
                        .child(Text::new("Total:").bold().flex(1.0))
                        .child(Text::new(format!("EUR {}", amount(gross_total))).bold()),
                ),
            ])),
    );

    doc.add(Spacer::new(20.0));
    doc.add(Text::new("Payable without deduction by 02/16/2026."));
    doc.add(Spacer::new(10.0));
    doc.add(Text::new("Delivered goods remain our property until paid in full."));
    doc.add(Spacer::new(10.0));
    doc.add(
        Text::new("Sample document \u{2014} all names, addresses and amounts are fictional.")
            .size(9.0)
            .color(Color::rgb(0x88, 0x88, 0x88)),
    );
    doc.add(Spacer::new(20.0));
    doc.add(Text::new("Kind regards"));
    doc.add(Spacer::new(20.0));
    doc.add(Text::new("John Doe"));

    let bytes = doc.render().expect("render should succeed");
    std::fs::write("examples/demo_invoice.pdf", &bytes).expect("write examples/demo_invoice.pdf");
    println!("wrote examples/demo_invoice.pdf ({} bytes)", bytes.len());
}

1 page · 23.4 KiB · Open PDF: Invoice · Download: Invoice

Page 1 of 1 of the rendered PDF
  • Rust
  • Table
  • Image
  • Footer

Quote

examples/demo_offer.rsDetail: Quote
//! Example: a quote/offer — the same letterhead/contact-box layout as
//! `demo_invoice`, but with lump-sum positions (no per-line VAT column,
//! VAT-inclusive unit prices), bullet-separated scope details under each
//! item, and labeled payment/delivery terms paragraphs instead of a tax
//! summary block.
//!
//! All names, addresses, amounts and bank data below are fictional demo
//! data — this file exists purely to demonstrate layout, not to reproduce
//! any real document.
//!
//! Run: `cargo run -p lightweight-pdf --example demo_offer`

use lightweight_pdf::*;

/// PDF points per millimeter (72pt / 25.4mm).
const MM: f32 = 72.0 / 25.4;

/// Dummy logo: white "LOGO" lettering on a silver-gray rectangle, baseline
/// JPEG (560x160px, 3.5:1) so it embeds without the optional `png` feature.
const LOGO_JPEG: &[u8] = include_bytes!("assets/logo.jpg");

struct LineItem {
    title: &'static str,
    description: &'static str,
    scope: &'static [&'static str],
    qty: u32,
    unit: &'static str,
    unit_price_cents: i64,
}

/// English-style amount formatting without a currency symbol, e.g.
/// `1,234.56` — the library only ships `format_currency_de` (German comma-
/// decimal), so an English demo needs its own small formatter.
fn amount(cents: i64) -> String {
    let sign = if cents < 0 { "-" } else { "" };
    let abs = cents.unsigned_abs();
    let whole = abs / 100;
    let frac = abs % 100;
    format!("{sign}{}.{frac:02}", group_thousands(whole))
}

fn group_thousands(n: u64) -> String {
    let digits = n.to_string();
    let len = digits.len();
    let mut out = String::with_capacity(len + len / 3);
    for (i, c) in digits.chars().enumerate() {
        if i > 0 && (len - i).is_multiple_of(3) {
            out.push(',');
        }
        out.push(c);
    }
    out
}

fn main() {
    let items = [
        LineItem {
            title: "Website Development",
            description: "Development of a modern, responsive website (demo data)",
            scope: &[
                "Design and concept",
                "Frontend development (HTML, CSS, JavaScript)",
                "Responsive design for all devices",
            ],
            qty: 1,
            unit: "lump",
            unit_price_cents: 500_000,
        },
        LineItem {
            title: "Content Management System",
            description: "CMS setup and configuration (demo data)",
            scope: &["Installation", "Theme customization", "Editor training"],
            qty: 1,
            unit: "lump",
            unit_price_cents: 200_000,
        },
    ];

    let total: i64 = items.iter().map(|i| i.qty as i64 * i.unit_price_cents).sum();

    let top_margin = 20.0 * MM;
    let mut doc = Document::new(PageFormat::A4)
        .margin(Margin::symmetric(20.0 * MM, top_margin))
        .footer(Footer::new(62.0, |_ctx| {
            Column::new()
                .gap(4.0)
                .child(Line::new())
                .child(
                    Row::new()
                        .gap(16.0)
                        .child(
                            Column::new()
                                .gap(1.0)
                                .flex(1.0)
                                .child(Text::new("Sample Design Studio").bold().size(8.0))
                                .child(Text::new("John Doe").size(8.0))
                                .child(Text::new("1 Sample Street").size(8.0))
                                .child(Text::new("12345 Sampletown").size(8.0)),
                        )
                        .child(
                            Column::new()
                                .gap(1.0)
                                .flex(1.0)
                                .child(Text::new("Phone: +1 555 0100").size(8.0))
                                .child(Text::new("Email: hello@sample-design.example").size(8.0))
                                .child(Text::new("Web: www.sample-design.example").size(8.0)),
                        )
                        .child(
                            Column::new()
                                .gap(1.0)
                                .flex(1.0)
                                .child(Text::new("Owner:").size(8.0))
                                .child(Text::new("John Doe").size(8.0))
                                .child(Text::new("VAT ID:").size(8.0))
                                .child(Text::new("EU123456789").size(8.0)),
                        )
                        .child(
                            Column::new()
                                .gap(1.0)
                                .flex(1.0)
                                .child(Text::new("Bank: Sample Bank").size(8.0))
                                .child(Text::new("Account Holder: John Doe").size(8.0))
                                .child(Text::new("IBAN: DE12 3456 7890 1234 5678 90").size(8.0))
                                .child(Text::new("BIC/SWIFT: SMPLUS33").size(8.0)),
                        ),
                )
                .into()
        }));

    // --- letterhead: logo, right-aligned --------------------------------
    doc.add(
        Column::new()
            .align(Align::End)
            .child(Image::new(LOGO_JPEG).expect("valid demo logo JPEG").width(120.0).height(34.3)),
    );
    doc.add(Spacer::new(10.0 * MM));

    doc.add(
        Row::new()
            .gap(20.0)
            .child(
                Column::new()
                    .gap(2.0)
                    .flex(1.0)
                    .child(Text::new("John Doe \u{b7} 1 Sample Street \u{b7} 12345 Sampletown").size(7.0))
                    .child(Spacer::new(8.0))
                    .child(Text::new("Prospect Inc."))
                    .child(Text::new("Jane Roe"))
                    .child(Text::new("7 Prospect Avenue"))
                    .child(Text::new("98765 Prospectville")),
            )
            .child(
                Column::new()
                    .gap(2.0)
                    .width(190.0)
                    .child(Text::new("Inquiries to:").bold())
                    .child(Text::new("Sample Design Studio"))
                    .child(Text::new("+1 555 0100"))
                    .child(Text::new("hello@sample-design.example"))
                    .child(Spacer::new(8.0))
                    .child(
                        Row::new()
                            .child(Text::new("Quote No.:").bold().flex(1.0))
                            .child(Text::new("QUO-DEMO-0001")),
                    )
                    .child(
                        Row::new()
                            .child(Text::new("Customer No.:").bold().flex(1.0))
                            .child(Text::new("C-0002")),
                    )
                    .child(
                        Row::new()
                            .child(Text::new("Quote Date:").bold().flex(1.0))
                            .child(Text::new("02/02/2026")),
                    )
                    .child(
                        Row::new()
                            .child(Text::new("Valid Until:").bold().flex(1.0))
                            .child(Text::new("03/04/2026")),
                    ),
            ),
    );
    doc.add(Spacer::new(14.0 * MM));

    doc.add(Text::new("Quote").heading3());
    doc.add(Spacer::new(10.0));

    doc.add(
        Table::new()
            .columns([
                TableColumn::fixed(32.0),
                TableColumn::flex(1.0),
                TableColumn::fixed(45.0).align(Align::End),
                TableColumn::fixed(40.0),
                TableColumn::fixed(80.0).align(Align::End),
                TableColumn::fixed(80.0).align(Align::End),
            ])
            .header(["No.", "Description", "Qty", "Unit", "Unit Price", "Total"])
            .rows(items.iter().enumerate().map(|(i, item)| {
                let line_total = item.qty as i64 * item.unit_price_cents;
                vec![
                    Element::from(Text::new((i + 1).to_string())),
                    Element::from(
                        Column::new()
                            .gap(2.0)
                            .child(Text::new(item.title).bold())
                            .child(Text::new(item.description).size(9.0))
                            .child(
                                Text::new(item.scope.join(" \u{2022} "))
                                    .size(8.0)
                                    .color(Color::rgb(0x66, 0x66, 0x66)),
                            ),
                    ),
                    Element::from(Text::new(item.qty.to_string()).align(Align::End)),
                    Element::from(Text::new(item.unit)),
                    Element::from(Text::new(format!("EUR {}", amount(item.unit_price_cents))).align(Align::End)),
                    Element::from(Text::new(format!("EUR {}", amount(line_total))).bold().align(Align::End)),
                ]
            })),
    );

    doc.add(Spacer::new(14.0));
    doc.add(
        Column::new().align(Align::End).child(
            Row::new()
                .width(200.0)
                .child(Text::new("Total (net):").bold().flex(1.0))
                .child(Text::new(format!("EUR {}", amount(total))).bold()),
        ),
    );
    doc.add(Spacer::new(14.0));
    doc.add(Text::new("This quote is valid for 30 days from the quote date."));
    doc.add(Spacer::new(10.0));
    doc.add(
        Row::new().gap(4.0).child(Text::new("Payment Terms:").bold()).child(
            Text::new(
                "50% deposit on order confirmation, 50% on project completion. Payable \
                 within 14 days of invoicing, without deduction.",
            )
            .flex(1.0),
        ),
    );
    doc.add(Spacer::new(6.0));
    doc.add(
        Row::new().gap(4.0).child(Text::new("Delivery Terms:").bold()).child(
            Text::new(
                "Completion within 8 weeks of order confirmation and receipt of all \
                 required content and approvals.",
            )
            .flex(1.0),
        ),
    );
    doc.add(Spacer::new(10.0));
    doc.add(
        Text::new("Sample document \u{2014} all names, addresses and amounts are fictional.")
            .size(9.0)
            .color(Color::rgb(0x88, 0x88, 0x88)),
    );
    doc.add(Spacer::new(20.0));
    doc.add(Text::new("Kind regards"));
    doc.add(Spacer::new(20.0));
    doc.add(Text::new("John Doe"));

    let bytes = doc.render().expect("render should succeed");
    std::fs::write("examples/demo_offer.pdf", &bytes).expect("write examples/demo_offer.pdf");
    println!("wrote examples/demo_offer.pdf ({} bytes)", bytes.len());
}

1 page · 24.3 KiB · Open PDF: Quote · Download: Quote

Page 1 of 1 of the rendered PDF
  • Rust
  • Table
  • List

Audit report

examples/demo_report.rsDetail: Audit report
//! Example: a website-audit-style report — same cover/ToC/header/footer
//! pattern as `demo_concept`/`demo_documentation`, with a scorecard table
//! and "top issues" subsections (the WebCheck report structure from the
//! source system: result first, then the handful of issues that matter
//! most, impact before technical cause).
//!
//! All names, scores and findings below are fictional demo data — this
//! file exists purely to demonstrate layout, not to reproduce any real
//! document.
//!
//! Run: `cargo run -p lightweight-pdf --example demo_report`

use lightweight_pdf::*;

/// Dummy logo: white "LOGO" lettering on a silver-gray rectangle, baseline
/// JPEG (560x160px, 3.5:1) so it embeds without the optional `png` feature.
const LOGO_JPEG: &[u8] = include_bytes!("assets/logo.jpg");

const ACCENT: Color = Color(0xE0, 0x50, 0x40);
const GRAY_TEXT: Color = Color(0x88, 0x88, 0x88);

fn meta_row(label: &str, mut value: Element) -> Element {
    if let Some(common) = value.common_mut() {
        common.flex = Some(1.0);
        common.overflow = Overflow::Ellipsis;
    }
    Row::new().child(Text::new(label).color(GRAY_TEXT).width(90.0)).child(value).into()
}

fn tag_pill(label: &str) -> Element {
    Text::new(label)
        .bold()
        .size(8.0)
        .color(ACCENT)
        .padding(6.0)
        .background(Color(0xFB, 0xE4, 0xE1))
        .into()
}

fn toc_entry(title: &str, page: u32) -> Element {
    Row::new()
        .child(Text::new(title).flex(1.0))
        .child(Text::new(page.to_string()))
        .into()
}

struct Issue {
    title: &'static str,
    affected: &'static str,
    impact: &'static str,
    cause: &'static str,
}

fn issue_section(n: usize, issue: &Issue) -> Vec<Element> {
    vec![
        Text::new(format!("1.2.{n} {}", issue.title)).heading3().into(),
        Text::new(issue.affected).color(GRAY_TEXT).into(),
        Text::new(issue.impact).into(),
        Text::new(issue.cause).into(),
        Spacer::new(10.0).into(),
    ]
}

fn main() {
    let doc_id = "SMP-RPT-DEMO-0001";
    let title = "Website Analysis: sample-shop.example";

    let scorecard = [
        ("Accessibility", "58 of 100", "Significant action needed"),
        ("Performance", "74 of 100", "Solid baseline"),
        ("SEO", "81 of 100", "Professional"),
        ("Security", "69 of 100", "Gaps in headers"),
        ("Mobile", "77 of 100", "Good"),
    ];

    let issues = [
        Issue {
            title: "1. Some users can't perceive content",
            affected: "42 affected elements (demo)",
            impact: "The structure isn't parseable for screen readers (demo text).",
            cause: "Tables and lists are styled visually but not marked up as such in the source code (demo text).",
        },
        Issue {
            title: "2. Text is hard to read under real-world conditions",
            affected: "9 affected elements (demo)",
            impact: "Light text on a light background makes reading in sunlight difficult (demo text).",
            cause: "Contrast ratio as low as 2.1:1 in places \u{2014} the minimum is 4.5:1 (demo text).",
        },
    ];

    let mut doc = Document::new(PageFormat::A4)
        .margin(Margin::all(20.0 * 72.0 / 25.4))
        .header(Header::new(20.0, move |_ctx| {
            Row::new()
                .child(Text::new(doc_id).size(8.0).flex(1.0))
                .child(Text::new(title).size(8.0).flex(1.0).align(Align::Center))
                .child(Text::new("Version 1.0").size(8.0).flex(1.0).align(Align::End))
                .into()
        }))
        .header_visible_from(2)
        .footer(Footer::new(24.0, |ctx| {
            Column::new()
                .gap(4.0)
                .child(Line::new())
                .child(
                    Row::new()
                        .child(Text::new("Sample Shop Ltd.").size(8.0).flex(1.0))
                        .child(
                            Text::new(format!("Page {} of {}", ctx.page, ctx.total_pages))
                                .size(8.0)
                                .flex(1.0)
                                .align(Align::Center),
                        )
                        .child(Text::new("02/02/2026").size(8.0).align(Align::End).flex(1.0)),
                )
                .into()
        }));

    // --- cover page ------------------------------------------------------
    doc.add(Spacer::new(60.0));
    doc.add(
        Column::new()
            .align(Align::Center)
            .child(Image::new(LOGO_JPEG).expect("valid demo logo JPEG").width(160.0).height(45.7)),
    );
    doc.add(Spacer::new(70.0));
    doc.add(Text::new(title).heading1());
    doc.add(Spacer::new(20.0));
    doc.add(
        Row::new()
            .gap(4.0)
            .child(Text::new("REPORT").bold().color(ACCENT))
            .child(Text::new(format!("\u{b7} {doc_id}")).color(GRAY_TEXT)),
    );
    doc.add(Spacer::new(24.0));
    doc.add(Column::new().gap(4.0).width(320.0).children(vec![
        meta_row("Project", Text::new("WebCheck (Demo)").into()),
        meta_row("Client", Text::new("Sample Shop Ltd.").into()),
        meta_row("Version", Text::new("1.0").into()),
        meta_row("Status", Text::new("COMPLETED").bold().color(ACCENT).into()),
        meta_row("Created", Text::new("02/02/2026").into()),
    ]));
    doc.add(Spacer::new(200.0));
    doc.add(
        Row::new()
            .gap(8.0)
            .child(tag_pill("WEBSITE ANALYSIS"))
            .child(tag_pill("ACCESSIBILITY"))
            .child(tag_pill("PERFORMANCE")),
    );
    doc.add(Element::PageBreak);

    // --- table of contents -------------------------------------------------
    doc.add(Text::new("Table of Contents").heading2().color(ACCENT));
    doc.add(Spacer::new(10.0));
    doc.add(Column::new().gap(4.0).children(vec![
        toc_entry("1  Result", 3),
        toc_entry("1.1  Scorecard", 3),
        toc_entry("1.2  Top Issues", 3),
        toc_entry("2  Quick Wins", 4),
    ]));
    doc.add(Element::PageBreak);

    // --- content -----------------------------------------------------------
    doc.add(Text::new("1 Result").heading2().color(ACCENT));
    doc.add(Text::new(
        "The website works in principle \u{2014} but loses impact in several places (demo text).",
    ));
    doc.add(Spacer::new(10.0));

    doc.add(Text::new("1.1 Scorecard").heading3());
    doc.add(Spacer::new(6.0));
    doc.add(
        Table::new()
            .columns([TableColumn::flex(1.0), TableColumn::fixed(90.0), TableColumn::fixed(180.0)])
            .header(["Area", "Score", "Assessment"])
            .striped(Color::rgb(0xF5, 0xF5, 0xF5))
            .rows(scorecard.iter().map(|(area, score, note)| vec![*area, *score, *note])),
    );
    doc.add(Spacer::new(14.0));

    doc.add(Text::new("1.2 Top Issues").heading3());
    doc.add(Text::new(
        "The following points currently have the biggest impact on how the website functions (demo text).",
    ));
    doc.add(Spacer::new(10.0));
    for (i, issue) in issues.iter().enumerate() {
        for el in issue_section(i + 1, issue) {
            doc.add(el);
        }
    }

    doc.add(Text::new("2 Quick Wins").heading2().color(ACCENT));
    doc.add(Spacer::new(6.0));
    doc.add(
        List::new()
            .bullet(Text::new("Raise contrast ratios to 4.5:1 (effort: low, demo)"))
            .bullet(Text::new("Add ARIA labels to interactive elements (effort: medium, demo)"))
            .bullet(Text::new("Place a visible call-to-action on the homepage (effort: low, demo)")),
    );

    let bytes = doc.render().expect("render should succeed");
    std::fs::write("examples/demo_report.pdf", &bytes).expect("write examples/demo_report.pdf");
    println!("wrote examples/demo_report.pdf ({} bytes)", bytes.len());
}
  • Rust
  • Table of contents
  • Header
  • Footer

API documentation

examples/demo_documentation.rsDetail: API documentation
//! Example: API/technical documentation — same cover/ToC/header/footer
//! pattern as `demo_concept` (this source system reuses one template for
//! concepts, reports and documentation, only the cover label text
//! differs), with endpoint sections, a monospace-styled request/response
//! block, and an error-code table.
//!
//! All names, endpoints and figures below are fictional demo data — this
//! file exists purely to demonstrate layout, not to reproduce any real
//! document.
//!
//! Run: `cargo run -p lightweight-pdf --example demo_documentation`

use lightweight_pdf::*;

/// Dummy logo: white "LOGO" lettering on a silver-gray rectangle, baseline
/// JPEG (560x160px, 3.5:1) so it embeds without the optional `png` feature.
const LOGO_JPEG: &[u8] = include_bytes!("assets/logo.jpg");

const ACCENT: Color = Color(0xE0, 0x50, 0x40);
const GRAY_TEXT: Color = Color(0x88, 0x88, 0x88);
const CODE_BG: Color = Color(0xF5, 0xF5, 0xF5);

fn meta_row(label: &str, mut value: Element) -> Element {
    if let Some(common) = value.common_mut() {
        common.flex = Some(1.0);
        common.overflow = Overflow::Ellipsis;
    }
    Row::new().child(Text::new(label).color(GRAY_TEXT).width(90.0)).child(value).into()
}

fn tag_pill(label: &str) -> Element {
    Text::new(label)
        .bold()
        .size(8.0)
        .color(ACCENT)
        .padding(6.0)
        .background(Color(0xFB, 0xE4, 0xE1))
        .into()
}

fn toc_entry(title: &str, page: u32) -> Element {
    Row::new()
        .child(Text::new(title).flex(1.0))
        .child(Text::new(page.to_string()))
        .into()
}

/// A `code`-block-alike: monospace-adjacent styling isn't available (no
/// bundled monospace font, see README's `default-fonts` note) — a shaded
/// box with `Text` stands in for it, matching this document's own
/// convention for inline technical snippets.
fn code_block(lines: &[&str]) -> Element {
    Column::new()
        .gap(2.0)
        .padding(8.0)
        .background(CODE_BG)
        .children(lines.iter().map(|l| Text::new(*l).size(9.0)))
        .into()
}

fn main() {
    let doc_id = "SMP-DOC-DEMO-0001";
    let title = "Sample API Documentation";

    let mut doc = Document::new(PageFormat::A4)
        .margin(Margin::all(20.0 * 72.0 / 25.4))
        .header(Header::new(20.0, move |_ctx| {
            Row::new()
                .child(Text::new(doc_id).size(8.0).flex(1.0))
                .child(Text::new(title).size(8.0).flex(1.0).align(Align::Center))
                .child(Text::new("Version 2.0").size(8.0).flex(1.0).align(Align::End))
                .into()
        }))
        .header_visible_from(2)
        .footer(Footer::new(24.0, |ctx| {
            Column::new()
                .gap(4.0)
                .child(Line::new())
                .child(
                    Row::new()
                        .child(Text::new("Sample Studio Ltd.").size(8.0).flex(1.0))
                        .child(
                            Text::new(format!("Page {} of {}", ctx.page, ctx.total_pages))
                                .size(8.0)
                                .flex(1.0)
                                .align(Align::Center),
                        )
                        .child(Text::new("02/02/2026").size(8.0).align(Align::End).flex(1.0)),
                )
                .into()
        }));

    // --- cover page ------------------------------------------------------
    doc.add(Spacer::new(60.0));
    doc.add(
        Column::new()
            .align(Align::Center)
            .child(Image::new(LOGO_JPEG).expect("valid demo logo JPEG").width(160.0).height(45.7)),
    );
    doc.add(Spacer::new(70.0));
    doc.add(Text::new(title).heading1());
    doc.add(Spacer::new(20.0));
    doc.add(
        Row::new()
            .gap(4.0)
            .child(Text::new("DOCUMENTATION").bold().color(ACCENT))
            .child(Text::new(format!("\u{b7} {doc_id}")).color(GRAY_TEXT)),
    );
    doc.add(Spacer::new(24.0));
    doc.add(Column::new().gap(4.0).width(320.0).children(vec![
        meta_row("Project", Text::new("Sample Platform API (Demo)").into()),
        meta_row("Client", Text::new("Sample Studio Ltd.").into()),
        meta_row("Version", Text::new("2.0").into()),
        meta_row("Status", Text::new("FINAL").bold().color(ACCENT).into()),
        meta_row("Created", Text::new("02/02/2026").into()),
    ]));
    doc.add(Spacer::new(200.0));
    doc.add(
        Row::new()
            .gap(8.0)
            .child(tag_pill("API"))
            .child(tag_pill("REST"))
            .child(tag_pill("DEMO")),
    );
    doc.add(Element::PageBreak);

    // --- table of contents -------------------------------------------------
    doc.add(Text::new("Table of Contents").heading2().color(ACCENT));
    doc.add(Spacer::new(10.0));
    doc.add(Column::new().gap(4.0).children(vec![
        toc_entry("1  Overview", 3),
        toc_entry("2  Authentication", 3),
        toc_entry("3  Endpoints", 3),
        toc_entry("3.1  GET /v2/items", 3),
        toc_entry("4  Error Handling", 4),
    ]));
    doc.add(Element::PageBreak);

    // --- content -----------------------------------------------------------
    doc.add(Text::new("1 Overview").heading2().color(ACCENT));
    doc.add(Text::new(
        "The Sample API exposes resources of the Sample Platform as a REST interface. \
         All responses are JSON-encoded (demo text).",
    ));
    doc.add(Spacer::new(12.0));

    doc.add(Text::new("2 Authentication").heading2().color(ACCENT));
    doc.add(Text::new(
        "Every request needs a bearer token in the Authorization header (demo text):",
    ));
    doc.add(Spacer::new(6.0));
    doc.add(code_block(&["Authorization: Bearer REDACTED-DEMO-PLACEHOLDER"]));
    doc.add(Spacer::new(12.0));

    doc.add(Text::new("3 Endpoints").heading2().color(ACCENT));
    doc.add(Spacer::new(6.0));
    doc.add(Text::new("3.1 GET /v2/items").heading3());
    doc.add(Text::new("Returns a list of sample objects (demo text)."));
    doc.add(Spacer::new(6.0));
    doc.add(code_block(&[
        "GET /v2/items?limit=20 HTTP/1.1",
        "Host: api-demo.sample-design.example",
        "",
        "{ \"items\": [ { \"id\": \"itm_001\", \"name\": \"Sample\" } ], \"total\": 1 }",
    ]));
    doc.add(Spacer::new(12.0));

    doc.add(Text::new("4 Error Handling").heading2().color(ACCENT));
    doc.add(Spacer::new(6.0));
    doc.add(
        Table::new()
            .columns([TableColumn::fixed(60.0), TableColumn::fixed(140.0), TableColumn::flex(1.0)])
            .header(["Code", "Meaning", "Description"])
            .rows(vec![
                vec!["400", "Bad Request", "Invalid or missing parameters"],
                vec!["401", "Unauthorized", "Token missing or invalid"],
                vec!["404", "Not Found", "Resource does not exist"],
                vec!["429", "Too Many Requests", "Rate limit exceeded"],
            ]),
    );

    let bytes = doc.render().expect("render should succeed");
    std::fs::write("examples/demo_documentation.pdf", &bytes).expect("write examples/demo_documentation.pdf");
    println!("wrote examples/demo_documentation.pdf ({} bytes)", bytes.len());
}
  • Rust
  • Table of contents
  • Links

Template and data with the CLI

examples/invoice-template.jsonDetail: Template and data with the CLI
// examples/invoice-template.json
{
  "schema_version": 1,
  "document": {
    "page_format": "A4",
    "margin": { "top": 40.0, "right": 40.0, "bottom": 40.0, "left": 40.0 },
    "metadata": { "title": "{{invoice.number}}" },
    "children": [
      {
        "type": "text",
        "content": "Rechnung {{invoice.number}}",
        "style": { "size": 22.0, "font": "sans-bold" }
      },
      {
        "type": "text",
        "content": "{{customer.name}}"
      },
      {
        "type": "table",
        "columns": [
          { "width": { "flex": 1.0 } },
          { "width": { "fixed": 80.0 }, "align": "end" }
        ],
        "header": [
          { "element": { "type": "text", "content": "Beschreibung" } },
          { "element": { "type": "text", "content": "Betrag" } }
        ],
        "rows": [
          {
            "$each": "invoice.items",
            "template": [
              { "element": { "type": "text", "content": "{{description}}" } },
              { "element": { "type": "text", "content": "{{amount}}" } }
            ]
          }
        ]
      },
      {
        "type": "text",
        "content": "Gesamtsumme: {{invoice.total}}",
        "style": { "font": "sans-bold" }
      }
    ]
  }
}

// examples/invoice-data.json
{
  "invoice": {
    "number": "RE-2026-0100",
    "total": "1.200,00 €",
    "items": [
      { "description": "Beratung Softwarearchitektur", "amount": "1.000,00 €" },
      { "description": "Reisekosten", "amount": "200,00 €" }
    ]
  },
  "customer": {
    "name": "Acme Software GmbH"
  }
}
  • CLI
  • JSON
  • Template

Tagged PDF / PDF/UA-1

examples/demo_pdf_ua.rsDetail: Tagged PDF / PDF/UA-1
//! Example: Tagged PDF/PDF-UA output (issue #27) — `Document::pdf_ua()`
//! writes a structure tree (`/StructTreeRoot`, one `/StructElem` per
//! heading/paragraph/table/list/figure), marked content (`BDC`/`EMC` with
//! MCIDs) in every content stream, and marks the watermark/footer as
//! artifacts (pagination decoration, excluded from reading order) rather
//! than structure. Implies `.pdf_a3b()` (ADR-019) — this document is both
//! PDF/A-3b and PDF/UA-1 conformant.
//!
//! Run: `cargo run -p lightweight-pdf --example demo_pdf_ua --features tagged-pdf,png`
//! Verify: `verapdf --flavour ua1 examples/demo_pdf_ua.pdf` (also passes
//! `--flavour 3b`) — see `docs.verapdf.org/install`, or `docker run --rm
//! -v "$PWD/examples:/data" verapdf/cli --flavour ua1 /data/demo_pdf_ua.pdf`

use lightweight_pdf::*;

const LOGO: &[u8] = include_bytes!("../test-fixtures/images/logo_rgba.png");

fn main() {
    let mut doc = Document::new(PageFormat::A4)
        .margin(Margin::all(40.0))
        .pdf_ua()
        .lang("en-US")
        .watermark(Watermark::new("SAMPLE"))
        .footer(Footer::new(20.0, |ctx| {
            Text::new(format!("Page {} of {}", ctx.page, ctx.total_pages)).into()
        }));
    doc.metadata.title = Some("Tagged PDF / PDF-UA Demo".to_string());
    doc.metadata.author = Some("lightweight-pdf".to_string());

    doc.add(Text::new("Tagged PDF / PDF-UA Demo").heading1());
    doc.add(Text::new(
        "This document has a real structure tree: headings, this paragraph, the table and \
         list below, and the image all have their own tagged structure element and reading \
         order that follows document order, not render order. The watermark and page-number \
         footer are marked as artifacts, excluded from that reading order entirely.",
    ));

    doc.add(Text::new("Accessibility checklist").heading2());
    doc.add(
        Table::new()
            .columns([TableColumn::flex(1.0), TableColumn::fixed(70.0).align(Align::End)])
            .header(["Check", "Status"])
            .rows(vec![
                vec![Element::from("Structure tree"), Element::from("yes")],
                vec![Element::from("Reading order"), Element::from("yes")],
                vec![Element::from("Alt text"), Element::from("yes")],
            ]),
    );

    doc.add(Text::new("Highlights").heading2());
    doc.add(
        List::new()
            .bullet(Text::new("Headings tagged H1/H2"))
            .bullet(Text::new("Table tagged Table/TR/TH/TD"))
            .numbered(Text::new("Image tagged Figure with Alt text")),
    );

    doc.add(
        Image::new(LOGO)
            .expect("valid PNG fixture")
            .width(100.0)
            .alt("lightweight-pdf logo"),
    );

    let (bytes, warnings) = doc.render_with_diagnostics().expect("render should succeed");
    assert!(warnings.is_empty(), "unexpected warnings: {warnings:?}");
    std::fs::write("examples/demo_pdf_ua.pdf", &bytes).expect("write examples/demo_pdf_ua.pdf");
    println!("wrote examples/demo_pdf_ua.pdf ({} bytes)", bytes.len());
}
  • Rust
  • tagged-pdf
  • Accessibility

ZUGFeRD / Factur-X invoice

examples/demo_zugferd.rsDetail: ZUGFeRD / Factur-X invoice
//! Example: ZUGFeRD/Factur-X output (issue #26) — `Document::zugferd_xml()`
//! embeds a caller-supplied EN 16931 invoice XML as an associated file
//! (implies `.pdf_a3b()`: ZUGFeRD *is* a PDF/A-3 file with an embedded
//! invoice, not an independent format). This crate embeds only — it
//! never generates or validates the XML itself (ADR-018 in the local
//! `plan/00-decisions.md`); the sample XML below is a real EN
//! 16931-conformant invoice from the ZUGFeRD reference test corpus, not
//! something this crate produced.
//!
//! Run: `cargo run -p lightweight-pdf --example demo_zugferd --features zugferd`
//! Verify: any ZUGFeRD/Factur-X validator (e.g. the [Mustang
//! Project](https://www.mustangproject.org/) validator, or
//! <https://www.itb.ec.europa.eu/invoice/upload> for EN 16931).

use lightweight_pdf::*;

const INVOICE_XML: &[u8] = include_bytes!("../test-fixtures/zugferd/en16931-sample.xml");

fn main() {
    let mut doc = Document::new(PageFormat::A4).margin(Margin::all(40.0)).zugferd_xml(INVOICE_XML);
    doc.metadata.title = Some("ZUGFeRD Demo Invoice".to_string());
    doc.metadata.author = Some("lightweight-pdf".to_string());

    doc.add(Text::new("Invoice RE-20201121/508").heading1());
    doc.add(Text::new(
        "This PDF embeds a machine-readable EN 16931 invoice (factur-x.xml) alongside this \
         human-readable rendering — the hybrid format ZUGFeRD/Factur-X e-invoicing requires. \
         Open this file's attachments panel in a PDF reader to see the embedded XML.",
    ));
    doc.add(
        Table::new()
            .columns([TableColumn::flex(1.0), TableColumn::fixed(80.0).align(Align::End)])
            .header(["Item", "Amount"])
            .rows(vec![vec![Element::from("See embedded XML for line items"), Element::from("—")]]),
    );

    let bytes = doc.render().expect("render should succeed");
    std::fs::write("examples/demo_zugferd.pdf", &bytes).expect("write examples/demo_zugferd.pdf");
    println!("wrote examples/demo_zugferd.pdf ({} bytes)", bytes.len());
}
  • Rust
  • zugferd
  • PDF/A-3b