Зачем мне вообще эти метаданные?
Скажу сразу, это – скорее техническая статья про двоичные данные. Однако, чтобы люди с прикладным мышлением не обошли её стороной, опишу чем технология полезна, и какую ценность (надеюсь) добавляет моя работа. Если технические тонкости вам не интересны – после прочтения этого раздела смело переходите к последнему.
Итак, метаданные картинок нужны:
- Продуктовому аналитику, как источник ценной продуктовой информации: модель и марка телефона, геолокация и даты снимка и многое другое – полезные данные для сегментации рынка и других маркетинговых изысканий.
- Менеджеру, как источник фактической информации. Нередко работники, например курьеры, подтверждают выполнение задач фотографиями. Анализ геометок и даты снимка позволяют автоматизировать контроль.
- Специалисту по антифроду – помимо упомянутых геометок и дат, метаданные оставляют много других следов: была ли фотография отредактирована и когда, в какой программе было произведено редактирование, есть ли логическое несоответствие во внутренних timestamp’ах и прочее.
- Разработчику веб-сервисов на 1С – полезным будет оперирование файлом без сохранения на диск. Оперируя потоком можно отсекать логикой сервиса (например) картинки слишком маленького размера, черно-белые снимки, пустые архивы, аудиофайлы длины меньшей чем положено и т.д. Вас ограничивает только фантазия и соответствующий стандарт метаданных.
- Разработчику интерфейсов – метаданные это, пожалуй, единственный быстрый способ определить ориентацию картинки -- где «верх», а где «низ» -- чтобы оператору не требовалось вертеть головой как сыч
Метаданные можно менять, их может не быть, они могут быть сфальсифицированы – и это может добавить проблем. Насколько это будет полезно именно Вам – судите сами.
Моя работа тут состоит из двух частей. Во-первых, это описание моего опыта разработки и прикладного использования, которые надеюсь, должны быть интересны сообществу. Во-вторых, это презентация обработки для чтения метаданных, которую можно использовать в своих целях. Исходный код открыт (Apache 2.0). Я надеюсь, что решение будет развиваться в рамках opensource-проекта. Пожалуйста, ставьте звезды на infostart и в github.
Как это ваще блин устроено?
Как известно, GPS-метки хранятся в файле картинки JPEG в особой секции двоичных данных – EXIF.
Решение всегда было на поверхности, с момента, когда в 8.3.9 появились объекты для работы с двоичными данными. Однако чтение EXIF - низкоуровневая задача, которая требует погружения в скучные технические нюансы и утомительного всматривания в HEX-коды. Существующие публикации на эту тему используют сторонние технологии, которые делают всю «грязную работу».
Есть энтузиасты, которые в рамках закрытых внедрений, разрабатывали похожие решения на 1С, но в открытом доступе на момент написания статьи их нет.
Пожалуй единственным доступным релевантным источником является пример с сайта ИТС по получению размера картинки. Этот пример работает молниеносно, заставляет апологетов 1С радоваться за платформу (могёт!!), но оставляет больше вопросов чем ответов.
Чтобы разобраться в природе магических чисел я проследовал по ссылкам в комментариях к коду и в итоге нашел обстоятельный стандарт с примерами и другие полезных ресурсы .
Начнем разбираться.
Любой файл состоит из двоичных данных (привет, кэп!). Даже шапочного понимания достаточно, чтобы догадаться о существовании «служебных» последовательностей в файле, которые определяют формат, содержимое и другие аспекты. Двоичные данные, определяющие формат файла называются сигнатурой файла. За редким исключением, сигнатура -- это первые несколько байт файла. У наших картинок это два байта: ff d8 - в шестнадцатеричном и 255 216 – в десятичном представлении. Теперь должна быть понятна природа магических цифр – воспринимать их нужно не как значения, а как "ключевые слова". Кроме того, теперь понятно - чтобы не искать GPS картинок в файлах нерелевантного формата, достаточно проверить только сигнатуру файла.
Именно это полезное свойство можно использовать в своих сервисах, принимающих файлы. Можно отсекать неподходящие файлы (пустые архивы, неверный формат файла и т.п), не сохраняя их на диск.
Вообще говоря, файл JPEG логически можно разбить на множество секций. Я приведу пример, как может быть закодирован фрагмент простого JPEG-файл, из него должна быть понятна общая логика построения файловых секций:
Можно увидеть, что секции кодируются ключевыми маркерами и длиной. Длина указывается с учетом байтов, кодирующих длину, т.е. например первая секция в файле (ffe0) занимает 16 (00 10) байт, включая сами байты 00 10.
Размеры изображения* можно прочитать из секции SOF0 (ffc0). Собственно, это и происходит в волшебном коде с сайта ИТС (192 – это c0). Далее в секции мы читаем её размер (00 11), 1-байтовое количество бит на пиксель (08), 2-байтовые ширину и высоту рисунка (00 01 и 00 02, т.е. мы имеем дело с рисунком 1x2 пикселя), и другую информацию (см. описание формата)
Отдельные секции, как например EXIF, который нам интересен – кодируются по-особому.
Чтобы найти EXIF, нужно найти шестнадцатеричные байты: 45 78 69 66 00 00 (в примере на рисунке этой секции нет) – позиция этих байтов будет нужна как реперная точка (назову её landmark), от которой мы в дальнейшем будем отсчитывать смещения. Дело в том, что секция EXIF, это по сути таблица, каждая строка в которой не может быть больше 12 байт. В таблице 4 колонки – Тег (2 байта), Тип тега (2 байта), Количество значений (4 байта) и Значение/Смещение (4 байта). Если значение умещается в 4 байта – то оно записывается непосредственно по месту, в противном случае по месту записывается сколько байтов нужно отсчитать от позиции landmark, чтобы найти нужное значение (или значения, если их несколько).
Другая особенность секции EXIF в том, что значения в этой секции могут кодироваться разным образом: little-endian и big-endian форматом, которые отличаются порядком чтения байтов в наборе. Формат задается следующим байтом сразу после EXIF.
Третья забавная особенность в том, что сразу после порядка байтов должно быть записано число 42. Просто потому что «ну вот так вот».
Затем задается размер смещения в байтах, которое нужно будет добавить к позиции landmark, чтобы найти записи этой таблицы EXIF. Поэтому критично, чтобы порядок следования байтов был прочитан корректно.
В следующих двух байтах хранится количество записей в таблице.
Четвертая особенность «таблицы» EXIF в том, что она иерархическая. Отдельные записи этой таблицы указывают не на значения, а на другие таблицы (каталоги) такой же структуры. Например, все значения GPS-информации записаны в отдельном «каталоге». Это позволяет использовать значения тегов более одного раза. Для иллюстрации тег 00 00 в контексте GPS-каталога тегов должен быть прочитан как GPS Version ID, а в контексте обычных EXIF-тегов как Interop index. В интернетах можно найти ресурсы, подробно описывающие перечень, назначение и типы данных тегов.
В любом теге форматом EXIV2 поддерживаются строго определенные типы данных. Их всего 9: Byte, Short, Long, Ratio, и их беззнаковые близнецы для кодирования чисел и отдельно стоящий ASCII для кодирования строк.
Искусственный пример. В таблице EXIF мы нашли такую запись:
a4 33 00 02 00 00 00 f9 00 00 00 78
Это означает тег “Lens Make” (a4 33), типа “ASCII” (00 02) из 249 символов (00 00 00 f9) расположен по адресу 120 (00 00 00 78) байтов от позиции landmark. Если отступить указанное количество байтов – можно найти непосредственно 249 символов ASCII.
Ровно таким же способом кодируются и GPS-геометки. В основной таблице EXIF в очередной 12-байтной записи в первых двух байтах будет записан тег 88 25, который означает что в файле есть геоданные и, в последних четырех байтах, будет записано смещение до нужного каталога. Если перейти туда – можно будет прочитать сначала количество записей в этом каталоге, а затем такие же 12-байтовые записи.
Широта и долгота кодируются типом ratio. Т.е. Они задаются отношением двух четырехбайтовых чисел. Например, можно прочитать примерно следующее:
00 04 00 05 00 00 00 03 00 00 00 C0
00 04 – Долгота
00 05 – Тип Ratio
00 00 00 03 – Три значения (Градусы, минуты, секунды)
00 00 00 C0 – в 192 байтах от landmark
А в 192 байтах от landmark-байта можно будет прочитать например
00 00 00 2d 00 00 00 01 00 00 00 0d 00 00 00 01 00 00 70 30 00 00 27 10
Что можно записать условно как:
00 00 00 2d / 00 00 00 01; 00 00 00 0d / 00 00 00 01; 00 00 70 30 / 00 00 27 10
Что равно:
45 / 1; 13 / 1; 28720 / 10000
Что означает:
45 градусов 13 минут 2.872 секунды, вуаля. Мы прочитали GPS-метку!
Open source
Всё вышеописанное реализовано в виде внешней обработки.
Хотя чтение EXIF возможно и на клиенте, существующий код в данный момент серверный, потому что в моем случае файлы лежат на сервере вместе с 1С. Клиентская версия кода, если потребуется, может появиться в ходе открытой разработки. Присоединяйтесь, если вы разработчик и вам интересно.
Если вы пользовались обработкой, но что-то пошло не так – оставляйте отзывы, а лучше сразу issue на github.
Обработка в данный момент не является законченным решением, поэтому я не предлагаю никаких подходов ни к тому как хранить, ни к тому как представлять прочитанные значения. В production-коде работает немного более продвинутая версия, заточенная под конкретные нужды.
Я ожидаю, что сообщество внесет что-то новое в функционал этой обработки, или каждый адаптирует решение для своих нужд. Критика приветствуется, но звезды в гитхаб категорически приветствуются.
Быстрый путь к метаданным
С самого начала было очевидно, что чтение должно быть довольно шустрым. И хотя полноценный бенчмарк только назревает, первое боевое использование оказалось весьма конкурентным. Случайный файл с hdd 7200 диска анализируется за 20-40 мс в «холодном» режиме, и около 10 мс в «горячем». Характерное время для хардкорных утилит – несколько миллисекунд.
Тут надо оговориться – что, время чтения сильно зависит от содержимого, и некоторые файлы могут читаться несколько секунд, даже на exiftool. И справедливо ожидать, что пакетное чтение на большом объёме в 1С может значительно уступать скомпилированным библиотекам, но порядок цифр пока радует.