QGroundControl
Ground Control Station for MAVLink Drones
Loading...
Searching...
No Matches
ExifUtility.cc
Go to the documentation of this file.
1#include "ExifUtility.h"
3
4#include <cassert>
5#include <cmath>
6
7QGC_LOGGING_CATEGORY(ExifUtilityLog, "Utilities.ExifUtility")
8
9namespace ExifUtility
10{
11
12// ============================================================================
13// EXIF Data Management
14// ============================================================================
15
16ExifData* loadFromBuffer(const QByteArray &buffer)
17{
18 return exif_data_new_from_data(
19 reinterpret_cast<const unsigned char*>(buffer.constData()),
20 static_cast<unsigned int>(buffer.size())
21 );
22}
23
24ExifData* createNew()
25{
26 ExifData *data = exif_data_new();
27 if (data) {
28 // Set standard options as recommended by libexif examples
29 exif_data_set_option(data, EXIF_DATA_OPTION_FOLLOW_SPECIFICATION);
30 exif_data_set_data_type(data, EXIF_DATA_TYPE_COMPRESSED);
31 exif_data_set_byte_order(data, EXIF_BYTE_ORDER_INTEL);
32 // Create mandatory EXIF fields with default values
33 exif_data_fix(data);
34 }
35 return data;
36}
37
38bool saveToBuffer(ExifData *data, QByteArray &buffer)
39{
40 if (!data) {
41 qCWarning(ExifUtilityLog) << "Null EXIF data";
42 return false;
43 }
44
45 if (!isJpeg(buffer)) {
46 if (isTiff(buffer)) {
47 qCWarning(ExifUtilityLog) << "TIFF/DNG format not supported for EXIF writing";
48 } else {
49 qCWarning(ExifUtilityLog) << "Not a valid JPEG file";
50 }
51 return false;
52 }
53
54 // Generate EXIF output
55 unsigned char *exifBuffer = nullptr;
56 unsigned int exifSize = 0;
57 exif_data_save_data(data, &exifBuffer, &exifSize);
58
59 if (!exifBuffer || exifSize == 0) {
60 qCWarning(ExifUtilityLog) << "Failed to generate EXIF data";
61 if (exifBuffer) free(exifBuffer);
62 return false;
63 }
64
65 // Find existing APP1 marker
66 int app1Start = -1;
67 int app1End = -1;
68 int pos = 2;
69
70 while (pos < buffer.size() - 3) {
71 if (static_cast<unsigned char>(buffer[pos]) == 0xFF) {
72 unsigned char marker = static_cast<unsigned char>(buffer[pos + 1]);
73
74 if (marker == 0xE1) { // APP1 (EXIF)
75 app1Start = pos;
76 int segmentLen = (static_cast<unsigned char>(buffer[pos + 2]) << 8) |
77 static_cast<unsigned char>(buffer[pos + 3]);
78 app1End = pos + 2 + segmentLen;
79 break;
80 } else if (marker == 0xDA) { // SOS - stop searching
81 break;
82 } else if (marker >= 0xE0 && marker <= 0xEF) { // Other APPn
83 int segmentLen = (static_cast<unsigned char>(buffer[pos + 2]) << 8) |
84 static_cast<unsigned char>(buffer[pos + 3]);
85 pos += 2 + segmentLen;
86 continue;
87 } else if (marker == 0xD8 || marker == 0xD9 || marker == 0x00) {
88 pos++;
89 continue;
90 }
91 }
92 pos++;
93 }
94
95 // Build new APP1 segment
96 QByteArray newApp1;
97 newApp1.append('\xFF');
98 newApp1.append('\xE1');
99
100 int app1Len = exifSize + 2;
101 newApp1.append(static_cast<char>((app1Len >> 8) & 0xFF));
102 newApp1.append(static_cast<char>(app1Len & 0xFF));
103 newApp1.append(reinterpret_cast<const char*>(exifBuffer), exifSize);
104
105 free(exifBuffer);
106
107 // Reconstruct JPEG
108 QByteArray newBuffer;
109 newBuffer.reserve(buffer.size() + newApp1.size());
110
111 // SOI marker
112 newBuffer.append(buffer.left(2));
113
114 // New APP1
115 newBuffer.append(newApp1);
116
117 // Rest of image (skip old APP1 if found)
118 if (app1Start > 0 && app1End > app1Start && app1End <= buffer.size()) {
119 newBuffer.append(buffer.mid(app1End));
120 } else {
121 newBuffer.append(buffer.mid(2));
122 }
123
124 buffer = newBuffer;
125 return true;
126}
127
128bool isJpeg(const QByteArray &buffer)
129{
130 return buffer.size() >= 2 &&
131 static_cast<unsigned char>(buffer[0]) == 0xFF &&
132 static_cast<unsigned char>(buffer[1]) == 0xD8;
133}
134
135bool isTiff(const QByteArray &buffer)
136{
137 if (buffer.size() < 4) {
138 return false;
139 }
140 // TIFF files start with "II" (Intel/little-endian) or "MM" (Motorola/big-endian)
141 // followed by magic number 42 (0x002A)
142 if (buffer[0] == 'I' && buffer[1] == 'I') {
143 return static_cast<unsigned char>(buffer[2]) == 0x2A &&
144 static_cast<unsigned char>(buffer[3]) == 0x00;
145 }
146 if (buffer[0] == 'M' && buffer[1] == 'M') {
147 return static_cast<unsigned char>(buffer[2]) == 0x00 &&
148 static_cast<unsigned char>(buffer[3]) == 0x2A;
149 }
150 return false;
151}
152
153bool hasExifData(const QByteArray &buffer)
154{
155 if (buffer.size() < 12) {
156 return false;
157 }
158
159 // TIFF files contain EXIF data inline
160 if (isTiff(buffer)) {
161 return true;
162 }
163
164 // For JPEG, check for APP1 marker with EXIF header
165 if (!isJpeg(buffer)) {
166 return false;
167 }
168
169 QByteArray app1Marker("\xFF\xE1", 2);
170 int app1Pos = buffer.indexOf(app1Marker, 2);
171 if (app1Pos < 0 || app1Pos + 10 > buffer.size()) {
172 return false;
173 }
174
175 QByteArray exifHeader("Exif\0\0", 6);
176 return buffer.mid(app1Pos + 4, 6) == exifHeader;
177}
178
179// ============================================================================
180// Tag Reading Helpers
181// ============================================================================
182
183QString readString(ExifData *data, ExifTag tag, ExifIfd ifd)
184{
185 if (!data) return QString();
186
187 ExifEntry *entry = exif_content_get_entry(data->ifd[ifd], tag);
188 if (!entry) {
189 entry = exif_data_get_entry(data, tag);
190 }
191 if (!entry) {
192 return QString();
193 }
194
195 char value[256];
196 exif_entry_get_value(entry, value, sizeof(value));
197 return QString::fromUtf8(value).trimmed();
198}
199
200int readShort(ExifData *data, ExifTag tag, ExifIfd ifd)
201{
202 if (!data) return 0;
203
204 ExifEntry *entry = exif_content_get_entry(data->ifd[ifd], tag);
205 if (!entry || entry->size < 2) {
206 return 0;
207 }
208
209 ExifByteOrder order = exif_data_get_byte_order(data);
210 return exif_get_short(entry->data, order);
211}
212
213double readRational(ExifData *data, ExifTag tag, ExifIfd ifd)
214{
215 if (!data) return 0.0;
216
217 ExifEntry *entry = exif_content_get_entry(data->ifd[ifd], tag);
218 if (!entry || entry->size < 8) {
219 return 0.0;
220 }
221
222 ExifByteOrder order = exif_data_get_byte_order(data);
223 ExifRational rational = exif_get_rational(entry->data, order);
224 if (rational.denominator == 0) {
225 return 0.0;
226 }
227 return static_cast<double>(rational.numerator) / static_cast<double>(rational.denominator);
228}
229
230// ============================================================================
231// Tag Writing Helpers
232// ============================================================================
233
234ExifEntry* initTag(ExifData *data, ExifIfd ifd, ExifTag tag)
235{
236 if (!data) return nullptr;
237
238 // Return existing tag if present
239 ExifEntry *entry = exif_content_get_entry(data->ifd[ifd], tag);
240 if (entry) {
241 return entry;
242 }
243
244 // Create new entry
245 entry = exif_entry_new();
246 if (!entry) return nullptr;
247
248 // Tag must be set before adding to content
249 entry->tag = tag;
250
251 // Add to IFD
252 exif_content_add_entry(data->ifd[ifd], entry);
253
254 // Initialize with default data (allocates memory for standard tags)
255 exif_entry_initialize(entry, tag);
256
257 // IFD now owns the entry, release our reference
258 exif_entry_unref(entry);
259
260 return exif_content_get_entry(data->ifd[ifd], tag);
261}
262
263ExifEntry* createTag(ExifData *data, ExifIfd ifd, ExifTag tag, ExifFormat format, unsigned long components)
264{
265 if (!data) return nullptr;
266
267 // Create a memory allocator for this entry
268 ExifMem *mem = exif_mem_new_default();
269 if (!mem) return nullptr;
270
271 // Create entry using our allocator
272 ExifEntry *entry = exif_entry_new_mem(mem);
273 if (!entry) {
274 exif_mem_unref(mem);
275 return nullptr;
276 }
277
278 // Calculate size and allocate data buffer
279 size_t size = exif_format_get_size(format) * components;
280 unsigned char *buf = static_cast<unsigned char*>(exif_mem_alloc(mem, size));
281 if (!buf) {
282 exif_entry_unref(entry);
283 exif_mem_unref(mem);
284 return nullptr;
285 }
286
287 // Fill in entry fields
288 entry->data = buf;
289 entry->size = size;
290 entry->tag = tag;
291 entry->format = format;
292 entry->components = components;
293
294 // Add to IFD
295 exif_content_add_entry(data->ifd[ifd], entry);
296
297 // Release our references - IFD now owns the entry
298 exif_mem_unref(mem);
299 exif_entry_unref(entry);
300
301 return exif_content_get_entry(data->ifd[ifd], tag);
302}
303
304// ============================================================================
305// GPS Coordinate Helpers
306// ============================================================================
307
308double gpsRationalToDecimal(ExifEntry *entry, ExifByteOrder order)
309{
310 if (!entry || entry->components < 3 || !entry->data) {
311 return 0.0;
312 }
313
314 double degrees = 0.0;
315 double minutes = 0.0;
316 double seconds = 0.0;
317
318 for (unsigned int i = 0; i < entry->components && i < 3; i++) {
319 ExifRational rational = exif_get_rational(entry->data + i * 8, order);
320 if (rational.denominator == 0) {
321 continue;
322 }
323 double value = static_cast<double>(rational.numerator) / static_cast<double>(rational.denominator);
324 switch (i) {
325 case 0: degrees = value; break;
326 case 1: minutes = value; break;
327 case 2: seconds = value; break;
328 }
329 }
330
331 return degrees + (minutes / 60.0) + (seconds / 3600.0);
332}
333
334void writeGpsCoordinate(ExifEntry *entry, ExifByteOrder order, double value)
335{
336 if (!entry || !entry->data || entry->size < 24) return;
337
338 double absVal = fabs(value);
339 int degrees = static_cast<int>(absVal);
340 double minutesF = (absVal - degrees) * 60.0;
341 int minutes = static_cast<int>(minutesF);
342 double seconds = (minutesF - minutes) * 60.0;
343
344 ExifRational rationals[3] = {
345 {static_cast<ExifLong>(degrees), 1},
346 {static_cast<ExifLong>(minutes), 1},
347 {static_cast<ExifLong>(static_cast<int>(seconds * 1000)), 1000}
348 };
349
350 for (int i = 0; i < 3; i++) {
351 exif_set_rational(entry->data + i * 8, order, rationals[i]);
352 }
353}
354
355void writeRational(ExifEntry *entry, ExifByteOrder order, double value, int denominator)
356{
357 if (!entry || !entry->data || entry->size < 8) return;
358
359 ExifRational rational = {
360 static_cast<ExifLong>(static_cast<int>(fabs(value) * denominator)),
361 static_cast<ExifLong>(denominator)
362 };
363 exif_set_rational(entry->data, order, rational);
364}
365
366void writeGpsRef(ExifEntry *entry, char value)
367{
368 if (!entry || !entry->data || entry->size < 2) return;
369 entry->data[0] = static_cast<unsigned char>(value);
370 entry->data[1] = '\0';
371}
372
373void writeGpsAltRef(ExifEntry *entry, unsigned char value)
374{
375 if (!entry || !entry->data || entry->size < 1) return;
376 entry->data[0] = value;
377}
378
379// ============================================================================
380// DateTime Helpers
381// ============================================================================
382
383bool writeDateTimeOriginal(ExifData *data, const QDateTime &dateTime)
384{
385 if (!data || !dateTime.isValid()) {
386 return false;
387 }
388
389 // EXIF DateTime format: "YYYY:MM:DD HH:MM:SS" (20 bytes including null terminator)
390 const QString dateStr = dateTime.toString(QStringLiteral("yyyy:MM:dd hh:mm:ss"));
391 const QByteArray dateBytes = dateStr.toLatin1();
392
393 // Write to both DateTimeOriginal and DateTimeDigitized for compatibility
394 // (some readers prefer one over the other)
395 const ExifTag tags[] = {EXIF_TAG_DATE_TIME_ORIGINAL, EXIF_TAG_DATE_TIME_DIGITIZED};
396 bool success = false;
397
398 for (ExifTag tag : tags) {
399 ExifEntry *entry = initTag(data, EXIF_IFD_EXIF, tag);
400 if (!entry) {
401 qCWarning(ExifUtilityLog) << "Failed to create DateTime tag:" << tag;
402 continue;
403 }
404
405 // Entry should have 20 bytes allocated by initTag
406 if (entry->size >= 20 && entry->data) {
407 memcpy(entry->data, dateBytes.constData(), qMin(static_cast<qsizetype>(19), dateBytes.size()));
408 entry->data[19] = '\0';
409 success = true;
410 } else {
411 qCWarning(ExifUtilityLog) << "DateTime entry size mismatch:" << entry->size << "for tag" << tag;
412 }
413 }
414
415 return success;
416}
417
418} // namespace ExifUtility
#define QGC_LOGGING_CATEGORY(name, categoryStr)
ExifEntry * initTag(ExifData *data, ExifIfd ifd, ExifTag tag)
double readRational(ExifData *data, ExifTag tag, ExifIfd ifd)
Read a rational value from an EXIF tag.
void writeGpsAltRef(ExifEntry *entry, unsigned char value)
Write a byte value to a GPS altitude reference entry (0=above, 1=below sea level)
bool isTiff(const QByteArray &buffer)
ExifEntry * createTag(ExifData *data, ExifIfd ifd, ExifTag tag, ExifFormat format, unsigned long components)
ExifData * loadFromBuffer(const QByteArray &buffer)
void writeGpsCoordinate(ExifEntry *entry, ExifByteOrder order, double value)
Write GPS coordinate as EXIF rationals (degrees, minutes, seconds)
void writeRational(ExifEntry *entry, ExifByteOrder order, double value, int denominator)
Write a single rational value to an entry.
bool writeDateTimeOriginal(ExifData *data, const QDateTime &dateTime)
bool hasExifData(const QByteArray &buffer)
Check if a buffer contains valid JPEG with EXIF data.
QString readString(ExifData *data, ExifTag tag, ExifIfd ifd)
Read a string value from an EXIF tag.
bool isJpeg(const QByteArray &buffer)
Check if a buffer is a JPEG image (starts with 0xFF 0xD8)
bool saveToBuffer(ExifData *data, QByteArray &buffer)
double gpsRationalToDecimal(ExifEntry *entry, ExifByteOrder order)
Convert GPS coordinate from EXIF rational format (deg/min/sec) to decimal degrees.
ExifData * createNew()
void writeGpsRef(ExifEntry *entry, char value)
Write an ASCII character to a GPS reference entry (N/S/E/W)
int readShort(ExifData *data, ExifTag tag, ExifIfd ifd)
Read a short (16-bit) value from an EXIF tag.