Pull to refresh

Графический формат JNG — чем полезен, как устроен, чем сконвертить, посмотреть и загрузить

Reading time 9 min
Views 17K
На картинке изображенна турецкая снайперская винтовка с очень подходящим названием — JNG. В статье, как вы уже догадались — речь пойдёт о графическом формате JNG, а отнюдь не об оружии. На хабре уже мелькали темы, касающиеся этого формата, однако их было не много, а некоторые, к сожалению, впоследствии были удалены авторами. Не смотря на то, что JNG не особо популярный формат и базируется на формате MNG, который, судя по всему, можно считать мёртворождённым, у JNG есть одно очень хорошее свойство – это высокая степень сжатия графики с потерями плюс начиие альфа канала.
По сути JNG представляет из себя подвид формата MNG (однако со своим маркером в заголовке, позволяющим отличать оба этих формата). Цветовые данные сохраняются в JPEG формате, а вот альфа может хранится в одном из двух вариантов – либо тоже сжатая при помощи JPEG, как картинка в оттенках серого, либо используя такое же как в PNG — сжатие без потерь.
Где может пригодится JNG? Для меня он больше всего подошёл для хранения текстурных атласов в мобильных играх. Небольшой пример – исходный набор графики от игры весил 57 мегабайт, после замены всех png на jng – набор графики стал весить 15 мегабайт. Неплохой выигрыш для мобильной игры. Поиск других областей, где можно применить jng оставлю на усмотрение читателя, я же опишу чем смотреть, чем создавать а также как грузить (с примерами кода на C/C++) картинки в формате JNG, а также немного теории об его устройстве.



Чем просматривать JNG картинки?


— XnView (http://www.xnview.com/) – позволяет открывать JNG после установки плагина.
— IrfanView (http://www.irfanview.com/) – также требует плагин (поставляется в паке вместе с кучей других форматов).

Как сохранить графику в формат JNG?


Во первых есть плагин к фотошопу — download.fyxm.net/JNG-Format-8069.html. Похоже он не особо развивается, но тем не менее свою работу выполняет.
Во вторых можно воспользоваться довольно известной опенсорсной утилитой – ImageMagic (http://www.imagemagick.org)
Разбираясь с форматом JNG я написал простой конвертер, позволяющий конвертировать png в jng — code.google.com/p/png2jng/downloads/list
(конвертер собран под windows, однако после небольшого напилинга, касающегося замены функций из <tchar.h> может быть собран под любую другую платформу).
Из минусов моей программы:
– так и не допилил 16 битные png/jng (у меня не было необходимости поддерживать такую битность);
— не используется фильтрация данных альфа канала перед сжатием. Потенциально добавив поддержку PNG фильтров можно достичь лучшей степени сжатия альфа канала.

Как загружать JNG?


libmng

Использовать libmng sourceforge.net/projects/libmng
Плюсы:
— достаточно довно существует;
— может грузить не только jng но и анимированные mng;
— там же есть код для создания mng/jng картинок.
Минусы:
— большая и тяжеловесная библиотека
— довольно таки запутанное API для загрузки графики, рассчитанное на анимации;
— невозможность чтения картинки по строкам;
— достаточно прожорлива по памяти.
Также libmng требует наличие jpeglib и zlib.
Если вы решили использовать libmng – вот небольшой пример по загрузке графики с её помощью.
Целиком весь исходник примера вы можете найти здесь — code.google.com/p/png2jng/source/browse/trunk/jngdump/jngdump.cpp (как часть png2jng)
Здесь же опишу ключевые моменты.

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

struct mng_info
{
	FILE* file;//файл из которого читаем

	mng_uint32       width;
	mng_uint32       height;
	mng_uint32       timer;

	unsigned char* pixels;
};


Итак – инициализация libmng

/*Первым параметром можно передать указатель на любые пользовательские данные, которые будут подвешены под mng_handle.
Далее передаются указатели на функции для выделения и освобождения памяти, а также на функцию для вывода логов.
Важный момент – libmng не зануляет своиструктуры, поэтом ваш аллокатор должен делать это сам, иначе вы получите весьма странные не стабильные багги (как вариант – используйте calloc, ну или memset после своего аллокатора).
 */
	mng_handle hmng;
	mng_info mngInfo;
	mng_retcode retCode;

hmng = mng_initialize(&mngInfo, mymngalloc, mymngfree, MNG_NULL);

/*Устанвавливаем набор колбэков*/

/*Обработчик ошибок*/
      Mng_setcb_errorproc(hmng, mymngerror);
/*Открытие потока с исходными данными картинки. Может быть пустым, если вы заранее его открыли.*/
        mng_setcb_openstream(hmng, mymngopenstream);
/*Закрытие потока*/
        mng_setcb_closestream(hmng, mymngclosestream);
/*чтение данных*/
        mng_setcb_readdata(hmng, mymngreadstream);
/*Обработка заголовка – вот здесь можно получить всю информацию о загружаемой картинке, выделить буфер, который будет хранить декодированные пиксели*/

        mng_setcb_processheader(hmng, mymngprocessheader);
/*Данная функция позволяет libmng получить указатель на строку в выходном буфере изображения*/

        mng_setcb_getcanvasline(hmng, mymnggetcanvasline);
/*Получение прошедшего времени. Используется для анимаций, но указатель на ней всё равно надо установить, иначе будет ошибка. В нашем случае это заглушка – пустышка. */
        mng_setcb_gettickcount(hmng, mymnggettickcount);
/*Установка таймер. Также для анимаций, и снова у нас бесполезная заглушка. */
       mng_setcb_settimer(hmng, mymngsettimer);
/*Данным колбэком libmng уведомляет программу, что необходимо перерисовать определённую область картинки. В сулчае если мы сразу грузим в буфер, возвращая финальные указатели в getcanvasline , то тут можно ни чего не делать.*/
       mng_setcb_refresh(hmng, mymngimagerefresh);



Из колбэков приведу здесь лишь те, что представляют наибольший интерес

/*Чтение данных из файлов*/
static mng_bool mymngreadstream(mng_handle mng, mng_ptr buffer, mng_uint32 size, mng_uint32 *bytesread)
{
	mng_info *mymng = (mng_info*)mng_get_userdata(mng);
	*bytesread = fread(buffer, 1, size, mymng->file);

	return MNG_TRUE;
}

mng_bool mymngprocessheader (mng_handle hHandle, mng_uint32 iWidth, mng_uint32 iHeight)
{

	mng_info* mngInfo = (mng_info*)mng_get_userdata (hHandle);

/*устанавливаем желаемый формат пикселей*/
	if (mng_set_canvasstyle (hHandle, MNG_CANVAS_RGBA8))
	{
/*обрабатываем ошибку*/
		return MNG_FALSE;
	}

	mngInfo->width = iWidth;
	mngInfo->height = iHeight;

	mngInfo->image = new unsigned char[iWidth * 4 * iHeight];//Выделяем память для пикселей 
	return MNG_TRUE;
}

mng_ptr mymnggetcanvasline(mng_handle hHandle, mng_uint32 iLinenr)
{
	mng_info* mngInfo = (mng_info*)mng_get_userdata (hHandle);
	/*вычисляем указатель на запрошенную строку*/
	return mngInfo->image + mngInfo-> width  * 4 * iLinenr;
}



Загрузка картинки.

/*Далее по коду я опускаю код проверок на ошибки.*/
/*Данная функция вычитывает и парсит все чанки из mng файла*/
retCode = mng_read(hmng);

/*проверяем – действительно ли мы открываем JNG. Наш код не рассчитан на полноценную работу с MNG*/
if(mng_get_sigtype (hmng) != mng_it_jng)
{
/*ошибка – неверный тип файла*/
}



Теперь собственно нам необходимо получить декодированные пиксели.
До сих пор всё несколько напоминало такие библиотеки как libng или jpeglib.
Далее идёт кардинальное отличие, так как libmng изначально расчитанно на анимированные картинки.


/*циклически вызываем mng_display_resume пока libmng не отобразит все пиксели из картинки*/
retCode = mng_display(hmng);
while((retCode == MNG_NEEDTIMERWAIT) && (mngInfo.timer <= 1))
{
	retCode = mng_display_resume(hmng);
}

/*по завершению цикла – декодированные пиксели лежат в pixels нашей структуры mng_info*/



Далее остаётся только очистить ресурсы, выделенные libmng

mng_cleanup(&hmng);


Ну и не забываем закрыть наши потоки, освободить память и т.п.

Как видите ни чего сложного. Однако в моём случае одна особенность libmng оказалась фатальной, и заставила меня заняться велосипедостроительством.
Дело в том, что libmng при декодировании картинки у себя внутри выделяет буфер равный width * bpp * height. В моём мне надо было загрузить текстуру 2048x2048 в формат 5551. Сам libmng такой формат не поддерживает. Получается что мне тоже пришлось выделять буфер 2048*2048*4, грузить в него, а дальше перегонять в двухбайтный формат. Получился оверхэд в 32 мегабайта (плюс вес самого файла картинки, т.к. он достаётся из архива)! В какой-то момент у игры просто кончилась память. Поковыряв весьма запутанные внутренности libmng я понял что с наскоку одолеть эту проблему я не смогу и решил посмотреть в сторону – а не смогу ли я сам загрузить картинку, без помощи libmng, тем более, что сам формат не выглядел особо страшным, а на хабре уже была статья (к сожалению уже её удалили) где проделывали подобный трюк, но только в броузере.

libjng


Так я родил libjng ( code.google.com/p/libjng ) – простую библиотеку, которая только и умеет, что загружать jng. Библиотека – это слишком громкое слово. На самом деле она состоит из одного заголовочного файла и одного файла – исходник на C.

Плюсы:
— простое в использовании API;
— компактная – исходник с заголовочным файлом — 74 килобайта (бинарник немногим больше);
— позволяет загружать альфа канал без изменений битности (удобно, когда надо грузить в форматы типа 5551);
— и главное – позволяет декодировать файл по строкам, как libpng и jpeglib;
— может декодировать картинку сразу из буфера, не дублируя данные, а можно выгружать блоки из потоков, установив колбэки.
Недостатки:
— не умеем выдавать данные в 16 битах на канал (хотя в теории загрузить jng где JPEG будет иметь 12 бит на сэмпл, или альфа будет 16 бит – должно получится)
— тоже зависим от сторонних библиотек – jpeglib, zlib.

В месте с библиотекой в репозитарии лежит небольшой пример, позволяющий загрузить jng и сдампить пиксели в tga формат (т.к. это самый простой формат графики).

Краткий пример использования libjng (вариант использования, когда уже вся картинка загружена в память).


/*подключаем заголовочный файл*/
#include "jng_load.h"

/*определяем колбэки – их минимум*/

void* my_jng_alloc(size_t size)
{
	return malloc(size);
}

void my_jng_free(void* ptr)
{
	free(ptr);
}

void my_jng_errorproc(jng_handle handle, int errorCode, unsigned long chunkName, unsigned long chunkSeq, const char* erorText)
{
	printf("JNG error %d! chunk = %d, chunkSeq = %d, text = %s\r\n", errorCode, chunkName, chunkSeq, erorText);
}

/*
imageSize – размер буфера, содержащего данные картинки
imageBuffer – буфер с загруженными данными
*/

jng_handle handle;

handle = jng_create_from_data(NULL, imageBuffer, imageSize, my_jng_alloc, my_jng_free, my_jng_errorproc, JNG_FLAG_CRC_CRITICAL);/*флагами говорим что надо проверять crc только у критических чанков.*/

if(!handle)
{
	/*
Ошибка создания хэндла – или передали неверные параметры, или данные.
Код ошибки был скинут в errorproc
*/
}


/*Вычитываем и парсим чанки. Проверяем их целостность, порядок, наличие всех необходимых чанков.*/

if(!jng_read(handle))
{
	jng_cleanup(handle);
/*обрабатываем ошибку*/
}

/*получаем информацию о картинке*/
imageWidth = jng_get_image_width(handle);
imageHeight = jng_get_image_height(handle);

/*настраиваем формат пикселей, в которых хотим получать результат*/

jng_set_out_alpha_channel_bits(handle, JNG_OUT_BITS_8);
jng_set_out_color_channel_bits(handle, JNG_OUT_BITS_8);
jng_set_out_color_space(handle, JNG_RGB);
jng_set_color_jpeg_src_type(handle, JNG_JPEG_SRC_DEFAULT);/*потенциально, в будущем можно будет выбирать между 8 и 12 битными jpeg картинками – JNG может содержать сразу оба варианта*/


/*инциализируем декодер – создаётся декодер(ы) для jpeg и для png, если таковой нужен*/

if(!jng_start_decode(handle))
{
	jng_cleanup(handle);
/*ошибка*/
}


/*вычисляем размер буфера под картинку*/

colorBytesNum = jng_get_out_color_channel_bytes(handle);
alphaBytesNum = jng_get_out_alpha_channel_bytes(handle);
colorComponentsCount = jng_get_out_color_components_num(handle);
if(!colorBytesNum || !colorComponentsCount)
{
	jng_finish_decode(handle);
	jng_cleanup(handle);
/*и снова ошибка?!*/
}
/* вообще значение bpp можно было бы захардкодить, т.к. мы заранее настраиваем определённый формат пикселя * /
pixelBytesNum = colorBytesNum * colorComponentsCount + alphaBytesNum;
rowPitch = pixelBytesNum * imageWidth;
pixelBufferSize = rowPitch * imageHeight;/*финальный размер буфера для пикселей*/

/*
Выделяем выходной буфер.
Если, к примеру, надо будет грузить в 5551 и нет смысла выделять буфер с 32х битными пикселями под размер всей картинки – достаточно выделить буфер под одну строку, грузить построчно и конвертировать в нужный формат. Таким образом оверхэд будет только на размер одной строки.*/

pixelBuffer = (unsigned char*)malloc(pixelBufferSize);



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


unsigned char* imageLinePtr = (unsigned char*)pixelBuffer;
for(currentLine = 0; currentLine < imageHeight; currentLine++)
{
	if(!jng_read_scanline(handle, imageLinePtr))
	{
/*ошибка при попытке вычитать строку*/
		readRes = -1;
		break;
	}
	imageLinePtr += rowPitch;
}


/*освобождение ресурсов*/


jng_finish_decode(handle);
jng_cleanup(handle);

/*That’s All, Folks!*/



Вкратце, как это устроенно.


Формат JNG, как и MNG состоит из набора чанков (кусков). Каждый чанк содержит заголовок – 4-е байта — размер данных, 4-е байта — идентификатор типа, опционально данные и в конце 4-е байта — crc. Как видим вычитать и распарсить такую структуру – задача элементарная.

Нам нужно разбирать такие чанки:
JHDR – залоговок файла – описывает размер картинки, форматы сжатия, битность и прочее;
JDAT – чанк сожержащий цветовые данные картинки. Обычный JPEG (если выгрызти этот блок в хекс редакторе в отдельный файл, то можно прекрасно открывать его в любом просмотрщике);
JSEP – разделяет чанки JDAT, если их в картинке лежат одновременно восьми и двенадцати битные JPEG картинки (между такими чанками может появлятся только JSEP, но не какие другие данные);
JDAA – альфа канал, пожатый в JPEG – тут всё также как и с JDAT, но только может быть один, восьмибитный JPEG в оттенках серого;
IDAT – альфа канал, запакованный также, как пакуются пиксели в PNG.
IEND – конец картинки.

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

С данным в jpeg всё просто – тут настраивается jpeglib и далее декдоируем построчно данные через jpeglib. С IDAT несколько сложнее. libpng мы не прикрутим, т.к. это не полноценная png, а по сути лишь один из чанков из png.
Сами данные в IDAT лежат построчно, запакованны они при помощи zlib. Строка начинается с байта-маркера, означающего тип фильтрации, а далее сами данные. Таким образом декодирование сводится к тому, что мы распаковываем строку (zlib inflate), проверяем первый байт, фильтруем (это довольно таки обьёмная тема и кому интересно — советую заглянуть внутрь pnglib в файл pngrutil.c, функцию png_read_filter_row), далее если в одном байте упаковано более одного пикселя – распаковываем их в отдельные байты и при надобности расширяем до байта или слова.
Некоторые фильтры требуют наличия данных из предыдущей строки – поэтому добавляется оверхэд в ещё одну строку. Однако фильтрация производится не над пикселями а над байтами данных, поэтому если у нас меньше восьми бит на пиксель, то и оверхэд при распаковке будет меньше.

За сим всё – надеюсь текст был вам интересен и полезен. Вкратце я постарался осветить не только прикладную сторону использования JNG но и немного коснулся теории, ради любопытства.
Tags:
Hubs:
+32
Comments 13
Comments Comments 13

Articles