Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

Architecture

Overview of printwell’s internal architecture and design decisions.

Crate Structure

printwell-sys

Low-level FFI bindings to the native C++ library.

crates/printwell-sys/
├── build.rs          # Links libprintwell_native.so
├── src/
│   ├── lib.rs        # FFI declarations
│   └── bindings.rs   # Generated bindgen output

Key responsibilities:

  • Links the native library at compile time
  • Declares extern "C" functions
  • Defines C-compatible types

printwell-core

Safe Rust wrapper around printwell-sys for HTML to PDF conversion.

crates/printwell-core/
├── src/
│   ├── lib.rs        # Public API
│   ├── converter.rs  # Converter struct
│   ├── options.rs    # PdfOptions
│   ├── pool.rs       # ConverterPool
│   └── resources.rs  # Resource management

Key types:

  • Converter - Single-threaded converter
  • ConverterPool - Thread-safe pool of converters
  • PdfOptions - Conversion settings
  • PdfResult - Conversion result with PDF bytes

printwell

PDF post-processing features (no native dependency).

crates/printwell/
├── src/
│   ├── lib.rs          # Module exports
│   ├── watermark.rs    # Watermarks
│   ├── bookmarks.rs    # Bookmarks/outlines
│   ├── annotations.rs  # PDF annotations
│   ├── encrypt.rs      # PDF encryption
│   ├── signing.rs      # Digital signatures
│   ├── forms.rs        # Form fields
│   ├── pdfa.rs         # PDF/A validation
│   ├── pdfua.rs        # PDF/UA validation
│   └── crypto/         # Cryptographic utilities

printwell

Umbrella crate that re-exports everything.

#![allow(unused)]
fn main() {
pub use printwell_core::*;
pub use printwell::*;
}

printwell-cli

Command-line interface.

crates/printwell-cli/
├── src/
│   ├── main.rs       # Entry point
│   └── cli.rs        # Clap command definitions

Native Library

The native library (libprintwell_native.so) contains:

Chromium Components

  • Blink - Web rendering engine
  • Skia - 2D graphics library
  • PDFium - PDF rendering/generation

Build Configuration

Minimal Chromium build with disabled features:

# From docker/gn_args/common.gn
blink_enable_javascript = false
enable_pdf = false
enable_extensions = false
media_use_ffmpeg = false
use_ozone = true
ozone_platform_headless = true

C++ Interface

// cpp/src/printwell.h (simplified)
extern "C" {
    PrintwellConverter* printwell_converter_new();
    void printwell_converter_free(PrintwellConverter* converter);

    PrintwellResult printwell_convert_html(
        PrintwellConverter* converter,
        const char* html,
        size_t html_len,
        const PrintwellOptions* options,
        uint8_t** pdf_data,
        size_t* pdf_len
    );
}

Memory Management

Converter Lifecycle

#![allow(unused)]
fn main() {
// Converter owns native resources
let converter = Converter::new()?;

// Convert HTML to PDF
let result = converter.convert_html(html, options)?;

// Converter dropped here, native resources freed
}

PDF Data Ownership

#![allow(unused)]
fn main() {
// PdfResult owns the PDF bytes
let result = converter.convert_html(html, options)?;

// Get owned Vec<u8>
let pdf_data: Vec<u8> = result.into_bytes();
}

Thread Safety

Converter (Not Thread-Safe)

Converter is !Send and !Sync. Each converter must stay on one thread.

ConverterPool (Thread-Safe)

ConverterPool is Send + Sync. It manages thread-local converters:

#![allow(unused)]
fn main() {
// Pool can be shared across threads
let pool = Arc::new(ConverterPool::new(4)?);

// Each thread gets its own converter
let handle = pool.clone();
thread::spawn(move || {
    let result = handle.convert_html(html, options)?;
});
}

Error Handling

All errors use the PrintwellError type:

#![allow(unused)]
fn main() {
pub enum PrintwellError {
    // Conversion errors
    Conversion(String),
    InvalidHtml(String),
    Timeout,

    // Resource errors
    ResourceNotFound(String),
    IoError(std::io::Error),

    // Feature errors
    Encryption(String),
    Signing(String),
    Validation(String),
}
}

Feature Flags

Cargo features control optional functionality:

FeatureDescriptionDefault
signingDigital signaturesYes
encryptionPDF encryptionYes
formsForm fieldsYes
pdfaPDF/A validationYes
pdfuaPDF/UA validationYes

Bindings Architecture

Node.js (NAPI-RS)

bindings/node/
├── src/lib.rs        # NAPI bindings
├── index.d.ts        # TypeScript definitions
└── package.json

Uses NAPI-RS for zero-copy data transfer.

Python (PyO3)

bindings/python/
├── src/lib.rs        # PyO3 bindings
├── printwell/
│   ├── __init__.py   # Python module
│   └── __init__.pyi  # Type stubs
└── pyproject.toml

Uses PyO3 with maturin for building.

Shared Types

bindings/shared/
└── src/lib.rs        # Common type definitions

Shared between Node.js and Python bindings.

Performance Considerations

Converter Reuse

Creating a converter is expensive. Reuse converters:

#![allow(unused)]
fn main() {
// Good: reuse converter
let converter = Converter::new()?;
for html in documents {
    converter.convert_html(html, options)?;
}

// Bad: create new converter each time
for html in documents {
    let converter = Converter::new()?;  // Expensive!
    converter.convert_html(html, options)?;
}
}

Pool Sizing

Pool size should match available CPU cores:

#![allow(unused)]
fn main() {
let pool = ConverterPool::new(num_cpus::get())?;
}

Memory Usage

Each converter uses ~50MB of memory. Plan accordingly:

  • 4 converters ≈ 200MB
  • 8 converters ≈ 400MB