Convertir imágenes a PDF en JavaScript (React)

2026-09-10 05:33:33 Allen Yang
AI Summarize:
ChatGPT
ChatGPT ✓
Claude ✓
Grok ✓
Perplexity ✓
Quick
Quick
Concise overview
Highlights
Key takeaways
Detailed
Structured explanation
Brief
One sentence summary
Summarize |

Multiple images assembled into a single multi-page PDF using Spire.PDF for JavaScript in React

Una carpeta de imágenes es incómoda de entregar a alguien. Un PDF es un solo archivo, se abre en todas partes, se imprime de forma predecible y —la parte que realmente importa a la gente— mantiene un orden fijo. Por eso las páginas escaneadas, los conjuntos de fotos, las fotos de recibos y los fotogramas de diseño exportados tan a menudo se ensamblan en un PDF antes de enviarse a cualquier parte.

Construir ese PDF en el navegador es un problema distinto al de renderizar un PDF a una imagen. No estás decodificando algo que ya existe; estás tomando decisiones que un formato de documento normalmente tomaría por ti: qué tan grande es la página, dónde se sitúa la imagen en ella, qué sucede cuando una imagen tiene una forma distinta a la de la página y en qué orden salen las páginas.

Spire.PDF for JavaScript expone esas decisiones mediante un lienzo de página. Añades una página, cargas una imagen, la dibujas en esa página con un tamaño que calculas y guardas. Todo se ejecuta en el lado del cliente a través de WebAssembly, por lo que las imágenes nunca se suben.


Por qué las imágenes terminan en PDFs

Los escenarios comparten una misma forma: varias imágenes que necesitan comportarse como un solo documento.

  • Documentos de varias páginas escaneados o fotografiados — un contrato fotografiado página por página, reensamblado en un solo archivo que se puede archivar o enviar por correo electrónico.
  • Conjuntos de fotos y portafolios — una imagen por página, en un orden elegido por alguien.
  • Recibos e informes de gastos — una docena de fotos del teléfono que contabilidad quiere como un solo archivo adjunto.
  • Exportaciones de diseño y diagramas — fotogramas exportados de una herramienta, reunidos en algo revisable.

En cada caso, el PDF no se trata realmente del formato PDF. Se trata de obtener un artefacto estable, de un solo archivo y ordenado a partir de un montón de imágenes.


Requisitos previos

Este tutorial asume un proyecto de React con Spire.PDF for JavaScript instalado y el módulo WASM inicializado. Para la configuración, consulta Integrar Spire.PDF for JavaScript en un proyecto de React.

Necesitarás:

  • Uno o más archivos de imagen cargados en el VFS
  • El módulo WASM accesible en window.wasmModule.spirepdf

Una imagen, una página

El flujo básico consta de cuatro pasos: crear un documento, añadir una página, cargar la imagen y dibujarla. La parte interesante es el dibujo: tienes que decidir qué tan grande debe ser la imagen en la página.

function App() {
  const convertImageToPDF = async () => {
    // Get the Spire.PDF WASM module
    const pdfModule = window.wasmModule?.spirepdf;

    // Check if the WASM module is ready
    if (!pdfModule) {
      alert('Spire.PDF is not ready yet');
      return;
    }

    // Load the image file into VFS
    const inputFileName = 'Scenery.png';
    await window.spire.FetchFileToVFS(inputFileName, "", `${process.env.PUBLIC_URL}/data/`);

    // Create a PdfDocument object
    let doc = new pdfModule.PdfDocument();

    // Add a page
    let page = doc.Pages.Add();

    // Load the image
    let image = pdfModule.PdfImage.FromFile(inputFileName);

    // Calculate the scale ratio so the image fits the page completely
    let widthFitRate = image.PhysicalDimension.Width / page.Canvas.ClientSize.Width;
    let heightFitRate = image.PhysicalDimension.Height / page.Canvas.ClientSize.Height;
    let fitRate = Math.max(widthFitRate, heightFitRate);

    // Calculate the scaled dimensions of the image
    let fitWidth = image.PhysicalDimension.Width / fitRate;
    let fitHeight = image.PhysicalDimension.Height / fitRate;

    // Center the image on the page
    let x = (page.Canvas.ClientSize.Width - fitWidth) / 2;
    let y = (page.Canvas.ClientSize.Height - fitHeight) / 2;

    // Draw the image onto the page
    page.Canvas.DrawImage({ image: image, x: x, y: y, width: fitWidth, height: fitHeight });

    const outputFileName = 'ImageToPDF.pdf';

    // Save as PDF format
    doc.SaveToFile({ fileName: outputFileName, fileFormat: pdfModule.FileFormat.PDF });
    doc.Close();

    // Read the converted file from VFS and trigger download
    const fileArray = window.dotnetRuntime.Module.FS.readFile(outputFileName);
    const blob = new Blob([fileArray], { type: 'application/pdf' });
    const url = URL.createObjectURL(blob);
    const a = document.createElement('a');
    a.href = url;
    a.download = outputFileName;
    a.click();
    URL.revokeObjectURL(url);
  };

  return (
    <div style={{ textAlign: 'center', height: '300px' }}>
      <h1>Convert Image To PDF</h1>
      <button onClick={convertImageToPDF}>
        Generate
      </button>
    </div>
  );
}

export default App;

Documento PDF generado después de cargar una imagen mediante PdfImage.FromFile y dibujarla con Canvas.DrawImage

PDF document generated after loading an image via PdfImage.FromFile and drawing it with Canvas.DrawImage

Extrae el cálculo de tamaño a una función auxiliar, porque es la única pieza de este código que reutilizarás en todas las demás recetas a continuación. La matemática: toma page.Canvas.ClientSize —el área dibujable de la página, en puntos— como tu presupuesto, compárala con la PhysicalDimension natural de la imagen y divide ambas dimensiones por una única proporción para que la relación de aspecto se conserve. Escóndelo en una función para que la parte propensa a errores viva exactamente en un solo lugar:

// Contain: scale until the whole image fits inside the page
function fitContain(imgW, imgH, pageW, pageH) {
  const rate = Math.max(imgW / pageW, imgH / pageH);
  const width = imgW / rate;
  const height = imgH / rate;
  return { width, height, x: (pageW - width) / 2, y: (pageH - height) / 2 };
}

Ahora la llamada de dibujo del ejemplo anterior se reduce a tres líneas, y la decisión de "contener o cubrir" sale de las matemáticas y pasa al nombre de una función:

let page = doc.Pages.Add();
let box = fitContain(
  image.PhysicalDimension.Width, image.PhysicalDimension.Height,
  page.Canvas.ClientSize.Width, page.Canvas.ClientSize.Height
);
page.Canvas.DrawImage({ image: image, x: box.x, y: box.y, width: box.width, height: box.height });

Math.max es la opción de "contener": escala por el eje más restrictivo para que toda la imagen permanezca visible. Si en cambio quieres llenar la página y recortar el desbordamiento, cambia a Math.min; la sección sobre ajuste de tamaño te da la contraparte fitCover y una variante con margen.


Muchas imágenes, un documento

Una imagen por página significa un Pages.Add() y un DrawImage por imagen. Recorre en bucle un arreglo de nombres de archivo y el orden del arreglo se convierte en el orden de las páginas, que es exactamente lo que quieres cuando el usuario acaba de terminar de arrastrar miniaturas para ponerlas en secuencia.

const combineImagesToPdf = async () => {
  const pdfModule = window.wasmModule?.spirepdf;
  if (!pdfModule) return;

  // The order of this array is the order of pages in the PDF
  const imageFiles = ['scan_01.png', 'scan_02.png', 'scan_03.png', 'scan_04.png'];

  for (const fileName of imageFiles) {
    await window.spire.FetchFileToVFS(fileName, "", `${process.env.PUBLIC_URL}/data/`);
  }

  let doc = new pdfModule.PdfDocument();

  for (const fileName of imageFiles) {
    let page = doc.Pages.Add();
    let image = pdfModule.PdfImage.FromFile(fileName);

    let box = fitContain(
      image.PhysicalDimension.Width, image.PhysicalDimension.Height,
      page.Canvas.ClientSize.Width, page.Canvas.ClientSize.Height
    );

    page.Canvas.DrawImage({ image: image, x: box.x, y: box.y, width: box.width, height: box.height });
  }

  const outputFileName = 'ScannedDocument.pdf';
  doc.SaveToFile({ fileName: outputFileName, fileFormat: pdfModule.FileFormat.PDF });
  doc.Close();

  const fileArray = window.dotnetRuntime.Module.FS.readFile(outputFileName);
  const blob = new Blob([fileArray], { type: 'application/pdf' });
  const url = URL.createObjectURL(blob);
  const a = document.createElement('a');
  a.href = url;
  a.download = outputFileName;
  a.click();
  URL.revokeObjectURL(url);
};

Dos notas prácticas. Como cada página se dimensiona de forma independiente, las imágenes de distintas dimensiones no son problema: una foto horizontal y un escaneo vertical pueden estar en el mismo documento sin ningún manejo especial. Y como todo el documento se construye en memoria antes de SaveToFile, la descarga ocurre una sola vez al final, sin importar cuántas imágenes se incluyeron.

También te puede interesar: Ensamblar imágenes es lo inverso de renderizar. Si ya tienes un PDF y quieres cada una de sus páginas como imagen, consulta Cómo convertir páginas de PDF a imágenes en JavaScript (React).


Ajuste de tamaño: adaptar la imagen a la página

Hay dos formas razonables de poner una imagen en una página, y cuál quieres depende de si es aceptable perder parte de la imagen.

Contener (Math.max) Cubrir (Math.min)
Qué hace Escala hasta que la imagen completa encaja Escala hasta que la página se llena
Imagen completa visible Sí No: el desbordamiento se recorta
Espacio vacío Posible, en un eje Ninguno
Adecuado para Escaneos, documentos, cualquier cosa que deba permanecer completa Fotos a sangre, portadas, diapositivas

El primer ejemplo usa contener: la función auxiliar fitContain. Cambiar a cubrir es el espejo de esa función: Math.min en lugar de Math.max, llenando la página y dejando que el lienzo recorte lo que se desborde, con desplazamientos de centrado que se vuelven negativos:

// Cover: fill the page, clipping whatever overflows
function fitCover(imgW, imgH, pageW, pageH) {
  const rate = Math.min(imgW / pageW, imgH / pageH);
  const width = imgW / rate;
  const height = imgH / rate;
  return {
    width, height,
    x: (pageW - width) / 2,   // negative when the image is wider than the page
    y: (pageH - height) / 2   // negative when it is taller
  };
}

Si quieres un margen visible en lugar de una salida de borde a borde, reduce el área utilizable en vez de la imagen: introduce el presupuesto ajustado por margen en la misma función auxiliar fitContain:

const margin = 36; // 36 points = 0.5 inch
let usableWidth = page.Canvas.ClientSize.Width - margin * 2;
let usableHeight = page.Canvas.ClientSize.Height - margin * 2;

let box = fitContain(
  image.PhysicalDimension.Width, image.PhysicalDimension.Height,
  usableWidth, usableHeight
);
page.Canvas.DrawImage({ image: image, x: box.x, y: box.y, width: box.width, height: box.height });

Algo que vale la pena saber sobre PhysicalDimension: refleja el tamaño físico de la imagen, que no siempre es su tamaño en píxeles. Una foto de 4000 × 3000 guardada con una etiqueta DPI diferente reportará números distintos de los que podrías esperar. Por eso el enfoque basado en proporciones anterior es más seguro que codificar las dimensiones en píxeles: funciona independientemente de cómo se etiquetó la imagen.


Cargar imágenes desde memoria

PdfImage.FromFile espera que la imagen ya esté en el VFS. No siempre es ahí donde residen tus imágenes: una respuesta de API, un blob de base de datos o una exportación de lienzo te dan bytes en memoria en su lugar. PdfImage.FromStream toma esos bytes directamente.

// Read image bytes from VFS and build a memory stream
let bytes = window.dotnetRuntime.Module.FS.readFile(inputFileName);
let stream = new pdfModule.Stream(bytes);

// Load the image from the memory stream
let image = pdfModule.PdfImage.FromStream(stream);

A partir de ahí, es igual que cualquier otra imagen: calcula el tamaño y dibújala:

let page = doc.Pages.Add();
let box = fitContain(
  image.PhysicalDimension.Width, image.PhysicalDimension.Height,
  page.Canvas.ClientSize.Width, page.Canvas.ClientSize.Height
);
page.Canvas.DrawImage({ image: image, x: box.x, y: box.y, width: box.width, height: box.height });

La misma forma de bytes funciona independientemente de dónde provenga. Si tus imágenes llegan como un ArrayBuffer de fetch, envuélvelo en un Uint8Array antes de construir el flujo:

const response = await fetch('/api/images/invoice-001');
const bytes = new Uint8Array(await response.arrayBuffer());
let stream = new pdfModule.Stream(bytes);
let image = pdfModule.PdfImage.FromStream(stream);

Este es el patrón al que recurrir cuando el PDF se ensambla con imágenes desencadenadas por el servidor, cargas de usuario mantenidas en el estado o cualquier cosa generada en tiempo de ejecución por un lienzo: no se requiere un viaje de ida y vuelta a través del VFS.

Artículo recomendado: Spire.PDF también puede dibujar en las páginas de un PDF que ya tengas abierto, no solo en los documentos nuevos anteriores. Para colocar imágenes en un documento existente, consulta Cómo añadir imágenes a un PDF en JavaScript (React).


Problemas comunes

La imagen sale estirada o aplastada. Esto casi siempre se debe a dos factores de escala diferentes. Calcula un único fitRate y divide tanto el ancho como el alto por él; nunca escales los ejes de forma independiente.

La imagen es diminuta en el medio de una gran página vacía. Es de esperar cuando la relación de aspecto de la imagen difiere mucho de la de la página. Una foto panorámica en una página vertical siempre dejará franjas arriba y abajo. O lo aceptas (correcto para documentos), cambias a cubrir, o usas la versión ajustada por margen para al menos mantener simétrico el espacio en blanco.

La imagen se corta en los bordes. Estás usando el comportamiento de cubrir, intencionalmente o no. Comprueba si fitRate usó Math.min; cambia a Math.max si la imagen completa debe ser visible.

Una foto de alta resolución produce un PDF enorme. La imagen se incrusta con su propia resolución. Si el tamaño del archivo importa, reduce la escala antes de dibujar: dibújala en un lienzo al tamaño objetivo, expórtala y usa esos bytes con PdfImage.FromStream.

No ocurre nada en el primer clic. El módulo WASM se carga de forma asíncrona. La guarda if (!pdfModule) return; existe por esa razón; en una aplicación real, habilita el botón según la preparación del módulo en lugar de mostrar una alerta.


Preguntas frecuentes

¿Puedo insertar imágenes en un PDF existente en lugar de crear uno nuevo?

Sí. Los ejemplos aquí crean un documento nuevo, pero puedes abrir un PDF existente y dibujar en sus páginas de la misma manera. Consulta Cómo añadir imágenes a un PDF en JavaScript (React) para ese flujo de trabajo.

¿Qué formatos de imagen puedo cargar?

Los formatos de mapa de bits comunes —PNG, JPEG, BMP y similares— son compatibles con PdfImage.FromFile y PdfImage.FromStream. Usa FromStream cuando el formato se desconoce en tiempo de compilación o los bytes provienen de una respuesta de red.

¿Puedo controlar el orden de las páginas?

Sí. Las páginas se crean en el orden en que llamas a Pages.Add(), por lo que ordenar tu arreglo de nombres de archivo ordena la salida. Ese es el mecanismo detrás de las interfaces de arrastrar para reordenar: reordena el arreglo, reconstruye el PDF.

¿Esto requiere un backend?

No. El documento se ensambla en el navegador mediante el módulo WebAssembly, y el PDF terminado se devuelve como bytes que conviertes en un Blob. Las imágenes nunca salen del dispositivo.

¿Puedo mezclar imágenes verticales y horizontales en un mismo PDF?

Sí. Cada página se dimensiona y se dibuja de forma independiente, por lo que un escaneo vertical y una foto horizontal pueden estar uno junto al otro. Si quieres una orientación de página uniforme, esa es una razón para usar un tamaño de página fijo y dejar que las imágenes se escalen a él.

Tengo un PDF y quiero sus páginas como imágenes, no al revés.

Esa es la operación inversa: renderizar en lugar de ensamblar. Consulta Cómo convertir páginas de PDF a imágenes en JavaScript (React).

¿Necesito la imagen en el VFS?

Solo para FromFile. FromStream acepta bytes de cualquier lugar —una respuesta de fetch, una exportación de lienzo o el estado— y omite el VFS por completo.


Ver también

Las recetas de ensamblaje aquí crean un documento completamente nuevo. Si tus imágenes necesitan ir a un PDF existente —dibujando en páginas que ya tienes—, consulta Cómo añadir imágenes a un PDF en JavaScript (React). Las otras piezas útiles del flujo de trabajo de imagen a PDF: