Generar PDF de forma programática es una de esas tareas que suenan simples hasta que lo intentas de verdad. La mayoría de las librerías o bien te dan un flujo de bytes de bajo nivel con el que pelearte, o bien un motor de plantillas de alto nivel que se desmorona en cuanto necesitas control preciso sobre la maquetación, la tipografía o los gráficos. Hay un punto intermedio, y es el mismo motor gráfico que renderiza cada página web que imprimes a PDF en Chrome, cada pantalla de Android y cada fotograma de una app Flutter: Google Skia.
Esta guía muestra cómo usar Skia desde Rust a través del crate skia-safe para producir PDF vectoriales con control total sobre texto, formas, imágenes y maquetaciones de varias páginas.
Ideas clave
- Skia es la librería gráfica 2D de código abierto de Google, en desarrollo activo desde 2005, con más de 2 millones de descargas de sus bindings de Rust en crates.io.
- El crate skia-safe (v0.93) ofrece bindings de Rust seguros e idiomáticos con un patrón de typestate que detecta en tiempo de compilación la construcción malformada de un PDF.
- Skia produce PDF vectoriales de verdad, con texto seleccionable y no imágenes rasterizadas, el mismo enfoque que usa Chrome para su función de imprimir a PDF.
- Soporta conformidad de archivo PDF/A, PDF etiquetados y accesibles (PDF/UA), subconjuntos de fuentes vía HarfBuzz y documentos de varias páginas con tamaños mezclados.
- Se distribuyen binarios precompilados por defecto, así que la primera compilación tarda menos de un minuto en vez de 20-40 minutos desde el código fuente.
Qué es Skia y por qué usarlo para generar PDF en Rust
Skia es una librería gráfica 2D de código abierto mantenida por Google. Lleva en desarrollo activo desde 2005 y es uno de los motores de renderizado más probados en combate que existen. Cuando imprimes una página web a PDF en Google Chrome, es el backend PDF de Skia el que traduce la página renderizada a un archivo PDF. Cuando Android dibuja su interfaz, Skia se encarga del renderizado. Flutter usa Skia como motor de renderizado para apps multiplataforma.
Lo que hace atractivo a Skia para la generación programática de PDF en Rust:
- Salida vectorial de verdad. El texto sigue siendo seleccionable, las formas se mantienen nítidas a cualquier nivel de zoom y los tamaños de archivo se mantienen razonables, porque Skia traduce sus operaciones de dibujo a primitivas nativas de PDF en lugar de rasterizar píxeles.
- API 2D completa. Curvas de Bézier, degradados, transformaciones afines, regiones de recorte, modos de mezcla y controles tipográficos: todo lo que esperarías de una librería gráfica profesional, y todo se traduce a PDF.
- PDF etiquetados y accesibles. Skia soporta árboles de elementos de estructura de PDF e identificadores de nodo, lo que significa que puedes producir documentos que cumplan los estándares de accesibilidad PDF/UA.
- Conformidad de archivo PDF/A. Una sola marca de metadatos activa la salida PDF/A-2b para preservación documental a largo plazo, obligatoria en documentos legales, médicos y de administración pública.
- Subconjuntos de fuentes. Cuando incrustas fuentes, Skia elimina los glifos no usados vía HarfBuzz, manteniendo compactos los tamaños de archivo.
Configurar el crate skia-safe para generar PDF en Rust
El crate skia-safe ofrece bindings de Rust seguros e idiomáticos para Skia. Envuelve la librería de C++ mediante FFI y expone una API que resulta natural en Rust, con semántica de propiedad, garantías de tiempo de vida y un patrón de typestate que detecta categorías enteras de bugs en tiempo de compilación.
Añádelo a tu proyecto:
[dependencies]
skia-safe = "0.93"
La característica pdf está activada por defecto, así que no hacen falta flags adicionales. Por defecto, el crate descarga binarios de Skia precompilados para tu plataforma, lo que significa que la primera compilación tarda menos de un minuto en vez de los 20-40 minutos que llevaría compilar Skia desde el código fuente.
Cómo generar un archivo PDF en Rust con Skia
El concepto central es directo: crear un documento, empezar una página, dibujar en el lienzo, terminar la página, cerrar el documento. Este es un ejemplo mínimo que produce un PDF de una página:
use skia_safe::pdf;
fn main() {
let mut output = Vec::new();
// Create a document — US Letter size (612 x 792 points)
let mut document = pdf::new_document(&mut output, None)
.begin_page((612, 792), None);
let canvas = document.canvas();
// Draw a filled rectangle
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,
);
// Draw text
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();
}
Ejecuta esto, abre hello.pdf y verás un rectángulo azul con texto blanco, todo como gráficos vectoriales, completamente seleccionable y ampliable.

Seguridad en tiempo de compilación con el patrón typestate
Uno de los aspectos más elegantes de los bindings de Rust es el patrón typestate sobre el tipo Document. El documento transita entre dos estados: Open (listo para empezar una página) y OnPage (dibujando activamente).
// Document<state::Open> — can begin a page or close the document
let document = pdf::new_document(&mut output, None);
// Document<state::OnPage> — can access the canvas or end the page
let page = document.begin_page((612, 792), None);
let canvas = page.canvas();
// Back to Document<state::Open> — can begin another page or close
let document = page.end_page();
document.close();
Si intentas llamar a canvas() sobre un documento que no tiene ninguna página abierta, o intentas hacer close() de un documento mientras hay una página activa, el código no compila. No es una comprobación en tiempo de ejecución ni una aserción: es una garantía en tiempo de compilación impuesta por el sistema de tipos de Rust. No puedes producir un PDF malformado porque el compilador lo impide.
Compáralo con las librerías PDF de C++ o Python, donde estos errores producen salida corrupta, fallos silenciosos o caídas en tiempo de ejecución. En Rust con skia-safe, el compilador es tu corrector.
Añadir metadatos PDF para SEO y accesibilidad
Los metadatos de un PDF son lo que usan los buscadores, los sistemas de gestión documental y las herramientas de accesibilidad para entender tu documento. Skia lo expone a través de la estructura 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));
Para cumplimiento de archivo, activa PDF/A con una sola marca:
let metadata = Metadata {
pdf_a: true,
..Default::default()
};
Esto produce salida conforme a PDF/A-2b, apta para documentos legales, historiales médicos y cualquier contenido que tenga que poder leerse dentro de décadas.
Crear documentos PDF de varias páginas en Rust
Los documentos reales rara vez son de una sola página. Skia maneja los PDF de varias páginas con naturalidad: cada llamada a begin_page inicia una página nueva, y las páginas pueden tener tamaños distintos:
let mut output = Vec::new();
let document = pdf::new_document(&mut output, None);
// Page 1 — A4 portrait
let mut page = document.begin_page((595, 842), None);
draw_cover_page(page.canvas());
let document = page.end_page();
// Page 2 — A4 landscape
let mut page = document.begin_page((842, 595), None);
draw_data_table(page.canvas());
let document = page.end_page();
// Page 3 — back to portrait
let mut page = document.begin_page((595, 842), None);
draw_charts(page.canvas());
let document = page.end_page();
document.close();
Orientaciones y tamaños de página mezclados dentro de un mismo documento, algo con lo que muchas librerías PDF se pelean, funciona de serie.
Dibujar formas, texto y trazados en PDF con Skia
El Canvas de Skia es una superficie de dibujo 2D con todas las funcionalidades. Todo lo que dibujas en él se convierte en primitivas vectoriales de PDF.
Formas y trazados
let canvas = page.canvas();
let mut paint = Paint::default();
paint.set_anti_alias(true);
// Filled rectangle
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);
// Stroked circle
paint.set_style(skia_safe::PaintStyle::Stroke);
paint.set_stroke_width(2.0);
canvas.draw_circle((300.0, 100.0), 50.0, &paint);
// Custom bezier path
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);
Renderizado de texto con salida seleccionable
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);
// Smaller body text
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);
El texto del PDF resultante es seleccionable y buscable, no imágenes rasterizadas de caracteres.
Transformaciones afines
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();
Las transformaciones son componibles y se traducen directamente a la matriz de transformación de PDF, sin rasterización de por medio.
Incrustar imágenes en PDF generados con Rust
Incrustar imágenes en PDF es donde muchas librerías añaden tamaño de archivo innecesario. Skia lo gestiona con inteligencia:
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);
Cuando el soporte de codificación JPEG está configurado en los metadatos, Skia pasa los datos JPEG directamente al PDF sin recodificarlos, preservando la calidad y manteniendo mínimos los tamaños de archivo. Sin él, las imágenes se comprimen con deflate, lo que puede aumentar bastante el tamaño en contenido fotográfico.
Puedes controlarlo desde los metadatos:
let metadata = Metadata {
encoding_quality: 85, // JPEG quality (101 = lossless/deflate)
..Default::default()
};
Comparación de librerías PDF en Rust: skia-safe, printpdf y krilla
El ecosistema de Rust ofrece varios enfoques para generar PDF. Así se comparan:
| Librería | Tipo | Salida vectorial | Subconjuntos de fuentes | PDF/A | PDF etiquetado | Motor de maquetación | Madurez |
|---|---|---|---|---|---|---|---|
| skia-safe | Bindings de Skia (FFI a C++) | Sí | Sí (HarfBuzz) | Sí | Sí | No (API de lienzo) | 20+ años (Skia) |
| printpdf | Rust puro | Sí | Limitados | No | No | No | Estable |
| genpdf | Rust puro (sobre printpdf) | Sí | Limitados | No | No | Básico | Sin mantenimiento |
| krilla | Rust puro (sobre pdf-writer) | Sí | Sí (OpenType) | Sí | Sí | No | Más reciente |
| Chrome headless | Runtime de navegador | Sí | Sí | No | No | HTML/CSS completo | Maduro |
skia-safe te da una API 2D completa respaldada por más de 20 años de desarrollo en Google. Salida vectorial, subconjuntos de fuentes, PDF etiquetados, soporte de PDF/A. La contrapartida es una dependencia de C++, aunque los binarios precompilados lo hacen indoloro en la práctica.
printpdf es Rust puro sin dependencias de C. Funciona bien para documentos simples pero exige posicionar a mano cada elemento. Sin motor de maquetación y con soporte tipográfico limitado.
genpdf construye una API de maquetación de más alto nivel sobre printpdf. Es Rust puro y más fácil de usar para documentos simples, pero el proyecto lleva varios años sin mantenimiento.
krilla es una librería más reciente en Rust puro con una API moderna, excelente soporte de OpenType y funcionalidades como degradados y modos de mezcla. Es una opción fuerte si evitar dependencias de C es una prioridad, aunque está menos probada en combate que Skia.
Chrome headless / Puppeteer te da renderizado completo de HTML/CSS pero exige distribuir un runtime de navegador. Pesado, pero útil cuando necesitas renderizar maquetaciones web complejas.
La elección depende de tus restricciones. Si necesitas control preciso sobre gráficos y tipografía, quieres fiabilidad probada y te resulta cómoda una dependencia de C++, Skia es la opción más capaz. Si necesitas una solución en Rust puro, merece la pena evaluar krilla o printpdf.
Limitaciones conocidas del backend PDF de Skia
El backend PDF de Skia es completo, pero tiene fronteras que conviene conocer:
Soporte de modos de mezcla. La mayoría de los modos de mezcla estándar de PDF funcionan (Multiply, Screen, Overlay, etc.), pero algunos modos específicos de Skia como SrcATop, DstATop, Xor y Plus no tienen equivalente en PDF. Si tu dibujo los usa, la salida puede diferir de lo que ves en pantalla.
Rasterización de emergencia. Las funcionalidades sin representación nativa en PDF —como las transformaciones de perspectiva sobre texto— se rasterizan a los DPI indicados en los metadatos (raster_dpi, 72 por defecto). Para calidad de impresión, súbelo a 300 o más, con la contrapartida de archivos más grandes.
Compilar desde el código fuente. Si necesitas personalizar funcionalidades de Skia más allá de lo que ofrecen los binarios precompilados, compilar desde el fuente lleva 20-40 minutos. La característica binary-cache (activada por defecto) evita esto en configuraciones estándar.
Arquitectura de producción para generar PDF en Rust
Para sistemas en producción que generan muchos documentos —facturas, informes, certificados— una arquitectura limpia separa el contenido del renderizado:
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
}
Esta separación hace que tu lógica de negocio nunca toque directamente la API de PDF. Entra contenido, salen bytes. Puedes testear el renderizado de forma independiente, cambiar el backend de PDF sin tocar la lógica de negocio y paralelizar la generación de documentos entre hilos, ya que cada Document es dueño de su búfer de salida.
Empezar con la generación de PDF con Skia en Rust
Añade skia-safe a tu Cargo.toml, escribe unas líneas de código de dibujo y abre el PDF resultante. La curva de aprendizaje es suave si has usado antes cualquier API gráfica 2D: Canvas, Cairo, Core Graphics o incluso el Canvas de HTML5. Los conceptos se transfieren directamente.
El crate skia-safe va por la versión 0.93 con más de 2 millones de descargas en crates.io. La caché de binarios precompilados hace que tu primera compilación sea rápida, y el patrón typestate hace que tu primer PDF sea correcto.
Para aplicaciones que necesitan una salida PDF fiable y de alta calidad —facturas, informes, entradas, certificados o cualquier documento donde la precisión de la maquetación importe—, Skia desde Rust te da el mismo motor de renderizado en el que confían miles de millones de dispositivos, con las garantías de seguridad por las que Rust es conocido.
Usamos exactamente este enfoque para construir dxpdf, nuestro conversor de DOCX a PDF de código abierto escrito en Rust y basado en Skia. Convierte documentos de Word en PDF de alta fidelidad en unos 115 ms sin necesitar Microsoft Office ni LibreOffice.
Preguntas frecuentes
¿Cuál es la mejor librería de Rust para generar PDF?
Depende de tus requisitos. skia-safe es la opción más completa: ofrece salida vectorial, subconjuntos de fuentes, PDF etiquetados y soporte de PDF/A a través del motor Skia de Google. Para Rust puro sin dependencias de C, krilla y printpdf son alternativas sólidas. Para renderizar HTML/CSS a PDF, Chrome headless sigue siendo el enfoque más fiable.
¿Skia produce PDF vectoriales o rasterizados?
Skia produce PDF vectoriales de verdad. El texto es seleccionable y buscable, las formas se mantienen nítidas a cualquier zoom y las operaciones de dibujo se traducen a primitivas nativas de PDF. La rasterización solo ocurre como salida de emergencia para funcionalidades sin representación nativa en PDF, como las transformaciones de perspectiva.
¿Puedo crear documentos accesibles y conformes a PDF/A con skia-safe?
Sí. Skia soporta árboles de elementos de estructura de PDF e identificadores de nodo para crear PDF etiquetados y accesibles que cumplan los estándares PDF/UA. Para cumplimiento de archivo, poner pdf_a: true en la estructura Metadata produce salida conforme a PDF/A-2b.
¿Cuánto tarda en compilar skia-safe?
Con la característica binary-cache activada por defecto, la primera compilación tarda menos de un minuto porque se descargan binarios de Skia precompilados para tu plataforma. Sin la caché (compilando Skia desde el código C++), cuenta con 20-40 minutos.
¿Es skia-safe el mismo motor que usa Chrome para imprimir a PDF?
Sí. Cuando usas «Guardar como PDF» o «Imprimir a PDF» en Chrome, el navegador renderiza la página a través del backend PDF de Skia, el mismo motor que skia-safe expone a Rust. Es también el motor de renderizado que usa Android para su interfaz y Flutter para apps multiplataforma.
¿Cómo gestiona skia-safe las fuentes en los PDF?
Skia incrusta las fuentes directamente en el PDF. Con la integración de HarfBuzz (activada mediante la característica textlayout), genera subconjuntos de fuentes eliminando los glifos no usados para mantener compactos los tamaños de archivo. Las fuentes TrueType usan codificación por ID de glifo para un renderizado preciso.

