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 tienen contrapartidas 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 son viables 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 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 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 seguridad de tipos 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 54 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é ofrece

Validado contra ISO 29500 (Office Open XML): 75 entradas implementadas por completo, 12 parciales y 11 aún no admitidas; 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
  • Fórmulas: matemáticas OMML en línea (m:oMath): runs matemáticos, superíndices y fracciones, compuestos con la fuente matemática predeterminada de Word; una fórmula puede llevar un hipervínculo y aparecer en un título del esquema
  • 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

Cuatro 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) => { /* inspeccionar el párrafo */ }
        model::Block::Table(t) => { /* inspeccionar la tabla */ }
        model::Block::SectionBreak(props) => { /* inspeccionar las propiedades de sección */ }
    }
}

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 de entrada, bytes de salida
pdf_bytes = dxpdf.convert(open("input.docx", "rb").read())

# De archivo a archivo
dxpdf.convert_file("input.docx", "output.pdf")

Desde la 0.8.1 ambas llamadas liberan el GIL mientras trabaja el núcleo de Rust, de modo que un pool de hilos que convierte varios documentos a la vez los procesa realmente en paralelo en lugar de serializarlos en el intérprete.

Paquete de Go

Instálalo con go get y úsalo como cualquier otro paquete:

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

// Bytes de entrada, bytes de salida
pdfBytes, err := dxpdf.Convert(docxBytes)

// De archivo a archivo
err := dxpdf.ConvertFile("input.docx", "output.pdf")

// Cambiar la resolución de las imágenes incrustadas (220 DPI por defecto)
pdfBytes, err := dxpdf.ConvertWithOptions(docxBytes, 300)

El paquete de Go es una capa fina de cgo sobre el mismo núcleo de Rust que usan la CLI y el paquete de Python, así que necesita CGO_ENABLED=1 y un compilador de C. La librería precompilada para cada plataforma admitida está versionada en el repositorio, por lo que no hay un paso aparte de descarga o compilación. Funciona en linux/amd64, linux/arm64, darwin/amd64, darwin/arm64 y, desde la 0.8.1, windows/amd64.

Windows es la única plataforma que se despliega de otra forma: las otras cuatro enlazan un archivo estático que queda absorbido en tu binario, mientras que Windows carga dxpdf.dll de forma dinámica, así que el ejecutable necesita esa DLL a su lado o en el PATH en tiempo de ejecución; cópiala desde la caché de módulos a tu carpeta de release. Además, el módulo no tiene etiquetas propias, así que go get .../go@v0.8.1 no resuelve: sí lo hacen un go get normal, @main o un SHA de commit para fijarlo.

Rendimiento

Medido en un Apple M3 Max con hyperfine (30 ejecuciones, 5 de calentamiento) en la versión 0.5.1, contra fixtures versionadas en el repositorio. Los tiempos están redondeados a 5 ms y la dispersión entre ejecuciones en una máquina con carga normal ronda los ±10 ms, así que las diferencias menores no son significativas:

FixturePáginasEntradaTiempo de conversiónPico de RSS
Documento de negocio de 3 páginas334 KB170 ms54 MB
Documento de 7 páginas710 KB170 ms51 MB
Documento de 9 páginas con imágenes91,3 MB55 ms40 MB
Informe de 171 páginas17114 MB420 ms145 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.

Backends en Python y Go

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

Los servicios en Go obtienen el mismo núcleo a través de cgo: un handler llama a dxpdf.Convert directamente en lugar de invocar un binario o consultar un sidecar, de modo que la conversión se queda dentro de la propia goroutine de la petición y de su manejo de errores.

Notas de la última versión

La versión más reciente es la 0.8.1. Desde la 0.5.0 han salido cuatro versiones que, entre ellas, añadieron un binding de lenguaje y un tipo de contenido nuevo:

  • 0.6.0: bindings de Go sobre una nueva ABI de C, además de correcciones en tablas: el borde exterior de una tabla con espaciado entre celdas, el desbordamiento de vMerge que pasa a la última fila de la combinación, los bordes verticales inside/outside que se reflejan según la paridad de la página, y los runs ocultos con w:vanish, que ahora se eliminan de la maquetación en lugar de pintarse.
  • 0.7.0: el renderizado de fórmulas OMML, junto con correcciones del solape de columnas en un salto de sección continuo, del ajuste de tokens según la regla LB25 de UAX #14 y de la tabla PUA de la fuente Symbol. Las fórmulas admiten hipervínculos y aparecen en los títulos del esquema.
  • 0.8.0: refinamiento de la vía matemática: geometría de fracciones coherente, superíndices sobre una fracción, números de nota al pie que toman la fuente de la fórmula que los abre, y el guion tratado correctamente con dígitos no ASCII. También lee las medidas universales de §22.9.2.15 y las grafías de porcentaje de §22.9.2.9.
  • 0.8.1: windows/amd64 se suma a los bindings de Go, y los bindings de Python liberan el GIL mientras dura una conversión.

El artículo largo sobre la 0.5.0, dxpdf 0.5.0: enseñar a un conversor de DOCX a leer el resto del mundo, sigue cubriendo el trabajo de internacionalización en el que se apoya todo esto: saltos de línea UAX #14, texto bidireccional UAX #9 y números y fechas de CLDR que siguen el idioma del propio documento.

No. dxpdf es un conversor independiente que lee los archivos DOCX directamente y renderiza los PDF con el motor gráfico Skia de Google. No depende de ninguna suite ofimática.
dxpdf está escrito en Rust y disponible como herramienta CLI (con cargo install), librería de Rust (en crates.io), paquete de Python (en PyPI) y bindings de Go (con go get github.com/nerdy-pro/dxpdf/go). Los cuatro funcionan en macOS, Linux y Windows; los bindings de Go cubren linux/amd64, linux/arm64, darwin/amd64, darwin/arm64 y, desde la 0.8.1, windows/amd64.
dxpdf usa un pipeline de medir y después colocar inspirado en Flutter y diseñado para lograr fidelidad a nivel de píxel, validado contra ISO 29500 con 75 entradas implementadas por completo, 12 parciales y 11 aún no admitidas. Los tests de regresión visual comparan la salida con referencias generadas por Word.
En un Apple M3 Max, las fixtures versionadas se convierten en 55-170 ms y un documento de 171 páginas y 14 MB en unos 420 ms. Importa más cómo se resuelven las fuentes que el tamaño del documento: las fuentes incrustadas o ya presentes en el sistema se resuelven en unos 4 ms, mientras que recurrir al índice de metadatos del sistema cuesta entre 120 y 185 ms una sola vez. Es lo bastante rápido como para ejecutarse dentro de un manejador de peticiones web.
Sí. Instálalo con pip install dxpdf. El paquete de Python envuelve el núcleo de Rust mediante PyO3 y ofrece rendimiento nativo. Usa dxpdf.convert() para bytes de entrada y salida, o dxpdf.convert_file() para conversión de archivo a archivo.
Sí, desde la 0.6.0. Ejecuta go get github.com/nerdy-pro/dxpdf/go y llama a dxpdf.Convert para bytes de entrada y salida o a dxpdf.ConvertFile para ir de archivo a archivo; ConvertWithOptions y ConvertFileWithOptions aceptan un DPI de imagen. Es una capa de cgo sobre el mismo núcleo de Rust, así que necesita CGO_ENABLED=1 y un compilador de C. Admite linux y macOS en amd64 y arm64 y, desde la 0.8.1, también windows/amd64: en Windows el binding carga dxpdf.dll de forma dinámica, así que distribuye esa DLL junto a tu ejecutable. El módulo de Go no tiene etiquetas propias, así que fíjalo con un SHA de commit en lugar de una etiqueta de versión.
Aún no admitidas: la reordenación de escrituras índicas, la partición automática de palabras, el control de cambios y los comentarios, SmartArt y gráficos, los bordes de página y la cuadrícula del documento, las imágenes WMF y SVG, los efectos de texto sombra, contorno, relieve y grabado, el sombreado de celdas por patrón, las tabulaciones y las etiquetas de numeración reflejadas bajo w:bidi y los formatos de numeración por conteo como chineseCounting. Parcialmente admitidas: el tachado y las versalitas se parsean pero no se renderizan, la mayoría de estilos de borde se aproximan con una línea sólida, el ajuste de imagen tight y through usa el rectángulo contenedor en lugar de un polígono, de EMF solo se decodifica un único mapa de bits incrustado, los saltos de sección even, odd y nextColumn se tratan como nextPage, los anchos de celda en porcentaje y automáticos recurren a la cuadrícula de la tabla, y la sustitución de fuente glifo a glifo toma el carácter que falta de cualquier fuente del sistema que lo cubra, por lo que la salida depende del sistema y todavía no se pasa la pista w:lang, lo que puede dar al texto han las formas de glifo del idioma equivocado.