
Недавно решил ознакомиться с платформой .NET, языком C# и Windows Presentation Foundation.
В процессе изучения (а изучаю языки и технологии я всегда в процессе разработки пробного проекта) мне встретилось довольно много подводных камней и тонких моментов. Поделиться с хабрасообществом (я полагаю, что многим начинающим разработчикам WPF это было бы интересно) хочется всем и сразу, но объем получившегося хабратопика был бы слишком большим, поэтому я решил начать с метаданных изображений, т.к. на эту тему информации даже в англоязычном интернете маловато.
Вообще метаданные могут иметься у изображений различных форматов, однако я буду рассказывать на примере JPEG-а, т.к. работал именно с ним. Я думаю, для других форматов разница будет невелика.
Типы метаданных
Для начала разберемся, какие вообще типы метаданных могут быть в изображении. Все скорее всего это итак знают, но на всякий случай расскажу:
- EXIF (Exchangeable Image File Format) — стандарт хранения метаданных в изображении, который используется цифровыми камерами для сохранения информации о выдержке, диафрагме и других параметрах съемки. Метаданные в формате EXIF могут храниться в файлах форматов JPEG, TIFF и RIFF WAV. По стандарту из пользовательских описательных метаданных в EXIF может храниться только описание (тег Description) и комментарий (тег «User Comment»), но Windows Explorer использует также несколько дополнительных тегов (XPTitle, XPSubject, XPAuthor, XPComment, XPKeywords). Windows Explorer игнорирует тег XPTitle при наличии стандартного тега Description.
- IPTC (International Press Telecommunications Council) — название скорее организации, разработавшей стандарт. Сам стандарт метаданных называется IIM (Information Interchange Model). Самый старый из описываемых стандартов. В изначальной версии стандарта метаданные хранились так, что ПО, не знающее о существовании IPTC, не могло работать с файлами изображений, в которых были такие метаданные. Однако позже Adobe расширила стандарт, перенеся метаданные в блок APP13 JPEG-файла, что позволило ПО, не знающему о стандарте, успешно читать JPEG-файл, игнорируя неизвестные метаданные. В метаданных IPTC могут храниться такие описательные поля, как ObjectName (заголовок), Keywords (ключевые слова), Caption (описание, есть несколько вариаций тега).
- XMP (eXtensible Metadata Platform) — стандарт, разработанный Adobe. Метаданные хранятся в модели RDF, представленной в формате XML, позволяя включать любую необходимую информацию в файл изображения. Именно этот формат предпочитает использовать WIC (Windows Imaging Component) в Windows Vista/7.
Принципы работы с метаданными в WPF
Для работы с метаданными в WPF используются классы BitmapEncoder, BitmapDecoder, BitmapSource, BitmapFrame, BitmapMetadata, InPlaceMetadataWriter.
У классов BitmapEncoder и BitmapDecoder есть наследники, позволяющие работать с конкретными форматами изображений. В моем случае — JpegBitmapEncoder и JpegBitmapDecoder.
Класс InPlaceMetadataWriter используется для изменения метаданных прямо на месте, без перекодирования файла.
Данные читать и записывать можно двумя методами — либо с помощью функций GetQuery/SetQuery, оперирующих с иерархическими именами тегов метаданных, либо с помощью полей класса BitmapMetadata, позволяющих легко обращаться к метаданным.
При обращении к метаданным через поля класса BitmapMetadata, WIC пытается найти соответствующие поля в метаданных разных стандартов в следующем порядке: сначала XMP, затем IPTC и EXIF. При записи тегов через поля класса BitmapMetadata, WIC записывает их в формате XMP.
Чтение метаданных
Вот готовый пример функции, с помощью которой можно читать метаданные из файла:
- FileStream f = File.Open("test.jpg", FileMode.Open);
- BitmapDecoder decoder = JpegBitmapDecoder.Create(f, BitmapCreateOptions.IgnoreColorProfile, BitmapCacheOption.Default);
- BitmapMetadata metadata = (BitmapMetadata)decoder.Frames[ 0].Metadata;
- // Получаем заголовок через поле класса
- string title = metadata.Title;
- // Получаем заголовок из XMP
- string xmptitle = (string)metadata.GetQuery(@"/xmp/<xmpalt>dc:title");
- // Получаем заголовок из EXIF
- string exiftitle = (string)metadata.GetQuery(@"/app1/ifd/{ushort=40091}");
- // Получаем заголовок из IPTC
- string iptctitle = (string)metadata.GetQuery(@"/app13/irb/8bimiptc/iptc/object name");
Тут все достаточно просто и прозрачно, поэтому сразу перейдем к записи.
Запись метаданных
- BitmapMetadata md = new BitmapMetadata("jpg");
- md.SetQuery(@"/xmp/<xmpalt>dc:title", xmptitle);
- md.SetQuery(@"/app1/ifd/{ushort=40091}", exiftitle);
- md.SetQuery(@"/app13/irb/8bimiptc/iptc/object name", iptctitle);
- BitmapFrame frame = BitmapFrame.Create(decoder.Frames[ 0], decoder.Frames[ 0].Thumbnail, md, decoder.Frames[ 0].ColorContexts);
- BitmapEncoder encoder = new JpegBitmapEncoder();
- encoder.Frames.Add(frame);
- FileStream of = File.Open("test2.jpg", FileMode.Create, FileAccess.Write);
- encoder.Save(of);
- of.Close();
Код идет, как продолжение фрагмента, читающего метаданные. Мы создаем копию оригинального файла, записав в его метаданные тайтл во всех трех форматах метаданных.
Редактирование метаданных «на месте»
До сих пор я рассказывал вобщем-то достаточно хорошо документированные и простые вещи, однако здесь все уже сложнее. Пример в официальной документации (MSDN) неверен и вообще противоположен по смыслу реальному положению вещей.
Для редактирования метаданных «на месте» необходимо создать объект класса InPlaceBitmapMetadataWriter:
- InPlaceBitmapMetadataWriter writer;
- writer = decoder.Frames[ 0].CreateInPlaceBitmapMetadataWriter();
После этого с ним можно работать, как с обычным BitmapMetadata, вызывая SetQuery для задания нужных метаданных.
Чтобы сохранить изменения, нужно вызвать метод TrySave(), пытающийся сохранить изменения в оригинальный поток. Попытка записи может быть успешной, а может и нет. При успешной попытке метод возвращает true, при ошибке — false.
Самая частая ошибка, которая может помешать записать изменения — в метаданных недостаточно свободного места. Как правило, все свежеснятые фотографии не содержат в метаданных достаточного места, поэтому для того, чтобы начать пользоваться редактированием метаданных на месте, следует один раз сделать копию файла, дополнив метаданные в нем специальными полями padding, оставляющими свободное место для последующих изменений. Для этого файл открывается, нужный кадр и его метаданные клонируются, и выполняется несколько запросов:
- BitmapFrame frame = (BitmapFrame)decoder.Frames[ 0].Clone();
- BitmapMetadata metadata = (BitmapMetadata)decoder.Frames[ 0].Metadata.Clone();
- metadata.SetQuery("/app1/ifd/PaddingSchema:Padding", 2048);
- metadata.SetQuery("/app1/ifd/exif/PaddingSchema:Padding", 2048);
- metadata.SetQuery("/xmp/PaddingSchema:Padding", 2048);
- BitmapFrame newframe = BitmapFrame.Create(frame, frame.Thumbnail, metadata, original.Frames[ 0].ColorContexts);
После этого кадр достаточно закодировать энкодером и записать в нужный поток, в результате чего в изображении появится свободное место для редактирования метаданных на месте впоследствии.
Значение паддинга в 2048 байт как правило достаточно. Если вам необходимо больше — можно указать большее значение.
Строки запросов
Я думаю у всех при изучении методов SetQuery/GetQuery возникает резонный вопрос — откуда брать все эти строки запросов, которые простыми и интуитивно понятными не назовешь?
После продолжительных поисков в MSDN нашелся соответствующий список. Здесь есть пожалуй все необходимые запросы. Отсутствующие можно в принципе составить по аналогии, примеров — предостаточно :)
Тонкости и подводные камни
- Версии WIC в Windows XP и Windows Vista могут глючить, если у вызывающего функцию JpegBitmapEncoder.Save() потока не указан атрибут STAThread (по умолчанию, все создаваемые в приложении потоки получают атрибут MTAThread, если не указано обратное).
- Версия WIC в Windows 7 сохраняет значения тега EXIF UserComment по умолчанию в Unicode, тогда как в Windows XP и Windows Vista — в кодировке текущего языка системы (CP1251 для русского). Формат записи UTF-8 параметров такой: само значение тега сохраняется не как строка, а как массив байт. Первые 7 байт — ASCII строка «UNICODE», после чего начинается Unicode-закодированная последовательность символов тега.
- К параметру BitmapCacheOptions следует относиться внимательно. Значение OnLoad кэширует все данные изображений в несжатом виде в RAM, поэтому если вы откроете штук 20 крупноформатных JPEG-ов с этой опцией — свободная память будет съедена очень быстро. Эта память не освобождается при удалении самих классов изображений (BitmapFrame, BitmapDecoder и пр.) и обработке их сборщиком мусора. Кроме того, для использования InPlaceBitmapMetadataWriter следует открывать изображение с BitmapCacheOptions = OnDemand или Default.
- В примере я открываю изображение с флагом IgnoreColorProfile, т.к. без него на некоторых изображениях BitmapDecoder выбрасывает исключение.
Заключение
В целом работа с метаданными с помощью WPF мне показалась достаточно сложной и запутанной. Практически все из описанных подводных камней стоили мне нескольких часов отладки и гугления, информации об этом нигде нет, а симптомы иногда очень странные. Официальная документация (MSDN) освещает этот вопрос плохо, а местами и вовсе неверна.
Надеюсь, что эта собранная информация поможет тем, кому понадобится работать с метаданными через WPF, и сэкономит им несколько часов времени :)
P.S. Буду рад увидеть в комментариях замечания (если я где-то ошибся) и описания подводных камней, с которыми я не встречался или забыл упомянуть.
P.P.S. Стоит ли продолжать писать о WPF, или я пишу давно известные вещи?