/* TinyEXIF.cpp -- A simple ISO C++ library to parse basic EXIF and XMP information from a JPEG file. Copyright (c) 2015-2025 Seacave cdc.seacave@gmail.com MIT License */ #include "TinyEXIF.h" #include #include #include #include #include #include #include #include #ifndef TINYEXIF_NO_XMP_SUPPORT #include #endif // TINYEXIF_NO_XMP_SUPPORT #ifdef _MSC_VER namespace { int strcasecmp(const char* a, const char* b) { return _stricmp(a, b); } } #else #include #endif namespace Tools { // search string inside a string, case sensitive static const char* strrnstr(const char* haystack, const char* needle, size_t len) { const size_t needle_len(strlen(needle)); if (0 == needle_len) return haystack; if (len <= needle_len) return NULL; for (size_t i=len-needle_len; i-- > 0; ) { if (haystack[0] == needle[0] && 0 == strncmp(haystack, needle, needle_len)) return haystack; haystack++; } return NULL; } // split an input string with a delimiter and fill a string vector static void strSplit(const std::string& str, char delim, std::vector& values) { values.clear(); std::string::size_type start(0), end(0); while (end != std::string::npos) { end = str.find(delim, start); values.emplace_back(str.substr(start, end-start)); start = end + 1; } } // make sure the given degrees value is between -180 and 180 static double NormD180(double d) { return (d = fmod(d+180.0, 360.0)) < 0 ? d+180.0 : d-180.0; } } // namespace Tools namespace TinyEXIF { enum JPEG_MARKERS { JM_START = 0xFF, JM_SOF0 = 0xC0, JM_SOF1 = 0xC1, JM_SOF2 = 0xC2, JM_SOF3 = 0xC3, JM_DHT = 0xC4, JM_SOF5 = 0xC5, JM_SOF6 = 0xC6, JM_SOF7 = 0xC7, JM_JPG = 0xC8, JM_SOF9 = 0xC9, JM_SOF10 = 0xCA, JM_SOF11 = 0xCB, JM_DAC = 0xCC, JM_SOF13 = 0xCD, JM_SOF14 = 0xCE, JM_SOF15 = 0xCF, JM_RST0 = 0xD0, JM_RST1 = 0xD1, JM_RST2 = 0xD2, JM_RST3 = 0xD3, JM_RST4 = 0xD4, JM_RST5 = 0xD5, JM_RST6 = 0xD6, JM_RST7 = 0xD7, JM_SOI = 0xD8, JM_EOI = 0xD9, JM_SOS = 0xDA, JM_DQT = 0xDB, JM_DNL = 0xDC, JM_DRI = 0xDD, JM_DHP = 0xDE, JM_EXP = 0xDF, JM_APP0 = 0xE0, JM_APP1 = 0xE1, // EXIF and XMP JM_APP2 = 0xE2, JM_APP3 = 0xE3, JM_APP4 = 0xE4, JM_APP5 = 0xE5, JM_APP6 = 0xE6, JM_APP7 = 0xE7, JM_APP8 = 0xE8, JM_APP9 = 0xE9, JM_APP10 = 0xEA, JM_APP11 = 0xEB, JM_APP12 = 0xEC, JM_APP13 = 0xED, // IPTC JM_APP14 = 0xEE, JM_APP15 = 0xEF, JM_JPG0 = 0xF0, JM_JPG1 = 0xF1, JM_JPG2 = 0xF2, JM_JPG3 = 0xF3, JM_JPG4 = 0xF4, JM_JPG5 = 0xF5, JM_JPG6 = 0xF6, JM_JPG7 = 0xF7, JM_JPG8 = 0xF8, JM_JPG9 = 0xF9, JM_JPG10 = 0xFA, JM_JPG11 = 0xFB, JM_JPG12 = 0xFC, JM_JPG13 = 0xFD, JM_COM = 0xFE }; // Parser helper class EntryParser { private: const uint8_t* buf; const unsigned len; const unsigned tiff_header_start; const bool alignIntel; // byte alignment (defined in EXIF header) unsigned offs; // current offset into buffer uint16_t tag, format; uint32_t length; public: EntryParser(const uint8_t* _buf, unsigned _len, unsigned _tiff_header_start, bool _alignIntel) : buf(_buf), len(_len), tiff_header_start(_tiff_header_start), alignIntel(_alignIntel), offs(0) {} void Init(unsigned _offs) { offs = _offs - 12; } void ParseTag() { offs += 12; tag = parse16(buf + offs, alignIntel); format = parse16(buf + offs + 2, alignIntel); length = parse32(buf + offs + 4, alignIntel); } const uint8_t* GetBuffer() const { return buf; } unsigned GetOffset() const { return offs; } bool IsIntelAligned() const { return alignIntel; } uint16_t GetTag() const { return tag; } uint32_t GetLength() const { return length; } uint32_t GetData() const { return parse32(buf + offs + 8, alignIntel); } uint32_t GetSubIFD() const { return tiff_header_start + GetData(); } bool IsShort() const { return format == 3; } bool IsLong() const { return format == 4; } bool IsRational() const { return format == 5 || format == 10; } bool IsSRational() const { return format == 10; } bool IsFloat() const { return format == 11; } bool IsUndefined() const { return format == 7; } std::string FetchString() const { return parseString(buf, length, GetData(), tiff_header_start, len, alignIntel); } bool Fetch(std::string& val) const { if (format != 2 || length == 0) return false; val = FetchString(); return true; } bool Fetch(uint8_t& val) const { if ((format != 1 && format != 2 && format != 6) || length == 0) return false; val = parse8(buf + offs + 8); return true; } bool Fetch(uint16_t& val) const { if (!IsShort() || length == 0) return false; val = parse16(buf + offs + 8, alignIntel); return true; } bool Fetch(uint16_t& val, uint32_t idx) const { if (!IsShort() || length <= idx) return false; const uint32_t offset = GetSubIFD() + idx*2; if (offset + 2 > len) return false; val = parse16(buf + offset, alignIntel); return true; } bool Fetch(uint32_t& val) const { if (!IsLong() || length == 0) return false; val = parse32(buf + offs + 8, alignIntel); return true; } bool Fetch(float& val) const { if (!IsFloat() || length == 0) return false; val = parseFloat(buf + offs + 8, alignIntel); return true; } bool Fetch(double& val) const { if (!IsRational() || length == 0) return false; val = parseRational(buf + GetSubIFD(), alignIntel, IsSRational()); return true; } bool Fetch(double& val, uint32_t idx) const { if (!IsRational() || length <= idx) return false; const uint32_t offset = GetSubIFD() + idx*8; if (offset + 8 > len) return false; val = parseRational(buf + offset, alignIntel, IsSRational()); return true; } bool FetchFloat(double& val) const { float _val; if (!Fetch(_val)) return false; val = _val; return true; } public: static uint8_t parse8(const uint8_t* buf) { return buf[0]; } static uint16_t parse16(const uint8_t* buf, bool intel) { if (intel) return ((uint16_t)buf[1]<<8) | buf[0]; return ((uint16_t)buf[0]<<8) | buf[1]; } static uint32_t parse32(const uint8_t* buf, bool intel) { if (intel) return ((uint32_t)buf[3]<<24) | ((uint32_t)buf[2]<<16) | ((uint32_t)buf[1]<<8) | buf[0]; return ((uint32_t)buf[0]<<24) | ((uint32_t)buf[1]<<16) | ((uint32_t)buf[2]<<8) | buf[3]; } static float parseFloat(const uint8_t* buf, bool intel) { union { uint32_t i; float f; } i2f; i2f.i = parse32(buf, intel); return i2f.f; } static double parseRational(const uint8_t* buf, bool intel, bool isSigned) { const uint32_t denominator = parse32(buf+4, intel); if (denominator == 0) return 0.0; const uint32_t numerator = parse32(buf, intel); return isSigned ? (double)(int32_t)numerator/(double)(int32_t)denominator : (double)numerator/(double)denominator; } static std::string parseString(const uint8_t* buf, unsigned num_components, unsigned data, unsigned base, unsigned len, bool intel) { std::string value; if (num_components <= 4) { value.resize(num_components); char j = intel ? 0 : 24; char j_m = intel ? -8 : 8; for (unsigned i=0; i> j) & 0xff; if (value[num_components-1] == '\0') value.resize(num_components-1); } else if ((uint64_t)base+data+num_components <= (uint64_t)len) { const char* const sz((const char*)buf+base+data); unsigned num(0); while (num < num_components && sz[num] != '\0') ++num; while (num && sz[num-1] == ' ') --num; value.assign(sz, num); } return value; } }; // Constructors EXIFInfo::EXIFInfo() : Fields(FIELD_NA) { } EXIFInfo::EXIFInfo(EXIFStream& stream) { parseFrom(stream); } EXIFInfo::EXIFInfo(std::istream& stream) { parseFrom(stream); } EXIFInfo::EXIFInfo(const uint8_t* data, unsigned length) { parseFrom(data, length); } // Parse tag as Image IFD void EXIFInfo::parseIFDImage(EntryParser& parser, unsigned& exif_sub_ifd_offset, unsigned& gps_sub_ifd_offset) { switch (parser.GetTag()) { case 0x0102: // Bits per sample parser.Fetch(BitsPerSample); break; case 0x010e: // Image description parser.Fetch(ImageDescription); break; case 0x010f: // Camera maker parser.Fetch(Make); break; case 0x0110: // Camera model parser.Fetch(Model); break; case 0x0112: // Orientation of image parser.Fetch(Orientation); break; case 0x011a: // XResolution parser.Fetch(XResolution); break; case 0x011b: // YResolution parser.Fetch(YResolution); break; case 0x0128: // Resolution Unit parser.Fetch(ResolutionUnit); break; case 0x0131: // Software used for image parser.Fetch(Software); break; case 0x0132: // EXIF/TIFF date/time of image modification parser.Fetch(DateTime); break; case 0x1001: // Original Image width if (!parser.Fetch(RelatedImageWidth)) { uint16_t _RelatedImageWidth; if (parser.Fetch(_RelatedImageWidth)) RelatedImageWidth = _RelatedImageWidth; } break; case 0x1002: // Original Image height if (!parser.Fetch(RelatedImageHeight)) { uint16_t _RelatedImageHeight; if (parser.Fetch(_RelatedImageHeight)) RelatedImageHeight = _RelatedImageHeight; } break; case 0x8298: // Copyright information parser.Fetch(Copyright); break; case 0x8769: // EXIF SubIFD offset exif_sub_ifd_offset = parser.GetSubIFD(); break; case 0x8825: // GPS IFS offset gps_sub_ifd_offset = parser.GetSubIFD(); break; default: // Try to parse as EXIF tag, as some images store them in here parseIFDExif(parser); break; } } // Parse tag as Exif IFD void EXIFInfo::parseIFDExif(EntryParser& parser) { switch (parser.GetTag()) { case 0x02bc: #ifndef TINYEXIF_NO_XMP_SUPPORT // XMP Metadata (Adobe technote 9-14-02) if (parser.IsUndefined()) { const std::string strXML(parser.FetchString()); parseFromXMPSegmentXML(strXML.c_str(), (unsigned)strXML.length()); } #endif // TINYEXIF_NO_XMP_SUPPORT break; case 0x829a: // Exposure time in seconds parser.Fetch(ExposureTime); break; case 0x829d: // FNumber parser.Fetch(FNumber); break; case 0x8822: // Exposure Program parser.Fetch(ExposureProgram); break; case 0x8827: // ISO Speed Rating parser.Fetch(ISOSpeedRatings); break; case 0x9003: // Original date and time parser.Fetch(DateTimeOriginal); break; case 0x9004: // Digitization date and time parser.Fetch(DateTimeDigitized); break; case 0x9201: // Shutter speed value parser.Fetch(ShutterSpeedValue); ShutterSpeedValue = 1.0/exp(ShutterSpeedValue*log(2)); break; case 0x9202: // Aperture value parser.Fetch(ApertureValue); ApertureValue = exp(ApertureValue*log(2)*0.5); break; case 0x9203: // Brightness value parser.Fetch(BrightnessValue); break; case 0x9204: // Exposure bias value parser.Fetch(ExposureBiasValue); break; case 0x9206: // Subject distance parser.Fetch(SubjectDistance); break; case 0x9207: // Metering mode parser.Fetch(MeteringMode); break; case 0x9208: // Light source parser.Fetch(LightSource); break; case 0x9209: // Flash info parser.Fetch(Flash); break; case 0x920a: // Focal length parser.Fetch(FocalLength); break; case 0x9214: // Subject area if (parser.IsShort() && parser.GetLength() > 1) { SubjectArea.resize(parser.GetLength()); for (uint32_t i=0; i parser.GetLength()) return; parser.Init(off+2); parser.ParseTag(); --num_entries; std::string maker; if (parser.GetTag() == 1 && parser.Fetch(maker)) { if (0 == strcasecmp(maker.c_str(), "DJI")) { while (--num_entries >= 0) { parser.ParseTag(); switch (parser.GetTag()) { case 3: // SpeedX parser.FetchFloat(GeoLocation.SpeedX); break; case 4: // SpeedY parser.FetchFloat(GeoLocation.SpeedY); break; case 5: // SpeedZ parser.FetchFloat(GeoLocation.SpeedZ); break; case 9: // Camera Pitch parser.FetchFloat(GeoLocation.PitchDegree); break; case 10: // Camera Yaw parser.FetchFloat(GeoLocation.YawDegree); break; case 11: // Camera Roll parser.FetchFloat(GeoLocation.RollDegree); break; } } } } parser.Init(startOff+12); } // Parse tag as GPS IFD void EXIFInfo::parseIFDGPS(EntryParser& parser) { switch (parser.GetTag()) { case 1: // GPS north or south parser.Fetch(GeoLocation.LatComponents.direction); break; case 2: // GPS latitude if (parser.IsRational() && parser.GetLength() == 3) { parser.Fetch(GeoLocation.LatComponents.degrees, 0); parser.Fetch(GeoLocation.LatComponents.minutes, 1); parser.Fetch(GeoLocation.LatComponents.seconds, 2); } break; case 3: // GPS east or west parser.Fetch(GeoLocation.LonComponents.direction); break; case 4: // GPS longitude if (parser.IsRational() && parser.GetLength() == 3) { parser.Fetch(GeoLocation.LonComponents.degrees, 0); parser.Fetch(GeoLocation.LonComponents.minutes, 1); parser.Fetch(GeoLocation.LonComponents.seconds, 2); } break; case 5: // GPS altitude reference (below or above sea level) parser.Fetch((uint8_t&)GeoLocation.AltitudeRef); break; case 6: // GPS altitude parser.Fetch(GeoLocation.Altitude); break; case 7: // GPS timestamp if (parser.IsRational() && parser.GetLength() == 3) { double h,m,s; parser.Fetch(h, 0); parser.Fetch(m, 1); parser.Fetch(s, 2); char buffer[256]; snprintf(buffer, 256, "%g %g %g", h, m, s); GeoLocation.GPSTimeStamp = buffer; } break; case 11: // Indicates the GPS DOP (data degree of precision) parser.Fetch(GeoLocation.GPSDOP); break; case 18: // GPS geodetic survey data parser.Fetch(GeoLocation.GPSMapDatum); break; case 29: // GPS date-stamp parser.Fetch(GeoLocation.GPSDateStamp); break; case 30: // GPS differential indicates whether differential correction is applied to the GPS receiver parser.Fetch(GeoLocation.GPSDifferential); break; } } // // Locates the JM_APP1 segment and parses it using // parseFromEXIFSegment() or parseFromXMPSegment() // int EXIFInfo::parseFrom(EXIFStream& stream) { clear(); if (!stream.IsValid()) return PARSE_INVALID_JPEG; // Sanity check: all JPEG files start with 0xFFD8 and end with 0xFFD9 // This check also ensures that the user has supplied a correct value for len. const uint8_t* buf(stream.GetBuffer(2)); if (buf == NULL || buf[0] != JM_START || buf[1] != JM_SOI) return PARSE_INVALID_JPEG; // Scan for JM_APP1 header (bytes 0xFF 0xE1) and parse its length. // Exit if both EXIF and XMP sections were parsed. struct APP1S { uint32_t& val; inline APP1S(uint32_t& v) : val(v) {} inline operator uint32_t () const { return val; } inline operator uint32_t& () { return val; } inline int operator () (int code=PARSE_ABSENT_DATA) const { return val&FIELD_ALL ? (int)PARSE_SUCCESS : code; } } app1s(Fields); while ((buf=stream.GetBuffer(2)) != NULL) { // find next marker; // in cases of markers appended after the compressed data, // optional JM_START fill bytes may precede the marker if (*buf++ != JM_START) break; uint8_t marker; while ((marker=buf[0]) == JM_START && (buf=stream.GetBuffer(1)) != NULL); // select marker uint16_t sectionLength; switch (marker) { case 0x00: case 0x01: case JM_START: case JM_RST0: case JM_RST1: case JM_RST2: case JM_RST3: case JM_RST4: case JM_RST5: case JM_RST6: case JM_RST7: case JM_SOI: break; case JM_SOS: // start of stream: and we're done case JM_EOI: // no data? not good return app1s(); case JM_APP1: if ((buf=stream.GetBuffer(2)) == NULL) return app1s(PARSE_INVALID_JPEG); sectionLength = EntryParser::parse16(buf, false); if (sectionLength <= 2 || (buf=stream.GetBuffer(sectionLength-=2)) == NULL) return app1s(PARSE_INVALID_JPEG); switch (int ret=parseFromEXIFSegment(buf, sectionLength)) { case PARSE_ABSENT_DATA: #ifndef TINYEXIF_NO_XMP_SUPPORT switch (ret=parseFromXMPSegment(buf, sectionLength)) { case PARSE_ABSENT_DATA: break; case PARSE_SUCCESS: if ((app1s|=FIELD_XMP) == FIELD_ALL) return PARSE_SUCCESS; break; default: return app1s(ret); // some error } #endif // TINYEXIF_NO_XMP_SUPPORT break; case PARSE_SUCCESS: if ((app1s|=FIELD_EXIF) == FIELD_ALL) return PARSE_SUCCESS; break; default: return app1s(ret); // some error } break; default: // skip the section if ((buf=stream.GetBuffer(2)) == NULL || (sectionLength=EntryParser::parse16(buf, false)) <= 2 || !stream.SkipBuffer(sectionLength-2)) return app1s(PARSE_INVALID_JPEG); } } return app1s(); } int EXIFInfo::parseFrom(std::istream& stream) { class EXIFStdStream : public EXIFStream { public: EXIFStdStream(std::istream& stream) : stream(stream) { // Would be nice to assert here that the stream was opened in binary mode, but // apparently that's not possible: https://stackoverflow.com/a/224259/19254 } bool IsValid() const override { return !!stream; } const uint8_t* GetBuffer(unsigned desiredLength) override { buffer.resize(desiredLength); if (!stream.read(reinterpret_cast(buffer.data()), desiredLength)) return NULL; return buffer.data(); } bool SkipBuffer(unsigned desiredLength) override { return (bool)stream.seekg(desiredLength, std::ios::cur); } private: std::istream& stream; std::vector buffer; }; EXIFStdStream streamWrapper(stream); return parseFrom(streamWrapper); } int EXIFInfo::parseFrom(const uint8_t* buf, unsigned len) { class EXIFStreamBuffer : public EXIFStream { public: explicit EXIFStreamBuffer(const uint8_t* buf, unsigned len) : it(buf), end(buf+len) {} bool IsValid() const override { return it != NULL; } const uint8_t* GetBuffer(unsigned desiredLength) override { const uint8_t* const itNext(it+desiredLength); if (itNext >= end) return NULL; const uint8_t* const begin(it); it = itNext; return begin; } bool SkipBuffer(unsigned desiredLength) override { return GetBuffer(desiredLength) != NULL; } private: const uint8_t* it, * const end; }; EXIFStreamBuffer stream(buf, len); return parseFrom(stream); } // // Main parsing function for an EXIF segment. // Do a sanity check by looking for bytes "Exif\0\0". // The marker has to contain at least the TIFF header, otherwise the // JM_APP1 data is corrupt. So the minimum length specified here has to be: // 6 bytes: "Exif\0\0" string // 2 bytes: TIFF header (either "II" or "MM" string) // 2 bytes: TIFF magic (short 0x2a00 in Motorola byte order) // 4 bytes: Offset to first IFD // ========= // 14 bytes // // PARAM: 'buf' start of the EXIF TIFF, which must be the bytes "Exif\0\0". // PARAM: 'len' length of buffer // int EXIFInfo::parseFromEXIFSegment(const uint8_t* buf, unsigned len) { unsigned offs = 6; // current offset into buffer if (!buf || len < offs) return PARSE_ABSENT_DATA; if (!std::equal(buf, buf+offs, "Exif\0\0")) return PARSE_ABSENT_DATA; // Now parsing the TIFF header. The first two bytes are either "II" or // "MM" for Intel or Motorola byte alignment. Sanity check by parsing // the uint16_t that follows, making sure it equals 0x2a. The // last 4 bytes are an offset into the first IFD, which are added to // the global offset counter. For this block, we expect the following // minimum size: // 2 bytes: 'II' or 'MM' // 2 bytes: 0x002a // 4 bytes: offset to first IDF // ----------------------------- // 8 bytes if (offs + 8 > len) return PARSE_CORRUPT_DATA; const uint32_t _ONE32 = 1; const bool IS_LITTLE_ENDIAN = reinterpret_cast(&_ONE32)[0] == 1; bool alignIntel; if (buf[offs] == 'I' && buf[offs+1] == 'I') alignIntel = IS_LITTLE_ENDIAN; // 1: Intel byte alignment else if (buf[offs] == 'M' && buf[offs+1] == 'M') alignIntel = !IS_LITTLE_ENDIAN; // 0: Motorola byte alignment else return PARSE_UNKNOWN_BYTEALIGN; EntryParser parser(buf, len, offs, alignIntel); offs += 2; if (0x2a != EntryParser::parse16(buf + offs, alignIntel)) return PARSE_CORRUPT_DATA; offs += 2; const unsigned first_ifd_offset = EntryParser::parse32(buf + offs, alignIntel); offs += first_ifd_offset - 4; if (offs >= len) return PARSE_CORRUPT_DATA; // Now parsing the first Image File Directory (IFD0, for the main image). // An IFD consists of a variable number of 12-byte directory entries. The // first two bytes of the IFD section contain the number of directory // entries in the section. The last 4 bytes of the IFD contain an offset // to the next IFD, which means this IFD must contain exactly 6 + 12 * num // bytes of data. // Note that it's possible that the next IFD offset doesn't exist, // so here the last 4 bytes are considered optional. if (offs + 2 > len) return PARSE_CORRUPT_DATA; unsigned num_entries = EntryParser::parse16(buf + offs, alignIntel); if (offs + 2 + 12 * num_entries > len) return PARSE_CORRUPT_DATA; unsigned exif_sub_ifd_offset = len; unsigned gps_sub_ifd_offset = len; parser.Init(offs+2); while (num_entries-- > 0) { parser.ParseTag(); parseIFDImage(parser, exif_sub_ifd_offset, gps_sub_ifd_offset); } // Jump to the EXIF SubIFD if it exists and parse all the information // there. Note that it's possible that the EXIF SubIFD doesn't exist. // The EXIF SubIFD contains most of the interesting information that a // typical user might want. if (exif_sub_ifd_offset + 4 <= len) { offs = exif_sub_ifd_offset; num_entries = EntryParser::parse16(buf + offs, alignIntel); if (offs + 2 + 12 * num_entries > len) return PARSE_CORRUPT_DATA; parser.Init(offs+2); while (num_entries-- > 0) { parser.ParseTag(); parseIFDExif(parser); } } // Jump to the GPS SubIFD if it exists and parse all the information // there. Note that it's possible that the GPS SubIFD doesn't exist. if (gps_sub_ifd_offset + 4 <= len) { offs = gps_sub_ifd_offset; num_entries = EntryParser::parse16(buf + offs, alignIntel); if (offs + 2 + 12 * num_entries > len) return PARSE_CORRUPT_DATA; parser.Init(offs+2); while (num_entries-- > 0) { parser.ParseTag(); parseIFDGPS(parser); } GeoLocation.parseCoords(); } return PARSE_SUCCESS; } #ifndef TINYEXIF_NO_XMP_SUPPORT // // Main parsing function for a XMP segment. // Do a sanity check by looking for bytes "http://ns.adobe.com/xap/1.0/\0". // So the minimum length specified here has to be: // 29 bytes: "http://ns.adobe.com/xap/1.0/\0" string // // PARAM: 'buf' start of the XMP header, which must be the bytes "http://ns.adobe.com/xap/1.0/\0". // PARAM: 'len' length of buffer // int EXIFInfo::parseFromXMPSegment(const uint8_t* buf, unsigned len) { unsigned offs = 29; // current offset into buffer if (!buf || len < offs) return PARSE_ABSENT_DATA; if (!std::equal(buf, buf+offs, "http://ns.adobe.com/xap/1.0/\0")) return PARSE_ABSENT_DATA; if (offs >= len) return PARSE_CORRUPT_DATA; return parseFromXMPSegmentXML((const char*)(buf + offs), len - offs); } int EXIFInfo::parseFromXMPSegmentXML(const char* szXML, unsigned len) { // Skip xpacket end section so that tinyxml2 lib parses the section correctly. const char* szEnd(Tools::strrnstr(szXML, "FirstChildElement("rdf:RDF")) == NULL || (document=document->FirstChildElement("rdf:Description")) == NULL) return PARSE_ABSENT_DATA; // Try parsing the XMP content for tiff details. if (Orientation == 0) { uint32_t _Orientation(0); document->QueryUnsignedAttribute("tiff:Orientation", &_Orientation); Orientation = (uint16_t)_Orientation; } if (ImageWidth == 0 && ImageHeight == 0) { document->QueryUnsignedAttribute("tiff:ImageWidth", &ImageWidth); if (document->QueryUnsignedAttribute("tiff:ImageHeight", &ImageHeight) != tinyxml2::XML_SUCCESS) document->QueryUnsignedAttribute("tiff:ImageLength", &ImageHeight) ; } if (XResolution == 0 && YResolution == 0 && ResolutionUnit == 0) { document->QueryDoubleAttribute("tiff:XResolution", &XResolution); document->QueryDoubleAttribute("tiff:YResolution", &YResolution); uint32_t _ResolutionUnit(0); document->QueryUnsignedAttribute("tiff:ResolutionUnit", &_ResolutionUnit); ResolutionUnit = (uint16_t)_ResolutionUnit; } // Try parsing the XMP content for projection type. { const tinyxml2::XMLElement* const element(document->FirstChildElement("GPano:ProjectionType")); if (element != NULL) { const char* const szProjectionType(element->GetText()); if (szProjectionType != NULL) { if (0 == strcasecmp(szProjectionType, "perspective")) ProjectionType = 1; else if (0 == strcasecmp(szProjectionType, "equirectangular") || 0 == strcasecmp(szProjectionType, "spherical")) ProjectionType = 2; } } } // Try parsing the XMP content for supported maker's info. struct ParseXMP { // try yo fetch the value both from the attribute and child element // and parse if needed rational numbers stored as string fraction static bool Value(const tinyxml2::XMLElement* document, const char* name, double& value) { const char* szAttribute = document->Attribute(name); if (szAttribute == NULL) { const tinyxml2::XMLElement* const element(document->FirstChildElement(name)); if (element == NULL || (szAttribute=element->GetText()) == NULL) return false; } std::vector values; Tools::strSplit(szAttribute, '/', values); switch (values.size()) { case 1: value = strtod(values.front().c_str(), NULL); return true; case 2: value = strtod(values.front().c_str(), NULL)/strtod(values.back().c_str(), NULL); return true; } return false; } // same as previous function but with unsigned int results static bool Value(const tinyxml2::XMLElement* document, const char* name, uint32_t& value) { const char* szAttribute = document->Attribute(name); if (szAttribute == NULL) { const tinyxml2::XMLElement* const element(document->FirstChildElement(name)); if (element == NULL || (szAttribute = element->GetText()) == NULL) return false; } value = strtoul(szAttribute, NULL, 0); return true; } // same as previous function but with std::string static bool Value(const tinyxml2::XMLElement* document, const char* name, std::string& value) { const char* szAttribute = document->Attribute(name); if (szAttribute == NULL) { const tinyxml2::XMLElement* const element(document->FirstChildElement(name)); if (element == NULL || (szAttribute = element->GetText()) == NULL) return false; } value = std::string(szAttribute); return true; } }; const char* szAbout(document->Attribute("rdf:about")); if (0 == strcasecmp(Make.c_str(), "DJI") || (szAbout != NULL && 0 == strcasecmp(szAbout, "DJI Meta Data"))) { ParseXMP::Value(document, "drone-dji:AbsoluteAltitude", GeoLocation.Altitude); ParseXMP::Value(document, "drone-dji:RelativeAltitude", GeoLocation.RelativeAltitude); ParseXMP::Value(document, "drone-dji:GimbalRollDegree", GeoLocation.RollDegree); ParseXMP::Value(document, "drone-dji:GimbalPitchDegree", GeoLocation.PitchDegree); ParseXMP::Value(document, "drone-dji:GimbalYawDegree", GeoLocation.YawDegree); ParseXMP::Value(document, "drone-dji:CalibratedFocalLength", Calibration.FocalLength); ParseXMP::Value(document, "drone-dji:CalibratedOpticalCenterX", Calibration.OpticalCenterX); ParseXMP::Value(document, "drone-dji:CalibratedOpticalCenterY", Calibration.OpticalCenterY); std::string dewarpData; ParseXMP::Value(document, "drone-dji:DewarpFlag", Distortion.DewarpFlag); ParseXMP::Value(document, "drone-dji:DewarpData", dewarpData); std::vector distortionParams; size_t pos = dewarpData.find(';'); if (pos != std::string::npos) { std::stringstream ss(dewarpData.substr(pos + 1)); std::string item; while (std::getline(ss, item, ',')) { distortionParams.push_back(std::stod(item)); } } // The DewarpData string has the following format: // date;Fx,Fy,Cx,Cy,K1,K2,P1,P2,K3 // , where Fx, Fy are focal lengths in pixels, Cx, Cy are optical center offsets from the image center in pixels if (distortionParams.size() == 9) { Distortion.K1 = distortionParams[4]; Distortion.K2 = distortionParams[5]; Distortion.P1 = distortionParams[6]; Distortion.P2 = distortionParams[7]; Distortion.K3 = distortionParams[8]; } } else if (0 == strcasecmp(Make.c_str(), "senseFly") || 0 == strcasecmp(Make.c_str(), "Sentera")) { ParseXMP::Value(document, "Camera:Roll", GeoLocation.RollDegree); if (ParseXMP::Value(document, "Camera:Pitch", GeoLocation.PitchDegree)) { // convert to DJI format: senseFly uses pitch 0 as NADIR, whereas DJI -90 GeoLocation.PitchDegree = Tools::NormD180(GeoLocation.PitchDegree-90.0); } ParseXMP::Value(document, "Camera:Yaw", GeoLocation.YawDegree); ParseXMP::Value(document, "Camera:GPSXYAccuracy", GeoLocation.AccuracyXY); ParseXMP::Value(document, "Camera:GPSZAccuracy", GeoLocation.AccuracyZ); } else if (0 == strcasecmp(Make.c_str(), "PARROT")) { ParseXMP::Value(document, "Camera:Roll", GeoLocation.RollDegree) || ParseXMP::Value(document, "drone-parrot:CameraRollDegree", GeoLocation.RollDegree); if (ParseXMP::Value(document, "Camera:Pitch", GeoLocation.PitchDegree) || ParseXMP::Value(document, "drone-parrot:CameraPitchDegree", GeoLocation.PitchDegree)) { // convert to DJI format: senseFly uses pitch 0 as NADIR, whereas DJI -90 GeoLocation.PitchDegree = Tools::NormD180(GeoLocation.PitchDegree-90.0); } ParseXMP::Value(document, "Camera:Yaw", GeoLocation.YawDegree) || ParseXMP::Value(document, "drone-parrot:CameraYawDegree", GeoLocation.YawDegree); ParseXMP::Value(document, "Camera:AboveGroundAltitude", GeoLocation.RelativeAltitude); } ParseXMP::Value(document, "GPano:PosePitchDegrees", GPano.PosePitchDegrees); ParseXMP::Value(document, "GPano:PoseRollDegrees", GPano.PoseRollDegrees); // parse GCamera:MicroVideo if (document->Attribute("GCamera:MicroVideo")) { ParseXMP::Value(document, "GCamera:MicroVideo", MicroVideo.HasMicroVideo); ParseXMP::Value(document, "GCamera:MicroVideoVersion", MicroVideo.MicroVideoVersion); ParseXMP::Value(document, "GCamera:MicroVideoOffset", MicroVideo.MicroVideoOffset); } return PARSE_SUCCESS; } #endif // TINYEXIF_NO_XMP_SUPPORT bool EXIFInfo::Calibration_t::hasCalibration() const { return FocalLength > 0.0 && OpticalCenterX > 0.0 && OpticalCenterY > 0.0; } bool EXIFInfo::Distortion_t::hasDewarpFlag() const { return DewarpFlag != UINT32_MAX; } bool EXIFInfo::Distortion_t::hasDistortion() const { return K1 != 0.0 || K2 != 0.0 || P1 != 0.0 || P2 != 0.0 || K3 != 0.0; } void EXIFInfo::Geolocation_t::parseCoords() { // Convert GPS latitude if (LatComponents.degrees != DBL_MAX || LatComponents.minutes != 0 || LatComponents.seconds != 0) { Latitude = LatComponents.degrees + LatComponents.minutes / 60 + LatComponents.seconds / 3600; if ('S' == LatComponents.direction) Latitude = -Latitude; } // Convert GPS longitude if (LonComponents.degrees != DBL_MAX || LonComponents.minutes != 0 || LonComponents.seconds != 0) { Longitude = LonComponents.degrees + LonComponents.minutes / 60 + LonComponents.seconds / 3600; if ('W' == LonComponents.direction) Longitude = -Longitude; } // Convert GPS altitude if (hasAltitude() && AltitudeRef == 1) { Altitude = -Altitude; } } bool EXIFInfo::Geolocation_t::hasLatLon() const { return Latitude != DBL_MAX && Longitude != DBL_MAX; } bool EXIFInfo::Geolocation_t::hasAltitude() const { return Altitude != DBL_MAX; } bool EXIFInfo::Geolocation_t::hasRelativeAltitude() const { return RelativeAltitude != DBL_MAX; } bool EXIFInfo::Geolocation_t::hasOrientation() const { return RollDegree != DBL_MAX && PitchDegree != DBL_MAX && YawDegree != DBL_MAX; } bool EXIFInfo::Geolocation_t::hasSpeed() const { return SpeedX != DBL_MAX && SpeedY != DBL_MAX && SpeedZ != DBL_MAX; } bool EXIFInfo::Geolocation_t::hasAccuracy() const { return AccuracyXY != 0 && AccuracyZ != 0; } bool EXIFInfo::GPano_t::hasPosePitchDegrees() const { return PosePitchDegrees != DBL_MAX; } bool EXIFInfo::GPano_t::hasPoseRollDegrees() const { return PoseRollDegrees != DBL_MAX; } void EXIFInfo::clear() { Fields = FIELD_NA; // Strings ImageDescription = ""; Make = ""; Model = ""; SerialNumber = ""; Software = ""; DateTime = ""; DateTimeOriginal = ""; DateTimeDigitized = ""; SubSecTimeOriginal= ""; Copyright = ""; // Shorts / unsigned / double ImageWidth = 0; ImageHeight = 0; RelatedImageWidth = 0; RelatedImageHeight= 0; Orientation = 0; XResolution = 0; YResolution = 0; ResolutionUnit = 0; BitsPerSample = 0; ExposureTime = 0; FNumber = 0; ExposureProgram = 0; ISOSpeedRatings = 0; ShutterSpeedValue = 0; ApertureValue = 0; BrightnessValue = 0; ExposureBiasValue = 0; SubjectDistance = 0; FocalLength = 0; Flash = 0; MeteringMode = 0; LightSource = 0; ProjectionType = 0; SubjectArea.clear(); // Calibration Calibration.FocalLength = 0; Calibration.OpticalCenterX = 0; Calibration.OpticalCenterY = 0; // LensInfo LensInfo.FocalLengthMax = 0; LensInfo.FocalLengthMin = 0; LensInfo.FStopMax = 0; LensInfo.FStopMin = 0; LensInfo.DigitalZoomRatio = 0; LensInfo.FocalLengthIn35mm = 0; LensInfo.FocalPlaneXResolution = 0; LensInfo.FocalPlaneYResolution = 0; LensInfo.FocalPlaneResolutionUnit = 0; LensInfo.Make = ""; LensInfo.Model = ""; // Geolocation GeoLocation.Latitude = DBL_MAX; GeoLocation.Longitude = DBL_MAX; GeoLocation.Altitude = DBL_MAX; GeoLocation.AltitudeRef = 0; GeoLocation.RelativeAltitude = DBL_MAX; GeoLocation.RollDegree = DBL_MAX; GeoLocation.PitchDegree = DBL_MAX; GeoLocation.YawDegree = DBL_MAX; GeoLocation.SpeedX = DBL_MAX; GeoLocation.SpeedY = DBL_MAX; GeoLocation.SpeedZ = DBL_MAX; GeoLocation.AccuracyXY = 0; GeoLocation.AccuracyZ = 0; GeoLocation.GPSDOP = 0; GeoLocation.GPSDifferential = 0; GeoLocation.GPSMapDatum = ""; GeoLocation.GPSTimeStamp = ""; GeoLocation.GPSDateStamp = ""; GeoLocation.LatComponents.degrees = DBL_MAX; GeoLocation.LatComponents.minutes = 0; GeoLocation.LatComponents.seconds = 0; GeoLocation.LatComponents.direction = 0; GeoLocation.LonComponents.degrees = DBL_MAX; GeoLocation.LonComponents.minutes = 0; GeoLocation.LonComponents.seconds = 0; GeoLocation.LonComponents.direction = 0; // Distortion Distortion.DewarpFlag = UINT32_MAX; Distortion.K1 = 0; Distortion.K2 = 0; Distortion.P1 = 0; Distortion.P2 = 0; Distortion.K3 = 0; // GPano GPano.PosePitchDegrees = DBL_MAX; GPano.PoseRollDegrees = DBL_MAX; // Video metadata MicroVideo.HasMicroVideo = 0; MicroVideo.MicroVideoVersion = 0; MicroVideo.MicroVideoOffset = 0; } } // namespace TinyEXIF