⏱️ Lectura: 12 min

Un JPEG normal se corta cuando la descarga se corta a la mitad. El formato JPEG progresivo, en cambio, permite guardar los datos en capas, de gruesa a fina, para que una imagen a medio cargar ya se vea completa en baja resolución. El desarrollador que firma como Maurycy publicó el 17 de julio de 2026 una técnica que lleva esa idea al extremo: concatenar decenas de imágenes distintas dentro de un mismo archivo .jpg para que, mientras carga, el navegador muestre una tras otra como si fuera un video.

📑 En este artículo
  1. TL;DR
  2. Qué pasó
  3. Contexto e historia
  4. Detalles técnicos del JPEG progresivo
  5. Cómo empezar a probarlo
  6. Impacto y análisis
  7. Qué sigue
  8. Preguntas frecuentes
    1. ¿Esto es una vulnerabilidad de seguridad?
    2. ¿Por qué Chrome deja de mostrar frames después de cierto punto?
    3. ¿Se puede controlar la velocidad de reproducción del video?
    4. ¿Qué diferencia hay entre un scan DC-only y uno progresivo completo?
    5. ¿Qué herramienta se usa para generar estos archivos?
    6. ¿Funciona igual en todos los navegadores?
  9. Referencias

La técnica no usa códecs de video ni contenedores como MP4, ni una sola línea de JavaScript. Solo abusa de un detalle poco conocido del estándar JPEG: cada scan dentro del archivo declara su propio rango de datos, y nada impide que un scan posterior sobrescriba los píxeles que dibujó el anterior.

TL;DR

  • Maurycy publicó el 17 de julio de 2026 una técnica para esconder video dentro de un JPEG progresivo.
  • El truco concatena varias imágenes del mismo tamaño en un solo archivo aprovechando los scans del formato.
  • Chrome renderiza cerca de 90 scans antes de abandonar la carga; Firefox admite más antes de rendirse.
  • Cada scan DC-only guarda solo el bin 0, lo que reduce cada frame a 1/16 de la resolución original.
  • jpegtran, parte de libjpeg-turbo, genera estos scans DC-only con un archivo de especificación de escaneos.
  • No hay forma de controlar el tiempo entre frames: la reproducción depende por completo de la red.
  • El autor publicó merge.c, el código fuente usado para generar los archivos de ejemplo.

Qué pasó

El 17 de julio de 2026, Maurycy publicó en su blog personal un artículo técnico que documenta cómo abusar de las scans del JPEG progresivo para esconder video dentro de un único archivo de imagen. La técnica no requiere codecs de video, ni contenedores como MP4 o WebM: todo el efecto ocurre porque el navegador interpreta el archivo como una imagen JPEG normal, scan tras scan.

El resultado son archivos .jpg que, mientras se descargan (o se cargan artificialmente despacio), muestran una secuencia de imágenes distintas una tras otra, como si fuera una animación. El propio autor demostró el truco con un clip del meme \”Bad Apple\” y con una aplicación interactiva de una sola página, sin CSS ni JavaScript, publicada en badapple.rose.systems.

Secuencia de scans JPEG progresivo superponiéndose en una sola imagen
Cada scan puede redeclarar el mismo rango de bins DCT que uno anterior. Foto de Igor Rodrigues en Unsplash

Contexto e historia

Para entender el truco hay que entender qué hace especial al JPEG progresivo frente al JPEG baseline, el modo clásico. En baseline, los datos de cada bloque de 8×8 píxeles se guardan completos, uno detrás de otro. Si la descarga se corta a la mitad, la mitad inferior de la imagen directamente no aparece.

El modo progresivo resuelve ese problema dividiendo los datos en múltiples scans. Cada scan trae un fragmento de la información de frecuencia (los coeficientes de la Transformada Discreta de Coseno, DCT) para uno o varios canales. El primer scan trae solo el coeficiente DC, básicamente el color promedio de cada bloque, y los siguientes agregan coeficientes AC de frecuencias cada vez más altas hasta completar la imagen. El resultado práctico: una imagen a medio cargar ya se ve completa, aunque borrosa.

JPEG separa el color en tres canales YCbCr en lugar de RGB: Y es la luminancia (brillo) y Cb/Cr son la crominancia (color). El ojo humano es más sensible a los cambios de brillo que a los de color, así que los codificadores suelen guardar Cb y Cr a media resolución, lo que reduce su peso incluso con precisión completa.

ScanCanalesRango de bins DCTPrecisión
0Y, Cb, Cr0-0 (DC)Media (-1 bit)
1Y1-5Cuarto (-2 bits)
2Cb1-63Media
3Cr1-63Media
4Y6-63Cuarto
5Y1-63Media
6Y, Cr, Cb0-0 (DC)Completa
7Cr1-63Completa
8Cb1-63Completa
9Y1-63Completa

Nueve o diez scans de refinamiento progresivo es normal en un JPEG bien codificado. Ahí está la grieta que aprovecha el truco: cada scan declara explícitamente su propio rango de bins DCT y sus propios canales. Nada en el estándar impide que un scan posterior redeclare el rango 0-0 de una imagen completamente distinta, sobrescribiendo lo que el scan anterior ya había dibujado.

Detalles técnicos del JPEG progresivo

Construir el archivo es, según describe el propio autor, sorprendentemente simple: concatenar varias imágenes JPEG del mismo ancho y alto, filtrando los marcadores de inicio y fin de cada una (start-of-image, start-of-frame, end-of-image) para dejar solo los datos de scan. El navegador no distingue un archivo con nueve scans de refinamiento de la misma foto de un archivo con nueve scans que en realidad son nueve fotos distintas: simplemente los procesa en orden y sobrescribe cada rango DCT según lo declarado.

Ese primer intento tiene dos problemas. El primero es de compatibilidad: los navegadores limitan cuántos scans procesan por archivo, aparentemente para evitar un problema al estilo zip bomb. Maurycy midió que Chrome renderiza cerca de 90 scans antes de abandonar la carga, mientras que Firefox tiene más paciencia. Eso da, como mucho, unos 90 frames por archivo si cada frame ocupa un solo scan.

📌 Nota: El límite de scans no está documentado formalmente en el estándar JPEG: es una decisión de implementación de cada navegador para evitar abusos de recursos, similar a los límites contra los zip bombs en descompresores.

El segundo problema es visual: si cada frame ocupa varios scans progresivos (DC más refinamientos AC), aparece un efecto fantasma (ghosting), porque los scans AC están diseñados para refinar datos DC ya dibujados, no para reemplazar una imagen distinta. El resultado se ve borroso, con artefactos del frame anterior superpuestos.

La solución que encontró el autor es más elegante: usar un solo scan DC-only por frame, en modo baseline dentro de un contenedor progresivo. El estándar exige que en modo progresivo un scan no puede mezclar datos DC (bin 0) y AC (bins 1 en adelante) al mismo tiempo, así que el scan progresivo DC-only más pequeño posible ya es, de por sí, una imagen JPEG válida y completa. Como la DCT trabaja sobre bloques de 16×16 píxeles con el submuestreo de crominancia típico, cada frame queda a 1/16 de la resolución original: borroso, pero sin ghosting.

cat > frame.scans <<'EOF'
0,1,2:0-0,0,0;
EOF
jpegtran -scans frame.scans -outfile frame01.jpg entrada01.jpg

El archivo frame.scans le indica a jpegtran que genere un único scan (canales 0, 1 y 2, es decir Y, Cb y Cr) con rango de bins DCT 0-0 y precisión completa. El resultado, frame01.jpg, es una imagen JPEG progresiva válida de un solo scan.

Herramienta jpegtran generando un scan DC-only de un JPEG progresivo
Un scan DC-only ya es, por sí solo, un JPEG progresivo válido. Foto de Anthony Bernardo Buqui en Unsplash

Cómo empezar a probarlo

Para reproducir el truco hace falta jpegtran, parte del paquete libjpeg-turbo. La instalación cambia según el sistema operativo:

# Linux (Debian/Ubuntu)
sudo apt install libjpeg-turbo-progs

# macOS (Homebrew)
brew install jpeg-turbo

# Windows (PowerShell con Chocolatey)
choco install libjpeg-turbo

Con jpegtran instalado, el flujo tiene tres pasos: redimensionar todos los frames de origen al mismo ancho y alto, convertir cada uno a un JPEG DC-only con el comando anterior, y concatenar los datos de scan (sin los marcadores SOI, SOF y EOI intermedios) en un solo archivo final. Un script así automatiza el último paso:

import sys

SOI, EOI = b"\xff\xd8", b"\xff\xd9"

def extraer_scan(datos):
    inicio = datos.index(SOI) + len(SOI)
    fin = datos.index(EOI, inicio)
    return datos[inicio:fin]

def construir_video_jpeg(rutas_frames, salida):
    scans = [extraer_scan(open(ruta, "rb").read()) for ruta in rutas_frames]
    with open(salida, "wb") as out:
        out.write(SOI)
        for scan in scans:
            out.write(scan)
        out.write(EOI)

if __name__ == "__main__":
    construir_video_jpeg(sys.argv[2:], sys.argv[1])

Ejecutado como python3 concat_frames.py salida.jpg frame01.jpg frame02.jpg frame03.jpg, el script produce un único archivo con todos los scans DC-only encadenados. Abrí el resultado con la red limitada desde las DevTools del navegador (throttling) para ver cómo cada scan sobrescribe al anterior.

Para confirmar que un frame quedó como DC-only y no como progresivo completo, se puede inspeccionar con exiftool:

exiftool -EncodingProcess frame01.jpg
# Encoding Process: Progressive DCT, Huffman coding
sequenceDiagram
participant N as Navegador
participant S as Servidor
N->>S: solicita video.jpg
S-->>N: scan 0 (DC, imagen A)
Note over N: dibuja imagen A en baja resolucion
S-->>N: scan 1 (DC, imagen B)
Note over N: sobrescribe con imagen B
S-->>N: scan 2 (DC, imagen C)
Note over N: sobrescribe con imagen C
Note over N,S: la carga continua hasta el limite de scans del navegador

Impacto y análisis

Conviene ser claro sobre qué es esto y qué no: no es una vulnerabilidad de seguridad ni un exploit. Es un uso creativo, no previsto por los diseñadores del estándar, de una característica legítima del JPEG progresivo. El propio Maurycy lo resume diciendo que, más allá de rickrolls poco convencionales y trolling, la técnica no tiene aplicación práctica real, porque no hay forma de codificar información de temporización dentro del archivo: la velocidad de reproducción depende enteramente de qué tan rápido, o lento, llegan los bytes por la red.

Aun así, la técnica ilustra algo interesante sobre el diseño de formatos de archivo: cuantas más capas de flexibilidad tiene un estándar (en este caso, que cada scan redeclare su propio rango de datos), más superficie queda disponible para usos que sus autores originales nunca imaginaron. Es el mismo principio detrás de otros trucos clásicos de polyglot files, archivos válidos en dos formatos a la vez, solo que acá el otro formato es, en la práctica, video improvisado dentro de una imagen fija.

💭 Clave: El límite de scans en Chrome no es un número fijado por el estándar JPEG: es una barrera defensiva del navegador contra archivos que intentan expandirse en recursos de forma desproporcionada.

Qué sigue

Maurycy dejó abierta una pista para seguir explorando el truco: una aplicación interactiva de una sola página, sin CSS ni JavaScript, que aprovecha el mismo principio del JPEG progresivo con contenido cargado dinámicamente en vez de precompilado. El propio autor admite en el artículo que la demo todavía tiene fallas y que la sigue investigando. Queda pendiente ver si algún navegador ajusta el límite de scans, o el manejo de scans redundantes, como respuesta a este tipo de publicaciones, algo que ya pasó antes con otros trucos de polyglot files y parsers laxos de formatos de imagen.

Por ahora, el código fuente que genera estos archivos, merge.c, está publicado y disponible para quien quiera experimentar con sus propias secuencias de frames.

📖 Resumen en Telegram: Ver resumen

Pruébalo vos: instalá libjpeg-turbo, generá tres o cuatro frames DC-only con jpegtran y concatenalos con el script de arriba para ver en tu propio navegador cuántos scans aguanta antes de rendirse.

Preguntas frecuentes

¿Esto es una vulnerabilidad de seguridad?

No. No hay ejecución de código ni fuga de datos: es un uso no convencional de una característica legítima del estándar JPEG progresivo, con fines de curiosidad técnica y trolling.

¿Por qué Chrome deja de mostrar frames después de cierto punto?

Porque limita la cantidad de scans que procesa por archivo, una medida defensiva similar a los límites contra ataques de tipo zip bomb. Maurycy midió el límite de Chrome cerca de 90 scans.

¿Se puede controlar la velocidad de reproducción del video?

No de forma directa. El archivo no lleva información de temporización: la velocidad depende de qué tan rápido el navegador recibe cada scan por la red.

¿Qué diferencia hay entre un scan DC-only y uno progresivo completo?

Un scan DC-only guarda solo el coeficiente de frecuencia cero de cada bloque, lo que da una imagen borrosa a 1/16 de resolución pero sin refinamientos AC posteriores, evitando el efecto de ghosting.

¿Qué herramienta se usa para generar estos archivos?

jpegtran, incluida en libjpeg-turbo, con la opción -scans para definir manualmente el rango de bins DCT y precisión de cada scan.

¿Funciona igual en todos los navegadores?

No. Chrome tolera cerca de 90 scans según las mediciones del autor, mientras que Firefox admite más antes de abandonar la carga, aunque el comportamiento exacto puede cambiar entre versiones.

Referencias

📱 ¿Te gusta este contenido? Únete a nuestro canal de Telegram @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.

Imagen destacada: Foto de Mika Baumeister en Unsplash


Andrés Morales

Desarrollador e investigador en inteligencia artificial. Escribe sobre modelos de lenguaje, frameworks, herramientas para devs y lanzamientos open source. Cubre papers de ML, ecosistema de startups tech y tendencias de programación.

0 Comentarios

Deja un comentario

Marcador de posición del avatar

Tu dirección de correo electrónico no será publicada. Los campos obligatorios están marcados con *

Este sitio usa Akismet para reducir el spam. Aprende cómo se procesan los datos de tus comentarios.