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, vreet geheugen en levert per versie 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, -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 grafische bibliotheek . 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 54 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): 75 regels volledig geïmplementeerd, 12 gedeeltelijk, 11 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
  • Formules — inline OMML-wiskunde (m:oMath): wiskundige runs, superscripts en breuken, gezet in het standaard wiskundelettertype van Word; een formule kan een hyperlink dragen en in een outlinetitel belanden
  • 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

Vier 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")

Sinds 0.8.1 geven beide aanroepen de GIL vrij zolang de Rust-kern werkt, zodat een threadpool die meerdere documenten tegelijk omzet ze ook echt parallel verwerkt in plaats van ze op de interpreter achter elkaar te zetten.

Go-pakket

Installeer met go get en gebruik het als elk ander pakket:

go get github.com/nerdy-pro/dxpdf/go
import "github.com/nerdy-pro/dxpdf/go"

// Bytes erin, bytes eruit
pdfBytes, err := dxpdf.Convert(docxBytes)

// Bestand naar bestand
err := dxpdf.ConvertFile("input.docx", "output.pdf")

// De resolutie van ingesloten afbeeldingen overschrijven (standaard 220 DPI)
pdfBytes, err := dxpdf.ConvertWithOptions(docxBytes, 300)

Het Go-pakket is een dunne cgo-laag over dezelfde Rust-kern die de CLI en het Python-pakket gebruiken, dus het vereist CGO_ENABLED=1 en een C-compiler. De voorgebouwde bibliotheek voor elk ondersteund platform staat in de repository, zodat er geen aparte download- of bouwstap is. Het draait op linux/amd64, linux/arm64, darwin/amd64, darwin/arm64 en — sinds 0.8.1 — windows/amd64.

Windows wordt als enige anders uitgerold: de andere vier linken een statisch archief dat volledig in je binary wordt opgenomen, terwijl Windows dxpdf.dll dynamisch laadt. Je gebouwde executable heeft die DLL dus ernaast of op PATH nodig tijdens runtime; kopieer hem uit de modulecache naar je releasemap. Daarnaast heeft de module geen eigen tags, dus go get .../go@v0.8.1 lost niet op; een gewone go get, @main of een commit-SHA om vast te pinnen werkt wel.

Prestaties

Gemeten op een Apple M3 Max met hyperfine (30 runs, 5 opwarmronden) bij v0.5.1, tegen fixtures die in de repository staan. Tijden zijn afgerond op 5 ms, en de spreiding tussen runs op een normaal belaste machine is ongeveer ±10 ms, dus kleinere verschillen zeggen niets:

FixturePagina'sInvoerConversietijdPiek-RSS
Zakelijk document van 3 pagina's334 KB170 ms54 MB
Document van 7 pagina's710 KB170 ms51 MB
Beeldrijk document van 9 pagina's91,3 MB55 ms40 MB
Rapport van 171 pagina's17114 MB420 ms145 MB

Het vinden van lettertypen bepaalt wat een conversie kost, niet de documentgrootte. De fixture van 9 pagina's bevat 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.

Embedded en edge

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.

Backends in Python en Go

Diensten 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.

Go-diensten krijgen dezelfde kern via cgo: een handler roept dxpdf.Convert direct aan in plaats van een binary te starten of een sidecar te bevragen, waardoor de conversie binnen de goroutine en de foutafhandeling van het request zelf blijft.

Releasenotities

De nieuwste release is 0.8.1. Sinds 0.5.0 zijn er vier releases uitgekomen, die samen een taalbinding en een nieuw soort inhoud toevoegden:

  • 0.6.0 — Go-bindings bovenop een nieuwe C-ABI, plus tabelcorrecties: de buitenrand van een tabel met celafstand, vMerge-overloop die naar de laatste rij van de samenvoeging gaat, verticale inside/outside-randen die met de pariteit van de pagina meespiegelen, en verborgen w:vanish-runs die uit de lay-out worden verwijderd in plaats van getekend.
  • 0.7.0 — het renderen van OMML-formules, samen met correcties voor overlappende kolommen bij een doorlopend sectie-einde, tokenafbreking volgens LB25 uit UAX #14, en de PUA-tabel van het Symbol-lettertype. Formules dragen hyperlinks en verschijnen in outlinetitels.
  • 0.8.0 — de wiskundeweg verfijnd: consistente breukgeometrie, superscripts boven een breuk, voetnootnummers die het lettertype van de openende formule overnemen, en correcte verwerking van het koppelteken bij niet-ASCII-cijfers. Daarnaast worden universele maten volgens §22.9.2.15 en procentnotaties volgens §22.9.2.9 gelezen.
  • 0.8.1 — windows/amd64 komt bij de Go-bindings, en de Python-bindings geven de GIL vrij voor de duur van een conversie.

Het uitgebreide verhaal over 0.5.0, dxpdf 0.5.0: een DOCX-converter de rest van de wereld leren lezen, beschrijft nog steeds het internationalisatiewerk waarop dit alles rust — 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.

Nee. dxpdf is een zelfstandige converter die DOCX-bestanden rechtstreeks leest en PDF's rendert met Googles grafische engine Skia. Hij heeft geen enkele afhankelijkheid van een kantoorsuite.
dxpdf is geschreven in Rust en beschikbaar als CLI-tool (via cargo install), als Rust-bibliotheek (via crates.io), als Python-pakket (via PyPI) en als Go-bindings (via go get github.com/nerdy-pro/dxpdf/go). Alle vier draaien op macOS, Linux en Windows; de Go-bindings dekken linux/amd64, linux/arm64, darwin/amd64, darwin/arm64 en, sinds 0.8.1, windows/amd64.
dxpdf gebruikt een door Flutter geïnspireerde pijplijn van meten-dan-plaatsen die is ontworpen op getrouwheid tot op de pixel, getoetst aan ISO 29500 met 75 regels volledig geïmplementeerd, 12 gedeeltelijk en 11 nog niet ondersteund. Visuele regressietests vergelijken de uitvoer met referenties uit Word.
Op een Apple M3 Max converteert dxpdf de meegeleverde fixtures in 55-170 ms en een document van 171 pagina's en 14 MB in ongeveer 420 ms. Het vinden van lettertypen weegt zwaarder dan de documentgrootte: lettertypen die ingesloten zijn of al op de host staan zijn in ongeveer 4 ms gevonden, terwijl terugvallen op de metadata-index van de host eenmalig 120-185 ms kost. Het is snel genoeg om binnen een webrequesthandler te draaien.
Ja. Installeer met pip install dxpdf. Het Python-pakket verpakt de Rust-kern via PyO3 en levert native prestaties. Gebruik dxpdf.convert() voor bytes erin en bytes eruit, of dxpdf.convert_file() voor conversie van bestand naar bestand.
Ja, sinds 0.6.0. Voer go get github.com/nerdy-pro/dxpdf/go uit en roep dxpdf.Convert aan voor bytes erin en bytes eruit, of dxpdf.ConvertFile voor bestand naar bestand; ConvertWithOptions en ConvertFileWithOptions nemen een afbeeldings-DPI. Het is een cgo-laag over dezelfde Rust-kern, dus het vereist CGO_ENABLED=1 en een C-compiler. Het ondersteunt linux en macOS op amd64 en arm64, en sinds 0.8.1 ook windows/amd64 — op Windows laadt de binding dxpdf.dll dynamisch, dus lever die DLL mee naast je executable. De Go-module heeft geen eigen tags, dus pin met een commit-SHA in plaats van een versietag.
Nog niet ondersteund: herordening van Indische schriften, automatische woordafbreking, bijgehouden wijzigingen en opmerkingen, SmartArt en grafieken, paginaranden en het documentraster, WMF- en SVG-afbeeldingen, de teksteffecten schaduw, contour, reliëf en gravure, patroonarcering van cellen, gespiegelde tabstops en nummeringslabels onder w:bidi, en de telsysteemnotaties zoals chineseCounting. Gedeeltelijk ondersteund: doorhaling en klein kapitaal worden wel gelezen maar niet gerenderd, de meeste randstijlen worden benaderd als doorlopende lijnen, strakke en doorlopende beeldomloop gebruiken het omhullende kader in plaats van een veelhoek, EMF-afbeeldingen decoderen slechts één ingesloten bitmap, sectie-einden even, odd en nextColumn worden als nextPage behandeld, celbreedtes in procenten en auto vallen terug op het tabelraster, en terugval per glyph haalt een ontbrekend teken uit elk host-lettertype dat het dekt, waardoor de uitvoer afhankelijk is van de host en er nog geen w:lang-hint wordt doorgegeven, wat Han-tekst de glyphvormen van de verkeerde taal kan geven.