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());
}
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());
}
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());
}


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());
}


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"
}
}
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());
}
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());
}