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:
- 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) |
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.
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