Работа с EXIF геотегами в C#

После того как я закончил программу для геотегинга, появилась мысль написать данную статью – дабы поменьше людей наступало на те же грабли, так как толковой информации по данным вопросам не так уж и много.
Итак, я не собираюсь рассказывать, что такое геотегинг или EXIF, об этом можно и в Википедии почитать. А вот как сделать программку на C#, которая бы считывала и записывала данные в EXIF я и собираюсь рассказать. Работать с метаданными фотографии мы будем, как, на мой взгляд, самым простым методом – через JpegBitmapDecoder, для этого потребуется подключить несколько модулей
using System.IO;
using System.Globalization;
using System.Windows.Media.Imaging;


* This source code was highlighted with Source Code Highlighter.

Для начала просто откроем файл фотографии:
FileStream Foto = File.Open(s, FileMode.Open, FileAccess.Read); // открыли файл по адресу s для чтения
          BitmapDecoder decoder = JpegBitmapDecoder.Create(Foto, BitmapCreateOptions.IgnoreColorProfile, BitmapCacheOption.Default); //"распаковали" снимок и создали объект decoder
          BitmapMetadata TmpImgEXIF = (BitmapMetadata)decoder.Frames[0].Metadata.Clone(); //считали и сохранили метаданные


* This source code was highlighted with Source Code Highlighter.

Очень часто требуется получить дату и время снимка. Если заглянуть в спецификацию EXIF, то дат там хранится несколько (дата и время совершения снимка, создания файла, оцифровки, по GPS и др.), причем еще точно неизвестно какую из них записал фотоаппарат, а какую проигнорировал. Но не стоит на этом особо заморачиватся – здесь все просто. BitmapMetadata
содержит свойства для получения даты и времени съемки, а также некоторых других параметров (например, модель фотокамеры).
DateTime DateOfShot = Convert.ToDateTime(TmpImgEXIF.DateTaken);

* This source code was highlighted with Source Code Highlighter.

Теперь перейдем непосредственно к записи геотега. Для работы с любыми метаданными (не только с EXIF) BitmapMetadata содержит методы SetQuery и GetQuery. Они принимают в качестве параметра строку-запрос, которая определяет какое поле метаданных необходимо считать или записать. Есть еще и RemoveQuery для удаления поля. Начнем с простого: добавим в EXIF отметку о том, что у нас северная широта. В EXIF ей соответствует поле раздела GPS с ID=1, а в C# запрос "/app1/ifd/gps/{ushort=1}" (откуда брать эти запросы я потом скажу) и тип данных string. Если широта северная записываем «N», а южная — «S»:
TmpImgEXIF.SetQuery("/app1/ifd/gps/{ushort=1}", "N");

* This source code was highlighted with Source Code Highlighter.

Аналогично с долготой:
TmpImgEXIF.SetQuery("/app1/ifd/gps/{ushort=3}", "E");

* This source code was highlighted with Source Code Highlighter.

И версией, она должна быть 2.2.0.0:
TmpImgEXIF.SetQuery("/app1/ifd/gps/{ushort=0}", "2.2.0.0");

* This source code was highlighted with Source Code Highlighter.

Теперь посложнее: добавим высоту над уровнем моря – это необязательный параметр, но на его примере рассмотрим работу с типом Rational. Если открыть спецификацию EXIF, то там можно увидеть, что высота над уровнем моря хранится в формате Rational. Этот тип выражает вещественные числа в виде простой дроби, так число 182.053 будет записано как 182053/1000 (или 1820530/10000). Для работы с таким типом в C# можно выбрать тип ulong и использовать для хранения числителя 4 младших байта, а для знаменателя — 4 старших. Вот вам функция для перевода типа double в Rational:
private ulong rational(double a)
    {
      uint denom = 1000;
      uint num = (uint)(a * denom);
      ulong tmp;
      tmp = (ulong)denom << 32;
      tmp |= (ulong)num;
      return tmp;
    }


* This source code was highlighted with Source Code Highlighter.

Запишем высоту 95.3м (в EXIF высота над уровнем моря хранится в метрах, например, в отличии от .plt трека, где она хранится в футах).
TmpImgEXIF.SetQuery("/app1/ifd/gps/{ushort=2}", rational(95.3));

* This source code was highlighted with Source Code Highlighter.

Ну а теперь добавим широту и долготу. Она хранится в виде трех Rational, которые выражают градусы, минуты, секунды и доли секунды. Градусы и минуты хранятся целыми числами(то есть знаменатель равен 1, но это не обязательное условие), а секунды – вещественным. Пример: 50⁰30’12.345″. В C# эти три числа нужно объединить в массив. Вот пример записи географической широты:
ulong[] t = { rational(50), rational(30), rational(12.345) };
TmpImgEXIF.SetQuery("/app1/ifd/gps/{ushort=2}", t);


* This source code was highlighted with Source Code Highlighter.

и долготы:
TmpImgEXIF.SetQuery("/app1/ifd/gps/{ushort=4}", t);

* This source code was highlighted with Source Code Highlighter.

Ну вот, пожалуй, и все. Иногда приходится переводить координаты из формата градусов и долей градуса полученные из файла трека, метки, какого-либо интернет-сервиса и прочих, так как такой формат более удобен для работы, но я думаю, что с подобной конвертацией проблем возникнуть не должно. На всякий случай вот формулы перевода:
Degree = Math.Floor(value);
Minute = Math.Floor(((value - Math.Floor(value)) * 60.0));
Second = (((value - Math.Floor(value)) * 60.0) - Math.Floor(((value - Math.Floor(value)) * 60.0))) * 60;


* This source code was highlighted with Source Code Highlighter.

Итак, мы имеем объект BitmapMetadata с добавленными новыми записями (если там уже что-то было записано до нас, то оно должно автоматически заменится на новые значения). Теперь создадим новый файл снимка, в который перенесем все, кроме метаданных, из первого файла, а метаданные возьмем те, которые мы изменили.
JpegBitmapEncoder Encoder = new JpegBitmapEncoder();//создали новый энкодер для Jpeg
          Encoder.Frames.Add(BitmapFrame.Create(decoder.Frames[0], decoder.Frames[0].Thumbnail, TmpImgEXIF, decoder.Frames[0].ColorContexts)); //добавили в энкодер новый кадр(он там всего один) с указанными параметрами
          string NewFileName = s + "+GeoTag.jpg";//имя исходного файла +GeoTag.jpg
          using (Stream jpegStreamOut = File.Open(NewFileName, FileMode.CreateNew, FileAccess.ReadWrite))//создали новый файл
          {
            Encoder.Save(jpegStreamOut);//сохранили новый файл
          }
          Foto.Close();//и закрыли исходный файл

* This source code was highlighted with Source Code Highlighter.

Вот теперь у нас есть новый jpeg-файл, но уже с геотегом. Обратите внимание, что размер исходного файла и нового могут значительно отличатся, это связано с разными способами и параметрами сжатия изображения. Вы можете работать с любыми полями (тегами) EXIF используя методы SetQuery и GetQuery. GetQuery работает аналогично – принимает строку-запрос, возвращает значение, какого типа — смотрим в спецификации EXIF. Какой запрос, какому параметру отвечает можно посмотреть здесь. Например, функция считывания даты:
GetQuery("/app1/ifd/exif:{uint=36867}");

* This source code was highlighted with Source Code Highlighter.

Можно использовать и свои запрсы как показано в этом примере.
AdBlock has stolen the banner, but banners are not teeth — they will be back

More
Ads

Comments 11

    +2
    > размер исходного файла и нового могут значительно отличаться, это связано с разными способами и параметрами сжатия изображения

    А вот это очень плохо. Самое правильное было бы не трогать данные картинки вообще, меняя только с EXIF. Не знаю, насколько это сложно, но утилиты на вырезанию/редактированию существуют. Я побайтово сравнивал — меняется только заголовок.
      0
      *меняя только EXIF
        0
        В моем случае это было к лучшему, так как брал большие фото из камеры, а потом выкладывал в инет. На глаз изменений качества изображения не видно, размер, для примера, менялся с 4.82МБ до 3.39МБ. Хотя, конечно, правильно бы было само изображение не трогать
        0
        >> (BitmapMetadata)decoder.Frames[0].Metadata.Clone();

        А что лежит во Frames[1]? Всегда настороженно относился к такого вида константам в коде.
          0
          Обычно, на один jpeg-файл приходится по одному кадру, соответственно и Frames состоит всего из одного элемента. Frames[1] просто не существует.
            0
            Обычно?

            Спецификации JPEG не копал, но никогда не видел jpg-анимашку.
              0
              Я имел в виду что стандартные JPEG состоят только из одного кадра. Если умудритесь сделать нечто свое — то уже не в счет. JpegBitmapEncoder игнорит все остальные кадры, кроме нулевого. А вот TIFF и GIF — можно, просто для всех этих типов используется один и тот же класс BitmapEncoder.
          0
          Вообще для работы с EXIF уже давно есть замечательная тулза ExifTool и даже GUI к ней, с геотегами и гугл-картами в частности.
            0
            И? Уже давно есть куча графических редакторов, но продолжают создаваться новые. Существует куча медиаплееров, но пишутся все новые. И, вообще, сейчас большинство программ имеет аналоги, если не клоны, но тем не менее у программистов есть работа. А какой смысл писать прогу «Hello World!», если можно скачать готовую.
              0
              Я не хотел сказать, что ваш труд напрасен. Но упомянуть про ExifTool мне кажется уместным уже хотя бы потому, что на сегодня это самая продвинутая и полная реализация работы с EXIF-тегами. Фил (автор) и другие участники как раз подчас и являются источником «толковой информации по данным вопросам», которой, к слову, не так уж и мало. При этом ExifTool кросс-платформенный, т.к. написан на Perl, и многолетняя история также говорит в его пользу.

              С другой стороны, надо думать, что удобный и полноценный (в смысле охватываемого набора всевозможных тегов, камер и форматов файлов — а это не самая легкая задача) C# модуль будет востребован. Но в таком случае странно, что вы не упомянули про уже существующие начинания, ссылки на которые легко выдает поиск по stackoverflow (1, 2).

              И все же я сомневаюсь, что любой из этих проектов дотягивает до уровня ExifTool (например, про враперы вокруг exiv2 можно сразу забыть, т.к. exiv2 до сих пор не умеет работать с, к примеру, CR2).
            0
            Хотел бы добавить один момент
            В конце статьи есть полезная ссылка на документацию от Microsoft, где описаны возможные запросы
            Но там не указано еще одно поле — «Object Name», хотя некоторые программы предпочитают именно это поле в качестве названия изображения
            metadata.SetQuery(@"/app13/irb/8bimiptc/iptc/Object Name", Title);
            

            Only users with full accounts can post comments. Log in, please.