Het probleem
Word-documenten naar PDF omzetten is een van de meest voorkomende taken in bedrijfssoftware. Facturen, contracten, rapporten, complianceformulieren — ze beginnen als .docx-bestand en moeten PDF worden om te delen, te archiveren of af te drukken.
De bestaande oplossingen brengen stuk voor stuk flinke nadelen mee:
- Microsoft Office / LibreOffice — vereist een volledige kantoorsuite op elke server. De headlessmodus van LibreOffice is traag, geheugenhongerig en levert tussen versies wisselende resultaten. Opschalen betekent meerdere instanties draaien die gigabytes aan RAM opslokken.
- Cloud-API's (Google Docs, Adobe, CloudConvert) — voegen vertraging toe, kosten geld per conversie, en sturen mogelijk gevoelige documenten naar servers van derden. Niet werkbaar voor gereguleerde sectoren of afgeschermde omgevingen.
- HTML-naar-PDF-tools (wkhtmltopdf, Puppeteer) — vereisen dat DOCX eerst HTML wordt, waarbij opmaakgetrouwheid verloren gaat. Tabellen, kop- en voetteksten en pagina-einden overleven die omweg zelden.
Geen van deze werkt goed wanneer je snelle, nauwkeurige, offline conversie op schaal nodig hebt — zeker niet in geautomatiseerde pijplijnen, CI/CD-systemen of ingebedde applicaties waar LibreOffice installeren geen optie is.
Hoe dxpdf dat oplost
dxpdf is een zelfstandige DOCX-naar-PDF-converter geschreven in Rust en aangedreven door Googles Skia-grafiekbibliotheek. Hij leest .docx-bestanden rechtstreeks, ontleedt de OOXML-structuur, en rendert pixelnauwkeurige PDF-uitvoer — alles in één binary zonder externe afhankelijkheden buiten Skia.
Een door Flutter geïnspireerd meten-dan-plaatsen-model zorgt dat tekstafbreking, tabelafmetingen en pagina-einden overeenkomen met wat Microsoft Word oplevert:
DOCX (ZIP) → Parse → Document Model → Resolve → Layout → Subset → Paint → PDF
Twips/Emu/HalfPoints ←──── Pt throughout ────→ Skia
Typeveilige afmetingen lopen door de hele pijplijn: OOXML-eenheden (Twips, Emu, HalfPoints) liggen in het ontlede model op i64 zodat ze verliesvrij heen en terug gaan, de lay-out rekent in typografische punten (Pt), en kale f32 verschijnt pas op de rendergrens met Skia — zodat eenheden door elkaar halen een compileerfout is en geen document dat subtiel verkeerd is.
Het resultaat is een converter die een document van 3 pagina's met tabellen en afbeeldingen in 170 ms naar PDF omzet met 55 MB geheugen, en een document van 171 pagina's en 14 MB in 420 ms — snel genoeg om binnen een requesthandler te draaien of duizenden documenten in batch te verwerken.
Wat het ondersteunt
Getoetst aan ISO 29500 (Office Open XML): 74 functies volledig geïmplementeerd, 11 gedeeltelijk, 12 nog niet ondersteund — de volledige matrix noemt elke regel met zijn status. In het kort:
- Tekstopmaak — vet, cursief, onderstreept, markeren, tekengrootte, lettertype, kleur, letterafstand, tekenschaling, superscript, subscript, arcering en randen op runs
- Alinea's — uitlijning (links, gecentreerd, rechts, uitvullen, verdelen), afstand, inspringing, tabstops (waaronder decimaal, balk en absolute positie), randen, arcering, bij volgende houden, regels bij elkaar houden, weduwe- en wezencontrole
- Tabellen — kolombreedtes, celmarges met cascade over 3 niveaus, samengevoegde cellen, rijhoogtes, randen, celarcering, tabelstijlen met voorwaardelijke opmaak, geneste en zwevende tabellen, rijen die over pagina's splitsen
- Afbeeldingen — inline (PNG, JPEG, GIF, BMP, WebP) en zwevend of verankerd met uitlijning, omloop, bijsnijden en positionering op percentage
- Vormen en tekstvakken — DrawingML- en VML-vormen, tekstinhoud van vormen met binnenmarges, verankering, automatisch passend maken, eigen geometrie
- Stijlen — alinea- en tekenstijlen met overerving via
basedOn, documentstandaarden, themalettertypes - Kop- en voetteksten — tekst, afbeeldingen, paginanummers (veldcodes PAGE/NUMPAGES), varianten voor de eerste pagina en voor even/oneven
- Lijsten — nummering op meerdere niveaus: opsommingstekens, decimaal, letters, Romeins, rangtelwoorden en voluit geschreven tekst, plus niet-Latijnse reeksen en afbeeldingsbullets
- Navigatie — aanklikbare linkannotaties, bladwijzers en kruisverwijzingen als benoemde bestemmingen, en een PDF-overzicht opgebouwd uit kopniveaus
- Secties — meerdere paginaformaten en marges, sectie-einden, lay-out met meerdere kolommen, staande en liggende oriëntatie
- Lay-out — automatische paginering, voet- en eindnoten, tekstafbreking, regelafstandmodi, tekstloop rond zwevende afbeeldingen
- Internationalisatie — regelafbreking volgens UAX #14 (waaronder Thai, Lao, Khmer, Birmees), bidirectionele tekst volgens UAX #9 met spiegeling, HarfBuzz-shaping voor verbindende schriften, en decimaalscheidingstekens, datumnotaties en voluit geschreven getallen gestuurd door
w:lang - Tekst en emoji — segmentatie die grafemen respecteert en emoji in volle kleur, inclusief ZWJ-, modifier-, keycap- en vlagreeksen
Drie manieren om het te gebruiken
Opdrachtregelprogramma
Installeren en draaien met één commando:
cargo install dxpdf
dxpdf input.docx -o output.pdf
Rust-bibliotheek
Eén functieaanroep — bytes erin, bytes eruit:
let docx_bytes = std::fs::read("document.docx")?;
let pdf_bytes = dxpdf::convert(&docx_bytes)?;
std::fs::write("output.pdf", &pdf_bytes)?;
Wil je meer controle, inspecteer dan het ontlede documentmodel vóór het renderen:
use dxpdf::{docx, model, render};
let document = docx::parse(&std::fs::read("document.docx")?)?;
for block in &document.body {
match block {
model::Block::Paragraph(p) => { /* inspect paragraph */ }
model::Block::Table(t) => { /* inspect table */ }
model::Block::SectionBreak(props) => { /* inspect section properties */ }
}
}
let pdf_bytes = render::render(document, &dxpdf::RenderOptions::default())?;
Python-pakket
Installeer vanaf PyPI en gebruik het in elke Python-applicatie:
pip install dxpdf
import dxpdf
# Bytes erin, bytes eruit
pdf_bytes = dxpdf.convert(open("input.docx", "rb").read())
# Van bestand naar bestand
dxpdf.convert_file("input.docx", "output.pdf")
Prestaties
Gemeten op een Apple M3 Max met hyperfine (30 runs, 5 opwarmronden) bij v0.5.0, tegen fixtures die in de repository staan:
| Fixture | Pagina's | Invoer | Conversietijd | Piek-RSS |
|---|---|---|---|---|
| Zakelijk document van 3 pagina's | 3 | 34 KB | 170 ms | 55 MB |
| Document van 7 pagina's | 7 | 10 KB | 170 ms | 52 MB |
| Beeldrijk document van 9 pagina's | 9 | 1,3 MB | 55 ms | 42 MB |
| Rapport van 171 pagina's | 171 | 14 MB | 420 ms | 159 MB |
Lettertypen oplossen bepaalt wat een conversie kost, niet de documentgrootte. De fixture van 9 pagina's draagt veertig keer zoveel invoer als die van 3 pagina's en converteert in een derde van de tijd, omdat de lettertypen ingesloten zijn of al op de host staan — dat pad kost ongeveer 4 ms, terwijl terugvallen op de metadata-index van de host 120–185 ms kost, eenmalig. Bij batchwerk is de nuttige vraag dus niet hoe groot de documenten zijn, maar of ze lettertypen noemen die de host heeft.
De juistheid van de conversie ligt vast in tests op basis van fixtures, waaronder visuele regressietests die gerenderde PDF's vergelijken met referentiedocumenten uit Word.
Toepassingen in de praktijk
Geautomatiseerde documentpijplijnen
CI/CD-systemen of batchverwerkers die contracten, facturen of rapporten uit .docx-sjablonen genereren. dxpdf draait als één binary — geen LibreOffice-installatie, geen Docker-image met een volledige desktopomgeving, geen API-kosten per document.
Gereguleerde omgevingen
Toepassingen in de zorg, de advocatuur en de financiële sector waar documenten het netwerk niet mogen verlaten. dxpdf draait volledig offline zonder aanroepen naar externe diensten, wat het geschikt maakt voor afgeschermde en lokale uitrol.
Ingebed en aan de rand van het netwerk
IoT-apparaten, kiosken of lichte containers waar een kantoorsuite van 500 MB installeren niet praktisch is. Het geheugengebruik van enkele tientallen megabytes en de conversietijden onder de seconde maken dxpdf haalbaar in omgevingen met weinig middelen.
Python-webapplicaties
Backends in Django, Flask of FastAPI die geüploade DOCX-bestanden ter plekke moeten omzetten. De Python-bindings verpakken de Rust-kern via PyO3 en leveren native prestaties zonder subproces of externe dienst.
Releasenotities
De nieuwste release, 0.5.0, is een internationalisatierelease — regelafbreking volgens UAX #14, bidirectionele tekst volgens UAX #9, en getallen en datums die door CLDR worden gestuurd en de taal van het document volgen. We beschreven het in dxpdf 0.5.0: een DOCX-converter de rest van de wereld leren lezen, met de ontwerpkeuzes erachter, de gemeten cijfers, en waar de converter nog tekortschiet.