Открыть список
Как стать автором
Обновить

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

.NET
image
Недавно решил ознакомиться с платформой .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.

Чтение метаданных



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

  1. FileStream f = File.Open("test.jpg", FileMode.Open);
  2. BitmapDecoder decoder = JpegBitmapDecoder.Create(f, BitmapCreateOptions.IgnoreColorProfile, BitmapCacheOption.Default);
  3. BitmapMetadata metadata = (BitmapMetadata)decoder.Frames[ 0].Metadata;
  4. // Получаем заголовок через поле класса
  5. string title = metadata.Title;
  6. // Получаем заголовок из XMP
  7. string xmptitle = (string)metadata.GetQuery(@"/xmp/<xmpalt>dc:title");
  8. // Получаем заголовок из EXIF
  9. string exiftitle = (string)metadata.GetQuery(@"/app1/ifd/{ushort=40091}");
  10. // Получаем заголовок из IPTC
  11. string iptctitle = (string)metadata.GetQuery(@"/app13/irb/8bimiptc/iptc/object name");


Тут все достаточно просто и прозрачно, поэтому сразу перейдем к записи.

Запись метаданных



  1. BitmapMetadata md = new BitmapMetadata("jpg");
  2. md.SetQuery(@"/xmp/<xmpalt>dc:title", xmptitle);
  3. md.SetQuery(@"/app1/ifd/{ushort=40091}", exiftitle);
  4. md.SetQuery(@"/app13/irb/8bimiptc/iptc/object name", iptctitle);
  5. BitmapFrame frame = BitmapFrame.Create(decoder.Frames[ 0], decoder.Frames[ 0].Thumbnail, md, decoder.Frames[ 0].ColorContexts);
  6. BitmapEncoder encoder = new JpegBitmapEncoder();
  7. encoder.Frames.Add(frame);
  8. FileStream of = File.Open("test2.jpg", FileMode.Create, FileAccess.Write);
  9. encoder.Save(of);
  10. of.Close();


Код идет, как продолжение фрагмента, читающего метаданные. Мы создаем копию оригинального файла, записав в его метаданные тайтл во всех трех форматах метаданных.

Редактирование метаданных «на месте»



До сих пор я рассказывал вобщем-то достаточно хорошо документированные и простые вещи, однако здесь все уже сложнее. Пример в официальной документации (MSDN) неверен и вообще противоположен по смыслу реальному положению вещей.
Для редактирования метаданных «на месте» необходимо создать объект класса InPlaceBitmapMetadataWriter:

  1. InPlaceBitmapMetadataWriter writer;
  2. writer = decoder.Frames[ 0].CreateInPlaceBitmapMetadataWriter();


После этого с ним можно работать, как с обычным BitmapMetadata, вызывая SetQuery для задания нужных метаданных.
Чтобы сохранить изменения, нужно вызвать метод TrySave(), пытающийся сохранить изменения в оригинальный поток. Попытка записи может быть успешной, а может и нет. При успешной попытке метод возвращает true, при ошибке — false.
Самая частая ошибка, которая может помешать записать изменения — в метаданных недостаточно свободного места. Как правило, все свежеснятые фотографии не содержат в метаданных достаточного места, поэтому для того, чтобы начать пользоваться редактированием метаданных на месте, следует один раз сделать копию файла, дополнив метаданные в нем специальными полями padding, оставляющими свободное место для последующих изменений. Для этого файл открывается, нужный кадр и его метаданные клонируются, и выполняется несколько запросов:

  1. BitmapFrame frame = (BitmapFrame)decoder.Frames[ 0].Clone();
  2. BitmapMetadata metadata = (BitmapMetadata)decoder.Frames[ 0].Metadata.Clone();
  3. metadata.SetQuery("/app1/ifd/PaddingSchema:Padding", 2048);
  4. metadata.SetQuery("/app1/ifd/exif/PaddingSchema:Padding", 2048);
  5. metadata.SetQuery("/xmp/PaddingSchema:Padding", 2048);
  6. 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, или я пишу давно известные вещи?
Теги:CWPFметаданные
Хабы: .NET
Всего голосов 84: ↑60 и ↓24 +36
Просмотры16.3K

Комментарии 20

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

Похожие публикации

Программист С# (WPF, CAD, middle)
от 80 000 ₽RubiusМожно удаленно
.net developer
от 100 000 до 200 000 ₽БАРС ГрупКазаньМожно удаленно
.NET C# Software Engineer
от 3 500 до 4 000 $Hand2NoteМожно удаленно
Senior .Net Engineer (C#)
до 230 000 ₽ItivitiСанкт-Петербург

Лучшие публикации за сутки