1.3.77
 
Loading...
Searching...
No Matches
exif_writer.cpp
Go to the documentation of this file.
1
16#include "exif_writer.h"
17
18#include <array>
19#include <cmath>
20#include <cstdint>
21#include <cstring>
22#include <sstream>
23#include <string>
24#include <vector>
25
26#include "global.h"
27#include "pugixml.hpp"
28
29namespace helios::detail {
30
31 namespace {
32
33 // EXIF/TIFF data types (TIFF 6.0 / EXIF 2.32)
34 constexpr uint16_t TYPE_BYTE = 1;
35 constexpr uint16_t TYPE_ASCII = 2;
36 constexpr uint16_t TYPE_SHORT = 3;
37 constexpr uint16_t TYPE_LONG = 4;
38 constexpr uint16_t TYPE_RATIONAL = 5;
39 constexpr uint16_t TYPE_UNDEFINED = 7;
40 constexpr uint16_t TYPE_SRATIONAL = 10;
41
42 // JPEG APP1 segment payload hard limit (65535 - 2 length bytes - 0)
43 constexpr size_t APP1_MAX_PAYLOAD = 65533;
44
45 // Number of bytes occupied by an IFD entry's "value/offset" field
46 constexpr size_t IFD_ENTRY_VALUE_BYTES = 4;
47
48 // ---- Little-endian raw writers ----
49
50 void putU16(std::vector<unsigned char> &b, uint16_t v) {
51 b.push_back(static_cast<unsigned char>(v & 0xFF));
52 b.push_back(static_cast<unsigned char>((v >> 8) & 0xFF));
53 }
54 void putU32(std::vector<unsigned char> &b, uint32_t v) {
55 b.push_back(static_cast<unsigned char>(v & 0xFF));
56 b.push_back(static_cast<unsigned char>((v >> 8) & 0xFF));
57 b.push_back(static_cast<unsigned char>((v >> 16) & 0xFF));
58 b.push_back(static_cast<unsigned char>((v >> 24) & 0xFF));
59 }
60 void patchU32(std::vector<unsigned char> &b, size_t pos, uint32_t v) {
61 b[pos] = static_cast<unsigned char>(v & 0xFF);
62 b[pos + 1] = static_cast<unsigned char>((v >> 8) & 0xFF);
63 b[pos + 2] = static_cast<unsigned char>((v >> 16) & 0xFF);
64 b[pos + 3] = static_cast<unsigned char>((v >> 24) & 0xFF);
65 }
66
67 // ---- IFD entry representation ----
68 //
69 // Each entry has: tag, type, count, and either an inline value (<=4 bytes) or a pointer
70 // to data appended to a trailing data blob. We collect entries first, then emit the IFD
71 // in two passes: pass 1 finalizes data-blob offsets given a base offset; pass 2 emits the
72 // 12-byte entry records and the trailing data blob.
73 struct IFDEntry {
74 uint16_t tag = 0;
75 uint16_t type = 0;
76 uint32_t count = 0;
77 // If the value fits in 4 bytes, it is stored inline here (padded with zeros).
78 std::array<unsigned char, 4> inline_value{};
79 // If it does not fit, the data is stored here and a pointer is written instead.
80 std::vector<unsigned char> external_data;
81 bool inline_only = true;
82 // For sub-IFD pointer tags (ExifSubIFD, GPSSubIFD): inline_value carries an offset
83 // patched in later. The patch site within the entry record itself is at byte offset 8
84 // (tag=2 + type=2 + count=4 = 8).
85 bool is_pointer_placeholder = false;
86 // Bookkeeping for pointer placeholders to know which sub-IFD they point to.
87 int pointer_target_index = -1; // 0 = ExifSubIFD, 1 = GPS IFD
88 };
89
90 size_t typeSize(uint16_t t) {
91 switch (t) {
92 case TYPE_BYTE:
93 case TYPE_ASCII:
94 case TYPE_UNDEFINED:
95 return 1;
96 case TYPE_SHORT:
97 return 2;
98 case TYPE_LONG:
99 return 4;
100 case TYPE_RATIONAL:
101 case TYPE_SRATIONAL:
102 return 8;
103 default:
104 return 1;
105 }
106 }
107
108 // Pack a value into IFDEntry, choosing inline vs external based on byte length.
109 void finalizeEntry(IFDEntry &e, const std::vector<unsigned char> &raw) {
110 const size_t bytes = raw.size();
111 if (bytes <= IFD_ENTRY_VALUE_BYTES) {
112 e.inline_only = true;
113 e.inline_value.fill(0);
114 std::memcpy(e.inline_value.data(), raw.data(), bytes);
115 } else {
116 e.inline_only = false;
117 e.external_data = raw;
118 }
119 }
120
121 IFDEntry makeAscii(uint16_t tag, const std::string &s) {
122 // EXIF ASCII strings are NUL-terminated; count includes the NUL byte.
123 IFDEntry e;
124 e.tag = tag;
125 e.type = TYPE_ASCII;
126 e.count = static_cast<uint32_t>(s.size() + 1);
127 std::vector<unsigned char> raw(s.size() + 1, 0);
128 if (!s.empty()) {
129 std::memcpy(raw.data(), s.data(), s.size());
130 }
131 finalizeEntry(e, raw);
132 return e;
133 }
134
135 IFDEntry makeUndefined(uint16_t tag, const std::string &s) {
136 // UNDEFINED is raw bytes; not NUL-terminated.
137 IFDEntry e;
138 e.tag = tag;
139 e.type = TYPE_UNDEFINED;
140 e.count = static_cast<uint32_t>(s.size());
141 std::vector<unsigned char> raw(s.size());
142 if (!s.empty()) {
143 std::memcpy(raw.data(), s.data(), s.size());
144 }
145 finalizeEntry(e, raw);
146 return e;
147 }
148
149 IFDEntry makeShort(uint16_t tag, uint16_t v) {
150 IFDEntry e;
151 e.tag = tag;
152 e.type = TYPE_SHORT;
153 e.count = 1;
154 // SHORT values <=4 bytes always inline. EXIF requires SHORT inline values to be
155 // written in the low half of the 4-byte value field (little-endian).
156 std::vector<unsigned char> raw(2);
157 raw[0] = static_cast<unsigned char>(v & 0xFF);
158 raw[1] = static_cast<unsigned char>((v >> 8) & 0xFF);
159 finalizeEntry(e, raw);
160 return e;
161 }
162
163 IFDEntry makeLong(uint16_t tag, uint32_t v) {
164 IFDEntry e;
165 e.tag = tag;
166 e.type = TYPE_LONG;
167 e.count = 1;
168 std::vector<unsigned char> raw(4);
169 raw[0] = static_cast<unsigned char>(v & 0xFF);
170 raw[1] = static_cast<unsigned char>((v >> 8) & 0xFF);
171 raw[2] = static_cast<unsigned char>((v >> 16) & 0xFF);
172 raw[3] = static_cast<unsigned char>((v >> 24) & 0xFF);
173 finalizeEntry(e, raw);
174 return e;
175 }
176
177 IFDEntry makeSRational(uint16_t tag, int32_t numerator, int32_t denominator) {
178 IFDEntry e;
179 e.tag = tag;
180 e.type = TYPE_SRATIONAL;
181 e.count = 1;
182 std::vector<unsigned char> raw(8);
183 const uint32_t n = static_cast<uint32_t>(numerator);
184 const uint32_t d = static_cast<uint32_t>(denominator);
185 raw[0] = static_cast<unsigned char>(n & 0xFF);
186 raw[1] = static_cast<unsigned char>((n >> 8) & 0xFF);
187 raw[2] = static_cast<unsigned char>((n >> 16) & 0xFF);
188 raw[3] = static_cast<unsigned char>((n >> 24) & 0xFF);
189 raw[4] = static_cast<unsigned char>(d & 0xFF);
190 raw[5] = static_cast<unsigned char>((d >> 8) & 0xFF);
191 raw[6] = static_cast<unsigned char>((d >> 16) & 0xFF);
192 raw[7] = static_cast<unsigned char>((d >> 24) & 0xFF);
193 finalizeEntry(e, raw);
194 return e;
195 }
196
197 IFDEntry makeRational(uint16_t tag, uint32_t numerator, uint32_t denominator) {
198 IFDEntry e;
199 e.tag = tag;
200 e.type = TYPE_RATIONAL;
201 e.count = 1;
202 std::vector<unsigned char> raw(8);
203 raw[0] = static_cast<unsigned char>(numerator & 0xFF);
204 raw[1] = static_cast<unsigned char>((numerator >> 8) & 0xFF);
205 raw[2] = static_cast<unsigned char>((numerator >> 16) & 0xFF);
206 raw[3] = static_cast<unsigned char>((numerator >> 24) & 0xFF);
207 raw[4] = static_cast<unsigned char>(denominator & 0xFF);
208 raw[5] = static_cast<unsigned char>((denominator >> 8) & 0xFF);
209 raw[6] = static_cast<unsigned char>((denominator >> 16) & 0xFF);
210 raw[7] = static_cast<unsigned char>((denominator >> 24) & 0xFF);
211 finalizeEntry(e, raw);
212 return e;
213 }
214
215 IFDEntry makeRationalArray(uint16_t tag, const std::vector<std::pair<uint32_t, uint32_t>> &rats) {
216 IFDEntry e;
217 e.tag = tag;
218 e.type = TYPE_RATIONAL;
219 e.count = static_cast<uint32_t>(rats.size());
220 std::vector<unsigned char> raw(rats.size() * 8);
221 for (size_t i = 0; i < rats.size(); ++i) {
222 const uint32_t n = rats[i].first;
223 const uint32_t d = rats[i].second;
224 raw[i * 8 + 0] = static_cast<unsigned char>(n & 0xFF);
225 raw[i * 8 + 1] = static_cast<unsigned char>((n >> 8) & 0xFF);
226 raw[i * 8 + 2] = static_cast<unsigned char>((n >> 16) & 0xFF);
227 raw[i * 8 + 3] = static_cast<unsigned char>((n >> 24) & 0xFF);
228 raw[i * 8 + 4] = static_cast<unsigned char>(d & 0xFF);
229 raw[i * 8 + 5] = static_cast<unsigned char>((d >> 8) & 0xFF);
230 raw[i * 8 + 6] = static_cast<unsigned char>((d >> 16) & 0xFF);
231 raw[i * 8 + 7] = static_cast<unsigned char>((d >> 24) & 0xFF);
232 }
233 finalizeEntry(e, raw);
234 return e;
235 }
236
237 // Convert decimal degrees (absolute value) to (degrees/1, minutes/1, seconds*1e6/1e6) rationals.
238 std::vector<std::pair<uint32_t, uint32_t>> decimalDegToDMSRationals(double deg) {
239 double abs_deg = std::fabs(deg);
240 uint32_t d = static_cast<uint32_t>(std::floor(abs_deg));
241 double rem_min = (abs_deg - static_cast<double>(d)) * 60.0;
242 uint32_t m = static_cast<uint32_t>(std::floor(rem_min));
243 double rem_sec = (rem_min - static_cast<double>(m)) * 60.0;
244 // Encode seconds with microsecond precision.
245 constexpr uint32_t SEC_DENOM = 1000000;
246 uint32_t s_num = static_cast<uint32_t>(std::round(rem_sec * static_cast<double>(SEC_DENOM)));
247 return {{d, 1u}, {m, 1u}, {s_num, SEC_DENOM}};
248 }
249
250 // Encode shutter speed as a rational. Prefer 1/N for sub-second exposures.
251 std::pair<uint32_t, uint32_t> shutterSpeedRational(float seconds) {
252 if (seconds <= 0.f) {
253 return {0u, 1u};
254 }
255 if (seconds < 1.0f) {
256 // 1/N form
257 double n = std::round(1.0 / static_cast<double>(seconds));
258 if (n < 1.0) n = 1.0;
259 if (n > 4294967294.0) n = 4294967294.0;
260 return {1u, static_cast<uint32_t>(n)};
261 }
262 // Whole / fractional second form with 1000 denominator.
263 return {static_cast<uint32_t>(std::round(seconds * 1000.0)), 1000u};
264 }
265
266 // ---- IFD assembly ----
267 //
268 // Emit an IFD at base_offset within the TIFF stream. Returns the byte offset of the next
269 // free location (where the next IFD or trailing data can be placed).
270 //
271 // `tiff_buf` is the buffer the IFD is being written into; the TIFF header occupies bytes
272 // 0..7 of this buffer.
273 //
274 // `entries` is consumed: pointer placeholders are patched with their target offsets
275 // (provided via subifd_offsets keyed by pointer_target_index).
276 size_t emitIFD(std::vector<unsigned char> &tiff_buf,
277 size_t base_offset,
278 std::vector<IFDEntry> &entries,
279 uint32_t next_ifd_offset,
280 const std::array<uint32_t, 2> &subifd_offsets) {
281 // IFD layout: 2-byte count, N*12-byte entries, 4-byte next-IFD pointer, then the
282 // trailing data blob for entries whose value didn't fit inline.
283 const size_t n = entries.size();
284 const size_t record_bytes = 2 + n * 12 + 4;
285 const size_t data_blob_offset = base_offset + record_bytes;
286
287 // Pad buffer to base_offset if needed (the caller has typically already done this).
288 if (tiff_buf.size() < base_offset) {
289 tiff_buf.resize(base_offset, 0);
290 }
291
292 // Compute external-data offsets first.
293 std::vector<uint32_t> entry_external_offset(n, 0);
294 size_t running = data_blob_offset;
295 for (size_t i = 0; i < n; ++i) {
296 if (!entries[i].inline_only) {
297 // EXIF/TIFF requires WORD alignment of offsets per the spec; we use 2-byte
298 // alignment for safety with RATIONAL/SHORT arrays.
299 if (running % 2 != 0) {
300 running += 1;
301 }
302 entry_external_offset[i] = static_cast<uint32_t>(running);
303 running += entries[i].external_data.size();
304 }
305 }
306
307 // Now write the IFD record itself.
308 const size_t pre_size = tiff_buf.size();
309 (void) pre_size;
310 // Resize buffer to hold the record + data blob.
311 tiff_buf.resize(running, 0);
312
313 size_t cursor = base_offset;
314 // Entry count
315 tiff_buf[cursor++] = static_cast<unsigned char>(n & 0xFF);
316 tiff_buf[cursor++] = static_cast<unsigned char>((n >> 8) & 0xFF);
317
318 // Entries
319 for (size_t i = 0; i < n; ++i) {
320 const IFDEntry &e = entries[i];
321 // tag (2)
322 tiff_buf[cursor + 0] = static_cast<unsigned char>(e.tag & 0xFF);
323 tiff_buf[cursor + 1] = static_cast<unsigned char>((e.tag >> 8) & 0xFF);
324 // type (2)
325 tiff_buf[cursor + 2] = static_cast<unsigned char>(e.type & 0xFF);
326 tiff_buf[cursor + 3] = static_cast<unsigned char>((e.type >> 8) & 0xFF);
327 // count (4)
328 tiff_buf[cursor + 4] = static_cast<unsigned char>(e.count & 0xFF);
329 tiff_buf[cursor + 5] = static_cast<unsigned char>((e.count >> 8) & 0xFF);
330 tiff_buf[cursor + 6] = static_cast<unsigned char>((e.count >> 16) & 0xFF);
331 tiff_buf[cursor + 7] = static_cast<unsigned char>((e.count >> 24) & 0xFF);
332
333 // value / offset (4)
334 if (e.is_pointer_placeholder) {
335 const uint32_t target = (e.pointer_target_index >= 0)
336 ? subifd_offsets[static_cast<size_t>(e.pointer_target_index)]
337 : 0u;
338 patchU32(tiff_buf, cursor + 8, target);
339 } else if (e.inline_only) {
340 tiff_buf[cursor + 8] = e.inline_value[0];
341 tiff_buf[cursor + 9] = e.inline_value[1];
342 tiff_buf[cursor + 10] = e.inline_value[2];
343 tiff_buf[cursor + 11] = e.inline_value[3];
344 } else {
345 patchU32(tiff_buf, cursor + 8, entry_external_offset[i]);
346 }
347 cursor += 12;
348 }
349
350 // Next IFD offset
351 patchU32(tiff_buf, cursor, next_ifd_offset);
352 cursor += 4;
353
354 // Trailing data blob
355 for (size_t i = 0; i < n; ++i) {
356 if (!entries[i].inline_only) {
357 const uint32_t off = entry_external_offset[i];
358 std::memcpy(&tiff_buf[off], entries[i].external_data.data(), entries[i].external_data.size());
359 }
360 }
361
362 return running;
363 }
364
365 // ---- Build the EXIF SubIFD entries from m ----
366 std::vector<IFDEntry> buildExifSubIFD(const ImageEXIFData &m) {
367 std::vector<IFDEntry> e;
368 if (m.exposure_time_s > 0.f) {
369 auto r = shutterSpeedRational(m.exposure_time_s);
370 e.push_back(makeRational(0x829A, r.first, r.second));
371 }
372 if (m.f_number > 0.f) {
373 // FNumber: RATIONAL, 1/100 precision.
374 uint32_t num = static_cast<uint32_t>(std::round(static_cast<double>(m.f_number) * 100.0));
375 e.push_back(makeRational(0x829D, num, 100u));
376 }
377 if (!m.datetime_original.empty()) {
378 e.push_back(makeAscii(0x9003, m.datetime_original));
379 }
380 if (!m.datetime_digitized.empty()) {
381 e.push_back(makeAscii(0x9004, m.datetime_digitized));
382 }
383 if (!m.offset_time.empty()) {
384 // OffsetTime / OffsetTimeOriginal / OffsetTimeDigitized share the same value here.
385 e.push_back(makeAscii(0x9010, m.offset_time));
386 e.push_back(makeAscii(0x9011, m.offset_time));
387 e.push_back(makeAscii(0x9012, m.offset_time));
388 }
389 if (m.iso > 0) {
390 e.push_back(makeShort(0x8827, static_cast<uint16_t>(std::min<unsigned int>(m.iso, 65535u))));
391 }
392 if (m.has_exposure_bias) {
393 // ExposureBiasValue: SRATIONAL, 1/100 EV precision.
394 int32_t num = static_cast<int32_t>(std::round(static_cast<double>(m.exposure_bias_ev) * 100.0));
395 e.push_back(makeSRational(0x9204, num, 100));
396 }
397 if (m.max_aperture_value_apex > 0.f) {
398 // MaxApertureValue: RATIONAL (APEX units), 1/100 precision.
399 uint32_t num = static_cast<uint32_t>(std::round(static_cast<double>(m.max_aperture_value_apex) * 100.0));
400 e.push_back(makeRational(0x9205, num, 100u));
401 }
402 if (m.subject_distance_m > 0.f) {
403 // SubjectDistance: RATIONAL, mm precision.
404 uint32_t num = static_cast<uint32_t>(std::round(static_cast<double>(m.subject_distance_m) * 1000.0));
405 e.push_back(makeRational(0x9206, num, 1000u));
406 }
407 if (m.focal_length_mm > 0.f) {
408 // 1/1000 mm precision.
409 uint32_t num = static_cast<uint32_t>(std::round(static_cast<double>(m.focal_length_mm) * 1000.0));
410 e.push_back(makeRational(0x920A, num, 1000u));
411 }
412 if (m.pixel_x_dimension > 0) {
413 e.push_back(makeLong(0xA002, m.pixel_x_dimension));
414 }
415 if (m.pixel_y_dimension > 0) {
416 e.push_back(makeLong(0xA003, m.pixel_y_dimension));
417 }
418 if (m.focal_plane_x_resolution > 0.f) {
419 uint32_t num = static_cast<uint32_t>(std::round(static_cast<double>(m.focal_plane_x_resolution) * 1000.0));
420 e.push_back(makeRational(0xA20E, num, 1000u));
421 }
422 if (m.focal_plane_y_resolution > 0.f) {
423 uint32_t num = static_cast<uint32_t>(std::round(static_cast<double>(m.focal_plane_y_resolution) * 1000.0));
424 e.push_back(makeRational(0xA20F, num, 1000u));
425 }
426 if (m.focal_plane_x_resolution > 0.f || m.focal_plane_y_resolution > 0.f) {
427 e.push_back(makeShort(0xA210, static_cast<uint16_t>(m.focal_plane_resolution_unit)));
428 }
429 if (m.exposure_mode != (unsigned int) -1) {
430 e.push_back(makeShort(0xA402, static_cast<uint16_t>(m.exposure_mode)));
431 }
432 if (m.white_balance != (unsigned int) -1) {
433 e.push_back(makeShort(0xA403, static_cast<uint16_t>(m.white_balance)));
434 }
435 if (m.digital_zoom_ratio > 0.f) {
436 // DigitalZoomRatio: RATIONAL, 1/100 precision.
437 uint32_t num = static_cast<uint32_t>(std::round(static_cast<double>(m.digital_zoom_ratio) * 100.0));
438 e.push_back(makeRational(0xA404, num, 100u));
439 }
440 if (m.focal_length_in_35mm > 0) {
441 e.push_back(makeShort(0xA405, static_cast<uint16_t>(std::min<unsigned int>(m.focal_length_in_35mm, 65535u))));
442 }
443 if (!m.lens_make.empty()) {
444 e.push_back(makeAscii(0xA433, m.lens_make));
445 }
446 if (!m.lens_model.empty()) {
447 e.push_back(makeAscii(0xA434, m.lens_model));
448 }
449 if (!m.lens_specification.empty()) {
450 // LensSpecification (0xA432) is officially 4 RATIONALs (min focal, max focal, min F, max F).
451 // We don't have those decomposed; emit as ASCII LensSpecificationDescription instead.
452 e.push_back(makeAscii(0xA435, m.lens_specification));
453 }
454
455 // ExifVersion (UNDEFINED 4) = "0232"
456 e.push_back(makeUndefined(0x9000, "0232"));
457
458 // IFD entries must be sorted by tag.
459 std::sort(e.begin(), e.end(), [](const IFDEntry &a, const IFDEntry &b) { return a.tag < b.tag; });
460 return e;
461 }
462
463 // ---- Build GPS IFD entries from m ----
464 std::vector<IFDEntry> buildGPSIFD(const ImageEXIFData &m) {
465 std::vector<IFDEntry> e;
466
467 // GPSVersionID (BYTE x4) = 2,3,0,0
468 {
469 IFDEntry v;
470 v.tag = 0x0000;
471 v.type = TYPE_BYTE;
472 v.count = 4;
473 std::vector<unsigned char> raw = {2, 3, 0, 0};
474 finalizeEntry(v, raw);
475 e.push_back(v);
476 }
477 // GPSLatitudeRef
478 e.push_back(makeAscii(0x0001, (m.latitude_deg >= 0.0) ? "N" : "S"));
479 // GPSLatitude
480 e.push_back(makeRationalArray(0x0002, decimalDegToDMSRationals(m.latitude_deg)));
481 // GPSLongitudeRef
482 e.push_back(makeAscii(0x0003, (m.longitude_deg >= 0.0) ? "E" : "W"));
483 // GPSLongitude
484 e.push_back(makeRationalArray(0x0004, decimalDegToDMSRationals(m.longitude_deg)));
485 // GPSAltitudeRef (BYTE: 0 = above sea level, 1 = below)
486 {
487 IFDEntry v;
488 v.tag = 0x0005;
489 v.type = TYPE_BYTE;
490 v.count = 1;
491 std::vector<unsigned char> raw(1);
492 raw[0] = (m.altitude_m >= 0.0) ? 0 : 1;
493 finalizeEntry(v, raw);
494 e.push_back(v);
495 }
496 // GPSAltitude (always non-negative; sign carried by AltitudeRef)
497 {
498 const double abs_alt = std::fabs(m.altitude_m);
499 const uint32_t num = static_cast<uint32_t>(std::round(abs_alt * 1000.0));
500 e.push_back(makeRational(0x0006, num, 1000u));
501 }
502 // GPSTimeStamp (RATIONAL x3: H, M, S)
503 if (!m.gps_timestamp_hms.empty()) {
504 int hh = 0, mm = 0, ss = 0;
505 if (std::sscanf(m.gps_timestamp_hms.c_str(), "%d:%d:%d", &hh, &mm, &ss) == 3) {
506 std::vector<std::pair<uint32_t, uint32_t>> ts = {
507 {static_cast<uint32_t>(hh), 1u},
508 {static_cast<uint32_t>(mm), 1u},
509 {static_cast<uint32_t>(ss), 1u}};
510 e.push_back(makeRationalArray(0x0007, ts));
511 }
512 }
513 // GPSImgDirectionRef = 'T' (true north)
514 e.push_back(makeAscii(0x0010, "T"));
515 // GPSImgDirection (RATIONAL, 1/100 deg precision)
516 {
517 double dir = m.img_direction_deg;
518 if (dir < 0.0) dir += 360.0;
519 if (dir >= 360.0) dir -= 360.0;
520 uint32_t num = static_cast<uint32_t>(std::round(dir * 100.0));
521 e.push_back(makeRational(0x0011, num, 100u));
522 }
523 // GPSDateStamp (ASCII, 11 bytes: "YYYY:MM:DD\0")
524 if (!m.gps_datestamp.empty()) {
525 e.push_back(makeAscii(0x001D, m.gps_datestamp));
526 }
527
528 std::sort(e.begin(), e.end(), [](const IFDEntry &a, const IFDEntry &b) { return a.tag < b.tag; });
529 return e;
530 }
531
532 } // namespace
533
534 std::vector<unsigned char> buildEXIFAppSegment(const ImageEXIFData &m) {
535 // Payload layout:
536 // "Exif\0\0" (6 bytes)
537 // TIFF header: "II" 0x002A 0x00000008 (8 bytes)
538 // IFD0 (record + data blob)
539 // ExifSubIFD (record + data blob) -- pointed to by IFD0 tag 0x8769
540 // GPS IFD (record + data blob) -- pointed to by IFD0 tag 0x8825 (if m.gps_valid)
541
542 // ---- Build IFD0 entries ----
543 std::vector<IFDEntry> ifd0;
544 if (!m.make.empty()) ifd0.push_back(makeAscii(0x010F, m.make));
545 if (!m.model.empty()) ifd0.push_back(makeAscii(0x0110, m.model));
546 ifd0.push_back(makeShort(0x0112, static_cast<uint16_t>(m.orientation)));
547 if (!m.software.empty()) ifd0.push_back(makeAscii(0x0131, m.software));
548 if (!m.datetime.empty()) ifd0.push_back(makeAscii(0x0132, m.datetime));
549
550 // ExifSubIFD pointer (LONG, tag 0x8769) -- patched later.
551 IFDEntry exif_ptr;
552 exif_ptr.tag = 0x8769;
553 exif_ptr.type = TYPE_LONG;
554 exif_ptr.count = 1;
555 exif_ptr.is_pointer_placeholder = true;
556 exif_ptr.pointer_target_index = 0;
557 exif_ptr.inline_only = true;
558 ifd0.push_back(exif_ptr);
559
560 // GPS IFD pointer (LONG, tag 0x8825) -- patched later, only if GPS valid.
561 if (m.gps_valid) {
562 IFDEntry gps_ptr;
563 gps_ptr.tag = 0x8825;
564 gps_ptr.type = TYPE_LONG;
565 gps_ptr.count = 1;
566 gps_ptr.is_pointer_placeholder = true;
567 gps_ptr.pointer_target_index = 1;
568 gps_ptr.inline_only = true;
569 ifd0.push_back(gps_ptr);
570 }
571
572 std::sort(ifd0.begin(), ifd0.end(), [](const IFDEntry &a, const IFDEntry &b) { return a.tag < b.tag; });
573
574 std::vector<IFDEntry> exif_sub = buildExifSubIFD(m);
575 std::vector<IFDEntry> gps_ifd;
576 if (m.gps_valid) {
577 gps_ifd = buildGPSIFD(m);
578 }
579
580 // ---- Two-pass layout: compute offsets, then emit ----
581 //
582 // TIFF stream begins right after "Exif\0\0". Inside the TIFF stream:
583 // bytes 0..7 : TIFF header (II 002A 00000008)
584 // byte 8 : IFD0 record start
585 // then : IFD0 data blob
586 // then : ExifSubIFD record + data blob
587 // then : GPS IFD record + data blob
588
589 std::vector<unsigned char> tiff;
590 tiff.reserve(1024);
591 // TIFF header
592 tiff.push_back('I');
593 tiff.push_back('I');
594 putU16(tiff, 0x002A);
595 putU32(tiff, 8); // offset to IFD0
596
597 // Compute IFD0 size to know where ExifSubIFD will start.
598 auto ifdRecordSize = [](const std::vector<IFDEntry> &v) {
599 size_t s = 2 + v.size() * 12 + 4;
600 for (const auto &e: v) {
601 if (!e.inline_only) {
602 if (s % 2 != 0) s += 1;
603 s += e.external_data.size();
604 }
605 }
606 return s;
607 };
608
609 const size_t ifd0_base = 8;
610 const size_t ifd0_size = ifdRecordSize(ifd0);
611 const size_t exif_base = ifd0_base + ifd0_size;
612 const size_t exif_size = ifdRecordSize(exif_sub);
613 const size_t gps_base = exif_base + exif_size;
614
615 std::array<uint32_t, 2> subifd_offsets = {static_cast<uint32_t>(exif_base),
616 m.gps_valid ? static_cast<uint32_t>(gps_base) : 0u};
617
618 // Emit IFD0
619 emitIFD(tiff, ifd0_base, ifd0, /*next_ifd=*/0u, subifd_offsets);
620 // Emit ExifSubIFD
621 emitIFD(tiff, exif_base, exif_sub, /*next_ifd=*/0u, subifd_offsets);
622 // Emit GPS IFD
623 if (m.gps_valid) {
624 emitIFD(tiff, gps_base, gps_ifd, /*next_ifd=*/0u, subifd_offsets);
625 }
626
627 // Compose final payload: "Exif\0\0" + tiff
628 std::vector<unsigned char> payload = {'E', 'x', 'i', 'f', 0, 0};
629 payload.reserve(6 + tiff.size());
630 payload.insert(payload.end(), tiff.begin(), tiff.end());
631
632 if (payload.size() > APP1_MAX_PAYLOAD) {
633 helios_runtime_error("ERROR (buildEXIFAppSegment): EXIF payload (" + std::to_string(payload.size()) +
634 " bytes) exceeds JPEG APP1 size limit (" + std::to_string(APP1_MAX_PAYLOAD) + ").");
635 }
636 return payload;
637 }
638
639 std::vector<unsigned char> buildXMPAppSegment(const ImageEXIFData &m) {
640 // RDF/XML packet using pugixml.
641 pugi::xml_document doc;
642
643 pugi::xml_node xpacket_begin = doc.append_child(pugi::node_pi);
644 xpacket_begin.set_name("xpacket");
645 xpacket_begin.set_value("begin=\"\xEF\xBB\xBF\" id=\"W5M0MpCehiHzreSzNTczkc9d\"");
646
647 pugi::xml_node x_xmpmeta = doc.append_child("x:xmpmeta");
648 x_xmpmeta.append_attribute("xmlns:x") = "adobe:ns:meta/";
649 x_xmpmeta.append_attribute("x:xmptk") = "Helios";
650
651 pugi::xml_node rdf = x_xmpmeta.append_child("rdf:RDF");
652 rdf.append_attribute("xmlns:rdf") = "http://www.w3.org/1999/02/22-rdf-syntax-ns#";
653
654 pugi::xml_node desc = rdf.append_child("rdf:Description");
655 desc.append_attribute("rdf:about") = "";
656 desc.append_attribute("xmlns:tiff") = "http://ns.adobe.com/tiff/1.0/";
657 desc.append_attribute("xmlns:xmp") = "http://ns.adobe.com/xap/1.0/";
658 desc.append_attribute("xmlns:Camera") = "http://pix4d.com/camera/1.0";
659
660 if (!m.make.empty()) desc.append_attribute("tiff:Make") = m.make.c_str();
661 if (!m.model.empty()) desc.append_attribute("tiff:Model") = m.model.c_str();
662 if (!m.software.empty()) desc.append_attribute("xmp:CreatorTool") = m.software.c_str();
663
664 auto fmt = [](double v) {
665 std::ostringstream oss;
666 oss.precision(6);
667 oss << std::fixed << v;
668 return oss.str();
669 };
670
671 if (m.xmp_valid) {
672 desc.append_attribute("Camera:Yaw") = fmt(m.yaw_deg).c_str();
673 desc.append_attribute("Camera:Pitch") = fmt(m.pitch_deg).c_str();
674 desc.append_attribute("Camera:Roll") = fmt(m.roll_deg).c_str();
675 }
676
677 pugi::xml_node xpacket_end = doc.append_child(pugi::node_pi);
678 xpacket_end.set_name("xpacket");
679 xpacket_end.set_value("end=\"w\"");
680
681 std::ostringstream xml_oss;
682 doc.save(xml_oss, "", pugi::format_raw | pugi::format_no_declaration);
683 const std::string xml = xml_oss.str();
684
685 // Assemble payload: "http://ns.adobe.com/xap/1.0/\0" + RDF/XML
686 static const char NS_ID[] = "http://ns.adobe.com/xap/1.0/";
687 const size_t ns_len = std::strlen(NS_ID) + 1; // include NUL
688 std::vector<unsigned char> payload;
689 payload.reserve(ns_len + xml.size());
690 payload.insert(payload.end(), NS_ID, NS_ID + ns_len);
691 payload.insert(payload.end(), xml.begin(), xml.end());
692
693 if (payload.size() > APP1_MAX_PAYLOAD) {
694 helios_runtime_error("ERROR (buildXMPAppSegment): XMP payload (" + std::to_string(payload.size()) +
695 " bytes) exceeds JPEG APP1 size limit (" + std::to_string(APP1_MAX_PAYLOAD) + ").");
696 }
697 return payload;
698 }
699
700} // namespace helios::detail