Одно из удобных свойств файлов JPEG заключается в том, что в них можно опционально сохранять первыми низкочастотные компоненты. Это означает, что частично скачанное изображение будет отображаться в низком разрешении, а не обрезаться снизу.

Внутри файла это реализуется разбиением сжатых данных на несколько «сканов», перед каждым из которых добавляется заголовок. Вот первый скан изображения:

FF DA - Маркер "начало скана"
00 0C - Поле длины в big endian (12 байт); включает и свою длину
03 - Количество каналов в скане (3)
  01 - Глобальный id первого включённого канала
  00 - Индекс 1 таблицы Хаффмана (DC: 0, AC: 0)
  02 - Глобальный id второго включённого канала
  10 - Индекс 2 таблицы Хаффмана (DC: 1, AC: 0)
  03 - Глобальный id третьего включённого канала
  10 - Индекс 2 таблицы Хаффмана (DC: 0, AC: 0)
00 - Начальный коэффициент DCT (DC)
00 - Конечный коэффициент DCT (тоже DC)
01 - Точность: половинная, предыдущие данные отсутствуют.

f8ad 512d d3f1 cd96 - Зашифрованные кодом Хаффмана коэффициенты DCT
bcb0 58df 53d5 5d97
[...и так далее]

В него включён самый низкочастотный коэффициент (DC‑компонент) преобразования Фурье для всех трёх цветовых каналов.

В качестве трёх цветовых каналов вместо привычного RGB используется YCbCr. Яркость (Luminance, Y) отделена, потому что должна иметь высокое качество, а цвета можно передавать кое‑как и они всё равно будут выглядеть неплохо.

Очень приблизительно: Y = G, Cb = B — G, Cr = R — G

После этого блока файл содержит ещё восемь сканов, добавляющих остальную часть данных:

Номер скана

Каналы

Диапазон коэффициентов DCT

Точность

0

Y Cb Cr

0 — 0

Половинная (-1 бит)

1

Y

1 — 5

Четвертная (-2 бита)

2

Cb

1 — 63

Половинная

3

Cr

1 — 63

Половинная

4

Y

6 — 63

Четвертная

5

Y

1 — 63

Половинная

6

Y Cr Cb

0 — 0

Полная

7

Cr

1 — 63

Полная

8

Cb

1 — 63

Полная

9

Y

1 — 63

Полная

Скан 0 содержит превью изображения очень низкого разрешения.

Скан 1 добавляет деталей яркости.

Сканы 2–5 содержат полные данные низкой точности.

Скан 4 имеет необычный диапазон спектра, потому что заполняет пробелы, оставленные сканом 1. Благодаря этому скану 5 будут доступны все данные четвертной точности, которые можно дополнять.

Сканы 6–9 добавляют оставшиеся части, обеспечивающие полное качество изображения.

Примечание

Я сказал, что цвет не так важен, поэтому может показаться странным, что в моём примере цветовые данные идут первыми. Эта система работает, потому что цветность сохраняется с половинным разрешением (четвертью количества пикселей). В результате полные данные цветности (Cr + Cb) весят вдвое меньше данных яркости.

Так как каждый скан в явном виде задаёт свой диапазон спектра, есть возможность создания файла JPEG, в котором будущие сканы переписывают уже отрендеренные данные изображения.

На самом деле, сделать это очень легко: достаточно конкатенировать несколько изображений с одинаковым разрешением и отфильтровать маркеры начала изображения, начала кадра и конца изображения. Это можно сделать в шестнадцатеричном редакторе, но я написал для этого небольшую программу на C.

При загрузке по медленному каналу этот конкатенированный файл будет переключаться между несколькими изображениями:

An image of a cat that switches between a cat sitting in grass and a different one on concrete as it loads

[Прим. пер.: похоже, Хабр не поддерживает прогрессивные JPEG, поэтому я добавил анимированные GIF]

Однако большинство декодеров после определённого количества сканов сдаются: наверно, это сделано, чтобы избежать проблемы в стиле zip‑бомбы... но из‑за этого такая система не работает более чем с девятью кадрами, чего недостаточно для анимации.

Для этого мне придётся минимизировать количество сканов в каждом кадре. Проще всего начать с базовых JPEG, имеющих только один скан.

Но это не сработало: в прогрессивном режиме скан не может содержать одновременно данные и AC (компоненты выше 0), и DC (компонент 0). Этого ограничения нет для базового режима, но декодер базового изображения прекращает работу после первого скана.

Так как данные AC должны следовать за данными DC, наименьший возможный «прогрессивный» JPEG содержит один скан только с DC. Так как DCT выполняется блоками 16×16, это будет 1/16-я от исходного разрешения.

Номер скана

Каналы

Компоненты DCT

Точность

0

Y Cb Cr

0 — 0

Полная

В этом случае я могу заставить Chrome отрендерить примерно 90 кадров, прежде чем он сдастся. Другие браузеры, например Firefox, более терпеливы, но изображение из 90 сканов работает как будто почти везде.

Кроме того, это позволяет избежать гостинга в наивном решении: оно возникало, потому что сканы AC должны уточнять предыдущие данные. Обычно это позволяет изображениям содержать несколько уровней точности, не увеличивая при этом размер файла... но плохо сочетается с моими трюками.

Если файл содержит только сканы DC без прогрессии, то это не проблема.

Так как кадр с одним DC отвечает стандартам изображения, для его создания не требуется ничего особенного:

cat > frame.scans<<EOF
# только скан DC:
0,1,2:0-0,0,0;
# и ничего больше
EOF
jpegtran -scans frame.scans -outfile out.jpg in.jpg

Благодаря этому можно упаковать в одно изображение целое видео:

A 'video' of a black cat walking towards the camera.

За исключением необычного рикроллинга и других возможностей троллинга, у этой методики нет практичных способов применения: мы никак не можем добавить информацию о таймингах, поэтому скорость воспроизведения целиком зависит от сетевых задержек.

Однако можно здорово поразвлечься с частичным рендерингом:

Вот видео на чистом HTML с использованием только тегов <dialog>: badapple.rose.systems

Разумеется, нет никаких правил о жёстко прописанных данных: можете посмотреть интерактивное одностраничное приложение без CSS и JavaScript.

Дополнительная информация