Crear PDF con JavaScript y jsPDF

Crear archivos PDF dinámicos directamente en el navegador es posible gracias a la librería JavaScript jsPDF. Aprende cómo en este artículo.

http://raw.githack.com/MrRio/jsPDF/master/docs/index.html “The leading HTML5 client solution for generating PDFs. Perfect for event tickets, reports, certificates, you name it!” 

En la última parte de éste artículo he preparado un tutorial práctico donde te enseño a implementar la librería. A continuación puedes ver el ejercicio una vez completado. Haz click aquí para abrirlo en una ventana nueva

¿Quieres aprender a hacer este ejercicio? Saltar directamente al tutorial

Ofrecer a los usuarios, la posibilidad de descargar un archivo PDF, con información específica de su interés, puede ser de enorme utilidad. Resolver, técnicamente, una funcionalidad así, implica generar el archivo de forma dinámica, bajo petición. Y durante mucho tiempo, hacer esto, se ha considerado competencia exclusiva de entornos backend.

Sin embargo, hoy veremos que nada más alejado de la realidad. Con el incremento computacional de los dispositivos, la parte cliente ha ido ganando terreno a la parte servidora. Funciones que antes solo eran posibles en un servidor, ahora se realizan sin problemas, directamente en el navegador.

La parte cliente gana terreno a la parte servidor

La librería JsPDF, es una muestra de ello. Mediante su API, se pueden generar archivos PDF a petición, directamente en el navegador. Lo cual, es perfecto para crear tickets, informes o certificados, a tiempo real.

A decir verdad, esta librería, da soporte tanto al desarrollo de front end, como de back end. Su API JavaScript, se puede implementar en el navegador y en entornos NodeJs por igual. Sin embargo, en esta ocasión, nos centraremos en la implementación front end.

JsPDF, es una herramienta creada por la agencia digital Parallax. Su repositorio se encuentra activamente mantenido por un equipo de 191 personas. Y en el momento de redactar estas líneas, ostenta más de 25K estrellas en su repositorio de Github.

La versión “empaquetada” ocupa alrededor de 350Kb. Por aquí, estamos acostumbrados a ver librerías más ligeras. Pero teniendo en cuenta la problemática que resuelve, es comprensible que ocupe eso.

Por supuesto, se puede instalar mediante el comando “npm install jspdf –save”, e integrarlo cómodamente en nuestro proyecto.

Crear PDF es sencillo con JavaScript y jsPDF

tostadora JS que genera un pdf
Tostadora JS que genera un pdf

Su API expone una gran cantidad de métodos, propiedades y módulos. La página de presentación de la librería ofrece una documentación perfectamente ordenada. Aun así, es recomendable pasarse por el apartado “live demo”. En esta sección, se pueden ver varios ejemplos de aplicación de la librería.

En cualquier caso, es imprescindible empezar instanciando un objeto de la clase jsPDF. Ese objeto, por convención, lo llamamos “doc”, haciendo alusión al documento PDF que representa. 

Como es habitual, instanciar la clase, admite pasar un objeto con parámetros de configuración opcionales. Se pueden decidir propiedades como el formato y la orientación de la página. Las unidades de medida y la comprensión del documento. Hasta la encriptación, por si se desea que el archivo esté protegido bajo contraseña.

El objeto “doc” recuerda en algunos aspectos al “CanvasRenderingContext2D” de las etiquetas canvas. A través de métodos como rect(), lineTo(), elipse(), stroke() o fill(), se pueden dibujar gráficos vectoriales. También, mediante el método text(), se pueden incluir textos en determinados puntos de las páginas. En otra ocasión, vimos otra librería cuya API, también era muy parecida a la de Canvas.

Crear arte con JavaScript y P5.js

Pero jsPDF, va más allá. Además, expone funciones para crear elementos más complejos, o propios de archivos PDF. Por ejemplo, a través del método table(), se pueden crear tablas de datos con filas y columnas. O mediate el método createAnnotation(), hasta se pueden incluir comentarios al archivo.

La opción de incluir más páginas en el documento, se encuentra disponible a través del método addPage().

Una vez que esté el documento configurador y listo, jsPDF resuelve todo el proceso de descarga, al llamar a la función save().

A continuación, veremos cómo crear archivos PDF dinámicamente con JavaScript y JsPDF.

Crea un archivo PDF dinámicamente con JavaScript

Para ello, programaremos una sencilla aplicación web. En ella, el usuario podrá crear y exportar una ficha de un personaje de ficción. A partir de una plantilla, se podrán introducir datos como el nombre, el tipo de personaje o las habilidades. Y posteriormente, descargar un PDF con dicha información.

Empezaremos preparando la interfaz de usuario. La interfaz se va a componer, por un lado, de un formulario HTML con distintos campos. Y por el otro, de una etiqueta iframe, que nos ayudará a pre-visualizar el pdf, antes de descargarlo.

En el formulario incluiremos los siguientes campos: nombre, sobrenombre, descripción, tipo, fuerza, magia y velocidad. Seguidamente también incluiremos un elemento para mostrar mensajes de error cuando sea necesario, y un par de botones. Los botones permitirán al usuario ver el archivo y descargarlo.

A continuación incluimos una etiqueta iframe vacía. Sirviéndonos de la librería de estilos css Tailwind, incluiremos clases CSS a todos los elementos para darle estilos. Es importante, incluir atributos “id” en las etiquetas formulario, mensaje de errores, y en el iframe. Posteriormente, se usarán esos ids para el script Js.

También destacar que en el listado de opciones de tipo de personaje, se ha incluido un atributo “data-image-url” especial. Este  servirá para incluir una imagen específica para cada tipo.

<body class="h-screen flex flex-col items-center bg-slate-50">
    <div class="block md:flex main-layout w-screen">
      <div class="flex justify-center items-center p-5">
        <form id="form-character-profile" class="w-full">
          <div class="mb-5">
            <label for="" class="block text-sky-900 font-bold">
              Nombre: 
            </label>
            <input type="text" name="name" placeholder="Nombre" value="Gandalf" class="block w-full p-3 border-2 border-sky-700 bg-sky-50 rounded" />
          </div>
          <div class="mb-5">
            <label for="" class="block text-sky-900 font-bold">
              Sobrenombre: 
            </label>
            <input type="text" name="surname" placeholder="Apellidos" value="El gris" class="block w-full p-3 border-2 border-sky-700 bg-sky-50 rounded" />
          </div>
          <div class="mb-5">
            <label for="" class="block text-sky-900 font-bold">Descripción: </label>
            <textarea
              name="description"
              cols="30"
              rows="5"
              class="block w-full p-3 border-2 border-sky-700 bg-sky-50 rounded"
              placeholder="Descripción"
            >Llevaba un sombrero azul alto y puntiagudo, una capa larga gris y una bufanda plateada. Tenía una larga barba blanca y cejas pobladas que sobresalían más allá del ala de su sombrero.</textarea>
          </div>
          <div class="mb-5">
            <label for="" class="block text-sky-900 font-bold">
              Tipo:
            </label>
            <select name="type" class="block w-full p-3 border-2 border-sky-700 bg-sky-50 rounded">
              <option
                value="wizard"
                data-image-url="imgs/Wizard_Idle.png"
              >Mago</option>
              <option
                value="bandit"
                data-image-url="imgs/Bandit_Idle_1.png"
              >Bandido</option>
              <option
                value="bear"
                data-image-url="imgs/Bear_Idle_1.png"
              >Oso</option>
              <option
                value="elf"
                data-image-url="imgs/HighElf_F_Idle_1.png"
              >Elfa</option>
              <option
                value="ent"
                data-image-url="imgs/Ent_Idle_1.png"
              >Ent</option>
              <option
                value="golem"
                data-image-url="imgs/Golem_Idle_1.png"
              >Golem</option>
            </select>
          </div>
          <div>
            <div>
              <label class="block text-sky-900 font-bold">Fuerza:</label>
              <input
                type="range"
                name="strength"
                value="40"
                class="w-full"
              
              /></div>
            <div>
              <label class="block text-sky-900 font-bold">Magia:</label>
              <input type="range" name="magic" class="w-full" value="90" />
            </div>
            <div>
              <label class="block text-sky-900 font-bold">Velocidad:</label>
              <input
                type="range"
                value="60"
                name="velocity"
                class="w-full"
              
              /></div>
          </div>
          <div id="error-message-container" class="hidden text-red-500 bg-red-100 p-4 rounded mt-3">
            *Error messages
          </div>
          <hr class="my-5">
          <div class="mt-3">
            <button type="button" class="bg-sky-500 text-white px-3 py-2 rounded preview-pdf-btn">Previsualizar PDF</button>
            <button type="submit" class="bg-sky-500 text-white px-3 py-2 rounded">Descargar PDF</button>
          </div>
        </form>
      </div>
      <div>
        <iframe
          id="frame"
          src=""
          frameborder="0"
        ></iframe>
      </div>
    </div>
  </body>

En el archivo SCSS, incluiremos los recursos básicos de Tailwind, y añadiremos unas pocas líneas de CSS adicionales.

@tailwind base;
@tailwind components;
@tailwind utilities;
body {
  margin: 0;
  padding: 0;
}
.main-layout {
  & > div {
    width: 100%;
    iframe {
      width: 100%;
      min-height: 500px;
      height: 100%;
      max-height: 800px;
    }
  }
}

Con la estructura y los estilos de la interfaz listos, podemos empezar a programar el script. En primer lugar, importamos la librería JsPDF y los estilos.

import { jsPDF } from "jspdf";
import "./SCSS/index.scss";

Seguidamente, guardamos en variables, referencias a elementos del DOM. Estos elementos serán, el formulario, el botón de previsualización, el contenedor de los mensajes de error, y el iframe.

const formCharacterProfile = document.querySelector("#form-character-profile");
const previewBtn = formCharacterProfile.querySelector(".preview-pdf-btn");
const errorMessageContainer = document.querySelector(
  "#error-message-container"
);
const frame = document.querySelector("#frame");

Antes de generar el pdf con la librería JsPDF, es importante extraer y validar los datos del formulario. Para ello, declaramos la función handleOnSubmitForm. Esta función se encargará de recorrer todos los campos del formulario con atributo “name”. Para cada uno, tratará de guardar en la variable “characterData” la propiedad y el valor. En caso de que un valor no esté definido, lanzará un error y actualizará la interfaz, para mostrar ese error.

const handleOnSubmitForm = (e) => {
  e.preventDefault();
  try {
    const characterProperties = Array.from(e.target.querySelectorAll("[name]"));
    const characterData = {};
    errorMessageContainer.classList.add("hidden");
    for (let i = 0, j = characterProperties.length; i < j; i++) {
      const field = characterProperties[i];
      const attribute = field.getAttribute("name");
      const value = field.value;
      if (!field.value) {
        throw new Error(`El campo ${attribute} está vacio!`);
      }
      characterData[attribute] = value;
      if (attribute === "type") {
        const option = field.querySelector(`[value=${value}]`);
        characterData[attribute] = {
          name: option.innerHTML,
          image: option.dataset.imageUrl,
        };
      }
    }
    generatePDF(characterData, e.isPreview);
  } catch (err) {
    errorMessageContainer.innerHTML = err.message;
    errorMessageContainer.classList.remove("hidden");
  }
};

Al final de la función, si está todo correcto, llamará a “generatePDF()”.

GeneratePDF, es la función que se va a encargar de crear el archivo PDF dinámicamente. Va a recibir dos argumentos. El primero es el objeto que describe las propiedades del personaje. Y el segundo, es una booleana, para decidir si hacer una previsualización o una descarga.

Al inicio de esta función instanciamos la clase jsPDF. Aunque podríamos pasar un objeto de configuración, dejaremos los valores predefinidos. Mediante el objeto “doc” posicionamos todos los textos e imágenes. Para ello, nos servimos de los métodos “text()” y “addImage()”.

Adicionalmente, podemos dar estilos a esos textos con métodos como “setFont()” y “setFontSize()”. Así como añadir algunas líneas separadoras, con el método “line()”.

Ya con el contenido “maquetado” en el documento, pasamos a generar el PDF. En caso de que la variable “preview” sea “true, actualizamos la propiedad del iframe, llamando al método “output(«bloburl»)”. En caso que sea false, directamente podemos llamar al método “save()”. Éste último método, creará y descargará el archivo por nosotros con el nombre que se le indique.

const generatePDF = (characterData, preview) => {
  const doc = new jsPDF();
  doc.setFontSize(40);
  doc.setFont("helvetica", "bold");
  doc.text(characterData.name, 60, 30);
  doc.setFont("helvetica", "normal");
  doc.text(characterData.surname, 60, 42);
  doc.addImage(characterData.type.image, "PNG", 5, 0, 50, 50);
  doc.setFontSize(20);
  const docWidth = doc.internal.pageSize.getWidth();
  const docHeight = doc.internal.pageSize.getHeight();
  doc.line(0, 60, docWidth, 60);
  doc.setFont("helvetica", "italic");
  const splitDescription = doc.splitTextToSize(
    characterData.description,
    docWidth - 20
  );
  doc.text(splitDescription, 10, 80);
  doc.setFontSize(20);
  doc.setFont("helvetica", "bold");
  doc.text(characterData.type.name, docWidth - 20, 45, { align: "right" });
  doc.line(0, docHeight - 60, docWidth, docHeight - 60);
  doc.text(`Fuerza: `, 10, docHeight - 40);
  doc.text(`Magia: `, 10, docHeight - 30);
  doc.text(`Velocidad: `, 10, docHeight - 20);
  doc.setFont("helvetica", "normal");
  doc.text(`${characterData.strength}`, 50, docHeight - 40);
  doc.text(`${characterData.magic}`, 50, docHeight - 30);
  doc.text(`${characterData.velocity}`, 50, docHeight - 20);
  if (preview) {
    frame.src = doc.output("bloburl");
    return;
  }
  doc.save(`${characterData.name}-${characterData.surname}`);
};

Finalmente, solo queda escuchar los eventos de envío de formulario, y de click en el botón de previsualizar.

Al enviar el formulario, llamaremos a la función “handleOnSubmitForm”. Y al hacer click en el botón, provocaremos igualmente un envío. Sin embargo, en ese caso, ampliaremos el evento, con la boleana “isPreview” seteada como “true.

previewBtn.addEventListener("click", () => {
  const event = new Event("submit");
  event.isPreview = true;
  formCharacterProfile.dispatchEvent(event);
});
formCharacterProfile.addEventListener("submit", handleOnSubmitForm);

Con este simple ejercicio, hemos visto cómo crear fácilmente archivos PDF con JavaScript y JsPDF.

Conectar el mundo online y offline

No se debe subestimar la capacidad de comunicación de un archivo PDF. A pesar de ser algo aparentemente simple, puede ser el puente perfecto que conecta el mundo online con el offline.

Un claro ejemplo de esta afirmación, es que hoy en día, mucha gente sigue imprimiendo tickets y billetes de avión. Y en estos aparece un QR generador dinámicamente, que demuestra su autenticidad.

Por otra parte, puede que llegado a éste punto te preguntes, ¿Y qué pasaría si en vez de un PDF, quiero generar cualquier otro tipo de archivo? Bien, recientemente he escrito un artículo analizando otra herramienta que te puede interesar.

Como descargar cualquier tipo de archivo directamente desde el navegador

Este ha sido un pequeño vistazo a la librería JsPDF. A continuación os dejo enlaces a más recursos, así como el ejercicio subido al repositorio de Github de “Librerías Js”:

Nos vemos pronto, un abrazo desarrolladores!

9 comentarios en «Crear PDF con JavaScript y jsPDF»

  1. Mil gracias!! He pasado horas buscando cómo usar esta librería de forma exitosa, pero en la documentación oficial no es muy clara al respecto; este artículo me fue súper útil.

    Responder
  2. Segui los pasos de este tutorial y al final no he podido ejecutarlo, me da este error en la consola Uncaught SyntaxError: Cannot use import statement outside a module (at scripts.js:1:1)

    Responder
  3. Hola!

    Excelente tutorial, muy bien explicado, justo lo que estaba buscando.

    También me gustaría que al descargar desde la previsualización, con el ícono de la flecha, descargara con un nombre asignado, no vi en la documentación una forma para hacerlo, y al descargar desde ahí no toma en cuenta el nombre escrito en doc.save().

    Muchas gracias.

    Responder

Deja un comentario