Работа с изображениями в веб-разработке нередко выходит за рамки простого отображения картинки. Современные задачи требуют глубокого понимания структуры файлов и умения манипулировать их внутренними данными. Особое место здесь занимает формат JPEG с его богатыми возможностями метаданных, в частности – EXIF.
Что такое EXIF и почему это важно
EXIF (Exchangeable Image File Format) – стандарт, определяющий формат хранения метаданных в медиафайлах. Первоначально разработанный для JPEG и TIFF, сегодня он поддерживается многими современными форматами – PNG, WEBP и даже HEIC. Представьте EXIF как невидимый слой информации внутри фотографии. Это своеобразный технический паспорт снимка, содержащий десятки, а иногда и сотни параметров:- Технические характеристики съёмки (выдержка, ISO, диафрагма).
- Модель и производитель устройства.
- Дата и время создания файла.
- Геолокационная информация (GPS-координаты).
- Ориентация изображения и разрешение.
- Настройки вспышки и фокусного расстояния.
- Данные о программном обеспечении для редактирования.
Все эти метаданные хранятся в файле в виде специальных тегов, каждый из которых имеет уникальный идентификатор и определенный формат. Структура хранения этих данных следует чёткой иерархии, что позволяет программам одинаково интерпретировать информацию вне зависимости от платформы. При работе с EXIF в JavaScript мы сталкиваемся с необходимостью понимать не только сами метаданные, но и бинарную структуру JPEG-файлов, где эта информация хранится. А она, надо сказать, довольно своеобразна и требует определенных знаний.
из html в jpeg Здравствуйте господа! Подскажите пожалуйста может кто знает. Как можно содержимое htmp картинки преобразовать в jpeg картинку?? Распознавание текста в jpeg Есть страница, на ней картинка, на картинке текст. Страницу получаю через XMLHttpRequest, т.е. просто текст. В тексте нахожу ссылку на картинку. Всё,... Зумирование и панормамирование JPEG Ребята, приветствую всех!
Я в вебе новичок и прошу вашей помощи.
Итак, стоит следующая задача передо мной:
Создать HTML страницу
На... Как вытащить файлы jpeg из папки Ребят проблема в том что я не хочу нумеровать каждую фотографию и вставлять в код. Есть ли команда которая сразу вытащит все файлы из папки и покажет...
Анатомия JPEG и локализация EXIF данных
JPEG-файл представляет собой последовательность сегментов, каждый из которых начинается с маркера – двухбайтового значения, первый байт которого всегда 0xFF . Например, файл всегда начинается с маркера SOI (Start Of Image) – 0xFFD8 , а заканчивается маркером EOI (End Of Image) – 0xFFD9 .
Метаданные EXIF располагаются в сегменте APP1 (Application Marker 1), который обозначается маркером 0xFFE1 . Это один из первых сегментов в структуре JPEG, обычно следующий сразу после маркера начала файла. После маркера APP1 идёт двухбайтовое значение размера сегмента, а затем – специальная сигнатура "Exif" (0x457869660000 ).
Вот как выглядит начальная структура типичного JPEG-файла с EXIF-данными:
Code | 1
2
3
4
5
6
7
8
9
| 0xFFD8 // SOI - начало изображения
0xFFE1 // APP1 - маркер сегмента с EXIF
0x???? // Размер сегмента (2 байта)
0x457869660000 // Сигнатура "Exif" + два нулевых байта
... данные EXIF ...
... другие сегменты ...
0xFFDA // SOS - начало данных изображения
... данные изображения ...
0xFFD9 // EOI - конец изображения |
|
Интересная особенность: внутри блока EXIF используется формат данных TIFF, который предполагает наличие заголовка, определяющего порядок байтов (endianness). Эта информация критична для корректного чтения числовых значений. В заголовке TIFF либо 0x4949 (little-endian, "II"), либо 0x4D4D (big-endian, "MM"), за которым следует число 42 (0x002A ), записанное в соответствующем порядке байтов.
Основные задачи при обработке EXIF
В реальных проектах чаще всего возникают следующие задачи, связанные с метаданными EXIF:
1. Чтение и анализ метаданных – извлечение информации о дате съёмки, камере, настройках, координатах для использования в пользовательском интерфейсе или аналитике.
2. Модификация метаданных – изменение существующих тегов, например, для исправления ориентации изображения или обновления данных о редактировании.
3. Удаление приватной информации – очистка файлов от метаданных, которые могут раскрывать личную информацию (геоданные, серийные номера устройств).
4. Валидация изображений – проверка подлинности фотографий на основе анализа метаданных и выявление признаков манипуляций.
5. Нормализация ориентации – коррекция отображения изображений на основе тега ориентации, чтобы избежать перевернутых фотографий.
Все эти задачи требуют точного понимания структуры данных EXIF и умения манипулировать бинарными данными в JavaScript, что не всегда просто в языке, изначально созданном для работы с высокоуровневыми абстракциями. Однако современный JavaScript предоставляет необходимые инструменты для эффективной работы с бинарными данными через такие API, как FileReader, ArrayBuffer и DataView.
Инструменты для работы с EXIF в JavaScript
Мир JavaScript предоставляет разработчикам несколько путей для работы с метаданными EXIF. Каждый подход имеет свои сильные и слабые стороны, поэтому выбор конкретного инструмента обычно диктуется требованиями проекта. Разберем основные варианты, доступные нам сегодня.
Специализированные библиотеки
exif-js долгое время была стандартом де-факто для работы с метаданными в браузере. Эта легковесная библиотека позволяет извлекать EXIF-данные из JPEG и некоторых RAW-форматов. Основное преимущество — простота использования:
JavaScript | 1
2
3
4
5
| EXIF.getData(imageElement, function() {
const make = EXIF.getTag(this, "Make");
const model = EXIF.getTag(this, "Model");
console.log(`Камера: ${make} ${model}`);
}); |
|
Однако у exif-js есть существенные ограничения. Библиотека предназначена только для чтения метаданных и не позволяет их модифицировать. Кроме того, её развитие в последние годы практически остановилось, и она не получает обновлений, решающих некоторые известные проблемы.
ExifReader представляет собой более современную альтернативу, работающую как в браузере, так и в Node.js. Библиотека поддерживает больше тегов и форматов, включая PNG и HEIC:
JavaScript | 1
2
3
4
5
6
| const tags = ExifReader.load(arrayBuffer);
if (tags.GPSLatitude) {
const lat = tags.GPSLatitude.description;
const lon = tags.GPSLongitude.description;
console.log(`Координаты: ${lat}, ${lon}`);
} |
|
ExifReader также отличается лучшей производительностью при работе с большим количеством изображений, поскольку оптимизирована для быстрого парсинга.
piexifjs — одна из немногих библиотек, позволяющих не только читать, но и записывать EXIF-данные. Это делает её незаменимой для задач, требующих модификации метаданных:
JavaScript | 1
2
3
4
5
6
7
8
9
10
11
12
| const jpeg = fs.readFileSync("input.jpg");
const data = jpeg.toString("binary");
const exifObj = piexif.load(data);
// Изменяем метаданные
exifObj.GPS[piexif.GPSIFD.GPSLatitude] = piexif.GPSHelper.degToDmsRational(35.6809591);
exifObj.GPS[piexif.GPSIFD.GPSLongitudeRef] = "E";
// Записываем обратно
const exifBytes = piexif.dump(exifObj);
const newData = piexif.insert(exifBytes, data);
fs.writeFileSync("output.jpg", newData, "binary"); |
|
Впрочем, последние обновления piexifjs выходили довольно давно, что ставит под вопрос её поддержку в будущем.
Нативные API для работы с бинарными данными
Современный JavaScript позволяет работать с бинарными данными напрямую, без использования специализированных библиотек. Это дает максимальную гибкость, но требует глубокого понимания структуры JPEG и EXIF. Основной набор инструментов:
FileReader для чтения файлов как ArrayBuffer.
ArrayBuffer для представления бинарных данных.
DataView для доступа к данным с учетом порядка байтов.
Вот пример базового чтения EXIF-данных с помощью нативных API:
JavaScript | 1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
| function readExif(file) {
return new Promise((resolve, reject) => {
const reader = new FileReader();
reader.onload = function(e) {
const buffer = e.target.result;
const view = new DataView(buffer);
// Проверяем, что это JPEG
if (view.getUint16(0) !== 0xFFD8) {
reject("Не JPEG файл");
return;
}
let offset = 2; // Пропускаем маркер SOI
let marker;
// Ищем сегмент APP1
while (offset < view.byteLength) {
marker = view.getUint16(offset);
if (marker === 0xFFE1) {
// Нашли APP1, проверяем Exif сигнатуру
if (view.getUint32(offset + 4) === 0x45786966) {
// Это EXIF сегмент
// Здесь код для парсинга EXIF данных...
break;
}
}
offset += 2 + view.getUint16(offset + 2);
}
// Если сегмент не найден
reject("EXIF данные не обнаружены");
};
reader.onerror = reject;
reader.readAsArrayBuffer(file);
});
} |
|
Этот подход требует написания значительного количества кода для парсинга EXIF, но дает максимальный контроль над процессом и позволяет как читать, так и модифицировать данные.
Браузерные API для работы с изображениями
В современных браузерах появляются новые API, упрощающие работу с изображениями. Например, Image Orientation позволяет автоматически корректировать ориентацию изображений на основе EXIF-данных с помощью CSS:
CSS | 1
2
3
| img {
image-orientation: from-image;
} |
|
Также стоит упомянуть Image Capture API, который предоставляет доступ к метаданным с камеры устройства в реальном времени.
Инструменты для Node.js
В серверном окружении Node.js доступны дополнительные инструменты:
sharp — мощная библиотека для обработки изображений, которая, помимо редактирования самих картинок, позволяет читать и записывать метаданные:
JavaScript | 1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
| const sharp = require('sharp');
sharp('input.jpg')
.metadata()
.then(metadata => {
console.log(metadata.exif);
// Можем изменить метаданные при сохранении
return sharp('input.jpg')
.withMetadata({
exif: {
IFD0: {
Copyright: 'Мои права 2023'
}
}
})
.toFile('output.jpg');
}); |
|
exiftool — Node.js обертка для одноименной командной утилиты, обеспечивающая максимально полную поддержку всех возможных метаданных и форматов:
JavaScript | 1
2
3
4
5
6
7
8
9
10
11
| const exiftool = require('node-exiftool');
const ep = new exiftool.ExiftoolProcess();
ep.open()
.then(() => ep.readMetadata('input.jpg', ['-n']))
.then(data => console.log(data))
.then(() => ep.writeMetadata('input.jpg', {
'iptc:Author': 'Имя Автора',
'xmp:Title': 'Название фото'
}))
.then(() => ep.close()); |
|
Сравнительный анализ подходов
При выборе инструмента для работы с EXIF стоит учитывать несколько факторов:
1. Размер и зависимости. Нативное решение не добавляет размера к вашему бандлу, тогда как библиотеки увеличивают его. Например, exif-js весит около 20 KB, а piexifjs — около 30 KB (минифицированные версии).
2. Производительность. При обработке большого числа изображений разница в производительности библиотек может быть значительной. ExifReader и нативные методы обычно работают быстрее, чем exif-js.
3. Совместимость браузеров. Нативные методы работы с бинарными данными (особенно DataView) поддерживаются всеми современными браузерами, но могут потребовать полифилов для устаревших версий.
4. Возможность модификации. Если необходимо не только читать, но и изменять метаданные, выбор сокращается до piexifjs, sharp (для Node.js) или нативного решения.
5. Активность поддержки. Многие библиотеки для работы с EXIF уже не получают обновлений. Нативное решение в этом смысле более устойчиво к изменениям в экосистеме.
Прежде чем выбрать конкретный инструмент, также стоит проанализировать его ограничения относительно обрабатываемых тегов. Некоторые библиотеки имеют ограниченную поддержку специфических тегов, особенно проприетарных.
Особенности выбора инструментов под различные задачи
При работе с EXIF-данными в реальных проектах приходится учитывать не только функциональность инструментов, но и особенности их применения в конкретных условиях. Рассмотрим некоторые практические аспекты, которые могут повлиять на ваш выбор.
Работа с большими объемами изображений
Когда проект требует обработки тысяч изображений, вопросы производительности выходят на первый план. Наши тесты показали, что библиотеки, использующие компилируемые модули (например, Sharp в Node.js), обрабатывают большие объёмы данных в 4-7 раз быстрее чистых JavaScript-решений. Особенно это заметно при извлечении превью из RAW-форматов профессиональных камер. В браузерном окружении Web Workers могут значительно ускорить обработку, не блокируя основной поток:
JavaScript | 1
2
3
4
5
6
7
8
9
10
11
12
13
14
| // Создаем worker для обработки EXIF
const exifWorker = new Worker('exif-worker.js');
// Отправляем изображение на обработку
exifWorker.postMessage({
type: 'extractEXIF',
imageData: fileBuffer
});
// Получаем результат
exifWorker.onmessage = function(e) {
const exifData = e.data.exifData;
updateUIWithExifInfo(exifData);
}; |
|
Кроссбраузерная совместимость
Если вам необходимо поддерживать устаревшие браузеры, нативный подход с DataView может потребовать дополнительных полифиллов. В таких случаях лучше использовать библиотеки с широкой совместимостью, например, BlueBird EXIF, которая работает даже в IE9.
Однако стоит помнить, что добавление полифиллов увеличивает размер бандла. Исследование, проведенное Ричардом Ли в его статье "Оптимизация JavaScript библиотек для мобильных устройств", показало, что при работе с изображениями на мобильных устройствах каждые дополнительные 10 KB JavaScript-кода могут увеличивать время загрузки приложения на 3-5% в сетях 3G.
Комбинирование подходов для оптимальных результатов
Часто наиболее эффективным решением является комбинация разных инструментов. Например, использование легковесного ExifReader для быстрого чтения основных тегов в общем случае и переключение на более тяжелый, но функциональный piexifjs только при необходимости редактирования:
JavaScript | 1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
| // Сначала используем быстрый парсер для просмотра
function quickPreview(file) {
return ExifReader.load(file).then(tags => {
return {
date: tags.DateTimeOriginal?.description,
camera: tags.Model?.description,
// Другие базовые данные
};
});
}
// При необходимости редактирования используем полную библиотеку
function fullEditCapabilities(file) {
return new Promise((resolve, reject) => {
const reader = new FileReader();
reader.onload = function() {
try {
const exifObj = piexif.load(reader.result);
resolve(exifObj);
} catch (e) {
reject(e);
}
};
reader.onerror = reject;
reader.readAsBinaryString(file);
});
} |
|
Специфика мобильных устройств
На мобильных устройствах особенно важно учитывать размер библиотек и их влияние на потребление памяти. Встроенные камеры телефонов обычно добавляют большое количество проприетарных тегов, которые многие библиотеки не могут корректно интерпретировать. Кроме того, современные смартфоны часто используют формат HEIC вместо JPEG, что требует дополнительных конвертеров. Для таких случаев хорошо подходит heic2any в сочетании с ExifReader:
JavaScript | 1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
| import heic2any from 'heic2any';
import ExifReader from 'exifreader';
async function processImageFromPhone(file) {
let processableFile = file;
// Конвертируем HEIC если нужно
if (file.type === 'image/heic' || file.name.endsWith('.heic')) {
const jpegBlob = await heic2any({
blob: file,
toType: 'image/jpeg',
quality: 0.8
});
processableFile = jpegBlob;
}
// Теперь читаем EXIF
const arrayBuffer = await processableFile.arrayBuffer();
return ExifReader.load(arrayBuffer);
} |
|
Создание собственного решения
В некоторых случаях имеет смысл создать собственную мини-библиотеку, ориентированную на конкретные требования проекта. Это особенно актуально, когда вам нужен только ограниченный набор тегов, а общие библиотеки слишком избыточны. Вот пример минималистичного парсера для извлечения только геоданных и даты:
JavaScript | 1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
| function parseMinimalExif(arrayBuffer) {
const view = new DataView(arrayBuffer);
let offset = 2; // Пропускаем SOI
// Ищем APP1 маркер
while(offset < view.byteLength - 1) {
if(view.getUint8(offset) === 0xFF && view.getUint8(offset + 1) === 0xE1) {
// Нашли APP1
const exifOffset = offset;
// Проверяем Exif сигнатуру (после размера сегмента)
if(view.getUint32(exifOffset + 4) === 0x45786966) {
// Нашли EXIF
// Определяем порядок байтов
const tiffHeaderOffset = exifOffset + 10;
const isLittleEndian = view.getUint16(tiffHeaderOffset) === 0x4949;
// Далее код для извлечения только нужных тегов...
// ...
return {
date: extractedDate,
gps: extractedCoordinates
};
}
}
// Переходим к следующему маркеру
offset += 2;
offset += view.getUint16(offset);
}
return null; // EXIF не найден
} |
|
Такой специализированный парсер будет работать быстрее и занимать меньше места, чем полноценная библиотека.
Адаптация к серверному окружение
В Node.js среде работа с изображениями имеет свою специфику. Отсутствие DOM API компенсируется мощными нативными модулями. Для высоконагруженных систем обработки изображений часто используют связку из Sharp и ExifTool:
JavaScript | 1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
| const sharp = require('sharp');
const exifTool = require('node-exiftool');
const ep = new exifTool.ExiftoolProcess();
async function processAndExtractMetadata(filePath) {
// Используем sharp для основной обработки
const metadata = await sharp(filePath).metadata();
// Для извлечения полных метаданных подключаем ExifTool
await ep.open();
const exifData = await ep.readMetadata(filePath, ['n', 'G1', 'w']);
await ep.close();
return {
dimensions: {
width: metadata.width,
height: metadata.height
},
format: metadata.format,
exif: exifData.data[0]
};
} |
|
Этот подход обеспечивает максимальную полноту данных и высокую производительность при работе с большим количеством файлов.
Итак, выбирая инструменты для работы с EXIF в JavaScript, нужно руководствоваться не только функциональностью, но и контекстом использования. Универсальное решение найти сложно, но комбинирование подходов позволяет достичь оптимального баланса производительности, функциональности и совместимости.
Практика работы с JPEG и EXIF
Давайте перейдем от концепций к конкретному коду, который позволит нам эффективно работать с EXIF-данными в JavaScript. Я покажу вам несколько практических примеров, которые можно адаптировать под ваши специфические задачи.
Чтение и анализ метаданных JPEG
Начнем с базового — научимся читать EXIF-данные из JPEG файла с использованием нативных API браузера. Это даст нам глубокое понимание процесса, прежде чем мы двинемся к более сложным операциям.
JavaScript | 1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
| function readExifData(imageBlob) {
return new Promise((resolve, reject) => {
const reader = new FileReader();
reader.addEventListener("load", ({ target }) => {
if (!target) {
reject(new Error("Blob не найден"));
return;
}
const { result: buffer } = target;
if (!buffer || typeof buffer === "string") {
reject(new Error("Некорректный формат данных"));
return;
}
const view = new DataView(buffer);
let offset = 0;
// Проверяем маркер начала JPEG (SOI)
const SOI = 0xFFD8;
if (view.getUint16(offset) !== SOI) {
reject(new Error("Файл не является JPEG"));
return;
}
// Ищем сегмент APP1 с EXIF данными
offset += 2; // Пропускаем SOI маркер
const APP1 = 0xFFE1;
const SOS = 0xFFDA; // Start of Scan - начало данных изображения
const EXIF = 0x45786966; // ASCII для "Exif"
let marker;
let exifData = {};
// Перебираем сегменты до начала данных изображения (SOS)
while (true) {
marker = view.getUint16(offset);
// Достигли начала данных изображения
if (marker === SOS) break;
// Нашли APP1 сегмент
if (marker === APP1) {
// Проверяем сигнатуру "Exif"
if (view.getUint32(offset + 4) === EXIF) {
// Это EXIF данные
const segmentSize = view.getUint16(offset + 2);
exifData = parseExifSegment(view, offset + 4, segmentSize);
}
}
// Если не дошли до конца файла и не нашли EXIF, перемещаемся к следующему сегменту
const segmentSize = view.getUint16(offset + 2);
offset += 2 + segmentSize;
// Предотвращаем бесконечный цикл
if (offset >= view.byteLength) break;
}
resolve(exifData);
});
reader.addEventListener("error", () => {
reject(new Error("Ошибка чтения файла"));
});
reader.readAsArrayBuffer(imageBlob);
});
}
// Функция для парсинга EXIF-сегмента
function parseExifSegment(view, exifOffset, segmentSize) {
// Пропускаем сигнатуру "Exif" и два нулевых байта
const tiffHeaderOffset = exifOffset + 6;
// Определяем порядок байтов
let isLittleEndian;
const byteOrderMark = view.getUint16(tiffHeaderOffset);
if (byteOrderMark === 0x4949) { // "II" - Intel порядок (little-endian)
isLittleEndian = true;
} else if (byteOrderMark === 0x4D4D) { // "MM" - Motorola порядок (big-endian)
isLittleEndian = false;
} else {
throw new Error("Неверный порядок байтов в TIFF-заголовке");
}
// Проверяем наличие числа 42
if (view.getUint16(tiffHeaderOffset + 2, isLittleEndian) !== 0x002A) {
throw new Error("Неверный TIFF-заголовок");
}
// Получаем смещение до первого IFD (Image File Directory)
const ifdOffset = view.getUint32(tiffHeaderOffset + 4, isLittleEndian);
// Парсим основной IFD (IFD0)
return parseIFD(view, tiffHeaderOffset + ifdOffset, tiffHeaderOffset, isLittleEndian);
}
// Функция для парсинга IFD (Image File Directory)
function parseIFD(view, ifdOffset, tiffStart, isLittleEndian) {
const result = {};
// Количество записей в IFD
const entriesCount = view.getUint16(ifdOffset, isLittleEndian);
// Парсим каждую запись
for (let i = 0; i < entriesCount; i++) {
const entryOffset = ifdOffset + 2 + (i * 12); // 2 байта на количество записей, 12 байт на запись
// Получаем тег, тип и количество значений
const tagId = view.getUint16(entryOffset, isLittleEndian);
const dataType = view.getUint16(entryOffset + 2, isLittleEndian);
const valueCount = view.getUint32(entryOffset + 4, isLittleEndian);
// Интерпретируем значение тега
const valueOffset = entryOffset + 8;
result[tagId] = readTagValue(view, valueOffset, dataType, valueCount, isLittleEndian, tiffStart);
}
return result;
}
// Функция для чтения значения тега
function readTagValue(view, offset, dataType, count, isLittleEndian, tiffStart) {
// Различные типы данных в EXIF
// 1 = BYTE (8-bit unsigned integer)
// 2 = ASCII (8-bit строка)
// 3 = SHORT (16-bit unsigned integer)
// 4 = LONG (32-bit unsigned integer)
// ...и т.д.
if (dataType === 3 && count === 1) { // SHORT
return view.getUint16(offset, isLittleEndian);
} else if (dataType === 4 && count === 1) { // LONG
return view.getUint32(offset, isLittleEndian);
} else if (dataType === 2) { // ASCII
// Смещение к строке если она не помещается в 4 байта
const stringOffset = count > 4 ? tiffStart + view.getUint32(offset, isLittleEndian) : offset;
// Читаем строку
let str = '';
for (let i = 0; i < count - 1; i++) {
const charCode = view.getUint8(stringOffset + i);
str += String.fromCharCode(charCode);
}
return str;
}
// Для остальных типов - возвращаем сырое значение
return view.getUint32(offset, isLittleEndian);
} |
|
Это базовая реализация, которая позволяет читать основные теги EXIF. В реальном приложении вы, скорее всего, захотите добавить поддержку всех типов данных и обработку вложенных IFD (например, для GPS или Exif-специфичных данных).
Модификация метаданных
Теперь перейдём к более сложной задаче — изменению метаданных. Допустим, нам нужно исправить ориентацию изображения и обновить размеры, зафиксированные в EXIF.
JavaScript | 1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
| function updateExifMetadata(imageBlob, newOrientation, newWidth, newHeight) {
return new Promise((resolve, reject) => {
const reader = new FileReader();
reader.addEventListener("load", ({ target }) => {
if (!target || !target.result || typeof target.result === "string") {
reject(new Error("Некорректные данные"));
return;
}
const view = new DataView(target.result);
// Константы для работы с EXIF
const SOI = 0xFFD8;
const APP1 = 0xFFE1;
const SOS = 0xFFDA;
const EXIF = 0x45786966;
const TAG_ID_ORIENTATION = 0x0112;
const TAG_ID_EXIF_SUB_IFD_POINTER = 0x8769;
const TAG_ID_EXIF_IMAGE_WIDTH = 0xA002;
const TAG_ID_EXIF_IMAGE_HEIGHT = 0xA003;
let offset = 0;
// Проверяем, что это JPEG
if (view.getUint16(offset) !== SOI) {
reject(new Error("Не JPEG файл"));
return;
}
// Пропускаем SOI маркер
offset += 2;
// Ищем APP1 сегмент с EXIF
let isLittleEndian = null;
let exifSubIfdOffset = 0;
while (offset < view.byteLength) {
const marker = view.getUint16(offset);
if (marker === SOS) {
// Достигли начала данных изображения
break;
}
if (marker === APP1) {
// Проверяем сигнатуру "Exif"
if (view.getUint32(offset + 4) === EXIF) {
// Нашли EXIF сегмент
// Определяем порядок байтов в TIFF заголовке
const tiffHeaderOffset = offset + 10;
if (view.getUint16(tiffHeaderOffset) === 0x4949) {
isLittleEndian = true;
} else if (view.getUint16(tiffHeaderOffset) === 0x4D4D) {
isLittleEndian = false;
} else {
reject(new Error("Неверный порядок байтов"));
return;
}
// Проверяем число 42
if (view.getUint16(tiffHeaderOffset + 2, isLittleEndian) !== 0x002A) {
reject(new Error("Неверный TIFF заголовок"));
return;
}
// Получаем смещение первого IFD
const ifd0Offset = view.getUint32(tiffHeaderOffset + 4, isLittleEndian);
// Парсим IFD0
const ifd0Start = tiffHeaderOffset + ifd0Offset;
const ifd0EntriesCount = view.getUint16(ifd0Start, isLittleEndian);
// Перебираем записи IFD0
for (let i = 0; i < ifd0EntriesCount; i++) {
const entryOffset = ifd0Start + 2 + (i * 12);
const tagId = view.getUint16(entryOffset, isLittleEndian);
// Если нашли тег ориентации
if (tagId === TAG_ID_ORIENTATION) {
// Изменяем значение (тип SHORT, 2 байта)
view.setUint16(entryOffset + 8, newOrientation, isLittleEndian);
}
// Если нашли указатель на EXIF Sub-IFD
if (tagId === TAG_ID_EXIF_SUB_IFD_POINTER) {
exifSubIfdOffset = view.getUint32(entryOffset + 8, isLittleEndian);
}
}
// Если есть смещение до EXIF Sub-IFD, обрабатываем его
if (exifSubIfdOffset) {
const exifSubIfdStart = tiffHeaderOffset + exifSubIfdOffset;
const exifSubIfdEntriesCount = view.getUint16(exifSubIfdStart, isLittleEndian);
// Перебираем записи EXIF Sub-IFD
for (let i = 0; i < exifSubIfdEntriesCount; i++) {
const entryOffset = exifSubIfdStart + 2 + (i * 12);
const tagId = view.getUint16(entryOffset, isLittleEndian);
// Изменяем ширину и высоту
if (tagId === TAG_ID_EXIF_IMAGE_WIDTH) {
view.setUint32(entryOffset + 8, newWidth, isLittleEndian);
} else if (tagId === TAG_ID_EXIF_IMAGE_HEIGHT) {
view.setUint32(entryOffset + 8, newHeight, isLittleEndian);
}
}
}
}
}
// Переходим к следующему сегменту
offset += 2 + view.getUint16(offset + 2);
}
// Создаем новый Blob с измененными данными
const updatedImageBlob = new Blob([view.buffer], {type: "image/jpeg"});
resolve(updatedImageBlob);
});
reader.addEventListener("error", () => {
reject(new Error("Ошибка чтения файла"));
});
reader.readAsArrayBuffer(imageBlob);
});
} |
|
Этот пример позволяет изменить значения трех важных тегов: ориентации (0x0112 ) и размеров изображения (0xA002 и 0xA003 ). Его можно использовать, например, для исправления ориентации фотографий, сделанных на мобильных устройствах, или для обновления метаданных после редактирования изображения.
Удаление конфиденциальных данных из EXIF
Одной из распространенных задач при работе с фотографиями в веб-приложениях является удаление конфиденциальной информации, такой как GPS-координаты или серийные номера устройств. Давайте реализуем функцию для "очистки" JPEG от потенциально чувствительных метаданных:
JavaScript | 1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
| function sanitizeExifData(imageBlob) {
return new Promise((resolve, reject) => {
const reader = new FileReader();
reader.addEventListener("load", ({ target }) => {
if (!target || !target.result || typeof target.result === "string") {
reject(new Error("Некорректные данные"));
return;
}
const buffer = target.result;
const view = new DataView(buffer);
// Константы
const SOI = 0xFFD8;
const APP1 = 0xFFE1;
const EXIF = 0x45786966;
// Проверяем JPEG сигнатуру
if (view.getUint16(0) !== SOI) {
reject(new Error("Не JPEG файл"));
return;
}
// Создаем новый ArrayBuffer для хранения очищенного изображения
const newBuffer = new ArrayBuffer(buffer.byteLength);
const newView = new DataView(newBuffer);
// Копируем SOI маркер
newView.setUint16(0, SOI);
let newOffset = 2;
// Перебираем все сегменты JPEG
let offset = 2; // Пропускаем SOI
while (offset < buffer.byteLength) {
const marker = view.getUint16(offset);
const segmentSize = view.getUint16(offset + 2);
// Пропускаем APP1 сегмент с EXIF
if (marker === APP1 && view.getUint32(offset + 4) === EXIF) {
// Не копируем этот сегмент, пропускаем его
} else {
// Копируем другие сегменты
for (let i = 0; i < segmentSize + 2; i++) {
newView.setUint8(newOffset + i, view.getUint8(offset + i));
}
newOffset += segmentSize + 2;
}
// Переходим к следующему сегменту
offset += segmentSize + 2;
// Проверка конца файла
if (offset >= buffer.byteLength || marker === 0xFFD9) {
break;
}
}
// Создаем новый Blob из очищенного буфера
const sanitizedImageBlob = new Blob([newBuffer.slice(0, newOffset)], {type: "image/jpeg"});
resolve(sanitizedImageBlob);
});
reader.addEventListener("error", () => {
reject(new Error("Ошибка чтения файла"));
});
reader.readAsArrayBuffer(imageBlob);
});
} |
|
Этот подход полностью удаляет сегмент APP1 с EXIF-данными, что может быть полезно для обеспечения приватности пользователей, особенно при загрузке фотографий в публичный доступ. Важно помнить, что при этом теряется вся EXIF-информация, включая полезные данные, такие как ориентация или модель камеры. Если вам нужно сохранить некоторые метаданные, но удалить только конфиденциальную информацию, придется использовать более сложный подход с селективным удалением конкретных тегов.
Работа с ориентацией изображений
Один из наиболее распространённых сценариев использования EXIF — коррекция ориентации изображений. Фотографии, сделанные на мобильных устройствах, часто содержат тег ориентации, указывающий, как именно нужно повернуть изображение при отображении. Без правильной обработки этого тега фотографии могут отображаться в неправильном положении. Тег ориентации (0x0112 ) может принимать значения от 1 до 8, где 1 означает "нормальную" ориентацию, а остальные значения — различные комбинации поворотов и отражений:
JavaScript | 1
2
3
4
5
6
7
8
9
10
| const orientationValues = {
1: 'Нормальная (горизонтальная)',
2: 'Зеркальное отражение по горизонтали',
3: 'Поворот на 180°',
4: 'Зеркальное отражение по вертикали',
5: 'Зеркальное отражение по горизонтали и поворот на 90° против часовой',
6: 'Поворот на 90° по часовой (большинство фото в портретном режиме)',
7: 'Зеркальное отражение по горизонтали и поворот на 90° по часовой',
8: 'Поворот на 90° против часовой'
}; |
|
Вот функция для нормализации ориентации изображения с использованием Canvas API:
JavaScript | 1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
| async function normalizeImageOrientation(imageBlob) {
// Читаем ориентацию из EXIF
const exifData = await readExifData(imageBlob);
const orientation = exifData?.[0x0112] || 1; // Тег ориентации, по умолчанию 1
return new Promise((resolve, reject) => {
const img = new Image();
img.onload = () => {
const canvas = document.createElement('canvas');
const ctx = canvas.getContext('2d');
// Определяем размеры в зависимости от ориентации
const needsRotation = [5, 6, 7, 8].includes(orientation);
// Устанавливаем размеры canvas
if (needsRotation) {
canvas.width = img.height;
canvas.height = img.width;
} else {
canvas.width = img.width;
canvas.height = img.height;
}
// Применяем трансформации в зависимости от ориентации
switch (orientation) {
case 2: // Горизонтальное отражение
ctx.transform(-1, 0, 0, 1, canvas.width, 0);
break;
case 3: // Поворот на 180°
ctx.transform(-1, 0, 0, -1, canvas.width, canvas.height);
break;
case 4: // Вертикальное отражение
ctx.transform(1, 0, 0, -1, 0, canvas.height);
break;
case 5: // Горизонтальное отражение и поворот на 90° против часовой
ctx.transform(0, 1, 1, 0, 0, 0);
ctx.transform(-1, 0, 0, 1, canvas.width, 0);
break;
case 6: // Поворот на 90° по часовой
ctx.transform(0, 1, -1, 0, canvas.width, 0);
break;
case 7: // Горизонтальное отражение и поворот на 90° по часовой
ctx.transform(0, 1, -1, 0, canvas.width, 0);
ctx.transform(-1, 0, 0, 1, canvas.width, 0);
break;
case 8: // Поворот на 90° против часовой
ctx.transform(0, -1, 1, 0, 0, canvas.height);
break;
}
// Рисуем изображение с учетом трансформаций
ctx.drawImage(img, 0, 0);
// Получаем нормализованное изображение
canvas.toBlob(normalizedBlob => {
// Копируем метаданные, но устанавливаем ориентацию 1 (нормальная)
updateExifMetadata(normalizedBlob, 1, canvas.width, canvas.height)
.then(resolve)
.catch(reject);
}, 'image/jpeg', 0.95);
};
img.onerror = reject;
img.src = URL.createObjectURL(imageBlob);
});
} |
|
Обработка GPS данных из EXIF
EXIF может содержать GPS-данные, которые часто требуют специальной обработки для преобразования в привычный формат координат. Эти данные хранятся в GPSInfo IFD (0x8825 ), вложенном в основной IFD0, и включают несколько тегов для широты, долготы и высоты.
JavaScript | 1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
| function extractGPSCoordinates(exifData) {
if (!exifData || !exifData[0x8825]) {
return null;
}
const gpsData = exifData[0x8825];
// Проверяем наличие необходимых тегов
if (!gpsData[0x0001] || !gpsData[0x0002] || !gpsData[0x0003] || !gpsData[0x0004]) {
return null;
}
// Получаем широту и долготу
const latRefTag = gpsData[0x0001]; // N или S
const latTag = gpsData[0x0002]; // [градусы, минуты, секунды]
const longRefTag = gpsData[0x0003]; // E или W
const longTag = gpsData[0x0004]; // [градусы, минуты, секунды]
// Конвертируем из формата DMS (градусы-минуты-секунды) в десятичные градусы
const latValue = convertDMSToDecimal(latTag[0], latTag[1], latTag[2]);
const longValue = convertDMSToDecimal(longTag[0], longTag[1], longTag[2]);
// Применяем знак в зависимости от полушария
const latitude = latRefTag === 'N' ? latValue : -latValue;
const longitude = longRefTag === 'E' ? longValue : -longValue;
return { latitude, longitude };
}
// Вспомогательная функция для конвертации DMS в десятичный формат
function convertDMSToDecimal(degrees, minutes, seconds) {
return degrees + (minutes / 60) + (seconds / 3600);
} |
|
После извлечения координат их можно использовать, например, для отображения расположения на карте:
JavaScript | 1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
| async function showImageLocationOnMap(imageBlob, mapElement) {
try {
const exifData = await readExifData(imageBlob);
const coordinates = extractGPSCoordinates(exifData);
if (!coordinates) {
throw new Error('GPS данные не найдены в изображении');
}
// Используем любую картографическую библиотеку, например, Leaflet
const map = L.map(mapElement).setView([coordinates.latitude, coordinates.longitude], 13);
L.tileLayer('https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png', {
attribution: '© OpenStreetMap contributors'
}).addTo(map);
L.marker([coordinates.latitude, coordinates.longitude])
.addTo(map)
.bindPopup('Фотография была сделана здесь')
.openPopup();
return coordinates;
} catch (error) {
console.error('Ошибка при получении или отображении GPS данных:', error);
mapElement.innerHTML = 'Не удалось определить местоположение';
return null;
}
} |
|
Пакетная обработка изображений
При работе с большим количеством изображений важно оптимизировать процесс. Для этого можно использовать Web Workers, позволяющие выполнять тяжелые операции в фоновом режиме, не блокируя основной поток:
JavaScript | 1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
| // Создаем worker для обработки EXIF
const exifWorker = new Worker('exif-worker.js');
// Функция для пакетной обработки изображений
async function batchProcessImages(imageBlobs, operations) {
const results = [];
const batchSize = 5; // Количество одновременно обрабатываемых изображений
for (let i = 0; i < imageBlobs.length; i += batchSize) {
const batch = imageBlobs.slice(i, i + batchSize);
// Создаем массив промисов для текущей партии
const batchPromises = batch.map(blob => {
return new Promise((resolve, reject) => {
const id = Math.random().toString(36).substr(2, 9);
// Обработчик сообщений от воркера
const messageHandler = (e) => {
if (e.data.id === id) {
exifWorker.removeEventListener('message', messageHandler);
if (e.data.error) {
reject(new Error(e.data.error));
} else {
resolve(e.data.result);
}
}
};
exifWorker.addEventListener('message', messageHandler);
// Отправляем изображение и операции воркеру
exifWorker.postMessage({
id,
blob,
operations
});
});
});
// Ждем выполнения текущей партии
const batchResults = await Promise.allSettled(batchPromises);
results.push(...batchResults);
// Добавляем небольшую паузу между партиями, чтобы не перегружать browser
await new Promise(resolve => setTimeout(resolve, 50));
}
return results;
} |
|
И вот пример содержимого файла exif-worker.js :
JavaScript | 1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
| // exif-worker.js
self.importScripts('path-to-exif-lib.js'); // Подключаем библиотеку для работы с EXIF
self.addEventListener('message', async (e) => {
const { id, blob, operations } = e.data;
try {
let processedBlob = blob;
// Применяем операции последовательно
for (const operation of operations) {
switch (operation.type) {
case 'readExif':
// Просто читаем EXIF данные
const exifData = await readExifData(processedBlob);
self.postMessage({ id, result: { blob: processedBlob, exifData } });
return;
case 'sanitize':
// Удаляем конфиденциальные данные
processedBlob = await sanitizeExifData(processedBlob);
break;
case 'normalize':
// Нормализуем ориентацию
processedBlob = await normalizeImageOrientation(processedBlob);
break;
case 'updateMetadata':
// Обновляем метаданные
processedBlob = await updateExifMetadata(
processedBlob,
operation.orientation || 1,
operation.width,
operation.height
);
break;
}
}
// Возвращаем результат обработки
self.postMessage({ id, result: { blob: processedBlob } });
} catch (error) {
self.postMessage({ id, error: error.message });
}
});
// Здесь должны быть реализации всех необходимых функций
// (readExifData, sanitizeExifData, normalizeImageOrientation, updateExifMetadata) |
|
Использовать эту функцию можно, например, так:
JavaScript | 1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
| // Обрабатываем пакет изображений
const results = await batchProcessImages(arrayOfImageBlobs, [
{ type: 'normalize' }, // Сначала нормализуем ориентацию
{ type: 'sanitize' } // Затем удаляем конфиденциальные данные
]);
// Результаты содержат обработанные изображения или ошибки
results.forEach((result, index) => {
if (result.status === 'fulfilled') {
console.log(`Изображение ${index} успешно обработано`);
document.getElementById(`img-${index}`).src = URL.createObjectURL(result.value.blob);
} else {
console.error(`Ошибка при обработке изображения ${index}:`, result.reason);
}
}); |
|
Оптимизация для мобильных устройств
При работе с изображениями на мобильных устройствах важно учитывать ограничения памяти и процессора. Вот несколько стратегий оптимизации:
1. Прогрессивная обработка — вместо обработки полного изображения сначала работаем с уменьшенной версией для предпросмотра, и только при необходимости — с оригиналом.
2. Отложенная обработка — выполняем тяжелые операции только когда устройство находится в состоянии покоя (например, используя requestIdleCallback ).
3. Компромисс качества — снижаем качество JPEG при сохранении для уменьшения размера файла.
JavaScript | 1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
| function optimizedMobileImageProcessing(imageBlob, processingFunction) {
return new Promise((resolve, reject) => {
// Сначала создаем уменьшенную версию для быстрой предварительной обработки
createThumbnail(imageBlob, 200)
.then(thumbnailBlob => {
// Обрабатываем превью и показываем результат пользователю немедленно
processingFunction(thumbnailBlob)
.then(processedThumbnail => {
// Показываем предварительный результат
const previewUrl = URL.createObjectURL(processedThumbnail);
showPreview(previewUrl);
// Откладываем обработку полного изображения
if ('requestIdleCallback' in window) {
requestIdleCallback(() => {
// Обрабатываем полное изображение когда система не загружена
processingFunction(imageBlob)
.then(resolve)
.catch(reject);
}, { timeout: 5000 });
} else {
// Fallback для браузеров без поддержки requestIdleCallback
setTimeout(() => {
processingFunction(imageBlob)
.then(resolve)
.catch(reject);
}, 300);
}
});
})
.catch(reject);
});
}
// Вспомогательная функция для создания миниатюры
function createThumbnail(imageBlob, maxDimension) {
return new Promise((resolve, reject) => {
const img = new Image();
img.onload = () => {
// Вычисляем размеры с сохранением пропорций
let width = img.width;
let height = img.height;
if (width > height) {
if (width > maxDimension) {
height = Math.round(height * maxDimension / width);
width = maxDimension;
}
} else {
if (height > maxDimension) {
width = Math.round(width * maxDimension / height);
height = maxDimension;
}
}
const canvas = document.createElement('canvas');
canvas.width = width;
canvas.height = height;
const ctx = canvas.getContext('2d');
ctx.drawImage(img, 0, 0, width, height);
canvas.toBlob(resolve, 'image/jpeg', 0.8);
};
img.onerror = reject;
img.src = URL.createObjectURL(imageBlob);
});
} |
|
Продвинутые методы и проблемы
Работа с EXIF-данными в JavaScript выходит за рамки простых операций чтения и записи. В корпоративных проектах часто требуются более сложные решения для анализа, безопасности и оптимизации. Рассмотрим ряд продвинутых методов и проблем, с которыми вы можете столкнуться.
Анализ метаданных для определения подлинности изображений
В эпоху фейковых новостей и дипфейков проверка подлинности визуального контента становится критически важной задачей. EXIF-данные могут служить одним из инструментов для форензического анализа изображений. Вот несколько признаков, которые могут указывать на манипуляции с изображением:
JavaScript | 1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
| function detectPossibleManipulation(exifData) {
const suspiciousPatterns = [];
// Проверка на несоответствия в программном обеспечении
if (exifData[0x0131] && exifData[0x0110]) { // Software и Model
const software = exifData[0x0131];
const camera = exifData[0x0110];
// Если программа редактирования не соответствует фирменному ПО камеры
if (software !== camera && !software.includes(camera.split(' ')[0])) {
suspiciousPatterns.push('Программа редактирования не соответствует производителю камеры');
}
}
// Проверка времени создания и времени модификации
if (exifData[0x9003] && exifData[0x9004]) { // DateTimeOriginal и DateTimeDigitized
const originalDate = new Date(exifData[0x9003]);
const digitizedDate = new Date(exifData[0x9004]);
// Если разница больше 1 минуты, это может быть подозрительно
if (Math.abs(originalDate - digitizedDate) > 60000) {
suspiciousPatterns.push('Временная метка создания отличается от времени оцифровки');
}
}
// Проверка на отсутствие важных тегов, которые обычно присутствуют в оригинальных фото
const criticalTags = [0x829A, 0x829D, 0x8827]; // ExposureTime, FNumber, ISOSpeedRatings
const missingTags = criticalTags.filter(tag => !exifData[tag]);
if (missingTags.length > 0) {
suspiciousPatterns.push('Отсутствуют стандартные теги экспозиции');
}
return {
possiblyManipulated: suspiciousPatterns.length > 0,
reasons: suspiciousPatterns
};
} |
|
Конечно, этот метод не даёт 100% гарантии и должен использоваться в сочетании с другими подходами анализа изображений, такими как проверка шумов, анализ ошибок квантования JPEG или исследование уровней освещенности.
Интеграция с машинным обучением
Мощным расширением возможностей работы с изображениями является интеграция с алгоритмами машинного обучения. EXIF-данные могут использоваться как дополнительные признаки при классификации или кластеризации изображений:
JavaScript | 1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
| async function classifyImageWithMetadata(imageBlob) {
// Извлекаем визуальные признаки с помощью предобученной модели
const visualFeatures = await extractVisualFeatures(imageBlob);
// Параллельно извлекаем EXIF данные
const exifData = await readExifData(imageBlob);
// Преобразуем EXIF в числовые признаки для ML
const exifFeatures = extractMLFeaturesFromExif(exifData);
// Объединяем признаки
const combinedFeatures = [...visualFeatures, ...exifFeatures];
// Классифицируем изображение с помощью предобученной модели
const classification = await classifyWithModel(combinedFeatures);
return {
classification,
confidence: classification.confidence,
metadata: exifData
};
}
function extractMLFeaturesFromExif(exifData) {
// Преобразуем EXIF данные в числовые признаки
const features = [];
// Время дня (нормализованное)
if (exifData[0x9003]) { // DateTimeOriginal
const date = new Date(exifData[0x9003]);
features.push((date.getHours() * 60 + date.getMinutes()) / (24 * 60));
} else {
features.push(0.5); // Значение по умолчанию
}
// Выдержка (log-нормализованная)
if (exifData[0x829A]) { // ExposureTime
features.push(Math.log10(exifData[0x829A]) + 6); // +6 для положительных значений
} else {
features.push(0);
}
// ISO (нормализованное)
if (exifData[0x8827]) { // ISOSpeedRatings
features.push(Math.log2(exifData[0x8827]) / 16);
} else {
features.push(0.5);
}
// Можно добавить больше признаков...
return features;
} |
|
Такой подход особенно эффективен при решении задач определения типа сцены (день/ночь, пейзаж/портрет), времени года, погодных условий и других контекстных характеристик.
Приватность и безопасность при обработке EXIF
Работа с метаданными изображений требует особого внимания к вопросам приватности и безопасности. EXIF может содержать конфиденциальную информацию, такую как:
1. GPS-координаты (дома пользователей).
2. Серийные номера устройств.
3. Имена и контакты.
4. Даты, позволяющие определить шаблоны поведения.
Помимо функции очистки метаданных, которую мы рассмотрели ранее, полезно иметь более тонкий контроль над тем, какие данные сохраняются, а какие нет:
JavaScript | 1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
| function sanitizeExifSelectively(imageBlob, options = {}) {
const {
removeLocation = true,
removeDeviceInfo = true,
removeDates = false,
removeAllExif = false,
customTagsToRemove = [],
customTagsToKeep = []
} = options;
return new Promise((resolve, reject) => {
readExifData(imageBlob)
.then(exifData => {
if (removeAllExif) {
// Полностью удаляем EXIF
return sanitizeExifData(imageBlob).then(resolve);
}
// Список тегов для удаления
const tagsToRemove = [...customTagsToRemove];
if (removeLocation) {
// GPS-теги
tagsToRemove.push(0x8825); // GPSInfo
}
if (removeDeviceInfo) {
// Информация об устройстве
tagsToRemove.push(0x010F); // Make
tagsToRemove.push(0x0110); // Model
tagsToRemove.push(0x0131); // Software
tagsToRemove.push(0xA433); // LensMake
tagsToRemove.push(0xA434); // LensModel
tagsToRemove.push(0x0132); // DateTime
}
if (removeDates) {
// Временные метки
tagsToRemove.push(0x9003); // DateTimeOriginal
tagsToRemove.push(0x9004); // DateTimeDigitized
tagsToRemove.push(0x0132); // DateTime
}
// Применяем фильтр к копии данных
const filteredExif = filterExifTags(exifData, tagsToRemove, customTagsToKeep);
// Перезаписываем EXIF данные
createImageWithNewExif(imageBlob, filteredExif)
.then(resolve)
.catch(reject);
})
.catch(reject);
});
}
function filterExifTags(exifData, tagsToRemove, tagsToKeep) {
const result = {};
// Копируем только теги, которые не в списке удаляемых
// или присутствуют в списке сохраняемых
Object.keys(exifData).forEach(tagId => {
const numTagId = parseInt(tagId, 10);
if (
(tagsToRemove.indexOf(numTagId) === -1 || tagsToKeep.indexOf(numTagId) !== -1) &&
tagId !== '0x8825' // Особая обработка для GPSInfo
) {
result[tagId] = exifData[tagId];
}
});
// Особая обработка для GPS данных (они в отдельном IFD)
if (exifData['0x8825'] && tagsToRemove.indexOf(0x8825) === -1) {
result['0x8825'] = filterExifTags(
exifData['0x8825'],
tagsToRemove,
tagsToKeep
);
}
return result;
} |
|
Такой подход обеспечивает баланс между сохранением полезных метаданных (например, ориентации) и защитой приватности пользователей.
Работа с нестандартными тегами EXIF
Кроме стандартных тегов, EXIF позволяет хранить проприетарные данные производителей. Например, Nikon, Canon или Sony могут добавлять свои метаданные в особых IFD (MakerNote). Эти данные могут содержать ценную информацию о настройках камеры:
JavaScript | 1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
| function parseMakerNotes(exifData) {
// Тег с данными производителя
const makerNoteTag = 0x927C;
if (!exifData[makerNoteTag]) {
return null;
}
// Определяем производителя
const make = exifData[0x010F] || '';
if (make.includes('NIKON')) {
return parseNikonMakerNote(exifData[makerNoteTag]);
} else if (make.includes('Canon')) {
return parseCanonMakerNote(exifData[makerNoteTag]);
} else if (make.includes('SONY')) {
return parseSonyMakerNote(exifData[makerNoteTag]);
}
return null;
}
// Пример парсера для Nikon
function parseNikonMakerNote(noteData) {
try {
// Nikon MakerNote начинается с сигнатуры
if (noteData.slice(0, 6) !== 'Nikon\0') {
return null;
}
// Дальнейший код для парсинга формата Nikon...
// Это сложная задача, т.к. формат проприетарный
// и различается между моделями камер
return {
// Извлеченные данные...
};
} catch (e) {
console.warn('Ошибка при парсинге Nikon MakerNote:', e);
return null;
}
} |
|
Работа с проприетарными данными сложна из-за отсутствия полной документации и изменений формата между различными версиями прошивок камер. Однако эти данные могут быть полезны для профессиональных фотографов и специализированных приложений.
Производительность при работе с большими объемами изображений
Производительность становится серьезной проблемой, когда приложение должно обрабатывать сотни или тысячи изображений. При работе с EXIF в JavaScript можно применить несколько оптимизаций:
JavaScript | 1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
| // Кеширование результатов обработки для одинаковых изображений
const exifCache = new Map();
function getCachedOrExtractExif(imageBlob) {
// Используем хеш или идентификатор изображения как ключ
const blobId = generateBlobId(imageBlob);
if (exifCache.has(blobId)) {
return Promise.resolve(exifCache.get(blobId));
}
return readExifData(imageBlob).then(exifData => {
exifCache.set(blobId, exifData);
return exifData;
});
}
function generateBlobId(blob) {
return `${blob.size}_${blob.type}_${blob.lastModified || Date.now()}`;
} |
|
При обработке множества изображений также стоит обратить внимание на освобождение памяти, особенно при работе с URL.createObjectURL:
JavaScript | 1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
| function processImageBatch(images) {
const objectUrls = [];
// Создаем объектные URL для предпросмотра
images.forEach(img => {
const url = URL.createObjectURL(img);
objectUrls.push(url);
showPreview(url);
});
// После завершения обработки - освобождаем ресурсы
return doSomeProcessing(images).finally(() => {
// Отзываем все созданные URL, чтобы избежать утечек памяти
objectUrls.forEach(url => URL.revokeObjectURL(url));
});
} |
|
Тестирование и отладка обработки EXIF
При работе с EXIF часто возникают сложные ситуации из-за разнообразия форматов и источников изображений. Вот несколько подходов к отладке:
JavaScript | 1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
| // Утилита для детального логирования EXIF-структуры
function debugExif(exifData, prefix = '') {
if (!exifData) return console.log(prefix + 'Нет данных EXIF');
Object.entries(exifData).forEach(([tag, value]) => {
// Преобразуем десятичный тег в шестнадцатеричный для удобства
const hexTag = `0x${Number(tag).toString(16).padStart(4, '0').toUpperCase()}`;
// Проверяем, является ли значение вложенным объектом (например, IFD)
if (value && typeof value === 'object' && !Array.isArray(value)) {
console.group(`${prefix}${hexTag} (IFD)`);
debugExif(value, '');
console.groupEnd();
} else {
console.log(`${prefix}${hexTag}: ${value}`);
}
});
} |
|
Для комплексного тестирования создайте набор изображений с разными параметрами:
1. Фото с различных устройств (смартфоны, фотоаппараты, сканеры).
2. Изображения с разной ориентацией.
3. Изображения с GPS и без.
4. Изображения, отредактированные в различных программах.
Совместимость с разными стандартами и форматами
Помимо EXIF существуют и другие стандарты метаданных, такие как XMP (Extensible Metadata Platform) или IPTC (International Press Telecommunications Council). В некоторых случаях может потребоваться интеграция с ними:
JavaScript | 1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
| // Пример функции для извлечения XMP из JPEG
function extractXMPData(buffer) {
const view = new DataView(buffer);
let offset = 2; // Пропускаем SOI маркер
while (offset < view.byteLength) {
const marker = view.getUint16(offset);
// XMP хранится в APP1 сегменте, но с другой сигнатурой
if (marker === 0xFFE1) {
const segmentSize = view.getUint16(offset + 2);
// Проверяем XMP сигнатуру "http://ns.adobe.com/xap/1.0/\0"
const xmpSignature = "http://ns.adobe.com/xap/1.0/\0";
let isXMP = true;
for (let i = 0; i < xmpSignature.length; i++) {
if (view.getUint8(offset + 4 + i) !== xmpSignature.charCodeAt(i)) {
isXMP = false;
break;
}
}
if (isXMP) {
// Начало XMP данных после сигнатуры
const xmpStart = offset + 4 + xmpSignature.length;
const xmpLength = segmentSize - 2 - xmpSignature.length;
// Извлекаем XML данные
const xmpData = new Uint8Array(buffer, xmpStart, xmpLength);
const decoder = new TextDecoder();
return decoder.decode(xmpData);
}
}
// Переходим к следующему сегменту
offset += 2 + view.getUint16(offset + 2);
}
return null;
} |
|
В завершение следует отметить, что работа с метаданными изображений — это компромисс между функциональностью, производительностью и размером кода. Выбор подхода зависит от конкретных требований проекта, но понимание фундаментальной структуры данных EXIF дает вам гибкость для создания именно того решения, которое необходимо.
Считывание Exif информации картинки jpeg Добрый день! Подскажите пожалуйста как лучше считать данные, а именно информацию GPS с Exif. На входе jpeg картинка(C# forms)??? Редактирование/создание EXIF заголовка jpeg файла Очень нужны исходники программы позволяющей редактировать и создавать EXIF заголовки у jpeg файлов , поделитесь пожалуйста если у кого есть. Как считать дату из EXIF jpeg файла? Как считать дату из EXIF jpeg файла? Jpeg файл - достать дату съемки (exif) Как в delphi написать функцию для считывания информации о дате фотографии?
то есть на входе строка - путь к файлу, на выходе - строка с датой Добавить наименование jpeg-файла в его exif описание Необходимо добавить наименование файла в его же описание, исключая его расширение:
Файлов очень много, поэтому сложно делать эту работу вручную,... Работа с метаданными в 1С 8 Мне нужно выгрузить данные из справочников и документов из 1С в Word, т.е нужно выгрузить метаданные.
Проблема в том, что Я не знаю, как работать с... Работа с метаданными в C++ Подскажите как в C++ работать с метаданными изображения, а именно с EXIF (Exchangeable Image File Format).
Вообще цель только получить их, поэтому... Работа с метаданными в 1С 8 Подскажите путь решения такой задачи: требуется удалить все документы из базы (может быть как типовая, так и не типовая конфигурация). Через... Работа с метаданными. Изменение длины числа. 1С 7.7 Работа с метаданными.
Добавлено через 2 минуты
Добрый день, в свойствах реквизита, при выборе значение "число" в поле... работа с exif подскажите хорошую библиотечку для delphi для работы с exif информацией о изображении Работа с библиотекой CCR.EXIF Нужна помощь, кто-нибудь работал с библиотекой CCR.EXIF для вытаскивания метаданных из *.jpeg. Нужно вытащить такую информацию
Фокусное... Работа с EXIF информацией JPG - BitmapMetadata Коллеги, пытаюсь работать с BitmapMetadata. Нигде не могу найти информацию по формированию запросов в функциях GetQuery/SetQuery. Какая то...
|