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);

Java

import fyi.oxide.pdf.PdfDocument;
import java.nio.file.*;

try (PdfDocument doc = PdfDocument.open(Path.of("document.pdf"))) {
    // Render first page as PNG at 150 DPI
    byte[] png = doc.render(0, 150);
    Files.write(Path.of("page1.png"), png);
}

Kotlin

import fyi.oxide.pdf.PdfDocument
import java.nio.file.*

PdfDocument.open(Path.of("document.pdf")).use { doc ->
    // Render first page as PNG at 150 DPI
    val png = doc.render(0, 150)
    Files.write(Path.of("page1.png"), png)
}

Scala

import fyi.oxide.pdf.PdfDocument
import scala.util.Using
import java.nio.file.{Files, Paths}

Using.resource(PdfDocument.open("document.pdf")) { doc =>
  // Render first page as PNG at 150 DPI
  val png = doc.render(0, 150)
  Files.write(Paths.get("page1.png"), png)
}

Clojure

(require '[pdf-oxide.core :as pdf])
(require '[clojure.java.io :as io])

(with-open [doc (pdf/open "document.pdf")]
  ;; Render first page as PNG at 150 DPI
  (with-open [out (io/output-stream "page1.png")]
    (.write out (pdf/render doc 0 150))))

Ruby

require 'pdf_oxide'

PdfOxide::PdfDocument.open('document.pdf') do |doc|
  # Render first page as PNG at 150 DPI
  File.binwrite('page1.png', doc.render(0, dpi: 150))

  # Render as JPEG (format 1)
  jpeg = doc.render_with_layers(0, dpi: 150, format: 1)
  File.binwrite('page1.jpg', jpeg)
end

C++

#include <pdf_oxide/pdf_oxide.hpp>

auto doc = pdf_oxide::Document::open("document.pdf");

// Render first page as PNG (format 0 = PNG, 1 = JPEG)
doc.render_page(0, 0).save("page1.png");

// Render as JPEG
doc.render_page(0, 1).save("page1.jpg");

Swift

import PdfOxide

let doc = try Document.open("document.pdf")

// Render first page as PNG (format 0 = PNG, 1 = JPEG)
try doc.renderPage(0, format: 0).save("page1.png")

// Render as JPEG
try doc.renderPage(0, format: 1).save("page1.jpg")

Dart

import 'package:pdf_oxide/pdf_oxide.dart';

final doc = PdfDocument.open('document.pdf');

// Render first page as PNG (format 0 = PNG, 1 = JPEG)
doc.renderPage(0, 0).save('page1.png');

// Render as JPEG
doc.renderPage(0, 1).save('page1.jpg');

R

library(pdfoxide)

doc <- pdf_open("document.pdf")

# Render first page as PNG (format 0 = PNG, 1 = JPEG)
img <- pdf_render_page(doc, 0, format = 0L)
pdf_rendered_image_save(img, "page1.png")

# Render as JPEG
jpg <- pdf_render_page(doc, 0, format = 1L)
pdf_rendered_image_save(jpg, "page1.jpg")

Julia

using PdfOxide

doc = open_document("document.pdf")

# Render first page as PNG (format 0 = PNG, 1 = JPEG)
img = render_page(doc, 0, 0)
save(img, "page1.png")

# Render as JPEG
jpg = render_page(doc, 0, 1)
save(jpg, "page1.jpg")

Zig

const pdf_oxide = @import("pdf_oxide");
const a = std.heap.page_allocator;

var doc = try pdf_oxide.Document.open("document.pdf");

// Render first page as PNG (format 0 = PNG, 1 = JPEG)
var png = try doc.renderPage(a, 0, 0);
defer png.deinit();
try png.save("page1.png");

// Render as JPEG
var jpg = try doc.renderPage(a, 0, 1);
defer jpg.deinit();
try jpg.save("page1.jpg");

Objective-C

#import "POXPdfOxide.h"
NSError *err = nil;

POXDocument *doc = [POXDocument openPath:@"document.pdf" error:&err];

// Render first page as PNG (format 0 = PNG, 1 = JPEG)
POXRenderedImage *png = [doc renderPage:0 format:0 error:&err];
[png saveToPath:@"page1.png" error:&err];

// Render as JPEG
POXRenderedImage *jpg = [doc renderPage:0 format:1 error:&err];
[jpg saveToPath:@"page1.jpg" error:&err];

Elixir

{:ok, doc} = PdfOxide.open("document.pdf")

# Render first page as PNG (format 0 = PNG, 1 = JPEG)
{:ok, png} = PdfOxide.render_page(doc, 0, 0)
PdfOxide.save(png, "page1.png")

# Render as JPEG
{:ok, jpg} = PdfOxide.render_page(doc, 0, 1)
PdfOxide.save(jpg, "page1.jpg")

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

Advanced Rendering Variants

Beyond the encoded PNG/JPEG path, PDF Oxide exposes three lower-level rendering entry points: a raw pixel buffer (skip PNG/JPEG encoding entirely), an extended options variant that adds optional-content-group (OCG) layer filtering, and a cheap render-time estimate.

How do I get a raw RGBA pixel buffer instead of PNG/JPEG?

Use the raw render path when you want to hand pixels straight to a GPU texture, an image library, or a compositor without paying for PNG/JPEG encoding. The buffer is premultiplied RGBA8888, row-major, top-left origin, so len == width * height * 4.

Rust

use pdf_oxide::PdfDocument;
use pdf_oxide::rendering::{render_page, RenderOptions};

let mut doc = PdfDocument::open("document.pdf")?;

// `.as_raw()` switches the encoder off — `image.data` is the raw RGBA buffer.
let opts = RenderOptions::with_dpi(150).as_raw();
let image = render_page(&mut doc, 0, &opts)?;

assert_eq!(image.data.len(), (image.width * image.height * 4) as usize);
println!("Raw RGBA buffer: {}×{}, {} bytes", image.width, image.height, image.data.len());

Python

from pdf_oxide import PdfDocument

doc = PdfDocument("document.pdf")

# render_pixmap returns a RenderedPixmap (raw RGBA8888, no encoding)
pixmap = doc.render_pixmap(0, dpi=150)
assert len(pixmap.data) == pixmap.width * pixmap.height * 4
print(f"Raw RGBA buffer: {pixmap.width}x{pixmap.height}, {len(pixmap.data)} bytes")

Swift

import PdfOxide

let doc = try PdfDocument(path: "document.pdf")

// renderPageRaw returns the RenderedImage plus the pixel dimensions
let (image, width, height) = try doc.renderPageRaw(0, dpi: 150)
print("Raw RGBA buffer: \(width)x\(height), \(image.data.count) bytes")

C++

#include <pdf_oxide/pdf_oxide.hpp>

auto doc = pdf_oxide::Document::open("document.pdf");

// render_page_raw writes the pixel dimensions into out_width/out_height;
// the raw RGBA buffer is the returned image's data().
int width = 0, height = 0;
auto image = doc.render_page_raw(0, /*dpi=*/150, width, height);
// data().size() == width * height * 4
printf("Raw RGBA buffer: %dx%d, %zu bytes\n", width, height, image.data().size());

Dart

import 'package:pdf_oxide/pdf_oxide.dart';

final doc = PdfDocument.open('document.pdf');

// renderPageRaw returns a RenderedImage holding the raw RGBA8888 buffer
final image = doc.renderPageRaw(0, 150);
assert(image.data.length == image.width * image.height * 4);
print('Raw RGBA buffer: ${image.width}x${image.height}, ${image.data.length} bytes');

R

library(pdfoxide)

doc <- pdf_open("document.pdf")

# pdf_render_page_raw returns a rendered-image with the raw RGBA8888 buffer
img <- pdf_render_page_raw(doc, 0, dpi = 150L)
stopifnot(length(img$data) == img$width * img$height * 4)
cat(sprintf("Raw RGBA buffer: %dx%d, %d bytes\n", img$width, img$height, length(img$data)))

Julia

using PdfOxide

doc = open_document("document.pdf")

# render_page_raw returns a RenderedImage holding the raw RGBA8888 buffer
img = render_page_raw(doc, 0, 150)
@assert length(img.data) == img.width * img.height * 4
println("Raw RGBA buffer: $(img.width)x$(img.height), $(length(img.data)) bytes")

Zig

const pdf_oxide = @import("pdf_oxide");
const a = std.heap.page_allocator;

var doc = try pdf_oxide.Document.open("document.pdf");

// renderPageRaw returns a RenderedImage whose `data` is the raw RGBA8888 buffer
var image = try doc.renderPageRaw(a, 0, 150);
defer image.deinit();
std.debug.assert(image.data.len == @as(usize, @intCast(image.width * image.height * 4)));
std.debug.print("Raw RGBA buffer: {d}x{d}, {d} bytes\n", .{ image.width, image.height, image.data.len });

Objective-C

#import "POXPdfOxide.h"
NSError *err = nil;

POXDocument *doc = [POXDocument openPath:@"document.pdf" error:&err];

// renderPageRaw writes the pixel dimensions into outWidth/outHeight
int32_t width = 0, height = 0;
POXRenderedImage *image = [doc renderPageRaw:0 dpi:150 outWidth:&width outHeight:&height error:&err];
// image.data.length == width * height * 4
NSLog(@"Raw RGBA buffer: %dx%d, %lu bytes", width, height, (unsigned long)image.data.length);

Elixir

{:ok, doc} = PdfOxide.open("document.pdf")

# render_page_raw returns a RenderedImage whose data holds the raw RGBA8888 buffer
{:ok, image} = PdfOxide.render_page_raw(doc, 0, 150)
true = byte_size(image.data) == image.width * image.height * 4
IO.puts("Raw RGBA buffer: #{image.width}x#{image.height}, #{byte_size(image.data)} bytes")

The C ABI exposes this as pdf_render_page_raw(doc, page_index, dpi, *out_width, *out_height, *error_code); retrieve the pixel bytes with pdf_get_rendered_image_data and free the handle with pdf_rendered_image_free.

How do I hide optional-content (OCG) layers while rendering?

render_page_with_options_ex is the full render-options surface plus a list of optional-content-group /Names to suppress. Pass the layer names you want hidden (e.g. a “Confidential” watermark layer or a “Construction lines” CAD layer). OCMD references that resolve to any named OCG are honoured too, per ISO 32000-1 §8.11.2.

Rust

use pdf_oxide::PdfDocument;
use pdf_oxide::rendering::{render_page, RenderOptions};

let mut doc = PdfDocument::open("layered.pdf")?;

let mut opts = RenderOptions::with_dpi(200);
// Suppress these optional-content groups by /Name
opts.excluded_layers = ["Watermark", "Draft Notes"].into_iter().map(String::from).collect();

let image = render_page(&mut doc, 0, &opts)?;
image.save("page_no_watermark.png")?;

Python

from pdf_oxide import PdfDocument

doc = PdfDocument("layered.pdf")

# render_page takes excluded_layers — the OCG /Name strings to hide
png = doc.render_page(0, dpi=200, excluded_layers=["Watermark", "Draft Notes"])
with open("page_no_watermark.png", "wb") as f:
    f.write(png)

Swift

import PdfOxide

let doc = try PdfDocument(path: "layered.pdf")

// dpi 200, PNG (format 0), excluded OCG names
let image = try doc.renderPageWithOptionsEx(
    0, dpi: 200, format: 0,
    excludedLayers: ["Watermark", "Draft Notes"]
)
try image.save("page_no_watermark.png")

C++

#include <pdf_oxide/pdf_oxide.hpp>

auto doc = pdf_oxide::Document::open("layered.pdf");

// dpi 200, PNG (format 0), white bg, render annotations, then OCG /Names to suppress
auto image = doc.render_page_with_options_ex(
    0, /*dpi=*/200, /*format=*/0,
    1.0f, 1.0f, 1.0f, 1.0f, /*transparent=*/false, /*render_annotations=*/true,
    /*jpeg_quality=*/85, {"Watermark", "Draft Notes"});
image.save("page_no_watermark.png");

Ruby

require 'pdf_oxide'

PdfOxide::PdfDocument.open('layered.pdf') do |doc|
  # render_with_layers takes excluded_layers — the OCG /Name strings to hide
  png = doc.render_with_layers(0, dpi: 200, excluded_layers: ["Watermark", "Draft Notes"])
  File.binwrite('page_no_watermark.png', png)
end

Dart

import 'package:pdf_oxide/pdf_oxide.dart';

final doc = PdfDocument.open('layered.pdf');

// renderPageWithOptionsEx takes excludedLayers — the OCG /Name strings to hide
final image = doc.renderPageWithOptionsEx(
  0,
  dpi: 200,
  format: 0,
  excludedLayers: ["Watermark", "Draft Notes"],
);
image.save('page_no_watermark.png');

R

library(pdfoxide)

doc <- pdf_open("layered.pdf")

# excluded_layers is a character vector of OCG /Names to suppress
img <- pdf_render_page_with_options_ex(
  doc, 0, dpi = 200L, format = 0L,
  excluded_layers = c("Watermark", "Draft Notes")
)
pdf_rendered_image_save(img, "page_no_watermark.png")

Julia

using PdfOxide

doc = open_document("layered.pdf")

# excluded_layers is a vector of OCG /Name strings to suppress
img = render_page_with_options_ex(
    doc, 0, 200, 0, 1.0, 1.0, 1.0, 1.0, 0, 1, 85,
    ["Watermark", "Draft Notes"],
)
save(img, "page_no_watermark.png")

Zig

const pdf_oxide = @import("pdf_oxide");
const a = std.heap.page_allocator;

var doc = try pdf_oxide.Document.open("layered.pdf");

// excluded_layers are the OCG /Names to suppress
const layers = [_][*:0]const u8{ "Watermark", "Draft Notes" };
var image = try doc.renderPageWithOptionsEx(
    a, 0, 200, 0, 1.0, 1.0, 1.0, 1.0, false, true, 85, &layers,
);
defer image.deinit();
try image.save("page_no_watermark.png");

Objective-C

#import "POXPdfOxide.h"
NSError *err = nil;

POXDocument *doc = [POXDocument openPath:@"layered.pdf" error:&err];

// excludedLayers are the OCG /Names to suppress
POXRenderedImage *image = [doc renderPageWithOptionsEx:0
                                                   dpi:200
                                                format:0
                                                   bgR:1.0 bgG:1.0 bgB:1.0 bgA:1.0
                                 transparentBackground:0
                                     renderAnnotations:1
                                           jpegQuality:85
                                        excludedLayers:@[@"Watermark", @"Draft Notes"]
                                                 error:&err];
[image saveToPath:@"page_no_watermark.png" error:&err];

Elixir

{:ok, doc} = PdfOxide.open("layered.pdf")

# render_page_with_options_ex takes the OCG /Name strings to hide
{:ok, image} =
  PdfOxide.render_page_with_options_ex(doc, 0, ["Watermark", "Draft Notes"], dpi: 200)
PdfOxide.save(image, "page_no_watermark.png")

The C ABI signature is pdf_render_page_with_options_ex(doc, page_index, dpi, format, bg_r, bg_g, bg_b, bg_a, transparent_background, render_annotations, jpeg_quality, excluded_layers, excluded_layers_count, *error_code). Pass a null excluded_layers pointer (or zero count) to disable filtering, which matches plain pdf_render_page_with_options.

How do I estimate render cost before rendering?

estimate_render_time returns a cheap, implementation-defined cost figure for a page so a batch job can prioritise or budget work without actually rasterising. It is exposed today through the C ABI and the Swift wrapper; the Rust crate and the Python/Go/Node bindings do not surface it as an idiomatic method.

Swift

import PdfOxide

let doc = try PdfDocument(path: "document.pdf")

// Implementation-defined cost units — useful for relative comparisons
let cost = try doc.estimateRenderTime(0)
print("Estimated render cost for page 0: \(cost)")

C ABI: int32_t pdf_estimate_render_time(const void *doc, int32_t page_index, int32_t *error_code).

Advanced variant coverage

Method C ABI Rust Python Swift
Raw RGBA buffer pdf_render_page_raw RenderOptions::as_raw() + render_page render_pixmap(page, dpi) renderPageRaw(_:dpi:)
Options + OCG filter pdf_render_page_with_options_ex RenderOptions.excluded_layers + render_page render_page(..., excluded_layers=[...]) renderPageWithOptionsEx(...)
Render-time estimate pdf_estimate_render_time estimateRenderTime(_:)

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);
}

Java

try (PdfDocument doc = PdfDocument.open(Path.of("document.pdf"))) {
    for (int i = 0; i < doc.pageCount(); i++) {
        byte[] png = doc.render(i, 200);
        Files.write(Path.of("page_" + (i + 1) + ".png"), png);
    }
}

Kotlin

PdfDocument.open(Path.of("document.pdf")).use { doc ->
    for (i in 0 until doc.pageCount()) {
        val png = doc.render(i, 200)
        Files.write(Path.of("page_${i + 1}.png"), png)
    }
}

Scala

Using.resource(PdfDocument.open("document.pdf")) { doc =>
  for (i <- 0 until doc.pageCount()) {
    val png = doc.render(i, 200)
    Files.write(Paths.get(s"page_${i + 1}.png"), png)
  }
}

Clojure

(with-open [doc (pdf/open "document.pdf")]
  (doseq [i (range (pdf/page-count doc))]
    (with-open [out (io/output-stream (str "page_" (inc i) ".png"))]
      (.write out (pdf/render doc i 200)))))

Ruby

PdfOxide::PdfDocument.open('document.pdf') do |doc|
  (0...doc.page_count).each do |i|
    File.binwrite("page_#{i + 1}.png", doc.render(i, dpi: 200))
  end
end

C++

auto doc = pdf_oxide::Document::open("document.pdf");
for (int i = 0; i < doc.page_count(); i++) {
    doc.render_page_with_options(i, /*dpi=*/200, /*format=*/0,
                                 1.0f, 1.0f, 1.0f, 1.0f, false, true, 85)
       .save("page_" + std::to_string(i + 1) + ".png");
}

Swift

let doc = try Document.open("document.pdf")
for i in 0..<(try doc.pageCount()) {
    let image = try doc.renderPageWithOptions(i, dpi: 200)
    try image.save("page_\(i + 1).png")
}

Dart

final doc = PdfDocument.open('document.pdf');
for (var i = 0; i < doc.pageCount; i++) {
  doc.renderPageWithOptions(i, dpi: 200).save('page_${i + 1}.png');
}

R

doc <- pdf_open("document.pdf")
for (i in seq_len(pdf_page_count(doc)) - 1L) {
  img <- pdf_render_page_with_options(doc, i, dpi = 200L)
  pdf_rendered_image_save(img, sprintf("page_%d.png", i + 1))
}

Julia

doc = open_document("document.pdf")
for i in 0:(page_count(doc) - 1)
    img = render_page_with_options(doc, i, 200, 0, 1.0, 1.0, 1.0, 1.0, 0, 1, 85)
    save(img, "page_$(i + 1).png")
end

Zig

var doc = try pdf_oxide.Document.open("document.pdf");
var i: i32 = 0;
const pages = try doc.pageCount();
while (i < pages) : (i += 1) {
    var image = try doc.renderPageWithOptions(a, i, 200, 0, 1.0, 1.0, 1.0, 1.0, false, true, 85);
    defer image.deinit();
    var buf: [64]u8 = undefined;
    const name = try std.fmt.bufPrintZ(&buf, "page_{d}.png", .{i + 1});
    try image.save(name);
}

Objective-C

POXDocument *doc = [POXDocument openPath:@"document.pdf" error:&err];
for (NSInteger i = 0; i < [doc pageCountError:&err]; i++) {
    POXRenderedImage *image = [doc renderPageWithOptions:i dpi:200 format:0
                                                     bgR:1.0 bgG:1.0 bgB:1.0 bgA:1.0
                                   transparentBackground:0 renderAnnotations:1
                                             jpegQuality:85 error:&err];
    [image saveToPath:[NSString stringWithFormat:@"page_%ld.png", (long)(i + 1)] error:&err];
}

Elixir

{:ok, doc} = PdfOxide.open("document.pdf")
{:ok, n} = PdfOxide.page_count(doc)
for i <- 0..(n - 1) do
  {:ok, image} = PdfOxide.render_page_with_options(doc, i, dpi: 200)
  PdfOxide.save(image, "page_#{i + 1}.png")
end

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);

Java

try (PdfDocument doc = PdfDocument.open(Path.of("document.pdf"))) {
    // Low DPI for fast thumbnail generation (PNG)
    byte[] thumb = doc.render(0, 72);
    Files.write(Path.of("thumbnail.png"), thumb);
}

Kotlin

PdfDocument.open(Path.of("document.pdf")).use { doc ->
    // Low DPI for fast thumbnail generation (PNG)
    Files.write(Path.of("thumbnail.png"), doc.render(0, 72))
}

Scala

Using.resource(PdfDocument.open("document.pdf")) { doc =>
  // Low DPI for fast thumbnail generation (PNG)
  Files.write(Paths.get("thumbnail.png"), doc.render(0, 72))
}

Clojure

(with-open [doc (pdf/open "document.pdf")]
  ;; Low DPI for fast thumbnail generation (PNG)
  (with-open [out (io/output-stream "thumbnail.png")]
    (.write out (pdf/render doc 0 72))))

Ruby

PdfOxide::PdfDocument.open('document.pdf') do |doc|
  # Low DPI + JPEG (format 1) for fast thumbnail generation
  thumb = doc.render_with_layers(0, dpi: 72, format: 1)
  File.binwrite('thumbnail.jpg', thumb)
end

C++

auto doc = pdf_oxide::Document::open("document.pdf");
// render_page_thumbnail fits the page within `size` px (format 1 = JPEG)
doc.render_page_thumbnail(0, /*size=*/256, /*format=*/1).save("thumbnail.jpg");

Swift

let doc = try Document.open("document.pdf")
// renderPageThumbnail fits the page within `size` px (format 1 = JPEG)
try doc.renderPageThumbnail(0, size: 256, format: 1).save("thumbnail.jpg")

Dart

final doc = PdfDocument.open('document.pdf');
// renderPageThumbnail fits the page within `size` px (format 1 = JPEG)
doc.renderPageThumbnail(0, 256, 1).save('thumbnail.jpg');

R

doc <- pdf_open("document.pdf")
# pdf_render_page_thumbnail fits the page within `size` px (format 1 = JPEG)
thumb <- pdf_render_page_thumbnail(doc, 0, size = 256L, format = 1L)
pdf_rendered_image_save(thumb, "thumbnail.jpg")

Julia

doc = open_document("document.pdf")
# render_page_thumbnail fits the page within `size` px (format 1 = JPEG)
thumb = render_page_thumbnail(doc, 0, 256, 1)
save(thumb, "thumbnail.jpg")

Zig

var doc = try pdf_oxide.Document.open("document.pdf");
// renderPageThumbnail fits the page within `size` px (format 1 = JPEG)
var thumb = try doc.renderPageThumbnail(a, 0, 256, 1);
defer thumb.deinit();
try thumb.save("thumbnail.jpg");

Objective-C

POXDocument *doc = [POXDocument openPath:@"document.pdf" error:&err];
// renderPageThumbnail fits the page within `size` px (format 1 = JPEG)
POXRenderedImage *thumb = [doc renderPageThumbnail:0 size:256 format:1 error:&err];
[thumb saveToPath:@"thumbnail.jpg" error:&err];

Elixir

{:ok, doc} = PdfOxide.open("document.pdf")
# render_page_thumbnail fits the page within `size` px (format 1 = JPEG)
{:ok, thumb} = PdfOxide.render_page_thumbnail(doc, 0, 256, 1)
PdfOxide.save(thumb, "thumbnail.jpg")

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)

FAQ

What format is the raw render buffer? Premultiplied RGBA8888, row-major, top-left origin. The length is exactly width * height * 4 bytes — no PNG/JPEG header, no compression. Use it when you feed pixels into a GPU texture or an external image pipeline. In Rust call RenderOptions::with_dpi(dpi).as_raw(), in Python call doc.render_pixmap(page, dpi=...), and in Swift call doc.renderPageRaw(page, dpi:).

Can I hide a watermark or other layer when rendering? Yes. Pass the optional-content-group (OCG) /Names you want suppressed: render_page(0, excluded_layers=["Watermark"]) in Python, RenderOptions.excluded_layers in Rust, or renderPageWithOptionsEx(... excludedLayers:) in Swift. The renderer also resolves OCMD references to those groups.

Why isn’t estimate_render_time available in Python? It is feature-gated to the C ABI and the Swift wrapper in v0.3.69 — the Rust crate and the Python/Go/Node bindings do not expose it. Use the Swift estimateRenderTime(_:) method or the C function pdf_estimate_render_time directly.

How fast is rendering? PDF Oxide’s text extraction core runs at 0.8 ms mean / 100% pass rate on the benchmark corpus; rendering reuses the same pure-Rust parser with a process-level font cache, so repeat renders avoid re-parsing the system font database.