Abbreviated jpeg в Java

Я думаю все, кто хоть раз запускал компьютер, знают, что такое jpeg. Этот стандарт появился в далёком 1991 году и с тех пор оброс различными расширениями и дополнительными возможностями. Для обычных пользователей все эти изменения не видны. Вы просто кликаете по файлу и видите красивую картинку. Для 99% разработчиков jpeg также представляет собой чёрный ящик. Максимум, с чем может столкнуться среднестатистический разработчик - это оптимизировать размер картинок с помощью jpegtran или сконвертировать один формат в другой с помощью ImageIO.

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

Одним из способов передачи/хранения картинки является “Abbreviated image”. Однако, прежде, чем пытаться понять, что это такое, необходимо вспомнить формат хранения jpeg в обычном файле.

Обычный файл

Если вкратце, то данные в jpeg файле хранятся с использованием маркеров. Маркер - это специальные 2 байта, которые идентифицируют тип блока и данные в нём. Например, структура обычного jpeg файла может быть следующая:

В данном примере, SOI - это маркер начала изображения, DQT - это маркер начала таблицы квантования, SOS - это маркер начала сканирования. Он содержит небольшую метаинформацию и непосредственно данные изображения. EOI - это маркер конца изображения.

В стандарте описано большое количество различных маркеров и их данных. Здесь наиболее интересные блоки - это таблицы квантования и таблицы кодирования хаффмана. В спецификации jpeg описаны рекомендованные значения для этих таблиц. Однако, ничто не мешает в каждом файле иметь свои собственные таблицы. Например, можно подобрать более оптимальные таблицы в зависимости от количества цветов в данном изображении.

Abbreviated jpeg

Идея Abbreviated jpeg достаточно простая: выкинуть все таблицы из файла и хранить только данные изображения.

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

У такого способа хранения есть как свои достоинства, так и недостатки.

К достоинствам прежде всего следует отнести то, что размер файла чуть меньше. Также такие файлы устойчивы к повреждениям. Если отсутствует кусок файла или он повреждён, то достаточно найти следующий маркер RST и продолжить декодирование.

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

Несмотря на это, такой способ хранения и передачи изображений jpeg используется в узкоспециализированных системах. Например, формат ssdv использует Abbreviated image.

Поддержка в Java

Я был сильно удивлён, когда узнал что поддержка такого экзотичного типа хранения jpeg файла поддерживается в java. В официальной документации очень детально описано, что происходит при вызове различных методов API. К сожалению, за целый день пристального вглядывания в документацию, я так и не понял как же работать с таким форматом. Лишь на следующий день я решил бросить это дело и просто почитать исходники. Их оказалось, на удивление, не так много, и дело сдвинулось с мёртвой точки.

Итак, для того, чтобы прочитать такой файл нужно сделать 2 вещи. Во-первых, необходимо задать таблицы, о которых заранее договорились поставщик и получатель.

JPEGImageReadParam param = new JPEGImageReadParam();
param.setDecodeTables(qTables, DCHuffmanTables, ACHuffmanTables);

Во-вторых, необходимо передать эти параметры при чтении из файла:

ImageReader jpgReader = ImageIO.getImageReadersByFormatName("jpg").next();
jpgReader.setInput(new FileImageInputStream(new File("test.jpeg")));
BufferedImage image  = jpgReader.read(0, param);

В результате BufferedImage будет содержать вполне обычное изображение, которое можно сохранить или преобразовать далее.

Несмотря на всё это, API есть куда расти. Например, в JPEGImageReadParam нельзя задать subsampling mode и ColorSpace. А это значит, что они должны быть в самом Abbreviated image. Это в свою очередь сводит на нет, устойчивость к повреждениям файла.