Skip to main content

Crate pdfsmith

Crate pdfsmith 

Source
Expand description

§pdfsmith

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

§Features

FeatureDescription
Markdown → PDFConvert any Markdown string or file to a styled PDF
JSON → PDFDefine document content with typed JSON blocks (headings, paragraphs, tables, code, lists, etc.)
HTML → PDFPass your own pre-built HTML directly
Custom CSSReplace the default stylesheet entirely, or append extra rules
Paper SizesA4, Letter, Legal, or any custom width × height
Page MarginsSet top, bottom, left, right margins independently (in inches)
OrientationPortrait or landscape
HeadersSimple left/center/right text, or full custom HTML
FootersSimple left/center/right text with auto page numbers, or full custom HTML
Heading NumbersAutomatic hierarchical numbering (1, 1.1, 1.1.1) via JSON paraSequence or CSS counters
Markdown ExtensionsTables, footnotes, strikethrough, task lists, autolinks, superscript, description lists — all toggleable
File InputRead .md or .json files directly from disk
Builder PatternFluent, chainable API — configure only what you need

§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 used as-is — no Markdown conversion, no CSS injection. Header/footer and Chrome 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" }
FieldTypeRequiredDescription
paraSequencestringNoHierarchical 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.
textstringYesHeading text (Markdown allowed)
levelnumber (1–6)NoExplicit 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)." }
FieldTypeRequiredDescription
textstringYesParagraph content (Markdown allowed)
§code
{ "type": "code", "language": "rust", "text": "fn main() {\n    println!(\"Hello\");\n}" }
FieldTypeRequiredDescription
languagestringNo (default: "")Syntax highlighting hint
textstringYesCode content
§list
{ "type": "list", "ordered": true, "items": ["First", "Second", "Third"] }
FieldTypeRequiredDescription
orderedbooleanNo (default: false)true for numbered list, false for bullets
itemsarray of stringsYesList items (Markdown allowed in each item)
§quote (alias: blockquote)
{ "type": "quote", "text": "To be or not to be." }
FieldTypeRequiredDescription
textstringYesQuote content (Markdown allowed)
§table
{
  "type": "table",
  "headers": ["Name", "Role", "Location"],
  "rows": [
    ["Alice", "Engineer", "London"],
    ["Bob", "Designer", "Berlin"]
  ]
}
FieldTypeRequiredDescription
headersarray of stringsYesColumn headers
rowsarray of arraysNoData rows (each row is an array of strings)
§image (alias: img)
{ "type": "image", "src": "https://example.com/photo.png", "alt": "A photo" }
FieldTypeRequiredDescription
srcstringYesImage URL or path
altstringNo (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>" }
FieldTypeRequiredDescription
textstringYesRaw HTML passed through as-is

§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

§See working examples

You can browse the full example source files on GitHub:


§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

VariantWhen 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

Re-exports§

pub use config::FooterConfig;
pub use config::HeaderConfig;
pub use config::MarkdownOptions;
pub use config::PageMargins;
pub use config::PaperSize;
pub use config::PdfConfig;
pub use css::DEFAULT_CSS;
pub use error::MdocError;
pub use error::Result;

Modules§

config
css
error
parser
renderer

Structs§

PdfBuilder
Fluent builder for generating PDFs.