⏱️ 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
- TL;DR
- Qué pasó
- Contexto e historia
- Detalles técnicos del JPEG progresivo
- Cómo empezar a probarlo
- Impacto y análisis
- Qué sigue
- Preguntas frecuentes
- ¿Esto es una vulnerabilidad de seguridad?
- ¿Por qué Chrome deja de mostrar frames después de cierto punto?
- ¿Se puede controlar la velocidad de reproducción del video?
- ¿Qué diferencia hay entre un scan DC-only y uno progresivo completo?
- ¿Qué herramienta se usa para generar estos archivos?
- ¿Funciona igual en todos los navegadores?
- 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.
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.
| Scan | Canales | Rango de bins DCT | Precisión |
|---|---|---|---|
| 0 | Y, Cb, Cr | 0-0 (DC) | Media (-1 bit) |
| 1 | Y | 1-5 | Cuarto (-2 bits) |
| 2 | Cb | 1-63 | Media |
| 3 | Cr | 1-63 | Media |
| 4 | Y | 6-63 | Cuarto |
| 5 | Y | 1-63 | Media |
| 6 | Y, Cr, Cb | 0-0 (DC) | Completa |
| 7 | Cr | 1-63 | Completa |
| 8 | Cb | 1-63 | Completa |
| 9 | Y | 1-63 | Completa |
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.
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
- Regressive JPEGs (maurycyz.com): artículo original con la explicación técnica completa y las demos.
- merge.c: código fuente usado por el autor para concatenar los scans de las imágenes.
- JPEG (Wikipedia): contexto general sobre el formato, sus modos baseline y progresivo, y la codificación DCT.
- libjpeg-turbo (GitHub): repositorio oficial de la librería que incluye jpegtran.
📱 ¿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
0 Comentarios