This guide shows developers how to deploy the PDFluent SDK for four specific tasks: serverless functions, containerised services, browser-based apps, and high-volume batch processing.
PDFluent has no native dependencies, which makes it ideal for Lambda. Cold start times are under 50 ms for a typical PDF handler.
use lambda_runtime::{service_fn, LambdaEvent, Error};
use aws_sdk_s3::Client as S3Client;
use pdfluent::PdfDocument;
use serde_json::Value;
#[tokio::main]
async fn main() -> Result<(), Error> {
lambda_runtime::run(service_fn(handler)).await
}
async fn handler(event: LambdaEvent<Value>) -> Result<Value, Error> {
let bucket = event.payload["bucket"].as_str().unwrap_or_default();
let key = event.payload["key"].as_str().unwrap_or_default();
let config = aws_config::load_from_env().await;
let s3 = S3Client::new(&config);
let resp = s3.get_object().bucket(bucket).key(key).send().await?;
let bytes = resp.body.collect().await?.into_bytes();
let doc = PdfDocument::from_bytes(&bytes)?;
let page_count = doc.page_count();
let text = doc.text()?;
Ok(serde_json::json!({
"pages": page_count,
"chars": text.len()
}))
}Use the lambda_runtime crate from AWS. Build a binary named bootstrap, which is the required name for Lambda custom runtimes.
# Cargo.toml
[package]
name = "pdf-lambda"
version = "1.0.0-beta.8"
edition = "2021"
[[bin]]
name = "bootstrap"
path = "src/main.rs"
[dependencies]
pdfluent = "1.0.0-beta.18"
lambda_runtime = "0.11"
aws-config = { version = "1", features = ["behavior-version-latest"] }
aws-sdk-s3 = "1"
tokio = { version = "1", features = ["full"] }
serde_json = "1"Lambda runs on Amazon Linux 2 (x86_64 or arm64). Use cargo-lambda to cross-compile without a Linux machine.
# Install cargo-lambda
cargo install cargo-lambda
# Build for x86_64 Lambda
cargo lambda build --release --target x86_64-unknown-linux-musl
# Or build for arm64 Lambda (Graviton2, cheaper)
cargo lambda build --release --target aarch64-unknown-linux-muslRead the S3 bucket and key from the event payload. Download the PDF bytes from S3 and pass them to PdfDocument::from_bytes.
async fn handler(event: LambdaEvent<Value>) -> Result<Value, Error> {
let bucket = event.payload["bucket"].as_str().unwrap_or_default();
let key = event.payload["key"].as_str().unwrap_or_default();
let config = aws_config::load_from_env().await;
let s3 = S3Client::new(&config);
let resp = s3.get_object()
.bucket(bucket)
.key(key)
.send()
.await?;
let bytes = resp.body.collect().await?.into_bytes();
let doc = PdfDocument::from_bytes(&bytes)?;
// ... process the document
Ok(serde_json::json!({ "pages": doc.page_count() }))
}cargo lambda deploy uploads the binary as a Lambda function with the provided.al2 runtime.
cargo lambda deploy \
--region eu-west-1 \
--memory 512 \
--timeout 30 \
pdf-lambdaThe function needs GetObject permission on the S3 bucket. Attach an inline policy or a managed policy to the execution role.
{
"Version": "2012-10-17",
"Statement": [
{
"Effect": "Allow",
"Action": ["s3:GetObject"],
"Resource": "arn:aws:s3:::my-pdf-bucket/*"
}
]
}PDFluent has no native dependencies. The Docker image is small and requires no additional apt packages.
# Dockerfile
FROM rust:1.78-slim AS builder
WORKDIR /app
COPY . .
RUN cargo build --release
FROM debian:bookworm-slim
COPY --from=builder /app/target/release/pdf-processor /usr/local/bin/
ENTRYPOINT ["pdf-processor"]Your binary reads PDF bytes from stdin or a file path passed as an argument and writes results to stdout.
# Cargo.toml
[package]
name = "pdf-processor"
version = "1.0.0-beta.8"
edition = "2021"
[dependencies]
pdfluent = "1.0.0-beta.18"Accept the file path as a command-line argument. Process the document and print results to stdout.
// src/main.rs
use pdfluent::PdfDocument;
use std::env;
fn main() -> pdfluent::Result<()> {
let path = env::args().nth(1).expect("usage: pdf-processor <file.pdf>");
let doc = PdfDocument::open(&path)?;
println!("pages: {}", doc.page_count());
println!("title: {:?}", doc.metadata().title);
println!("chars: {}", doc.text()?.len());
Ok(())
}The builder stage compiles the binary. The final stage copies only the binary into a minimal debian image. No Rust toolchain ships in the final image.
# Dockerfile
FROM rust:1.78-slim AS builder
WORKDIR /app
# Cache dependencies first
COPY Cargo.toml Cargo.lock ./
RUN mkdir src && echo "fn main(){}" > src/main.rs
RUN cargo build --release
RUN rm -f target/release/deps/pdf_processor*
# Build the real binary
COPY src ./src
RUN cargo build --release
# Final image
FROM debian:bookworm-slim
RUN apt-get update && apt-get install -y --no-install-recommends ca-certificates && rm -rf /var/lib/apt/lists/*
COPY --from=builder /app/target/release/pdf-processor /usr/local/bin/
ENTRYPOINT ["pdf-processor"]Build the image and run it with a test PDF mounted as a volume.
docker build -t pdf-processor:latest .
# Run with a local PDF
docker run --rm -v "$(pwd)/samples:/data" pdf-processor:latest /data/test.pdfIf your final base image is scratch or alpine, build a statically linked musl binary. This eliminates the glibc version dependency.
# Dockerfile (scratch variant)
FROM rust:1.78-slim AS builder
RUN rustup target add x86_64-unknown-linux-musl
RUN apt-get update && apt-get install -y musl-tools
WORKDIR /app
COPY . .
RUN cargo build --release --target x86_64-unknown-linux-musl
FROM scratch
COPY --from=builder /app/target/x86_64-unknown-linux-musl/release/pdf-processor /
ENTRYPOINT ["/pdf-processor"]Run PDF processing entirely client-side. No server round-trip, no file upload, no privacy risk.
// src/lib.rs
use wasm_bindgen::prelude::*;
use pdfluent::PdfDocument;
#[wasm_bindgen]
pub fn page_count(bytes: &[u8]) -> Result<u32, JsValue> {
let doc = PdfDocument::from_bytes(bytes)
.map_err(|e| JsValue::from_str(&e.to_string()))?;
Ok(doc.page_count())
}
#[wasm_bindgen]
pub fn extract_text(bytes: &[u8]) -> Result<String, JsValue> {
let doc = PdfDocument::from_bytes(bytes)
.map_err(|e| JsValue::from_str(&e.to_string()))?;
doc.text()
.map_err(|e| JsValue::from_str(&e.to_string()))
}PDFluent has a wasm feature that disables std-only dependencies. wasm-bindgen generates the JS glue code.
# Cargo.toml
[package]
name = "pdf-wasm"
version = "0.1.0"
edition = "2021"
[lib]
crate-type = ["cdylib"]
[dependencies]
pdfluent = { version = "0.9", features = ["wasm"] }
wasm-bindgen = "0.2"Mark each function with #[wasm_bindgen]. Functions that receive PDF bytes take &[u8]. Return JsValue errors so JavaScript can catch them.
use wasm_bindgen::prelude::*;
use pdfluent::PdfDocument;
#[wasm_bindgen]
pub fn page_count(bytes: &[u8]) -> Result<u32, JsValue> {
let doc = PdfDocument::from_bytes(bytes)
.map_err(|e| JsValue::from_str(&e.to_string()))?;
Ok(doc.page_count())
}Install wasm-pack and run the build command. This produces a pkg/ directory with .wasm and JS binding files.
# Install wasm-pack
cargo install wasm-pack
# Build for the browser (ES module output)
wasm-pack build --target web --out-dir pkg
# Or build for Node.js
wasm-pack build --target nodejs --out-dir pkgLoad the WASM module asynchronously, then pass ArrayBuffer data from a file input or fetch.
import init, { page_count, extract_text } from './pkg/pdf_wasm.js';
await init();
const input = document.getElementById('file-input');
input.addEventListener('change', async (e) => {
const file = e.target.files[0];
const buffer = await file.arrayBuffer();
const bytes = new Uint8Array(buffer);
const pages = page_count(bytes);
const text = extract_text(bytes);
console.log(`Pages: ${pages}`);
console.log(`Text: ${text.slice(0, 200)}`);
});Browsers require .wasm files to be served with Content-Type: application/wasm. Most dev servers handle this automatically. In production, verify your CDN or server config.
# Vite, webpack, and Parcel all handle WASM automatically.
# For a plain nginx config:
# types {
# application/wasm wasm;
# }
# Quick local test with wasm-pack's built-in server:
npx serve .Use rayon and PDFluent together to process a folder of PDFs across all CPU cores. No thread pool setup required.
use rayon::prelude::*;
use std::fs;
use pdfluent::PdfDocument;
fn main() -> anyhow::Result<()> {
let files: Vec<_> = fs::read_dir("./input")?
.filter_map(|e| e.ok())
.map(|e| e.path())
.filter(|p| p.extension().map_or(false, |e| e == "pdf"))
.collect();
files.par_iter().for_each(|path| {
match process_file(path) {
Ok(_) => println!("OK {:?}", path.file_name().unwrap()),
Err(e) => eprintln!("ERR {:?}: {}", path.file_name().unwrap(), e),
}
});
Ok(())
}
fn process_file(path: &std::path::Path) -> pdfluent::Result<()> {
let doc = PdfDocument::open(path)?;
let text = doc.text()?;
let out = path.with_extension("txt");
fs::write(out, text)?;
Ok(())
}rayon provides a parallel iterator that distributes work across all available CPU cores automatically.
# Cargo.toml
[dependencies]
pdfluent = "1.0.0-beta.18"
rayon = "1.10"
anyhow = "1"Use std::fs::read_dir to walk the input directory. Filter by extension to skip non-PDF files.
use std::fs;
use std::path::PathBuf;
let files: Vec<PathBuf> = fs::read_dir("./input")?
.filter_map(|e| e.ok())
.map(|e| e.path())
.filter(|p| p.extension().map_or(false, |ext| ext == "pdf"))
.collect();
println!("Found {} PDF files", files.len());Replace .iter() with .par_iter() to run each file on a separate thread. PdfDocument::open is Send, so it works safely across rayon threads.
use rayon::prelude::*;
files.par_iter().for_each(|path| {
match process_file(path) {
Ok(_) => println!("OK {}", path.display()),
Err(e) => eprintln!("ERR {}: {}", path.display(), e),
}
});Keep the function focused on one document. Open, process, save. Errors are returned and logged by the caller.
use pdfluent::{PdfDocument, WatermarkOptions};
fn process_file(path: &std::path::Path) -> pdfluent::Result<()> {
let mut doc = PdfDocument::open(path)?;
// Example: stamp a watermark across the document
doc.add_watermark("CONFIDENTIAL", WatermarkOptions::centered())?;
let out_path = std::path::Path::new("./output")
.join(path.file_name().unwrap());
doc.save(out_path)?;
Ok(())
}Use par_iter().map() instead of par_iter().for_each() when you need to collect success and error counts.
use std::sync::atomic::{AtomicUsize, Ordering};
use std::sync::Arc;
let ok_count = Arc::new(AtomicUsize::new(0));
let err_count = Arc::new(AtomicUsize::new(0));
files.par_iter().for_each(|path| {
let ok = ok_count.clone();
let err = err_count.clone();
match process_file(path) {
Ok(_) => { ok.fetch_add(1, Ordering::Relaxed); }
Err(e) => {
eprintln!("ERR {}: {}", path.display(), e);
err.fetch_add(1, Ordering::Relaxed);
}
}
});
println!(
"Done. {} ok, {} errors",
ok_count.load(Ordering::Relaxed),
err_count.load(Ordering::Relaxed)
);