lightweight-pdfv0.3.0

← All examples

Quote

The invoice letterhead with lump-sum positions, bullet-separated scope details under each item and labelled payment and delivery terms instead of a tax summary.

Quote

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