lightweight-pdfv0.3.0

← All examples

Audit report

Cover page, table of contents, running header and footer with page numbers, a scorecard table and one subsection per top issue.

Audit report

examples/demo_report.rs
//! 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