Exportar un trozo de HTML a una imagen

Figura
Figura. Imagen exportada desde HTML

Exportar un trozo de HTML a una imagen se basa en la facultad de albergar un SVG (Scalable Vector Graphics) en un elemento imagen (IMG). Se usa la API Image(), que es un constructor del elemento imagen, conjuntamente con un Canvas para convertir el SVG en un formato de imagen, como PNG, JPG y otros. Un SVG no es otra cosa que un XML. Y yo empecé a estudiar HTML aprendido primero XHTML que debe ser escrito como XML. Es necesario respetar las reglas XHTML en el trozo de HTML que queremos convertir en imagen, pero no es nada complicado. La imagen de la Figura es un archivo PNG que se extrae de un trozo de HTML, como veremos después.

Figura
Figura. Fórmula HTML+CSS

En esto días he incorporado Copy Math para evaluar, copiar y exportar como imagen fórmulas matemáticas escritas en HTML. La Figura es un ejemplo de ello: una imagen exportada desde un trozo de HTML que visualiza una fórmula matemática en una página web. Previamente también lo había incorporado en algunas herramientas como las siguientes:

Por supuesto que lo más simple para copiar como imagen una visualización de un trozo HTML en pantalla es hacer una captura con diversos recursos, como la tecla "IMPR PANT" de los ordenadores, para luego pegarla en un editor de imágenes y recortar el trozo que deseemos. Navegadores como Firefox ofrecen también una funcionalidad para copiar como imagen trozos de la página. Así que no es este un recurso imprescindible y nada nuevo, por supuesto.

En este tema intentaré explicar cómo se hace, pues de paso recordaremos cosas que se relacionan como SVG, XML, XHTML, HTML-5, Base64, Canvas e Image. Y otras cosas relacionadas con la serialización de bytes, manejando Base64 con el uso de los métodos encodeURIComponent(), btoa() y atob(). O bien con TextEncoder y TextDecoder, donde se usa el array tipado (Typed Array) Uint8Array, que junto a Blob y objetos URL veremos también para la descarga de archivos, algo que explicaremos en el último apartado.

Veamos primero el ejemplo interactivo que nos permite exportar y descargar a una imagen PNG un trozo de HTML como el de la Figura 1:

Ejemplo: Exportar HTML a imagen

HTML to IMG

SVG en un elemento imagen

IMG
Figura. SVG

Para explicar todo el proceso primero hay que ver como podemos insertar un SVG dentro de un elemento imagen. Primero veamos un SVG muy simple, el de la Figura. Se trata de escribir en SVG el texto "IMG" con fuente Arial y color rojo. El código SVG es muy simple, como se observa a continuación. Para insertarlo en esta página no es necesario especificar el atributo xmlns que establece el espacio de nombres xmlns="http://www.w3.org/2000/svg", puesto que en HTML-5 se asigna automáticamente a un SVG. Pero para el próposito de exportar HTML a imagen si que necesitaremos aportarlo.

<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 4 4" 
width="100" height="60">
    <text x="0" y="2.6" font-family="Arial" font-size="2" 
    stroke-width="0" fill="red">IMG</text>
</svg>
Figura
Figura. IMG + file SVG

Y en la Figura vemos un elemento <img> que porta en su atributo src un SVG desde el archivo sample.svg, archivo cuyo texto contiene el código SVG que vimos antes.

No podemos incluir ese código directamente en src, pues las reglas de formación de atributos no lo permiten. Para lograrlo hemos de usar la codificación Base64, que es una forma de codificar en texto un contenido binario.

JavaScript dispone de la función Window.btoa(content) para codificar en una cadena Base64 ASCII desde una cadena binaria. Si copia el código siguiente y lo ejecuta en la consola del navegador:

console.log(window.btoa(`<svg xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 4 4" width="100" height="60">
    <text x="0" y="2.6" font-family="Arial" font-size="2" 
    stroke-width="0" fill="red">IMG</text>
</svg>`))

obtendremos el siguiente Base64 del SVG anterior, con una codificación que usa los 64 caracteres ASCII a-z, A-Z, 0-9, +, /, =

PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjAgMCA0IDQiCndpZHRoPSIxMDAiIGhlaWdodD0iNjAiPgogICAgPHRleHQgeD0iMCIgeT0iMi42IiBmb250LWZhbWlseT0iQXJpYWwiIGZvbnQtc2l6ZT0iMiIKICAgIHN0cm9rZS13aWR0aD0iMCIgZmlsbD0icmVkIj5JTUc8L3RleHQ+Cjwvc3ZnPg==
    

Por otro lado en el atributo src del elemento IMG además de una URL que apunta a un archivo podemos insertar la imagen en formato Base64 directamente. Para ello construimos el contenido de ese atributo de la siguiente forma:

data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjAgMCA0IDQiCndpZHRoPSIxMDAiIGhlaWdodD0iNjAiPgogICAgPHRleHQgeD0iMCIgeT0iMi42IiBmb250LWZhbWlseT0iQXJpYWwiIGZvbnQtc2l6ZT0iMiIKICAgIHN0cm9rZS13aWR0aD0iMCIgZmlsbD0icmVkIj5JTUc8L3RleHQ+Cjwvc3ZnPg==
    

En la herramienta Compositor imágenes puede arrastrar un archivo de imagen desde su ordenador y obtener el Base64 con el prefijo data, cuyo contenido será igual que el anterior para el archivo sample.svg

Figura
Figura. IMG + data SVG

Anteponemos data:image/svg+xml;base64, al Base64 declarando que contiene un SVG en formato XML, con objeto de que el navegador procese adecuadamente la imagen como SVG. Otros formatos permitidos en estos src data son los tipos de imagen que soporte, como image/png, image/jpg, etc. Todo ese contenido de texto lo incluimos en el atributo src de la imagen de la Figura

Función JavaScript para exportar HTML a Imagen

La función exportImage() en JavaScript se fundamenta en estos pasos:

  • Incorporar trozo HTML en un XML+SVG
  • Obtener el Base64 del XML+SVG
  • Incorporar el Base64 en un elemento temporal Image
  • A la carga de la imagen temporal:
    • Dibujar la imagen temporal en un Canvas
    • Recuperar imagen del Canvas en PNG e insertarla en Base64 en el elemento definitivo de imagen
  • Gestionar errores en la carga de la imagen temporal

Con detalle esta es la función:

function exportImage(){
    try {
        errorExport.textContent = "";
        let imageExport = document.getElementById("image-export");
        imageExport.src = "";
        // 1) Incorporar trozo HTML en un XML+SVG
        let elementExport = document.getElementById("mydiv");
        let parent = elementExport.parentElement;
        let [width, height] = [parent.offsetWidth, parent.offsetHeight];
        let html = elementExport.outerHTML;
        html = html.replace(regEmptyElements, "<$1 />");
        let cssExport = document.getElementById("css-export");
        let css = cssExport ? cssExport.textContent : "";
        let style = `<style>${css}</style>`;
        let xml = `<svg xmlns="http://www.w3.org/2000/svg" width="${width}" height="${height}" style="background-color:white">
        <foreignObject width="100%" height="100%">
            <div xmlns="http://www.w3.org/1999/xhtml">${style}${html}</div>
        </foreignObject>
        </svg>`;
        // 2) Obtener el Base64 del XML+SVG
        let b64 = window.btoa(encodeURIComponent(xml).replace(/%([0-9A-F]{2})/g, (b0,b1) => String.fromCharCode(`0x${b1}`)));
        let data = `data:image/svg+xml;base64,${b64}`;
        // 3) Incorporar el Base64 en un elemento temporal Image
        let imgTemp = new Image();
        imgTemp.width = width;
        imgTemp.height = height;
        imgTemp.src = data;
        imgTemp.decode().then(() => {
            try {
                // 4) Dibujar la imagen temporal en un Canvas
                let canvas = document.createElement("canvas");
                canvas.width = width;
                canvas.height = height;
                let context = canvas.getContext("2d");
                context.drawImage(imgTemp, 0, 0, width, height);
                // 5) Recuperar imagen del Canvas e insertarla en Base64 en el elemento definitivo de imagen
                let mimeType = `image/png`;
                imageBase64 = context.canvas.toDataURL(mimeType);
                imageExport.src = imageBase64;
                imageExport.width = width;
                imageExport.height = height;
                imageExport.style.display = "inline";
                document.getElementById("text-export").value = imageBase64;
            } catch (e) {errorExport.textContent = e.message}
        }).catch((msg) => {
            // 6) Gestionar errores
            let imageExport = document.getElementById("image-export");
            imageExport.src = noImageData;
            imageExport.width = 186;
            imageExport.height = 45;
            imageBase64 = noImageData;
            document.getElementById("text-export").value = noImageData;
            errorExport.textContent = msg;
        });
    } catch(e) {errorExport.textContent = e.message}
}

El metodo decode() devuelve una promesa que nos permite gestionar mejor la carga de la imagen, o en su caso, el posible error. Antes lo hacíamos con los eventos load y error, con algo como esto agregando el resto de líneas para los dos casos carga y error:

imgTemp.addEventListener("load", () => {
    ...
});
imgTemp.addEventListener("error", (e) => {
    ...
    // arg e es un Event Object pero no aporta información
    errorExport.textContent = `Error cargando imagen`;
});

Una mejora es que cuando la imagen no se pudiera cargar, como veremos en ejemplos en siguientes apartados, el evento error no aporta mensaje, mientras que cuando se captura desde una promesa si que porta un mensaje de error.

HTML+CSS fuente

Empecemos ahora a explicar todo el proceso de exportación. Como HTML fuente usaremos este trozo de código que visualiza lo que vemos en la Figura 2:

<div id="mydiv">
    <b>HTML</b> to <span style="color: red">IMG</span>
</div>

Se incorpora estilo en línea con el elemento SPAN y también en la cabecera de la página:

<style id="css-export">
    #mydiv {
        padding: 1em;
        border: navy solid 2px;
        border-radius: 0.5em;
        background: linear-gradient(lightpink, lightcyan);
        display: inline-block;
        font-family: "Arial", sans-serif;
        font-size: 2em;
    }
    #mydiv b {
        text-shadow: 4px 2px 2px blue;
        color: lime;
    }
    #mydiv span {
        border: blue double 4px;
        padding: 0.2em;
        box-shadow: 0.3em 0.3em 0.25em 0.02em rgba(0,0,255,0.5);
    }
</style>

Aplicamos estilos como bordes redondos, gradientes de fondo, sombras de texto y de caja con objeto de verificar que la imagen exportada es copia fiel del original HTML.

Incorporando el trozo de HTML en un XML+SVG

Para ejcutar la exportación empezaremos detectando el trozo de HTML y su CSS asociado:

let elementExport = document.getElementById("mydiv");
let parent = elementExport.parentElement;
let [width, height] = [parent.offsetWidth, parent.offsetHeight];
let html = elementExport.outerHTML;
html = html.replace(regEmptyElements, "<$1 />");
let cssExport = document.getElementById("css-export");
let css = cssExport ? cssExport.textContent : "";
let style = `<style>${css}</style>`;

Observe que las dimensiones ancho y alto las obtenemos del padre del elemento, pues hemos de considerar los posibles márgenes del elemento así como los rellenos del padre. Extraemos el trozo de HTML usando el navegador con let html = elementExport.outerHTML. Y el contenido CSS del elemento STYLE identificado componiendo el elemento con let style = `<style>${css}</style>`. Y componemos el XML como sigue:

let xml = `<svg xmlns="http://www.w3.org/2000/svg" width="${width}" 
height="${height}" style="background-color:white">
<foreignObject width="100%" height="100%">
    <div xmlns="http://www.w3.org/1999/xhtml">${style}${html}</div>
</foreignObject>
</svg>`;

Se observa que componemos un SVG, que no es otra cosa que un XML. Hay que dotarlo del espacio de nombres xmlns="http://www.w3.org/2000/svg". Le dotamos de ancho y alto que recuperamos del elemento padre del que queremos pasar a imagen. Como hemos comentado antes, hemos de tomar estas medidas pues el padre pudiera contener rellenos (padding) o el elemento objetivo márgenes (margin) que afectarían a la presentación.

Para insertar un trozo de HTML dentro de un XML podemos usar el elemento <foreignObject>. En su interior ubicamos un elemento <div> al que tenemos que dotar del espacio de nombres xmlns="http://www.w3.org/1999/xhtml" para que lo reciba como un XHTML. Por eso, como explicaremos después, el trozo de HTML debe cumplir las reglas XHTML. Finalmente insertamos el elemento <style> y el propio trozo de HTML.

Figura
Figura. Error decode()

En lo que sigue veremos algunas reglas que debe cumplir un trozo de HTML para que se pueda decodificar como XHTML. Empecé estudiando XHTML en el año 2010 y publiqué un Glosario XHTML que lo explica a fondo.

En el contenido del SVG, y por tanto del XML, teníamos la línea <b>HTML</b> to <span style="color: red">IMG</span>. Supongamos que agregamos a "to" el elemento <br>, un salto de línea en HTML. La visualización sería como lo vemos en la Figura. Cuando intentemos exportar ese contenido obtendremos un mensaje de error Encoding Error: The source image cannot be decoded, avisando que hubo un error decodificando la imagen.

Este error es que los elementos vacíos en XHTML y, por tanto, en XML, debe cerrarse con una barra final <br />. Las diferencias a tener en cuenta entre XHTML y HTML las explique hace años en el tema Sintaxis HTML-5. Sucede que en HTML-5 no es necesario incluir la barra final. Además aunque escribamos el HTML con barra como <br /> y dado que recuperamos el HTML desde el navegador con let html = elementExport.outerHTML, los navegadores prescinden de esa barra final, recuperándose <br>. Para resolverlo disponemos de lo siguiente en la función exportImage():

let emptyElements = ["br", "col", "embed", "hr", "img", "input", "keygen", "link", "meta", "param", "source", "track", "wbr"];
let regEmptyElements = new RegExp(`<((?:${emptyElements.join("|")})[^\/>]*)\/?>`, "g");

function exportImage(){
    try {
        ...
        let html = elementExport.outerHTML;
        html = html.replace(regEmptyElements, "<$1 />");
        ...
}

Por un lado disponemos de una lista de elementos vacíos de HTML-5, de la cual espero que esté actualizada, pues hace tiempo que no lo he comprobado. Construimos una expresión regular para usarla en la función y agregarle una barra al final.

No hay que olvidar que una cosa son los elementos vacíos y otra los elementos a los cuáles se puede prescindir del tag de cierre. Por ejemplo, los elementos <p> o <li> no son elementos vacíos, pues lo usual es que tengan un contenido, pero puede prescindirse del tag de cierre. Y esto ocasionaría error al tratarlo como XML. La función exportImage() comprueba elementos vacíos pero no elementos sin tags de cierre. En mi caso me acostumbré a escribir en modo XHTML y nunca omito tags de cierre. Y siempre cierro elementos vacíos con la barra final, aunque luego el navegador se las quite.

El HTML donde teníamos <span style="color: red">IMG</span> podíamos haberlo escrito como <span style=color:red>IMG</span> sin comillas en el valor del atributo, puesto que HTML permite omitir comillas cuando no hay espacios en el valor del atributo. Si pasamos este HTML al XML nos dará error, pues siempre son obligatorias las comillas. Pero de hecho esto no sucede porque recuperamos el HTML con let html = elementExport.outerHTML y el navegador se encarga de ponerle comillas.

Por último si incluimos un elemento vacío como <input type="text" value="abc" disabled /> que porta el atributo booleano disabled, hay que tener en cuenta que en XML y XHTML todos los atributos deben tener un valor. En HTML-5 este tipo de atributos es de tal forma que si no aparece se considera que toma el valor falso. Y si aparece toma el valor verdadero, independientemente de cual sea el valor, por ejemplo, con <input type="text" value="abc" disabled="false" /> el elemento aparecerá desactivado aunque ponga disabled="false" pues se ignora el valor. En XHTML funciona igual, pero cuando aparece hay que escribirlo como disabled="disabled", repitiendo el nombre en el valor. En cualquier caso al recuperar el outerHTML, a los atributos sin valor el navegador les pone una cadena vacía disabled="" y, por tanto, no se manifestará el error, pues lo que quiere XML es que todos los atributos tengan un valor, aunque sea una cadena vacía.

Figura
Figura Parseador XML

En la herramienta Parseador XML puede verificar cualquier trozo de XHTML, como el que se observa en la Figura, donde usamos la configuración XHTML para verificar los errores que hemos comentado con este trozo de HTML, que está correctamente escrito en HTML-5 pero no en XHTML:

<div id="mydiv">
    <b>HTML</b> to<br>
    <input type="text" value="abc" disabled />
    <span style=color:red>IMG</span>
</div>

Obteniendo el Base64 del XML+SVG

Seguimos obteniendo el Base64 del XML+SVG anterior, lo que puede hacerse usando la función window.btoa():

// btoa() cursará error con caracteres no Latín 1
let b64 = window.btoa(xml);
let data = `data:image/svg+xml;base64,${b64}`;

Sin embargo si lo hacemos así sin más en ciertos casos se genera un error. Para explicar el motivo supongamos que sustituimos la cadena "IMG" por la letra griega "Ω" (Omega) en el trozo de HTML que queremos exportar <span style="color: red">Ω</span> de nuestro SVG. Al ejecutar la exportación acusaremos un error.

Figura
Figura. Error window.btoa()

El error que se observa en la Figura dice que contiene caracteres fuera del rango Latín 1. Se refiere a caracteres que necesitan más de 1 byte (0 a 255 posiciones) para codificarse, como le sucede a la letra griega "Ω". En lugar de escribirla directamente en el HTML, también puede escribirse como &Omega; o bien &#937; donde 937 es el código decimal o bien &#x03A9; donde 03A9 es el código hexadecimal. Se observa que el código decimal es la posición 937 en la lista UNICODE, superando 255 y por tanto necesita 2 bytes para representarlo (0000 0011 1010 1001). Esto se explica en la documentación de la función btoa().

Figura
Figura. UTF-8

Para evitarlo podemos usar la función encodeURIComponent() que escapa a UTF-8 todos los caracteres a excepción de A–Z a–z 0–9 - _ . ! ~ * ' ( ). Esto quiere decir que para la cadena de caracteres "A.Ω.語.𐎄" que vemos en la Figura separados por un punto, resultará encodeURIComponent("A.Ω.語.𐎄") = "A.%CE%A9.%E8%AA%9E.%F0%90%8E%84".

Como se observa en la Figura, una captura del enlace al tema que explica como funciona la codificación UTF-8, a excepción de la "A" y los puntos que se pasan literalmente, la ejecución anterior de encodeURIComponent() resultan los bytes en hexadecimal "CEA9", "E8AA9E" y "F0908E84" para los 3 últimos caracteres, igual que la observada en la imagen.

Observe como la letra "Ω", cuya posición en UNICODE es la 937 en decimal y 03A9 en hexadecimal, al transformarlo a UTF-8 resulta en CEA9. En el tema algoritmos de transformación UTF-8 explicamos como funciona esta transformación.

Figura
Figura. Unicode

Cuando estudié el tema de algoritmos de transformación UTF-8 en el año 2010 los navegadores no soportaban completamente UNICODE. Hoy en día si lo hacen. En cualquier caso si los dos últimos caracteres de la cadena A.Ω.語.𐎄 no se ven en su navegador es porque no están instaladas esas fuentes. La Figura presenta como debería verse esa cadena.

Finalmente la ejecución de encodeURIComponent resulta en una cadena con caracteres de la lista A–Z a–z 0–9 - _ . ! ~ * ' ( ), que se devuelven literalmente, más escapes %YY que representan 1 byte, donde "Y" es un caracter hexadecimal [0-9AF]. Entonces esta cadena final solo contendrá caracteres ASCII de 1 byte de la lista más el caracter de escape "%", con lo que puede ser transmitido por cualquier medio y soportado por cualquier sistema operativo.

Pero btoa() debe recibir caracteres bien formados, no secuencias de escapes URI. En JavaScript cualquier caracter puede codificarse con 0xYY donde "YY" es un caracter hexadecimal [0-9AF]. Así que hacemos lo siguiente:

let b64 = window.btoa(encodeURIComponent(xml).
   replace(/%([0-9A-F]{2})/g, (b0,b1) => String.fromCharCode(`0x${b1}`)));
let data = `data:image/svg+xml;base64,${b64}`;

Se trata de reemplazar el resultado de cada byte tipo %YY que nos devuelve encodeURIComponent() por el String que podemos generar con el escape JavaScript 0xYY usando el método String.fromCharCode(). Tras aplicar esto y btoa() a la cadena de ejemplo A.Ω.語.𐎄 finalmente tendremos QS7OqS7oqp4u8JCOhA== en Base64. Y esa cadena en Base64, que no es otra cosa que la serialización de los bytes del texto A.Ω.語.𐎄, representaría para una imagen XML+SVG una ristra de bytes ya preparada para insertar en un elemento Image como veremos a continuación.

Funciones btoa(), atob(), TextEncoder.encode() y TextDecoder.decode()

Puede probar en la consola del navegador la ejecución anterior de btoa() y encodeURIComponent() para obtener los bytes serializados de la cadena "A.Ω.語.𐎄":

window.btoa(encodeURIComponent("A.Ω.語.𐎄").
replace(/%([0-9A-F]{2})/g, (b0,b1) => String.fromCharCode(`0x${b1}`)));
// QS7OqS7oqp4u8JCOhA==
    

De paso podemos hacer el proceso inverso usando atob(), método que usaremos en el último apartado para descargar la imagen. Con esta ejecución en la consola veremos que se obtienen los bytes de la cadena Base64 "QS7OqS7oqp4u8JCOhA==":

[...window.atob("QS7OqS7oqp4u8JCOhA==")].map(v => v.charCodeAt(0));
// [65, 46, 206, 169, 46, 232, 170, 158, 46, 240, 144, 142, 132]

Con lo anterior se obtienen enteros entre 0 y 255 que podemos presentar como hexadecimales con esto:

[...window.atob("QS7OqS7oqp4u8JCOhA==")].
map(v => v.charCodeAt(0).toString(16).toUpperCase()).join(" ");
// 41 2E CE A9 2E E8 AA 9E 2E F0 90 8E 84

Las funciones btoa() y atob() fueron pensadas para codificar y decodificar respectivamente en Base64. Que, como ya hemos dicho, es una forma de escapar caracteres no Latín 1 (o ASCII extendido de 8 bits) y así poder transmitirlos por una red que no soporte UNICODE. No fueron pensados para extraer bytes de una cadena con caracteres UNICODE, aunque pueda usarse. Más recientemente se incoporan los objetos TextEncoder para extraer los bytes en un ArrayBuffer (un typed array o array tipado). Y por otro lado TextDecoder para recuperarlos desde un ArrayBuffer.

Puede probar en la consola del navegador este código, donde codificamos la cadena "A.Ω.語.𐎄" con TextEncoder, devolviendo un ArrayBuffer Uint8Array, que es como un array de enteros sin signo de 8 bits, lo más adecuado para almacenar bytes:

(new TextEncoder).encode("A.Ω.語.𐎄");
// Uint8Array(13) [65, 46, 206, 169, 46, 232, 170, 158, 
// 46, 240, 144, 142, 132, buffer: ArrayBuffer(13), ...]

Lo valores son números entre 0 y 255. Podemos presentarlos como hexadecimales con esto:

[...(new TextEncoder).encode("A.Ω.語.𐎄")].
map(v => v.toString(16).toUpperCase()).join(" ");
// 41 2E CE A9 2E E8 AA 9E 2E F0 90 8E 84

Una vez que tengamos un ArrayBuffer que represente la cadena UNICODE "A.Ω.語.𐎄" podemos decodificarlo con TexDecoder. En este ejemplo obtenemos el ArrayBuffer y lo guardamos en la variable bytesBuffer para luego decodificarla y volver a obtener esa cadena:

let bytesBuffer = (new TextEncoder).encode("A.Ω.語.𐎄");
(new TextDecoder).decode(bytesBuffer);
// 'A.Ω.語.𐎄'

Y podemos hacer una mezcla entre TextDecoder y atob. En este ejemplo decodificamos con atob() el Base64 "QS7OqS7oqp4u8JCOhA==" componiendo un ArrayBuffer de tipo Uint8Array obteniendo los números de código de cada caracter con codePointAt(), para finalmente decodificar ese array de bytes y obtener la cadena de partida "A.Ω.語.𐎄":

(new TextDecoder).decode(Uint8Array.
from(atob("QS7OqS7oqp4u8JCOhA=="), v => v.codePointAt(0)));
// A.Ω.語.𐎄
    

Recursos adicionales para serializar los bytes de una entrada

En este apartado veremos algunas de las herramientas de este sitio que serializan los bytes de una entrada, aparte de lo que ya se explica en el tema citado Algoritmos de transformación UTF-8.

Figura
Figura Calculadora binaria

Como recursos adicionales para ver los bytes de un texto puede usar la herramienta de este sitio Calculadora binaria tal como se observa en la Figura, con los bytes de la cadena de ejemplo A.Ω.語.𐎄 donde se usa la misma técnica con encodeURIComponent() para extraer los bytes.

Figura
Figura Visor binario

O guardar el contenido A.Ω.語.𐎄 en un archivo TXT chars.txt en UTF-8 en su ordenador y visualizarlo en el Visor binario que también extrae los bytes individuales de cada caracter con encodeURIComponent(), como se observa en la Figura.

Como hemos dicho antes, encodeURIComponent() fue creada para serializar bytes con objeto de que puedan formar parte de una URI (Identificador Uniforme de Recursos), que puede ser una URL (Localizador Uniforme de Recursos) o una URN (Nombre Uniforme de Recursos). Para una aplicación general de serialización de bytes es mejor usar TextEncoder como ya vimos, una interfaz de JavaScript que toma una entrada y devuelve una ristra de bytes que componen el UTF-8. Veamos esto en lo que sigue.

Figura
Figura Gestor ZIP

En la Figura puede ver la herramienta Gestor ZIP, que empaqueta uno o más archivos en un ZIP. Como se observa, empaquetamos en un ZIP el archivo chars.txt que contiene la cadena A.Ω.語.𐎄 codificada en UTF-8. Cada archivo a empaquetar en el ZIP es un objeto con una propiedad content que contiene un array de bytes (Uint8Array) como números decimales entre 0 y 255. Se observa en la imagen que ese array es content: [65, 46, 206, 169, 46, 232, 170, 158, 46, 240, 144, 142, 132].

En esa herramienta en lugar de usar encodeURIComponent() para la serialización de los bytes usamos el constructor TextEncoder. Puede probar en la consola del navegador el siguiente código:

let encoder = new TextEncoder;
let arr = encoder.encode("A.Ω.語.𐎄");
console.log(arr);

Se obtiene ese mismo array, un array tipado Uint8Array que es un Unsigned Integer 8 bits Array, un array de números enteros sin signo:

arr =[65, 46, 206, 169, 46, 232, 
170, 158, 46, 240, 144, 142, 132]

Esto es lo mismo que obteníamos con encodeURIComponent("A.Ω.語.𐎄") = "A.%CE%A9.%E8%AA%9E.%F0%90%8E%84", como se observa si convertimos los valores decimales anteriores en hexadecimales, marcamos el valor Unicode del punto (46 decimal 2E hexadecimal) en resalte amarillo y sabiendo que a "A" le corresponde el 65 en decimal y el 41 en hexadecimal:

41 2E CE A9 2E E8 AA 9E 2E F0 90 8E 84

Incorporar el Base64 en un elemento temporal Image

El siguiente paso es incorporar el Base64 del XML+SVG en un elemento temporal Image:

// 3) Incorporar el Base64 en un elemento temporal Image
let imgTemp = new Image();
imgTemp.width = width;
imgTemp.height = height;
imgTemp.src = data;
imgTemp.decode().then(() => {
    try {
        // 4) Dibujar la imagen temporal en un Canvas
        ...
        // 5) Recuperar imagen del Canvas e insertarla en Base64
        //    en el elemento definitivo de imagen
        ...
    } catch (e) {errorExport.textContent = e.message}
}).catch((msg) => {
    // 6) Gestionar errores
    ...
});

    

Creamos un nuevo elemento temporal let imgTemp = new Image(). Se trata de usar el constructor del elemento <img> para usarlo en JavaScript. Damos ancho y alto y adjudicamos a su atributo src el Base64 que en el paso anterior habíamos guardado en la variable data.

Usaremos el nuevo método decode() que es una promesa para cuando la imagen esté disponible finalizar el proceso. O, en caso de error, gestionar lo que tendremos que hacer. Veamos primero la finalización del proceso:

// 4) Dibujar la imagen temporal en un Canvas
let canvas = document.createElement("canvas");
canvas.width = width;
canvas.height = height;
let context = canvas.getContext("2d");
context.drawImage(imgTemp, 0, 0, width, height);
// 5) Recuperar imagen del Canvas e insertarla en Base64
//    en el elemento definitivo de imagen
let mimeType = `image/png`;
imageBase64 = context.canvas.toDataURL(mimeType);
imageExport.src = imageBase64;
imageExport.width = width;
imageExport.height = height;
imageExport.style.display = "inline";
document.getElementById("text-export").value = imageBase64;

Crearemos un elemento canvas y le damos las mismas medidas de ancho y alto. Obteniendo el contexto en 2D usamos el método drawImage para dibujar la imagen temporal ya disponible, ubicandola en el punto (0, 0) superior izquierda y con ese ancho y alto. Este es el momento clave en que la imagen XML+SVG pasa a ser una verdadera imagen, en principio como un bitmap, es decir, una mapa de bits de puntos.

Recuperamos la imagen en formato PNG (también se permiten otros como JPG y WEBP) desde el contexto del canvas usando el método toDataURL(mimeType), con lo que obtendremos otro Base64 con la imagen en PNG para incorporar en el atributo src del elemento de imagen definitivo, el que aparece en esta página web.

En caso de error al decodificar la imagen lo gestionamos con esto:

// 6) Gestionar errores
let imageExport = document.getElementById("image-export");
imageExport.src = noImageData;
imageExport.width = 186;
imageExport.height = 45;
imageBase64 = noImageData;
document.getElementById("text-export").value = noImageData;
errorExport.textContent = msg;

Inicialmente guardamos en la variable global noImageData el Base64 de una imagen que presentará el texto "No image", aplicándola al elemento imagen que tenemos en la página para presentarlo como aviso de que no hay imagen.

Descargar un archivo de imagen

Otra utilidad que ofrece el ejemplo es decargar el archivo de imagen. Utilizo esto en muchas partes de este sitio, pero creo que aún no lo había comentado. El código para la descarga es el siguiente:

function downloadImage(){
    try {
        errorExport.textContent = "";
        let imageExport = document.getElementById("image-export");
        let content = imageBase64;
        content = window.atob(content.split(/,/)[1]);
        content = [...content].map(v => v.charCodeAt(0));
        content = new Uint8Array(content);
        let fileName = "image.png";
        let link = document.createElement("a");
        link.href = window.URL.createObjectURL(new Blob([content],
            {type: "application/octet-stream"}));
        link.download = fileName;
        link.style.display = "none";
        document.body.appendChild(link);
        link.click();
        document.body.removeChild(link);
    } catch(e) {errorExport.textContent = e.message}
}

En el paso 5) del código del apartado anterior recuperábamos el Base64 del Canvas con la imagen definitiva y la guardábamos en la variable global imageBase64. Lo hacíamos así para ahora poder acceder a esa global para descargar el archivo de imagen, siendo el contenido que vamos a descargar let content = imageBase64. Si antes con window.btoa() serializábamos en bytes un contenido, el método opuesto window.atob() recuperará los bytes de esa serialización. En un apartado anterior vimos un ejemplo que puede ejecutar en la consola del navegador para probar como funciona. Como en imageBase64 teníamos lo siguiente, abreviando contenido con puntos suspensivos:

data:image/png;base64,iVBORw0K...

los bytes de la imagen serializados es todo lo que está a partir de la coma, asi que usando lo siguiente

content = window.atob(content.split(/,/)[1])

obtenemos esos bytes originales. Como cada byte viene a ser un número entre 0 y 255, los convertimos en caracteres usando el método charCodeAt(). Con ese contenido creamos un Array tipado (Typed array) del tipo Uint8Array, que viene a ser un Array de enteros sin signo de 8 bits, que es realmente lo que tenemos: una ristra de bytes.

El siguiente paso es crear un elemento vínculo <a> temporal. Creamos una objeto URL para adjudicarlo a su atributo href así:

link.href = window.URL.createObjectURL(new Blob([content], 
        {type: "application/octet-stream"}));

Se trata de crear un Blob con ese contenido en bytes. Un Blob (Binary Large Object) representa datos de archivo no modificables. Son una serie de bytes sin tratar (raw) y que no se pueden modificar (immutable). Con el tipo application/octet-stream le estamos diciendo que cree un objeto URL del tipo ristra de octetos (bytes) para ser tratados por una aplicación.

Finalmente en el atributo download del vinculo ponemos el nombre del archivo "image.png" (con extensión PNG) y ejecutamos el método click(), lo que hará que se descargue el archivo de imagen. Finalizamos borrando el vínculo temporal.