17 декабря 2011

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

.NET
Из песочницы
После того как я закончил программу для геотегинга, появилась мысль написать данную статью – дабы поменьше людей наступало на те же грабли, так как толковой информации по данным вопросам не так уж и много.
Итак, я не собираюсь рассказывать, что такое геотегинг или 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.

Можно использовать и свои запрсы как показано в этом примере.
Теги:EXIFгеотеггинггеотегинггеотегСметаданные
Хабы: .NET
+25
15,7k 61
Комментарии 11