Работа с нестандартным форматом растровой графики

<< Предыдущая Содержание   Следующая - Graphics View Framework (пишем свою игру) >>

Для отображения изображений, которые не могут целиком уместиться в памяти программы, мы использовали класс QImageReader (см. главу 9 моей книги). Возможности класса могут оказаться весьма полезными, если нужно ускорить загрузку изображения или считывать изображение по частям. В этой статье мы рассмотрим расширение возможностей QImageReader при работе с разными графическими форматами. Для этой цели я даже придумал свой собственный формат (назовем его multiresolution PNG). В основе этого формата лежит тот простой факт, что метод setScaledSize() вовсе не обязан выполнять масштабирование изображения. Можно представить себе такой формат, где несколько изображений разных размеров хранятся в одном файле (собственно, такие форматы муже существуют – multiresolution Icon, например). В зависимости от требований приложения формат возвращает то изображение, которое по размерам наилучшим образом подходит к затребованному (при этом, для точного соответствия размерам затребованного изображения можно выполнить вспомогательное масштабирование). Все это имеет смысл при работе с такими изображениями, как например, пиктограммы. Художник, рисующий пиктограмму в разных размерах, может менять детали и даже все изображение, в зависимости от размера.

Формат файла multiresolution PNG (мы присвоим ему расширение pngm) представлен на рисунке. В архиве исходных текстов, сопровождающих эту статью, вы найдете файл в этом формате.

PNG 1

PNG 2

PNG 3

Ширина PNG 3 (2 байта)

Высота PNG 3 (2 байта)

Длина PNG 3 в байтах (4 байта)

Ширина PNG 2 (2 байта)

Высота PNG 2 (2 байта)

Длина PNG 2 в байтах (4 байта)

Ширина PNG 1 (2 байта)

Высота PNG 1 (2 байта)

Длина PNG 1 в байтах (4 байта)

PNGM (идентификатор формата)

Попросту говоря, PNGM это набор изображений в формате и оглавление, которое описывает ширину, высоту и длину в байтах каждого изображения (которых может быть не три, а сколько угодно). Обратите внимание, что «заголовок» формата расположен в конце файла, а не в начале. Это обеспечивает совместимость нашего формата с обычным форматом PNG. Если переименовать расширение файла из PNGM в PNG, то практически любая программа просмотра PNG-файлов сможет прочитать изображение (это будет первое изображение из коллекции изображений PNG, хранящихся в нашем файле).

Наша задача - научить QImageReader читать файлы формата PNGM, и обрабатывать обращения к методу setScaledSize() в соответствии с концепцией, изложенной выше. Все классы Qt, способные загружать изображения из файлов, пользуются для этого одними и теми же библиотеками-расширениями. Вы можете найти эти библиотеки в папке plugins/imageformats вашего дистрибутива Qt. Каждая из этих библиотек должна реализовать два класса. Первый, это класс-потомок класса QImageIOHandler, который будет выполнять всю реальную работу по чтению изображения из файла и конвертации этого изображения в объект QImage (класс QImage является универсальным промежуточным представлением данных при работе с растровой графикой). Второй класс должен происходить от класса QImageIOPlugin. Задача это класса – обслуживать подключение нового модуля расширений к системе Qt. Класс-хендлер не является потомком QObject, так что никаких макросов Q_OBJECT, сигналов и слотов быть не должно. Проект библиотеки модуля расширения мы создадим в Qt Creator. Пусть проект называется qpngm. Если заготовки классов создавать методам Qt Creator, система добавить к ним макрос QPNGMSHARED_EXPORT. Этот макрос нам не нужен, удаляем его. Класс-потомок QImageIOHandler мы назовем QxPngmIOHandler. Вот его объявление:

class QxPngmIOHandler : public QImageIOHandler 
{ 
public: 
	explicit QxPngmIOHandler(); 
	bool canRead() const; 
	bool read(QImage *image); 
	QVariant option(ImageOption option) const; 
	void setOption(ImageOption option, const QVariant &value); 
	bool supportsOption(ImageOption option) const; 
protected: 
	typedef struct _Chunk { 
		QSize size; 
		quint32 offset; 
		quint32 length; } Chunk; 
	typedef QList Chunks;
	QSize requiredSize; 
	int numImages; 
	Chunks chunks; 
	quint16 maxW; 
	quint16 maxH; 
	quint32 maxOffset; 
	quint32 maxLength; 
	bool readHeader() const; 
	bool readChunks(); 
}; 

Любой потомок QImageIOHandler должен реализовать как минимум два виртуальных метода: read() и canRead(). Мы, вдобавок к этому, реализуем еще три: option(), setOption() и supportsOption().

Начнем с метода canRead(). Тут понадобится небольшое вступление. Прежде чем система начинает работать с объектом QImageIOHandler, она передает этому объекту указатель на объект QIODevice. Как вы можете узнать из документации, QIODevice является базовым классом для классов, представляющих устройства ввода/вывода (файлы, сокеты, буферы памяти и т.д.). Когда система вызовет метод canRead() объекта нашего класса, ему (объекту) уже будет передан указатель на объект QIODevice. Задача метода canRead() – ответить, может ли он прочитать требуемые ему данные из заданного объекта QIODevice. Помимо метода canRead() у класса QImageIOHandler есть метод canWrite(), который отвечает на вопрос, может ли хендлер формата писать в заданное устройство и метод canReadIncremental(), который позволяет узнать, возможно ли постепенное чтение данных. В отличие от метода canRead(), который является чистым виртуальным, создавать реализации этих двух методов необязательно (наш модуль формата pngm не предполагает возможности записи в файл) и мы этого делать не будем.

 Что делает метод read(), я думаю, долго объяснять не нужно. В качестве параметра этому методу передается указатель на объект QImage, который должен быть заполнен графическими данными (используя уже имеющийся у нас объект QIODevice). Метод read() возвращает значение true, если он сумел прочитать данные и false – в противном случае. Может показаться, что значения, возвращаемые методом read() избыточны, ведь на этот же вопрос уже ответил метод canRead(). Но это не так. Дело в том, что метод canRead() должен работать быстро. Обычно этот метод не проверяет корректность всех предоставленных ему данных, а ограничивается проверкой корректности заголовка и, возможно, некоторых других параметров. Только метод read() может дать окончательный ответ на вопрос, действительно ли данные читабельны.

Метод supportsOption() сообщает системе, что именно объект-хендлер умеет делать. Этот метод работает так же, как одноименный метод класса QImageReader (который его и вызывает). В нашем примере этот метод возвращает значение true в ответ на вопрос, поддерживает ли хендлер опцию ScaledSize. В стальных случаях мы просто возвращаем значение, которое возвращает метод базового класса.

bool QxPngmIOHandler::supportsOption(QImageIOHandler::ImageOption option) const 
{
	if (option == ScaledSize) return true; 
	return QImageIOHandler::supportsOption(option); 
} 

Методы option() и setOption() необходимы, соответственно, для считывания и установки значений отдельных опций, таких как ScaledSize, ClipRect и т.д. На высоком уровне эти операции выполняют классы QImageReader и QImageWriter, у которых для чтения/установки значений каждой опции есть своя пара методов. Поскольку класс QImageIOHandler использует одну пару методов для всех опций, а сами значения опций могут быть разных типов, для чтения/записи значений используется тип QVariant, который уже преобразуется в тип, соответствующий конкретной опции. Использование одной пары методов для работы со всеми возможными опциями демонстрирует нам принцип разделения интерфейсов объектов на разных уровнях. Интерфейс высокоуровневого объекта (QImageReader, например) может меняться вместе с изменением списка опций, тогда как интерфейс низкоуровневого объекта модуля останется неизменным. Более того, поскольку, столкнувшись с неизвестной опцией модуль просто сообщает о том, что не поддерживает ее, при добавлении новых опций в высокоуровневые классы не возникает необходимости пересобирать все модули поддержки графических форматов.

Второй класс, который экспортирует библиотека модуля поддержки графического формата – потомок QImageIOPlugin, который в рассматриваемом примере именуется QxtPNGMIOPlugin. Этот класс является потомком QObject, так что в нем можно применять сигналы и слоты и другие элементы наследников QObject, однако практическая необходимость в этом обычно не возникает.

class QxtPNGMIOPlugin : public QImageIOPlugin 
{ 
public:
	QxtPNGMIOPlugin(QObject * parent = 0);
	Capabilities capabilities ( QIODevice * device, const QByteArray & format ) const; 
	QImageIOHandler * create ( QIODevice * device, const QByteArray & format = QByteArray() ) const;
	QStringList keys () const;
}; 

При создании класса QxtPNGMIOPlugin мы должны перекрыть три виртуальных метода, унаследованных от его предка: capabilities(), create() и keys(). Метод capabilities() получает два параметра: указатель на объект QIODevice и описание формата в виде объекта QByteArray. Объект QIODevice, как вы уже поняли, содержит графические данные в некотором формате. Описание формата, которое передается во втором параметре, имеет вид "png", "jpg" и тому подобные. Исходя из этих данных метод capabilities() должен вернуть перечень операций, которые он может выполнять с предоставленными данными в предоставленном формате (то есть, комбинацию флагов CanRead, CanWrite и т.д.).

Метод create() создает экземпляр хендлера (в нашем случае – объект QxPngmIOHandler). При этом хендлеру, с помощью метода setDevice() передается указатель на объект QIODevice, с которым хендлер потом и будет работать. Кроме того хендлеру может быть передана информация о формате (если это необходимо). Метод create() создает экземпляр хендлера с помощью оператора new и передает его системе. Дальше жизнью созданного хендлера распоряжается уже система.

Для корректного экспорта классов из библиотеки модуля в ее текст необходимо включить макрос Q_EXPORT_PLUGIN2().

Теперь библиотеку можно собрать. Для того чтобы система Qt могла работать с новым форматом, достаточно положить библиотеку в ту же директорию, где хранятся другие библиотеки поддержки графических форматов. Теперь новый формат станет доступен всем классам, работающим с растровой графикой.

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

На этом наше знакомство с изобразительными средствами Qt 4 не закончилось. В следующий раз мы познакомимся с системой Graphics View, которая появилась в  Qt 4.2 и была серьезно дополнена и переработана в Qt 4.3 и Qt 4.4.

Исходные тексты модуля расширения для работы с форматом PNGM  Исходные тексты демонстрационной программы Перед тем, как тестировать программу, необходимо собрать модуль расширения PNGM и поместить его в директорию plugins/imageformats.

Понравилась статья? Нажми:

<< Предыдущая Содержание    Следующая - Graphics View Framework (пишем свою игру) >>


© 2012  Андрей Боровский <anb@symmetrica.net>

Вернуться на страницу серии На главную