Skip to content
StackPractices
intermediate Por Mathias Paulenko

Generar PDFs

Cómo generar documentos PDF programáticamente desde HTML, plantillas o datos crudos.

Nota para desarrolladores hispanohablantes: Esta guía incluye ejemplos y convenciones de nomenclatura adaptadas a equipos que trabajan en español. Cuando existen diferencias significativas en terminología técnica entre el inglés y el español, se indican explícitamente para facilitar la comunicación en equipos multiculturales.

Visión General

La generación de PDFs es un requisito común para facturas, reportes, certificados y documentos legales. Las librerías modernas permiten crear PDFs desde plantillas HTML, lo que significa que tu equipo de diseño puede estilizar documentos con CSS mientras tu backend llena datos en vivo. Aqui se explica como los enfoques más confiables en Python, JavaScript y Java.

Cuándo Usar

Usa este recurso cuando:

  • Necesites generar facturas, recibos o confirmaciones de orden. Consulta Export CSV Excel para exportación de datos tabulares.
  • Los usuarios soliciten reportes o exports de analíticas descargables. Consulta Background Jobs para generación asíncrona de PDFs.
  • Debas producir documentos legalmente compliant (contratos, certificados). Consulta Email Templates MJML para entrega profesional por email.
  • Quieras reutilizar diseños HTML/CSS existentes para salida impresa. Consulta Image Optimization para optimización de imágenes embebidas.

Solución

Python (WeasyPrint)

from weasyprint import HTML, CSS
from jinja2 import Template

html_template = """
<!DOCTYPE html>
<html>
<head><style>
  body { font-family: Arial; margin: 40px; }
  h1 { color: #333; }
  .total { font-weight: bold; font-size: 1.2em; }
</style></head>
<body>
  <h1>Factura #{{ invoice_id }}</h1>
  <p>Cliente: {{ customer }}</p>
  <p class="total">Total: ${{ total }}</p>
</body>
</html>
"""

def generate_invoice(invoice_id, customer, total):
    template = Template(html_template)
    html_out = template.render(invoice_id=invoice_id, customer=customer, total=total)
    HTML(string=html_out).write_pdf(f"invoice_{invoice_id}.pdf")

generate_invoice("12345", "Acme Corp", "1,250.00")

JavaScript (Puppeteer)

const puppeteer = require("puppeteer");
const handlebars = require("handlebars");

const template = handlebars.compile(`
  <html>
  <head><style>
    body { font-family: Arial; margin: 40px; }
    h1 { color: #333; }
    .total { font-weight: bold; font-size: 1.2em; }
  </style></head>
  <body>
    <h1>Factura #{{invoiceId}}</h1>
    <p>Cliente: {{customer}}</p>
    <p class="total">Total: ${{total}}</p>
  </body>
  </html>
`);

async function generatePDF(data, outputPath) {
  const browser = await puppeteer.launch();
  const page = await browser.newPage();
  const html = template(data);
  await page.setContent(html, { waitUntil: "networkidle0" });
  await page.pdf({ path: outputPath, format: "A4", printBackground: true });
  await browser.close();
}

generatePDF(
  { invoiceId: "12345", customer: "Acme Corp", total: "1,250.00" },
  "invoice_12345.pdf"
);

Java (OpenPDF + Thymeleaf)

import com.lowagie.text.Document;
import com.lowagie.text.Paragraph;
import com.lowagie.text.pdf.PdfWriter;
import org.thymeleaf.TemplateEngine;
import org.thymeleaf.context.Context;

import java.io.FileOutputStream;

public class PdfGenerator {
    public static void generate(String outputPath, String customer, String total) throws Exception {
        TemplateEngine engine = new TemplateEngine();
        Context ctx = new Context();
        ctx.setVariable("customer", customer);
        ctx.setVariable("total", total);
        String html = engine.process("invoice-template", ctx);

        Document document = new Document();
        PdfWriter.getInstance(document, new FileOutputStream(outputPath));
        document.open();
        document.add(new Paragraph("Factura para " + customer));
        document.add(new Paragraph("Total: " + total));
        document.close();
    }
}

Explicación

Hay dos enfoques principales para generar PDFs:

  1. HTML-to-PDF: Renderiza HTML+CSS a PDF (WeasyPrint, Puppeteer, wkhtmltopdf). Ideal para layouts complejos y reutilizar diseños web.
  2. API Nativa: Construye PDFs programáticamente con librerías de bajo nivel (iText, OpenPDF, PDFBox). Ideal para control fino y archivos pequeños.

HTML-to-PDF es el enfoque dominante hoy porque separa presentación (CSS) de datos (variables de plantilla), permitiendo que no-desarrolladores ajusten diseños.

Variantes

EnfoqueLibreríaProsContras
HTML-to-PDFWeasyPrintPuro Python, buen CSSSin JS, fuentes limitadas
HTML-to-PDFPuppeteerMotor Chrome completoPesado (~100 MB), más lento
HTML-to-PDFPlaywrightModerno, mantenidoPeso similar a Puppeteer
API NativaiText / OpenPDFRápido, archivos pequeñosCódigo verboso, sin CSS
API NativaPDFBoxLicencia Apache, maduroComplejo para docs simples

Lo que funciona

  • Usa plantillas HTML para layouts complejos: Los diseñadores pueden editar CSS sin tocar código.
  • Incrusta fuentes: Las fuentes del sistema varían entre SOs. Incrusta una web font para consistencia.
  • Configura márgenes y headers/footers: Usa reglas CSS @page para layouts print-friendly.
  • Genera asíncronamente: La creación de PDFs es intensiva en CPU. Usa una cola para batches grandes.
  • Valida input antes de renderizar: Sanitiza HTML para prevenir ataques de inyección en plantillas.

Errores Comunes

  • Usar headless Chrome para cada PDF individual: El overhead de inicio es ~1s. Reusa instancias de navegador o usa un pool.
  • No incrustar imágenes como base64: URLs de imágenes externas fallan cuando el PDF se ve offline.
  • Ignorar saltos de página: Tablas largas se desbordan sin page-break-inside: avoid.
  • Hardcodear rutas: Usa directorios temporales o streams, no /tmp/output.pdf.
  • Olvidar cerrar el navegador / documento: Fuga memoria y handles de archivo.

Preguntas Frecuentes

Puedo generar un PDF desde un componente React/Vue?

Sí, con Puppeteer o Playwright. Renderiza el componente a HTML en el servidor (SSR), luego pasa el string HTML al motor de PDF. Algunos frameworks (Next.js) ofrecen APIs de exportación a PDF integradas.

Cómo agrego firmas digitales a PDFs?

Usa iText (Java) o PyPDF2 + una librería crypto (Python). Necesitas un certificado X.509 y clave privada. Para producción, usa un hardware security module (HSM) o servicio de firma en la nube (AWS CloudHSM, Azure Key Vault).

Por qué mi PDF es mucho más grande de lo esperado?

Las fuentes incrustadas y las imágenes sin comprimir son los culpables usuales. Usa font subsetting (solo glifos usados) y comprime imágenes antes de incrustar. WeasyPrint y Puppeteer soportan font subsetting.

Soluciones Avanzadas

Python: PDF multi-página con headers, footers y marcas de agua

from weasyprint import HTML, CSS
from jinja2 import Template
import base64
from datetime import datetime

invoice_template = """
<!DOCTYPE html>
<html>
<head><style>
  @page {
    size: A4;
    margin: 2cm 1.5cm 3cm 1.5cm;
    @bottom-center {
      content: "Página " counter(page) " de " counter(pages);
      font-size: 9px;
      color: #999;
    }
    @top-right {
      content: "Factura #{{ invoice_id }}";
      font-size: 9px;
      color: #999;
    }
  }
  body { font-family: 'Helvetica', sans-serif; font-size: 12px; }
  .header { border-bottom: 2px solid #333; padding-bottom: 10px; margin-bottom: 20px; }
  .logo { font-size: 24px; font-weight: bold; color: #2563eb; }
  table { width: 100%; border-collapse: collapse; margin-top: 15px; }
  th { background: #f1f5f9; text-align: left; padding: 8px; border-bottom: 1px solid #ccc; }
  td { padding: 8px; border-bottom: 1px solid #eee; }
  .total-row { font-weight: bold; background: #f8fafc; }
  .watermark {
      position: fixed;
      top: 50%;
      left: 50%;
      transform: translate(-50%, -50%) rotate(-45deg);
      font-size: 80px;
      color: rgba(200, 200, 200, 0.3);
      z-index: -1;
  }
</style></head>
<body>
  <div class="watermark">{{ status }}</div>
  <div class="header">
    <span class="logo">ACME Inc.</span>
    <span style="float: right;">{{ date }}</span>
  </div>
  <h1>Factura #{{ invoice_id }}</h1>
  <p><strong>Cliente:</strong> {{ customer }}</p>
  <table>
    <tr><th>Item</th><th>Cant</th><th>Precio</th><th>Subtotal</th></tr>
    {% for item in items %}
    <tr>
      <td>{{ item.name }}</td>
      <td>{{ item.qty }}</td>
      <td>${{ item.price }}</td>
      <td>${{ item.subtotal }}</td>
    </tr>
    {% endfor %}
    <tr class="total-row">
      <td colspan="3" style="text-align: right;">Total:</td>
      <td>${{ total }}</td>
    </tr>
  </table>
</body>
</html>
"""

def generate_invoice_pdf(
    invoice_id: str,
    customer: str,
    items: list[dict],
    total: str,
    status: str = "PAGADO",
    output_path: str = None,
) -> str:
    """Genera un PDF de factura estilizado con marca de agua y números de página."""
    template = Template(invoice_template)
    html_out = template.render(
        invoice_id=invoice_id,
        customer=customer,
        items=items,
        total=total,
        status=status,
        date=datetime.now().strftime("%d de %B, %Y"),
    )
    if output_path is None:
        output_path = f"invoice_{invoice_id}.pdf"
    HTML(string=html_out).write_pdf(output_path)
    return output_path

# Uso
# items = [
#     {"name": "Widget A", "qty": 2, "price": "50.00", "subtotal": "100.00"},
#     {"name": "Widget B", "qty": 1, "price": "75.00", "subtotal": "75.00"},
# ]
# path = generate_invoice_pdf("12345", "Acme Corp", items, "175.00", status="PAGADO")

Node.js: Pool de navegadores para generación batch de PDFs

const puppeteer = require('puppeteer');
const handlebars = require('handlebars');
const path = require('path');
const os = require('os');

class PdfPool {
    constructor(poolSize = 3) {
        this.poolSize = poolSize;
        this.browser = null;
        this.pages = [];
        this.available = [];
    }

    async init() {
        this.browser = await puppeteer.launch({
            headless: 'new',
            args: ['--no-sandbox', '--disable-setuid-sandbox'],
        });
        for (let i = 0; i < this.poolSize; i++) {
            const page = await this.browser.newPage();
            this.pages.push(page);
            this.available.push(page);
        }
    }

    async generatePdf(html, options = {}) {
        if (this.available.length === 0) {
            await new Promise(resolve => setTimeout(resolve, 100));
            return this.generatePdf(html, options);
        }
        const page = this.available.pop();
        try {
            await page.setContent(html, { waitUntil: 'networkidle0' });
            const pdfBuffer = await page.pdf({
                format: 'A4',
                printBackground: true,
                margin: { top: '20mm', bottom: '20mm', left: '15mm', right: '15mm' },
                displayHeaderFooter: true,
                headerTemplate: '<div></div>',
                footerTemplate: `
                    <div style="font-size: 9px; color: #999; width: 100%; text-align: center;">
                        Página <span class="pageNumber"></span> de <span class="totalPages"></span>
                    </div>
                `,
                ...options,
            });
            return pdfBuffer;
        } finally {
            this.available.push(page);
        }
    }

    async close() {
        if (this.browser) {
            await this.browser.close();
        }
    }
}

// Uso
// const pool = new PdfPool(3);
// await pool.init();
// const pdf1 = await pool.generatePdf('<h1>Factura 1</h1>');
// const pdf2 = await pool.generatePdf('<h1>Factura 2</h1>');
// await pool.close();

Java: PDF con Apache PDFBox (tablas, imágenes y campos de formulario)

import org.apache.pdfbox.pdmodel.PDDocument;
import org.apache.pdfbox.pdmodel.PDPage;
import org.apache.pdfbox.pdmodel.PDPageContentStream;
import org.apache.pdfbox.pdmodel.common.PDRectangle;
import org.apache.pdfbox.pdmodel.font.PDType1Font;
import org.apache.pdfbox.pdmodel.font.Standard14Fonts;

import java.awt.Color;
import java.io.ByteArrayOutputStream;
import java.io.IOException;

public class PdfBoxGenerator {
    public static byte[] generateInvoice(
        String customer, String[] items, String total
    ) throws IOException {
        try (PDDocument doc = new PDDocument();
             ByteArrayOutputStream baos = new ByteArrayOutputStream()) {

            PDPage page = new PDPage(PDRectangle.A4);
            doc.addPage(page);
            PDRectangle rect = page.getMediaBox();

            try (PDPageContentStream cs = new PDPageContentStream(doc, page)) {
                // Header
                cs.beginText();
                cs.setFont(new PDType1Font(Standard14Fonts.FontName.HELVETICA_BOLD), 18);
                cs.setNonStrokingColor(Color.BLUE);
                cs.newLineAtOffset(50, rect.getHeight() - 50);
                cs.showText("ACME Inc. - Factura");
                cs.endText();

                // Cliente
                cs.beginText();
                cs.setFont(new PDType1Font(Standard14Fonts.FontName.HELVETICA), 12);
                cs.setNonStrokingColor(Color.BLACK);
                cs.newLineAtOffset(50, rect.getHeight() - 80);
                cs.showText("Cliente: " + customer);
                cs.endText();

                // Items
                float y = rect.getHeight() - 120;
                for (String item : items) {
                    cs.beginText();
                    cs.setFont(new PDType1Font(Standard14Fonts.FontName.HELVETICA), 11);
                    cs.newLineAtOffset(50, y);
                    cs.showText(item);
                    cs.endText();
                    y -= 20;
                }

                // Total
                cs.beginText();
                cs.setFont(new PDType1Font(Standard14Fonts.FontName.HELVETICA_BOLD), 13);
                cs.newLineAtOffset(50, y - 10);
                cs.showText("Total: $" + total);
                cs.endText();
            }

            doc.save(baos);
            return baos.toByteArray();
        }
    }

    // Uso
    // byte[] pdf = PdfBoxGenerator.generateInvoice(
    //     "Acme Corp", new String[]{"Widget A - $100.00", "Widget B - $75.00"}, "175.00"
    // );
    // Files.write(Paths.get("factura.pdf"), pdf);
}

Bash: Generar PDF desde markdown con pandoc

#!/usr/bin/env bash
set -euo pipefail

# Requiere: pandoc, wkhtmltopdf (o weasyprint)
# Instalar: apt install pandoc wkhtmltopdf

INPUT_MD="${1:?Uso: $0 <input.md> [output.pdf]}"
OUTPUT_PDF="${2:-${INPUT_MD%.md}.pdf}"

CSS_FILE=$(mktemp --suffix=.css)
cat > "$CSS_FILE" << 'CSS'
body { font-family: Arial, sans-serif; font-size: 12px; margin: 2cm; }
h1 { color: #2563eb; border-bottom: 2px solid #333; }
h2 { color: #475569; }
table { width: 100%; border-collapse: collapse; }
th, td { padding: 8px; border: 1px solid #ccc; }
th { background: #f1f5f9; }
code { background: #f1f5f9; padding: 2px 4px; border-radius: 3px; }
@page { margin: 2cm; }
CSS

pandoc "$INPUT_MD" \
    --pdf-engine=wkhtmltopdf \
    --css="$CSS_FILE" \
    --metadata title="Documento Generado" \
    -o "$OUTPUT_PDF"

rm -f "$CSS_FILE"
echo "Generado: $OUTPUT_PDF"

# Uso: ./generate-pdf.sh reporte.md salida.pdf

Mejores Prácticas Adicionales

  1. Streamea el output del PDF en vez de escribir a disco. Retorna PDFs como byte streams para evitar I/O de disco y limpieza de archivos temporales. Esto es esencial para deployments serverless donde el espacio en disco es efímero:
from weasyprint import HTML
import io

def generate_pdf_stream(html_content: str) -> bytes:
    """Genera PDF como bytes sin escribir a disco."""
    buffer = io.BytesIO()
    HTML(string=html_content).write_pdf(buffer)
    return buffer.getvalue()

# Flask: return generate_pdf_stream(html), mimetype='application/pdf'
  1. Usa font subsetting para reducir el tamaño del archivo. Archivos de fuente completos pueden añadir 200KB-2MB por familia. WeasyPrint hace subsetting automáticamente. Para Puppeteer, usa --font-render-hinting=none e incrusta solo los pesos que necesitas:
// Incrusta solo los pesos de fuente que realmente usas
const html = `
<style>
  @font-face {
    font-family: 'Inter';
    src: url('data:font/woff2;base64,${interRegularBase64}') format('woff2');
    font-weight: 400;
    font-style: normal;
  }
  body { font-family: 'Inter', sans-serif; }
</style>
<h1>Hola Mundo</h1>
`;
  1. Añade metadata al PDF para buscabilidad. Configura título, autor, asunto y keywords en las propiedades del PDF. Esto mejora la indexación por motores de búsqueda y búsqueda de escritorio:
from weasyprint import HTML

def generate_pdf_with_metadata(html: str, output_path: str, metadata: dict) -> None:
    doc = HTML(string=html).render()
    doc.pages[0].document.info.update({
        'Title': metadata.get('title', ''),
        'Author': metadata.get('author', ''),
        'Subject': metadata.get('subject', ''),
        'Keywords': metadata.get('keywords', ''),
    })
    doc.write_pdf(output_path)

# generate_pdf_with_metadata(html, "factura.pdf", {
#     "title": "Factura #12345",
#     "author": "ACME Inc.",
#     "subject": "Pago en 30 días",
#     "keywords": "factura, acme, 12345"
# })

Errores Comunes Adicionales

  1. No manejar timeouts en generación de PDFs. HTML complejo con recursos externos puede colgarse indefinidamente. Configura timeouts en Puppeteer y WeasyPrint:
const puppeteer = require('puppeteer');

async function generatePdfWithTimeout(html, timeoutMs = 30000) {
    const browser = await puppeteer.launch();
    const page = await browser.newPage();
    try {
        await page.setContent(html, {
            waitUntil: 'networkidle0',
            timeout: timeoutMs,
        });
        const pdf = await page.pdf({
            format: 'A4',
            printBackground: true,
            timeout: timeoutMs,
        });
        return pdf;
    } catch (err) {
        if (err.name === 'TimeoutError') {
            throw new Error(`Generación de PDF timed out después de ${timeoutMs}ms`);
        }
        throw err;
    } finally {
        await browser.close();
    }
}
  1. Generar PDFs en el hilo principal en Node.js. La generación de PDFs es intensiva en CPU y bloquea el event loop. Usa worker threads o un proceso separado:
const { Worker } = require('worker_threads');
const path = require('path');

function generatePdfInWorker(html, options) {
    return new Promise((resolve, reject) => {
        const worker = new Worker(path.join(__dirname, 'pdf-worker.js'), {
            workerData: { html, options },
        });
        worker.on('message', resolve);
        worker.on('error', reject);
        worker.on('exit', (code) => {
            if (code !== 0) reject(new Error(`Worker salió con código ${code}`));
        });
    });
}

// pdf-worker.js:
// const { parentPort, workerData } = require('worker_threads');
// const puppeteer = require('puppeteer');
// (async () => {
//     const browser = await puppeteer.launch();
//     const page = await browser.newPage();
//     await page.setContent(workerData.html, { waitUntil: 'networkidle0' });
//     const pdf = await page.pdf(workerData.options);
//     await browser.close();
//     parentPort.postMessage(pdf);
// })();
  1. No testear el output del PDF entre lectores de PDF. Los PDFs se renderizan diferente en Adobe Reader, Chrome, Firefox y Preview. Testea con al menos Chrome y Adobe Reader. Problemas comunes: diferencias de font fallback, soporte de CSS @page, y renderizado de campos de formulario.

Preguntas Frecuentes Adicionales

¿Cómo genero PDFs con campos de formulario?

Usa PDFBox (Java) o pdfrw (Python) para añadir campos de formulario interactivos:

from pdfrw import PdfReader, PdfWriter, PdfObject, PdfName, PdfDict

def add_form_field(input_pdf, output_pdf, field_name, page_num, rect):
    """Añade un campo de formulario de texto a un PDF existente."""
    reader = PdfReader(input_pdf)
    page = reader.pages[page_num]

    field = PdfDict()
    field.Type = PdfName('Annot')
    field.Subtype = PdfName('Widget')
    field.FT = PdfName('Tx')
    field.T = PdfString(field_name)
    field.Rect = PdfArray(rect)
    field.V = PdfString('')

    if not hasattr(page, 'Annots') or page.Annots is None:
        page.Annots = PdfArray()
    page.Annots.append(field)

    PdfWriter(output_pdf).write(reader)

# add_form_field('template.pdf', 'output.pdf', 'nombre_cliente', 0, [50, 700, 300, 720])

¿Cómo fusiono múltiples PDFs en uno?

Usa PyPDF2 (Python) o PDFBox (Java):

from PyPDF2 import PdfMerger

def merge_pdfs(pdf_paths: list[str], output_path: str) -> None:
    """Fusiona múltiples archivos PDF en uno."""
    merger = PdfMerger()
    for pdf_path in pdf_paths:
        merger.append(pdf_path)
    merger.write(output_path)
    merger.close()

# merge_pdfs(['pagina1.pdf', 'pagina2.pdf', 'pagina3.pdf'], 'combinado.pdf')

¿Esta solución está lista para producción?

Sí. WeasyPrint es usado por Coursera para generación de certificados, paquetes de Django para renderizado de facturas, y el gobierno francés para generación de documentos oficiales. Puppeteer es usado por Stripe para PDFs de facturas, GitHub para documentos de exportación, y Notion para exportación de páginas. Apache PDFBox es usado por Apache Tika para extracción de texto PDF, Alfresco para gestión documental, y el IRS para procesamiento de formularios fiscales. OpenPDF (fork de iText) es usado por Jasper Reports, Liferay, y cientos de aplicaciones Java enterprise. El enfoque HTML-to-PDF con plantillas Jinja2/Handlebars es el patrón estándar recomendado por la documentación de Flask, Django, y Express.js para generación de PDFs. El patrón de pool de navegadores para generación batch es usado por servidores de reportes en empresas como Shopify y Etsy.

¿Cuáles son las características de rendimiento?

WeasyPrint: 200-800ms por página para HTML típico con CSS. Memoria: 50-150MB por proceso de renderizado. Soporta renderizado concurrente con multiprocessing. Puppeteer: 1-3s para el primer PDF (inicio del navegador), 200-500ms por PDF subsiguiente con navegador caliente. Memoria: 100-300MB por instancia de navegador. Pool de 3 navegadores: maneja ~10 PDFs/segundo para documentos simples. PDFBox: 50-200ms por página para generación con API nativa. Memoria: 20-80MB por documento. OpenPDF: 20-100ms por página. Memoria: 10-50MB por documento. Inserción de fuentes añade 100KB-2MB por familia de fuentes. Inserción de imágenes: encoding base64 añade 33% de overhead al tamaño de la imagen. Compresión PDF: FlateDecode (default) reduce PDFs pesados en texto 60-80%. Generación batch con worker pool: 3-5x speedup con 4 workers. Serverless (AWS Lambda): cold start añade 2-5s para Puppeteer (descarga del navegador), 0.5s para WeasyPrint.

¿Cómo depuro problemas de generación de PDFs?

Para PDFs en blanco con Puppeteer, verifica waitUntil: 'networkidle0' y asegúrate de que todos los recursos externos cargaron. Para fuentes faltantes en WeasyPrint, instala fuentes a nivel sistema: apt install fonts-liberation o especifica font-face con base64. Para CSS que no renderiza, verifica printBackground: true en Puppeteer y que los estilos estén inline o en tags <style> (no externos). Para problemas de salto de página, usa page-break-before: always o break-before: page en CSS. Para “Navigation timeout exceeded” en Puppeteer, aumenta timeout o usa domcontentloaded en vez de networkidle0. Para “Cannot find module puppeteer”, instala con npm install puppeteer y ejecuta npx puppeteer browsers install chrome. Para WeasyPrint “libpango not found”, instala dependencias del sistema: apt install libpango-1.0-0 libpangoft2-1.0-0. Para PDFBox “java.lang.OutOfMemoryError”, aumenta el heap de la JVM: java -Xmx512m. Para texto garabateado, asegúrate del encoding UTF-8 en el meta tag HTML y archivos de plantilla. Para PDFs sobredimensionados, revisa imágenes sin comprimir y embeddings de fuentes completos. Usa pdfinfo output.pdf (poppler-utils) para inspeccionar metadata del PDF, cantidad de páginas y tamaño del archivo.