El problema
Convertir documentos de Word a PDF es una de las tareas más habituales del software de empresa. Facturas, contratos, informes, formularios de cumplimiento: nacen como archivos .docx y tienen que convertirse en PDF para compartirlos, archivarlos o imprimirlos.
Todas las soluciones existentes vienen con compromisos importantes:
- Microsoft Office / LibreOffice — obliga a instalar una suite ofimática completa en cada servidor. El modo headless de LibreOffice es lento, consume mucha memoria y produce resultados inconsistentes entre versiones. Escalar significa levantar varias instancias que se comen gigabytes de RAM.
- APIs en la nube (Google Docs, Adobe, CloudConvert) — añaden latencia, cuestan por conversión y envían documentos potencialmente sensibles a servidores de terceros. No es viable en sectores regulados ni en entornos aislados de la red.
- Herramientas de HTML a PDF (wkhtmltopdf, Puppeteer) — obligan a convertir primero el DOCX a HTML, perdiendo fidelidad de formato. Las tablas, los encabezados y pies de página y los saltos de página rara vez sobreviven al viaje de ida y vuelta.
Ninguna funciona bien cuando necesitas una conversión rápida, fiel y sin conexión a escala, especialmente en pipelines automatizados, sistemas de CI/CD o aplicaciones embebidas donde instalar LibreOffice no es una opción.
Cómo lo resuelve dxpdf
dxpdf es un conversor de DOCX a PDF independiente, escrito en Rust y basado en la librería gráfica Skia de Google. Lee los archivos .docx directamente, parsea la estructura OOXML y renderiza una salida PDF fiel al píxel, todo en un único binario sin más dependencias externas que Skia.
Un modelo medir y después colocar inspirado en Flutter garantiza que el ajuste del texto, el dimensionado de las tablas y los saltos de página coincidan con lo que produce Microsoft Word:
DOCX (ZIP) → Parse → Document Model → Resolve → Layout → Subset → Paint → PDF
Twips/Emu/HalfPoints ←──── Pt throughout ────→ Skia
Las dimensiones con tipo recorren todo el pipeline: las unidades de OOXML (Twips, Emu, HalfPoints) están respaldadas por i64 en el modelo parseado, así que van y vuelven sin pérdida; la maquetación trabaja en puntos tipográficos (Pt), y el f32 a secas solo aparece en la frontera de renderizado con Skia. Mezclar unidades es, por tanto, un error de compilación y no un documento sutilmente equivocado.
El resultado es un conversor que convierte un documento de 3 páginas con tablas e imágenes en 170 ms usando 55 MB de memoria, y uno de 171 páginas y 14 MB en 420 ms: lo bastante rápido como para ejecutarse dentro de un manejador de peticiones o procesar miles de documentos por lotes.
Qué soporta
Validado contra ISO 29500 (Office Open XML): 74 funciones implementadas por completo, 11 parciales y 12 aún no soportadas; la matriz completa enumera cada entrada con su estado. Lo más destacado:
- Formato de texto — negrita, cursiva, subrayado, resaltado, tamaño de fuente, familia tipográfica, color, espaciado y escalado de caracteres, superíndice, subíndice, sombreado y bordes de fragmentos
- Párrafos — alineación (izquierda, centro, derecha, justificada, distribuida), espaciado, sangría, tabulaciones (incluidas la decimal, la de barra y las de posición absoluta), bordes, sombreado, conservar con el siguiente, conservar líneas juntas y control de líneas viudas y huérfanas
- Tablas — anchos de columna, márgenes de celda con cascada de 3 niveles, celdas combinadas, alturas de fila, bordes, sombreado de celdas, estilos de tabla con formato condicional, tablas anidadas y flotantes, y división de filas entre páginas
- Imágenes — en línea (PNG, JPEG, GIF, BMP, WebP) y flotantes/ancladas con alineación, ajuste de texto, recorte y posicionamiento porcentual
- Formas y cuadros de texto — formas DrawingML y VML, cuerpos de texto con márgenes internos, anclaje, autoajuste y geometría personalizada
- Estilos — estilos de párrafo y de carácter con herencia
basedOn, valores por defecto del documento y fuentes del tema - Encabezados y pies de página — texto, imágenes, números de página (códigos de campo PAGE/NUMPAGES) y variantes de primera página y pares/impares
- Listas — numeración multinivel: viñetas, decimal, letras, romanos, ordinales y números escritos en palabras, además de secuencias no latinas y viñetas de imagen
- Navegación — anotaciones de enlace clicables, marcadores y referencias cruzadas como destinos con nombre, y un índice de PDF construido a partir de los niveles de encabezado
- Secciones — varios tamaños de página y márgenes, saltos de sección, diseños multicolumna y orientaciones vertical y horizontal
- Maquetación — paginación automática, notas al pie y notas al final, ajuste de línea, modos de interlineado y flujo del texto alrededor de imágenes flotantes
- Internacionalización — saltos de línea UAX #14 (incluidos tailandés, lao, jemer y birmano), texto bidireccional UAX #9 con reflejo especular, shaping con HarfBuzz para escrituras de unión cursiva, y separadores decimales, máscaras de fecha y números en palabras guiados por
w:lang - Texto y emoji — segmentación correcta por clústeres de grafemas y emoji a todo color, incluidas secuencias ZWJ, modificadores, teclas y banderas
Tres formas de usarlo
Herramienta de línea de comandos
Instálalo y ejecútalo con un solo comando:
cargo install dxpdf
dxpdf input.docx -o output.pdf
Librería de Rust
Una llamada a función: bytes de entrada, bytes de salida.
let docx_bytes = std::fs::read("document.docx")?;
let pdf_bytes = dxpdf::convert(&docx_bytes)?;
std::fs::write("output.pdf", &pdf_bytes)?;
Para más control, inspecciona el modelo de documento parseado antes de renderizar:
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())?;
Paquete de Python
Instálalo desde PyPI y úsalo en cualquier aplicación Python:
pip install dxpdf
import dxpdf
# Bytes in, bytes out
pdf_bytes = dxpdf.convert(open("input.docx", "rb").read())
# File to file
dxpdf.convert_file("input.docx", "output.pdf")
Rendimiento
Medido en un Apple M3 Max con hyperfine (30 ejecuciones, 5 de calentamiento) en la versión 0.5.0, contra fixtures versionadas en el repositorio:
| Fixture | Páginas | Entrada | Tiempo de conversión | Pico de RSS |
|---|---|---|---|---|
| Documento de negocio de 3 páginas | 3 | 34 KB | 170 ms | 55 MB |
| Documento de 7 páginas | 7 | 10 KB | 170 ms | 52 MB |
| Documento de 9 páginas con imágenes | 9 | 1,3 MB | 55 ms | 42 MB |
| Informe de 171 páginas | 171 | 14 MB | 420 ms | 159 MB |
Lo que decide el coste de una conversión es cómo se resuelven las fuentes, no el tamaño del documento. La fixture de 9 páginas lleva cuarenta veces más entrada que la de 3 y se convierte en un tercio del tiempo, porque sus fuentes están incrustadas o ya presentes en el sistema: esa ruta cuesta unos 4 ms, mientras que recurrir al índice de metadatos del sistema cuesta entre 120 y 185 ms, una sola vez. Para una carga por lotes la pregunta útil no es cómo de grandes son los documentos, sino si nombran fuentes que el sistema ya tiene.
La corrección de la conversión está fijada por tests basados en fixtures, incluidos tests de regresión visual que comparan los PDF renderizados con documentos de referencia generados por Word.
Casos de uso reales
Pipelines documentales automatizados
Sistemas de CI/CD o procesadores por lotes que generan contratos, facturas o informes a partir de plantillas .docx. dxpdf se ejecuta como un único binario: sin instalar LibreOffice, sin una imagen Docker con un entorno de escritorio completo y sin costes de API por documento.
Entornos regulados
Aplicaciones sanitarias, jurídicas y financieras donde los documentos no pueden salir de la red. dxpdf funciona completamente sin conexión y sin llamadas a servicios externos, lo que lo hace apto para despliegues aislados y on-premise.
Sistemas embebidos y edge computing
Dispositivos IoT, kioscos o contenedores ligeros donde instalar una suite ofimática de 500 MB no es práctico. El consumo de memoria de dxpdf, de unas pocas decenas de megabytes, y sus tiempos de conversión por debajo del segundo lo hacen viable en entornos con recursos limitados.
Aplicaciones web en Python
Backends de Django, Flask o FastAPI que necesitan convertir archivos DOCX subidos al vuelo. Los bindings de Python envuelven el núcleo de Rust mediante PyO3, ofreciendo rendimiento nativo sin subprocesos ni servicios externos.
Notas de la última versión
La versión más reciente, 0.5.0, es una versión centrada en la internacionalización: saltos de línea UAX #14, texto bidireccional UAX #9 y números y fechas de CLDR que siguen el idioma del propio documento. La contamos en dxpdf 0.5.0: enseñar a un conversor de DOCX a leer el resto del mundo, donde repasamos las decisiones de diseño, las cifras medidas y dónde el conversor todavía se queda corto.