DEV Community

Cover image for Guardó un sitio web completo en un favicon de 9 9 píxeles
lu1tr0n
lu1tr0n

Posted on • Originally published at elsolitario.org

Guardó un sitio web completo en un favicon de 9 9 píxeles

Un favicon es ese ícono diminuto que vive en la pestaña del navegador. Lo subís una vez y te olvidás de él para siempre. Pero el desarrollador alemán Tim Wehrle se hizo una pregunta incómoda: si un favicon es solo una imagen, y una imagen son píxeles, y los píxeles son bytes… ¿se puede guardar un sitio web en un favicon?

La respuesta es sí. Wehrle logró meter una página HTML completa dentro de un favicon de 9×9 píxeles que pesa apenas 212 bytes. Acá desglosamos cómo lo hizo, qué código hay detrás y por qué este experimento aparentemente inútil dice algo profundo sobre la naturaleza de los datos en la web.

TL;DR

  • Tim Wehrle codificó una página HTML completa dentro de un favicon de 9×9 píxeles usando los canales RGB de cada píxel.- El payload pesó 208 bytes; con una cabecera de 4 bytes para la longitud, el total fue de 212 bytes.- Cada píxel guarda 3 bytes (rojo, verde y azul), así que 71 píxeles bastaron para todo el contenido.- La imagen de 9×9 (81 píxeles) ofrece 239 bytes de capacidad y se usó al 87%.- El navegador recupera el contenido dibujando el favicon en un canvas y leyendo los píxeles con getImageData.- Hace falta un pequeño bootstrap en JavaScript: sin él, el favicon es solo un PNG con ruido visual.- El código es open source en GitHub y hay un demo en vivo con un botón Render Website.

Qué pasó: una web que cabe en su propio ícono

Todo empezó con una obsesión peligrosa. Wehrle ya había escrito antes sobre cómo guardó dos bytes dentro del registro de DPI de su mouse. No era útil ni práctico, pero le hizo algo raro al cerebro: una vez que escondés datos donde no deberían estar, empezás a ver todo como almacenamiento potencial. Un monitor es almacenamiento. Un teclado es almacenamiento. La pantalla de arranque del BIOS, tal vez, también. Y un favicon, por supuesto, también lo es.

El razonamiento es brutal en su simpleza. Cada sitio web tiene un favicon, y un favicon es solo una imagen. Una imagen son píxeles. Y los píxeles son bytes. Entonces, ¿por qué no escribir los bytes de un documento HTML directamente en los píxeles de la imagen? Al navegador no le importa qué representan esos bytes: para él son colores. Para Wehrle, son HTML.

El resultado no se parece en nada a un ícono. Es ruido visual puro, una manchita cuadrada de colores aleatorios. Pero ese ruido, leído con el código correcto, se reconstruye en una página web funcional. El favicon dejó de ser un ícono para convertirse en un medio de almacenamiento.
El favicon resultante parece ruido, pero cada píxel guarda tres bytes de HTML.

Contexto e historia: esto no es exactamente esteganografía

La primera idea de Wehrle fue la esteganografía clásica: ocultar datos dentro de una imagen sin que se note. La técnica tradicional toma una fotografía perfectamente normal y modifica unos pocos bits —típicamente el bit menos significativo de cada canal de color— para que contenga un mensaje secreto sin alterar visiblemente la imagen. Así se han escondido mensajes durante décadas.

Pero este experimento da un paso más radical. Acá el favicon no finge ser un ícono. No hay imagen portadora que disimule nada. La imagen es el dato, sin disfraz. Cada uno de los tres canales de color de cada píxel —rojo, verde y azul— almacena un byte completo del HTML. No se esconden bits sueltos: se escribe el documento entero, byte por byte, como si la imagen fuera un disco diminuto.

Esta distinción importa. La esteganografía busca el sigilo; este truco busca el límite. La pregunta no es "¿puedo ocultar esto sin que se vea?", sino "¿cuál es la cosa más improbable que puede funcionar como almacenamiento?". Y un favicon, ese elemento al que nadie le presta atención, resultó ser un candidato perfecto para meter un sitio web en un favicon de verdad.

💭 Clave: La diferencia entre esteganografía y este experimento es el propósito: una oculta datos en una imagen real; el otro convierte la imagen en almacenamiento puro, sin importar que parezca ruido.

Cómo funciona: del HTML a los píxeles

El proceso de codificación es sorprendentemente directo. Primero, se convierte el HTML en bytes con TextEncoder, la API estándar del navegador para serializar texto a UTF-8. Después, se antepone una cabecera de cuatro bytes que contiene la longitud del payload. Esa cabecera es crítica: como la imagen puede tener píxeles sobrantes al final (relleno), sin un valor de longitud no habría forma de saber dónde termina el contenido real y dónde empieza la basura.

Con el arreglo de bytes listo, se empiezan a llenar píxeles. El primer byte se convierte en el canal rojo del primer píxel. El segundo byte, en el canal verde. El tercero, en el canal azul. Luego el siguiente píxel. Y el siguiente. Y el siguiente. Hasta que todo el documento HTML existe como una secuencia de colores. Acá está la lógica de codificación en JavaScript:

function codificarHTMLaBytes(html) {
  const bytes = new TextEncoder().encode(html);
  const longitud = bytes.length;

  // Cabecera de 4 bytes con la longitud (big-endian)
  const cabecera = new Uint8Array(4);
  cabecera[0] = (longitud >>> 24) & 0xff;
  cabecera[1] = (longitud >>> 16) & 0xff;
  cabecera[2] = (longitud >>> 8) & 0xff;
  cabecera[3] = longitud & 0xff;

  // payload = cabecera + contenido HTML
  const payload = new Uint8Array(4 + bytes.length);
  payload.set(cabecera, 0);
  payload.set(bytes, 4);

  // estos bytes se escriben de a 3 en cada pixel (R, G, B)
  return payload;
}
Enter fullscreen mode Exit fullscreen mode

Este flujo es fácil de visualizar como una tubería de transformaciones, primero al escribir y después al leer:

graph LR
  A["HTML"] --> B["TextEncoder: bytes"]
  B --> C["cabecera 4 bytes + payload"]
  C --> D["pixeles RGB"]
  D --> E["favicon PNG"]
  E --> F["canvas getImageData"]
  F --> G["leer longitud"]
  G --> H["TextDecoder: HTML"]
Enter fullscreen mode Exit fullscreen mode

El mismo flujo en reversa permite leer el sitio de vuelta desde la imagen.

Datos y cifras: lo más impactante fue el tamaño

Lo que más sorprendió a Wehrle no fue que el truco funcionara, sino lo pequeño que quedó todo. El payload final fue de 208 bytes de HTML. Sumando la cabecera de 4 bytes, el total llegó a 212 bytes. Como cada píxel almacena tres bytes, hacían falta exactamente 71 píxeles para contener todo.

El cuadrado más pequeño que cabe 71 píxeles es uno de 9×9, que da 81 píxeles. Eso significa una capacidad total de 239 bytes (81 × 3), de los cuales se usaron 212: un 87% de aprovechamiento. Las cifras finales quedaron así:

  • Payload: 208 bytes de HTML.- Tamaño de imagen: 9×9 píxeles.- Capacidad total: 239 bytes.- Uso: 87%.

El detalle desconcertante es que un sitio web completo —HTML con algo de estilo incluido— terminó cabiendo en una imagen más liviana que un favicon convencional. La mayoría de los favicons que cargamos a diario pesan más que este pequeño cuadrado que contiene una página entera.

📌 Nota: 9×9 píxeles es más chico que el favicon estándar de 16×16 o 32×32. El "sitio" entero ocupa menos espacio que el ícono que normalmente lo representaría.

Cómo se lee de vuelta: el rol del canvas

Guardar los datos es solo la mitad del problema. La otra mitad es recuperarlos. Y acá los navegadores ya traen todo lo necesario. El favicon se carga como imagen, se dibuja sobre un elemento <canvas> y la API del canvas le permite a JavaScript leer cada píxel con getImageData.

Una vez que tenemos los datos de los píxeles, el proceso se invierte: se leen los valores RGB, se reconstruye el arreglo de bytes, se leen los primeros cuatro bytes para determinar la longitud del payload, se extrae el contenido y se decodifica el texto UTF-8. En ese punto recuperamos el HTML original. Así se ve la decodificación:



async function decodificarSitioDesdeFavicon(url) {
  const img = new Image();
  img.crossOrigin = 'anonymous';
  img.src = url;
  await img.decode();

  const canvas = document.createElement('canvas');
  canvas.width = img.width;
  canvas.height = img.height;
  const ctx = canvas.getContext('2d');
  ctx.drawImage(img, 0, 0);

  const { data } = ctx.getImageData(0, 0, img.width, img.height);

  // data viene en RGBA: ignoramos el canal alfa (cada 4to byte)
  const bytes = [];
  for (let i = 0; i  **⚠️ Ojo:** El canvas devuelve los píxeles en formato RGBA y puede aplicar *alpha premultiplication*. Si el canal alfa baja de 255, los valores RGB se corrompen silenciosamente. Para que esto funcione, el alfa de cada píxel debe quedar fijo en 255.

## Impacto y análisis: el detalle que casi nadie nota

Hay una trampa importante que vale la pena entender bien. El favicon no contiene "el sitio web" en el sentido completo: contiene el *contenido* de un sitio web. Todavía hace falta un pequeño bootstrap loader en JavaScript que sepa decodificar la imagen. Sin ese código, el favicon es solo un PNG con ruido. Por eso el demo incluye un botón "Render Website" que lee el favicon, decodifica el HTML y reemplaza la página con el contenido reconstruido.

Esta distinción es la que da sentido al experimento. La página real que ves —la que tiene el botón, el loader y la lógica— vive en un servidor normal. Lo que vive en el favicon es la carga útil: el documento que el loader inyecta. Es la diferencia entre tener un programa y tener los datos que ese programa procesa.

¿Es útil? No, para nada, y Wehrle lo admite sin rodeos. La cantidad de datos que podés guardar es minúscula. La página necesita JavaScript para arrancarse a sí misma. Y existen docenas de formas mejores de distribuir un documento HTML pequeño. Pero esa nunca fue la cuestión. El punto era probar los límites. Un favicon parece una cosa muy específica —se supone que es un ícono— pero al final no es más que un PNG, y un PNG es básicamente solo bytes. Guardar un sitio web en un favicon es, según Wehrle, probablemente el sitio más pequeño que jamás construyó.

Para desarrolladores en LATAM, el valor de este tipo de ejercicios es pedagógico. Te obliga a desarmar abstracciones que dabas por sentadas: qué es realmente una imagen, qué hace `TextEncoder`, cómo el canvas te da acceso crudo a los píxeles. Entender que "todo es bytes" no es un cliché: es la base para razonar sobre serialización, formatos de archivo, codificación de caracteres y hasta vulnerabilidades de seguridad donde datos disfrazados de imágenes esconden algo más.

## Qué sigue: otras formas de hacer lo mismo

El propio Wehrle sugiere enfoques alternativos que valen la pena explorar si querés experimentar. El primero es usar un favicon en formato SVG y guardar el marcado directamente dentro del XML del SVG, leyéndolo al cargar la página. Como el SVG es texto, ni siquiera hace falta codificar a píxeles.

El segundo es aprovechar los *chunks* de comentario del formato PNG —`tEXt`, `zTXt` e `iTXt`— que están diseñados precisamente para guardar metadatos de texto dentro de la imagen sin tocar los píxeles visibles. El tercero es usar el formato ICO, que permite incluir varios íconos de distintas resoluciones en un mismo archivo, multiplicando el espacio disponible.

Cada variante es un ejercicio distinto sobre el mismo principio: los formatos de archivo son contenedores flexibles, y casi todos tienen rincones donde se pueden meter bytes que no esperabas. La próxima vez que subas un favicon, vas a saber que ese cuadradito puede ser mucho más que un ícono.

> **💡 Tip:** Si solo querés guardar texto dentro de un PNG sin volverlo ruido visual, los chunks `tEXt` son la opción más limpia: mantienen la imagen intacta y son estándar del formato.

📖 Resumen en Telegram: [Ver resumen](#)

## Preguntas frecuentes

### ¿Esto es lo mismo que la esteganografía?

No exactamente. La esteganografía clásica oculta datos dentro de una imagen normal modificando pocos bits para que no se note. En este experimento la imagen no disimula nada: cada canal RGB de cada píxel guarda un byte completo del HTML, y el resultado parece ruido visual puro. Es almacenamiento directo, no ocultamiento.

### ¿El favicon realmente contiene todo el sitio web?

Contiene el contenido HTML, pero no el código que lo decodifica. Hace falta un pequeño bootstrap en JavaScript que lea el favicon, lo dibuje en un canvas, extraiga los píxeles y reconstruya el documento. Sin ese loader, el favicon es solo un PNG con colores aleatorios.

### ¿Cuántos datos se pueden guardar así?

Muy pocos. En el experimento, una imagen de 9×9 píxeles ofrece 239 bytes de capacidad (81 píxeles × 3 bytes por canal RGB). El sitio de demostración usó 212 bytes, un 87%. Para más capacidad habría que usar una imagen más grande, lo que la haría más pesada que cualquier favicon razonable.

### ¿Por qué se necesita una cabecera de 4 bytes?

Porque la imagen puede tener píxeles de relleno al final. La cabecera almacena la longitud exacta del payload, así el decodificador sabe dónde termina el contenido real y dónde empieza la basura sobrante. Sin ese dato no habría forma de cortar correctamente el HTML.

### ¿Hay algún problema técnico al leer los píxeles?

Sí: el canvas devuelve los datos en formato RGBA y puede aplicar premultiplicación de alfa. Si el canal alfa de un píxel baja de 255, los valores de rojo, verde y azul se corrompen. Por eso, al codificar, el alfa de cada píxel debe quedar fijo en su valor máximo.

### ¿Sirve para algo en un proyecto real?

No para producción. La capacidad es minúscula y depende de JavaScript para funcionar. Su valor es educativo: ayuda a entender que las imágenes son solo bytes, cómo funciona la codificación UTF-8 y cómo el canvas da acceso crudo a los píxeles. Es un ejercicio para razonar sobre formatos de archivo y serialización.

## Referencias

- [Tim Wehrle — I Stored a Website in a Favicon](https://www.timwehrle.de/blog/i-stored-a-website-in-a-favicon/) — artículo original que explica el experimento paso a paso.- [timwehrle/favicon (GitHub)](https://github.com/timwehrle/favicon) — código fuente completo de la codificación y decodificación.- [Demo en vivo del sitio en un favicon](https://www.timwehrle.de/labs/favicon-site/) — incluye el botón Render Website para ver la reconstrucción.- [MDN — getImageData()](https://developer.mozilla.org/en-US/docs/Web/API/CanvasRenderingContext2D/getImageData) — documentación de la API del canvas usada para leer los píxeles.- [MDN — TextEncoder](https://developer.mozilla.org/en-US/docs/Web/API/TextEncoder) — referencia de la API que convierte texto a bytes UTF-8.

📱 **¿Te gusta este contenido?** Únete a nuestro canal de Telegram [@programacion](https://t.me/programacion) donde publicamos a diario lo más relevante de tecnología, IA y desarrollo. Resúmenes rápidos, contenido fresco todos los días.
Enter fullscreen mode Exit fullscreen mode

Top comments (0)