Rendering PDF pages to PNG
pdfboss rasterizes pages to RGBA pixels with anti-aliasing and encodes them as
PNG, PPM, BMP or JPEG. The scale factor is the points-to-pixels ratio: at 1.0 one PDF point
becomes one pixel, so a US Letter page renders to 612 × 792 pixels; at 2.0
to 1224 × 1584. The pixel size is ceil(crop_w * scale) × ceil(crop_h * scale) after page rotation, on a white background.
This chapter is about rasterizing whole pages. To pull out the images a page embeds, at their native resolution, see Extracting images.
CLI
pdfboss render report.pdf --page 1
pdfboss render report.pdf --page 1 --scale 2 -o page-1@2x.png
pdfboss render report.pdf --page 1 --scale 2 -o page-1.ppm
The first form writes page-1.png (--page is 1-based). Further flags:
-o <OUT>: the output file; its extension picks the format,.png,.ppm,.bmpor.jpg(see Output formats). Any other extension is an error.--fonts <FONTS>: which fonts to paint, one ofembedded-only,all-embeddedorfull(see the tiers below). The default resolves tofullwhen substitute faces are available (the compiled-in OFL set or--font-dir), otherwiseall-embedded.--font-dir <FONT_DIR>: a directory of substitute faces for--fonts full(e.g. an installedpdfboss-fontspackage), named as listed under Substitute face files. Overrides the compiled-in OFL set.--png-compression <PNG_COMPRESSION>:none,fast,defaultorbest; PNG only.--jpeg-quality <1-100>: the JPEG quality (default 90); JPEG only.--password <PASSWORD>: for encrypted files, covered in Encrypted documents.
Anything the render dropped is warned on stderr.
Font tiers
Glyph painting is staged in tiers; each tier is a strict superset of the previous one.
embedded-onlypaints only embedded TrueType outlines: the fastest tier, and TrueType only, not every embedded font.all-embedded(the default when no substitute faces are available) paints every embedded font program: TrueType, CFF, Type1 and Type3.full(the default whenever substitute faces are available) additionally substitutes a replacement face for non-embedded simple fonts: from a directory you supply (--font-dir,font_dir=), or from the compiled-in OFL Croscore set (Arimo/Tinos/Cousine, metric-compatible with Helvetica/Times/Courier). In Python,pip install pdfboss[full]installs those faces as thepdfboss-fontspackage; with neitherfont_dirnor that package available,fonts="full"raisesValueError.
Text a tier leaves unpainted still advances (through the PDF's own /Widths,
or the Adobe Core-14 AFM tables for a standard-14 face), so everything painted
around it stays where the page put it. /Symbol and /ZapfDingbats have no
license-clean substitute and stay blank; see
Limitations.
Substitute face files
A substitute directory (--font-dir, font_dir=, SubstituteSource::Dir)
holds one file per face, looked up by these exact names:
| Family | Files |
|---|---|
| Sans (Arimo) | Arimo[wght].ttf, Arimo-Italic[wght].ttf (variable files, rendered at their default instance), Arimo-Bold.ttf, Arimo-BoldItalic.ttf (static bold instances) |
| Serif (Tinos) | Tinos-Regular.ttf, Tinos-Bold.ttf, Tinos-Italic.ttf, Tinos-BoldItalic.ttf |
| Mono (Cousine) | Cousine-Regular.ttf, Cousine-Bold.ttf, Cousine-Italic.ttf, Cousine-BoldItalic.ttf |
A file that is missing or unreadable means no substitution for the faces that map to it; nothing else in the directory is read.
Leniency and reporting
Rendering never fails because one construct in a page would not read: content
pdfboss cannot fetch, decode or parse is skipped and the rest of the page
still rasterizes. The honest consequence is that a page can come back blank
without an error. The reporting variants return what was dropped or
approximated, one line per distinct loss, for example
"153 glyphs skipped: no glyph for code 9 in /MBIPWP+Times-Roman" or
"6 annotations skipped: the resource is missing". An empty report means the
page rasterized exactly as it describes itself.
Two things are deliberately not reported, because they are configured behavior
rather than a failure: text left unpainted by the requested font tier, and
content in optional-content layers (PDF layers) the document's default
configuration turns off. The latter is counted separately, on the report's
hidden counter in Rust.
Python
Page.render returns PNG bytes. scale must be positive and finite
(ValueError otherwise).
from pathlib import Path
import pdfboss
doc = pdfboss.Document("report.pdf")
page = doc[0]
png = page.render(scale=2.0)
Path("page-1.png").write_bytes(png)
render accepts fonts= ("embedded-only", "all-embedded", "full"),
font_dir=, compression= ("none", "fast", "default", "best"),
format= ("png", "ppm", "bmp", "jpeg", see
Output formats) and quality= (1 to 100, JPEG only),
and releases the GIL while it runs. fonts= defaults to None, which
resolves to "full" when font_dir= is given or the pdfboss-fonts
package is importable, and to "all-embedded" otherwise. Page.render_reporting renders the same
way and returns (png, warnings):
png, warnings = page.render_reporting()
for line in warnings:
print(line)
Document.render_pages renders many pages fanned out across the machine's
cores: every page by default, or the 0-based pages given, returned in the
order given.
pngs = doc.render_pages(scale=2.0)
first_two_reversed = doc.render_pages(pages=[1, 0])
The full signature is render_pages(pages=None, scale=1.0, fonts=None, font_dir=None, compression="default", format="png", quality=90); fonts,
font_dir, compression, format and quality mean the same as on
Page.render, applied to every page, and a fonts of None resolves the
same way. The stub file
_pdfboss.pyi
documents each parameter.
All three have async twins on AsyncPage/AsyncDocument, which also render
documents opened over HTTP. See
Async and remote documents.
Output formats
Every render entry point writes one of four formats, chosen by format=
in Python, by the -o extension on the CLI, and by
pdfboss_render::ImageFormat in Rust:
| format | pixels | what it costs |
|---|---|---|
png (default) | RGBA 8-bit, filtered and deflated | the compression level below |
ppm | binary P6: P6 <w> <h> 255\n then RGB rows, top-down | one packing pass |
bmp | 24-bit BGR, bottom-up rows padded to four bytes, 54-byte header | one packing pass |
jpeg (jpg) | baseline JFIF, 4:4:4, lossy at quality 1 to 100 (default 90) | a DCT per 8×8 block |
PPM and BMP drop the alpha channel; a rendered page is filled white, so alpha is 255 everywhere and nothing is lost. Reach for them when the pixels are consumed right away (a benchmark, an OCR or vision pipeline, a diff against another renderer) and the PNG encode would be wasted work: Pillow, numpy and ImageMagick read both directly.
JPEG is the one lossy choice: it trades exact pixels for smaller files on photographic and scanned pages, with the quality knob scaling the standard quantization tables (50 uses them as printed, 100 keeps every coefficient). On a mostly white text page PNG stays both smaller and exact, so prefer JPEG only where the content is continuous-tone. Chroma is not subsampled, so colored text keeps sharp edges. The encoder is pdfboss's own, written from the JPEG specification, and costs two to three times the default PNG encode per page.
ppm = page.render(scale=2.0, format="ppm")
bmp, warnings = page.render_reporting(scale=2.0, format="bmp")
jpg = page.render(scale=2.0, format="jpeg", quality=80)
PNG compression
The compression level trades encode time against file size; every level
produces the same pixels. none is fastest and largest, fast is very fast
with a decent ratio, default balances the two, and best produces the
smallest files, much slower. The level only touches the PNG encoder; even
none still filters rows and writes checksums, so a raw-pixel consumer is
better served by ppm or bmp. Pick it by whether you are writing
throwaway intermediates or archiving.
Rust
pdfboss_render::render_page(doc, page, scale) returns a Pixmap: width,
height, and data holding width * height * 4 RGBA bytes (straight alpha,
row-major from the top-left). Pixmap::save_png writes it to a file;
encode_png/encode_png_with return the bytes, the latter taking a
PngCompression (None, Fast, Balanced or Best; Balanced is the
level the other surfaces call default).
render_page_with_options adds RenderOptions, a struct of four public
fields:
glyph_paintingselects theGlyphPaintingtier:EmbeddedTrueTypeOnly,AllEmbeddedorFull.substitutessays where theFulltier's replacement faces come from.SubstituteSource::Builtinis the compiled-in OFL set, present only when the crate is built with thesubstitute-fontsCargo feature; without that feature,Builtindegrades to no substitution, soFullbehaves exactly likeAllEmbedded. The probepdfboss_render::builtin_fonts_available()reports whether the compiled-in set exists.SubstituteSource::Dir(path)reads faces from a directory named as in Substitute face files. The defaultSubstituteSource::Nonesubstitutes nothing, soFullbehaves likeAllEmbeddeduntil you opt in.oc: Option<Arc<OcState>>is the document's optional-content visibility. The synchronous entry points fill it from the document when it isNone; an asynchronous caller builds it itself, fromAsyncDocument::oc_state, and leaving itNonethere renders every layer. See Async and remote documents.cache: Option<Arc<RenderCache>>shares oneRenderCacheacross a whole-document walk. It retains loaded fonts and parsedICCBasedcolorspace outcomes across pages;Nonekeeps every load page-local.
render_page_reporting returns the
RenderReport alongside the pixels; report.summary() is a one-line count
per kind, report.warnings() one line per distinct drop.
use pdfboss_core::Document;
use pdfboss_render::{
render_page, render_page_reporting, GlyphPainting, PngCompression, RenderOptions,
};
fn main() -> Result<(), Box<dyn std::error::Error>> {
let doc = Document::open("report.pdf")?;
let page = doc.page(0)?;
let pixmap = render_page(&doc, &page, 2.0)?;
pixmap.save_png("page-1.png")?;
let opts = RenderOptions {
glyph_painting: GlyphPainting::EmbeddedTrueTypeOnly,
..RenderOptions::default()
};
let (pixmap, report) = render_page_reporting(&doc, &page, 1.0, &opts)?;
if let Some(summary) = report.summary() {
eprintln!("{summary}");
}
for warning in report.warnings() {
eprintln!("{warning}");
}
let png = pixmap.encode_png_with(PngCompression::Best)?;
std::fs::write("page-1-small.png", png)?;
Ok(())
}
For a whole document, pass one RenderCache to every page, so each font
program and each ICCBased profile loads once per document rather than once
per page:
use std::sync::Arc;
use pdfboss_core::{map_pages, Document};
use pdfboss_render::{render_page_with_options, RenderCache, RenderOptions};
fn main() -> Result<(), Box<dyn std::error::Error>> {
let doc = Document::open("report.pdf")?;
let opts = RenderOptions {
cache: Some(Arc::new(RenderCache::default())),
..RenderOptions::default()
};
let outcomes = map_pages(&doc, |doc, page| {
render_page_with_options(doc, page, 2.0, &opts)
});
for (index, pixmap) in outcomes.into_iter().enumerate() {
pixmap?.save_png(format!("page-{}.png", index + 1))?;
}
Ok(())
}