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

    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, или я пишу давно известные вещи?

    Средняя зарплата в IT

    120 000 ₽/мес.
    Средняя зарплата по всем IT-специализациям на основании 7 204 анкет, за 1-ое пол. 2021 года Узнать свою зарплату
    Реклама
    AdBlock похитил этот баннер, но баннеры не зубы — отрастут

    Подробнее

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

    • НЛО прилетело и опубликовало эту надпись здесь
        0
        Мне тоже в один прекрасный момент потребовалось использовать встроенные в WPF конструкты для работы с изображениями. Действительно, товар сыроват, и хотя возможность рендерить любой XAML-контент в битмап это хорошая возможность, но количество «подводных камней» просто поражает.

        Спасибо за пост, ждем следующих.
          0
          Да, я тоже слышал о такой возможности, но пока не доводилось пробовать пользоваться.

          А топик вызвал очень противоречивую реакцию, собрал уже 12 минусов ) Наверное ненавистники .NET технологии отыгрываются.
            +4
            А топик вызвал очень противоречивую реакцию, собрал уже 12 минусов ) Наверное ненавистники .NET технологии отыгрываются

            Ну, это же Хабр :-)
          0
          Пишите ещё! Как минимум — по одному топику мало оценивать очевидность описываемых вещей…
          А что касается данной статьи — то для меня было очень интересно в связи с актуальностью задачи, нужно было написать тулзу для прописывания EXIF-данных для последующего их считывания на php. Нашёл море примеров как читать метаданные (облазил www.codeproject.com), но не нашёл как записывать их. Спасибо вам за пример, сегодня попробую!
            0
            Спасибо! Рад, что кому-то информация пригодилась :) Для этого писал.

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

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

            Какая тематика вам вообще больше интересна?
              0
              >Какая тематика вам вообще больше интересна?

              Полноценные темплейты были бы действительно интересны.

              >немного о многопоточности хотел рассказать, там есть пара тонкостей (о многопоточности вообще есть целая хорошая статья на русском, гуглится на раз-два…

              Многопоточность в WPF? Она ведь запрещена в WPF, поскольку приложение работате STAThread, а вся синхронизация работы с другими потоками осуществляется через Dispatcher… Или я чего упустил?
                0
                Именно о синхронизации между UI потоком и другими потоками при использовании WPF я и хотел рассказать. Там в принципе тоже самое, что и в WinForms, но в любом случае не сразу догадаешься до некоторых вещей.
            0
            С многопоточностью тоже столкнулся, когда писал программу выгрузки дампа базы на хостинг-сервер, тоже было прочитано не мало статей ))
            А вот с темплейтами не разбирался, было бы интересно почитать!
              0
              Статью бы да пол года назад))))) Сам через все прошел — тогда думал что задачка на пол часа оказалась значительно больше.
              InPlaceMetadataWriter тогда не заметил — это полезная новая информация — буду смотреть на днях. Спасибо.

              От себя хочу добавить что многие программы берут что-то одно из XMP, IPTC и EXIF и плюют на приоритеты. Потому в своем софте при записи я пишу (а мне надо было работать с ключевыми, заголовком и описаниями) три раза в каждый формат явно иначе если файл был создан другой программой а я изменил метаданные — то могут получиться противоречивые метаданные.
                0
                У меня все тоже самое. Пишу во всех форматах. И нужны тоже ключевики, заголовок и описание. Ну и еще служебная инфа, ее пишу в user comment в экзифе.

                Про приоритеты писал касательно именно BitmapMetadata — он читает все три варианта метаданных.

                А другие программы кто в лес кто по дрова. Чаще всего берут только экзиф или только iptc.
                  0
                  И еще бы наверное правильно говорить что XMP это не xml а RDF представленный в форме XML — в этом есть некоторые нюансы которые не заметны в конкретной задаче.
                    0
                    Спасибо за замечание, исправил.
                    Сам не очень вникал в тонкости XMP, т.к. по сути мне были нужны в первую очередь именно EXIF и IPTC :)
                0
                Спасибо, полезная информация.
                  0
                  Пишите, пожалуйста, и побольше.
                    0
                    Офигенная статья! Пишите больше!
                      0
                      Я некоторое время назад тоже столкнулся с необходимостью читать тэги из jpg. Провозился пару дней в поисках готовых натинвых решений на c#. Потом плюнул, скачал exiftool и написал за пару часов обертку. О своем выборе не жалею.
                        0
                        У меня кстати тоже возникало желание написать обертку к решению не на C#, но как то все таки разобрался с WPF :)
                        0
                        Кстати
                        Провозился всю ночь с вот этими строками:
                        metadata.SetQuery("/app1/ifd/PaddingSchema:Padding", 2048);
                        metadata.SetQuery("/app1/ifd/exif/PaddingSchema:Padding", 2048);
                        metadata.SetQuery("/xmp/PaddingSchema:Padding", 2048);

                        Пока явно не указал тип uint для 2048
                          0
                          И еще
                          В статье не правильная ссылка на перечень запросов, сейчас она находится тут
                          Но и даже по этой ссылке нет еще одного нужного запроса:
                          metadata.SetQuery(@"/app13/irb/8bimiptc/iptc/Object Name", Title);
                          

                          Запись в поле Object Name
                          Хотя многие программы предпочитают именно это поле в качестве названия изображения

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

                        Самое читаемое