Cómo convertir HTML en una imagen usando Puppeteer en Node.js

Cómo configurar Puppeteer dentro de Node.js para generar imágenes sobre la marcha usando HTML y CSS y cómo escribir las imágenes generadas en el disco y Amazon S3.

Primeros pasos

Para este tutorial, vamos a utilizar CheatCode Node.js Boilerplate como punto de partida. Esto nos dará una base sólida sobre la que construir sin necesidad de mucho código personalizado.

Para comenzar, clone el modelo estándar de Github:

Terminal

git clone https://github.com/cheatcode/nodejs-server-boilerplate.git

Y luego, cd en el directorio e instalar las dependencias:

Terminal

cd nodejs-server-boilerplate && npm install

A continuación, instale el puppeteer paquete:

Terminal

npm i puppeteer

Finalmente, una vez que todas las dependencias estén instaladas, inicie el servidor con:

Terminal

npm run dev

Con todo esto completo, nuestro primer paso será configurar una ruta donde mostraremos nuestra imagen para probarla.

Agregar una ruta en el servidor para probar

Dentro del proyecto clonado, abre el /api/index.js archivo desde la raíz del proyecto:

/api/index.js

import graphql from "./graphql/server";

export default (app) => {
  graphql(app);

  // We'll add our test route here.
};

Aquí, app representa la instancia de la aplicación Express.js configurada para nosotros en el modelo en /index.js . Usaremos esto para crear nuestra ruta de prueba:

/api/index.js

import graphql from "./graphql/server";

export default (app) => {
  graphql(app);

  app.use("/graphic", (req, res) => {
    res.send("Testing 123");
  });
};

Pan comido. Para probarlo, con su servidor en ejecución, abra su navegador y diríjase a http://localhost:5001/graphic y debería ver "Prueba 123" en la pantalla.

Conectando el generador de imágenes usando Puppeteer

A continuación, debemos conectar nuestra generación de imágenes. Para hacerlo, vamos a crear un módulo separado que podemos importar donde nos gustaría convertir HTML en una imagen en nuestra aplicación:

/lib/htmlToImage.js

import puppeteer from "puppeteer";

export default async (html = "") => {
 // We'll handle our image generation here.
};

Para empezar, importamos puppeteer del paquete que instalamos anteriormente. A continuación, configuramos nuestro htmlToImage() función, tomando en un único html cadena como argumento.

/lib/htmlToImage.js

import puppeteer from "puppeteer";

export default async (html = "") => {
  const browser = await puppeteer.launch();
  const page = await browser.newPage();
};

Primero, necesitamos crear una instancia de Titiritero. Para hacerlo, usamos puppeteer.launch() . Tenga en cuenta que aquí estamos usando la sintaxis JavaScript async/await porque esperamos puppeteer.launch() para devolvernos una Promesa. Usando el await palabra clave aquí, le estamos diciendo a JavaScript, y por extensión, a Node.js, que espere hasta que reciba una respuesta de puppeteer.launch() .

A continuación, con nuestro browser creado, creamos un page con browser.newPage() (Piense en esto como abrir una pestaña en su propio navegador, pero en un estado "sin cabeza", lo que significa que no hay una interfaz de usuario, el navegador solo existe en la memoria). Nuevamente, anticipamos que se devolverá una Promesa, por lo que await esta llamada antes de continuar.

/lib/htmlToImage.js

import puppeteer from "puppeteer";

export default async (html = "") => {
  const browser = await puppeteer.launch();
  const page = await browser.newPage();

  await page.setContent(html);

  const content = await page.$("body");
  const imageBuffer = await content.screenshot({ omitBackground: true });
};

A continuación, entramos en la parte importante. Aquí, usando page.setContent() le decimos a Titiritero que llene la página del navegador con el html cadena que pasamos a nuestra función como argumento. Esto es equivalente a cargar un sitio web en su navegador y el HTML de la respuesta del servidor se carga en la memoria.

A continuación, utilizamos la API DOM (modelo de objeto de documento) integrada de Puppeteer para acceder al HTML del navegador en memoria. Aquí, en nuestro content variable, almacenamos el resultado de llamar a await page.$("body"); . Lo que está haciendo es tomar la versión renderizada en memoria de nuestro HTML y extraer el content s del <body></body> etiqueta (nuestro HTML renderizado).

En respuesta, obtenemos un Titiritero ElementHandle que es una forma de decir "el elemento tal como lo representa Titiritero en la memoria", o nuestro HTML representado como un objeto compatible con Titiritero.

Luego, usando ese content , utilizamos el Titiritero .screenshot() método para tomar una captura de pantalla de nuestra página HTML renderizada en memoria. Para dar control total de lo que se representa en nuestra imagen, pasamos omitBackground a true para asegurarnos de que el fondo de la página sea completamente transparente.

En respuesta, esperamos obtener un imageBuffer . Este es el archivo de imagen sin procesar contenido , pero no la imagen real en sí (lo que significa que verá un montón de datos binarios aleatorios, no una imagen). Antes de ver cómo obtener nuestra imagen real, debemos hacer una limpieza:

/lib/htmlToImage.js

import puppeteer from "puppeteer";

export default async (html = "") => {
  const browser = await puppeteer.launch();
  const page = await browser.newPage();

  await page.setContent(html);

  const content = await page.$("body");
  const imageBuffer = await content.screenshot({ omitBackground: true });

  await page.close();
  await browser.close();

  return imageBuffer;
};

Aquí, hemos agregado dos llamadas:page.close() y browser.close() . Como era de esperar, estos cierran la página (o pestaña del navegador) que abrimos en la memoria, así como el navegador. Es muy importante hacer esto porque, si no lo hace, terminará dejando navegadores sin cerrar en la memoria, lo que agota los recursos de su servidor (y puede causar un bloqueo potencial debido a un desbordamiento de la memoria) .

Finalmente, devolvemos nuestro imageBuffer recuperado de la función.

Renderizando la imagen en nuestra ruta

Un paso más. Técnicamente, en este punto, no hemos pasado ningún código HTML a nuestra función. Importemos htmlToImage() de vuelta a nuestro /api/index.js archivo y llamarlo desde nuestra ruta:

/api/index.js

import graphql from "./graphql/server";
import htmlToImage from "../lib/htmlToImage";

export default (app) => {
  graphql(app);

  app.use("/graphic", async (req, res) => {
    const imageBuffer = await htmlToImage(`<!-- Our HTML will go here. -->`);

    res.set("Content-Type", "image/png");
    res.send(imageBuffer);
  });
};

Aquí, hemos importado nuestro htmlToImage función de /lib/htmlToImage . En la devolución de llamada de nuestra ruta, hemos agregado el async bandera porque, ahora, estamos usando el await palabra clave antes de nuestro htmlToImage() función. Recuerde, esto es necesario porque debemos esperar a que Titiritero haga su trabajo antes podemos confiar en que nos devuelva datos.

Además de nuestra llamada, también modificamos la forma en que respondemos a la solicitud de ruta. Aquí, hemos agregado una llamada a res.set() , configurando el Content-Type encabezado a image/png . Recuerda cómo mencionamos que el imageBuffer recibimos de content.screenshot() no era técnicamente una imagen todavía? Esto es lo que cambia eso. Aquí, image/png se conoce como tipo MIME; un tipo de datos reconocido por los navegadores que dice "los datos sin procesar que le estoy dando deben representarse como ___". En este caso, decimos "procesar estos datos sin procesar como una imagen .png".

Finalmente, como cuerpo de respuesta para nuestra solicitud, pasamos imageBuffer a res.send() . Con esto, ahora, agreguemos algo de HTML a la mezcla y luego hagamos una prueba:

/api/index.js

import graphql from "./graphql/server";
import htmlToImage from "../lib/htmlToImage";

export default (app) => {
  graphql(app);

  app.use("/graphic", async (req, res) => {
    const imageBuffer = await htmlToImage(`
      <html>
        <head>
          <style>
            * {
              margin: 0;
              padding: 0;
            }

            *,
            *:before,
            *:after {
              box-sizing: border-box;
            }

            html,
            body {
              background: #0099ff;
              width: 1200px;
              height: 628px;
              font-family: "Helvetica Neue", "Helvetica", "Arial", sans-serif;
            }

            div {
              width: 1200px;
              height: 628px;
              padding: 0 200px;
              display: flex;
              align-items: center;
              justify-content: center;
            }
            
            h1 {
              font-size: 48px;
              line-height: 56px;
              color: #fff;
              margin: 0;
              text-align: center;
            }
          </style>
        </head>
        <body>
          <div>
            <h1>How to Convert HTML to an Image Using Puppeteer in Node.js</h1>
          </div>
        </body>
      </html>
    `);

    res.set("Content-Type", "image/png");
    res.send(imageBuffer);
  });
};

Aquí, estamos pasando una cadena de JavaScript simple que contiene algo de HTML. Hemos configurado una plantilla HTML básica que consta de un <html></html> etiqueta rellenada con un <head></head> etiqueta y un <body></body> etiqueta. En el <head></head> etiqueta, hemos añadido un <style></style> etiqueta que contiene algo de CSS para diseñar nuestro contenido HTML.

En el <body></body> , hemos agregado algo de HTML simple:un <div></div> etiqueta rellenada con un <h1></h1> etiqueta. Ahora, si regresamos a nuestra ruta de prueba en http://localhost:5001/graphic y deberías ver algo como esto:

¿Guay, verdad? Si hace clic derecho en la imagen y la descarga, podrá abrirla en su computadora como cualquier otra imagen.

Antes de terminar, es bueno entender cómo almacenar estos datos de forma permanente en lugar de simplemente mostrarlos en el navegador y descargarlos a mano. A continuación, veremos dos métodos:guardar la imagen generada en el disco y guardar la imagen generada en Amazon S3.

Escribir la imagen generada en el disco

Afortunadamente, escribir nuestro archivo en el disco es bastante simple. Hagamos una pequeña modificación a nuestra ruta (seguiremos usando la URL en el navegador para "activar" la generación):

/api/index.js

import fs from "fs";
import graphql from "./graphql/server";
import htmlToImage from "../lib/htmlToImage";

export default (app) => {
  graphql(app);

  app.use("/graphic", async (req, res) => {
    const imageBuffer = await htmlToImage(`
      <html>
        [...]
      </html>
    `);

    fs.writeFileSync("./image.png", imageBuffer);

    res.set("Content-Type", "image/png");
    res.send(imageBuffer);
  });
};

Bastante simplista. Aquí, todo lo que hemos hecho es importar fs (el sistema de archivos en Node.js—fs no necesita ser instalado), y luego agregó una llamada a fs.writeFileSync() , pasando la ruta en la que queremos que se almacene nuestro archivo (en este caso, en un archivo llamado image.png en la raíz de nuestro proyecto) y los datos para el archivo.

Cabe destacar que para la extensión de archivo hemos establecido explícitamente image/png . Similar a lo que vimos renderizando nuestra imagen directamente a nuestra ruta, ese .png comunica a la computadora que el contenido de este archivo representa una imagen en un .png formato.

Ahora, cuando visitemos nuestra ruta, nuestro archivo se escribirá en /image.png en el disco, así como en el navegador.

Envío de la imagen generada a Amazon S3

Antes de continuar, para acceder a Amazon S3 necesitamos agregar una nueva dependencia:aws-sdk . Instalémoslo ahora:

Terminal

npm i aws-sdk

A continuación, aunque similar, enviar nuestra imagen generada a Amazon S3 es un poco más complicado. Para hacerlo, vamos a crear un nuevo archivo en /lib/s3.js para implementar algún código que nos ayude a conectarnos a Amazon S3 y escribir nuestro archivo (lo que se conoce como "colocar un objeto en el depósito").

/lib/s3.js

import AWS from "aws-sdk";

AWS.config = new AWS.Config({
  accessKeyId: "<Your Access Key ID Here>",
  secretAccessKey: "<Your Secret Access Key Here>",
  region: "us-east-1",
});

// We'll write the S3 code for writing files here.

Aquí, importamos el AWS del aws-sdk acabamos de instalar. A continuación, configuramos AWS.config igual a una nueva instancia de AWS.Config (observe que la diferencia entre los nombres es la "C" mayúscula), pasando las credenciales que queremos usar para comunicarnos con AWS.

Si aún no tiene las credenciales necesarias, querrá leer este tutorial de Amazon sobre cómo crear un nuevo usuario. Para este ejemplo, al crear su usuario, asegúrese de habilitar "Acceso programático" en el paso uno y adjunte el AmazonS3FullAccess política en "Adjuntar políticas existentes directamente" en el paso dos.

Una vez que haya generado su ID de clave de acceso y su clave de acceso secreta, puede completar los campos anteriores.

Advertencia justa:NO envíe estas claves a un repositorio público de Github. Hay bots en Github que buscan claves de AWS desprotegidas y las usan para activar granjas de bots y realizar actividades ilegales (mientras te hacen pagar la factura).

Para region , querrá especificar la región en la que crea su depósito de Amazon S3. La región es la ubicación geográfica de su depósito en Internet. Si aún no ha creado un depósito, querrá leer este tutorial de Amazon sobre cómo crear un depósito nuevo.

Al configurar su cubo, para este tutorial, asegúrese de desmarcar "Bloquear acceso público". Esta es una buena configuración para entornos de producción, pero dado que solo estamos jugando, es seguro desmarcarla. Advertencia justa:NO almacene ningún dato confidencial en este cubo.

/lib/s3.js

import AWS from "aws-sdk";

AWS.config = new AWS.Config({
  accessKeyId: "<Your Access Key ID Here>",
  secretAccessKey: "<Your Secret Access Key Here>",
  region: "us-east-1",
});

const s3 = new AWS.S3();

export default {
  putObject(options = {}) {
    return new Promise((resolve, reject) => {
      s3.putObject(
        {
          Bucket: options.bucket,
          ACL: options.acl || "public-read",
          Key: options.key,
          Body: options.body,
          ContentType: options.contentType,
        },
        (error, response) => {
          if (error) {
            console.warn("[s3] Upload Error: ", error);
            reject(error);
          } else {
            resolve({
              url: `https://${options.bucket}.s3.amazonaws.com/${options.key}`,
              name: options.key,
              type: options.contentType || "application/",
            });
          }
        }
      );
    });
  },
};

Una vez que hayamos configurado nuestro usuario de AWS IAM y la región del depósito, a continuación, queremos crear una instancia de s3 llamando al new AWS.S3() .

Pensando en el futuro, queremos anticipar la necesidad de otros métodos de S3 más adelante, por lo que en lugar de solo exportar una sola función de nuestro archivo, aquí exportamos un objeto con un putObject método.

Para ese método (el nombre de una función definida como parte de un objeto), anticipamos un options objeto a pasar que contiene los datos e instrucciones de cómo manejar nuestro archivo. En el cuerpo de esta función, devolvemos una Promesa para que podamos envolver el s3.putObject() asíncrono método del aws-sdk paquete.

Cuando llamamos a ese método, pasamos las opciones según la documentación del SDK de Amazon S3, describiendo nuestro archivo, dónde queremos que viva y los permisos para asociarlo. En el método de devolución de llamada para s3.putObject() , asumiendo que no tenemos un error, construimos un objeto que describe la ubicación de nuestro nuevo archivo en Amazon S3 y resolve() la Promesa que hemos devuelto de la función.

/api/index.js

import fs from "fs";
import graphql from "./graphql/server";
import htmlToImage from "../lib/htmlToImage";
import s3 from "../lib/s3";

export default (app) => {
  graphql(app);

  app.use("/graphic", async (req, res) => {
    const imageBuffer = await htmlToImage(`
      <html>
        [...]
      </html>
    `);

    fs.writeFileSync("./image.png", imageBuffer);

    const s3File = await s3.putObject({
      bucket: "<Your Bucket Name Here>",
      key: `generated-image.png`,
      body: imageBuffer,
      contentType: "image/png",
    });

    console.log(s3File);

    res.set("Content-Type", "image/png");
    res.send(imageBuffer);
  });
};

De vuelta en nuestro /api/index.js archivo, ahora estamos listos para subir a S3. Modificando ligeramente nuestro código anterior, importamos nuestro s3 archivo de /lib/s3.js en la parte superior y luego en el cuerpo de la devolución de llamada de nuestra ruta, agregamos nuestra llamada a s3.putObject() , pasando el bucket queremos que nuestro archivo se almacene en el key (ruta y nombre de archivo relativo a la raíz de nuestro depósito) para nuestro archivo, el body (sin procesar imageBuffer datos), y el contentType (el mismo image/png tipo MIME que discutimos anteriormente).

Finalmente, nos aseguramos de await nuestra llamada a S3 para asegurarnos de recuperar nuestro archivo. En su propia aplicación, esto puede no ser necesario si está de acuerdo con que el archivo se cargue en segundo plano.

¡Eso es todo! Ahora, si visitamos http://localhost:5001/graphic en nuestra aplicación, deberíamos ver nuestro gráfico cargado en Amazon S3, seguido de la confirmación de que se cerró la sesión en la terminal:

Terminal

{
  url: 'https://cheatcode-tutorials.s3.amazonaws.com/generated-image.png',
  name: 'generated-image.png',
  type: 'image/png'
}

Terminando

En este tutorial, aprendimos cómo generar una imagen desde HTML y CSS usando Puppeteer. Aprendimos cómo activar un navegador en la memoria, pasarle algo de HTML y luego tomar una captura de pantalla de esa página renderizada usando Titiritero. También aprendimos cómo devolver nuestra imagen a un navegador directamente y cómo almacenar ese archivo en el disco usando el sistema de archivos Node.js y cargar nuestra imagen en Amazon S3 usando el SDK de JavaScript de AWS.