Programmatisch PDF's genereren is zo'n taak die eenvoudig klinkt tot je het werkelijk probeert. De meeste bibliotheken geven je ofwel een laag stroompje bytes om mee te worstelen, ofwel een sjabloonmotor op hoog niveau die uit elkaar valt zodra je precieze grip nodig hebt op lay-out, typografie of graphics. Er is een middenweg — en het is dezelfde grafiekmotor die elke webpagina rendert die je in Chrome naar PDF afdrukt, elk scherm op Android, en elk frame in een Flutter-app: Google Skia.
Deze gids laat zien hoe je Skia vanuit Rust aanstuurt via de crate skia-safe om vector-PDF's te produceren met volledige controle over tekst, vormen, afbeeldingen en lay-outs over meerdere pagina's.
Kernpunten
- Skia is de opensource 2D-grafiekbibliotheek van Google, sinds 2005 in actieve ontwikkeling, met meer dan 2 miljoen downloads van de Rust-bindings op crates.io.
- De crate skia-safe (v0.93) biedt veilige, idiomatische Rust-bindings met een typestate-patroon dat misvormde PDF-opbouw al tijdens het compileren opvangt.
- Skia produceert echte vector-PDF's met selecteerbare tekst, geen gerasterde afbeeldingen — dezelfde aanpak die Chrome gebruikt voor print-naar-PDF.
- Het ondersteunt archiefconformiteit volgens PDF/A, getagde en toegankelijke PDF's (PDF/UA), lettertypen uitdunnen via HarfBuzz, en documenten met meerdere pagina's in gemengde formaten.
- Kant-en-klare binaries worden standaard meegeleverd, dus de eerste build duurt onder de minuut in plaats van 20 tot 40 minuten vanaf de bron.
Wat Skia is en waarom je het in Rust voor PDF-generatie gebruikt
Skia is een opensource 2D-grafiekbibliotheek die door Google wordt onderhouden. Ze is sinds 2005 in actieve ontwikkeling en is een van de meest beproefde renderengines die er bestaan. Druk je in Google Chrome een webpagina naar PDF af, dan is het de PDF-achterkant van Skia die de gerenderde pagina in een PDF-bestand vertaalt. Tekent Android zijn interface, dan verzorgt Skia het renderen. Flutter gebruikt Skia als renderengine voor cross-platform apps.
Wat Skia aantrekkelijk maakt voor programmatische PDF-generatie in Rust:
- Echte vectoruitvoer. Tekst blijft selecteerbaar, vormen blijven scherp bij elke zoom, en bestandsgroottes blijven redelijk — omdat Skia zijn tekenbewerkingen naar native PDF-primitieven vertaalt in plaats van pixels te rasteren.
- Volledige 2D-grafiek-API. Beziercurves, verlopen, affiene transformaties, cliprijen, mengmodi en typografische bediening — alles wat je van een professionele grafiekbibliotheek verwacht, en het vertaalt allemaal naar PDF.
- Getagde en toegankelijke PDF's. Skia ondersteunt structuurbomen en knooppunt-ID's, wat betekent dat je documenten kunt produceren die aan de toegankelijkheidsnorm PDF/UA voldoen.
- Archiefconformiteit volgens PDF/A. Eén metadatavlag schakelt uitvoer volgens PDF/A-2b in voor langdurig documentbehoud — vereist voor juridische, medische en overheidsdocumenten.
- Lettertypen uitdunnen. Sluit je lettertypen in, dan verwijdert Skia via HarfBuzz de ongebruikte tekens, waardoor bestanden compact blijven.
De skia-safe-crate opzetten voor PDF-generatie in Rust
De crate skia-safe biedt veilige, idiomatische Rust-bindings voor Skia. Ze verpakt de C++-bibliotheek via FFI en toont daarbij een API die in Rust natuurlijk aanvoelt — compleet met eigenaarschapssemantiek, levensduurgaranties, en een typestate-patroon dat hele categorieën bugs tijdens het compileren opvangt.
Voeg het toe aan je project:
[dependencies]
skia-safe = "0.93"
De functie pdf staat standaard aan, dus er zijn geen extra vlaggen nodig. Standaard haalt de crate kant-en-klare Skia-binaries voor jouw platform op, waardoor de eerste build onder de minuut duurt in plaats van de 20 tot 40 minuten die Skia vanaf de bron compileren zou kosten.
Hoe je in Rust met Skia een PDF-bestand genereert
Het kernidee is rechttoe rechtaan: maak een document, begin een pagina, teken op het canvas, sluit de pagina af, sluit het document. Dit is een minimaal voorbeeld dat een PDF van één pagina oplevert:
use skia_safe::pdf;
fn main() {
let mut output = Vec::new();
// Maak een document — US Letter-formaat (612 x 792 punten)
let mut document = pdf::new_document(&mut output, None)
.begin_page((612, 792), None);
let canvas = document.canvas();
// Teken een gevulde rechthoek
let mut paint = skia_safe::Paint::default();
paint.set_color(skia_safe::Color::from_rgb(41, 98, 255));
canvas.draw_rect(
skia_safe::Rect::from_xywh(72.0, 72.0, 468.0, 100.0),
&paint,
);
// Teken tekst
let font = skia_safe::Font::default();
paint.set_color(skia_safe::Color::WHITE);
canvas.draw_str("Hello from Skia + Rust", (90.0, 130.0), &font, &paint);
// Finalize
document.end_page().close();
std::fs::write("hello.pdf", &output).unwrap();
}
Draai dit, open hello.pdf, en je ziet een blauwe rechthoek met witte tekst — allemaal als vectorgraphics, volledig selecteerbaar en zoombaar.

Veiligheid tijdens het compileren met het typestate-patroon
Een van de elegantste aspecten van de Rust-bindings is het typestate-patroon op het type Document. Het document wisselt tussen twee toestanden: Open (klaar om een pagina te beginnen) en OnPage (actief aan het tekenen).
// Document<state::Open> — kan een pagina beginnen of het document sluiten
let document = pdf::new_document(&mut output, None);
// Document<state::OnPage> — kan bij het canvas of de pagina afsluiten
let page = document.begin_page((612, 792), None);
let canvas = page.canvas();
// Terug naar Document<state::Open> — kan een volgende pagina beginnen of sluiten
let document = page.end_page();
document.close();
Probeer je canvas() aan te roepen op een document zonder open pagina, of een document te sluiten terwijl er een pagina actief is, dan compileert de code niet. Dat is geen controle tijdens het draaien en geen assertie — het is een garantie tijdens het compileren, afgedwongen door het typesysteem van Rust. Je kunt geen misvormde PDF produceren omdat de compiler het verhindert.
Vergelijk dat met PDF-bibliotheken in C++ of Python, waar deze vergissingen leiden tot beschadigde uitvoer, stille fouten of crashes tijdens het draaien. In Rust met skia-safe is de compiler je proeflezer.
PDF-metadata toevoegen voor vindbaarheid en toegankelijkheid
PDF-metadata is wat zoekmachines, documentbeheersystemen en toegankelijkheidsgereedschap gebruiken om je document te begrijpen. Skia stelt dat beschikbaar via de struct Metadata:
use skia_safe::pdf::{self, Metadata};
let metadata = Metadata {
title: "Q4 Financial Report".to_string(),
author: "Finance Team".to_string(),
subject: "Quarterly results and projections".to_string(),
creator: "Report Generator v2.1".to_string(),
..Default::default()
};
let mut output = Vec::new();
let document = pdf::new_document(&mut output, Some(&metadata));
Voor archiefconformiteit zet je PDF/A aan met één vlag:
let metadata = Metadata {
pdf_a: true,
..Default::default()
};
Dat levert uitvoer op die aan PDF/A-2b voldoet — geschikt voor juridische stukken, medische dossiers, en alles wat over tientallen jaren nog leesbaar moet zijn.
PDF-documenten met meerdere pagina's maken in Rust
Echte documenten bestaan zelden uit één pagina. Skia gaat vanzelfsprekend om met meerdere pagina's — elke aanroep van begin_page start een nieuwe pagina, en pagina's mogen verschillende formaten hebben:
let mut output = Vec::new();
let document = pdf::new_document(&mut output, None);
// Pagina 1 — A4 staand
let mut page = document.begin_page((595, 842), None);
draw_cover_page(page.canvas());
let document = page.end_page();
// Pagina 2 — A4 liggend
let mut page = document.begin_page((842, 595), None);
draw_data_table(page.canvas());
let document = page.end_page();
// Pagina 3 — weer staand
let mut page = document.begin_page((595, 842), None);
draw_charts(page.canvas());
let document = page.end_page();
document.close();
Gemengde oriëntaties en paginaformaten binnen één document — iets waar veel PDF-bibliotheken mee worstelen — werkt direct.
Vormen, tekst en paden tekenen in Skia-PDF's
Het Canvas van Skia is een volwaardig 2D-tekenoppervlak. Alles wat je erop tekent wordt omgezet in vector-PDF-primitieven.
Vormen en paden
let canvas = page.canvas();
let mut paint = Paint::default();
paint.set_anti_alias(true);
// Gevulde rechthoek
paint.set_color(Color::from_rgb(59, 130, 246));
paint.set_style(skia_safe::PaintStyle::Fill);
canvas.draw_rect(Rect::from_xywh(50.0, 50.0, 200.0, 100.0), &paint);
// Omlijnde cirkel
paint.set_style(skia_safe::PaintStyle::Stroke);
paint.set_stroke_width(2.0);
canvas.draw_circle((300.0, 100.0), 50.0, &paint);
// Eigen bézierpad
let mut path = skia_safe::Path::new();
path.move_to((400.0, 50.0));
path.cubic_to((450.0, 20.0), (500.0, 80.0), (550.0, 50.0));
path.line_to((550.0, 150.0));
path.close();
paint.set_style(skia_safe::PaintStyle::Fill);
canvas.draw_path(&path, &paint);
Tekst renderen met selecteerbare uitvoer
let typeface = skia_safe::Typeface::from_name("Helvetica", skia_safe::FontStyle::bold())
.unwrap_or_else(skia_safe::Typeface::default);
let font = skia_safe::Font::from_typeface(&typeface, 24.0);
let mut paint = Paint::default();
paint.set_color(Color::BLACK);
canvas.draw_str("Section Title", (72.0, 72.0), &font, &paint);
// Kleinere bodytekst
let body_font = skia_safe::Font::from_typeface(&typeface, 12.0);
canvas.draw_str("Body text goes here.", (72.0, 100.0), &body_font, &paint);
De tekst in de PDF-uitvoer is selecteerbaar en doorzoekbaar — geen gerasterde afbeeldingen van letters.
Affiene transformaties
canvas.save();
canvas.translate((200.0, 300.0));
canvas.rotate(45.0, None);
canvas.draw_rect(Rect::from_xywh(-50.0, -25.0, 100.0, 50.0), &paint);
canvas.restore();
Transformaties zijn samen te stellen en vertalen rechtstreeks naar de transformatiematrix van PDF — zonder rasteren.
Afbeeldingen insluiten in PDF's die je in Rust genereert
Afbeeldingen in PDF's insluiten is waar veel bibliotheken onnodige bestandsgrootte toevoegen. Skia gaat daar verstandig mee om:
let image_data = std::fs::read("photo.jpg").unwrap();
let data = skia_safe::Data::new_copy(&image_data);
let image = skia_safe::Image::from_encoded(data).expect("Failed to decode image");
canvas.draw_image(&image, (72.0, 200.0), None);
Is ondersteuning voor JPEG-codering in de metadata ingesteld, dan geeft Skia JPEG-gegevens rechtstreeks door aan de PDF zonder ze opnieuw te coderen — met behoud van kwaliteit en met minimale bestandsgrootte. Zonder dat worden afbeeldingen met deflate gecomprimeerd, wat bij fotografisch materiaal de bestandsgrootte flink kan vergroten.
Je stuurt dat via de metadata:
let metadata = Metadata {
encoding_quality: 85, // JPEG quality (101 = lossless/deflate)
..Default::default()
};
Rust-PDF-bibliotheken vergeleken: skia-safe, printpdf en krilla
Het Rust-ecosysteem biedt verschillende aanpakken voor PDF-generatie. Zo verhouden ze zich:
| Bibliotheek | Soort | Vectoruitvoer | Lettertypen uitdunnen | PDF/A | Getagde PDF | Lay-outmotor | Volwassenheid |
|---|---|---|---|---|---|---|---|
| skia-safe | Skia-bindings (C++ via FFI) | Ja | Ja (HarfBuzz) | Ja | Ja | Nee (canvas-API) | 20+ jaar (Skia) |
| printpdf | Puur Rust | Ja | Beperkt | Nee | Nee | Nee | Stabiel |
| genpdf | Puur Rust (op printpdf) | Ja | Beperkt | Nee | Nee | Basaal | Onbeheerd |
| krilla | Puur Rust (op pdf-writer) | Ja | Ja (OpenType) | Ja | Ja | Nee | Nieuwer |
| Headless Chrome | Browserruntime | Ja | Ja | Nee | Nee | Volledig HTML/CSS | Volwassen |
skia-safe geeft je een volledige 2D-grafiek-API met meer dan 20 jaar ontwikkeling bij Google eronder. Vectoruitvoer, lettertypen uitdunnen, getagde PDF's, PDF/A-ondersteuning. De afweging is een C++-afhankelijkheid — al maken kant-en-klare binaries dat in de praktijk pijnloos.
printpdf is puur Rust zonder C-afhankelijkheden. Het werkt goed voor eenvoudige documenten, maar vraagt dat je elk element met de hand positioneert. Geen lay-outmotor, beperkte typografie.
genpdf bouwt een lay-out-API op hoger niveau boven op printpdf. Het is puur Rust en makkelijker te gebruiken voor eenvoudige documenten, maar het project wordt al enkele jaren niet meer onderhouden.
krilla is een nieuwere, pure Rust-bibliotheek met een moderne API, uitstekende OpenType-ondersteuning, en functies als verlopen en mengmodi. Een sterke keuze wanneer C-afhankelijkheden vermijden voorop staat, al is het minder beproefd dan Skia.
Headless Chrome / Puppeteer geeft je volledige HTML- en CSS-rendering maar vraagt dat je een browserruntime meelevert. Zwaar, maar nuttig wanneer je complexe weblay-outs moet renderen.
De keuze hangt af van je randvoorwaarden. Heb je precieze grip op graphics en typografie nodig, wil je bewezen betrouwbaarheid, en heb je geen bezwaar tegen een C++-afhankelijkheid, dan is Skia de meest capabele optie. Wil je een oplossing in puur Rust, dan zijn krilla of printpdf het bekijken waard.
Bekende beperkingen van de PDF-achterkant van Skia
De PDF-achterkant van Skia is uitgebreid, maar heeft grenzen die het waard zijn te kennen:
Ondersteuning van mengmodi. De meeste mengmodi uit de PDF-standaard werken (Multiply, Screen, Overlay en meer), maar sommige Skia-eigen modi als SrcATop, DstATop, Xor en Plus hebben geen PDF-equivalent. Gebruikt je tekening die, dan kan de uitvoer afwijken van wat je op het scherm ziet.
Terugvallen op rasteren. Functies zonder native PDF-weergave — zoals perspectieftransformaties op tekst — worden gerasterd op de resolutie die in de metadata staat (raster_dpi, standaard 72). Voor drukkwaliteit zet je die op 300 of hoger, met grotere bestanden als afweging.
Bouwen vanaf de bron. Moet je Skia-functies aanpassen voorbij wat de kant-en-klare binaries bieden, dan kost bouwen vanaf de bron 20 tot 40 minuten. De functie binary-cache (standaard aan) voorkomt dat voor standaardconfiguraties.
Productiearchitectuur voor PDF-generatie in Rust
Voor productiesystemen die veel documenten genereren — facturen, rapporten, certificaten — scheidt een nette architectuur de inhoud van het renderen:
struct DocumentContent {
title: String,
sections: Vec<Section>,
footer: String,
}
struct Section {
heading: String,
body: String,
charts: Vec<ChartData>,
}
fn render_document(content: &DocumentContent) -> Vec<u8> {
let mut output = Vec::new();
let metadata = Metadata {
title: content.title.clone(),
creator: "My Document Engine".to_string(),
..Default::default()
};
let mut doc = pdf::new_document(&mut output, Some(&metadata));
for section in &content.sections {
let mut page = doc.begin_page((595, 842), None); // A4
render_section(page.canvas(), section);
doc = page.end_page();
}
doc.close();
output
}
Die scheiding betekent dat je bedrijfslogica de PDF-API nooit rechtstreeks aanraakt. Inhoud gaat erin, bytes komen eruit. Je kunt het renderen afzonderlijk testen, de PDF-achterkant vervangen zonder de bedrijfslogica te wijzigen, en documentgeneratie over threads parallelliseren — want elk Document bezit zijn eigen uitvoerbuffer.
Aan de slag met PDF-generatie in Rust met Skia
Voeg skia-safe toe aan je Cargo.toml, schrijf een paar regels tekencode, en open de resulterende PDF. De leercurve is mild als je eerder met een 2D-grafiek-API hebt gewerkt — Canvas, Cairo, Core Graphics of zelfs HTML5 Canvas. De begrippen dragen rechtstreeks over.
De crate skia-safe staat op versie 0.93 met meer dan 2 miljoen downloads op crates.io. De cache met kant-en-klare binaries maakt je eerste build snel, en het typestate-patroon maakt je eerste PDF correct.
Voor toepassingen die betrouwbare, hoogwaardige PDF-uitvoer nodig hebben — facturen, rapporten, tickets, certificaten, of elk document waar lay-outprecisie telt — geeft Skia via Rust je dezelfde renderengine waarop miljarden apparaten vertrouwen, met de veiligheidsgaranties waar Rust om bekendstaat.
Precies deze aanpak gebruikten we om dxpdf te bouwen — onze opensource DOCX-naar-PDF-converter geschreven in Rust en aangedreven door Skia. Hij zet Word-documenten in ongeveer 115 ms om naar getrouwe PDF's, zonder dat Microsoft Office of LibreOffice nodig is.
Veelgestelde vragen
Wat is de beste Rust-bibliotheek om PDF's te genereren?
Dat hangt af van je eisen. skia-safe is de meest complete optie, met vectoruitvoer, lettertypen uitdunnen, getagde PDF's en PDF/A-ondersteuning via de Skia-engine van Google. Wil je puur Rust zonder C-afhankelijkheden, dan zijn krilla en printpdf sterke alternatieven. Voor HTML en CSS naar PDF renderen blijft headless Chrome de betrouwbaarste aanpak.
Produceert Skia vector- of gerasterde PDF's?
Skia produceert echte vector-PDF's. Tekst is selecteerbaar en doorzoekbaar, vormen blijven scherp bij elke zoom, en tekenbewerkingen worden vertaald naar native PDF-primitieven. Rasteren gebeurt alleen als terugval voor functies zonder PDF-eigen weergave, zoals perspectieftransformaties.
Kan ik met skia-safe toegankelijke en PDF/A-conforme documenten maken?
Ja. Skia ondersteunt structuurbomen en knooppunt-ID's om getagde, toegankelijke PDF's te maken die aan PDF/UA voldoen. Voor archiefconformiteit levert pdf_a: true in de struct Metadata uitvoer op die aan PDF/A-2b voldoet.
Hoe lang duurt het om skia-safe te bouwen?
Met de standaard ingeschakelde functie binary-cache duurt de eerste build onder de minuut, omdat er kant-en-klare Skia-binaries voor je platform worden opgehaald. Zonder die cache (Skia vanaf de C++-bron bouwen) moet je op 20 tot 40 minuten rekenen.
Is skia-safe dezelfde engine die Chrome voor print-naar-PDF gebruikt?
Ja. Gebruik je in Chrome "Opslaan als PDF" of "Afdrukken naar PDF", dan rendert de browser de pagina via de PDF-achterkant van Skia — dezelfde engine die skia-safe aan Rust beschikbaar stelt. Het is ook de renderengine die Android voor zijn interface gebruikt en die Flutter voor cross-platform apps gebruikt.
Hoe gaat skia-safe om met lettertypen in PDF's?
Skia sluit lettertypen rechtstreeks in de PDF in. Met HarfBuzz-integratie (aan te zetten via de functie textlayout) dunt het lettertypen uit — ongebruikte tekens worden verwijderd om bestanden compact te houden. TrueType-lettertypen gebruiken glyph-ID-codering voor nauwkeurige weergave.

