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 converterConverterPool- Thread-safe pool of convertersPdfOptions- Conversion settingsPdfResult- 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:
| Feature | Description | Default |
|---|---|---|
signing | Digital signatures | Yes |
encryption | PDF encryption | Yes |
forms | Form fields | Yes |
pdfa | PDF/A validation | Yes |
pdfua | PDF/UA validation | Yes |
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