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

printwell

Early Development - This project is in active early development. APIs may change, and some features are incomplete. Some commercial features may become free in future releases. Feedback and contributions welcome!

High-performance HTML to PDF conversion powered by Chromium’s rendering engine.

printwell is a library and CLI tool that converts HTML to PDF using Chromium’s Blink rendering engine, compiled as a native library without the browser chrome. This gives you pixel-perfect PDF rendering with full CSS support, web fonts, and modern layout features.

Key Features

  • Pixel-perfect rendering - Uses Chromium’s Blink engine for accurate HTML/CSS rendering
  • No browser required - Compiled as a native library, no Chrome/Chromium installation needed
  • Multiple language bindings - Use from Rust, Node.js, or Python
  • PDF post-processing - Add watermarks, bookmarks, annotations, forms, and more
  • Digital signatures - Sign PDFs with PAdES-compliant signatures
  • Encryption - Password-protect PDFs with AES-256 encryption
  • PDF/A compliance - Generate archival-ready PDFs
  • PDF/UA accessibility - Create accessible PDFs for screen readers
  • Small binary size - ~50MB native library (no V8/JavaScript engine)

Quick Example

CLI

# Convert HTML file to PDF
printwell convert input.html -o output.pdf

# Convert URL to PDF
printwell convert https://example.com -o example.pdf

# Add watermark
printwell watermark input.pdf -o output.pdf --text "CONFIDENTIAL"

# Sign PDF
printwell sign input.pdf -o signed.pdf --cert certificate.p12 --password secret

Rust

#![allow(unused)]
fn main() {
use printwell::{Converter, PdfOptions};

let converter = Converter::new()?;
let result = converter.html_to_pdf(
    "<h1>Hello, World!</h1>",
    None,
    Some(PdfOptions::default()),
)?;
result.write_to_file("output.pdf")?;
}

Node.js

import { Converter } from 'printwell';

const converter = new Converter();
const result = await converter.htmlToPdf('<h1>Hello, World!</h1>');
await result.writeToFile('output.pdf');

Python

from printwell import Converter

converter = Converter()
result = converter.html_to_pdf("<h1>Hello, World!</h1>")
result.write_to_file("output.pdf")

Next Steps

Installation

printwell can be installed as a CLI tool, or as a library for Rust, Node.js, or Python.

CLI Installation

From Pre-built Binaries

Download the latest release for your platform from the releases page.

# Linux (x86_64)
curl -LO https://github.com/printwell-dev/core/releases/latest/download/printwell-linux-x64.tar.gz
tar xzf printwell-linux-x64.tar.gz
sudo mv printwell /usr/local/bin/

# Verify installation
printwell --version

From Source

See Building from Source for detailed instructions.

Rust Library

Add printwell to your Cargo.toml:

[dependencies]
printwell = "0.1"

For specific features, use feature flags:

[dependencies]
printwell = { version = "0.1", features = ["signing", "forms", "pdfa", "pdfua"] }

Available Features

FeatureDescriptionLicense
defaultCore conversion only (no PDF post-processing)AGPL
watermarkAdd text and image watermarksAGPL
bookmarksPDF outline/bookmark managementAGPL
annotationsPDF annotation supportAGPL
encryptPDF encryption with password protectionCommercial
signingDigital signature support (PAdES)Commercial
timestampTimestamp server support for signaturesCommercial
formsPDF form field creation and validationCommercial
pdfaPDF/A archival complianceCommercial
pdfuaPDF/UA accessibility complianceCommercial
pdf-fullAll PDF post-processing featuresCommercial

Note: The default feature includes only core HTML-to-PDF conversion. Features marked Commercial require a license - see printwell.dev/pricing.

Node.js Library

npm install printwell

Or with yarn:

yarn add printwell

Supported Platforms

  • Linux x64 (glibc 2.17+)
  • macOS x64 (10.15+)
  • macOS ARM64 (11.0+)
  • Windows x64 (Windows 10+)

Python Library

pip install printwell

Or with uv:

uv pip install printwell

Supported Platforms

  • Linux x64 (glibc 2.17+, Python 3.9+)
  • macOS x64 (10.15+, Python 3.9+)
  • macOS ARM64 (11.0+, Python 3.9+)
  • Windows x64 (Windows 10+, Python 3.9+)

Docker

For containerized deployments:

FROM ghcr.io/printwell-dev/core:latest

# Your application
COPY . /app
WORKDIR /app

# Use the CLI
RUN printwell convert input.html -o output.pdf

Or use the library in your container:

FROM python:3.11-slim

RUN pip install printwell

COPY app.py /app/
CMD ["python", "/app/app.py"]

Verifying Installation

CLI

printwell --version
printwell info  # Show renderer information

Rust

use printwell::Converter;

fn main() -> Result<(), Box<dyn std::error::Error>> {
    let converter = Converter::new()?;
    let info = converter.info();
    println!("Chromium: {}", info.chromium_version);
    println!("Skia: {}", info.skia_version);
    Ok(())
}

Node.js

import { Converter } from 'printwell';

const converter = new Converter();
const info = converter.info();
console.log(`Chromium: ${info.chromiumVersion}`);
console.log(`Skia: ${info.skiaVersion}`);

Python

from printwell import Converter

converter = Converter()
info = converter.info()
print(f"Chromium: {info.chromium_version}")
print(f"Skia: {info.skia_version}")

Next Steps

Quick Start

This guide will have you converting HTML to PDF in under 5 minutes.

Basic Conversion

From HTML String

The simplest way to create a PDF is from an HTML string:

{{#tabs }} {{#tab name=“CLI” }}

echo "<h1>Hello, World!</h1>" | printwell convert - -o hello.pdf

{{#endtab }} {{#tab name=“Rust” }}

use printwell::{Converter, PdfOptions};

fn main() -> printwell::Result<()> {
    let converter = Converter::new()?;

    let html = r#"
        <!DOCTYPE html>
        <html>
        <head>
            <style>
                body { font-family: Arial, sans-serif; padding: 40px; }
                h1 { color: #333; }
            </style>
        </head>
        <body>
            <h1>Hello, World!</h1>
            <p>This PDF was generated with printwell.</p>
        </body>
        </html>
    "#;

    let result = converter.html_to_pdf(html, None, None)?;
    result.write_to_file("hello.pdf")?;

    println!("Created hello.pdf ({} pages)", result.page_count);
    Ok(())
}

{{#endtab }} {{#tab name=“Node.js” }}

import { Converter } from 'printwell';

const converter = new Converter();

const html = `
<!DOCTYPE html>
<html>
<head>
    <style>
        body { font-family: Arial, sans-serif; padding: 40px; }
        h1 { color: #333; }
    </style>
</head>
<body>
    <h1>Hello, World!</h1>
    <p>This PDF was generated with printwell.</p>
</body>
</html>
`;

const result = await converter.htmlToPdf(html);
await result.writeToFile('hello.pdf');

console.log(`Created hello.pdf (${result.pageCount} pages)`);

{{#endtab }} {{#tab name=“Python” }}

from printwell import Converter

converter = Converter()

html = """
<!DOCTYPE html>
<html>
<head>
    <style>
        body { font-family: Arial, sans-serif; padding: 40px; }
        h1 { color: #333; }
    </style>
</head>
<body>
    <h1>Hello, World!</h1>
    <p>This PDF was generated with printwell.</p>
</body>
</html>
"""

result = converter.html_to_pdf(html)
result.write_to_file("hello.pdf")

print(f"Created hello.pdf ({result.page_count} pages)")

{{#endtab }} {{#endtabs }}

From URL

Convert any webpage to PDF:

{{#tabs }} {{#tab name=“CLI” }}

printwell convert https://example.com -o example.pdf

{{#endtab }} {{#tab name=“Rust” }}

#![allow(unused)]
fn main() {
let result = converter.url_to_pdf("https://example.com", None, None)?;
result.write_to_file("example.pdf")?;
}

{{#endtab }} {{#tab name=“Node.js” }}

const result = await converter.urlToPdf('https://example.com');
await result.writeToFile('example.pdf');

{{#endtab }} {{#tab name=“Python” }}

result = converter.url_to_pdf("https://example.com")
result.write_to_file("example.pdf")

{{#endtab }} {{#endtabs }}

From File

Convert a local HTML file:

{{#tabs }} {{#tab name=“CLI” }}

printwell convert report.html -o report.pdf

{{#endtab }} {{#tab name=“Rust” }}

#![allow(unused)]
fn main() {
let html = std::fs::read_to_string("report.html")?;
let result = converter.html_to_pdf(&html, None, None)?;
result.write_to_file("report.pdf")?;
}

{{#endtab }} {{#tab name=“Node.js” }}

import { readFile } from 'fs/promises';

const html = await readFile('report.html', 'utf-8');
const result = await converter.htmlToPdf(html);
await result.writeToFile('report.pdf');

{{#endtab }} {{#tab name=“Python” }}

with open("report.html") as f:
    html = f.read()

result = converter.html_to_pdf(html)
result.write_to_file("report.pdf")

{{#endtab }} {{#endtabs }}

Customizing Output

Page Size and Margins

{{#tabs }} {{#tab name=“CLI” }}

printwell convert input.html -o output.pdf \
    --page-size A4 \
    --orientation landscape \
    --margin-top 20 \
    --margin-bottom 20

{{#endtab }} {{#tab name=“Rust” }}

#![allow(unused)]
fn main() {
use printwell::{PdfOptions, PageSize, Orientation, Margins};

let options = PdfOptions::builder()
    .page_size(PageSize::A4)
    .orientation(Orientation::Landscape)
    .margins(Margins::new(20.0, 10.0, 20.0, 10.0))
    .build();

let result = converter.html_to_pdf(html, None, Some(options))?;
}

{{#endtab }} {{#tab name=“Node.js” }}

const result = await converter.htmlToPdf(html, null, {
    pageSize: 'A4',
    orientation: 'Landscape',
    margins: { top: 20, right: 10, bottom: 20, left: 10 }
});

{{#endtab }} {{#tab name=“Python” }}

from printwell import PdfOptions, PageSize, Orientation, Margins

options = PdfOptions(
    page_size=PageSize.A4,
    orientation=Orientation.Landscape,
    margins=Margins(top=20, right=10, bottom=20, left=10)
)

result = converter.html_to_pdf(html, pdf_options=options)

{{#endtab }} {{#endtabs }}

Headers and Footers

{{#tabs }} {{#tab name=“CLI” }}

printwell convert input.html -o output.pdf \
    --header-template '<div style="font-size:10px;text-align:center;width:100%">My Document</div>' \
    --footer-template '<div style="font-size:10px;text-align:center;width:100%">Page <span class="pageNumber"></span> of <span class="totalPages"></span></div>'

{{#endtab }} {{#tab name=“Rust” }}

#![allow(unused)]
fn main() {
let options = PdfOptions::builder()
    .header_template("<div style=\"font-size:10px;text-align:center;width:100%\">My Document</div>")
    .footer_template("<div style=\"font-size:10px;text-align:center;width:100%\">Page <span class=\"pageNumber\"></span> of <span class=\"totalPages\"></span></div>")
    .build();
}

{{#endtab }} {{#tab name=“Node.js” }}

const result = await converter.htmlToPdf(html, null, {
    headerTemplate: '<div style="font-size:10px;text-align:center;width:100%">My Document</div>',
    footerTemplate: '<div style="font-size:10px;text-align:center;width:100%">Page <span class="pageNumber"></span> of <span class="totalPages"></span></div>'
});

{{#endtab }} {{#tab name=“Python” }}

options = PdfOptions(
    header_template='<div style="font-size:10px;text-align:center;width:100%">My Document</div>',
    footer_template='<div style="font-size:10px;text-align:center;width:100%">Page <span class="pageNumber"></span> of <span class="totalPages"></span></div>'
)

{{#endtab }} {{#endtabs }}

Adding Post-Processing

Watermark

{{#tabs }} {{#tab name=“CLI” }}

printwell watermark document.pdf -o watermarked.pdf \
    --text "CONFIDENTIAL" \
    --opacity 0.3 \
    --rotation 45

{{#endtab }} {{#tab name=“Rust” }}

#![allow(unused)]
fn main() {
use printwell::watermark::{Watermark, add_watermark};

let pdf_data = std::fs::read("document.pdf")?;
let watermark = Watermark::text("CONFIDENTIAL")
    .opacity(0.3)
    .rotation(45.0);

let result = add_watermark(&pdf_data, &watermark)?;
std::fs::write("watermarked.pdf", result)?;
}

{{#endtab }} {{#tab name=“Node.js” }}

import { addWatermark } from 'printwell';

const pdfData = await readFile('document.pdf');
const result = addWatermark(pdfData, {
    text: 'CONFIDENTIAL',
    opacity: 0.3,
    rotation: 45
});
await writeFile('watermarked.pdf', result);

{{#endtab }} {{#tab name=“Python” }}

from printwell import add_watermark, Watermark

with open("document.pdf", "rb") as f:
    pdf_data = f.read()

watermark = Watermark.text_watermark(
    text="CONFIDENTIAL",
    opacity=0.3,
    rotation=45
)
result = add_watermark(pdf_data, watermark)

with open("watermarked.pdf", "wb") as f:
    f.write(result)

{{#endtab }} {{#endtabs }}

Password Protection

{{#tabs }} {{#tab name=“CLI” }}

printwell encrypt document.pdf -o encrypted.pdf \
    --owner-password admin123 \
    --user-password user123 \
    --no-print --no-copy

{{#endtab }} {{#tab name=“Rust” }}

#![allow(unused)]
fn main() {
use printwell::encrypt::{encrypt_pdf, EncryptionOptions, Permissions};

let options = EncryptionOptions::builder()
    .owner_password("admin123")
    .user_password("user123")
    .permissions(Permissions::none().allow_screen_readers())
    .build();

let result = encrypt_pdf(&pdf_data, &options)?;
}

{{#endtab }} {{#tab name=“Node.js” }}

import { encryptPdf } from 'printwell';

const result = encryptPdf(pdfData, {
    ownerPassword: 'admin123',
    userPassword: 'user123',
    permissions: { print: false, copy: false }
});

{{#endtab }} {{#tab name=“Python” }}

from printwell import encrypt_pdf, EncryptionOptions, Permissions

options = EncryptionOptions(
    owner_password="admin123",
    user_password="user123",
    permissions=Permissions(print=False, copy=False)
)
result = encrypt_pdf(pdf_data, options)

{{#endtab }} {{#endtabs }}

Next Steps

CLI Reference

The printwell command-line tool provides access to all printwell features.

Global Options

printwell [OPTIONS] <COMMAND>

Options:
  -v, --verbose    Increase verbosity (-v, -vv, -vvv)
  -q, --quiet      Suppress output
  -h, --help       Print help
  -V, --version    Print version

Commands

convert

Convert HTML to PDF.

printwell convert [OPTIONS] <INPUT>

Arguments:

  • <INPUT> - HTML file path, URL, or - for stdin

Options:

OptionDescriptionDefault
-o, --output <FILE>Output PDF file path-
--page-size <SIZE>Page size: A3, A4, A5, Letter, Legal, TabloidA4
--width <LENGTH>Custom page width (e.g., “210mm”)-
--height <LENGTH>Custom page height (e.g., “297mm”)-
--margin <MARGINS>Page margins (e.g., “10mm” or “10mm,20mm,10mm,20mm”)10mm
--landscapeUse landscape orientationfalse
--no-backgroundDon’t print background graphicsfalse
--scale <FACTOR>Scale factor1.0
--page-ranges <RANGES>Pages to print (e.g., “1-5,8”)All
--header <HTML>Header HTML template-
--footer <HTML>Footer HTML template-
--timeout <DURATION>Resource fetch timeout30s
--font <SPEC>Custom font (format: “family:path”)-

Metadata Options:

OptionDescription
--title <TEXT>PDF document title
--author <TEXT>PDF document author
--subject <TEXT>PDF document subject
--keywords <TEXT>PDF document keywords (comma-separated)
--creator <TEXT>PDF creator application name
--producer <TEXT>PDF producer name

Form Detection Options:

OptionDescription
--detect-formsDetect HTML form elements and output their positions
--convert-formsAuto-convert detected HTML forms to native PDF form fields
--forms-output <FILE>Output detected form elements to JSON file
--validate-forms <FILE>Validate detected forms against rules JSON file
--validation-output <FILE>Output validation results to JSON file

Boundary Extraction Options:

OptionDescription
--boundaries <SELECTORS>CSS selectors for boundary extraction (comma-separated)
--boundaries-output <FILE>Output boundaries to JSON file

Examples:

# Basic conversion
printwell convert report.html -o report.pdf

# From URL with custom page size
printwell convert https://example.com -o example.pdf --page-size Letter

# Landscape with custom margins
printwell convert input.html -o output.pdf --landscape --margin "25mm,10mm,25mm,10mm"

# With header and footer
printwell convert input.html -o output.pdf \
    --header '<div style="font-size:9px">Header</div>' \
    --footer '<div style="font-size:9px">Page <span class="pageNumber"></span></div>'

# With custom fonts and metadata
printwell convert input.html -o output.pdf \
    --font "MyFont:./fonts/custom.ttf" \
    --title "My Document" \
    --author "John Doe"

# From stdin
cat template.html | printwell convert - -o output.pdf

convert-batch

Convert multiple HTML files to PDF in batch.

printwell convert-batch [OPTIONS] <INPUTS>...

Options:

OptionDescriptionDefault
-o, --output-dir <DIR>Output directory for generated PDFs.
--workers <NUM>Maximum concurrent conversions4
--page-size <SIZE>Page sizeA4
--backgroundPrint background colors and imagesfalse
--landscapeUse landscape orientationfalse

Examples:

# Convert multiple files
printwell convert-batch file1.html file2.html file3.html -o ./output

# With 8 parallel workers
printwell convert-batch *.html -o ./pdfs --workers 8

watermark

Add watermark to PDF. Requires the watermark feature.

printwell watermark [OPTIONS] <INPUT> -o <OUTPUT>

Options:

OptionDescriptionDefault
-o, --output <FILE>Output PDF file pathRequired
--text <TEXT>Watermark text-
--image <FILE>Watermark image path (PNG or JPEG)-
--position <POS>Position: center, top-left, top-center, top-right, middle-left, middle-right, bottom-left, bottom-center, bottom-right, or “x,y”center
--rotation <DEG>Rotation in degrees (counter-clockwise)0
--opacity <FLOAT>Opacity (0.0-1.0)0.3
--font-size <PT>Font size in points72
--color <COLOR>Color as hex (e.g., “#FF0000”) or name (e.g., “gray”)gray
--foregroundPlace watermark in foregroundfalse
--pages <SELECTION>Pages: “1,3,5”, “1-10”, “odd”, “even”, “first”, “last”all
--scale <FACTOR>Scale factor for the watermark1.0

Examples:

# Text watermark
printwell watermark document.pdf -o watermarked.pdf --text "DRAFT"

# Diagonal confidential watermark
printwell watermark document.pdf -o watermarked.pdf \
    --text "CONFIDENTIAL" \
    --rotation 45 \
    --opacity 0.2 \
    --color "#FF0000"

# Image watermark
printwell watermark document.pdf -o watermarked.pdf \
    --image logo.png \
    --position bottom-right \
    --opacity 0.5

# Watermark only odd pages
printwell watermark document.pdf -o watermarked.pdf \
    --text "SAMPLE" \
    --pages odd

bookmarks

Add or extract bookmarks from a PDF. Requires the bookmarks feature.

printwell bookmarks [OPTIONS] <INPUT>

Options:

OptionDescription
-o, --output <FILE>Output PDF file (for adding bookmarks)
--add <SPEC>Add bookmark (format: “title:page” or “title:page:y_position” or “title:page:y_position:parent_index”)
--extract <FILE>Extract bookmarks to JSON file
--from-json <FILE>Add bookmarks from JSON file
--format <FMT>Output format for extraction: text, json

Examples:

# Add bookmarks inline
printwell bookmarks document.pdf -o output.pdf \
    --add "Chapter 1:1" \
    --add "Chapter 2:5" \
    --add "Chapter 3:10"

# Add bookmarks from JSON
printwell bookmarks document.pdf -o output.pdf --from-json bookmarks.json

# Extract bookmarks to JSON
printwell bookmarks document.pdf --extract bookmarks.json

# Extract bookmarks as text
printwell bookmarks document.pdf --extract - --format text

annotate

Add, list, or remove annotations from a PDF. Requires the annotations feature.

printwell annotate [OPTIONS] <INPUT>

Options:

OptionDescription
-o, --output <FILE>Output PDF file
--highlight <SPEC>Add highlight (format: “page:x:y:width:height” or “page:x:y:width:height:color”)
--note <SPEC>Add sticky note (format: “page:x:y:contents”)
--underline <SPEC>Add underline (format: “page:x:y:width:height”)
--strikeout <SPEC>Add strikeout (format: “page:x:y:width:height”)
--square <SPEC>Add rectangle (format: “page:x:y:width:height:color”)
--listList existing annotations
--removeRemove all annotations (or specific types)
--page <NUM>Page to filter for remove operation (1-indexed)
--color <HEX>Default annotation color
--format <FMT>Output format for list: text, json

Examples:

# Add highlight
printwell annotate document.pdf -o annotated.pdf \
    --highlight "1:100:200:150:20"

# Add sticky note
printwell annotate document.pdf -o annotated.pdf \
    --note "1:50:100:Review this section"

# List annotations
printwell annotate document.pdf --list --format json

# Remove all annotations
printwell annotate document.pdf -o cleaned.pdf --remove

encrypt

Encrypt PDF with password protection. Requires the encrypt feature. Commercial license required.

printwell encrypt [OPTIONS] <INPUT> -o <OUTPUT>

Password Options:

OptionDescription
--owner-password <PASS>Owner password (visible in process listings)
--owner-password-file <FILE>Read owner password from file (more secure)
--owner-password-env <VAR>Read owner password from environment variable
--user-password <PASS>User password (visible in process listings)
--user-password-file <FILE>Read user password from file (more secure)
--user-password-env <VAR>Read user password from environment variable

Permission Options:

OptionDescription
--allow-printAllow printing
--allow-print-hqAllow high-quality printing
--allow-copyAllow copying text and graphics
--allow-accessibilityAllow extracting text for accessibility
--allow-modifyAllow modifying the document
--allow-annotateAllow adding annotations
--allow-assembleAllow assembling the document
--allow-fill-formsAllow filling form fields
--allow-allAllow all permissions

Other Options:

OptionDescriptionDefault
--algorithm <ALG>aes256, aes128, or rc4aes256

Examples:

# Basic encryption with owner password from file
printwell encrypt document.pdf -o encrypted.pdf --owner-password-file secret.txt

# With user password and restrictions
printwell encrypt document.pdf -o encrypted.pdf \
    --owner-password-env OWNER_PASS \
    --user-password-env USER_PASS \
    --allow-print --allow-fill-forms

decrypt

Decrypt a password-protected PDF. Requires the encrypt feature. Commercial license required.

printwell decrypt [OPTIONS] <INPUT> -o <OUTPUT>

Options:

OptionDescription
--password <PASS>Password (visible in process listings)
--password-file <FILE>Read password from file (more secure)
--password-env <VAR>Read password from environment variable

sign

Digitally sign a PDF. Requires the signing feature. Commercial license required.

printwell sign [OPTIONS] <INPUT> -o <OUTPUT>

Options:

OptionDescriptionDefault
--certificate <FILE>PKCS#12 certificate file (.p12/.pfx)Required
--password <PASS>Certificate password (visible in process listings)-
--password-file <FILE>Read password from file (more secure)-
--password-env <VAR>Read password from environment variable-
--reason <TEXT>Reason for signing-
--location <TEXT>Location of signing-
--level <LEVEL>PAdES level: B, T, LT, LTAB
--timestamp-url <URL>Timestamp server URL-
--visible <FIELD>Visible signature field name-
--position <SPEC>Visible signature position (format: “page:x,y,w,h”)-
--certifyCreate a certification signaturefalse
--mdp <LEVEL>MDP permissions: 1=no-changes, 2=form-filling, 3=annotations2

Examples:

# Invisible signature
printwell sign document.pdf -o signed.pdf \
    --certificate certificate.p12 \
    --password-file pass.txt \
    --reason "Approved" \
    --location "New York"

# Visible signature with timestamp
printwell sign document.pdf -o signed.pdf \
    --certificate certificate.p12 \
    --password-env CERT_PASS \
    --visible "Signature1" \
    --position "1:400,50,150,50" \
    --timestamp-url "http://timestamp.digicert.com"

# Certification signature
printwell sign document.pdf -o certified.pdf \
    --certificate certificate.p12 \
    --password-file pass.txt \
    --certify \
    --mdp 2

verify

Verify PDF signatures. Requires the signing feature. Commercial license required.

printwell verify [OPTIONS] <INPUT>

Options:

OptionDescriptionDefault
--format <FMT>Output format: text, jsontext
--use-system-trustUse system trust store for certificate chain validationfalse

list-fields

List signature fields in a PDF. Requires the signing feature. Commercial license required.

printwell list-fields [OPTIONS] <INPUT>

Options:

OptionDescriptionDefault
--format <FMT>Output format: text, jsontext

forms

Add form fields to PDF. Requires the forms feature. Commercial license required.

printwell forms [OPTIONS] <INPUT> -o <OUTPUT>

Options:

OptionDescription
--text-field <SPEC>Add text field (format: “name:page:x,y,w,h”)
--checkbox <SPEC>Add checkbox (format: “name:page:x,y,size”)
--dropdown <SPEC>Add dropdown (format: “name:page:x,y,w,h:opt1,opt2,…”)
--signature-field <SPEC>Add signature field (format: “name:page:x,y,w,h”)

Examples:

# Add form fields
printwell forms document.pdf -o form.pdf \
    --text-field "name:1:50,700,200,20" \
    --text-field "email:1:50,650,200,20" \
    --checkbox "agree:1:50,600,15" \
    --signature-field "signature:1:50,500,200,50"

pdfa-validate

Validate PDF/A compliance. Requires the pdfa feature. Commercial license required.

printwell pdfa-validate [OPTIONS] <INPUT>

Options:

OptionDescriptionDefault
--level <LEVEL>PDF/A level: 1b, 1a, 2b, 2u, 2a, 3b, 3u, 3a2b
--format <FMT>Output format: text, jsontext

pdfa-convert

Add PDF/A metadata to a PDF. Requires the pdfa feature. Commercial license required.

printwell pdfa-convert [OPTIONS] <INPUT> -o <OUTPUT>

Options:

OptionDescriptionDefault
--level <LEVEL>PDF/A level: 1b, 1a, 2b, 2u, 2a, 3b, 3u, 3a2b
--title <TEXT>Document title-
--author <TEXT>Document author-

pdfua-validate

Validate PDF/UA accessibility compliance. Requires the pdfua feature. Commercial license required.

printwell pdfua-validate [OPTIONS] <INPUT>

Options:

OptionDescriptionDefault
--level <LEVEL>PDF/UA level: 1, 21
--format <FMT>Output format: text, jsontext

pdfua-convert

Add PDF/UA accessibility metadata to a PDF. Requires the pdfua feature. Commercial license required.

printwell pdfua-convert [OPTIONS] <INPUT> -o <OUTPUT>

Options:

OptionDescriptionDefault
--level <LEVEL>PDF/UA level: 1, 21
--title <TEXT>Document title-
--language <LANG>Document language (BCP 47 format, e.g., “en-US”)en

info

Display PDF/renderer information.

printwell info [OPTIONS] [INPUT]

Arguments:

  • [INPUT] - Optional PDF file to inspect

Options:

OptionDescription
--rendererShow renderer information

Examples:

# Show renderer info
printwell info --renderer

# Inspect PDF file
printwell info document.pdf

Output:

printwell 0.1.0
Chromium: 120.0.6099.0
Skia: m120
Build: release

Exit Codes

CodeDescription
0Success
1General error
2Invalid arguments
3Input file not found
4Output write error
5Conversion error
6Validation failed

Rust Guide

This guide covers using printwell as a Rust library.

Installation

Add to your Cargo.toml:

[dependencies]
printwell = "0.1"

With specific features:

[dependencies]
printwell = { version = "0.1", features = ["signing", "forms"] }

Basic Usage

Creating a Converter

The Converter is the main entry point. Create one instance and reuse it:

use printwell::Converter;

fn main() -> printwell::Result<()> {
    // Create a converter (initializes the rendering engine)
    let converter = Converter::new()?;

    // Reuse for multiple conversions
    let pdf1 = converter.html_to_pdf("<h1>Document 1</h1>", None, None)?;
    let pdf2 = converter.html_to_pdf("<h1>Document 2</h1>", None, None)?;

    Ok(())
}

Converting HTML to PDF

#![allow(unused)]
fn main() {
use printwell::{Converter, PdfOptions, RenderOptions};

let converter = Converter::new()?;

// Simple conversion
let result = converter.html_to_pdf(
    "<h1>Hello</h1>",
    None,  // RenderOptions
    None,  // PdfOptions
)?;

// Get PDF as bytes
let pdf_bytes = result.data();

// Write to file
result.write_to_file("output.pdf")?;

// Get page count
println!("Pages: {}", result.page_count);
}

Converting URLs

#![allow(unused)]
fn main() {
let result = converter.url_to_pdf(
    "https://example.com",
    None,
    None,
)?;
}

Configuration Options

PDF Options

#![allow(unused)]
fn main() {
use printwell::{PdfOptions, PageSize, Orientation, Margins, PdfMetadata};

let options = PdfOptions::builder()
    // Page size
    .page_size(PageSize::A4)
    // Or custom size in mm
    .page_width_mm(210.0)
    .page_height_mm(297.0)

    // Orientation
    .orientation(Orientation::Portrait)

    // Margins in mm
    .margins(Margins::new(20.0, 15.0, 20.0, 15.0))
    // Or uniform margins
    .margins(Margins::uniform(10.0))

    // Scale (0.1 to 2.0)
    .scale(1.0)

    // Print backgrounds
    .print_background(true)

    // Page ranges
    .page_ranges("1-5,8,10-12")

    // Headers and footers
    .header_template("<div style='font-size:10px'>Header</div>")
    .footer_template("<div style='font-size:10px'>Page <span class='pageNumber'></span></div>")

    // Metadata
    .metadata(PdfMetadata {
        title: Some("My Document".into()),
        author: Some("Author Name".into()),
        subject: Some("Subject".into()),
        keywords: Some("pdf, rust".into()),
        creator: Some("My App".into()),
        producer: None,
    })

    .build();
}

Render Options

#![allow(unused)]
fn main() {
use printwell::{RenderOptions, ResourceOptions, Viewport};

let options = RenderOptions::builder()
    // Base URL for relative resources
    .base_url("https://example.com/")

    // Viewport size
    .viewport(Viewport {
        width: 1920,
        height: 1080,
        device_scale_factor: 2.0,
    })

    // Resource loading
    .resources(ResourceOptions {
        allow_remote: true,
        timeout_ms: 30000,
        blocked_domains: vec!["ads.example.com".into()],
        ..Default::default()
    })

    // Custom stylesheets
    .user_stylesheets(vec![
        "body { font-family: 'Custom Font'; }".into()
    ])

    .build();
}

Batch Processing

For high-throughput scenarios, use ConverterPool:

#![allow(unused)]
fn main() {
use printwell::ConverterPool;

// Create a pool with 4 concurrent converters
let pool = ConverterPool::new(4)?;

// Convert multiple documents
let html_docs = vec![
    "<h1>Doc 1</h1>".to_string(),
    "<h1>Doc 2</h1>".to_string(),
    "<h1>Doc 3</h1>".to_string(),
];

let results = pool.convert_batch(&html_docs)?;

for (i, result) in results.iter().enumerate() {
    result.write_to_file(format!("output_{}.pdf", i))?;
}
}

PDF Post-Processing

Watermarks

#![allow(unused)]
fn main() {
use printwell::watermark::{Watermark, WatermarkPosition, add_watermark};

let pdf_data = std::fs::read("input.pdf")?;

// Text watermark
let watermark = Watermark::text("CONFIDENTIAL")
    .position(WatermarkPosition::Center)
    .rotation(45.0)
    .opacity(0.3)
    .font_size(72.0)
    .color(0x80, 0x80, 0x80);

let result = add_watermark(&pdf_data, &watermark)?;
std::fs::write("watermarked.pdf", result)?;
}

Bookmarks

#![allow(unused)]
fn main() {
use printwell::bookmarks::{Bookmark, add_bookmarks, extract_bookmarks};

let pdf_data = std::fs::read("input.pdf")?;

let bookmarks = vec![
    Bookmark::new("Chapter 1", 1),
    Bookmark::new("Chapter 2", 5).with_children(vec![
        Bookmark::new("Section 2.1", 5),
        Bookmark::new("Section 2.2", 8),
    ]),
    Bookmark::new("Chapter 3", 12),
];

let result = add_bookmarks(&pdf_data, &bookmarks)?;

// Extract existing bookmarks
let existing = extract_bookmarks(&pdf_data)?;
}

Encryption

#![allow(unused)]
fn main() {
use printwell::encrypt::{encrypt_pdf, decrypt_pdf, EncryptionOptions, Permissions};

let pdf_data = std::fs::read("input.pdf")?;

let options = EncryptionOptions::builder()
    .owner_password("admin")
    .user_password("user")
    .permissions(Permissions {
        print: true,
        copy: false,
        modify: false,
        annotate: true,
        ..Default::default()
    })
    .build();

let encrypted = encrypt_pdf(&pdf_data, &options)?;

// Decrypt
let decrypted = decrypt_pdf(&encrypted, "admin")?;
}

Digital Signatures

#![allow(unused)]
fn main() {
use printwell::signing::{sign_pdf, SigningOptions, SignatureAppearance};

let pdf_data = std::fs::read("input.pdf")?;
let cert_data = std::fs::read("certificate.p12")?;

// Invisible signature
let signed = sign_pdf(
    &pdf_data,
    &cert_data,
    "certificate_password",
    SigningOptions {
        reason: Some("Approved".into()),
        location: Some("New York".into()),
        ..Default::default()
    },
)?;

// Visible signature
let signed = sign_pdf_visible(
    &pdf_data,
    &cert_data,
    "password",
    SigningOptions::default(),
    SignatureAppearance {
        page: 1,
        x: 400.0,
        y: 50.0,
        width: 200.0,
        height: 75.0,
        ..Default::default()
    },
)?;
}

Error Handling

printwell uses a custom Result type:

#![allow(unused)]
fn main() {
use printwell::{Result, Error};

fn convert_document() -> Result<()> {
    let converter = Converter::new()?;

    match converter.html_to_pdf("<h1>Test</h1>", None, None) {
        Ok(result) => {
            result.write_to_file("output.pdf")?;
            Ok(())
        }
        Err(Error::Conversion(msg)) => {
            eprintln!("Conversion failed: {}", msg);
            Err(Error::Conversion(msg))
        }
        Err(e) => Err(e),
    }
}
}

Thread Safety

  • Converter is Send + Sync and can be shared across threads
  • ConverterPool manages thread-safe access to multiple converters
  • All PDF manipulation functions are thread-safe
#![allow(unused)]
fn main() {
use std::sync::Arc;
use std::thread;

let converter = Arc::new(Converter::new()?);

let handles: Vec<_> = (0..4).map(|i| {
    let conv = Arc::clone(&converter);
    thread::spawn(move || {
        let html = format!("<h1>Document {}</h1>", i);
        conv.html_to_pdf(&html, None, None)
    })
}).collect();

for handle in handles {
    let result = handle.join().unwrap()?;
    // Process result
}
}

Feature Flags

FeatureDescriptionDependencies
defaultCore + all PDF features-
pdf-fullAll PDF post-processing-
signingDigital signaturesp12, x509-cert, rsa, ecdsa, cms
formsPDF form fieldsregex
pdfaPDF/A compliancechrono
pdfuaPDF/UA accessibilitychrono

Minimal build:

[dependencies]
printwell = { version = "0.1", default-features = false }

Node.js Guide

This guide covers using printwell with Node.js.

Installation

npm install printwell

Or with yarn:

yarn add printwell

Basic Usage

import { Converter } from 'printwell';

const converter = new Converter();
const result = await converter.htmlToPdf('<h1>Hello, World!</h1>');
await result.writeToFile('output.pdf');

CommonJS

const { Converter } = require('printwell');

async function main() {
    const converter = new Converter();
    const result = await converter.htmlToPdf('<h1>Hello, World!</h1>');
    await result.writeToFile('output.pdf');
}

TypeScript

Full TypeScript support with included type definitions:

import { Converter, PdfOptions, RenderOptions, PdfResult } from 'printwell';

const converter = new Converter();

const options: PdfOptions = {
    pageSize: 'A4',
    margins: { top: 20, right: 15, bottom: 20, left: 15 }
};

const result: PdfResult = await converter.htmlToPdf(html, null, options);

Converting Documents

HTML String to PDF

import { Converter } from 'printwell';

const converter = new Converter();

const html = `
<!DOCTYPE html>
<html>
<head>
    <style>
        body { font-family: Arial; padding: 40px; }
        h1 { color: #333; }
    </style>
</head>
<body>
    <h1>My Document</h1>
    <p>This is a PDF generated from HTML.</p>
</body>
</html>
`;

const result = await converter.htmlToPdf(html);

// Get as Buffer
const buffer = result.data;

// Write to file
await result.writeToFile('output.pdf');

// Get page count
console.log(`Pages: ${result.pageCount}`);

URL to PDF

const result = await converter.urlToPdf('https://example.com');
await result.writeToFile('example.pdf');

Configuration Options

PDF Options

const result = await converter.htmlToPdf(html, null, {
    // Page size: 'A3', 'A4', 'A5', 'Letter', 'Legal', 'Tabloid'
    pageSize: 'A4',

    // Or custom size in mm
    pageWidthMm: 210,
    pageHeightMm: 297,

    // Orientation: 'Portrait' or 'Landscape'
    orientation: 'Portrait',

    // Margins in mm
    margins: {
        top: 20,
        right: 15,
        bottom: 20,
        left: 15
    },

    // Scale factor (0.1 to 2.0)
    scale: 1.0,

    // Print background graphics
    printBackground: true,

    // Page ranges
    pageRanges: '1-5,8,10-12',

    // Headers and footers
    headerTemplate: '<div style="font-size:10px">Header</div>',
    footerTemplate: '<div style="font-size:10px">Page <span class="pageNumber"></span> of <span class="totalPages"></span></div>',

    // Metadata
    metadata: {
        title: 'My Document',
        author: 'Author Name',
        subject: 'Document Subject',
        keywords: 'pdf, nodejs'
    }
});

Render Options

const result = await converter.htmlToPdf(html, {
    // Base URL for relative resources
    baseUrl: 'https://example.com/',

    // Viewport
    viewport: {
        width: 1920,
        height: 1080,
        deviceScaleFactor: 2.0
    },

    // Resource loading
    resources: {
        allowRemote: true,
        timeoutMs: 30000,
        blockedDomains: ['ads.example.com'],
        userAgent: 'Custom User Agent'
    },

    // Custom CSS
    userStylesheets: [
        'body { font-family: "Custom Font"; }'
    ]
});

Batch Processing

Converter Pool

For high-throughput scenarios:

import { ConverterPool } from 'printwell';

// Create pool with 4 concurrent converters
const pool = new ConverterPool(4);

// Convert multiple documents
const htmlDocs = [
    '<h1>Document 1</h1>',
    '<h1>Document 2</h1>',
    '<h1>Document 3</h1>'
];

const results = await pool.convertBatch(htmlDocs);

for (let i = 0; i < results.length; i++) {
    await results[i].writeToFile(`output_${i}.pdf`);
}

Parallel Conversion

const urls = [
    'https://example.com/page1',
    'https://example.com/page2',
    'https://example.com/page3'
];

const results = await Promise.all(
    urls.map(url => converter.urlToPdf(url))
);

PDF Post-Processing

Watermarks

import { addWatermark, addWatermarks } from 'printwell';
import { readFile, writeFile } from 'fs/promises';

const pdfData = await readFile('input.pdf');

// Text watermark
const result = addWatermark(pdfData, {
    text: 'CONFIDENTIAL',
    position: 'Center',
    rotation: 45,
    opacity: 0.3,
    fontSize: 72,
    color: { r: 128, g: 128, b: 128 }
});

await writeFile('watermarked.pdf', result);

// Multiple watermarks
const multiResult = addWatermarks(pdfData, [
    { text: 'DRAFT', position: 'Center', opacity: 0.2 },
    { text: 'Page header', position: 'TopCenter', fontSize: 12 }
]);

Bookmarks

import { addBookmarks, extractBookmarks } from 'printwell';

const pdfData = await readFile('input.pdf');

// Add bookmarks
const result = addBookmarks(pdfData, [
    { title: 'Chapter 1', page: 1 },
    { title: 'Chapter 2', page: 5, children: [
        { title: 'Section 2.1', page: 5 },
        { title: 'Section 2.2', page: 8 }
    ]},
    { title: 'Chapter 3', page: 12 }
]);

// Extract existing bookmarks
const bookmarks = extractBookmarks(pdfData);
console.log(bookmarks);

Annotations

import { addAnnotations, listAnnotations, removeAnnotations } from 'printwell';

// Add highlight annotation
const result = addAnnotations(pdfData, [
    {
        type: 'Highlight',
        page: 1,
        rect: { x: 100, y: 700, width: 200, height: 20 },
        color: { r: 255, g: 255, b: 0 },
        opacity: 0.5
    },
    {
        type: 'Text',  // Sticky note
        page: 1,
        rect: { x: 50, y: 600, width: 24, height: 24 },
        contents: 'Review this section',
        author: 'Reviewer'
    }
]);

// List annotations
const annotations = listAnnotations(pdfData);

// Remove all highlight annotations
const cleaned = removeAnnotations(pdfData, {
    types: ['Highlight']
});

Encryption

import { encryptPdf, decryptPdf } from 'printwell';

// Encrypt PDF
const encrypted = encryptPdf(pdfData, {
    ownerPassword: 'admin123',
    userPassword: 'user123',
    permissions: {
        print: true,
        copy: false,
        modify: false,
        annotate: true
    },
    algorithm: 'Aes256'
});

await writeFile('encrypted.pdf', encrypted);

// Decrypt PDF
const decrypted = decryptPdf(encrypted, 'admin123');

Digital Signatures

import { signPdf, signPdfVisible, verifySignatures } from 'printwell';

const pdfData = await readFile('document.pdf');
const certData = await readFile('certificate.p12');

// Invisible signature
const signed = signPdf(pdfData, certData, 'certificate_password', {
    reason: 'Approved',
    location: 'New York',
    signatureLevel: 'PadesB'
});

// Visible signature
const signedVisible = signPdfVisible(pdfData, certData, 'password', {
    reason: 'Approved'
}, {
    page: 1,
    x: 400,
    y: 50,
    width: 200,
    height: 75,
    showName: true,
    showDate: true
});

// Verify signatures
const verifications = verifySignatures(signedData);
for (const v of verifications) {
    console.log(`Signer: ${v.signerName}, Valid: ${v.isValid}`);
}

Form Fields

import { addFormFields, validateFormFields } from 'printwell';

// Add form fields
const result = addFormFields(pdfData, [
    {
        fieldType: 'text',
        name: 'full_name',
        page: 1,
        rect: { x: 100, y: 700, width: 200, height: 20 },
        required: true
    },
    {
        fieldType: 'checkbox',
        name: 'agree_terms',
        page: 1,
        rect: { x: 100, y: 650, width: 15, height: 15 },
        checked: false
    },
    {
        fieldType: 'dropdown',
        name: 'country',
        page: 1,
        rect: { x: 100, y: 600, width: 150, height: 20 },
        options: ['USA', 'Canada', 'UK', 'Other']
    }
]);

// Validate form fields
const validation = validateFormFields(formElements, [
    { fieldName: 'full_name', required: true, minLength: 2 },
    { fieldName: 'email', required: true, pattern: '^[^@]+@[^@]+$' }
]);

console.log(`Valid: ${validation.allValid}`);

PDF/A Compliance

import { validatePdfa, addPdfaMetadata } from 'printwell';

// Validate PDF/A compliance
const result = validatePdfa(pdfData, 'PdfA2b');
console.log(`Compliant: ${result.isCompliant}`);
for (const issue of result.issues) {
    console.log(`${issue.severity}: ${issue.message}`);
}

// Add PDF/A metadata
const pdfaDoc = addPdfaMetadata(pdfData, 'PdfA2b', {
    title: 'Archived Document',
    author: 'Author Name'
});

PDF/UA Accessibility

import { validatePdfua, addPdfuaMetadata } from 'printwell';

// Validate accessibility
const result = validatePdfua(pdfData, 'PdfUA1');
console.log(`Accessible: ${result.isCompliant}`);

// Add accessibility metadata
const accessibleDoc = addPdfuaMetadata(pdfData, 'PdfUA1', {
    language: 'en',
    title: 'Accessible Document',
    generateOutline: true
});

Error Handling

import { Converter } from 'printwell';

try {
    const converter = new Converter();
    const result = await converter.htmlToPdf(html);
    await result.writeToFile('output.pdf');
} catch (error) {
    if (error.code === 'CONVERSION_ERROR') {
        console.error('Failed to convert HTML:', error.message);
    } else if (error.code === 'RESOURCE_ERROR') {
        console.error('Failed to load resource:', error.message);
    } else {
        throw error;
    }
}

Express.js Integration

import express from 'express';
import { Converter } from 'printwell';

const app = express();
const converter = new Converter();

app.get('/generate-pdf', async (req, res) => {
    try {
        const html = `<h1>Generated at ${new Date().toISOString()}</h1>`;
        const result = await converter.htmlToPdf(html);

        res.setHeader('Content-Type', 'application/pdf');
        res.setHeader('Content-Disposition', 'attachment; filename="document.pdf"');
        res.send(result.data);
    } catch (error) {
        res.status(500).json({ error: error.message });
    }
});

app.listen(3000);

Python Guide

This guide covers using printwell with Python.

Installation

pip install printwell

Or with uv:

uv pip install printwell

Basic Usage

from printwell import Converter

converter = Converter()
result = converter.html_to_pdf("<h1>Hello, World!</h1>")
result.write_to_file("output.pdf")

Converting Documents

HTML String to PDF

from printwell import Converter

converter = Converter()

html = """
<!DOCTYPE html>
<html>
<head>
    <style>
        body { font-family: Arial; padding: 40px; }
        h1 { color: #333; }
    </style>
</head>
<body>
    <h1>My Document</h1>
    <p>This is a PDF generated from HTML.</p>
</body>
</html>
"""

result = converter.html_to_pdf(html)

# Get as bytes
pdf_bytes = result.data()

# Write to file
result.write_to_file("output.pdf")

# Get page count
print(f"Pages: {result.page_count}")

URL to PDF

result = converter.url_to_pdf("https://example.com")
result.write_to_file("example.pdf")

File to PDF

with open("input.html") as f:
    html = f.read()

result = converter.html_to_pdf(html)
result.write_to_file("output.pdf")

Configuration Options

PDF Options

from printwell import (
    Converter, PdfOptions, PageSize, Orientation,
    Margins, PdfMetadata
)

converter = Converter()

options = PdfOptions(
    # Page size
    page_size=PageSize.A4,

    # Or custom size in mm
    page_width_mm=210.0,
    page_height_mm=297.0,

    # Orientation
    orientation=Orientation.Portrait,

    # Margins in mm
    margins=Margins(top=20, right=15, bottom=20, left=15),
    # Or uniform margins
    margins=Margins.uniform(10),

    # Scale factor (0.1 to 2.0)
    scale=1.0,

    # Print background graphics
    print_background=True,

    # Page ranges
    page_ranges="1-5,8,10-12",

    # Headers and footers
    header_template='<div style="font-size:10px">Header</div>',
    footer_template='<div style="font-size:10px">Page <span class="pageNumber"></span></div>',

    # Metadata
    metadata=PdfMetadata(
        title="My Document",
        author="Author Name",
        subject="Document Subject",
        keywords="pdf, python"
    )
)

result = converter.html_to_pdf(html, pdf_options=options)

Render Options

from printwell import RenderOptions, ResourceOptions, Viewport

options = RenderOptions(
    # Base URL for relative resources
    base_url="https://example.com/",

    # Viewport
    viewport=Viewport(
        width=1920,
        height=1080,
        device_scale_factor=2.0
    ),

    # Resource loading
    resources=ResourceOptions(
        allow_remote=True,
        timeout_ms=30000,
        blocked_domains=["ads.example.com"],
        user_agent="Custom User Agent"
    ),

    # Custom CSS
    user_stylesheets=[
        'body { font-family: "Custom Font"; }'
    ]
)

result = converter.html_to_pdf(html, render_options=options)

Batch Processing

Converter Pool

For high-throughput scenarios:

from printwell import ConverterPool

# Create pool with 4 concurrent converters
pool = ConverterPool(4)

# Convert multiple documents
html_docs = [
    "<h1>Document 1</h1>",
    "<h1>Document 2</h1>",
    "<h1>Document 3</h1>"
]

results = pool.convert_batch(html_docs)

for i, result in enumerate(results):
    result.write_to_file(f"output_{i}.pdf")

Parallel Conversion with Threading

from concurrent.futures import ThreadPoolExecutor
from printwell import Converter

converter = Converter()
urls = [
    "https://example.com/page1",
    "https://example.com/page2",
    "https://example.com/page3"
]

def convert_url(url):
    return converter.url_to_pdf(url)

with ThreadPoolExecutor(max_workers=4) as executor:
    results = list(executor.map(convert_url, urls))

PDF Post-Processing

Watermarks

from printwell import (
    add_watermark, add_watermarks, Watermark,
    WatermarkPosition, WatermarkColor
)

with open("input.pdf", "rb") as f:
    pdf_data = f.read()

# Text watermark
watermark = Watermark.text_watermark(
    text="CONFIDENTIAL",
    position=WatermarkPosition.Center,
    rotation=45,
    opacity=0.3,
    font_size=72,
    color=WatermarkColor(r=128, g=128, b=128)
)

result = add_watermark(pdf_data, watermark)

with open("watermarked.pdf", "wb") as f:
    f.write(result)

# Multiple watermarks
result = add_watermarks(pdf_data, [
    Watermark(text="DRAFT", opacity=0.2),
    Watermark(text="Page header", position=WatermarkPosition.TopCenter, font_size=12)
])

Bookmarks

from printwell import add_bookmarks, extract_bookmarks, Bookmark

with open("input.pdf", "rb") as f:
    pdf_data = f.read()

# Add bookmarks
bookmarks = [
    Bookmark("Chapter 1", page=1),
    Bookmark("Chapter 2", page=5),
    Bookmark("Chapter 3", page=12),
]

result = add_bookmarks(pdf_data, bookmarks)

# Extract existing bookmarks
existing = extract_bookmarks(pdf_data)
for bm in existing:
    print(f"{bm.title} -> Page {bm.page}")

Annotations

from printwell import (
    add_annotations, list_annotations, remove_annotations,
    Annotation, AnnotationType, AnnotationRect, AnnotationColor
)

# Add highlight annotation
result = add_annotations(pdf_data, [
    Annotation(
        annotation_type=AnnotationType.Highlight,
        page=1,
        rect=AnnotationRect(x=100, y=700, width=200, height=20),
        color=AnnotationColor.yellow(),
        opacity=0.5
    ),
    Annotation.sticky_note(
        page=1,
        x=50,
        y=600,
        contents="Review this section",
        author="Reviewer"
    )
])

# List annotations
annotations = list_annotations(pdf_data)
for ann in annotations:
    print(f"Page {ann.page}: {ann.annotation_type}")

# Remove annotations
cleaned = remove_annotations(pdf_data, annotation_types=[AnnotationType.Highlight])

Encryption

from printwell import (
    encrypt_pdf, decrypt_pdf, EncryptionOptions,
    Permissions, EncryptionAlgorithm
)

# Encrypt PDF
options = EncryptionOptions(
    owner_password="admin123",
    user_password="user123",
    permissions=Permissions(
        print=True,
        copy=False,
        modify=False,
        annotate=True
    ),
    algorithm=EncryptionAlgorithm.Aes256
)

encrypted = encrypt_pdf(pdf_data, options)

with open("encrypted.pdf", "wb") as f:
    f.write(encrypted)

# Decrypt PDF
decrypted = decrypt_pdf(encrypted, "admin123")

Digital Signatures

from printwell import (
    sign_pdf, sign_pdf_visible, verify_signatures,
    SigningOptions, SignatureAppearance, SignatureLevel
)

with open("document.pdf", "rb") as f:
    pdf_data = f.read()

with open("certificate.p12", "rb") as f:
    cert_data = f.read()

# Invisible signature
options = SigningOptions(
    reason="Approved",
    location="New York",
    signature_level=SignatureLevel.PadesB
)

signed = sign_pdf(pdf_data, cert_data, "certificate_password", options)

# Visible signature
appearance = SignatureAppearance(
    page=1,
    x=400,
    y=50,
    width=200,
    height=75,
    show_name=True,
    show_date=True
)

signed_visible = sign_pdf_visible(
    pdf_data, cert_data, "password", options, appearance
)

# Verify signatures
verifications = verify_signatures(signed_data)
for v in verifications:
    print(f"Signer: {v.signer_name}, Valid: {v.is_valid}")

Form Fields

from printwell import (
    add_form_fields, validate_form_fields,
    TextField, FormCheckbox, FormDropdown, FormRect,
    ValidationRule, FormElementInput
)

# Add form fields
rect = FormRect(x=100, y=700, width=200, height=20)

result = add_form_fields(
    pdf_data,
    text_fields=[
        TextField(
            name="full_name",
            page=1,
            rect=rect,
            required=True
        )
    ],
    checkboxes=[
        FormCheckbox(
            name="agree_terms",
            page=1,
            rect=FormRect(x=100, y=650, width=15, height=15)
        )
    ],
    dropdowns=[
        FormDropdown(
            name="country",
            page=1,
            rect=FormRect(x=100, y=600, width=150, height=20),
            options=["USA", "Canada", "UK", "Other"]
        )
    ]
)

# Validate form fields
elements = [
    FormElementInput(name="full_name", default_value="John Doe"),
    FormElementInput(name="email", default_value="invalid-email")
]

rules = [
    ValidationRule(field_name="full_name", required=True, min_length=2),
    ValidationRule(field_name="email", required=True, pattern=r"^[^@]+@[^@]+$")
]

validation = validate_form_fields(elements, rules)
print(f"All valid: {validation.all_valid}")
for result in validation.results:
    if not result.is_valid:
        print(f"{result.field_name}: {result.errors}")

PDF/A Compliance

from printwell import validate_pdfa, add_pdfa_metadata, PdfALevel

# Validate PDF/A compliance
result = validate_pdfa(pdf_data, PdfALevel.PdfA2b)
print(f"Compliant: {result.is_compliant}")
for issue in result.issues:
    print(f"{issue.severity}: {issue.message}")

# Add PDF/A metadata
pdfa_doc = add_pdfa_metadata(
    pdf_data,
    PdfALevel.PdfA2b,
    title="Archived Document",
    author="Author Name"
)

PDF/UA Accessibility

from printwell import (
    validate_pdfua, add_pdfua_metadata,
    PdfUALevel, AccessibilityOptions
)

# Validate accessibility
result = validate_pdfua(pdf_data, PdfUALevel.PdfUA1)
print(f"Accessible: {result.is_compliant}")

# Add accessibility metadata
options = AccessibilityOptions(
    language="en",
    title="Accessible Document",
    generate_outline=True
)

accessible_doc = add_pdfua_metadata(pdf_data, PdfUALevel.PdfUA1, options)

Error Handling

from printwell import Converter

try:
    converter = Converter()
    result = converter.html_to_pdf(html)
    result.write_to_file("output.pdf")
except RuntimeError as e:
    if "conversion" in str(e).lower():
        print(f"Conversion failed: {e}")
    elif "resource" in str(e).lower():
        print(f"Resource loading failed: {e}")
    else:
        raise

Flask Integration

from flask import Flask, send_file
from printwell import Converter
from io import BytesIO

app = Flask(__name__)
converter = Converter()

@app.route('/generate-pdf')
def generate_pdf():
    html = f"<h1>Generated at {datetime.now().isoformat()}</h1>"
    result = converter.html_to_pdf(html)

    buffer = BytesIO(result.data())
    buffer.seek(0)

    return send_file(
        buffer,
        mimetype='application/pdf',
        as_attachment=True,
        download_name='document.pdf'
    )

if __name__ == '__main__':
    app.run()

FastAPI Integration

from fastapi import FastAPI
from fastapi.responses import Response
from printwell import Converter

app = FastAPI()
converter = Converter()

@app.get("/generate-pdf")
async def generate_pdf():
    html = "<h1>Hello from FastAPI!</h1>"
    result = converter.html_to_pdf(html)

    return Response(
        content=result.data(),
        media_type="application/pdf",
        headers={
            "Content-Disposition": "attachment; filename=document.pdf"
        }
    )

Django Integration

from django.http import HttpResponse
from printwell import Converter

converter = Converter()

def generate_pdf(request):
    html = "<h1>Hello from Django!</h1>"
    result = converter.html_to_pdf(html)

    response = HttpResponse(result.data(), content_type='application/pdf')
    response['Content-Disposition'] = 'attachment; filename="document.pdf"'
    return response

HTML to PDF Conversion

The core feature of printwell is high-fidelity HTML to PDF conversion using Chromium’s Blink rendering engine.

How It Works

printwell embeds Chromium’s rendering engine (Blink + Skia) as a native library. When you convert HTML to PDF:

  1. HTML is parsed and a DOM tree is built
  2. CSS is applied and the layout is computed
  3. The page is rendered using Skia graphics
  4. PDF is generated using PDFium

This gives you the exact same rendering quality as Chrome, without needing a browser installation.

Supported Features

HTML5

  • Full HTML5 support
  • Semantic elements (<article>, <section>, <nav>, etc.)
  • Forms (for visual rendering; see Form Fields for interactive forms)
  • Tables with complex layouts
  • SVG graphics (inline and external)
  • Canvas elements (static rendering)

CSS3

  • Flexbox and Grid layouts
  • CSS Variables (custom properties)
  • Media queries (print media is used)
  • CSS animations (final frame is captured)
  • Web fonts (@font-face)
  • CSS transforms and filters
  • Multi-column layouts
  • CSS Paged Media (@page rules)
@page {
    size: A4 portrait;
    margin: 2cm;
}

@page :first {
    margin-top: 5cm;
}

.page-break {
    page-break-after: always;
}

@media print {
    .no-print { display: none; }
    a::after { content: " (" attr(href) ")"; }
}

Page Configuration

Page Sizes

SizeDimensions (mm)
A3297 × 420
A4210 × 297
A5148 × 210
Letter216 × 279
Legal216 × 356
Tabloid279 × 432

Or specify custom dimensions:

#![allow(unused)]
fn main() {
let options = PdfOptions::builder()
    .page_width_mm(200.0)
    .page_height_mm(300.0)
    .build();
}

Margins

Margins are specified in millimeters:

#![allow(unused)]
fn main() {
// Individual margins
let margins = Margins::new(20.0, 15.0, 20.0, 15.0); // top, right, bottom, left

// Uniform margins
let margins = Margins::uniform(10.0);

// Symmetric margins
let margins = Margins::symmetric(20.0, 15.0); // vertical, horizontal

// No margins
let margins = Margins::none();
}

Headers and Footers

Headers and footers use HTML templates with special CSS classes:

<div style="font-size: 10px; width: 100%; text-align: center;">
    <span class="title"></span> |
    Page <span class="pageNumber"></span> of <span class="totalPages"></span>
</div>

Available classes:

  • title - Document title
  • pageNumber - Current page number
  • totalPages - Total page count
  • url - Document URL
  • date - Current date

Resource Loading

Remote Resources

By default, remote resources (images, fonts, stylesheets) are loaded:

#![allow(unused)]
fn main() {
let options = RenderOptions::builder()
    .resources(ResourceOptions {
        allow_remote: true,
        timeout_ms: 30000,
        max_concurrent: 6,
        ..Default::default()
    })
    .build();
}

Blocking Domains

Block specific domains (e.g., ads, analytics):

#![allow(unused)]
fn main() {
let options = RenderOptions::builder()
    .resources(ResourceOptions {
        blocked_domains: vec![
            "ads.example.com".into(),
            "analytics.example.com".into(),
        ],
        ..Default::default()
    })
    .build();
}

Base URL

Set a base URL for relative resources:

#![allow(unused)]
fn main() {
let options = RenderOptions::builder()
    .base_url("https://example.com/assets/")
    .build();
}

Web Fonts

Web fonts are fully supported:

<style>
@font-face {
    font-family: 'CustomFont';
    src: url('https://example.com/fonts/custom.woff2') format('woff2');
}

body {
    font-family: 'CustomFont', sans-serif;
}
</style>

Configure font defaults:

#![allow(unused)]
fn main() {
let options = RenderOptions::builder()
    .fonts(FontOptions {
        default_sans_serif: "Arial".into(),
        default_serif: "Times New Roman".into(),
        default_monospace: "Courier New".into(),
        use_system_fonts: true,
        enable_web_fonts: true,
        ..Default::default()
    })
    .build();
}

Viewport Configuration

Control the viewport for rendering:

#![allow(unused)]
fn main() {
let options = RenderOptions::builder()
    .viewport(Viewport {
        width: 1920,
        height: 1080,
        device_scale_factor: 2.0, // Retina/HiDPI
    })
    .build();
}

Scale Factor

Adjust the overall scale:

#![allow(unused)]
fn main() {
let options = PdfOptions::builder()
    .scale(0.8)  // 80% scale
    .build();
}

Valid range: 0.1 to 2.0

Background Graphics

Control background printing:

#![allow(unused)]
fn main() {
// Include backgrounds (default)
let options = PdfOptions::builder()
    .print_background(true)
    .build();

// Exclude backgrounds
let options = PdfOptions::builder()
    .print_background(false)
    .build();
}

Page Ranges

Select specific pages:

#![allow(unused)]
fn main() {
let options = PdfOptions::builder()
    .page_ranges("1-5,8,10-12")
    .build();
}

Metadata

Set PDF metadata:

#![allow(unused)]
fn main() {
let options = PdfOptions::builder()
    .metadata(PdfMetadata {
        title: Some("My Document".into()),
        author: Some("Author Name".into()),
        subject: Some("Document Subject".into()),
        keywords: Some("pdf, rust, printwell".into()),
        creator: Some("My Application".into()),
        producer: Some("printwell".into()),
    })
    .build();
}

Performance Tips

  1. Reuse Converter - Create one Converter instance and reuse it
  2. Use ConverterPool - For batch processing, use a pool
  3. Minimize remote resources - Embed resources when possible
  4. Set appropriate timeouts - Avoid waiting for slow resources
  5. Use CSS print media - Optimize CSS for print output

Watermarks

Add text or image watermarks to PDF documents.

Text Watermarks

#![allow(unused)]
fn main() {
use printwell::watermark::{Watermark, WatermarkPosition, add_watermark};

let watermark = Watermark::text("CONFIDENTIAL")
    .position(WatermarkPosition::Center)
    .rotation(45.0)
    .opacity(0.3)
    .font_size(72.0)
    .color(128, 128, 128);

let result = add_watermark(&pdf_data, &watermark)?;
}

Image Watermarks

#![allow(unused)]
fn main() {
let logo = std::fs::read("logo.png")?;

let watermark = Watermark::image(&logo)
    .position(WatermarkPosition::BottomRight)
    .opacity(0.5)
    .scale(0.5);

let result = add_watermark(&pdf_data, &watermark)?;
}

Position Options

PositionDescription
CenterCenter of page
TopLeftTop-left corner
TopCenterTop center
TopRightTop-right corner
MiddleLeftMiddle left
MiddleRightMiddle right
BottomLeftBottom-left corner
BottomCenterBottom center
BottomRightBottom-right corner

Or use custom coordinates:

#![allow(unused)]
fn main() {
let watermark = Watermark::text("DRAFT")
    .custom_position(100.0, 200.0);
}

Layer Options

  • Background - Behind page content (default)
  • Foreground - On top of page content
#![allow(unused)]
fn main() {
let watermark = Watermark::text("SAMPLE")
    .layer(WatermarkLayer::Foreground);
}

Page Selection

Apply watermarks to specific pages:

#![allow(unused)]
fn main() {
// All pages (default)
let watermark = Watermark::text("DRAFT")
    .pages(WatermarkPageSelection::All);

// Odd pages only
let watermark = Watermark::text("DRAFT")
    .pages(WatermarkPageSelection::Odd);

// Even pages only
let watermark = Watermark::text("DRAFT")
    .pages(WatermarkPageSelection::Even);

// First page only
let watermark = Watermark::text("DRAFT")
    .pages(WatermarkPageSelection::First);

// Specific pages
let watermark = Watermark::text("DRAFT")
    .page_list(&[1, 3, 5, 7]);

// Page range
let watermark = Watermark::text("DRAFT")
    .page_range(2, 10);
}

Multiple Watermarks

Add multiple watermarks at once:

#![allow(unused)]
fn main() {
use printwell::watermark::add_watermarks;

let watermarks = vec![
    Watermark::text("DRAFT")
        .position(WatermarkPosition::Center)
        .opacity(0.2),
    Watermark::text("Company Name")
        .position(WatermarkPosition::TopCenter)
        .font_size(12.0)
        .opacity(0.5),
];

let result = add_watermarks(&pdf_data, &watermarks)?;
}

Styling Options

OptionDescriptionDefault
rotationRotation in degrees0.0
opacityOpacity (0.0-1.0)0.5
font_sizeFont size in points72.0
font_nameFont family“Helvetica”
colorRGB colorGray
scaleImage scale factor1.0

Bookmarks

Add navigation bookmarks (outlines) to PDF documents.

Adding Bookmarks

#![allow(unused)]
fn main() {
use printwell::bookmarks::{Bookmark, add_bookmarks};

let bookmarks = vec![
    Bookmark::new("Introduction", 1),
    Bookmark::new("Chapter 1", 3),
    Bookmark::new("Chapter 2", 10),
    Bookmark::new("Conclusion", 25),
];

let result = add_bookmarks(&pdf_data, &bookmarks)?;
}

Hierarchical Bookmarks

Create nested bookmark structures:

#![allow(unused)]
fn main() {
let bookmarks = vec![
    Bookmark::new("Part 1", 1).with_children(vec![
        Bookmark::new("Chapter 1", 2),
        Bookmark::new("Chapter 2", 15).with_children(vec![
            Bookmark::new("Section 2.1", 15),
            Bookmark::new("Section 2.2", 20),
        ]),
    ]),
    Bookmark::new("Part 2", 30).with_children(vec![
        Bookmark::new("Chapter 3", 31),
        Bookmark::new("Chapter 4", 45),
    ]),
];
}

Bookmark Options

#![allow(unused)]
fn main() {
Bookmark::new("Title", page)
    .y_position(500.0)  // Y position on page (optional)
    .open(true)         // Expanded by default
    .level(0)           // Nesting level
}

Extracting Bookmarks

#![allow(unused)]
fn main() {
use printwell::bookmarks::extract_bookmarks;

let bookmarks = extract_bookmarks(&pdf_data)?;

for bookmark in &bookmarks {
    println!("{} -> Page {}", bookmark.title, bookmark.page);
}
}

Node.js Example

import { addBookmarks, extractBookmarks } from 'printwell';

const result = addBookmarks(pdfData, [
    { title: 'Chapter 1', page: 1 },
    { title: 'Chapter 2', page: 10, children: [
        { title: 'Section 2.1', page: 10 },
        { title: 'Section 2.2', page: 15 }
    ]}
]);

const existing = extractBookmarks(pdfData);

Python Example

from printwell import add_bookmarks, extract_bookmarks, Bookmark

bookmarks = [
    Bookmark("Chapter 1", page=1),
    Bookmark("Chapter 2", page=10),
]

result = add_bookmarks(pdf_data, bookmarks)
existing = extract_bookmarks(pdf_data)

Annotations

Add, list, and remove PDF annotations.

Annotation Types

TypeDescription
HighlightHighlight text
UnderlineUnderline text
StrikeoutStrike through text
SquigglySquiggly underline
TextSticky note
FreeTextText box
LineLine shape
SquareRectangle shape
CircleEllipse shape
InkFreehand drawing
StampStamp annotation
LinkHyperlink

Adding Annotations

#![allow(unused)]
fn main() {
use printwell::annotations::{Annotation, AnnotationType, add_annotations};

let annotations = vec![
    Annotation::highlight(1, 100.0, 700.0, 200.0, 20.0)
        .color(255, 255, 0)
        .opacity(0.5),

    Annotation::sticky_note(1, 50.0, 600.0, "Review this")
        .author("Reviewer"),

    Annotation::new(AnnotationType::Square)
        .page(1)
        .rect(100.0, 500.0, 150.0, 100.0)
        .color(255, 0, 0),
];

let result = add_annotations(&pdf_data, &annotations)?;
}

Listing Annotations

#![allow(unused)]
fn main() {
use printwell::annotations::list_annotations;

let annotations = list_annotations(&pdf_data)?;

for ann in &annotations {
    println!(
        "Page {}: {:?} at ({}, {})",
        ann.page, ann.annotation_type, ann.rect.x, ann.rect.y
    );
}
}

Removing Annotations

#![allow(unused)]
fn main() {
use printwell::annotations::remove_annotations;

// Remove all annotations
let result = remove_annotations(&pdf_data, None, None)?;

// Remove from specific page
let result = remove_annotations(&pdf_data, Some(1), None)?;

// Remove specific types
let result = remove_annotations(
    &pdf_data,
    None,
    Some(&[AnnotationType::Highlight, AnnotationType::Underline])
)?;
}

Node.js Example

import { addAnnotations, listAnnotations, removeAnnotations } from 'printwell';

const result = addAnnotations(pdfData, [
    {
        type: 'Highlight',
        page: 1,
        rect: { x: 100, y: 700, width: 200, height: 20 },
        color: { r: 255, g: 255, b: 0 },
        opacity: 0.5
    }
]);

const annotations = listAnnotations(pdfData);
const cleaned = removeAnnotations(pdfData, { types: ['Highlight'] });

Python Example

from printwell import (
    add_annotations, list_annotations, remove_annotations,
    Annotation, AnnotationType
)

result = add_annotations(pdf_data, [
    Annotation.highlight(page=1, x=100, y=700, width=200, height=20)
])

annotations = list_annotations(pdf_data)
cleaned = remove_annotations(pdf_data, annotation_types=[AnnotationType.Highlight])

Encryption

Commercial License Required - This feature requires a commercial license. See printwell.dev/pricing.

Password-protect PDF documents with AES or RC4 encryption.

Encryption Algorithms

AlgorithmSecurityCompatibility
Aes256HighPDF 2.0+
Aes128MediumPDF 1.6+
Rc4_128LowPDF 1.4+ (legacy)

Encrypting PDFs

#![allow(unused)]
fn main() {
use printwell::encrypt::{encrypt_pdf, EncryptionOptions, Permissions};

let options = EncryptionOptions::builder()
    .owner_password("admin123")
    .user_password("user123")
    .algorithm(EncryptionAlgorithm::Aes256)
    .permissions(Permissions {
        print: true,
        copy: false,
        modify: false,
        annotate: true,
        fill_forms: true,
        extract_accessibility: true,
        assemble: false,
        print_high_quality: true,
    })
    .build();

let encrypted = encrypt_pdf(&pdf_data, &options)?;
}

Permissions

PermissionDescription
printAllow printing
copyAllow text/image copying
modifyAllow document modification
annotateAllow adding annotations
fill_formsAllow form filling
extract_accessibilityAllow accessibility extraction
assembleAllow page assembly
print_high_qualityAllow high-quality printing

Permission Presets

#![allow(unused)]
fn main() {
// All permissions
let perms = Permissions::all();

// No permissions (maximum restriction)
let perms = Permissions::none();

// Print only
let perms = Permissions::print_only();
}

Decrypting PDFs

#![allow(unused)]
fn main() {
use printwell::encrypt::decrypt_pdf;

let decrypted = decrypt_pdf(&encrypted_data, "password")?;
}

Node.js Example

import { encryptPdf, decryptPdf } from 'printwell';

const encrypted = encryptPdf(pdfData, {
    ownerPassword: 'admin123',
    userPassword: 'user123',
    algorithm: 'Aes256',
    permissions: {
        print: true,
        copy: false,
        modify: false
    }
});

const decrypted = decryptPdf(encrypted, 'admin123');

Python Example

from printwell import encrypt_pdf, decrypt_pdf, EncryptionOptions, Permissions

options = EncryptionOptions(
    owner_password="admin123",
    user_password="user123",
    permissions=Permissions(print=True, copy=False, modify=False)
)

encrypted = encrypt_pdf(pdf_data, options)
decrypted = decrypt_pdf(encrypted, "admin123")

Owner vs User Password

  • Owner password: Full access, can change permissions
  • User password: Opens document with specified permissions

If only owner password is set, anyone can open the document but restrictions apply.

Digital Signatures

Commercial License Required - This feature requires a commercial license. See printwell.dev/pricing.

Sign PDF documents with PAdES-compliant digital signatures.

Signature Levels

LevelDescription
PadesBBasic signature
PadesTWith trusted timestamp
PadesLTLong-term validation data
PadesLTALong-term archival

Invisible Signatures

#![allow(unused)]
fn main() {
use printwell::signing::{sign_pdf, SigningOptions};

let cert_data = std::fs::read("certificate.p12")?;

let options = SigningOptions {
    reason: Some("Document approved".into()),
    location: Some("New York, NY".into()),
    contact_info: Some("signer@example.com".into()),
    signature_level: SignatureLevel::PadesB,
    ..Default::default()
};

let signed = sign_pdf(&pdf_data, &cert_data, "cert_password", options)?;
}

Visible Signatures

#![allow(unused)]
fn main() {
use printwell::signing::{sign_pdf_visible, SignatureAppearance};

let appearance = SignatureAppearance {
    page: 1,
    x: 400.0,
    y: 50.0,
    width: 200.0,
    height: 75.0,
    show_name: true,
    show_date: true,
    show_reason: true,
    background_image: None, // Optional logo
};

let signed = sign_pdf_visible(
    &pdf_data,
    &cert_data,
    "password",
    SigningOptions::default(),
    appearance
)?;
}

Verifying Signatures

#![allow(unused)]
fn main() {
use printwell::signing::verify_signatures;

let results = verify_signatures(&signed_pdf)?;

for result in &results {
    println!("Signer: {}", result.signer_name);
    println!("Valid: {}", result.is_valid);
    println!("Covers whole doc: {}", result.covers_whole_document);
    println!("Time: {:?}", result.signing_time);

    if !result.cert_warnings.is_empty() {
        println!("Warnings: {:?}", result.cert_warnings);
    }
}
}

Extracting Signature Info

#![allow(unused)]
fn main() {
use printwell::signing::extract_signatures;

let signatures = extract_signatures(&pdf_data)?;

for sig in &signatures {
    println!("Signer: {:?}", sig.signer_name);
    println!("Reason: {:?}", sig.reason);
    println!("Location: {:?}", sig.location);
    println!("Time: {:?}", sig.signing_time);
}
}

Certificate Requirements

Certificates must be in PKCS#12 format (.p12 or .pfx):

  • Must contain private key
  • Should include certificate chain
  • Supported algorithms: RSA, ECDSA (P-256, P-384)

Creating Test Certificates

# Generate self-signed certificate (for testing only)
openssl req -x509 -newkey rsa:2048 -keyout key.pem -out cert.pem -days 365 -nodes
openssl pkcs12 -export -out certificate.p12 -inkey key.pem -in cert.pem

Node.js Example

import { signPdf, signPdfVisible, verifySignatures } from 'printwell';

const signed = signPdf(pdfData, certData, 'password', {
    reason: 'Approved',
    location: 'New York',
    signatureLevel: 'PadesB'
});

const signedVisible = signPdfVisible(pdfData, certData, 'password',
    { reason: 'Approved' },
    { page: 1, x: 400, y: 50, width: 200, height: 75 }
);

const results = verifySignatures(signedData);

Python Example

from printwell import sign_pdf, sign_pdf_visible, verify_signatures

signed = sign_pdf(pdf_data, cert_data, "password",
    SigningOptions(reason="Approved", location="New York"))

results = verify_signatures(signed)
for r in results:
    print(f"{r.signer_name}: {'Valid' if r.is_valid else 'Invalid'}")

Form Fields

Commercial License Required - This feature requires a commercial license. See printwell.dev/pricing.

Create interactive PDF forms with text fields, checkboxes, dropdowns, and signature fields.

Field Types

TypeDescription
TextSingle or multi-line text input
CheckboxBoolean checkbox
DropdownSelection from options
SignatureDigital signature field

Adding Form Fields

#![allow(unused)]
fn main() {
use printwell::forms::{add_form_fields, TextField, Checkbox, Dropdown, SignatureField, Rect};

let fields = FormFields {
    text_fields: vec![
        TextField {
            name: "full_name".into(),
            page: 1,
            rect: Rect::new(100.0, 700.0, 200.0, 20.0),
            default_value: None,
            required: true,
            ..Default::default()
        },
        TextField {
            name: "email".into(),
            page: 1,
            rect: Rect::new(100.0, 660.0, 200.0, 20.0),
            required: true,
            ..Default::default()
        },
    ],
    checkboxes: vec![
        Checkbox {
            name: "agree_terms".into(),
            page: 1,
            rect: Rect::new(100.0, 620.0, 15.0, 15.0),
            checked: false,
            export_value: "Yes".into(),
        },
    ],
    dropdowns: vec![
        Dropdown {
            name: "country".into(),
            page: 1,
            rect: Rect::new(100.0, 580.0, 150.0, 20.0),
            options: vec!["USA".into(), "Canada".into(), "UK".into()],
            selected_index: None,
            editable: false,
        },
    ],
    signature_fields: vec![
        SignatureField {
            name: "signature".into(),
            page: 1,
            rect: Rect::new(100.0, 500.0, 200.0, 50.0),
        },
    ],
};

let result = add_form_fields(&pdf_data, &fields)?;
}

Text Field Options

#![allow(unused)]
fn main() {
TextField {
    name: "notes".into(),
    page: 1,
    rect: Rect::new(100.0, 400.0, 300.0, 100.0),
    multiline: true,        // Multi-line text area
    password: false,        // Password field
    max_length: Some(500),  // Character limit
    required: true,
    read_only: false,
    font_size: 12.0,
    font_name: Some("Helvetica".into()),
    default_value: Some("Default text".into()),
}
}

Form Validation

Validate form field values against rules:

#![allow(unused)]
fn main() {
use printwell::forms::{validate_form_fields, ValidationRule};

let rules = vec![
    ValidationRule {
        field_name: "full_name".into(),
        required: true,
        min_length: Some(2),
        max_length: Some(100),
        ..Default::default()
    },
    ValidationRule {
        field_name: "email".into(),
        required: true,
        pattern: Some(r"^[^@]+@[^@]+\.[^@]+$".into()),
        pattern_message: Some("Invalid email format".into()),
        ..Default::default()
    },
    ValidationRule {
        field_name: "age".into(),
        min_value: Some(18.0),
        max_value: Some(120.0),
        ..Default::default()
    },
];

let summary = validate_form_fields(&form_elements, &rules);

if !summary.all_valid {
    for result in &summary.results {
        if !result.is_valid {
            println!("{}: {:?}", result.field_name, result.errors);
        }
    }
}
}

Node.js Example

import { addFormFields, validateFormFields } from 'printwell';

const result = addFormFields(pdfData, [
    {
        fieldType: 'text',
        name: 'full_name',
        page: 1,
        rect: { x: 100, y: 700, width: 200, height: 20 },
        required: true
    },
    {
        fieldType: 'checkbox',
        name: 'agree',
        page: 1,
        rect: { x: 100, y: 650, width: 15, height: 15 }
    }
]);

const validation = validateFormFields(elements, [
    { fieldName: 'full_name', required: true, minLength: 2 }
]);

Python Example

from printwell import add_form_fields, TextField, FormCheckbox, FormRect

result = add_form_fields(
    pdf_data,
    text_fields=[
        TextField(
            name="full_name",
            page=1,
            rect=FormRect(x=100, y=700, width=200, height=20),
            required=True
        )
    ],
    checkboxes=[
        FormCheckbox(
            name="agree",
            page=1,
            rect=FormRect(x=100, y=650, width=15, height=15)
        )
    ]
)

PDF/A Archival

Commercial License Required - This feature requires a commercial license. See printwell.dev/pricing.

Create and validate PDF/A compliant documents for long-term archival.

PDF/A Levels

LevelStandardDescription
PdfA1bISO 19005-1Basic visual preservation
PdfA1aISO 19005-1Full accessibility (tagged)
PdfA2bISO 19005-2Modern features (JPEG2000, transparency)
PdfA2uISO 19005-2Unicode text mapping
PdfA2aISO 19005-2Full accessibility
PdfA3bISO 19005-3Embedded files allowed
PdfA3uISO 19005-3Unicode + embedded files
PdfA3aISO 19005-3Full accessibility + embedded

Validating PDF/A Compliance

#![allow(unused)]
fn main() {
use printwell::pdfa::{validate_pdfa, PdfALevel};

let result = validate_pdfa(&pdf_data, PdfALevel::PdfA2b)?;

println!("Compliant: {}", result.is_compliant);
println!("Errors: {}", result.error_count);
println!("Warnings: {}", result.warning_count);

for issue in &result.issues {
    println!(
        "[{:?}] {:?}: {}",
        issue.severity,
        issue.category,
        issue.message
    );
}
}

Issue Categories

CategoryDescription
FontsFont embedding issues
ColorColor space problems
MetadataXMP metadata issues
StructureDocument structure
ActionsJavaScript/actions
EncryptionSecurity restrictions
AnnotationsAnnotation problems
TransparencyTransparency issues
AttachmentsEmbedded file issues

Adding PDF/A Metadata

#![allow(unused)]
fn main() {
use printwell::pdfa::add_pdfa_metadata;

let pdfa_doc = add_pdfa_metadata(
    &pdf_data,
    PdfALevel::PdfA2b,
    Some("Document Title"),
    Some("Author Name"),
)?;
}

Node.js Example

import { validatePdfa, addPdfaMetadata } from 'printwell';

const result = validatePdfa(pdfData, 'PdfA2b');
console.log(`Compliant: ${result.isCompliant}`);

for (const issue of result.issues) {
    console.log(`${issue.severity}: ${issue.message}`);
}

const pdfaDoc = addPdfaMetadata(pdfData, 'PdfA2b', {
    title: 'Archived Document',
    author: 'Author Name'
});

Python Example

from printwell import validate_pdfa, add_pdfa_metadata, PdfALevel

result = validate_pdfa(pdf_data, PdfALevel.PdfA2b)
print(f"Compliant: {result.is_compliant}")

for issue in result.issues:
    print(f"{issue.severity}: {issue.message}")

pdfa_doc = add_pdfa_metadata(pdf_data, PdfALevel.PdfA2b,
    title="Archived Document", author="Author")

Best Practices for PDF/A

  1. Embed all fonts - Use embed_fonts: true in PdfOptions
  2. Use RGB or CMYK color spaces - Avoid device-dependent colors
  3. Avoid JavaScript - Not allowed in PDF/A
  4. Include document metadata - Title, author, etc.
  5. Use tagged PDFs - For PDF/A-1a, 2a, 3a levels

PDF/UA Accessibility

Commercial License Required - This feature requires a commercial license. See printwell.dev/pricing.

Create accessible PDFs compliant with PDF/UA (Universal Accessibility) standards.

PDF/UA Levels

LevelStandardDescription
PdfUA1ISO 14289-1:2014Original PDF/UA standard
PdfUA2ISO 14289-2:2024Updated standard

Validating PDF/UA Compliance

#![allow(unused)]
fn main() {
use printwell::pdfua::{validate_pdfua, PdfUALevel};

let result = validate_pdfua(&pdf_data, PdfUALevel::PdfUA1)?;

println!("Compliant: {}", result.is_compliant);
println!("Pages checked: {}", result.pages_checked);
println!("Tagged elements: {}", result.tagged_elements);

for issue in &result.issues {
    println!(
        "[{:?}] {:?}: {} (Page {})",
        issue.severity,
        issue.category,
        issue.description,
        issue.page
    );
    if let Some(suggestion) = &issue.suggestion {
        println!("  Suggestion: {}", suggestion);
    }
}
}

Issue Categories

CategoryDescription
StructureDocument structure tags
TagsMissing or incorrect tags
AltTextMissing alternative text
LanguageLanguage specification
ReadingOrderContent reading order
MetadataAccessibility metadata
TablesTable structure
HeadingsHeading hierarchy
ColorColor contrast issues
FontsFont accessibility
NavigationNavigation aids

Adding PDF/UA Metadata

#![allow(unused)]
fn main() {
use printwell::pdfua::{add_pdfua_metadata, AccessibilityOptions};

let options = AccessibilityOptions {
    language: "en".into(),
    title: "Accessible Document".into(),
    generate_placeholder_alt: false,
    include_reading_order: true,
    generate_outline: true,
};

let accessible = add_pdfua_metadata(&pdf_data, PdfUALevel::PdfUA1, Some(options))?;
}

Node.js Example

import { validatePdfua, addPdfuaMetadata } from 'printwell';

const result = validatePdfua(pdfData, 'PdfUA1');
console.log(`Accessible: ${result.isCompliant}`);

const accessible = addPdfuaMetadata(pdfData, 'PdfUA1', {
    language: 'en',
    title: 'Accessible Document',
    generateOutline: true
});

Python Example

from printwell import validate_pdfua, add_pdfua_metadata, PdfUALevel, AccessibilityOptions

result = validate_pdfua(pdf_data, PdfUALevel.PdfUA1)
print(f"Accessible: {result.is_compliant}")

options = AccessibilityOptions(
    language="en",
    title="Accessible Document",
    generate_outline=True
)
accessible = add_pdfua_metadata(pdf_data, PdfUALevel.PdfUA1, options)

Best Practices for Accessible PDFs

1. Document Structure

Use semantic HTML that maps to PDF tags:

<article>
    <h1>Main Title</h1>
    <section>
        <h2>Section Heading</h2>
        <p>Paragraph content...</p>
    </section>
</article>

2. Alternative Text

Always provide alt text for images:

<img src="chart.png" alt="Sales chart showing 20% growth in Q4">

3. Language Specification

Set the document language:

<html lang="en">

4. Reading Order

Ensure logical reading order in your HTML source.

5. Tables

Use proper table markup:

<table>
    <caption>Quarterly Sales</caption>
    <thead>
        <tr><th>Quarter</th><th>Sales</th></tr>
    </thead>
    <tbody>
        <tr><td>Q1</td><td>$100k</td></tr>
    </tbody>
</table>

6. Headings

Maintain proper heading hierarchy (h1 → h2 → h3).

7. Color Contrast

Ensure sufficient contrast ratios (WCAG 2.1 guidelines).

API Reference

Detailed API documentation is generated from source code.

Rust API

Full Rust API documentation with examples:

Rust API Documentation

Generated with cargo doc. Includes:

  • All public types and functions
  • Module documentation
  • Code examples
  • Type signatures

Generate Locally

cargo xtask docs --target rust --open

Node.js API

TypeScript definitions with JSDoc comments:

Node.js API Documentation

Generated with TypeDoc. Includes:

  • All exported functions and classes
  • Type definitions
  • Parameter descriptions

Generate Locally

cargo xtask docs --target node --open

Python API

Python type stubs with docstrings:

Python API Documentation

Generated with pdoc. Includes:

  • All classes and functions
  • Type hints
  • Docstrings

Generate Locally

cargo xtask docs --target python --open

Quick Reference

Core Types

TypeRustNode.jsPython
ConverterConverterConverterConverter
PoolConverterPoolConverterPoolConverterPool
ResultPdfResultPdfResultPdfResult
OptionsPdfOptionsPdfOptionsPdfOptions

Functions by Feature

FeatureFunctions
Conversionhtml_to_pdf, url_to_pdf
Watermarksadd_watermark, add_watermarks
Bookmarksadd_bookmarks, extract_bookmarks
Annotationsadd_annotations, list_annotations, remove_annotations
Encryptionencrypt_pdf, decrypt_pdf
Signingsign_pdf, sign_pdf_visible, verify_signatures
Formsadd_form_fields, validate_form_fields
PDF/Avalidate_pdfa, add_pdfa_metadata
PDF/UAvalidate_pdfua, add_pdfua_metadata

Building from Source

This guide covers building printwell from source.

Prerequisites

  • Rust 1.92+ (with rustup)
  • Docker (for building Chromium)
  • Node.js 18+ (for Node.js bindings)
  • Python 3.9+ (for Python bindings)
  • ~100GB disk space (Chromium source)

Quick Build

# Clone repository
git clone https://github.com/printwell-dev/core
cd printwell

# Initialize (downloads Chromium, applies patches)
cargo xtask init

# Build native library
cargo xtask build

# Build CLI
cargo build --release -p printwell-cli

Build Steps Explained

1. Initialize

cargo xtask init

This command:

  • Creates the chromium/ directory
  • Runs fetch chromium via depot_tools
  • Applies patches from patches/
  • Sets up GN build files

2. Build Native Library

cargo xtask build

This command:

  • Runs inside Docker for reproducibility
  • Compiles Chromium’s Blink, Skia, and PDFium
  • Produces libprintwell_native.so (~50MB)

Build options:

# Debug build (faster, larger)
cargo xtask build --debug

# Release build (optimized)
cargo xtask build --release

# Specify CPU cores
cargo xtask build --jobs 8

3. Build Rust Crates

cargo build --release

4. Build Language Bindings

# Node.js binding
cargo xtask bindings node

# Python binding
cargo xtask bindings python

# Both
cargo xtask bindings all

Docker Build Environment

The build uses Docker to ensure reproducibility:

FROM debian:bookworm-slim

# Build tools
RUN apt-get update && apt-get install -y \
    build-essential \
    clang \
    lld \
    ninja-build \
    python3 \
    git \
    curl

GN Configuration

Build settings in docker/gn_args/common.gn:

# Disable features we don't need
blink_enable_javascript = false
enable_pdf = false
enable_extensions = false
media_use_ffmpeg = false

# Use headless rendering
use_ozone = true
ozone_platform_headless = true

Updating Chromium

# Update to latest
cd chromium/src
git fetch
git checkout <new-version>
gclient sync

# Re-apply patches
cd ../..
cargo xtask chromium patch

Troubleshooting

Build Fails with Memory Error

Reduce parallelism:

cargo xtask build --jobs 4

Missing depot_tools

git clone https://chromium.googlesource.com/chromium/tools/depot_tools.git
export PATH="$PATH:$(pwd)/depot_tools"

Docker Permission Denied

sudo usermod -aG docker $USER
# Log out and back in

Development Workflow

# Run tests
cargo xtask test

# Run lints
cargo xtask lint

# Run benchmarks
cargo xtask bench

# Run e2e tests
cargo xtask e2e

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

Contributing

Guidelines for contributing to printwell.

Getting Started

  1. Fork the repository

  2. Clone your fork:

    git clone https://github.com/YOUR_USERNAME/core
    cd core
    
  3. Set up the development environment:

    # Initialize Chromium (first time only)
    cargo xtask init
    
    # Build native library
    cargo xtask build
    

Development Workflow

Running Tests

# All tests
cargo xtask test

# Rust unit tests only
cargo test --workspace

# E2E tests
cargo xtask e2e

# Specific test
cargo test --package printwell test_encryption

Running Lints

# All lints
cargo xtask lint

# Rust only
cargo clippy --workspace --all-targets
cargo fmt --check

# Fix formatting
cargo fmt

Running Benchmarks

cargo xtask bench

Code Style

Rust

  • Follow standard Rust conventions
  • Use cargo fmt for formatting
  • Use cargo clippy for linting
  • Write doc comments for public APIs
#![allow(unused)]
fn main() {
/// Converts HTML to PDF.
///
/// # Arguments
///
/// * `html` - The HTML content to convert
/// * `options` - Conversion options
///
/// # Returns
///
/// A `PdfResult` containing the generated PDF bytes.
///
/// # Errors
///
/// Returns `PrintwellError::Conversion` if the conversion fails.
pub fn convert_html(&self, html: &str, options: PdfOptions) -> Result<PdfResult> {
    // ...
}
}

Commit Messages

Use conventional commits:

feat: add watermark rotation support
fix: handle empty HTML input
docs: update installation guide
test: add encryption round-trip test
refactor: simplify converter pool logic

Pull Request Process

  1. Create a feature branch:

    git checkout -b feature/my-feature
    
  2. Make your changes with tests

  3. Ensure all checks pass:

    cargo xtask test
    cargo xtask lint
    
  4. Push and create a PR:

    git push origin feature/my-feature
    
  5. Fill out the PR template

  6. Address review feedback

Adding New Features

To printwell (Pure Rust)

  1. Create module in crates/printwell/src/
  2. Add exports to crates/printwell/src/lib.rs
  3. Add feature flag if needed in Cargo.toml
  4. Add tests in the module
  5. Update documentation

To printwell-core (Native)

  1. Add C++ code in cpp/src/
  2. Update cpp/BUILD.gn if needed
  3. Add FFI in crates/printwell-sys/src/lib.rs
  4. Add safe wrapper in crates/printwell-core/src/
  5. Rebuild native library: cargo xtask build

To Bindings

  1. Add types to bindings/shared/src/lib.rs
  2. Add Node.js binding in bindings/node/src/lib.rs
  3. Add Python binding in bindings/python/src/lib.rs
  4. Update TypeScript types in bindings/node/index.d.ts
  5. Update Python stubs in bindings/python/printwell/__init__.pyi
  6. Add tests for both bindings

Testing Guidelines

Unit Tests

Test individual functions in isolation:

#![allow(unused)]
fn main() {
#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn test_watermark_position() {
        let pos = WatermarkPosition::Center;
        assert_eq!(pos.to_css(), "center");
    }
}
}

Integration Tests

Test complete workflows:

#![allow(unused)]
fn main() {
#[test]
fn test_html_to_pdf_with_watermark() {
    let converter = Converter::new().unwrap();
    let result = converter.convert_html("<h1>Test</h1>", PdfOptions::default()).unwrap();

    let watermark = Watermark::text("DRAFT");
    let watermarked = add_watermark(&result.bytes, &watermark).unwrap();

    assert!(!watermarked.is_empty());
}
}

E2E Tests

Located in e2e/ directory. Test CLI commands:

cargo xtask e2e

Documentation

API Documentation

Use rustdoc comments:

#![allow(unused)]
fn main() {
/// Brief description.
///
/// Longer description with more details.
///
/// # Examples
///
/// ```rust
/// use printwell::watermark::{add_watermark, Watermark};
///
/// let watermark = Watermark::text("CONFIDENTIAL");
/// let result = add_watermark(&pdf_data, &watermark)?;
/// ```
pub fn add_watermark(pdf: &[u8], watermark: &Watermark) -> Result<Vec<u8>> {
    // ...
}
}

User Documentation

Update mdBook docs in docs/src/:

# Build and serve docs
cargo xtask docs --target book --serve

Reporting Issues

Bug Reports

Include:

  • printwell version
  • Operating system
  • Minimal reproduction steps
  • Expected vs actual behavior

Feature Requests

Include:

  • Use case description
  • Proposed API (if applicable)
  • Any alternatives considered

License

Contributions are licensed under the same terms as the project (see LICENSE file).