GDI+: графика нового поколения

Часть2. Работа с растрами и графическими файлами

Автор: Виталий Брусенцев
The RSDN Group

Источник: RSDN Magazine #1
Введение
Что же такое GDI+?
Класс Bitmap – контейнер растровых изображений
Поддержка основных графических форматов
Загрузка из файлов и потоков (IStream)
Создание растров из ресурсов программы
Более сложные варианты загрузки изображений
Графические форматы файлов
Лирическое отступление: 4 основных графических формата
Работа со списком кодеков
Сохранение изображений
Специфические возможности файловых форматов
Сохранение GIF с прозрачностью
Загрузка и сохранение многокадровых файлов
Эскизы изображений
Работа с метаданными изображений
Использование растров при работе с объектом Graphics
Вывод изображений и геометрические преобразования
Качество изображения
Устранение мерцания
Несколько слов о производительности
Демонстрационные приложения
Прямая работа с растровыми данными
Класс Color
Прямой доступ к пикселам
Поддержка прозрачности
Растровые операции

Введение

В данной статье я постараюсь рассказать читателям о работе с растровой графикой средствами новой графической библиотеки от Microsoft – GDI+. Тем, кто до сих пор не ознакомился с этим API, советую прочитать статью "GDI+ – графика нового поколения. Краткое знакомство", опубликованную на сайте RSDN.RU (ее можно найти и в архиве сайта на компакт-диске, прилагаемом к данному журналу). Кроме того, предполагается, что читатели знакомы с основными принципами растровой графики и средствами Windows GDI.

Что же такое GDI+?

Коротко напомню основные принципы устройства этой библиотеки. Она предназначена для замены морально устаревшего программного интерфейса GDI. Ее использование позволит облегчить переход на 64-битные платформы и максимально полно использовать изобразительные способности и вычислительные мощности современных компьютеров.

Необходимо признать, что программисты Microsoft проделали впечатляющую работу, спроектировав GDI+. Был учтен многолетний опыт использования GDI и устранены некоторые моменты, зачастую приводившие к ошибкам при работе с этим API. В частности, отпала необходимость выбирать графические объекты в контексте устройства перед их использованием. Все необходимые кисти, перья, шрифты и т.д. передаются в качестве параметров функциям рисования. А так как удаление соответствующих объектов осуществляется автоматически в деструкторах классов-оберток GDI+ (или методе Dispose интерфейса IDispose в .Net), значительно снижается вероятность утечек графических ресурсов, так досаждавших программистам, использующим GDI.

GDI+ можно условно поделить на три функциональные составляющие: векторная графика (многочисленные векторные примитивы), растровая графика (с поддержкой прозрачности и альфа-канала) и работа со шрифтами. Кроме того, в ней имеются удобные средства работы с графическими файлами популярных форматов, поддерживается их загрузка и сохранение.

Библиотека GDI+ (GdiPlus.DLL) входит в состав операционных систем Windows XP и .NET Server, а для систем на базе Windows 98/ME/NT4/2000 доступна в виде дистрибутива на веб-узле Microsoft. Вы можете распространять эту библиотеку в составе собственных приложений.

Для C++-программистов создан объектно-ориентированный интерфейс к этой библиотеке. Он содержит около 40 классов, упрощающих работу с GDI+. Этот интерфейс, или, проще говоря, оболочка, доступен в составе Microsoft Platform SDK – в виде набора из 30 заголовочных файлов и библиотеки импорта GdiPlus.LIB. Для других сред программирования (VB 6, Delphi) подобные оболочки создаются энтузиастами, и разные их варианты можно найти в Интернете.

GDI+ является основной графической библиотекой в Microsoft .NET Framework. Правда, в библиотеке .NET Framework CLR-совместимые оберточные классы GDI+ иногда отличаются от своих аналогов в C++. В этой статье будет рассмотрена преимущественно C++-реализация. Но эта статья должна оказаться полезной и для .NET-программистов. Существенные отличия .NET- и C++-реализаций будут специально оговорены.

Класс Bitmap – контейнер растровых изображений

Для хранения изображений в библиотеке GDI+ предназначен класс Image. Его потомки, классы Bitmap и Metafile, реализуют, соответственно, функциональность для работы с растровыми и векторными изображениями. Кстати, в документации .NET Framework SDK утверждается, что потомком Image является и класс Icon, но это не так: он унаследован от System.MarshalByRefObject. В иерархии классов GDI+ для C++ класса Icon не существует (все необходимые средства для работы с иконками находятся в Bitmap).

Итак, в этой части статьи мы постараемся хорошенько изучить класс Bitmap и особенности работы с ним. Такое внимание к единственному классу вполне обосновано: он предоставляет очень много возможностей. Например, если вы создаете растровую кисть (TextureBrush) или графический контекст (Graphics) в памяти, для их инициализации потребуется экземпляр класса Bitmap.

Поддержка основных графических форматов

Это качество является одним из наиболее привлекательных свойств библиотеки GDI+. Например, скромный и неприметный редактор Paint в Windows XP неожиданно приобрел возможность открывать и сохранять не только картинки BMP, но также и JPG, TIF, GIF и PNG, что сразу сделало его на порядок более мощным средством. Это полезное качество появилось в нем благодаря использованию GDI+ (Paint из комплекта Windows 2000 тоже поддерживал GIF и JPG, но делал это заметно хуже, используя набор модулей расширения FLT, впервые появившихся в Office 97).

К графическим фильтрам GDI+ уже прочно прикрепилось жаргонное название "кодек" (codec, Compressor/Decompressor). Чтобы не отстать от моды, будем их так называть и мы.

У класса Bitmap существует набор перегруженных конструкторов для создания или загрузки изображений. При загрузке содержимого файла анализируется его формат (а вовсе не расширение!), и автоматически используется соответствующий кодек. Определить, какой кодек (и какой формат) был использован при загрузке, можно с помощью метода Image::GetRawFormat:

Bitmap bm(L"Picture.dat"); // обратите внимание на расширение
GUID guidFileFormat;

bm.GetRawFormat(&guidFileFormat);
// проверим, действительно ли в файле находился JPEG
if(guidFileFormat == ImageFormatJPEG) 
   MessageBox(0, "Это JPEG!", 0, MB_OK);

Константы форматов (ImageFormatXxx, где Xxx – формат) определены в заголовочных файлах GDI+. Только не спутайте их с идентификаторами кодеков, которые будут обсуждаться ниже. В .NET определение формата производится с помощью свойства:

public ImageFormat RawFormat {get;}

Кроме конструкторов, для создания растров можно использовать семейство статических методов FromXxx класса Bitmap. Они возвращают указатель на объект, который в конце работы необходимо удалять при помощи оператора delete.

Соответствующие статические методы имеются и у класса System.Drawing.Bitmap в среде Microsoft .NET Framework. Разумеется, в .NET вместо delete нужно использовать Dispose (а лучше даже пользоваться оператором using языка C#):

using(Bitmap bm = Bitmap.FromFile("photo.jpg"))
{
  // Используем картинку...
} // здесь bm будет освобожден

Каждый кодек по мере возможностей учитывает специфические качества своего формата – например, загрузчик GIF правильно распознаёт прозрачность в GIF89a, чего так не хватало функции OleLoadPicturePath.

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

Загрузка из файлов и потоков (IStream)

Итак, для загрузки изображения из файла существует следующий конструктор:

Bitmap(const WCHAR* filename, BOOL useIcm);

Параметр filename должен содержать имя существующего файла. Все строковые параметры методов GDI+ требуют UNICODE-строк, поэтому при передаче строковой константы в программе на C++ необходимо предварять ее префиксом ‘L’.

Параметр useIcm определяет, будет ли при загрузке растра использоваться ICM (Image Color Management), и по умолчанию равен FALSE. Если же использовать ICM необходимо, графический файл должен содержать всю нужную информацию, например, цветовые профили конкретных устройств.

Существует достаточно неприятная ошибка в коде GDI+, отвечающем за загрузку изображений из файлов – часто (но не всегда), при указании несуществующего имени файла, вместо того, чтобы вернуть код ошибки в переменной Status, приложение завершается с выдачей сообщения об ошибке (уродливое белое прямоугольное окно Application Error, которого я не видел со времен Windows 3.x).

Microsoft признает наличие этой проблемы, и в скором времени планируется выход заплатки (и очередной статьи в Knowledge Base). Сейчас единственный разумный способ избежать такого исхода – это убедиться в существовании файла перед попыткой его открытия.

ПРЕДУПРЕЖДЕНИЕ

Недавно найдены еще одни "грабли", связанные с тем, что некоторые цифровые фотокамеры не записывают в сохраняемый фотоснимок TIFF его физические размеры. Такой файл прекрасно прочитается кодеком TIFF, и, при везении, даже нарисуется – если явно задать пиксельные размеры получаемого рисунка. В противном случае, GDI+ услужливо попытается рассчитать их на основании физических размеров – и работа приложения завершится с такой же "диагностикой". Будьте бдительны!

Более гибкие возможности загрузки таятся в таком конструкторе Bitmap:

Bitmap(IStream* stream, BOOL useIcm);

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

Создание растров из ресурсов программы

Следующий конструктор Bitmap позволяет загрузить растр из ресурсов:

Bitmap(HINSTANCE hInstance, const WCHAR* bitmapName);

Но не обольщайтесь, далеко не всякий ресурс удастся загрузить таким образом. Этот конструктор предназначен для загрузки именно BITMAP-ресурсов, и не поддерживает, скажем, загрузку GIF. Возможно, это ограничения текущей реализации. К счастью, их достаточно легко обойти: просто предоставьте загрузчику интерфейс IStream с необходимыми данными. Это можно сделать, например, воспользовавшись функцией Windows API CreateStreamOnHGlobal:

Bitmap* BitmapFromResource(HINSTANCE hInstance,
               LPCTSTR szResName, LPCTSTR szResType)
{
  HRSRC hrsrc = FindResource(hInstance, szResName, szResType);
  if(!hrsrc) return 0;
  // "ненастоящий" HGLOBAL - см. описание LoadResource
  HGLOBAL hgTemp = LoadResource(hInstance, hrsrc);
  DWORD sz = SizeofResource(hInstance, hrsrc);
  void* ptrRes = LockResource(hgTemp);
  HGLOBAL hgRes = GlobalAlloc(GMEM_MOVEABLE, sz);
  if(!hgRes) return 0;
  void* ptrMem = GlobalLock(hgRes);
  // Копируем растровые данные
  CopyMemory(ptrMem, ptrRes, sz);
  GlobalUnlock(hgRes);
  IStream *pStream;
  // TRUE означает освободить память при последнем Release
  HRESULT hr = CreateStreamOnHGlobal(hgRes, TRUE, &pStream);
  if(FAILED(hr))
  {
    GlobalFree(hgRes);
    return 0;
  }
  // Используем загрузку из IStream
  Bitmap *image = Bitmap::FromStream(pStream);
  pStream->Release();
  return image;
}

Вызов Release в вышеприведенном примере не приведет к немедленному уничтожению потока IStream. Похоже, при загрузке из файлов и потоков объекты Bitmap удерживают источник данных в течение своей жизни и "отпускают" его только в деструкторе. Если об этом забыть, потом можно долго удивляться, почему к графическому файлу нет доступа после чтения его в Bitmap.

При загрузке графических ресурсов в .NET приложении, можно использовать загрузку из объекта System.IO.Stream. Получить экземпляр объекта Stream из ресурса позволяет следующий метод класса System.Reflection.Assembly:

public virtual Stream GetManifestResourceStream(string name);

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

Bitmap bmp = new Bitmap(
  Assembly.GetExecutingAssembly().
  GetManifestResourceStream("Demo.MyPicture.jpg"));

Примечание: При добавлении файла в ресурсы приложения с помощью VS.NET ресурсу дается имя, состоящее из имени файла и префикса пространства имен (в данном случае "Demo"). Если же ресурс помещается в модуль с помощью утилит командной строки, вы можете сами определить имя ресурса.

Более сложные варианты загрузки изображений

Помимо загрузки растровых изображений стандартных форматов из файлов или потоков, изображение можно создавать на лету или конвертировать из других форматов, например, объектов GDI или поверхностей DirectDraw. Вот соответствующие конструкторы:

Bitmap(const BITMAPINFO* gdiBitmapInfo, VOID* gdiBitmapData);
Bitmap(HBITMAP hbm, HPALETTE hpal);
Bitmap(HICON hicon);
Bitmap(IDirectDrawSurface7 * surface);

Важно понимать, что все эти конструкторы создают копию растровых данных. При любых модификациях объекта Bitmap (например, с помощью отрисовки в объект Graphics, созданный на его базе), источник данных остается неизменным. Более того, перенос изменений обратно в исходный объект GDI часто сопряжен с трудностями – например, может потребоваться самостоятельная работа с палитрами и битовыми массивами. Некоторые рекомендации по этому поводу можно встретить ниже, в разделе "Прямая работа с растровыми данными".

Отмечу, что, на мой взгляд, в GDI+ было уделено большое внимание обратной совместимости. Вместе с тем, эта совместимость в значительной степени реализована в режиме "только для чтения". Так, например, библиотека корректно прочитает из памяти растр с заголовком BITMAPV4HEADER, содержащим информацию об альфа-канале, но при сохранении в BMP из GDI+ будет сгенерирован только заголовок BITMAPINFOHEADER, и вся информация о прозрачности пропадет.

Не стоит смешивать функциональность кодеков GDI+ и возможности работы с JPEG, которые появились в Windows 98/2000 – они полностью независимы друг от друга. Это поначалу может привести к путанице. В частности, если попытаться создать объект Bitmap из структуры BITMAPINFO, в которой поле bmiHeader.biCompression содержит значение BI_JPEG, ничего не выйдет – потому, что загрузчик из растровых данных GDI+ поддерживает только простые форматы (вроде BI_RGB и BI_BITFIELDS). Для загрузки JPEG-изображений просто создайте IStream на блоке данных JPEG и вызовите надлежащий конструктор класса Bitmap.

Что же касается сохранения растров с прозрачностью, к вашим услугам форматы TIFF, PNG и GIF (об этом речь пойдет чуть ниже).

Существуют также конструкторы, позволяющие задать формат создаваемого растра:

Bitmap(INT width, INT height, PixelFormat format);

Перечисление PixelFormat содержит большое количество констант, определяющих растровые форматы. По умолчанию параметр format имеет значение PixelFormat32bppARGB. Наиболее быстрым для вывода является, однако, формат PixelFormat32bppPARGB. В этом формате каждая цветовая составляющая уже умножена на величину Alpha этого же пиксела, что избавляет от этого умножения (на каждый пиксел!) при накладывании изображения.

Можно самостоятельно выделить буфер для создаваемого растра. Вот соответствующий конструктор:

Bitmap(INT width, INT height, INT stride,PixelFormat format, BYTE* scan0 );

Здесь stride – величина смещения (в байтах) между концом одной строки растра и началом другой, scan0 – указатель на массив байтов, содержащий растровые данные. Невзирая на формат, величина stride должна делиться без остатка на 4 (и, естественно, быть достаточно большой для хранения строки необходимой ширины). Она может также быть отрицательной (для создания растров с обратным порядком следования строк).

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


После уничтожения такого объекта Bitmap выделенный буфер не удаляется – ответственность за его освобождение лежит на программисте.

Графические форматы файлов

Лирическое отступление: 4 основных графических формата

Для хранения сжатых растровых данных используются, в основном, форматы JPEG (Joint Photographers Expert Group), GIF (Graphics Interchange Format), PNG (Portable Network Graphics) и TIFF (Tagged Image File Format). Люди, работающие в сфере полиграфии, имеют собственное мнение относительно форматов изображений, и особенно не любят JPEG – за сжатие с потерями, хотя оно и позволяет достичь больших степеней компрессии. Не будем ввязываться в религиозный спор о форматах, а лучше сравним их возможности.

СвойствоJPEGGIFPNGTIFF
Полноцветные (True color) изображенияПоддерживаетНе поддерживаетПоддерживаетПоддерживает
Хранение нескольких кадров в одном файле.Не поддерживаетПоддерживаетНе поддерживаетПоддерживает
Прозрачность (color key)Не поддерживаетПоддерживаетПоддерживает1Не поддерживает
Прозрачность (альфа-канал)Не поддерживаетНе поддерживаетПоддерживает1Поддерживает (в 32-битных форматах)
Сжатие без потерь качестваНе поддерживаетПоддерживаетПоддерживаетПоддерживает
ПРИМЕЧАНИЕ

1 PNG является, пожалуй, уникальным форматом в сфере поддержки прозрачности. В этом формате можно сохранять полупрозрачные индексные изображения, grayscale- изображения с альфа-каналом и т.д. Однако полностью эти возможности в продуктах Microsoft (и, в частности, в GDI+) не реализованы.

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

Кратко остановлюсь на особенностях GIF. Этот формат является индексным, то есть цвет каждого пиксела в нем определяется его индексом в таблице цветов (палитре). Размер индекса ограничен величиной 8 бит, поэтому изображение может содержать не более 256 цветов одновременно. В этой модели реализована так называемая прозрачность по цветовому ключу (color key), то есть один из цветов в палитре можно назначить прозрачным. При загрузке GIF-файлов кодеком GDI+ они преобразуются в родной для этой библиотеки 32-битный растр. Пикселы прозрачного цвета при этом просто получают значение Alpha, равное 0.

Кроме того, напомню, что формат GIF имеет лицензионные ограничения: в нем используется компрессия LZW (Lempel-Ziv-Welch), патентом на которую до 2003 года владеет Unisys Corporation. Разработчики, использующие в своих продуктах библиотеки работы с LZW, должны платить отчисления Unisys – даже, если используется библиотека GDI+ (подробнее см. в Q193543 INFO: Unisys GIF and LZW Technology License Information).

То же самое относится и к файлам формата TIFF, если для их компрессии выбран метод LZW. TIFF является контейнерным форматом и технически может содержать несколько кадров, сжатых различными методами (в том числе и JPEG).

Работа со списком кодеков

У каждого кодека GDI+ имеется уникальный CLSID – как и у COM-объектов. Чтобы получить список доступных кодеков, необходимо вызвать функции GetImageDecoders (она вернет список фильтров импорта) и GetImageEncoders (для получения фильтров сохранения). Эти функции заполняют переданный им буфер размером size байт массивом структур ImageCodecInfo:

class ImageCodecInfo
{
public:
  CLSID Clsid;
  GUID  FormatID;
  const WCHAR* CodecName;
  const WCHAR* DllName;
  const WCHAR* FormatDescription;
  const WCHAR* FilenameExtension;
  const WCHAR* MimeType;
  DWORD Flags;
  DWORD Version;
  DWORD SigCount;
  DWORD SigSize;
  const BYTE* SigPattern;
  const BYTE* SigMask;
};

Для просмотра свойств установленных в системе кодеков GDI+ я написал утилиту на C++ с использованием MFC. Ее исходный код и откомпилированный вариант можно найти на компакт-диске, прилагающемся к журналу.

Codecs.zip – проект с исходным кодом.
Codecs_EXE.zip – готовое приложение.


Программа отображает список имеющихся в GDI+ компрессоров/декомпрессоров и позволяет просмотреть свойства каждого кодека, а для кодеков сохранения – и поддерживаемые параметры .

Первый же запуск программы дал интересные результаты. Например, несмотря на наличие двух раздельных списков – для загрузки и для сохранения – в них перечислены одни и те же кодеки (достаточно сравнить их CLSID). Это, конечно же, деталь реализации, которая может измениться в будущих версиях GDI+.

Немедленно возникает вопрос: а можно ли добавить в этот список свои кодеки, чтобы иметь возможность загружать файлы других форматов при помощи GDI+? Например, поле Flags содержит бит Builtin, который, судя по названию, должен показывать, является ли кодек встроенным. Увы, документированного способа пока не существует: в GDI+ версии 1.0 кодеки не являются объектами ActiveX, а встроены в код библиотеки. В следующей версии Microsoft обещает добавить поддержку внешних кодеков.

Сохранение изображений

При сохранении необходимо указать, кодек какого формата необходимо использовать. Функция Image::Save принимает Unicode-строку filename и указатель на GUID кодека clsidEncoder:

Status Save(
  const WCHAR* filename,
  const CLSID* clsidEncoder,
  const EncoderParameters* encoderParams
);

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

В документации Platform SDK подробно описано назначение настроек при работе с различными кодеками. Для быстрого освоения допустимых значений настроек конкретного установленного кодека вы можете воспользоваться приведенной программой. Например, согласно ей, для кодека JPEG параметр Transformation может принимать следующие значения: 13, 14, 15, 16, 17. Им соответствуют следующие значения констант из GdiPlusEnums.h:

EncoderValueTransformRotate90,
EncoderValueTransformRotate180,
EncoderValueTransformRotate270,
EncoderValueTransformFlipHorizontal,
EncoderValueTransformFlipVertical

Эти константы описывают допустимые геометрические преобразования при сохранении файлов формата JPEG.

ПРИМЕЧАНИЕ

В GDI+ реализована полезная особенность: если загрузить файл JPEG с размерами, кратными 16, то при сохранении его с одним из вышеперечисленных преобразований дополнительной потери качества не произойдет.

А откуда взять значение CLSID для выбранного кодека? Можно, конечно, жестко завести в код константу, взяв ее, например, из окна приведенной программы. Но этот подход оправдан, только если приложение будет распространяться именно с этой версией библиотеки GDI+ – иначе GUID кодеков могут измениться. Microsoft рекомендует более гибкий способ.

Обычно для получения CLSID для кодека, скажем, JPEG, формируют строку вида "image/jpeg" и выполняют в этом списке поиск кодека, имеющего такую строку в поле MimeType. Реализация этого подхода имеется в документации – в разделе "Retrieving the Class Identifier for an Encoder" приведен исходный код функции GetEncoderClsid.

Если выбор кодека приходится делать несколько раз, то я предпочитаю другой метод – заполнение ассоциативного контейнера (например, std::map) списком кодеков, и последующую выборку кодека по GUID графического формата (поле FormatID), с которым мы уже сталкивались, обсуждая метод GetRawFormat:

// тип ассоциативного массива, хранящего пары: 
// FormatID - CLSID кодека.
typedef std::map<GUID, GUID> CodecsList;

// Определяем оператор «<» для сравнения GUID.
bool operator<(REFGUID g1, REFGUID g2)
{
  return memcmp(&g1, &g2, sizeof(GUID))<0;
}

// Параметр указывает, какой список кодеков необходим – 
// для сохранения или чтения.
CodecsList ReadCodecsList(bool Encoders)
{
  using namespace Gdiplus;
  UINT  num, size;   
  if(Encoders) 
  GetImageEncodersSize(&num, &size);
  else
  GetImageDecodersSize(&num, &size);

  // размер буфера - в байтах!
  ImageCodecInfo* pArray = (ImageCodecInfo*)(malloc(size));
  if(Encoders) 
  GetImageEncoders(num, size, pArray);
  else
  GetImageDecoders(num, size, pArray);

  // заполняем map
  CodecsList codecs;
  for(UINT j = 0; j < num; ++j)
  codecs[pArray[j].FormatID]=pArray[j].Clsid;

  free(pArray);
  return codecs;
}

Тогда преобразование из PNG в JPEG будет выглядеть примерно так:

Bitmap bm(L"test.png");
CodecsList codecsList = ReadCodecsList(true);
GUID JpegId = codecsList[ImageFormatJPEG];
... // работаем с растром
bm.Save(L"test2.jpg", JpegId); // см. предупреждение

.NET-программистам в данном случае можно расслабиться: такая функциональность уже реализована за них. Вся черная работа по сопоставлению FormatID и CLSID будет проделана в недрах библиотеки .NET (в методе FindEncoder). В метод же Save достаточно передать именованное свойство класса System.Drawing.Imaging.ImageFormat, содержащее значение FormatID соответствующего кодека:

Bitmap bm = new Bitmap("test.png");
... // работаем с растром
bm.Save("test2.jpg", ImageFormat.Jpeg);
ПРЕДУПРЕЖДЕНИЕ

Метод Save() не сработает, если ему будет передано имя файла, уже открытого средствами GDI+. Это происходит, видимо, потому, что файл остается занятым вплоть до выполнения деструктора Bitmap.

Другая проблема возникнет, если файл доступен для записи, но имеет длину, превышающую результирующий размер изображения. Save() не производит усечения, и в конце файла останется "мусор".

Эти проблемы известны Microsoft. Вторая уже нашла отражение в Knowledge Base: PRB: Save Method of Bitmap Class Does Not Truncate File Size.

Специфические возможности файловых форматов

Сохранение GIF с прозрачностью

Это наиболее популярная тема обсуждений в новостных группах, связанных с графическими форматами GDI+. Действительно, в Web GIF-файлы с прозрачными областями приобрели чрезвычайное распространение. Загрузка прозрачного GIF-файла не составляет никакого труда – кодек сам корректно распознает прозрачные области и устанавливает у них нулевую величину Alpha-канала.

Однако это и служит причиной того, что при сохранении возникают проблемы. Все дело как раз в том, что при загрузке файлы формата GIF преобразуются в 32-битный формат. Для сохранения же GIF необходима палитра. Кодек умеет сохранять такие растры только в так называемой Halftone palette – некой известной Microsoft стандартной таблице цветов. Нужные цвета при этом подгоняются смешиванием с соседними точками. Картинка начинает выглядеть довольно уродливо, и ни о какой прозрачности речи не идет.

Если вам нужно сохранять растры с прозрачностью без дополнительных сложностей, воспользуйтесь 32-битным форматом (PNG или TIFF) с альфа-каналом.

ПРИМЕЧАНИЕ

Здесь вас подстерегает другая неприятность: как я уже говорил, относительно новый формат PNG некорректно отображается многими браузерами, в частности, Internet Explorer не понимает PNG с альфа-каналом.

Тем не менее, средствами GDI+ создать GIF с прозрачностью можно. Это потребует прямой работы с битами изображения, так как изображение должно будет содержать не более 256 цветов и оставаться в индексном формате. Только тогда кодек распознает первый цвет в таблице цветов, содержащий 0 в поле Alpha, как прозрачный, и правильно сохранит файл. Настоящих комсомольцев отсылаю к соответствующим статьям Knowledge Base с полным описанием технологии процесса:

INFO: GDI+ GIF Files Are Saved Using the 8-Bpp Format (Q318343)
HOW TO: Save a .gif File with a New Color Table By Using GDI+ (Q315780)
HOW TO: Save a .gif File with a New Color Table By Using Visual C# .NET (Q319061)

Загрузка и сохранение многокадровых файлов

Некоторые графические форматы поддерживают хранение нескольких картинок в одном файле. Скажем, файл TIFF может содержать несколько изображений одной страницы, отсканированных с различным разрешением.

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

Раньше было довольно сложно загрузить с диска такой файл, не прибегая к разбору его формата на низком уровне. Например, функция API OleLoadPicture не могла самостоятельно загружать анимированные файлы GIF. Теперь вся работа выполняется кодеком GDI+, и загрузка многокадровой картинки ничем не отличается от обычной загрузки графического файла. Узнать число кадров загруженного растра можно с помощью функции Image::GetFrameCount:

UINT GetFrameCount(
  const GUID* dimensionID
);

В качестве параметра она принимает указатель на GUID-константу, определяющую тип хранимых кадров. Возможные значения описаны в заголовочных файлах GDI+. Для GIF необходимо передавать константу FrameDimensionTime, а для файлов TIFF – FrameDimensionPage:

frameCount = bitmap.GetFrameCount(&FrameDimensionTime);

Перед отрисовкой нужного кадра его необходимо сделать активным с помощью функции Image::SelectActiveFrame. Подробности можно найти в документации и исходном коде демонстрационных приложений к статье.

С сохранением дело обстоит немного хуже. В GDI+ версии 1.0 многокадровое сохранение реализовано только для файлов формата TIFF. Анимированные GIF таким образом сохранить, к сожалению, не удастся.

Итак, рассмотрим пошаговую процедуру создания файла TIFF с несколькими кадрами:

Здесь возможны варианты. Если все необходимые кадры находятся в исходном объекте Image, нужно просто последовательно делать их текущими (с помощью SelectActiveFrame) и вызывать вариант метода SaveAdd, принимающего только указатель на структуру EncoderParameters.

Можно также добавлять в сохраняемый файл кадры из других растров, вызывая метод SaveAdd, принимающий наряду с параметрами указатель на объект класса Image.

ULONG value;
EncoderParameters params={1}; // один параметр
params.Parameter[0].NumberOfValues = 1;
params.Parameter[0].Guid  = EncoderSaveFlag;
params.Parameter[0].Type  = EncoderParameterValueTypeLong;
params.Parameter[0].Value = &value;
... // сохранение кадров
value = EncoderValueFlush; // завершить последовательность
myImage.SaveAdd(&encoderParameters);

Эскизы изображений

GDI+ предоставляет очень удобный механизм для создания эскизов (иконок) изображений (thumbnail images), которые содержат миниатюрную копию картинки. Поскольку эскизы имеют малые размеры и быстро отображаются, их используют для предварительного просмотра изображений на веб-страницах, в приложениях просмотра картинок (типа ACDSee) и т.д.

Вообще-то, у нас уже есть все необходимое для создания эскизов. Нужно всего лишь создать Bitmap нужных размеров, инициализировать устройство вывода (Graphics) в этот растр и вывести картинку каким-либо приемлемым способом (например, установив режим вывода InterpolationModeHighQualityBicubic, о чем речь пойдет попозже). Однако создателям библиотеки этого показалось мало, и они добавили в класс Image метод GetThumbnailImage, который выполняет всю эту работу:

Image* GetThumbnailImage(
  UINT thumbWidth,
  UINT thumbHeight,
  GetThumbnailImageAbort callback,
  VOID* callbackData
);

Параметры thumbWidth и thumbHeight сообщают этой функции требуемые размеры иконки. Параметры callback и callbackData используются, если нужна возможность прерывания процесса создания эскиза. По умолчанию они равны NULL, и эта возможность игнорируется.

Каковы же преимущества этого способа? На первый взгляд, только простота (и, следовательно, ограниченные возможности). На самом же деле, GetThumbnailImage() является наиболее быстрым способом получения иконок. В большинстве графических форматов предусмотрено хранение миниатюры вместе с оригинальным файлом. Если передать в параметрах thumbWidth и thumbHeight значение 0, кодек формата постарается извлечь именно эту миниатюру (разумеется, при использовании неподходящего формата GDI+ придется создавать ее самостоятельно). Кроме того, при создании заведомо маленького изображения возможны другие оптимизации, которыми не обладает метод DrawImage.

Аналогичный метод имеется и у класса System.Drawing.Image в .NET Framework. Единственная разница в том, что вместо указателя на функцию GetThumbnailImageAbort в .NET необходимо передавать делегат Image.GetThumbnailImageAbort:

public Image GetThumbnailImage(
   int thumbWidth,
   int thumbHeight,
   Image.GetThumbnailImageAbort callback,
   IntPtr callbackData
);

Так как пример использования этой функции на C# довольно компактен, приведем код приложения целиком:

using System;
using System.Drawing;
using System.Drawing.Drawing2D;
using System.Drawing.Imaging;

class ThumbnailExtractor
{
  public static void Main(string[] args)
  {
  foreach(string arg in args) // перебор элементов массива
    SaveThumbnail(arg);
  }

  static void SaveThumbnail(string name)
  {
  try
  {
    Bitmap bm = new Bitmap(name);
    Image thumb = bm.GetThumbnailImage(0, 0, null, IntPtr.Zero);
    thumb.Save(name + "_tn.jpg", ImageFormat.Jpeg);
  }
  // возникла проблема?
  catch(Exception e)
  {
    Console.WriteLine(e); //неявно вызываем e.ToString()
  }
  }
};

Эта программа создает (или извлекает) эскизы всех файлов, указанных в командной строке, и сохраняет их в формате JPEG в том же каталоге, с добавлением суффикса к имени файла.

ПРИМЕЧАНИЕ

В MSDN сказано, что вы должны создать экземпляр делегата Image.GetThumbnailImageAbort, даже если не хотите его использовать (указав на функцию-заглушку). Однако, приведенный выше код вполне работает, передавая вместо делегата null.

Работа с метаданными изображений

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

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

class PropertyItem
{
public:
  PROPID  id;    // Уникальный номер
  ULONG   length;  // Длина поля (в байтах)
  WORD  type;  // Тип значения (один из типов PropertyTagTypeXXXX
  VOID*   value;   // Указатель на данные
};

Значение поля id определяет вид хранимой информации. Каждому графическому формату соответствует свой набор возможных ID, описанных в заголовочном файле GdiPlusImaging.h.

Для демонстрации работы с метаданными приведу небольшой пример на C#, выводящий перечень информационных полей для заданного графического файла:

using System;
using System.Drawing;
using System.Drawing.Drawing2D;
using System.Drawing.Imaging;

class ImageProperties
{
  public static void Main(string[] args)
  {
    foreach(string arg in args) 
    {
      Bitmap bm=new Bitmap(arg);
      foreach(PropertyItem i in bm.PropertyItems)
        Console.WriteLine("Property #{0}: type={1}, len={2}", 
          i.Id, i.Type, i.Len);
    }
  }
};

Использование растров при работе с объектом Graphics

Текущая реализация библиотеки оперирует 32-битным цветом при любых цветовых вычислениях. Растры c другим представлением цвета перед обработкой приводятся к 32-битному формату. Таким образом, хороший способ экономить ресурсы процессора и память системы – это сразу работать с 32-битными растрами (что и предлагается по умолчанию). Например, при создании экземпляра Graphics из контекста устройства обычного GDI (HDC) создается промежуточный 32-битный буфер, в который и осуществляется вывод, а уже затем производится копирование этого буфера в контекст. Если контекст устройства имеет другую глубину цвета, понадобится дополнительное преобразование, что плохо скажется на производительности. Кроме того, такая организация работы с графическими контекстами диктует свои ограничения на обращение к контексту средствами GDI – во время существования Graphics этого делать нельзя. Подробно этот вопрос описан в статье Q311221.

Вывод изображений и геометрические преобразования

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

Итак, в объекте Graphics для вывода растров предназначен метод DrawImage. Постойте, я сказал "предназначен метод"? Правильнее будет применить множественное число: их 16 штук! По своему назначению эти перегруженные функции разделяются на 2 основные группы:

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

void PaintFlower(Graphics& g, Rect& rc)
{
  ...
  g.DrawImage(flowerImage, flowerPos.X, flowerPos.Y, 
  flowerImage->GetWidth(), flowerImage->GetHeight());
}
ПРИМЕЧАНИЕ

Обратите внимание на выделенные параметры. Такой способ (явное указание размеров выводимого изображения) является предпочтительным, и вот почему.

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

Во-вторых, явно указав исходные пиксельные размеры растра, вы тем самым подсказываете GDI+, что масштабирования выполнять не нужно, и вывод произойдет быстрее.

2. Вывод изображения или его части в параллелограмм с соответствующим преобразованием координат всех точек исходного растра. В GDI отсутствует подобная функциональность (в семействе NT нечто подобное выполняет функция PlgBlt). Этим методам для работы требуется массив из 3-х точек, образующих вершины параллелограмма (четвертая вершина вычисляется на их основе).

Замечу, что частным случаем преобразования при последнем способе является и вращение – достаточно задать 3 координаты прямоугольника, повернутого на требуемый угол. Порядок точек в массиве должен соответствовать точкам A1, B1 и C1 на приведенной схеме:


Но в большинстве случаев для поворота выводимого растра проще будет применить координатные преобразования GDI+.

Кроме того, для поворота изображений на угол, кратный 90 градусам, или их отражения относительно осей симметрии, в классе Image существует метод

Status RotateFlip(RotateFlipType rotateFlipType);

принимающий в качестве параметра элемент перечисления RotateFlipType. Все возможные варианты перечислений GDI+ находятся в файле GdiPlusEnums.h.

Для более гибкого контроля над выводом изображения существуют версии метода DrawImage, принимающие указатель на объект класса ImageAttributes. Большая часть задаваемых при этом параметров относится к цветовой коррекции, которая обсуждается ниже. Нас же сейчас может заинтересовать один метод этого класса, влияющий на геометрию выводимых изображений:

Status SetWrapMode(
  WrapMode wrap,
  const Color& color,
  BOOL clamp
);

Он позволяет указать, каким образом заполнять область вывода за пределами исходной картинки. Вид заполнения определяется параметром wrap из перечисления WrapMode. В частности, указав WrapModeTile, вы получите вывод мозаикой (tiling). При задании режима WrapModeClamp внешняя область заполняется цветом, указанным в параметре color. Параметр clamp в текущей реализации игнорируется.

Кстати, это справедливо не только для растров, но и для вывода объектов Metafile, которые также являются потомками класса Image.

Упомянув метод SetWrapMode класса ImageAttributes, нельзя не упомянуть о методах с таким же названием, присутствующих в классах LinearGradientBrush, PathGradientBrush и TextureBrush. Они выполняют аналогичную роль при задании параметров закраски областей.

Качество изображения

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

Тем не менее, существуют различные алгоритмы интерполяции, позволяющие добиться значительного улучшения качества результирующего изображения, или, точнее говоря, его качества с точки зрения человеческого восприятия. Некоторые из них изначально встроены в библиотеку GDI+.

Метод Graphics::SetInterpolationMode позволяет указать, какой режим (или алгоритм) интерполяции будет использован при выводе изображения с изменением его пиксельных размеров. Константы возможных режимов описаны в перечислении InterpolationMode.

В .NET-версии в объекте Graphics имеется соответствующее свойство InterpolationMode.

Выигрыш в качестве в данном случае приводит к проигрышу в скорости, поэтому при использовании режима с наивысшим качеством InterpolationModeHighQualityBicubic медленные компьютеры могут выводить изображения больших размеров в течение нескольких секунд (речь идет об экранном выводе, то есть умеренно больших, так как бикубическая интерполяция изображений полиграфического разрешения может длиться минутами и на самых современных компьютерах – прим. ред.)! Но только этот метод способен адекватно отображать картинку при уменьшении ее до 25 процентов (и менее) от оригинала. Этот режим очень поможет различным автоматическим генераторам иконок (thumbnail) изображений в ASP.NET.

На качество (и скорость) вывода растров также влияют некоторые другие установки объектов Graphics. Перечислим их с кратким описанием:

МетодНазначение
SetSmoothingModeПозволяет указать метод устранения ступенчатости (antialiasing) при выводе примитивов – линий и геометрических фигур.
SetCompositingModeУстанавливает или отключает учет прозрачности при наложении растровых изображений.
SetCompositingQualityУправляет качеством расчета цветовых компонентов при наложении растров.
SetPixelOffsetModeЗадает метод учета смещения пикселов при интерполяции. Грубо говоря, определяет, являются ли координаты пикселов (или их центров) целыми числами при расчетах.
SetRenderingOriginУстанавливает позицию начальной точки при псевдосмешении (dithering) цветов в 8- и 16-битных режимах.

Для получения значений соответствующих настроек служат аналогичные методы с префиксом "Get". В среде Microsoft .NET Framework у класса Graphics существуют аналогичные свойства без префиксов вообще (SmoothingMode, PixelOffsetMode и т.д).

Устранение мерцания

Часто встречающейся проблемой при создании динамично меняющейся графики (и, в частности, анимации) является мерцание. Для его устранения традиционно использовалась двойная буферизация, и эта возможность также имеется в GDI+. Объект Graphics можно создать "теневым", используя в качестве основы готовый экземпляр Bitmap (вообще говоря, Image, но нас сейчас метафайлы не интересуют):

Graphics( Image* image );
static Graphics* FromImage( Image* image );

При этом все операции вывода с участием такого объекта отразятся на содержимом используемого экземпляра Bitmap. Это предоставляет очень простую возможность двойной буферизации вывода – когда изображение сначала готовится "за кадром", а затем мгновенно (ну, почти мгновенно) переносится на экран, устраняя досадное мерцание при анимации:

Вывод с двойной буферизацией в GDI+ (фрагмент демонстрационного приложения)
void OnPaint(HDC hdc, RECT& rc)
{
  Graphics g(hdc);
  Rect paintRect(0, 0, rc.right, rc.bottom);
  // Создаем временный буфер
  Bitmap backBuffer(rc.right, rc.bottom, &g);
  Graphics temp(&backBuffer);
  // Рисуем в буфер
  PaintBackground(temp, paintRect);
  PaintFlower(temp, paintRect);
  PaintBatterfly(temp, paintRect);
  // Переносим на экран
  g.DrawImage(&backBuffer, 0, 0, 0, 0, 
  rc.right, rc.bottom, UnitPixel);
}

Для создания "теневого" Graphics подойдет далеко не всякий растр – в частности, это невозможно для индексных и Grayscale-растров. Это связано именно с ограничениями ядра GDI+.

Вы можете столкнуться с мерцанием, даже используя двойную буферизацию. Дело в том, что при перерисовке окна ему сначала посылается сообщение WM_ERASEBKGND. Используемый по умолчанию обработчик этого сообщения закрашивает область, нуждающуюся в обновлении, кистью hbrBackground, указанной в свойствах класса окна. Для устранения такого рода мерцания необходимо, во-первых, подавить это поведение (перекрыв WM_ERASEBKGND или задав нулевую кисть hbrBackground при регистрации класса окна), а, во-вторых, позаботиться о собственном заполнении всего окна при рисовании.

Что касается WinForms, то там режим двойной буферизации уже предусмотрен. Для его включения необходимо в окне, в которое производится отрисовка (например, элементе управления или форме) установить флаги UserPaint, AllPaintingInWmPaint и DoubleBuffer перечисления System.Windows.Forms.ControlStyles:

protected override void OnLoad(EventArgs e)
{
  SetStyle(ControlStyles.UserPaint, true);
  SetStyle(ControlStyles.AllPaintingInWmPaint, true);
  SetStyle(ControlStyles.DoubleBuffer, true);
  ... // другая инициализация
  base.OnLoad(e);
}

При этом, кстати, вывод довольно заметно ускоряется (несмотря на необходимость дополнительного переноса на экран) – у меня демонстрационное приложение вместо 70 FPS стало выдавать 75-80.

Несколько слов о производительности

Производительность в данный момент является "ахиллесовой пятой" библиотеки. Изначально GDI+ проектировалась с прицелом на аппаратную акселерацию, но в версии 1.0 она не реализована. Остается надеяться, что этот недостаток будет устранен в ближайшем будущем: ведь попиксельный расчет полупрозрачности и анти-алиасинга не являются самыми быстрыми операциями.

Раз уж речь зашла о скорости, думаю, уместно будет сказать, что наиболее быстро GDI+ умеет выводить растры оптимизированного для устройства формата, представленные классом CachedBitmap. При их создании необходимо указать оригинальный растр и устройство, на которое будет происходить вывод изображения:

CachedBitmap(Bitmap* bitmap, Graphics* graphics);

На этот конструктор накладываются определенные ограничения. В частности, устройство вывода Graphics не должно быть связано с HDC принтера или метафайла. Далее, при смене характеристик устройства вывода (например, при изменении разрешения или глубины цвета экрана) CachedBitmap необходимо пересоздавать для работы с новым устройством, в противном случае вывод производиться не будет.

Для вывода оптимизированных растров на экран служит метод Graphics::DrawCachedBitmap:

Status DrawCachedBitmap(
  CachedBitmap* cb,
  INT x,
  INT y
);

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

Хорошая новость: DrawCachedBitmap поддерживает прозрачность и альфа-канал (проверено в лабораторных условиях).

Плохая новость: применение координатных преобразований к устройству вывода при этом не поддерживается (кроме координатного переноса). В частности, поворачивать такие растры при выводе, к сожалению, нельзя. Если это необходимо, примените технику промежуточного вывода в память с поворотом, а затем уже кэшируйте полученный Bitmap. Разумеется, этот прием не подойдет, если угол поворота меняется динамически (тогда лучше совсем отказаться от кэширования, чтобы не тратить каждый раз время на создание промежуточного изображения).

Как быть с кэшированием анимированных (многокадровых) изображений? Очень просто: вам потребуется ровно столько объектов CachedBitmap, сколько кадров содержится в анимации. А для того, чтобы кэшировать очередной кадр, необходимо сделать его активным (как и перед выводом его на экран) с помощью метода Image::SelectActiveFrame. Нижеприведенный код иллюстрирует такую технику.

Пример кэширования многокадрового изображения формата GIF
// необходимые переменные
Bitmap *batterflyImage;       // анимированное изображение бабочки
CachedBitmap **cachedFrames;  // здесь будут размещаться кэшированные кадры
int frameCount;
int activeFrame;
...
// инициализация
frameCount = batterflyImage->GetFrameCount(&FrameDimensionTime);
cachedFrames = new CachedBitmap*[frameCount];

// создаем объект Graphics на базе окна десктопа
// и считаем, что текущий видеорежим не изменится
Graphics g(GetWindowDC(0)); 

// собственно кэширование
for(int i=0;i<frameCount;i++)
{
  batterflyImage->SelectActiveFrame(&FrameDimensionTime, i);
  cachedFrames[i] = new CachedBitmap(batterflyImage, &g); 
}
activeFrame=0;
batterflyImage->SelectActiveFrame(&FrameDimensionTime, 0);

Перед тем, как использовать CachedBitmap, подумайте о возможных неудобствах: вам придется отлавливать момент изменения видеорежима и соответственно перестраивать все оптимизированные растры. Кроме того, выгода от их применения невелика: у меня в разных тестах она не превышала 9% (при избавлении от многих других тормозящих операций). Куда выгоднее оказалось вынести создание промежуточного контекста Graphics из функции OnPaint в код инициализации, хотя и это не панацея: такой буфер придется пересоздавать при изменении размеров окна.

И последнее. Ничего, напоминающего технику DrawCachedBitmap, в среде .NET я не нашел.

Демонстрационные приложения

В завершении данного раздела приведу два небольших демонстрационных приложения.

Первое написано с использованием C++-варианта этой библиотеки. Оно иллюстрирует применение многих описанных приемов работы: отложенную загрузку GdiPlus.DLL, загрузку растров формата GIF из ресурсов программы и использование двойной буферизации для устранения мерцания. Кроме того, в исходном коде есть (хотя и упрощенный) пример работы с анимированным изображением формата GIF.

Вы также можете использовать это приложение как полигон для собственных испытаний производительности GDI+. В обработчике WM_PAINT главного окна немедленно вызывается функция InvalidateRect, гарантируя приход следующего сообщения WM_PAINT, и вычисляется число кадров, выводимых в секунду.

Demo.zip – Демонстрационный проект (VC++ 5.0 - 7.0)
Demo_exe.zip – Откомпилированное приложение (требуется GDI+)


Второе написано на языке C# и представляет собой пример вывода анимированных файлов GIF в окне WinForms-программы. В нем также реализованы подсчет производительности (с использованием класса System.Timers.Timer) и двойная буферизация.


Animated.zip – исходный файл (C#).
Animated_exe.zip – откомпилированное приложение (требуется .NET Runtime).

В обоих примерах вывод анимации намеренно происходит с максимально возможной скоростью, для наглядного показа производительности. В реальном же приложении потребуется получить параметры задержки кадров из графического файла (вызвав функцию Image::GetPropertyItem с параметром PropertyTagFrameDelay). Кроме того, в .NET есть специальный вспомогательный класс System.Drawing.ImageAnimator, который облегчает задачу анимации.

Прямая работа с растровыми данными

К сожалению, у библиотеки GDI+ существуют и ограничения. Некоторые из них связаны с дефектами текущей реализации, и, возможно, вскоре исчезнут. Другие же напрямую следуют из новой архитектуры библиотеки, которая попыталась избавиться от "проклятого наследия" кое-каких архаизмов GDI. Так или иначе, зачастую приходится обращаться напрямую к растровым данным, содержащимся внутри этих красивых оберток. В данном разделе мы узнаем, какие возможности для этого предоставляет GDI+.

Класс Color

В 32-битной цветовой модели наконец-то нашлось применение четвертому байту: он больше не является просто мусором для выравнивания (как в структуре RGBQUAD, например), а законно хранит значение Alpha – технически говоря, величину непрозрачности пиксела. Это было учтено при проектировании класса Color – у него появился соответствующий конструктор:

Color(
  BYTE a, // alpha
  BYTE r, // red
  BYTE g, // green
  BYTE b  // blue
);

Если используется более традиционная форма конструктора Color с тремя цветовыми компонентами, то значение Alpha устанавливается равным 255 (полная непрозрачность). Кроме того, в классе Color описан большой набор именованных цветовых констант (например, Color::AliceBlue) – в них значение Alpha также равно 255.

В среде .NET принят немного другой подход: для инициализации структуры Color у нее существует множество статических свойств – значений цвета (например, DeepSkyBlue), а также методов (например, семейство методов FromArgb). При этом величина каждого из четырех цветовых компонентов также не может превышать 8 бит:

public static Color FromArgb(int, int, int, int);

У структуры Color в .NET дополнительно имеются такие полезные качества, как возможность преобразования цвета в модель HSB (Hue-Saturation-Brightness, Оттенок-Насыщенность-Яркость):

public float GetBrightness();
public float GetHue();
public float GetSaturation();

а также реализация стандартного метода ToString, позволяющая получить строку с названием цвета (если это возможно) или с перечислением его ARGB-компонентов (в противном случае):

public override string ToString();

Прямой доступ к пикселам

Для получения и установки цвета определенной точки растра класс Graphics предоставляет методы GetPixel/SetPixel:

Status GetPixel(INT x, INT y, Color* color);
Status SetPixel(INT x, INT y, const Color& color);

Что можно о них сказать? Используйте их только при крайней необходимости – их производительность ужасна (как, впрочем, и производительность аналогичных функций GDI). Если есть желание нарисовать изображение этим способом, лучше один раз нарисовать его в объекте Graphics, созданном на базе Bitmap, а потом выводить на экран кэшированное изображение из Bitmap.

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

Status LockBits(
  IN const Rect* rect,             // область растра для доступа
  IN UINT flags,                   // параметры доступа (чтение, запись)
  IN PixelFormat format,           // константа перечисления PixelFormat
  OUT BitmapData* lockedBitmapData // место для выходных данных 
);

Параметр flags формируется из констант перечисления ImageLockMode. Помимо вида доступа к растру, он может содержать флаг ImageLockModeUserInputBuf, указывающий на то, что поле lockedBitmapData->Scan0 уже содержит указатель на выделенный пользователем буфер достаточного размера. Подробное описание этой функции и примеры ее использования можно найти в Platform SDK Documentation. В среде .NET эта функция также реализована, хотя для доступа к растровым данным вам придется применять unsafe-код.

ПРИМЕЧАНИЕ

На самом деле вызов LockBits приводит к копированию указанной области во временный буфер. Изменение растровых данных в этом буфере отразится на содержимом Bitmap только после обратного вызова UnlockBits с тем же указателем lockedBitmapData в качестве параметра.

При этом, если указать формат временного буфера (PixelFormat), отличный от формата исходного растра, вызовы LockBits/UnlockBits потребуют дополнительных преобразований.

Для доступа к растру исходного изображения можно также воспользоваться следующим (недокументированным) обстоятельством. В GDI+ версии 1 метод Bitmap::GetHBitmap всегда возвращает DIB Section. Вам достаточно вызвать GetObject() для этого HBITMAP, чтобы получить растровые данные и необходимые структуры BITMAPINFO (пример работы с DIB Sections см. в Q186221). Однако на это поведение нельзя полагаться в будущих версиях библиотеки.

Поддержка прозрачности

Если возникла необходимость назначить прозрачным определенный цвет растра, сделать это можно несколькими способами. Во-первых, используя прямую замену цветов при помощи функций GetPixel/SetPixel. При этом придется пройтись по всем точкам картинки, заменяя точки выбранного цвета на прозрачные. Как уже говорилось, этот способ не является быстрым. Во-вторых, можно применить прямой доступ к памяти посредством вызова LockBits.

Для .NET необязательно возиться с заменой цветов и "сырыми" растрами, возвращаемыми вызовом LockBits. В классе System.Drawing.Bitmap реализован метод MakeTransparent, который делает прозрачным все точки растра, имеющие выбранный цвет:

Bitmap bm=new Bitmap("test.bmp");
// примем цвет первой точки растра за прозрачный
Color backColor = bm.GetPixel(0, 0);
bm.MakeTransparent(backColor);

А что, если исходную картинку модифицировать нельзя? Вы, конечно, можете воспользоваться клонированием растра или его фрагмента (используя метод Bitmap::Clone) и совершить замену пикселов дубликата. Однако в GDI+ есть более простой метод, поддерживающий прозрачность только при выводе картинок. Для этого необходимо создать экземпляр класса ImageAttributes, с которым мы уже столкнулись при обсуждении методов Graphics::DrawImage. Этот класс позволяет корректировать многие цветовые параметры выводимого изображения. В частности, метод SetColorKey позволяет указать, что определенный диапазон цветов (вернее, все пикселы, компоненты цвета которых лежат в этом диапазоне) будет заменяться при выводе прозрачным цветом. Посмотрите, например, что станет с бабочкой из демонстрационного проекта, если так модифицировать фрагмент метода PaintBatterfly:

ImageAttributes attr;
// все цвета в диапазоне (0,0,0,0) – (100,100,100,100) станут прозрачными
attr.SetColorKey(Color(0, 0, 0, 0), Color(100, 100, 100, 100));
Rect destRect(batterflyPos, Size(batterflyImage->GetWidth(), 
        batterflyImage->GetHeight()));
g.DrawImage(batterflyImage, destRect, 0, 0, 
        batterflyImage->GetWidth(), 
        batterflyImage->GetHeight(),UnitPixel, &attr);


Если вы действительно вставите данный кусок в демонстрационную программу, то заметите досадную ошибку в текущей версии GDI+. При передаче в метод Graphics::DrawImage ненулевого указателя на класс ImageAttributes начинает выводиться только первый кадр анимации, несмотря на то, что активным кадром может быть любой другой.

Кроме прямой замены цветов, класс ImageAttributes поддерживает так называемый recoloring – использование матричной алгебры для выполнения вычислений над каждым цветовым компонентом пикселов. Так как Alpha-компонент полноправно участвует в цветовых вычислениях, это позволяет использовать recoloring, например, для увеличения степени прозрачности всего изображения в два раза. Вот соответствующая матрица:

|   1   0   0   0   0|
|   0   1   0   0   0|
|   0   0   1   0   0|
|   0   0   0 0.5   0|
|   0   0   0   0   1|

Каждый элемент этой матрицы является коэффициентом в диапазоне [0,1]. Число 0.5 на главной диагонали в четвертой строке означает, что при умножении вектора из 5 элементов (четырех цветовых и одного фиктивного, необходимого для вычислений) на эту матрицу 4-й элемент вектора-результата будет равен исходному элементу, умноженному на 0.5. А это как раз и есть компонент Alpha! Вот код, который подготавливает такое преобразование:

ColorMatrix colorMatrix = 
{
  1.0f, 0.0f, 0.0f, 0.0f, 0.0f,
  0.0f, 1.0f, 0.0f, 0.0f, 0.0f,
  0.0f, 0.0f, 1.0f, 0.0f, 0.0f,
  0.0f, 0.0f, 0.0f, 0.5f, 0.0f,
  0.0f, 0.0f, 0.0f, 0.0f, 1.0f
};
attr.SetColorMatrix(&colorMatrix,ColorMatrixFlagsDefault, ColorAdjustTypeBitmap);

А вот результат его применения к демонстрационному проекту:


Растровые операции

В форумах и группах новостей Usenet часто задается вопрос: а как можно заставить GDI+ использовать растровые операции (ROP), определенные в GDI? Например, для выделения набора объектов было бы неплохо их инвертировать (или закрасить инверсным прямоугольником). В САПР может понадобиться рисовать инверсную фигуру или контур (макет) будущего объекта. Кроме того, режим R2_XOR позволяет очень просто восстановить изображение под объектом, всего лишь повторно нарисовав объект на том же месте.

Специалисты Microsoft обычно отвечают в духе Дзен: "На самом деле, вам не нужна такая возможность. XOR-графика попросту уродлива (согласен! – В.Б.). Современные 32-битные графические видеорежимы позволяют выделять и накладывать изображения с помощью Alpha-канала. Применение различных кодов ROP для достижения прозрачности также устарело – прозрачность изначально реализована в GDI+". И действительно, в GDI+ вообще не поддерживаются ROP. Если попробовать "силой" выставить контексту устройства, например, режим R2_XOR, он будет проигнорирован при выводе.

Ну что ж, у программистов на C++ еще остается старушка GDI – только не забывайте про уже упомянутые проблемы взаимодействия, описанные в Q311221. А как быть работающим в среде .NET? Оказывается, так же: для .NET существует класс System.Windows.Forms.ControlPaint, не входящий в иерархию GDI+. Его методы фактически обращаются к соответствующим низкоуровневым средствам GDI32.DLL. Для рисования инверсных линий и прямоугольников он предоставляет методы DrawReversibleLine, DrawReversibleFrame и FillReversibleRectangle. Быстрый взгляд в недра последнего подтверждает догадку (без GDI, как видно, иногда не обойтись):

...
IL_002f:  ldarg.1
IL_0030:  call  int32 [System.Drawing]System.Drawing.ColorTranslator::
                      ToWin32(valuetype [System.Drawing]System.Drawing.Color)
IL_0035:  call  native int
      System.Windows.Forms.SafeNativeMethods::CreateSolidBrush(int32)
IL_003a:  stloc.3
IL_003b:  ldloc.2
IL_003c:  ldloc.1
IL_003d:  call     int32 System.Windows.Forms.SafeNativeMethods::
      SetROP2(native int, int32)
...

Как использовать эти методы? Вот ссылки на статьи Knowledge Base c соответствующими примерами:

HOW TO: Draw a Rubber Band Rectangle or Focus Rectangle in Visual C# (Q314945)
HOW TO: Draw a Rubber Band or Focus Rectangle in Visual Basic .NET (Q317479)

Не зная о существовании этих статей, я самостоятельно набрел на эту возможность и написал пример на C#, позволяющий инвертировать часть изображения. Попробуйте запустить пример, нажать где-нибудь на свободном участке формы левую кнопку мыши и потащить курсор. Выделенный участок инвертируется даже за пределами формы – взаимодействие высокоуровневой GDI+ и низкоуровневой GDI налицо!


RevFrame.zip - исходный файл (C#).
RevFrame_exe.zip - откомпилированная программа (требуется .NET Runtime).

Вот, пожалуй, и все. Пусть вас не пугает, что значительная часть статьи посвящена обходу различных ошибок и подводных камней в GDI+ – так всегда бывает с новыми технологиями. Однако все возрастающая ее популярность и внимательное отношение Microsoft к ее пользователям (подпишитесь на группу новостей microsoft.public.win32.programmer.gdi и сами в этом убедитесь!) не оставляют сомнений в том, что GDI+ будет развиваться и дальше.

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


Эта статья опубликована в журнале RSDN Magazine #1. Информацию о журнале можно найти здесь