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 - Get printwell set up
- Quick Start - Your first PDF in 5 minutes
- Features - Explore all capabilities
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
| Feature | Description | License |
|---|---|---|
default | Core conversion only (no PDF post-processing) | AGPL |
watermark | Add text and image watermarks | AGPL |
bookmarks | PDF outline/bookmark management | AGPL |
annotations | PDF annotation support | AGPL |
encrypt | PDF encryption with password protection | Commercial |
signing | Digital signature support (PAdES) | Commercial |
timestamp | Timestamp server support for signatures | Commercial |
forms | PDF form field creation and validation | Commercial |
pdfa | PDF/A archival compliance | Commercial |
pdfua | PDF/UA accessibility compliance | Commercial |
pdf-full | All PDF post-processing features | Commercial |
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 - Create your first PDF
- CLI Reference - Full CLI documentation
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 - Full command documentation
- Rust Guide - Detailed Rust usage
- Node.js Guide - Detailed Node.js usage
- Python Guide - Detailed Python usage
- Features - Explore all capabilities
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:
| Option | Description | Default |
|---|---|---|
-o, --output <FILE> | Output PDF file path | - |
--page-size <SIZE> | Page size: A3, A4, A5, Letter, Legal, Tabloid | A4 |
--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 |
--landscape | Use landscape orientation | false |
--no-background | Don’t print background graphics | false |
--scale <FACTOR> | Scale factor | 1.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 timeout | 30s |
--font <SPEC> | Custom font (format: “family:path”) | - |
Metadata Options:
| Option | Description |
|---|---|
--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:
| Option | Description |
|---|---|
--detect-forms | Detect HTML form elements and output their positions |
--convert-forms | Auto-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:
| Option | Description |
|---|---|
--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:
| Option | Description | Default |
|---|---|---|
-o, --output-dir <DIR> | Output directory for generated PDFs | . |
--workers <NUM> | Maximum concurrent conversions | 4 |
--page-size <SIZE> | Page size | A4 |
--background | Print background colors and images | false |
--landscape | Use landscape orientation | false |
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:
| Option | Description | Default |
|---|---|---|
-o, --output <FILE> | Output PDF file path | Required |
--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 points | 72 |
--color <COLOR> | Color as hex (e.g., “#FF0000”) or name (e.g., “gray”) | gray |
--foreground | Place watermark in foreground | false |
--pages <SELECTION> | Pages: “1,3,5”, “1-10”, “odd”, “even”, “first”, “last” | all |
--scale <FACTOR> | Scale factor for the watermark | 1.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:
| Option | Description |
|---|---|
-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:
| Option | Description |
|---|---|
-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”) |
--list | List existing annotations |
--remove | Remove 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:
| Option | Description |
|---|---|
--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:
| Option | Description |
|---|---|
--allow-print | Allow printing |
--allow-print-hq | Allow high-quality printing |
--allow-copy | Allow copying text and graphics |
--allow-accessibility | Allow extracting text for accessibility |
--allow-modify | Allow modifying the document |
--allow-annotate | Allow adding annotations |
--allow-assemble | Allow assembling the document |
--allow-fill-forms | Allow filling form fields |
--allow-all | Allow all permissions |
Other Options:
| Option | Description | Default |
|---|---|---|
--algorithm <ALG> | aes256, aes128, or rc4 | aes256 |
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:
| Option | Description |
|---|---|
--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:
| Option | Description | Default |
|---|---|---|
--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, LTA | B |
--timestamp-url <URL> | Timestamp server URL | - |
--visible <FIELD> | Visible signature field name | - |
--position <SPEC> | Visible signature position (format: “page:x,y,w,h”) | - |
--certify | Create a certification signature | false |
--mdp <LEVEL> | MDP permissions: 1=no-changes, 2=form-filling, 3=annotations | 2 |
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:
| Option | Description | Default |
|---|---|---|
--format <FMT> | Output format: text, json | text |
--use-system-trust | Use system trust store for certificate chain validation | false |
list-fields
List signature fields in a PDF. Requires the signing feature. Commercial license required.
printwell list-fields [OPTIONS] <INPUT>
Options:
| Option | Description | Default |
|---|---|---|
--format <FMT> | Output format: text, json | text |
forms
Add form fields to PDF. Requires the forms feature. Commercial license required.
printwell forms [OPTIONS] <INPUT> -o <OUTPUT>
Options:
| Option | Description |
|---|---|
--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:
| Option | Description | Default |
|---|---|---|
--level <LEVEL> | PDF/A level: 1b, 1a, 2b, 2u, 2a, 3b, 3u, 3a | 2b |
--format <FMT> | Output format: text, json | text |
pdfa-convert
Add PDF/A metadata to a PDF. Requires the pdfa feature. Commercial license required.
printwell pdfa-convert [OPTIONS] <INPUT> -o <OUTPUT>
Options:
| Option | Description | Default |
|---|---|---|
--level <LEVEL> | PDF/A level: 1b, 1a, 2b, 2u, 2a, 3b, 3u, 3a | 2b |
--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:
| Option | Description | Default |
|---|---|---|
--level <LEVEL> | PDF/UA level: 1, 2 | 1 |
--format <FMT> | Output format: text, json | text |
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:
| Option | Description | Default |
|---|---|---|
--level <LEVEL> | PDF/UA level: 1, 2 | 1 |
--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:
| Option | Description |
|---|---|
--renderer | Show 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
| Code | Description |
|---|---|
| 0 | Success |
| 1 | General error |
| 2 | Invalid arguments |
| 3 | Input file not found |
| 4 | Output write error |
| 5 | Conversion error |
| 6 | Validation 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
ConverterisSend + Syncand can be shared across threadsConverterPoolmanages 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
| Feature | Description | Dependencies |
|---|---|---|
default | Core + all PDF features | - |
pdf-full | All PDF post-processing | - |
signing | Digital signatures | p12, x509-cert, rsa, ecdsa, cms |
forms | PDF form fields | regex |
pdfa | PDF/A compliance | chrono |
pdfua | PDF/UA accessibility | chrono |
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
ES Modules (Recommended)
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:
- HTML is parsed and a DOM tree is built
- CSS is applied and the layout is computed
- The page is rendered using Skia graphics
- 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 (
@pagerules)
Print-Specific CSS
@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
| Size | Dimensions (mm) |
|---|---|
| A3 | 297 × 420 |
| A4 | 210 × 297 |
| A5 | 148 × 210 |
| Letter | 216 × 279 |
| Legal | 216 × 356 |
| Tabloid | 279 × 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 titlepageNumber- Current page numbertotalPages- Total page counturl- Document URLdate- 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
- Reuse Converter - Create one
Converterinstance and reuse it - Use ConverterPool - For batch processing, use a pool
- Minimize remote resources - Embed resources when possible
- Set appropriate timeouts - Avoid waiting for slow resources
- 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
| Position | Description |
|---|---|
Center | Center of page |
TopLeft | Top-left corner |
TopCenter | Top center |
TopRight | Top-right corner |
MiddleLeft | Middle left |
MiddleRight | Middle right |
BottomLeft | Bottom-left corner |
BottomCenter | Bottom center |
BottomRight | Bottom-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
| Option | Description | Default |
|---|---|---|
rotation | Rotation in degrees | 0.0 |
opacity | Opacity (0.0-1.0) | 0.5 |
font_size | Font size in points | 72.0 |
font_name | Font family | “Helvetica” |
color | RGB color | Gray |
scale | Image scale factor | 1.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
| Type | Description |
|---|---|
Highlight | Highlight text |
Underline | Underline text |
Strikeout | Strike through text |
Squiggly | Squiggly underline |
Text | Sticky note |
FreeText | Text box |
Line | Line shape |
Square | Rectangle shape |
Circle | Ellipse shape |
Ink | Freehand drawing |
Stamp | Stamp annotation |
Link | Hyperlink |
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
| Algorithm | Security | Compatibility |
|---|---|---|
Aes256 | High | PDF 2.0+ |
Aes128 | Medium | PDF 1.6+ |
Rc4_128 | Low | PDF 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
| Permission | Description |
|---|---|
print | Allow printing |
copy | Allow text/image copying |
modify | Allow document modification |
annotate | Allow adding annotations |
fill_forms | Allow form filling |
extract_accessibility | Allow accessibility extraction |
assemble | Allow page assembly |
print_high_quality | Allow 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
| Level | Description |
|---|---|
PadesB | Basic signature |
PadesT | With trusted timestamp |
PadesLT | Long-term validation data |
PadesLTA | Long-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
| Type | Description |
|---|---|
| Text | Single or multi-line text input |
| Checkbox | Boolean checkbox |
| Dropdown | Selection from options |
| Signature | Digital 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
| Level | Standard | Description |
|---|---|---|
PdfA1b | ISO 19005-1 | Basic visual preservation |
PdfA1a | ISO 19005-1 | Full accessibility (tagged) |
PdfA2b | ISO 19005-2 | Modern features (JPEG2000, transparency) |
PdfA2u | ISO 19005-2 | Unicode text mapping |
PdfA2a | ISO 19005-2 | Full accessibility |
PdfA3b | ISO 19005-3 | Embedded files allowed |
PdfA3u | ISO 19005-3 | Unicode + embedded files |
PdfA3a | ISO 19005-3 | Full 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
| Category | Description |
|---|---|
Fonts | Font embedding issues |
Color | Color space problems |
Metadata | XMP metadata issues |
Structure | Document structure |
Actions | JavaScript/actions |
Encryption | Security restrictions |
Annotations | Annotation problems |
Transparency | Transparency issues |
Attachments | Embedded 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
- Embed all fonts - Use
embed_fonts: truein PdfOptions - Use RGB or CMYK color spaces - Avoid device-dependent colors
- Avoid JavaScript - Not allowed in PDF/A
- Include document metadata - Title, author, etc.
- 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
| Level | Standard | Description |
|---|---|---|
PdfUA1 | ISO 14289-1:2014 | Original PDF/UA standard |
PdfUA2 | ISO 14289-2:2024 | Updated 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
| Category | Description |
|---|---|
Structure | Document structure tags |
Tags | Missing or incorrect tags |
AltText | Missing alternative text |
Language | Language specification |
ReadingOrder | Content reading order |
Metadata | Accessibility metadata |
Tables | Table structure |
Headings | Heading hierarchy |
Color | Color contrast issues |
Fonts | Font accessibility |
Navigation | Navigation 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:
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:
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:
Generated with pdoc. Includes:
- All classes and functions
- Type hints
- Docstrings
Generate Locally
cargo xtask docs --target python --open
Quick Reference
Core Types
| Type | Rust | Node.js | Python |
|---|---|---|---|
| Converter | Converter | Converter | Converter |
| Pool | ConverterPool | ConverterPool | ConverterPool |
| Result | PdfResult | PdfResult | PdfResult |
| Options | PdfOptions | PdfOptions | PdfOptions |
Functions by Feature
| Feature | Functions |
|---|---|
| Conversion | html_to_pdf, url_to_pdf |
| Watermarks | add_watermark, add_watermarks |
| Bookmarks | add_bookmarks, extract_bookmarks |
| Annotations | add_annotations, list_annotations, remove_annotations |
| Encryption | encrypt_pdf, decrypt_pdf |
| Signing | sign_pdf, sign_pdf_visible, verify_signatures |
| Forms | add_form_fields, validate_form_fields |
| PDF/A | validate_pdfa, add_pdfa_metadata |
| PDF/UA | validate_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 chromiumvia 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 converterConverterPool- Thread-safe pool of convertersPdfOptions- Conversion settingsPdfResult- Conversion result with PDF bytes
printwell
PDF post-processing features (no native dependency).
crates/printwell/
├── src/
│ ├── lib.rs # Module exports
│ ├── watermark.rs # Watermarks
│ ├── bookmarks.rs # Bookmarks/outlines
│ ├── annotations.rs # PDF annotations
│ ├── encrypt.rs # PDF encryption
│ ├── signing.rs # Digital signatures
│ ├── forms.rs # Form fields
│ ├── pdfa.rs # PDF/A validation
│ ├── pdfua.rs # PDF/UA validation
│ └── crypto/ # Cryptographic utilities
printwell
Umbrella crate that re-exports everything.
#![allow(unused)]
fn main() {
pub use printwell_core::*;
pub use printwell::*;
}
printwell-cli
Command-line interface.
crates/printwell-cli/
├── src/
│ ├── main.rs # Entry point
│ └── cli.rs # Clap command definitions
Native Library
The native library (libprintwell_native.so) contains:
Chromium Components
- Blink - Web rendering engine
- Skia - 2D graphics library
- PDFium - PDF rendering/generation
Build Configuration
Minimal Chromium build with disabled features:
# From docker/gn_args/common.gn
blink_enable_javascript = false
enable_pdf = false
enable_extensions = false
media_use_ffmpeg = false
use_ozone = true
ozone_platform_headless = true
C++ Interface
// cpp/src/printwell.h (simplified)
extern "C" {
PrintwellConverter* printwell_converter_new();
void printwell_converter_free(PrintwellConverter* converter);
PrintwellResult printwell_convert_html(
PrintwellConverter* converter,
const char* html,
size_t html_len,
const PrintwellOptions* options,
uint8_t** pdf_data,
size_t* pdf_len
);
}
Memory Management
Converter Lifecycle
#![allow(unused)]
fn main() {
// Converter owns native resources
let converter = Converter::new()?;
// Convert HTML to PDF
let result = converter.convert_html(html, options)?;
// Converter dropped here, native resources freed
}
PDF Data Ownership
#![allow(unused)]
fn main() {
// PdfResult owns the PDF bytes
let result = converter.convert_html(html, options)?;
// Get owned Vec<u8>
let pdf_data: Vec<u8> = result.into_bytes();
}
Thread Safety
Converter (Not Thread-Safe)
Converter is !Send and !Sync. Each converter must stay on one thread.
ConverterPool (Thread-Safe)
ConverterPool is Send + Sync. It manages thread-local converters:
#![allow(unused)]
fn main() {
// Pool can be shared across threads
let pool = Arc::new(ConverterPool::new(4)?);
// Each thread gets its own converter
let handle = pool.clone();
thread::spawn(move || {
let result = handle.convert_html(html, options)?;
});
}
Error Handling
All errors use the PrintwellError type:
#![allow(unused)]
fn main() {
pub enum PrintwellError {
// Conversion errors
Conversion(String),
InvalidHtml(String),
Timeout,
// Resource errors
ResourceNotFound(String),
IoError(std::io::Error),
// Feature errors
Encryption(String),
Signing(String),
Validation(String),
}
}
Feature Flags
Cargo features control optional functionality:
| Feature | Description | Default |
|---|---|---|
signing | Digital signatures | Yes |
encryption | PDF encryption | Yes |
forms | Form fields | Yes |
pdfa | PDF/A validation | Yes |
pdfua | PDF/UA validation | Yes |
Bindings Architecture
Node.js (NAPI-RS)
bindings/node/
├── src/lib.rs # NAPI bindings
├── index.d.ts # TypeScript definitions
└── package.json
Uses NAPI-RS for zero-copy data transfer.
Python (PyO3)
bindings/python/
├── src/lib.rs # PyO3 bindings
├── printwell/
│ ├── __init__.py # Python module
│ └── __init__.pyi # Type stubs
└── pyproject.toml
Uses PyO3 with maturin for building.
Shared Types
bindings/shared/
└── src/lib.rs # Common type definitions
Shared between Node.js and Python bindings.
Performance Considerations
Converter Reuse
Creating a converter is expensive. Reuse converters:
#![allow(unused)]
fn main() {
// Good: reuse converter
let converter = Converter::new()?;
for html in documents {
converter.convert_html(html, options)?;
}
// Bad: create new converter each time
for html in documents {
let converter = Converter::new()?; // Expensive!
converter.convert_html(html, options)?;
}
}
Pool Sizing
Pool size should match available CPU cores:
#![allow(unused)]
fn main() {
let pool = ConverterPool::new(num_cpus::get())?;
}
Memory Usage
Each converter uses ~50MB of memory. Plan accordingly:
- 4 converters ≈ 200MB
- 8 converters ≈ 400MB
Contributing
Guidelines for contributing to printwell.
Getting Started
-
Fork the repository
-
Clone your fork:
git clone https://github.com/YOUR_USERNAME/core cd core -
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 fmtfor formatting - Use
cargo clippyfor 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
-
Create a feature branch:
git checkout -b feature/my-feature -
Make your changes with tests
-
Ensure all checks pass:
cargo xtask test cargo xtask lint -
Push and create a PR:
git push origin feature/my-feature -
Fill out the PR template
-
Address review feedback
Adding New Features
To printwell (Pure Rust)
- Create module in
crates/printwell/src/ - Add exports to
crates/printwell/src/lib.rs - Add feature flag if needed in
Cargo.toml - Add tests in the module
- Update documentation
To printwell-core (Native)
- Add C++ code in
cpp/src/ - Update
cpp/BUILD.gnif needed - Add FFI in
crates/printwell-sys/src/lib.rs - Add safe wrapper in
crates/printwell-core/src/ - Rebuild native library:
cargo xtask build
To Bindings
- Add types to
bindings/shared/src/lib.rs - Add Node.js binding in
bindings/node/src/lib.rs - Add Python binding in
bindings/python/src/lib.rs - Update TypeScript types in
bindings/node/index.d.ts - Update Python stubs in
bindings/python/printwell/__init__.pyi - 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).