Skip to content

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:

  1. Dimensions — Calculate pixel size from page dimensions and DPI (72 points = 1 inch)
  2. Background — Create pixmap with configured background color
  3. Transform — Apply coordinate transform (PDF bottom-left origin → image top-left origin)
  4. 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
  5. 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)