Вернуться к разделу "DjVu-программы".


Консольная утилита fi_c44

Введение

Консольная утилита c44 из состава пакета DjVuLibre предназначена для создания однослойных DjVu-файлов, содержащих только лишь задний фон (без маски и без переднего плана). Такой режим DjVu-файлов называется "DjVuPhoto" - потому что он наиболее качественно сохраняет многоцветное "фотографическое" изображение в формате DjVu (при этом используется вейвлетный алгоритм IW44).

Утилите с44 соответствует коммерческая программа phototodjvu из пакета Document Express Enterprise 5.1. Эти 2 программы (с44 и phototodjvu) ничем не отличаются в плане качества и свойств создаваемых ими DjVu-файлов. Обе они работают под Windows (а с44 ещё и под Linux; а также она имеет открытые исходники).

Однако у с44 есть такое неудобство, что она принимает на входе только лишь файлы в форматах PPM, PGM или JPG. Файлы любого другого формата приходится сначала преобразовывать в PPM или в PGM (при помощи Irfan View), и только потом конвертировать в DjVu посредством с44.

Для преодоления этого недостатка я создал новую утилиту на базе исходного кода с44 - fi_c44.

fi_c44 - это гибридная версия с44, которая использует свободно-бесплатную графическую библиотеку FreeImage для открытия графических файлов (подаваемых на вход fi_c44).

fi_c44 предназначена для работы только под ОС Windows (при желании можно сделать отдельно её Linux-версию - для Linux-версии FreeImage).

Библиотека FreeImage поддерживает более 30 графических форматов (на данный момент). Поэтому fi_c44 может принимать на входе любой из этих графических форматов (например TIF, BMP, JPEG-2000 и т.д.) и напрямую кодировать его в DjVuPhoto.

Кроме того, это повышает быстродействие использования с44-кодирования в скриптах: исчезает необходимость промежуточного преобразования "произвольный графический формат" -> PPM (PGM).

При этом fi_c44 сохраняет все прочие свойства с44 - т.е. все параметры и опции командной строки с44 остаются неизменными.

Правда, я добавил в fi_c44 опцию -bsf (Backround subsample factor), которая может принимать целочисленные значения от 2 до 12. Эта опция задаёт, во сколько раз fi_c44 следует уменьшить размеры создаваемого DjVu-файла по сравнению с кодируемым графическим файлом (аналогично csepdjvu). Опция bsf нужна, если предполагается вставка чанка BG44 из полученного DjVu-файла в другие DjVu-файлы.

Единственное ограничение, которое имеет fi_c44 по сравнению с с44 - это необходимость присутствия в зоне досягаемости fi_c44 файла FreeImage.dll. Без него fi_c44 не сможет работать.

Впрочем, на мой взгляд, это весьма несущественная проблема. Её можно рассматривать как "плату" за нововведённую мультиформатность. Файл FreeImage.dll имеет размер около 1,5-2 МБ и его можно скачать на сайте FreeImage.

Использование FreeImage даёт не только мультиформатность - FreeImage также предоставляет набор встроенных алгоритмов и удобный интерфейс для обработки загруженных растровых изображений. Т.е. графический файл можно не просто открыть и закодировать в DjVuPhoto - но также и как-либо обработать его нужным образом перед кодированием.


Раздел скачивания

Готовую к работе утилиту fi_c44 можно скачать здесь (только для ОС Windows):


Скачать
fi_c44 (вместе с исходником и FreeImage.dll) (546 КБ)

fi_c44 принимает на входе только цветные 24-битные или серые 8-битные изображения (именно такие и подразумевает DjVuPhoto - остальные бессмысленно пытаться закодировать).

Синтаксис командной строки fi_c44 абсолютно идентичен синтаксису командной строки c44 (только вместо PPM-PGM файла указывается файл любого поддерживаемого FreeImage формата), за исключением добавленной новой опции:

- bsf  n  (2..12) - фактор уменьшения размеров создаваемого DjVu-файла (аналогично csepdjvu).


Детали реализации

Утилита fi_c44 была создана мною под руководством Леона Боту (одного из создателей формата DjVu) и при помощи Андрея Жежеруна (автора программы WinDjView).

Я привожу ниже частичную выдержку из исходного кода fi_c44 (где показана лишь разница между с44 и fi_c44):

....
int flag_bsf = 1;
....
void
parse(GArray<GUTF8String> &argv)
{
.....
else if (argv[i] == "-bsf")
{
   if (++i >= argc)
      G_THROW( ERR_MSG("fi_c44.no_bsf_arg") );
   if (flag_bsf > 1)
      G_THROW( ERR_MSG("fi_c44.duplicate_bsf") );
   char *ptr;
   flag_bsf = strtol(argv[i], &ptr, 10);
   if (*ptr || flag_bsf<2 || flag_bsf>12)
      G_THROW( ERR_MSG("fi_c44.illegal_bsf") );
}
...
}

////////////////////////////////////////////////////////////////////////////////
/////////////////////////////////////////////////////////////////////////////////
/////////////////////////////////////////////////////////////////////////////////
//
//  WinDjView functions section
//
//	Copyright (C) 2004-2009 Andrew Zhezherun
//

struct VersionInfo
{
	VersionInfo();

	bool bNT, b2kPlus, bXPPlus, bVistaPlus;
};
static VersionInfo theVersionInfo;

VersionInfo::VersionInfo()
	: bNT(false), b2kPlus(false), bXPPlus(false), bVistaPlus(false)
{
	OSVERSIONINFO vi;
	vi.dwOSVersionInfoSize = sizeof(OSVERSIONINFO);
	if (::GetVersionEx(&vi))
	{
		bNT = (vi.dwPlatformId == VER_PLATFORM_WIN32_NT);
		if (bNT)
		{
			b2kPlus = (vi.dwMajorVersion >= 5);
			bXPPlus = (vi.dwMajorVersion > 5 || vi.dwMajorVersion == 5 && vi.dwMinorVersion >= 1);
			bVistaPlus = (vi.dwMajorVersion >= 6);
		}
	}
}

////////////////////////////////////////////////////////////////////////////////////////////

bool IsWinXPOrLater()
{
	return theVersionInfo.bXPPlus;
}

int ReadUTF8Character(const char* str, int& nBytes)
{
	const unsigned char* s = reinterpret_cast<const unsigned char*>(str);
	unsigned char c = s[0];

	if (c < 0x80)
	{
		nBytes = 1;
		return c;
	}
	else if (c < 0xC2)
	{
		return -1;
	}
	else if (c < 0xE0)
	{
		if (s[1] == 0 || (s[1] ^ 0x80) >= 0x40)
			return -1;
		nBytes = 2;
		return ((s[0] & 0x1F) << 6) + (s[1] ^ 0x80);
	}
	else if (c < 0xF0)
	{
		if (s[1] == 0 || s[2] == 0 || (s[1] ^ 0x80) >= 0x40
				|| (s[2] ^ 0x80) >= 0x40 || (c == 0xE0 && s[1] < 0xA0))
			return -1;
		nBytes = 3;
		return ((s[0] & 0xF) << 12) + ((s[1] ^ 0x80) << 6) + (s[2] ^ 0x80);
	}
	else if (c < 0xF5)
	{
		if (s[1] == 0 || s[2] == 0 || s[3] == 0 || (s[1] ^ 0x80) >= 0x40
				|| (s[2] ^ 0x80) >= 0x40 || (s[3] ^ 0x80) >= 0x40
				|| (c == 0xF0 && s[1] < 0x90) || (c == 0xF4 && s[1] > 0x8F))
			return -1;
		nBytes = 4;
		return ((s[0] & 0x07) << 18) + ((s[1] ^ 0x80) << 12) + ((s[2] ^ 0x80) << 6)
				+ (s[3] ^ 0x80);
	}
	else
		return -1;
}

////////////////////////////////////////////////////////////////////////////////////////////

bool IsValidUTF8(const char* pszText)
{
	const char* s = pszText;
	while (*s != 0)
	{
		int nBytes = 0;
		if (ReadUTF8Character(s, nBytes) < 0)
			return false;
		s += nBytes;
	}

	return true;
}

////////////////////////////////////////////////////////////////////////////////////////////
//
// Convert GUTF8String to ASCII string:
//
// returns the lenght of the converted string
//
int MakeString(const GUTF8String& text, char* strResult)
{
	if (text.length() == 0)
		return 0;
	
	// Prepare Unicode text
	LPWSTR pszUnicodeText = NULL;
	int nResult = 0;
	
	// Bookmarks or annotations may not be encoded in UTF-8
	// (when file was created by old software)
	// Treat input string as non-UTF8 if it is not well-formed
	DWORD dwFlags = 0;
	
	// Only Windows XP supports checking for invalid characters in UTF8 encoding
	// inside MultiByteToWideChar function
	if (IsWinXPOrLater())
	{
		dwFlags |= MB_ERR_INVALID_CHARS;
	}
	
	// Make our own check anyway
	if (!IsValidUTF8(text))
	{
		strcpy(strResult,text);
		
		printf("%s\n","Not valid UTF8");
		
		return 0;
	}
	
	int nSize = ::MultiByteToWideChar(CP_UTF8, dwFlags, (LPCSTR)text, -1, NULL, 0);	
	
	if (nSize != 0)
	{
		pszUnicodeText = new WCHAR[nSize];
		nResult = ::MultiByteToWideChar(CP_UTF8, dwFlags, (LPCSTR)text, -1, pszUnicodeText, nSize);
	}
	
	if (nResult != 0)
	{
#ifdef _UNICODE
		// something may be implemented here (if needed)
		
		//strResult = pszUnicodeText;
#else
		// Prepare ANSI text
		nSize = ::WideCharToMultiByte(CP_ACP, WC_COMPOSITECHECK | WC_DISCARDNS,
			pszUnicodeText, -1, NULL, 0, NULL, NULL);
		if (nSize == 0)
		{
			delete[] pszUnicodeText;
			
			return 0;
		}
		
		if (nSize > MAX_PATH)
		{
			delete[] pszUnicodeText;			
			
			printf("Filepath is longer than %d symbols\n", MAX_PATH);
			
			return 0;
		}
		
		::WideCharToMultiByte(CP_ACP, WC_COMPOSITECHECK | WC_DISCARDNS,
			pszUnicodeText, -1, strResult, nSize+1, NULL, NULL);
		
#endif
	}
	else
	{		
		strcpy(strResult,text);
	}
	
	delete[] pszUnicodeText;
	
	return strlen(strResult);
}

//  End of the WinDjView functions section
//
/////////////////////////////////////////////////////////////////////////////////
/////////////////////////////////////////////////////////////////////////////////
/////////////////////////////////////////////////////////////////////////////////

// FreeImage routines:

////////////////////////////////////////////////////////////////////////////////
/**
FreeImage error handler
@param fif Format / Plugin responsible for the error 
@param message Error message
*/
void FreeImageErrorHandler(FREE_IMAGE_FORMAT fif, const char *message) {
	printf("\n*** "); 
	printf("%s Format\n", FreeImage_GetFormatFromFIF(fif));
	printf(message);
	printf(" ***\n");
}

////////////////////////////////////////////////////////////////////////////////

/** Generic image loader

  @param lpszPathName Pointer to the full file name
  @param flag Optional load flag constant
  @return Returns the loaded dib if successful, returns NULL otherwise
*/

FIBITMAP* GenericLoader(const char* lpszPathName, int flag)
{	
	FREE_IMAGE_FORMAT fif = FIF_UNKNOWN;
	// check the file signature and deduce its format
	// (the second argument is currently not used by FreeImage)
	
	fif = FreeImage_GetFileType(lpszPathName, 0);
	
	FIBITMAP* dib;
	
	if(fif == FIF_UNKNOWN)
	{
		// no signature ?
		// try to guess the file format from the file extension
		fif = FreeImage_GetFIFFromFilename(lpszPathName);
	}
	
	// check that the plugin has reading capabilities ...
	if((fif != FIF_UNKNOWN) && FreeImage_FIFSupportsReading(fif))
	{
		// ok, let's load the file
		dib = FreeImage_Load(fif, lpszPathName, flag);
		
		// unless a bad file format, we are done !
		if (!dib)
		{
			printf("%s%s%s\n","File \"", lpszPathName, "\" not found.");
			return NULL;
		}
	}	
	
	return dib;
}

/////////////////////////////////////////////////////////////////////////////////
/////////////////////////////////////////////////////////////////////////////////
/////////////////////////////////////////////////////////////////////////////////
//
// Main function:
//

int
main(int argc, char **argv)
{
	setlocale(LC_ALL,"");
	djvu_programname(argv[0]);
	GArray<GUTF8String> dargv(0,argc-1);
	for(int i=0;i<argc;++i)
		dargv[i]=GNativeString(argv[i]);
	G_TRY
    {
		// Parse arguments
		parse(dargv);
		
		FreeImage_SetOutputMessage(FreeImageErrorHandler);
		
		char strResult[MAX_PATH] = {0};
		
		MakeString(g().pnmurl.UTF8Filename(), strResult);
		
		// load a file into a universal FIBITMAP memory structure:
		
		FIBITMAP *dib;
		
		if (flag_bsf>1)
		{		
			FIBITMAP *dib_tmp = GenericLoader(strResult, 0);
			
			unsigned width  = FreeImage_GetWidth(dib_tmp);
			
			unsigned height = FreeImage_GetHeight(dib_tmp);
			
			dib = FreeImage_Rescale(dib_tmp, (int)((width+(flag_bsf-1))/flag_bsf), 
				(int)((height+(flag_bsf-1))/flag_bsf), FILTER_BICUBIC);
			
			FreeImage_Unload(dib_tmp);
		}	
		
		else
			
			dib = GenericLoader(LPCSTR(strResult), 0);
		
		if (FreeImage_GetImageType(dib) != FIT_BITMAP)
		{
			printf("Non-FIT_BITMAP images are not supported.\n");
			
			FreeImage_Unload(dib);
			
			return -2;
		}
		
		if (!(FreeImage_GetBPP(dib) == 24) &&
			
			!((FreeImage_GetBPP(dib) == 8) && (FreeImage_GetColorType(dib) != FIC_PALETTE)))
		{
			printf("Only 24-bit color or 8-bit greyscale images are supported.\n");
			
			FreeImage_Unload(dib);
			
			return -3;
		}
		
		// Load images		
		unsigned w = FreeImage_GetWidth(dib); // FIBITMAP width in pixels		
		unsigned h = FreeImage_GetHeight(dib); // FIBITMAP height in pixels		
		
		// Returns the width of the bitmap in bytes, rounded to the next 32-bit boundary,
		// also known as "pitch" or "stride" or "scan width".
		unsigned pitch = FreeImage_GetPitch(dib);
		
		BYTE* bits = (BYTE*)FreeImage_GetBits(dib); // the pointer to the 1 pixel
		// in the 1 pixel row (in FIBITMAP)		
		BYTE* lines;
		
		unsigned bpp = FreeImage_GetBPP(dib); // bitdepth, 24 - for color, 8 - for greyscale
		
		unsigned btpp = bpp/8;
		
		GP<IW44Image> iw;
		
		//Just create a GPixmap of size w*h 		
		GP<GPixmap> gipm = GPixmap::create(h,w);
		
		//and fill it with the RGB data:
		unsigned i,j;
		
		for (j=0;j<h;j++)
		{ 
			GPixel *row = (*gipm)[j]; 
			
			lines = bits + j * pitch;
			
			for (i=0; i<w; i++)
			{ 
				GPixel p; 
				
				if (btpp == 3) // color 24 bit
				{
					p.r = lines[FI_RGBA_RED]; // red byte
					p.g = lines[FI_RGBA_GREEN]; // green byte
					p.b = lines[FI_RGBA_BLUE]; // blue byte
					
					row[i] = p;
					
					lines += btpp;
					
				}
				else if (btpp == 1) // greyscale 8 bit
				{
					p.r = lines[i]; // red byte
					p.g = lines[i]; // green byte
					p.b = lines[i]; // blue byte
					
					row[i] = p;
				}
			}
		}
		
		GPixmap &ipm=*gipm;
		
		int bm_size = w*h*btpp;
		
		// Change percent specification into size specification
		if (flag_size && flag_percent)
			for (int i=0; i<argc_size; i++)
				argv_size[i] = (argv_size[i]*bm_size)/ 100;
			
			flag_percent = 0;
			
			iw = IW44Image::create_encode(ipm, getmask(w,h), arg_crcbmode);
			
			// Perform compression PM44 or BM44 as required
			if (iw)
			{
				g().iw4url.deletefile();
				GP<IFFByteStream> iff =
					IFFByteStream::create(ByteStream::create(g().iw4url,"wb"));
				if (flag_crcbdelay >= 0)
					iw->parm_crcbdelay(flag_crcbdelay);
				if (flag_dbfrac > 0)
					iw->parm_dbfrac((float)flag_dbfrac);
				int nchunk = resolve_quality(w*h);
				// Create djvu file
				create_photo_djvu_file(*iw, w, h, *iff, nchunk, g().parms);
			}
			
			FreeImage_Unload(dib);
    }
	G_CATCH(ex)
    {
		ex.perror();
		exit(1);
    }
	G_ENDCATCH;
	return 0;
}

//////////////////////////////////////////////////////////////////////////////////////////// 

Этот код включает в себя 2 интересных момента:

1. Преобразование пути:

(Создано с помощью Андрея Жежеруна)

char strResult[MAX_PATH] = {0};

MakeString(g().pnmurl.UTF8Filename(), strResult);

// load a file into a universal FIBITMAP memory structure:
FIBITMAP *dib = GenericLoader(strResult, 0);

Этот механизм целиком базируется на использовании функций, взятых из программы WinDjView. Функция MakeString - это слегка преобразованная функция MakeСString из исходников WinDjView.

Программа считывает путь к кодируемому файлу из командной строки и помещает его в объект класса GURL в виде URL-encoded строки.

Метод UTF8Filename класса GURL преобразует URL-encoded строку в объект GUTF8String (т.е. строка в кодировке UTF8 (Unicode).

Функция MakeString (взятая из исходников WinDjView) преобразует UTF8-строку в обычную ASCII-cтроку, которая затем используется для открытия графического файла функциями FreeImage.

В библиотеке FreeImage есть также Unicode-версии функций, открывающих файлы - поэтому последняя операция преобразования UTF8 -> ASCII, строго говоря, не обязательна. Однако, Unicode-режим вообще плохо поддерживается, например, в Windows 98 - поэтому, на мой взгляд, в данном случае надежнее работать с обычными ASCII-строками.

Примечание: Для преобразования UTF8 -> ASCII используется буфер strResult с фиксированной длиной 260 символов. В данном случае фиксированная длина буфера допускается (хотя обычно выделяется динамический массив переменной длины, размер которого определяется заранее). Дело в том, что Windows имеет максимальное ограничение длины файлового пути в 260 символов (задаётся библиотечной константой MAX_PATH). Подробнее см. в Википедии тут: Имя файла.

2. Загрузка изображения через FreeImage:

(Создано под руководством Леона Боту)

//Just create a GPixmap of size w*h
GP<GPixmap> gipm = GPixmap::create(h,w);

//and fill it with the RGB data:
unsigned i,j;

for (j=0;j<h;j++)
{
    GPixel *row = (*gipm)[j];

    lines = bits + j * pitch;

  
for (i=0; i<w; i++)
    {
    GPixel p;

  
if (btpp == 3) // color 24 bit
    {
        p.r = lines[FI_RGBA_RED]; // red byte
        p.g = lines[FI_RGBA_GREEN]; // green byte
        p.b = lines[FI_RGBA_BLUE]; // blue byte

        row[i] = p;

        lines += btpp;
    }
  
else if (btpp == 1) // greyscale 8 bit
    {
        p.r = lines[i]; // red byte
        p.g = lines[i]; // green byte
        p.b = lines[i]; // blue byte

        row[i] = p;
    }
}

Библиотека FreeImage внутренне использует универсальную рабочую структуру FIBITMAP.

FIBITMAP - это стандартный "контейнер" в памяти, в который (у FreeImage) распаковывается загруженное с диска изображение из файла любого поддерживаемого формата. Далее FreeImage работает уже с этим FIBITMAP - а не с самим исходным файлом. После окончания обработки готовое изображение выгружается из FIBITMAP на диск (в файл любого поддерживаемого графического формата).

Такой подход обеспечивает универсальность обработки и расширяемость использования самых разнообразных графических форматов в библиотеке FreeImage.

FIBITMAP с программной точки зрения идентичен стандартной Windows-структуре для работы с растровыми данными, которая называется "DIB" (Device independent bitmap). Подробнее про DIB см. в Википедии тут: Device-independent_bitmap. Это было сделано для того, чтобы дополнить стандартную функциональность Windows возможностями FreeImage (в отношении работы с растровыми иображениями).

Одной из особенностей FIBITMAP является дополнение каждой строки пикселей нулями справа до ближайшей 32-битной границы (т.е. выравнивание строк пикселей на границы двойных слов; a ещё точно так же выравнивается и начало пиксельной таблицы). Это предназначено для ускорения обработки растровых изображений - т.к. Windows считывает их сразу блоками, кратными 32-битам.

Форматы PPM и PGM не имеют такого выравнивания. Они представляют из себя, грубо говоря, просто прямоугольные таблицы пикселей. Это происходит от того, что форматы PPM и PGM были рождены в мире Linux - где такие Windows-понятия, как "DIB", не имеют никакого смысла.

Библиотека DjVuLibre с самого начала пошла по "лёгкому" "пути Linux" - и стала использовать форматы PPM и PGM как базовые (потому что с простыми прямоугольными таблицами пикселей легче и быстрее работать). Так что в DjVuLibre, так же как и в PPM-PGM, отсутствует 32-битное выравнивание для пиксельных строк.

Поэтому прямое использование FIBITMAP в DjVuLibre невозможно - приходится сначала делать преобразование из 32-выровненного FIBITMAP в 32-невыровненную структуру GPixmap (одна из внутренних структур DjVuLibre) и дальше работать уже с GPixmap.


Автор: monday2000.

5 июня 2009 г.

E-Mail  (monday2000 [at] yandex.ru)

Hosted by uCoz