페이지 렌더링
tiny-skia 기반의 순수 Rust 렌더링 엔진을 사용하여 PDF 페이지를 래스터 이미지(PNG 또는 JPEG)로 렌더링합니다. Poppler나 MuPDF 같은 외부 의존성이 필요 없습니다.
폰트 폴백 체인
렌더링에는 글리프가 필요합니다. PDF가 호스트에 설치되지 않은 비내장 폰트(ArialMT, TimesNewRomanPSMT 등)를 참조하면, PDF Oxide는 잘 알려진 오픈 소스 폰트 폴백 체인을 순서대로 탐색합니다.
- DejaVu Sans / DejaVu Serif / DejaVu Sans Mono
- Noto Sans / Noto Serif
- FreeSans / FreeSerif
폴백이 발동하면 누락된 폰트 이름과 함께 경고가 로그에 기록되며, 실행 가능한 힌트도 제공됩니다. Linux에서는 liberation-fonts, dejavu-fonts, noto-fonts를 설치하고, 최소 컨테이너 환경에서는 Dockerfile에 해당 패키지를 추가하세요.
성능 참고 사항
- 시스템 폰트 DB는 프로세스 수준에서 캐시되므로 이후 렌더링은 파싱된 인덱스를 재사용합니다.
- 다중 문자 글리프 클러스터의 너비가 올바르게 누적됩니다(라틴/아랍어 서브셋 CID 폰트에서 합자 누락 현상 수정).
- 잘못된 이미지(
/ColorSpace누락, 잘못된 크기)는 페이지를 중단시키지 않고 경고와 함께 건너뜁니다.
빠른 예제
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();
기능 활성화
페이지 렌더링은 rendering 피처 플래그가 필요합니다.
[dependencies]
pdf_oxide = { version = "0.3", features = ["rendering"] }
이를 통해 tiny-skia(2D 렌더링), fontdb(폰트 로딩), rustybuzz(텍스트 셰이핑)가 추가됩니다.
렌더링 옵션
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 필드
| 필드 | 타입 | 기본값 | 설명 |
|---|---|---|---|
dpi |
u32 |
150 |
인치당 점 수(해상도) |
format |
ImageFormat |
Png |
출력 형식(Png 또는 Jpeg) |
background |
Option<[f32; 4]> |
흰색 [1,1,1,1] |
RGBA 배경색(채널당 0.0–1.0) |
render_annotations |
bool |
true |
주석 렌더링 여부 |
jpeg_quality |
u8 |
85 |
JPEG 품질 1–100(PNG에서는 무시) |
빌더 메서드
| 메서드 | 설명 |
|---|---|
RenderOptions::with_dpi(dpi) |
지정한 DPI로 옵션 생성 |
.with_transparent_background() |
배경을 투명으로 설정(PNG 전용) |
.as_jpeg(quality) |
지정한 품질의 JPEG 출력으로 전환 |
ImageFormat
| 변형 | 설명 |
|---|---|
Png |
무손실 압축, 투명도 지원 |
Jpeg |
손실 압축, 파일 크기 작음, 투명도 미지원 |
RenderedImage
render_page() 함수는 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
}
메서드
| 메서드 | 반환값 | 설명 |
|---|---|---|
save(path) |
Result<()> |
이미지를 파일에 저장 |
as_bytes() |
&[u8] |
원시 이미지 바이트 가져오기 |
고급 렌더링 변형
인코딩된 PNG/JPEG 경로 외에도 PDF Oxide는 세 가지 저수준 렌더링 진입점을 제공합니다. 원시 픽셀 버퍼(PNG/JPEG 인코딩 생략), 선택적 콘텐츠 그룹(OCG) 레이어 필터링을 추가한 확장 옵션 변형, 그리고 저비용 렌더링 시간 추정입니다.
PNG/JPEG 대신 원시 RGBA 픽셀 버퍼를 얻으려면?
PNG/JPEG 인코딩 없이 GPU 텍스처, 이미지 라이브러리, 컴포지터에 픽셀을 직접 전달할 때 원시 렌더링 경로를 사용하세요. 버퍼는 사전 곱해진(premultiplied) RGBA8888, 행 우선, 좌상단 원점이며 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")
C ABI에서는 pdf_render_page_raw(doc, page_index, dpi, *out_width, *out_height, *error_code)로 노출되며, pdf_get_rendered_image_data로 픽셀 바이트를 가져오고 pdf_rendered_image_free로 핸들을 해제합니다.
렌더링 시 선택적 콘텐츠(OCG) 레이어를 숨기려면?
render_page_with_options_ex는 전체 렌더 옵션 표면에 더해 숨길 선택적 콘텐츠 그룹 /Name 목록을 받습니다. 숨기고 싶은 레이어 이름(예: “Confidential” 워터마크 레이어나 “Construction lines” CAD 레이어)을 전달하세요. ISO 32000-1 §8.11.2에 따라 명명된 OCG를 참조하는 OCMD 참조도 올바르게 처리됩니다.
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")
C ABI 시그니처는 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)입니다. excluded_layers에 null 포인터(또는 카운트 0)를 전달하면 필터링이 비활성화되어 일반 pdf_render_page_with_options와 동일하게 동작합니다.
렌더링 전에 비용을 추정하려면?
estimate_render_time은 실제 래스터화 없이 배치 작업이 우선순위를 정하거나 작업량을 예산에 맞출 수 있도록 페이지의 저비용 구현 정의 비용 수치를 반환합니다. 현재 C ABI와 Swift 래퍼를 통해 노출되며, Rust 크레이트와 Python/Go/Node 바인딩에는 관용적 메서드로 제공되지 않습니다.
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).
고급 변형 지원 범위
| 메서드 | C ABI | Rust | Python | Swift |
|---|---|---|---|---|
| 원시 RGBA 버퍼 | pdf_render_page_raw |
RenderOptions::as_raw() + render_page |
render_pixmap(page, dpi) |
renderPageRaw(_:dpi:) |
| 옵션 + OCG 필터 | pdf_render_page_with_options_ex |
RenderOptions.excluded_layers + render_page |
render_page(..., excluded_layers=[...]) |
renderPageWithOptionsEx(...) |
| 렌더링 시간 추정 | pdf_estimate_render_time |
— | — | estimateRenderTime(_:) |
Python API
doc.render_page(page, dpi=None, format=None)
페이지를 이미지 바이트로 렌더링합니다.
| 파라미터 | 타입 | 기본값 | 설명 |
|---|---|---|---|
page |
int |
필수 | 0부터 시작하는 페이지 인덱스 |
dpi |
int |
72 |
인치당 점 수 |
format |
str |
"png" |
출력 형식: "png" 또는 "jpeg" |
반환값: bytes — 인코딩된 이미지 데이터(PNG 또는 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?)
페이지를 PNG 바이트로 렌더링합니다.
| 파라미터 | 타입 | 기본값 | 설명 |
|---|---|---|---|
pageIndex |
number |
필수 | 0부터 시작하는 페이지 인덱스 |
dpi |
number |
150 |
인치당 점 수 |
반환값: Uint8Array — PNG 이미지 데이터
const pngBytes = doc.renderPage(0); // 150 DPI default
const hiRes = doc.renderPage(0, 300); // 300 DPI
주요 사용 사례
모든 페이지 렌더링
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
썸네일 생성
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")
합성용 투명 배경
let opts = RenderOptions::with_dpi(150).with_transparent_background();
let image = render_page(&mut doc, 0, &opts)?;
image.save("page_transparent.png")?;
사용자 정의 배경색
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)?;
고품질 인쇄 출력
// 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);
PDF를 이미지로 평탄화
전체 PDF를 평면적인 이미지 기반 PDF로 변환합니다. 각 페이지를 지정된 DPI의 래스터 이미지로 렌더링한 후 새 PDF로 조립합니다. 모든 주석, 폼 필드, 오버레이, 폰트가 영구적으로 소각됩니다.
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)?;
파라미터
| 파라미터 | Python | JavaScript | Rust | 기본값 | 설명 |
|---|---|---|---|---|---|
| DPI | dpi |
dpi |
dpi |
150 | 각 페이지 렌더링 해상도 |
반환값: PDF 파일 바이트 — 각 페이지가 전체 페이지 이미지인 새 PDF.
활용 사례
- 개인정보 삭제(Redaction) — 삭제 후 평탄화하여 숨겨진 콘텐츠를 영구 제거
- 보관 — 모든 뷰어에서 동일하게 보이는 시각적 스냅샷 생성
- 일관된 렌더링 — PDF 뷰어 간 폰트 및 레이아웃 차이 제거
- 인쇄 준비 — 복잡한 오버레이를 평탄화하여 안정적인 인쇄 보장
- 폼 제출 — 작성된 폼 필드 값을 영구적으로 소각
고품질 평탄화
# 300 DPI for print-quality flattening
flattened = doc.flatten_to_images(dpi=300)
with open("print_ready.pdf", "wb") as f:
f.write(flattened)
렌더링 파이프라인
PDF Oxide의 렌더러는 페이지 콘텐츠 스트림을 순서대로 처리합니다.
- 크기 계산 — 페이지 크기와 DPI로 픽셀 크기 계산(72포인트 = 1인치)
- 배경 — 설정된 배경색으로 픽스맵 생성
- 변환 — 좌표 변환 적용(PDF 좌하단 원점 → 이미지 좌상단 원점)
- 콘텐츠 스트림 — 모든 PDF 연산자 파싱 및 실행:
- 패스 — 채우기/획이 있는 선, 곡선, 사각형
- 텍스트 — 폰트 선택 및 간격이 적용된 위치 지정 텍스트
- 이미지 — 내장 래스터 이미지(DeviceGray, DeviceRGB, DeviceCMYK)
- 그래픽 상태 — 투명도, 블렌드 모드, 클리핑, 선 스타일
- 인코딩 — PNG 또는 JPEG로 출력
지원하는 PDF 연산자
| 카테고리 | 연산자 |
|---|---|
| 그래픽 상태 | q Q(저장/복원), cm(변환 행렬) |
| 색상 | rg RG g G k K(RGB, 회색조, CMYK) |
| 패스 구성 | m l c v y re h(이동, 선, 곡선, 사각형, 닫기) |
| 패스 그리기 | S s f F f* B B* b b* n(획, 채우기, 둘 다) |
| 클리핑 | W W*(비영 및 짝홀 감기) |
| 텍스트 | BT ET Td TD Tm Tf Tj TJ ' " |
| 이미지 | Do(XObject: 이미지 및 폼 XObject) |
| 확장 상태 | gs(투명도 ca/CA, 블렌드 모드 BM) |
자주 묻는 질문
원시 렌더 버퍼의 형식은 무엇인가요?
사전 곱해진(premultiplied) RGBA8888, 행 우선, 좌상단 원점입니다. 길이는 정확히 width * height * 4 바이트이며 PNG/JPEG 헤더나 압축이 없습니다. GPU 텍스처나 외부 이미지 파이프라인에 픽셀을 공급할 때 사용하세요. Rust에서는 RenderOptions::with_dpi(dpi).as_raw(), Python에서는 doc.render_pixmap(page, dpi=...), Swift에서는 doc.renderPageRaw(page, dpi:)를 호출하세요.
렌더링 시 워터마크나 다른 레이어를 숨길 수 있나요?
네. 숨기고 싶은 선택적 콘텐츠 그룹(OCG) /Name을 전달하세요. Python에서는 render_page(0, excluded_layers=["Watermark"]), Rust에서는 RenderOptions.excluded_layers, Swift에서는 renderPageWithOptionsEx(... excludedLayers:)를 사용합니다. 렌더러는 해당 그룹에 대한 OCMD 참조도 해석합니다.
estimate_render_time이 Python에서 사용할 수 없는 이유는?
v0.3.69에서는 C ABI와 Swift 래퍼에만 제공됩니다. Rust 크레이트와 Python/Go/Node 바인딩에는 노출되지 않습니다. Swift의 estimateRenderTime(_:) 메서드나 C 함수 pdf_estimate_render_time을 직접 사용하세요.
렌더링 속도는 어느 정도인가요? PDF Oxide의 텍스트 추출 코어는 벤치마크 코퍼스에서 평균 0.8ms, 통과율 100%로 실행됩니다. 렌더링은 동일한 순수 Rust 파서와 프로세스 수준 폰트 캐시를 재사용하므로 반복 렌더링 시 시스템 폰트 데이터베이스를 다시 파싱하지 않습니다.
관련 페이지
- 텍스트 추출 — 페이지에서 텍스트 콘텐츠 추출
- 이미지 추출 — PDF에서 내장 이미지 추출
- 스캔된 PDF OCR — OCR은 내부적으로 렌더링을 사용
- 이미지로부터 생성 — 이미지를 PDF로 변환