pdfsmith 0.1.1

A customizable PDF generation library from Markdown or structured JSON input
Documentation

pdfsmith

Rust Crates.io Docs.rs License: MIT Build Status

A fully customizable Rust library for generating PDFs from Markdown, structured JSON, or raw HTML! 🚀

Everything is under the user's control — CSS styling, page size, margins, orientation, headers, footers, Markdown extensions, and Chrome rendering options. Supply your own CSS or use the clean built-in default. ✨

  • 🚀 Built with Rust
  • 📄 Outputs polished PDF documents
  • ✨ Supports Markdown, JSON, and HTML input
  • 🎨 Fully customizable with CSS
  • 📏 Configurable paper sizes and margins
  • 🔄 Supports headers, footers, and page numbers
  • 🏷️ Markdown extensions galore
  • 🖼️ Handles images and tables seamlessly
  • ⚡ Fast and efficient PDF generation

Table of Contents


Features

Feature Description
Markdown → PDF Convert any Markdown string or file to a styled PDF 📝➡️📄
JSON → PDF Define document content with typed JSON blocks (headings, paragraphs, tables, code, lists, etc.) 📋➡️📄
HTML → PDF Pass your own pre-built HTML directly 🌐➡️📄
Custom CSS Replace the default stylesheet entirely, or append extra rules 🎨
Paper Sizes A4, Letter, Legal, or any custom width × height 📏
Page Margins Set top, bottom, left, right margins independently (in inches) 📐
Orientation Portrait or landscape 🔄
Headers Simple left/center/right text, or full custom HTML 📄
Footers Simple left/center/right text with auto page numbers, or full custom HTML 📄
Heading Numbers Automatic hierarchical numbering (1, 1.1, 1.1.1) via JSON paraSequence or CSS counters 🔢
Markdown Extensions Tables, footnotes, strikethrough, task lists, autolinks, superscript, description lists — all toggleable 📝
File Input Read .md or .json files directly from disk 📁
Builder Pattern Fluent, chainable API — configure only what you need 🛠️

API Overview

  • PdfBuilder — main fluent builder for generating PDFs from Markdown, JSON, or HTML. 🏗️
  • PdfConfig — full PDF generation configuration. ⚙️
  • PaperSize, PageMargins, HeaderConfig, FooterConfig, MarkdownOptions — core configuration helpers. 🧰
  • DEFAULT_CSS — inspect or reuse the built-in stylesheet. 🎨
  • MdocError / Result — error type and result alias used throughout the crate. ❌

Examples Summary

  • Minimal Markdown PDF generation 📝
  • JSON document generation with paraSequence headings 📋
  • Custom CSS styling and theming 🎨
  • Full document generation with headers, footers, and page options 📄
  • Report-style documents, newsletters, technical docs, and images 📰
  • And much more! 🌟

Prerequisites

Install Chromium / Chrome

pdfsmith uses headless Chrome/Chromium under the hood to render HTML to PDF. You must have Chrome or Chromium installed on any machine that generates PDFs. 🌐

Ubuntu / Debian

# Option A: via snap (recommended)
sudo snap install chromium

# Option B: via apt
sudo apt update
sudo apt install -y chromium-browser

Fedora / RHEL

sudo dnf install chromium

macOS

# If you have Google Chrome installed, it works automatically.
# Otherwise:
brew install --cask chromium

Windows

Install Google Chrome or Chromium. The headless_chrome crate will auto-detect it. 🪟

Docker

If your app runs in a container, add Chromium to your Dockerfile:

RUN apt-get update && apt-get install -y chromium-browser --no-install-recommends && rm -rf /var/lib/apt/lists/*

Why not auto-install? A library crate should never silently install system software. Chromium is a ~200 MB browser — the user should install it explicitly and control which version is on their system. 🤔

Verify the Installation

chromium --version
# or
chromium-browser --version
# or
google-chrome --version

If any of these prints a version number, you're ready. ✅


Installation

Add pdfsmith to your Cargo.toml:

[dependencies]
pdfsmith = "0.1.0"

Or via the command line:

cargo add pdfsmith

Optional: Enable Logging

pdfsmith uses the log crate. To see progress messages, add a logger like env_logger:

[dependencies]
env_logger = "0.11"

Then initialise it in your code:

fn main() {
    env_logger::init(); // call once at startup
    // ... your code ...
}

Run with RUST_LOG=info to see output:

RUST_LOG=info cargo run

Quick Start

use pdfsmith::PdfBuilder;

fn main() {
    let pdf_bytes = PdfBuilder::new()
        .from_markdown("# Hello World\n\nThis is my first PDF.")
        .expect("PDF generation failed");

    std::fs::write("hello.pdf", &pdf_bytes).expect("Failed to write file");
    println!("PDF saved! ({} bytes)", pdf_bytes.len());
}

That's it. Three lines of code, zero configuration. The default CSS, A4 paper, 0.75-inch margins, and no header/footer are applied automatically. 🚀


Input Sources

From Markdown String

use pdfsmith::PdfBuilder;

let pdf = PdfBuilder::new()
    .from_markdown("# Title\n\nSome **bold** and *italic* text.")
    .unwrap();

All standard Markdown is supported: headings, bold, italic, links, images, code blocks, tables, task lists, blockquotes, horizontal rules, footnotes, strikethrough, and more. 📝

From Markdown File

use pdfsmith::PdfBuilder;

let pdf = PdfBuilder::new()
    .from_markdown_file("docs/report.md")
    .unwrap();

From JSON

use pdfsmith::PdfBuilder;

let json = serde_json::json!([
    { "type": "heading", "paraSequence": "1", "text": "Report Title" },
    { "type": "paragraph", "text": "Some content with **Markdown** formatting." },
    { "type": "heading", "paraSequence": "1.1", "text": "Details" },
    { "type": "paragraph", "text": "The heading level is derived from paraSequence depth." }
]);

let pdf = PdfBuilder::new()
    .from_json(&json)
    .unwrap();

JSON is first converted to Markdown, then rendered through the same pipeline — so all CSS and config applies equally. 📋

From JSON File

use pdfsmith::PdfBuilder;

let pdf = PdfBuilder::new()
    .from_json_file("data/document.json")
    .unwrap();

From Raw HTML

use pdfsmith::PdfBuilder;

let html = r#"
<!DOCTYPE html>
<html>
<head><style>body { font-family: Arial; }</style></head>
<body><h1>Hello</h1><p>From raw HTML.</p></body>
</html>
"#;

let pdf = PdfBuilder::new()
    .from_html(html)
    .unwrap();

When using from_html, the HTML is sent to Chrome as-is — no CSS injection, no Markdown conversion. Header/footer and page options still apply. 🌐


JSON Structure Reference

The JSON input is an array of content blocks. Each block has a "type" field that determines how it's rendered.

Two top-level formats are accepted:

// Format A: bare array
[
  { "type": "heading", "paraSequence": "1", "text": "Title" },
  { "type": "paragraph", "text": "Body." },
  { "type": "heading", "paraSequence": "1.1", "text": "Sub-section" },
  { "type": "paragraph", "text": "More body." }
]

// Format B: object with "content" key
{
  "content": [
    { "type": "heading", "paraSequence": "1", "text": "Title" },
    { "type": "paragraph", "text": "Body." }
  ]
}

Block Types

heading

{ "type": "heading", "paraSequence": "1.2", "text": "Section Title" }
Field Type Required Description
paraSequence string No Hierarchical section number (e.g. "1", "1.2", "1.2.1", "A.1"). The heading level (h1–h6) is derived from the depth (number of dot-separated parts). The sequence is prefixed to the heading text automatically.
text string Yes Heading text (Markdown allowed)
level number (1–6) No Explicit heading level — overrides the level derived from paraSequence. If neither paraSequence nor level is given, defaults to h1.

paragraph (alias: text)

{ "type": "paragraph", "text": "Some **bold** text with [links](https://example.com)." }
Field Type Required Description
text string Yes Paragraph content (Markdown allowed)

code

{ "type": "code", "language": "rust", "text": "fn main() {\n    println!(\"Hello\");\n}" }
Field Type Required Description
language string No (default: "") Syntax highlighting hint
text string Yes Code content

list

{ "type": "list", "ordered": true, "items": ["First", "Second", "Third"] }
Field Type Required Description
ordered boolean No (default: false) true for numbered list, false for bullets
items array of strings Yes List items (Markdown allowed in each item)

quote (alias: blockquote)

{ "type": "quote", "text": "To be or not to be." }
Field Type Required Description
text string Yes Quote content (Markdown allowed)

table

{
  "type": "table",
  "headers": ["Name", "Role", "Location"],
  "rows": [
    ["Alice", "Engineer", "London"],
    ["Bob", "Designer", "Berlin"]
  ]
}
Field Type Required Description
headers array of strings Yes Column headers
rows array of arrays No Data rows (each row is an array of strings)

image (alias: img)

{ "type": "image", "src": "https://example.com/photo.png", "alt": "A photo" }
Field Type Required Description
src string Yes Image URL or path
alt string No (default: "") Alt text

divider (alias: hr)

{ "type": "divider" }

Renders a horizontal rule (---). No fields needed.

html (alias: raw)

{ "type": "html", "text": "<div style='color:red;'>Custom HTML</div>" }
Field Type Required Description
text string Yes Raw HTML passed through as-is

Customization Guide

Every aspect of the PDF is configurable through PdfBuilder methods or the PdfConfig struct directly. 🎛️

CSS Styling

Option 1: Use the built-in default

Do nothing — a clean, neutral, print-friendly stylesheet is applied automatically. 🎨

let pdf = PdfBuilder::new()
    .from_markdown("# Uses default CSS")
    .unwrap();

Option 2: Replace the default CSS entirely

let pdf = PdfBuilder::new()
    .custom_css(r#"
        body { font-family: Georgia, serif; font-size: 12pt; color: #333; }
        h1 { color: navy; border-bottom: 2px solid navy; }
        code { background: #f0f0f0; padding: 2px 4px; }
        table { border-collapse: collapse; width: 100%; }
        th, td { border: 1px solid #ccc; padding: 8px; }
    "#)
    .from_markdown("# Fully custom styled")
    .unwrap();

When custom_css is set, the built-in stylesheet is completely replaced. You are in full control. 🎨

Option 3: Append extra CSS to the default

let pdf = PdfBuilder::new()
    .extra_css("h1 { color: darkred; } blockquote { border-color: blue; }")
    .from_markdown("# Default + tweaks")
    .unwrap();

extra_css is added after the default stylesheet, so your rules override specific properties while keeping everything else intact. 🎨

Option 4: Load CSS from a file

let css = std::fs::read_to_string("styles/my-theme.css").unwrap();
let pdf = PdfBuilder::new()
    .custom_css(css)
    .from_markdown("# Themed")
    .unwrap();

Access the default CSS

The built-in stylesheet is exported as a constant if you want to inspect or extend it programmatically:

use pdfsmith::DEFAULT_CSS;
println!("{}", DEFAULT_CSS);

Paper Size

use pdfsmith::{PdfBuilder, PaperSize};

// Preset sizes
PdfBuilder::new().paper_size(PaperSize::A4);      // 8.27 × 11.69 in (default)
PdfBuilder::new().paper_size(PaperSize::Letter);   // 8.5  × 11    in
PdfBuilder::new().paper_size(PaperSize::Legal);    // 8.5  × 14    in

// Custom size (width × height in inches)
PdfBuilder::new().paper_size(PaperSize::Custom { width: 6.0, height: 9.0 }); // e.g. trade paperback

Page Margins

Margins are specified in inches:

use pdfsmith::{PdfBuilder, PageMargins};

let pdf = PdfBuilder::new()
    .margins(PageMargins {
        top: 1.0,
        bottom: 1.0,
        left: 0.5,
        right: 0.5,
    })
    .from_markdown("# Custom margins")
    .unwrap();

Default margins are 0.75 inches on all sides. 📐

Orientation

let pdf = PdfBuilder::new()
    .landscape(true)   // landscape
    .from_markdown("# Wide layout")
    .unwrap();

Default is false (portrait). 🔄

Headers

Headers appear on every page. Three approaches: 📄

Simple left / center / right

use pdfsmith::{PdfBuilder, HeaderConfig};

let pdf = PdfBuilder::new()
    .display_header_footer(true)
    .header(HeaderConfig {
        left: Some("Company Name".into()),
        center: Some("Document Title".into()),
        right: Some("2026-04-17".into()),
        font_size: Some("9px".into()),   // optional, default: "9px"
        color: Some("#333".into()),       // optional, default: "#555"
        ..Default::default()
    })
    .from_markdown("# With header")
    .unwrap();

Full custom HTML

use pdfsmith::{PdfBuilder, HeaderConfig};

let pdf = PdfBuilder::new()
    .display_header_footer(true)
    .header(HeaderConfig {
        custom_html: Some(r#"
            <div style="width:100%; text-align:center; font-size:10px; color:#999; padding:4px;">
                My Custom Header — Page <span class="pageNumber"></span>
            </div>
        "#.into()),
        ..Default::default()
    })
    .from_markdown("# With custom header HTML")
    .unwrap();

No header (default)

let pdf = PdfBuilder::new()
    .from_markdown("# No header")   // display_header_footer defaults to false
    .unwrap();

Footers

Footers work exactly like headers. 📄

Default footer (when enabled)

When display_header_footer(true) is set and no footer fields are configured, a simple centred page number is shown: Page 1 / 5.

let pdf = PdfBuilder::new()
    .display_header_footer(true)
    .from_markdown("# With page numbers")
    .unwrap();

Custom left / center / right

use pdfsmith::{PdfBuilder, FooterConfig};

let pdf = PdfBuilder::new()
    .display_header_footer(true)
    .footer(FooterConfig {
        left: Some("CONFIDENTIAL".into()),
        center: Some("Internal Use Only".into()),
        right: Some(r#"Page <span class="pageNumber"></span> of <span class="totalPages"></span>"#.into()),
        font_size: Some("8px".into()),
        color: Some("#888".into()),
        ..Default::default()
    })
    .from_markdown("# With footer")
    .unwrap();

Full custom HTML footer

use pdfsmith::{PdfBuilder, FooterConfig};

let pdf = PdfBuilder::new()
    .display_header_footer(true)
    .footer(FooterConfig {
        custom_html: Some(r#"
            <div style="width:100%; display:flex; justify-content:space-between; padding:4px 0.75in; font-size:8px; color:#666; font-family:Arial;">
                <span>© 2026 Acme Corp</span>
                <span>Page <span class="pageNumber"></span> / <span class="totalPages"></span></span>
            </div>
        "#.into()),
        ..Default::default()
    })
    .from_markdown("# Custom footer HTML")
    .unwrap();

Chrome Page Number Placeholders

Inside any header/footer text or custom_html, use these Chrome built-in placeholders:

Placeholder Renders as
<span class="pageNumber"></span> Current page number
<span class="totalPages"></span> Total page count
<span class="title"></span> Document title
<span class="url"></span> Page URL
<span class="date"></span> Current date

Markdown Extensions

All extensions are enabled by default. Toggle them individually: 📝

use pdfsmith::{PdfBuilder, MarkdownOptions};

let pdf = PdfBuilder::new()
    .markdown_options(MarkdownOptions {
        tables: true,              // | col1 | col2 |
        footnotes: true,           // [^1]: footnote text
        strikethrough: true,       // ~~deleted~~
        tasklist: true,            // - [x] done
        autolink: true,            // https://auto.link
        superscript: true,         // 2^10
        description_lists: true,   // term\n: definition
        unsafe_html: true,         // pass-through raw <html> tags
    })
    .from_markdown("# All extensions on")
    .unwrap();

To disable a specific extension:

let pdf = PdfBuilder::new()
    .markdown_options(MarkdownOptions {
        tasklist: false,       // disable task lists
        superscript: false,    // disable superscript
        ..Default::default()   // keep everything else on
    })
    .from_markdown("# Some extensions off")
    .unwrap();

Heading Numbers

Automatic hierarchical heading numbers like 1, 1.1, 1.1.1, 2, 2.1. 🔢

There are two approaches — use whichever fits your workflow:

Approach 1: JSON auto_number (JSON input only)

Set "auto_number": true in the top-level JSON object. Numbers are generated automatically from the heading level fields:

use pdfsmith::PdfBuilder;

let json = serde_json::json!({
    "auto_number": true,
    "content": [
        { "type": "heading", "level": 1, "text": "Introduction" },
        { "type": "paragraph", "text": "Intro body." },
        { "type": "heading", "level": 2, "text": "Background" },
        { "type": "heading", "level": 2, "text": "Scope" },
        { "type": "heading", "level": 3, "text": "Details" },
        { "type": "heading", "level": 1, "text": "Conclusion" }
    ]
});

let pdf = PdfBuilder::new()
    .from_json(&json)
    .unwrap();

This produces headings:

1 Introduction
1.1 Background
1.2 Scope
1.2.1 Details
2 Conclusion

Manual number field

Any heading block can carry a "number" field for manual control. This takes priority over auto-numbering:

[
  { "type": "heading", "level": 1, "number": "A", "text": "Appendix" },
  { "type": "heading", "level": 2, "number": "A.1", "text": "First Section" },
  { "type": "heading", "level": 2, "number": "A.2", "text": "Second Section" }
]

Approach 2: CSS-based heading_numbers (works with ALL input types)

Enable .heading_numbers(true) on the builder. This injects CSS counter rules that automatically number every heading — works for Markdown, JSON, and raw HTML:

use pdfsmith::PdfBuilder;

let pdf = PdfBuilder::new()
    .heading_numbers(true)
    .from_markdown("# Intro\n\n## Part A\n\n## Part B\n\n### Detail\n\n# Summary")
    .unwrap();

Rendered headings look like:

1. Intro
1.1 Part A
1.2 Part B
1.2.1 Detail
2. Summary

The CSS approach has no dependency on JSON — it works everywhere.

Which should I use?

  • Use auto_number in JSON when you want the numbers baked into the Markdown text (good for exports, copy-paste).
  • Use heading_numbers(true) on the builder when you want visual numbering via CSS (works with any input, numbers don’t appear in the underlying text).

Chrome Options

Fine-tune the headless Chrome rendering: 🌐

let pdf = PdfBuilder::new()
    .chrome_window_size(1920, 1080)   // browser viewport (default: 1280×900)
    .page_load_wait_secs(5)           // wait for images/JS (default: 2)
    .print_background(true)           // render background colours (default: true)
    .from_markdown("# Chrome settings")
    .unwrap();

Full Configuration Reference

You can also set everything at once via the PdfConfig struct:

use pdfsmith::{PdfBuilder, PdfConfig, PaperSize, PageMargins, HeaderConfig, FooterConfig, MarkdownOptions};

let config = PdfConfig {
    title: "My Document".into(),
    custom_css: None,                                  // use built-in default
    extra_css: Some("h1 { color: navy; }".into()),     // tweak headings
    paper_size: PaperSize::A4,
    margins: PageMargins { top: 1.0, bottom: 1.0, left: 0.75, right: 0.75 },
    landscape: false,
    display_header_footer: true,
    header: HeaderConfig {
        left: Some("Acme Corp".into()),
        center: Some("Report".into()),
        right: Some("April 2026".into()),
        font_size: Some("9px".into()),
        color: Some("#333".into()),
        custom_html: None,
    },
    footer: FooterConfig {
        left: None,
        center: None,
        right: Some(r#"Page <span class="pageNumber"></span>"#.into()),
        font_size: Some("8px".into()),
        color: Some("#888".into()),
        custom_html: None,
    },
    print_background: true,
    heading_numbers: false,
    markdown_options: MarkdownOptions::default(),
    chrome_window_size: (1280, 900),
    page_load_wait_secs: 5, // use 5 when any image is available in markdown or json.
};

let pdf = PdfBuilder::with_config(config)
    .from_markdown("# Hello from PdfConfig")
    .unwrap();

PdfConfig Fields

Field Type Default Description
title String "" Document title (used in <title> tag and default header)
custom_css Option<String> None Replaces the entire default stylesheet
extra_css Option<String> None Appended after the base stylesheet
paper_size PaperSize A4 Paper dimensions
margins PageMargins 0.75 all Page margins in inches
landscape bool false Landscape orientation
display_header_footer bool false Show header and footer
header HeaderConfig empty Header configuration
footer FooterConfig empty Footer configuration
print_background bool true Print background colours/images
markdown_options MarkdownOptions all true Markdown extension toggles
heading_numbers bool false CSS-based hierarchical heading numbers
chrome_window_size (u32, u32) (1280, 900) Headless Chrome viewport
page_load_wait_secs u64 5 Seconds to wait after page load ( Usually helpfull to load images perfectly)

Examples

The repository includes eight runnable examples in the examples/ directory, each producing a 2+ page PDF. 📁

Example 1 — Minimal Markdown

Markdown string → PDF with heading_numbers(true). Default CSS, no header/footer. Covers tables, code blocks, lists, blockquotes.

cargo run --example from_markdown

Example 2 — JSON with paraSequence

Structured JSON content blocks with "paraSequence" on every heading for hierarchical numbering. The heading level (h1–h6) is derived automatically from the sequence depth.

// examples/from_json.rs  (abbreviated)
let json = serde_json::json!([
    { "type": "heading", "paraSequence": "1",     "text": "Project Status Report" },
    { "type": "paragraph", "text": "This report summarises the current state." },
    { "type": "heading", "paraSequence": "1.1",   "text": "Timeline" },
    { "type": "table", "headers": ["Phase","Status","Due"], "rows": [["Design","Complete","2026-01-15"]] },
    { "type": "heading", "paraSequence": "1.2",   "text": "Key Highlights" },
    { "type": "heading", "paraSequence": "1.2.1", "text": "Performance Details" },
    { "type": "heading", "paraSequence": "2",     "text": "Architecture" },
    // ... more blocks ...
]);

let pdf = PdfBuilder::new()
    .from_json(&json)
    .expect("Failed to generate PDF");

Output headings:

1 Project Status Report       (depth 1 → h1)
  1.1 Timeline                (depth 2 → h2)
  1.2 Key Highlights          (depth 2 → h2)
    1.2.1 Performance Details  (depth 3 → h3)
2 Architecture                (depth 1 → h1)
cargo run --example from_json

Example 3 — Custom CSS

Replaces the entire default stylesheet. Indented sub-headings (h2 → 20px, h3 → 40px, h4 → 60px), per-level colours (blue → green → gold), dark code blocks, heading_numbers(true) on top.

cargo run --example custom_css

Example 4 — Full Document

Every option configured: title, Letter landscape paper, header, footer, heading_numbers(true), extra CSS for colours. Quarterly business review theme.

cargo run --example full_document

Example 5 — Corporate Report

A4 report with default CSS as the base, small tweaks via extra_css (indented headings, corporate colours). Header shows company name + report title, footer shows "INTERNAL" + page numbers.

cargo run --example report_style

Example 6 — Images

Markdown document with remote images (Wikimedia Commons). Uses page_load_wait_secs(5) to allow time for image loading. Header/footer, heading numbers, extra CSS for image styling.

cargo run --example with_images

Example 7 — Newsletter

Vibrant editorial style with full custom_css: gradient heading backgrounds, purple/green/gold colours, styled blockquotes. No heading numbers — clean magazine look. Header + footer.

cargo run --example newsletter

Example 8 — Technical Doc (JSON)

Complete API reference built entirely from JSON with paraSequence on every heading. Uses a build_content() helper to construct large JSON arrays. Header/footer, indented headings via extra_css.

cargo run --example technical_doc

Running the Examples

# Clone the repository
git clone github.com/DhvaniBhesaniya/pdfsmith.git
cd pdfsmith

# Run any example
cargo run --example from_markdown
cargo run --example from_json
cargo run --example custom_css
cargo run --example full_document
cargo run --example report_style
cargo run --example with_images
cargo run --example newsletter
cargo run --example technical_doc

# With logging enabled
RUST_LOG=info cargo run --example from_markdown

Error Handling

All generation methods return pdfsmith::Result<Vec<u8>>, which is Result<Vec<u8>, MdocError>.

use pdfsmith::{PdfBuilder, MdocError};

match PdfBuilder::new().from_markdown("# Test") {
    Ok(pdf) => std::fs::write("out.pdf", pdf).unwrap(),
    Err(MdocError::Chrome(msg)) => eprintln!("Chrome error: {}", msg),
    Err(MdocError::Io(err))     => eprintln!("IO error: {}", err),
    Err(MdocError::Json(msg))   => eprintln!("JSON error: {}", msg),
    Err(e)                      => eprintln!("Error: {}", e),
}

Error Variants

Variant When it occurs
MdocError::Chrome(String) Chrome/Chromium not found, failed to launch, navigation timeout, print failure
MdocError::Io(std::io::Error) File read/write errors
MdocError::Json(String) Invalid JSON structure (not an array or missing "content" key)
MdocError::ImageDownload { url, reason } Remote image fetch failed
MdocError::Other(String) Catch-all for uncategorised errors

Architecture

pdfsmith/
├── src/
│   ├── lib.rs              # Public API — PdfBuilder and re-exports
│   ├── config.rs           # PdfConfig, PaperSize, PageMargins, HeaderConfig, FooterConfig, MarkdownOptions
│   ├── css.rs              # DEFAULT_CSS constant
│   ├── error.rs            # MdocError enum and Result type alias
│   ├── parser/
│   │   ├── mod.rs          # Re-exports
│   │   ├── markdown.rs     # Markdown → HTML body (via comrak)
│   │   └── json.rs         # JSON content blocks → Markdown
│   └── renderer/
│       ├── mod.rs          # Re-exports
│       ├── html.rs         # Wraps body HTML in full document with CSS
│       ├── template.rs     # Header/footer template builders
│       └── chrome.rs       # Headless Chrome PDF rendering
├── examples/
│   ├── from_markdown.rs
│   ├── from_json.rs
│   ├── custom_css.rs
│   ├── full_document.rs
│   ├── report_style.rs
│   ├── with_images.rs
│   ├── newsletter.rs
│   └── technical_doc.rs
├── tests/
│   ├── basic.rs
│   └── json.rs
└── Cargo.toml

Pipeline

Markdown string ─┐
                  ├─→ HTML body ─→ Full HTML doc (+ CSS) ─→ Chrome ─→ PDF bytes
JSON blocks ─────┘    (comrak)     (wrap_body_in_html)      (headless)
  1. Input — Markdown text, JSON blocks, or raw HTML.
  2. Parse — Markdown is converted to HTML by comrak. JSON is first converted to Markdown, then through comrak.
  3. Wrap — The HTML body is wrapped in a full <!DOCTYPE html> document with the resolved CSS (default, custom, or default + extra).
  4. Template — Header and footer HTML templates are built from config.
  5. Render — Headless Chrome loads the HTML, applies print options (paper, margins, header, footer), and outputs PDF bytes.
  6. Return — Raw Vec<u8> PDF bytes ready to write to disk or send over HTTP.

Troubleshooting

"Chrome error: Failed to launch Chrome/Chromium"

Chromium is not installed or not in your PATH. See Prerequisites.

PDF is blank

  • Check that your Markdown/JSON input is not empty.
  • Increase page_load_wait_secs if your HTML has images that need time to load.

Header/footer not showing

  • You must call .display_header_footer(true) — it defaults to false.
  • Increase margins.top / margins.bottom to make room for the header/footer.

Custom CSS not applied

  • If you use custom_css, it replaces the default entirely. Make sure your CSS covers everything (body font, headings, tables, code blocks, etc.).
  • If you only want small tweaks, use extra_css instead.

Docker / CI: "No usable sandbox"

Chrome needs a sandbox. In Docker, run with --no-sandbox:

ENV CHROME_FLAGS="--no-sandbox --disable-setuid-sandbox"

Or use a container image that includes Chromium with proper sandbox config.


License

MIT


Table of Contents


Features

Feature Description
Markdown → PDF Convert any Markdown string or file to a styled PDF
JSON → PDF Define document content with typed JSON blocks (headings, paragraphs, tables, code, lists, etc.)
HTML → PDF Pass your own pre-built HTML directly
Custom CSS Replace the default stylesheet entirely, or append extra rules
Paper Sizes A4, Letter, Legal, or any custom width × height
Page Margins Set top, bottom, left, right margins independently (in inches)
Orientation Portrait or landscape
Headers Simple left/center/right text, or full custom HTML
Footers Simple left/center/right text with auto page numbers, or full custom HTML
Heading Numbers Automatic hierarchical numbering (1, 1.1, 1.1.1) via JSON paraSequence or CSS counters
Markdown Extensions Tables, footnotes, strikethrough, task lists, autolinks, superscript, description lists — all toggleable
File Input Read .md or .json files directly from disk
Builder Pattern Fluent, chainable API — configure only what you need

API Overview

  • PdfBuilder — main fluent builder for generating PDFs from Markdown, JSON, or HTML.
  • PdfConfig — full PDF generation configuration.
  • PaperSize, PageMargins, HeaderConfig, FooterConfig, MarkdownOptions — core configuration helpers.
  • DEFAULT_CSS — inspect or reuse the built-in stylesheet.
  • MdocError / Result — error type and result alias used throughout the crate.

Examples Summary

  • Minimal Markdown PDF generation
  • JSON document generation with paraSequence headings
  • Custom CSS styling and theming
  • Full document generation with headers, footers, and page options
  • Report-style documents, newsletters, technical docs, and images

Prerequisites

Install Chromium / Chrome

pdfsmith uses headless Chrome/Chromium under the hood to render HTML to PDF. You must have Chrome or Chromium installed on any machine that generates PDFs.

Ubuntu / Debian

# Option A: via snap (recommended)
sudo snap install chromium

# Option B: via apt
sudo apt update
sudo apt install -y chromium-browser

Fedora / RHEL

sudo dnf install chromium

macOS

# If you have Google Chrome installed, it works automatically.
# Otherwise:
brew install --cask chromium

Windows

Install Google Chrome or Chromium. The headless_chrome crate will auto-detect it.

Docker

If your app runs in a container, add Chromium to your Dockerfile:

RUN apt-get update && apt-get install -y chromium-browser --no-install-recommends && rm -rf /var/lib/apt/lists/*

Why not auto-install? A library crate should never silently install system software. Chromium is a ~200 MB browser — the user should install it explicitly and control which version is on their system.

Verify the Installation

chromium --version
# or
chromium-browser --version
# or
google-chrome --version

If any of these prints a version number, you're ready.


Installation

Add pdfsmith to your Cargo.toml:

[dependencies]
pdfsmith = "0.1.0"

Or via the command line:

cargo add pdfsmith

Optional: Enable Logging

pdfsmith uses the log crate. To see progress messages, add a logger like env_logger:

[dependencies]
env_logger = "0.11"

Then initialise it in your code:

fn main() {
    env_logger::init(); // call once at startup
    // ... your code ...
}

Run with RUST_LOG=info to see output:

RUST_LOG=info cargo run

Quick Start

use pdfsmith::PdfBuilder;

fn main() {
    let pdf_bytes = PdfBuilder::new()
        .from_markdown("# Hello World\n\nThis is my first PDF.")
        .expect("PDF generation failed");

    std::fs::write("hello.pdf", &pdf_bytes).expect("Failed to write file");
    println!("PDF saved! ({} bytes)", pdf_bytes.len());
}

That's it. Three lines of code, zero configuration. The default CSS, A4 paper, 0.75-inch margins, and no header/footer are applied automatically.


Input Sources

From Markdown String

use pdfsmith::PdfBuilder;

let pdf = PdfBuilder::new()
    .from_markdown("# Title\n\nSome **bold** and *italic* text.")
    .unwrap();

All standard Markdown is supported: headings, bold, italic, links, images, code blocks, tables, task lists, blockquotes, horizontal rules, footnotes, strikethrough, and more.

From Markdown File

use pdfsmith::PdfBuilder;

let pdf = PdfBuilder::new()
    .from_markdown_file("docs/report.md")
    .unwrap();

From JSON

use pdfsmith::PdfBuilder;

let json = serde_json::json!([
    { "type": "heading", "paraSequence": "1", "text": "Report Title" },
    { "type": "paragraph", "text": "Some content with **Markdown** formatting." },
    { "type": "heading", "paraSequence": "1.1", "text": "Details" },
    { "type": "paragraph", "text": "The heading level is derived from paraSequence depth." }
]);

let pdf = PdfBuilder::new()
    .from_json(&json)
    .unwrap();

JSON is first converted to Markdown, then rendered through the same pipeline — so all CSS and config applies equally.

From JSON File

use pdfsmith::PdfBuilder;

let pdf = PdfBuilder::new()
    .from_json_file("data/document.json")
    .unwrap();

From Raw HTML

use pdfsmith::PdfBuilder;

let html = r#"
<!DOCTYPE html>
<html>
<head><style>body { font-family: Arial; }</style></head>
<body><h1>Hello</h1><p>From raw HTML.</p></body>
</html>
"#;

let pdf = PdfBuilder::new()
    .from_html(html)
    .unwrap();

When using from_html, the HTML is sent to Chrome as-is — no CSS injection, no Markdown conversion. Header/footer and page options still apply.


JSON Structure Reference

The JSON input is an array of content blocks. Each block has a "type" field that determines how it's rendered.

Two top-level formats are accepted:

// Format A: bare array
[
  { "type": "heading", "paraSequence": "1", "text": "Title" },
  { "type": "paragraph", "text": "Body." },
  { "type": "heading", "paraSequence": "1.1", "text": "Sub-section" },
  { "type": "paragraph", "text": "More body." }
]

// Format B: object with "content" key
{
  "content": [
    { "type": "heading", "paraSequence": "1", "text": "Title" },
    { "type": "paragraph", "text": "Body." }
  ]
}

Block Types

heading

{ "type": "heading", "paraSequence": "1.2", "text": "Section Title" }
Field Type Required Description
paraSequence string No Hierarchical section number (e.g. "1", "1.2", "1.2.1", "A.1"). The heading level (h1–h6) is derived from the depth (number of dot-separated parts). The sequence is prefixed to the heading text automatically.
text string Yes Heading text (Markdown allowed)
level number (1–6) No Explicit heading level — overrides the level derived from paraSequence. If neither paraSequence nor level is given, defaults to h1.

paragraph (alias: text)

{ "type": "paragraph", "text": "Some **bold** text with [links](https://example.com)." }
Field Type Required Description
text string Yes Paragraph content (Markdown allowed)

code

{ "type": "code", "language": "rust", "text": "fn main() {\n    println!(\"Hello\");\n}" }
Field Type Required Description
language string No (default: "") Syntax highlighting hint
text string Yes Code content

list

{ "type": "list", "ordered": true, "items": ["First", "Second", "Third"] }
Field Type Required Description
ordered boolean No (default: false) true for numbered list, false for bullets
items array of strings Yes List items (Markdown allowed in each item)

quote (alias: blockquote)

{ "type": "quote", "text": "To be or not to be." }
Field Type Required Description
text string Yes Quote content (Markdown allowed)

table

{
  "type": "table",
  "headers": ["Name", "Role", "Location"],
  "rows": [
    ["Alice", "Engineer", "London"],
    ["Bob", "Designer", "Berlin"]
  ]
}
Field Type Required Description
headers array of strings Yes Column headers
rows array of arrays No Data rows (each row is an array of strings)

image (alias: img)

{ "type": "image", "src": "https://example.com/photo.png", "alt": "A photo" }
Field Type Required Description
src string Yes Image URL or path
alt string No (default: "") Alt text

divider (alias: hr)

{ "type": "divider" }

Renders a horizontal rule (---). No fields needed.

html (alias: raw)

{ "type": "html", "text": "<div style='color:red;'>Custom HTML</div>" }
Field Type Required Description
text string Yes Raw HTML passed through as-is

Customization Guide

Every aspect of the PDF is configurable through PdfBuilder methods or the PdfConfig struct directly.

CSS Styling

Option 1: Use the built-in default

Do nothing — a clean, neutral, print-friendly stylesheet is applied automatically.

let pdf = PdfBuilder::new()
    .from_markdown("# Uses default CSS")
    .unwrap();

Option 2: Replace the default CSS entirely

let pdf = PdfBuilder::new()
    .custom_css(r#"
        body { font-family: Georgia, serif; font-size: 12pt; color: #333; }
        h1 { color: navy; border-bottom: 2px solid navy; }
        code { background: #f0f0f0; padding: 2px 4px; }
        table { border-collapse: collapse; width: 100%; }
        th, td { border: 1px solid #ccc; padding: 8px; }
    "#)
    .from_markdown("# Fully custom styled")
    .unwrap();

When custom_css is set, the built-in stylesheet is completely replaced. You are in full control.

Option 3: Append extra CSS to the default

let pdf = PdfBuilder::new()
    .extra_css("h1 { color: darkred; } blockquote { border-color: blue; }")
    .from_markdown("# Default + tweaks")
    .unwrap();

extra_css is added after the default stylesheet, so your rules override specific properties while keeping everything else intact.

Option 4: Load CSS from a file

let css = std::fs::read_to_string("styles/my-theme.css").unwrap();
let pdf = PdfBuilder::new()
    .custom_css(css)
    .from_markdown("# Themed")
    .unwrap();

Access the default CSS

The built-in stylesheet is exported as a constant if you want to inspect or extend it programmatically:

use pdfsmith::DEFAULT_CSS;
println!("{}", DEFAULT_CSS);

Paper Size

use pdfsmith::{PdfBuilder, PaperSize};

// Preset sizes
PdfBuilder::new().paper_size(PaperSize::A4);      // 8.27 × 11.69 in (default)
PdfBuilder::new().paper_size(PaperSize::Letter);   // 8.5  × 11    in
PdfBuilder::new().paper_size(PaperSize::Legal);    // 8.5  × 14    in

// Custom size (width × height in inches)
PdfBuilder::new().paper_size(PaperSize::Custom { width: 6.0, height: 9.0 }); // e.g. trade paperback

Page Margins

Margins are specified in inches:

use pdfsmith::{PdfBuilder, PageMargins};

let pdf = PdfBuilder::new()
    .margins(PageMargins {
        top: 1.0,
        bottom: 1.0,
        left: 0.5,
        right: 0.5,
    })
    .from_markdown("# Custom margins")
    .unwrap();

Default margins are 0.75 inches on all sides.

Orientation

let pdf = PdfBuilder::new()
    .landscape(true)   // landscape
    .from_markdown("# Wide layout")
    .unwrap();

Default is false (portrait).

Headers

Headers appear on every page. Three approaches:

Simple left / center / right

use pdfsmith::{PdfBuilder, HeaderConfig};

let pdf = PdfBuilder::new()
    .display_header_footer(true)
    .header(HeaderConfig {
        left: Some("Company Name".into()),
        center: Some("Document Title".into()),
        right: Some("2026-04-17".into()),
        font_size: Some("9px".into()),   // optional, default: "9px"
        color: Some("#333".into()),       // optional, default: "#555"
        ..Default::default()
    })
    .from_markdown("# With header")
    .unwrap();

Full custom HTML

use pdfsmith::{PdfBuilder, HeaderConfig};

let pdf = PdfBuilder::new()
    .display_header_footer(true)
    .header(HeaderConfig {
        custom_html: Some(r#"
            <div style="width:100%; text-align:center; font-size:10px; color:#999; padding:4px;">
                My Custom Header — Page <span class="pageNumber"></span>
            </div>
        "#.into()),
        ..Default::default()
    })
    .from_markdown("# With custom header HTML")
    .unwrap();

No header (default)

let pdf = PdfBuilder::new()
    .from_markdown("# No header")   // display_header_footer defaults to false
    .unwrap();

Footers

Footers work exactly like headers.

Default footer (when enabled)

When display_header_footer(true) is set and no footer fields are configured, a simple centred page number is shown: Page 1 / 5.

let pdf = PdfBuilder::new()
    .display_header_footer(true)
    .from_markdown("# With page numbers")
    .unwrap();

Custom left / center / right

use pdfsmith::{PdfBuilder, FooterConfig};

let pdf = PdfBuilder::new()
    .display_header_footer(true)
    .footer(FooterConfig {
        left: Some("CONFIDENTIAL".into()),
        center: Some("Internal Use Only".into()),
        right: Some(r#"Page <span class="pageNumber"></span> of <span class="totalPages"></span>"#.into()),
        font_size: Some("8px".into()),
        color: Some("#888".into()),
        ..Default::default()
    })
    .from_markdown("# With footer")
    .unwrap();

Full custom HTML footer

use pdfsmith::{PdfBuilder, FooterConfig};

let pdf = PdfBuilder::new()
    .display_header_footer(true)
    .footer(FooterConfig {
        custom_html: Some(r#"
            <div style="width:100%; display:flex; justify-content:space-between; padding:4px 0.75in; font-size:8px; color:#666; font-family:Arial;">
                <span>© 2026 Acme Corp</span>
                <span>Page <span class="pageNumber"></span> / <span class="totalPages"></span></span>
            </div>
        "#.into()),
        ..Default::default()
    })
    .from_markdown("# Custom footer HTML")
    .unwrap();

Chrome Page Number Placeholders

Inside any header/footer text or custom_html, use these Chrome built-in placeholders:

Placeholder Renders as
<span class="pageNumber"></span> Current page number
<span class="totalPages"></span> Total page count
<span class="title"></span> Document title
<span class="url"></span> Page URL
<span class="date"></span> Current date

Markdown Extensions

All extensions are enabled by default. Toggle them individually:

use pdfsmith::{PdfBuilder, MarkdownOptions};

let pdf = PdfBuilder::new()
    .markdown_options(MarkdownOptions {
        tables: true,              // | col1 | col2 |
        footnotes: true,           // [^1]: footnote text
        strikethrough: true,       // ~~deleted~~
        tasklist: true,            // - [x] done
        autolink: true,            // https://auto.link
        superscript: true,         // 2^10
        description_lists: true,   // term\n: definition
        unsafe_html: true,         // pass-through raw <html> tags
    })
    .from_markdown("# All extensions on")
    .unwrap();

To disable a specific extension:

let pdf = PdfBuilder::new()
    .markdown_options(MarkdownOptions {
        tasklist: false,       // disable task lists
        superscript: false,    // disable superscript
        ..Default::default()   // keep everything else on
    })
    .from_markdown("# Some extensions off")
    .unwrap();

Heading Numbers

Automatic hierarchical heading numbers like 1, 1.1, 1.1.1, 2, 2.1.

There are two approaches — use whichever fits your workflow:

Approach 1: JSON auto_number (JSON input only)

Set "auto_number": true in the top-level JSON object. Numbers are generated automatically from the heading level fields:

use pdfsmith::PdfBuilder;

let json = serde_json::json!({
    "auto_number": true,
    "content": [
        { "type": "heading", "level": 1, "text": "Introduction" },
        { "type": "paragraph", "text": "Intro body." },
        { "type": "heading", "level": 2, "text": "Background" },
        { "type": "heading", "level": 2, "text": "Scope" },
        { "type": "heading", "level": 3, "text": "Details" },
        { "type": "heading", "level": 1, "text": "Conclusion" }
    ]
});

let pdf = PdfBuilder::new()
    .from_json(&json)
    .unwrap();

This produces headings:

1 Introduction
1.1 Background
1.2 Scope
1.2.1 Details
2 Conclusion

Manual number field

Any heading block can carry a "number" field for manual control. This takes priority over auto-numbering:

[
  { "type": "heading", "level": 1, "number": "A", "text": "Appendix" },
  { "type": "heading", "level": 2, "number": "A.1", "text": "First Section" },
  { "type": "heading", "level": 2, "number": "A.2", "text": "Second Section" }
]

Approach 2: CSS-based heading_numbers (works with ALL input types)

Enable .heading_numbers(true) on the builder. This injects CSS counter rules that automatically number every heading — works for Markdown, JSON, and raw HTML:

use pdfsmith::PdfBuilder;

let pdf = PdfBuilder::new()
    .heading_numbers(true)
    .from_markdown("# Intro\n\n## Part A\n\n## Part B\n\n### Detail\n\n# Summary")
    .unwrap();

Rendered headings look like:

1. Intro
1.1 Part A
1.2 Part B
1.2.1 Detail
2. Summary

The CSS approach has no dependency on JSON — it works everywhere.

Which should I use?

  • Use auto_number in JSON when you want the numbers baked into the Markdown text (good for exports, copy-paste).
  • Use heading_numbers(true) on the builder when you want visual numbering via CSS (works with any input, numbers don’t appear in the underlying text).

Chrome Options

Fine-tune the headless Chrome rendering:

let pdf = PdfBuilder::new()
    .chrome_window_size(1920, 1080)   // browser viewport (default: 1280×900)
    .page_load_wait_secs(5)           // wait for images/JS (default: 2)
    .print_background(true)           // render background colours (default: true)
    .from_markdown("# Chrome settings")
    .unwrap();

Full Configuration Reference

You can also set everything at once via the PdfConfig struct:

use pdfsmith::{PdfBuilder, PdfConfig, PaperSize, PageMargins, HeaderConfig, FooterConfig, MarkdownOptions};

let config = PdfConfig {
    title: "My Document".into(),
    custom_css: None,                                  // use built-in default
    extra_css: Some("h1 { color: navy; }".into()),     // tweak headings
    paper_size: PaperSize::A4,
    margins: PageMargins { top: 1.0, bottom: 1.0, left: 0.75, right: 0.75 },
    landscape: false,
    display_header_footer: true,
    header: HeaderConfig {
        left: Some("Acme Corp".into()),
        center: Some("Report".into()),
        right: Some("April 2026".into()),
        font_size: Some("9px".into()),
        color: Some("#333".into()),
        custom_html: None,
    },
    footer: FooterConfig {
        left: None,
        center: None,
        right: Some(r#"Page <span class="pageNumber"></span>"#.into()),
        font_size: Some("8px".into()),
        color: Some("#888".into()),
        custom_html: None,
    },
    print_background: true,
    heading_numbers: false,
    markdown_options: MarkdownOptions::default(),
    chrome_window_size: (1280, 900),
    page_load_wait_secs: 5, // use 5 when any image is available in markdown or json.
};

let pdf = PdfBuilder::with_config(config)
    .from_markdown("# Hello from PdfConfig")
    .unwrap();

PdfConfig Fields

Field Type Default Description
title String "" Document title (used in <title> tag and default header)
custom_css Option<String> None Replaces the entire default stylesheet
extra_css Option<String> None Appended after the base stylesheet
paper_size PaperSize A4 Paper dimensions
margins PageMargins 0.75 all Page margins in inches
landscape bool false Landscape orientation
display_header_footer bool false Show header and footer
header HeaderConfig empty Header configuration
footer FooterConfig empty Footer configuration
print_background bool true Print background colours/images
markdown_options MarkdownOptions all true Markdown extension toggles
heading_numbers bool false CSS-based hierarchical heading numbers
chrome_window_size (u32, u32) (1280, 900) Headless Chrome viewport
page_load_wait_secs u64 5 Seconds to wait after page load ( Usually helpfull to load images perfectly)

Examples

The repository includes eight runnable examples in the examples/ directory, each producing a 2+ page PDF.

Example 1 — Minimal Markdown

Markdown string → PDF with heading_numbers(true). Default CSS, no header/footer. Covers tables, code blocks, lists, blockquotes.

cargo run --example from_markdown

Example 2 — JSON with paraSequence

Structured JSON content blocks with "paraSequence" on every heading for hierarchical numbering. The heading level (h1–h6) is derived automatically from the sequence depth.

// examples/from_json.rs  (abbreviated)
let json = serde_json::json!([
    { "type": "heading", "paraSequence": "1",     "text": "Project Status Report" },
    { "type": "paragraph", "text": "This report summarises the current state." },
    { "type": "heading", "paraSequence": "1.1",   "text": "Timeline" },
    { "type": "table", "headers": ["Phase","Status","Due"], "rows": [["Design","Complete","2026-01-15"]] },
    { "type": "heading", "paraSequence": "1.2",   "text": "Key Highlights" },
    { "type": "heading", "paraSequence": "1.2.1", "text": "Performance Details" },
    { "type": "heading", "paraSequence": "2",     "text": "Architecture" },
    // ... more blocks ...
]);

let pdf = PdfBuilder::new()
    .title("Project Report")
    .from_json(&json)
    .expect("Failed to generate PDF");

Output headings:

1 Project Status Report       (depth 1 → h1)
  1.1 Timeline                (depth 2 → h2)
  1.2 Key Highlights          (depth 2 → h2)
    1.2.1 Performance Details  (depth 3 → h3)
2 Architecture                (depth 1 → h1)
cargo run --example from_json

Example 3 — Custom CSS

Replaces the entire default stylesheet. Indented sub-headings (h2 → 20px, h3 → 40px, h4 → 60px), per-level colours (blue → green → gold), dark code blocks, heading_numbers(true) on top.

cargo run --example custom_css

Example 4 — Full Document

Every option configured: title, Letter landscape paper, header, footer, heading_numbers(true), extra CSS for colours. Quarterly business review theme.

cargo run --example full_document

Example 5 — Corporate Report

A4 report with default CSS as the base, small tweaks via extra_css (indented headings, corporate colours). Header shows company name + report title, footer shows "INTERNAL" + page numbers.

cargo run --example report_style

Example 6 — Images

Markdown document with remote images (Wikimedia Commons). Uses page_load_wait_secs(5) to allow time for image loading. Header/footer, heading numbers, extra CSS for image styling.

cargo run --example with_images

Example 7 — Newsletter

Vibrant editorial style with full custom_css: gradient heading backgrounds, purple/green/gold colours, styled blockquotes. No heading numbers — clean magazine look. Header + footer.

cargo run --example newsletter

Example 8 — Technical Doc (JSON)

Complete API reference built entirely from JSON with paraSequence on every heading. Uses a build_content() helper to construct large JSON arrays. Header/footer, indented headings via extra_css.

cargo run --example technical_doc

Running the Examples

# Clone the repository
git clone github.com/DhvaniBhesaniya/pdfsmith.git
cd pdfsmith

# Run any example
cargo run --example from_markdown
cargo run --example from_json
cargo run --example custom_css
cargo run --example full_document
cargo run --example report_style
cargo run --example with_images
cargo run --example newsletter
cargo run --example technical_doc

# With logging enabled
RUST_LOG=info cargo run --example from_markdown

Error Handling

All generation methods return pdfsmith::Result<Vec<u8>>, which is Result<Vec<u8>, MdocError>.

use pdfsmith::{PdfBuilder, MdocError};

match PdfBuilder::new().from_markdown("# Test") {
    Ok(pdf) => std::fs::write("out.pdf", pdf).unwrap(),
    Err(MdocError::Chrome(msg)) => eprintln!("Chrome error: {}", msg),
    Err(MdocError::Io(err))     => eprintln!("IO error: {}", err),
    Err(MdocError::Json(msg))   => eprintln!("JSON error: {}", msg),
    Err(e)                      => eprintln!("Error: {}", e),
}

Error Variants

Variant When it occurs
MdocError::Chrome(String) Chrome/Chromium not found, failed to launch, navigation timeout, print failure
MdocError::Io(std::io::Error) File read/write errors
MdocError::Json(String) Invalid JSON structure (not an array or missing "content" key)
MdocError::ImageDownload { url, reason } Remote image fetch failed
MdocError::Other(String) Catch-all for uncategorised errors

Architecture

pdfsmith/
├── src/
│   ├── lib.rs              # Public API — PdfBuilder and re-exports
│   ├── config.rs           # PdfConfig, PaperSize, PageMargins, HeaderConfig, FooterConfig, MarkdownOptions
│   ├── css.rs              # DEFAULT_CSS constant
│   ├── error.rs            # MdocError enum and Result type alias
│   ├── parser/
│   │   ├── mod.rs          # Re-exports
│   │   ├── markdown.rs     # Markdown → HTML body (via comrak)
│   │   └── json.rs         # JSON content blocks → Markdown
│   └── renderer/
│       ├── mod.rs          # Re-exports
│       ├── html.rs         # Wraps body HTML in full document with CSS
│       ├── template.rs     # Header/footer template builders
│       └── chrome.rs       # Headless Chrome PDF rendering
├── examples/
│   ├── from_markdown.rs
│   ├── from_json.rs
│   ├── custom_css.rs
│   ├── full_document.rs
│   ├── report_style.rs
│   ├── with_images.rs
│   ├── newsletter.rs
│   └── technical_doc.rs
├── tests/
│   ├── basic.rs
│   └── json.rs
└── Cargo.toml

Pipeline

Markdown string ─┐
                  ├─→ HTML body ─→ Full HTML doc (+ CSS) ─→ Chrome ─→ PDF bytes
JSON blocks ─────┘    (comrak)     (wrap_body_in_html)      (headless)
  1. Input — Markdown text, JSON blocks, or raw HTML.
  2. Parse — Markdown is converted to HTML by comrak. JSON is first converted to Markdown, then through comrak.
  3. Wrap — The HTML body is wrapped in a full <!DOCTYPE html> document with the resolved CSS (default, custom, or default + extra).
  4. Template — Header and footer HTML templates are built from config.
  5. Render — Headless Chrome loads the HTML, applies print options (paper, margins, header, footer), and outputs PDF bytes.
  6. Return — Raw Vec<u8> PDF bytes ready to write to disk or send over HTTP.

Troubleshooting

"Chrome error: Failed to launch Chrome/Chromium"

Chromium is not installed or not in your PATH. See Prerequisites.

PDF is blank

  • Check that your Markdown/JSON input is not empty.
  • Increase page_load_wait_secs if your HTML has images that need time to load.

Header/footer not showing

  • You must call .display_header_footer(true) — it defaults to false.
  • Increase margins.top / margins.bottom to make room for the header/footer.

Custom CSS not applied

  • If you use custom_css, it replaces the default entirely. Make sure your CSS covers everything (body font, headings, tables, code blocks, etc.).
  • If you only want small tweaks, use extra_css instead.

Docker / CI: "No usable sandbox"

Chrome needs a sandbox. In Docker, run with --no-sandbox:

ENV CHROME_FLAGS="--no-sandbox --disable-setuid-sandbox"

Or use a container image that includes Chromium with proper sandbox config.


License

MIT