Page Rendering
Render PDF pages to raster images (PNG or JPEG) using a pure-Rust rendering engine built on tiny-skia. No external dependencies like Poppler or MuPDF required.
Font fallback chain
Rendering needs glyphs. When a PDF references a non-embedded font (ArialMT, TimesNewRomanPSMT, etc.) that isn’t installed on the host, PDF Oxide walks a fallback chain of well-known open-source fonts:
- DejaVu Sans / DejaVu Serif / DejaVu Sans Mono
- Noto Sans / Noto Serif
- FreeSans / FreeSerif
A warning is logged (with the missing font name) when fallback kicks in, plus an actionable hint: on Linux install liberation-fonts, dejavu-fonts, or noto-fonts; on minimal containers add one of those packages to your Dockerfile.
Performance notes
- The system fontdb is cached at the process level — subsequent renders reuse the parsed index.
- Multi-character glyph clusters accumulate widths correctly (fixes dropped ligatures on Latin/Arabic subset-CID fonts).
- Renders skip malformed images (missing
/ColorSpace, invalid dimensions) with a warning rather than panicking the page.
Quick Example
Rust
use pdf_oxide::PdfDocument;
use pdf_oxide::rendering::{render_page, RenderOptions};
let mut doc = PdfDocument::open("document.pdf")?;
// Render first page as PNG at 150 DPI (default)
let image = render_page(&mut doc, 0, &RenderOptions::default())?;
image.save("page1.png")?;
Python
from pdf_oxide import PdfDocument
doc = PdfDocument("document.pdf")
# Render first page as PNG at 150 DPI
png_bytes = doc.render_page(0, dpi=150)
with open("page1.png", "wb") as f:
f.write(png_bytes)
# Render as JPEG
jpeg_bytes = doc.render_page(0, dpi=150, format="jpeg")
with open("page1.jpg", "wb") as f:
f.write(jpeg_bytes)
Node.js
const { PdfDocument } = require("pdf-oxide");
const fs = require("node:fs");
const doc = new PdfDocument("document.pdf");
// Render first page as PNG
const pngBytes = doc.renderPage(0, "png");
fs.writeFileSync("page1.png", Buffer.from(pngBytes));
// Render as JPEG
const jpegBytes = doc.renderPage(0, "jpeg");
fs.writeFileSync("page1.jpg", Buffer.from(jpegBytes));
doc.close();
Go
import pdfoxide "github.com/yfedoseev/pdf_oxide/go"
doc, _ := pdfoxide.Open("document.pdf")
defer doc.Close()
// Render first page as PNG (format 0 = PNG, 1 = JPEG)
png, _ := doc.RenderPage(0, 0)
os.WriteFile("page1.png", png.Data, 0644)
// Render as JPEG
jpeg, _ := doc.RenderPage(0, 1)
os.WriteFile("page1.jpg", jpeg.Data, 0644)
C#
using PdfOxide.Core;
using var doc = PdfDocument.Open("document.pdf");
// Render first page as PNG (format 0 = PNG, 1 = JPEG)
var pngBytes = doc.RenderPage(0, 0);
File.WriteAllBytes("page1.png", pngBytes);
// Render as JPEG
var jpegBytes = doc.RenderPage(0, 1);
File.WriteAllBytes("page1.jpg", jpegBytes);
WASM
import { WasmPdfDocument } from "pdf-oxide-wasm";
const doc = new WasmPdfDocument(bytes);
// Render first page as PNG at 150 DPI
const pngBytes = doc.renderPage(0, 150);
// Save in Node.js
import { writeFileSync } from "fs";
writeFileSync("page1.png", Buffer.from(pngBytes));
doc.free();
Enabling the Feature
Page rendering requires the rendering feature flag:
[dependencies]
pdf_oxide = { version = "0.3", features = ["rendering"] }
This pulls in tiny-skia (2D rendering), fontdb (font loading), and rustybuzz (text shaping).
Render Options
Configure rendering via RenderOptions:
use pdf_oxide::rendering::{RenderOptions, ImageFormat};
// Default: 150 DPI, PNG, white background, render annotations
let opts = RenderOptions::default();
// High-quality rendering at 300 DPI
let opts = RenderOptions::with_dpi(300);
// JPEG output with 90% quality
let opts = RenderOptions::with_dpi(300).as_jpeg(90);
// Transparent background (PNG only)
let opts = RenderOptions::default().with_transparent_background();
RenderOptions Fields
| Field | Type | Default | Description |
|---|---|---|---|
dpi |
u32 |
150 |
Resolution in dots per inch |
format |
ImageFormat |
Png |
Output format (Png or Jpeg) |
background |
Option<[f32; 4]> |
White [1,1,1,1] |
RGBA background color (0.0–1.0 per channel) |
render_annotations |
bool |
true |
Whether to render annotations |
jpeg_quality |
u8 |
85 |
JPEG quality 1–100 (ignored for PNG) |
Builder Methods
| Method | Description |
|---|---|
RenderOptions::with_dpi(dpi) |
Create options with custom DPI |
.with_transparent_background() |
Set background to transparent (PNG only) |
.as_jpeg(quality) |
Switch to JPEG output with given quality |
ImageFormat
| Variant | Description |
|---|---|
Png |
Lossless compression, supports transparency |
Jpeg |
Lossy compression, smaller file size, no transparency |
RenderedImage
The render_page() function returns a RenderedImage:
pub struct RenderedImage {
pub data: Vec<u8>, // Encoded image bytes
pub width: u32, // Width in pixels
pub height: u32, // Height in pixels
pub format: ImageFormat, // PNG or JPEG
}
Methods
| Method | Returns | Description |
|---|---|---|
save(path) |
Result<()> |
Write image to file |
as_bytes() |
&[u8] |
Get raw image bytes |
Python API
doc.render_page(page, dpi=None, format=None)
Render a page to image bytes.
| Parameter | Type | Default | Description |
|---|---|---|---|
page |
int |
required | Zero-based page index |
dpi |
int |
72 |
Dots per inch |
format |
str |
"png" |
Output format: "png" or "jpeg" |
Returns: bytes — encoded image data (PNG or JPEG)
# PNG at default DPI
png = doc.render_page(0)
# High-quality PNG
png = doc.render_page(0, dpi=300)
# JPEG with default quality
jpeg = doc.render_page(0, format="jpeg")
# High-DPI JPEG
jpeg = doc.render_page(0, dpi=300, format="jpeg")
JavaScript API
doc.renderPage(pageIndex, dpi?)
Render a page to PNG bytes.
| Parameter | Type | Default | Description |
|---|---|---|---|
pageIndex |
number |
required | Zero-based page index |
dpi |
number |
150 |
Dots per inch |
Returns: Uint8Array — PNG image data
const pngBytes = doc.renderPage(0); // 150 DPI default
const hiRes = doc.renderPage(0, 300); // 300 DPI
Common Use Cases
Render All Pages
use pdf_oxide::PdfDocument;
use pdf_oxide::rendering::{render_page, RenderOptions};
let mut doc = PdfDocument::open("document.pdf")?;
let opts = RenderOptions::with_dpi(200);
for page in 0..doc.page_count()? {
let image = render_page(&mut doc, page, &opts)?;
image.save(format!("page_{}.png", page + 1))?;
}
Python
from pdf_oxide import PdfDocument
from pathlib import Path
doc = PdfDocument("document.pdf")
for i in range(doc.page_count()):
png_bytes = doc.render_page(i, dpi=200)
Path(f"page_{i + 1}.png").write_bytes(png_bytes)
Node.js
const doc = new PdfDocument("document.pdf");
for (let i = 0; i < doc.pageCount(); i++) {
const pngBytes = doc.renderPage(i, "png");
fs.writeFileSync(`page_${i + 1}.png`, Buffer.from(pngBytes));
}
doc.close();
Go
doc, _ := pdfoxide.Open("document.pdf")
defer doc.Close()
pages, _ := doc.PageCount()
for i := 0; i < pages; i++ {
img, _ := doc.RenderPage(i, 0)
os.WriteFile(fmt.Sprintf("page_%d.png", i+1), img.Data, 0644)
}
C#
using var doc = PdfDocument.Open("document.pdf");
for (int i = 0; i < doc.PageCount; i++)
{
var pngBytes = doc.RenderPage(i, 0);
File.WriteAllBytes($"page_{i + 1}.png", pngBytes);
}
Generate Thumbnails
use pdf_oxide::rendering::{render_page, RenderOptions};
// Low DPI for fast thumbnail generation
let opts = RenderOptions::with_dpi(72).as_jpeg(75);
let thumb = render_page(&mut doc, 0, &opts)?;
thumb.save("thumbnail.jpg")?;
println!("Thumbnail: {}×{} ({} bytes)", thumb.width, thumb.height, thumb.data.len());
Python
doc = PdfDocument("document.pdf")
thumb = doc.render_page(0, dpi=72, format="jpeg")
Path("thumbnail.jpg").write_bytes(thumb)
Node.js
const doc = new PdfDocument("document.pdf");
const thumb = doc.renderPage(0, "jpeg");
fs.writeFileSync("thumbnail.jpg", Buffer.from(thumb));
doc.close();
Go
doc, _ := pdfoxide.Open("document.pdf")
defer doc.Close()
// RenderThumbnail returns a 72-DPI thumbnail (format 1 = JPEG)
thumb, _ := doc.RenderThumbnail(0, 72, 1)
os.WriteFile("thumbnail.jpg", thumb.Data, 0644)
C#
using var doc = PdfDocument.Open("document.pdf");
// RenderThumbnail returns a 72-DPI thumbnail (format 1 = JPEG)
var thumb = doc.RenderThumbnail(0, 1);
File.WriteAllBytes("thumbnail.jpg", thumb);
Transparent Background for Compositing
let opts = RenderOptions::with_dpi(150).with_transparent_background();
let image = render_page(&mut doc, 0, &opts)?;
image.save("page_transparent.png")?;
Custom Background Color
let opts = RenderOptions {
dpi: 150,
background: Some([0.95, 0.95, 0.95, 1.0]), // Light gray
..RenderOptions::default()
};
let image = render_page(&mut doc, 0, &opts)?;
High-Quality Print Output
// 300 DPI for print-quality output
let opts = RenderOptions::with_dpi(300);
let image = render_page(&mut doc, 0, &opts)?;
image.save("print_quality.png")?;
println!("Image size: {}×{}", image.width, image.height);
Flatten PDF to Images
Convert an entire PDF into a flat image-based PDF. Each page is rendered as a raster image at the specified DPI, then assembled into a new PDF. This permanently burns in all annotations, form fields, overlays, and fonts.
Python
from pdf_oxide import PdfDocument
doc = PdfDocument("document.pdf")
flattened = doc.flatten_to_images(dpi=150)
with open("flattened.pdf", "wb") as f:
f.write(flattened)
WASM
import { WasmPdfDocument } from "pdf-oxide-wasm";
import { writeFileSync } from "fs";
const doc = new WasmPdfDocument(bytes);
const flattened = doc.flattenToImages(150);
writeFileSync("flattened.pdf", Buffer.from(flattened));
doc.free();
Rust
use pdf_oxide::PdfDocument;
use pdf_oxide::rendering::flatten_to_images;
let mut doc = PdfDocument::open("document.pdf")?;
let flattened = flatten_to_images(&mut doc, 150)?;
std::fs::write("flattened.pdf", flattened)?;
Parameters
| Parameter | Python | JavaScript | Rust | Default | Description |
|---|---|---|---|---|---|
| DPI | dpi |
dpi |
dpi |
150 | Resolution for rendering each page |
Returns: PDF file bytes — a new PDF where each page is a full-page image.
Use Cases
- Redaction — flatten after redacting to permanently remove hidden content
- Archival — create a visual snapshot identical in any viewer
- Consistent rendering — eliminate font and layout differences across PDF viewers
- Print preparation — flatten complex overlays for reliable printing
- Form submission — burn in filled form field values
Flatten with High Quality
# 300 DPI for print-quality flattening
flattened = doc.flatten_to_images(dpi=300)
with open("print_ready.pdf", "wb") as f:
f.write(flattened)
Rendering Pipeline
PDF Oxide’s renderer processes the page content stream in order:
- Dimensions — Calculate pixel size from page dimensions and DPI (72 points = 1 inch)
- Background — Create pixmap with configured background color
- Transform — Apply coordinate transform (PDF bottom-left origin → image top-left origin)
- Content stream — Parse and execute all PDF operators:
- Paths — Lines, curves, rectangles with fill/stroke
- Text — Positioned text with font selection and spacing
- Images — Embedded raster images (DeviceGray, DeviceRGB, DeviceCMYK)
- Graphics state — Transparency, blend modes, clipping, line styles
- Encode — Output as PNG or JPEG
Supported PDF Operators
| Category | Operators |
|---|---|
| Graphics state | q Q (save/restore), cm (transform matrix) |
| Color | rg RG g G k K (RGB, gray, CMYK) |
| Path construction | m l c v y re h (move, line, curve, rect, close) |
| Path painting | S s f F f* B B* b b* n (stroke, fill, both) |
| Clipping | W W* (non-zero and even-odd winding) |
| Text | BT ET Td TD Tm Tf Tj TJ ' " |
| Images | Do (XObjects: images and form XObjects) |
| Extended state | gs (transparency ca/CA, blend modes BM) |
Related Pages
- Text Extraction — Extract text content from pages
- Image Extraction — Extract embedded images from PDFs
- OCR Scanned PDFs — OCR uses rendering internally
- Create from Images — Convert images back to PDF