diff --git a/lib/TinyEXIF/README.md b/lib/TinyEXIF/README.md index 1d14e38..c8a52e7 100644 --- a/lib/TinyEXIF/README.md +++ b/lib/TinyEXIF/README.md @@ -34,24 +34,49 @@ int main(int argc, const char** argv) { ``` See `main.cpp` for more details. -## Copyright - -Redistribution and use in source and binary forms, with or without -modification, are permitted provided that the following conditions are met: - - - Redistributions of source code must retain the above copyright notice, - this list of conditions and the following disclaimer. - - Redistributions in binary form must reproduce the above copyright notice, - this list of conditions and the following disclaimer in the documentation - and/or other materials provided with the distribution. - -THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS ``AS IS'' AND ANY EXPRESS -OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES -OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN -NO EVENT SHALL THE FREEBSD PROJECT OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, -INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, -BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, -DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY -OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING -NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, -EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. +## Absent tags vs tags that are legitimately zero + +All data fields are zero-initialised, so a value of `0` on its own does not say whether the tag +was missing from the file or whether the camera really wrote a `0`. `HasField()` answers that, +and `GetFields()` lists everything that was found: + +``` + TinyEXIF::EXIFInfo imageEXIF(istream); + + // ISOSpeedRatings == 0 alone is ambiguous, this is not + if (imageEXIF.HasField(TinyEXIF::FIELD_ID_ISOSpeedRatings)) + std::cout << "ISO " << imageEXIF.ISOSpeedRatings << "\n"; + else + std::cout << "ISO not recorded by the camera\n"; + + // list every tag that was present + for (TinyEXIF::FieldID id: imageEXIF.GetFields()) + std::cout << TinyEXIF::FieldName(id) << "\n"; +``` + +There is one `FieldID` enumerator per data field, named `FIELD_ID_` followed by the path of the +member it fills, e.g. `FIELD_ID_GeoLocation_Altitude` for `GeoLocation.Altitude`; `FieldName()` +returns that path as a string. A field counts as present when the tag carrying it was parsed +successfully, from EXIF or from XMP; a tag that is present but malformed does not count. +Existing fields, sentinel values (`DBL_MAX`, `UINT32_MAX`) and `hasXxx()` accessors are +unchanged, so this is purely additive. + +Run the demo with `TinyEXIFdemo --fields` to print the list for a file. + +This API was added in 1.1.0 and can be feature-gated with the `TINYEXIF_VERSION` macro: + +``` + #if TINYEXIF_VERSION >= 10100 + // TinyEXIF::EXIFInfo::HasField() is available + #endif +``` + +## License + +MIT [License](https://github.com/cdcseacave/TinyEXIF/blob/master/LICENSE) + +Copyright (c) 2025 cdcseacave + +## Acknowledgments + +Forked from [easyexif](https://github.com/mayanklahiri/easyexif) library (2013 version) of Mayank Lahiri (mlahiri@gmail.com); see [LICENSE.easyexif](https://github.com/cdcseacave/TinyEXIF/blob/master/LICENSE.easyexif) for its terms. \ No newline at end of file diff --git a/lib/TinyEXIF/TinyEXIF.cpp b/lib/TinyEXIF/TinyEXIF.cpp index 2fadd6a..5408a45 100644 --- a/lib/TinyEXIF/TinyEXIF.cpp +++ b/lib/TinyEXIF/TinyEXIF.cpp @@ -2,48 +2,27 @@ TinyEXIF.cpp -- A simple ISO C++ library to parse basic EXIF and XMP information from a JPEG file. - Copyright (c) 2015-2017 Seacave + Copyright (c) 2015-2025 Seacave cdc.seacave@gmail.com - All rights reserved. - - Based on the easyexif library (2013 version) - https://github.com/mayanklahiri/easyexif - of Mayank Lahiri (mlahiri@gmail.com). - - Redistribution and use in source and binary forms, with or without - modification, are permitted provided that the following conditions are met: - - - Redistributions of source code must retain the above copyright notice, - this list of conditions and the following disclaimer. - - Redistributions in binary form must reproduce the above copyright notice, - this list of conditions and the following disclaimer in the documentation - and/or other materials provided with the distribution. - - THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS ``AS IS'' AND ANY EXPRESS - OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES - OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN - NO EVENT SHALL THE FREEBSD PROJECT OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, - INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, - BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, - DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY - OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING - NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, - EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + MIT License */ #include "TinyEXIF.h" - -#ifndef TINYEXIF_NO_XMP_SUPPORT -#include -#endif // TINYEXIF_NO_XMP_SUPPORT - -#include +#include #include +#include +#include +#include #include #include #include #include #include +#include + +#ifndef TINYEXIF_NO_XMP_SUPPORT +#include +#endif // TINYEXIF_NO_XMP_SUPPORT #ifdef _MSC_VER namespace { @@ -178,15 +157,30 @@ class EntryParser { 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) {} + // Every read of buf must be validated by this: does the range [offset, offset+size) + // lie inside the buffer? Written as a subtraction so the check itself can not + // overflow, and taking a 64bit offset so that offsets the caller computes from + // attacker controlled data (base + idx*size) can not wrap around either. + bool InBounds(uint64_t offset, uint32_t size) const { + return offset <= len && (uint64_t)len - offset >= size; + } + void Init(unsigned _offs) { + // ParseTag() steps forward by one entry before reading, so start one entry + // earlier; the wrap-around for _offs < 12 is undone by that same step offs = _offs - 12; } - void ParseTag() { + // Step to the next 12-byte IFD entry and read it; + // returns false without touching the current entry if it does not fit the buffer + bool ParseTag() { offs += 12; + if (!InBounds(offs, 12)) + return false; tag = parse16(buf + offs, alignIntel); format = parse16(buf + offs + 2, alignIntel); length = parse32(buf + offs + 4, alignIntel); + return true; } const uint8_t* GetBuffer() const { return buf; } @@ -196,7 +190,9 @@ class EntryParser { 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(); } + // absolute offset of the data this entry points to; 64bit because the data offset + // is fully attacker controlled and would wrap when added to the header start + uint64_t GetSubIFD() const { return (uint64_t)tiff_header_start + GetData(); } bool IsShort() const { return format == 3; } bool IsLong() const { return format == 4; } @@ -229,7 +225,10 @@ class EntryParser { bool Fetch(uint16_t& val, uint32_t idx) const { if (!IsShort() || length <= idx) return false; - val = parse16(buf + GetSubIFD() + idx*2, alignIntel); + const uint64_t offset = GetSubIFD() + (uint64_t)idx*2; + if (!InBounds(offset, 2)) + return false; + val = parse16(buf + (size_t)offset, alignIntel); return true; } bool Fetch(uint32_t& val) const { @@ -247,13 +246,19 @@ class EntryParser { bool Fetch(double& val) const { if (!IsRational() || length == 0) return false; - val = parseRational(buf + GetSubIFD(), alignIntel, IsSRational()); + const uint64_t offset = GetSubIFD(); + if (!InBounds(offset, 8)) + return false; + val = parseRational(buf + (size_t)offset, alignIntel, IsSRational()); return true; } bool Fetch(double& val, uint32_t idx) const { if (!IsRational() || length <= idx) return false; - val = parseRational(buf + GetSubIFD() + idx*8, alignIntel, IsSRational()); + const uint64_t offset = GetSubIFD() + (uint64_t)idx*8; + if (!InBounds(offset, 8)) + return false; + val = parseRational(buf + (size_t)offset, alignIntel, IsSRational()); return true; } @@ -310,6 +315,12 @@ class EntryParser { bool intel) { std::string value; + // num_components is a raw attacker controlled count and FetchString() passes + // it through unchecked, so reject zero here rather than at the callers: below, + // num_components-1 is an unsigned expression that would wrap to 0xffffffff and + // index that far past value.data(), then resize() to 4GiB if the byte read is 0 + if (num_components == 0) + return value; if (num_components <= 4) { value.resize(num_components); char j = intel ? 0 : 24; @@ -319,7 +330,7 @@ class EntryParser { if (value[num_components-1] == '\0') value.resize(num_components-1); } else - if (base+data+num_components <= len) { + 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') @@ -347,80 +358,221 @@ EXIFInfo::EXIFInfo(const uint8_t* data, unsigned length) { } +// Number of 64bit words needed to store one presence bit per FieldID +static const size_t FIELD_ID_WORDS = ((size_t)FIELD_ID_COUNT + 63) / 64; + +// Name of every field, in FieldID order: the path of the member it fills +static const char* const g_FieldNames[] = { + "ImageWidth", + "ImageHeight", + "RelatedImageWidth", + "RelatedImageHeight", + "ImageDescription", + "Make", + "Model", + "SerialNumber", + "Orientation", + "XResolution", + "YResolution", + "ResolutionUnit", + "BitsPerSample", + "Software", + "DateTime", + "DateTimeOriginal", + "DateTimeDigitized", + "SubSecTimeOriginal", + "Copyright", + "ExposureTime", + "FNumber", + "ExposureProgram", + "ISOSpeedRatings", + "ShutterSpeedValue", + "ApertureValue", + "BrightnessValue", + "ExposureBiasValue", + "SubjectDistance", + "FocalLength", + "Flash", + "MeteringMode", + "LightSource", + "ProjectionType", + "SubjectArea", + "Calibration.FocalLength", + "Calibration.OpticalCenterX", + "Calibration.OpticalCenterY", + "Distortion.DewarpFlag", + "Distortion.K1", + "Distortion.K2", + "Distortion.P1", + "Distortion.P2", + "Distortion.K3", + "LensInfo.FStopMin", + "LensInfo.FStopMax", + "LensInfo.FocalLengthMin", + "LensInfo.FocalLengthMax", + "LensInfo.DigitalZoomRatio", + "LensInfo.FocalLengthIn35mm", + "LensInfo.FocalPlaneXResolution", + "LensInfo.FocalPlaneYResolution", + "LensInfo.FocalPlaneResolutionUnit", + "LensInfo.Make", + "LensInfo.Model", + "GeoLocation.Latitude", + "GeoLocation.Longitude", + "GeoLocation.Altitude", + "GeoLocation.AltitudeRef", + "GeoLocation.RelativeAltitude", + "GeoLocation.RollDegree", + "GeoLocation.PitchDegree", + "GeoLocation.YawDegree", + "GeoLocation.SpeedX", + "GeoLocation.SpeedY", + "GeoLocation.SpeedZ", + "GeoLocation.AccuracyXY", + "GeoLocation.AccuracyZ", + "GeoLocation.GPSDOP", + "GeoLocation.GPSDifferential", + "GeoLocation.GPSMapDatum", + "GeoLocation.GPSTimeStamp", + "GeoLocation.GPSDateStamp", + "GeoLocation.LatComponents.direction", + "GeoLocation.LonComponents.direction", + "GPano.PosePitchDegrees", + "GPano.PoseRollDegrees", + "GPano.PoseHeadingDegrees", + "GPano.ProjectionType", + "GPano.CroppedAreaImageWidthPixels", + "GPano.CroppedAreaImageHeightPixels", + "GPano.FullPanoWidthPixels", + "GPano.FullPanoHeightPixels", + "GPano.CroppedAreaLeftPixels", + "GPano.CroppedAreaTopPixels", + "MicroVideo.HasMicroVideo", + "MicroVideo.MicroVideoVersion", + "MicroVideo.MicroVideoOffset", + "MicroVideo.HasMotionPhoto", + "MicroVideo.MotionPhotoLength", + "MicroVideo.MotionPhotoMime", +}; +static_assert(sizeof(g_FieldNames)/sizeof(g_FieldNames[0]) == (size_t)FIELD_ID_COUNT, + "g_FieldNames must have exactly one entry per FieldID"); + +const char* FieldName(FieldID id) { + const unsigned pos((unsigned)id); + return pos < (unsigned)FIELD_ID_COUNT ? g_FieldNames[pos] : ""; +} + +bool EXIFInfo::HasField(FieldID id) const { + const unsigned pos((unsigned)id); + if (pos >= (unsigned)FIELD_ID_COUNT) + return false; + const size_t word(pos/64); + // the bitset is only allocated once something is stored in it, so a parse + // that found nothing at all (or no parse at all) leaves it empty + return word < FieldsPresent.size() && + (FieldsPresent[word] & ((uint64_t)1 << (pos%64))) != 0; +} + +std::vector EXIFInfo::GetFields() const { + std::vector fields; + for (unsigned pos=0; pos<(unsigned)FIELD_ID_COUNT; ++pos) + if (HasField((FieldID)pos)) + fields.push_back((FieldID)pos); + return fields; +} + +void EXIFInfo::SetField(FieldID id) { + const unsigned pos((unsigned)id); + if (pos >= (unsigned)FIELD_ID_COUNT) + return; + // parseFromEXIFSegment()/parseFromXMPSegment() may be called directly, without + // the clear() that parseFrom() does, so the storage is allocated on demand + if (FieldsPresent.size() < FIELD_ID_WORDS) + FieldsPresent.resize(FIELD_ID_WORDS, 0); + FieldsPresent[pos/64] |= (uint64_t)1 << (pos%64); +} + +bool EXIFInfo::SetFieldIf(FieldID id, bool fetched) { + if (fetched) + SetField(id); + return fetched; +} + + // Parse tag as Image IFD -void EXIFInfo::parseIFDImage(EntryParser& parser, unsigned& exif_sub_ifd_offset, unsigned& gps_sub_ifd_offset) { +void EXIFInfo::parseIFDImage(EntryParser& parser, uint64_t& exif_sub_ifd_offset, uint64_t& gps_sub_ifd_offset) { switch (parser.GetTag()) { case 0x0102: // Bits per sample - parser.Fetch(BitsPerSample); + SetFieldIf(FIELD_ID_BitsPerSample, parser.Fetch(BitsPerSample)); break; case 0x010e: // Image description - parser.Fetch(ImageDescription); + SetFieldIf(FIELD_ID_ImageDescription, parser.Fetch(ImageDescription)); break; case 0x010f: // Camera maker - parser.Fetch(Make); + SetFieldIf(FIELD_ID_Make, parser.Fetch(Make)); break; case 0x0110: // Camera model - parser.Fetch(Model); + SetFieldIf(FIELD_ID_Model, parser.Fetch(Model)); break; case 0x0112: // Orientation of image - parser.Fetch(Orientation); + SetFieldIf(FIELD_ID_Orientation, parser.Fetch(Orientation)); break; case 0x011a: // XResolution - parser.Fetch(XResolution); + SetFieldIf(FIELD_ID_XResolution, parser.Fetch(XResolution)); break; case 0x011b: // YResolution - parser.Fetch(YResolution); + SetFieldIf(FIELD_ID_YResolution, parser.Fetch(YResolution)); break; case 0x0128: // Resolution Unit - parser.Fetch(ResolutionUnit); + SetFieldIf(FIELD_ID_ResolutionUnit, parser.Fetch(ResolutionUnit)); break; case 0x0131: // Software used for image - parser.Fetch(Software); + SetFieldIf(FIELD_ID_Software, parser.Fetch(Software)); break; case 0x0132: // EXIF/TIFF date/time of image modification - parser.Fetch(DateTime); + SetFieldIf(FIELD_ID_DateTime, parser.Fetch(DateTime)); break; case 0x1001: // Original Image width - if (!parser.Fetch(RelatedImageWidth)) { + if (!SetFieldIf(FIELD_ID_RelatedImageWidth, parser.Fetch(RelatedImageWidth))) { uint16_t _RelatedImageWidth; - if (parser.Fetch(_RelatedImageWidth)) + if (SetFieldIf(FIELD_ID_RelatedImageWidth, parser.Fetch(_RelatedImageWidth))) RelatedImageWidth = _RelatedImageWidth; } break; case 0x1002: // Original Image height - if (!parser.Fetch(RelatedImageHeight)) { + if (!SetFieldIf(FIELD_ID_RelatedImageHeight, parser.Fetch(RelatedImageHeight))) { uint16_t _RelatedImageHeight; - if (parser.Fetch(_RelatedImageHeight)) + if (SetFieldIf(FIELD_ID_RelatedImageHeight, parser.Fetch(_RelatedImageHeight))) RelatedImageHeight = _RelatedImageHeight; } break; case 0x8298: // Copyright information - parser.Fetch(Copyright); + SetFieldIf(FIELD_ID_Copyright, parser.Fetch(Copyright)); break; case 0x8769: @@ -455,87 +607,96 @@ void EXIFInfo::parseIFDExif(EntryParser& parser) { case 0x829a: // Exposure time in seconds - parser.Fetch(ExposureTime); + SetFieldIf(FIELD_ID_ExposureTime, parser.Fetch(ExposureTime)); break; case 0x829d: // FNumber - parser.Fetch(FNumber); + SetFieldIf(FIELD_ID_FNumber, parser.Fetch(FNumber)); break; case 0x8822: // Exposure Program - parser.Fetch(ExposureProgram); + SetFieldIf(FIELD_ID_ExposureProgram, parser.Fetch(ExposureProgram)); break; case 0x8827: // ISO Speed Rating - parser.Fetch(ISOSpeedRatings); + SetFieldIf(FIELD_ID_ISOSpeedRatings, parser.Fetch(ISOSpeedRatings)); break; case 0x9003: // Original date and time - parser.Fetch(DateTimeOriginal); + SetFieldIf(FIELD_ID_DateTimeOriginal, parser.Fetch(DateTimeOriginal)); break; case 0x9004: // Digitization date and time - parser.Fetch(DateTimeDigitized); + SetFieldIf(FIELD_ID_DateTimeDigitized, parser.Fetch(DateTimeDigitized)); break; case 0x9201: // Shutter speed value - parser.Fetch(ShutterSpeedValue); - ShutterSpeedValue = 1.0/exp(ShutterSpeedValue*log(2)); + // the APEX to seconds conversion only runs on a value that was really + // fetched: applied to the untouched 0 it would yield a plausible 1s + if (SetFieldIf(FIELD_ID_ShutterSpeedValue, 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); + // as above: the untouched 0 would convert to a plausible f/1 + if (SetFieldIf(FIELD_ID_ApertureValue, parser.Fetch(ApertureValue))) + ApertureValue = exp(ApertureValue*log(2)*0.5); break; case 0x9203: // Brightness value - parser.Fetch(BrightnessValue); + SetFieldIf(FIELD_ID_BrightnessValue, parser.Fetch(BrightnessValue)); break; case 0x9204: // Exposure bias value - parser.Fetch(ExposureBiasValue); + SetFieldIf(FIELD_ID_ExposureBiasValue, parser.Fetch(ExposureBiasValue)); break; case 0x9206: // Subject distance - parser.Fetch(SubjectDistance); + SetFieldIf(FIELD_ID_SubjectDistance, parser.Fetch(SubjectDistance)); break; case 0x9207: // Metering mode - parser.Fetch(MeteringMode); + SetFieldIf(FIELD_ID_MeteringMode, parser.Fetch(MeteringMode)); break; case 0x9208: // Light source - parser.Fetch(LightSource); + SetFieldIf(FIELD_ID_LightSource, parser.Fetch(LightSource)); break; case 0x9209: // Flash info - parser.Fetch(Flash); + SetFieldIf(FIELD_ID_Flash, parser.Fetch(Flash)); break; case 0x920a: // Focal length - parser.Fetch(FocalLength); + SetFieldIf(FIELD_ID_FocalLength, 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(); + if (!parser.InBounds(off+2, 12*(uint32_t)num_entries)) + return; + parser.Init((unsigned)(off+2)); + if (!parser.ParseTag()) { + parser.Init(startOff+12); + return; + } --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(); + if (!parser.ParseTag()) + break; switch (parser.GetTag()) { case 3: // SpeedX - parser.FetchFloat(GeoLocation.SpeedX); + SetFieldIf(FIELD_ID_GeoLocation_SpeedX, parser.FetchFloat(GeoLocation.SpeedX)); break; case 4: // SpeedY - parser.FetchFloat(GeoLocation.SpeedY); + SetFieldIf(FIELD_ID_GeoLocation_SpeedY, parser.FetchFloat(GeoLocation.SpeedY)); break; case 5: // SpeedZ - parser.FetchFloat(GeoLocation.SpeedZ); + SetFieldIf(FIELD_ID_GeoLocation_SpeedZ, parser.FetchFloat(GeoLocation.SpeedZ)); break; case 9: // Camera Pitch - parser.FetchFloat(GeoLocation.PitchDegree); + SetFieldIf(FIELD_ID_GeoLocation_PitchDegree, parser.FetchFloat(GeoLocation.PitchDegree)); break; case 10: // Camera Yaw - parser.FetchFloat(GeoLocation.YawDegree); + SetFieldIf(FIELD_ID_GeoLocation_YawDegree, parser.FetchFloat(GeoLocation.YawDegree)); break; case 11: // Camera Roll - parser.FetchFloat(GeoLocation.RollDegree); + SetFieldIf(FIELD_ID_GeoLocation_RollDegree, parser.FetchFloat(GeoLocation.RollDegree)); break; } } @@ -689,73 +860,81 @@ void EXIFInfo::parseIFDGPS(EntryParser& parser) { switch (parser.GetTag()) { case 1: // GPS north or south - parser.Fetch(GeoLocation.LatComponents.direction); + SetFieldIf(FIELD_ID_GeoLocation_LatComponents_direction, parser.Fetch(GeoLocation.LatComponents.direction)); break; case 2: // GPS latitude + // the components are what parseCoords() turns into GeoLocation.Latitude, + // so they mark that field rather than one enumerator each if (parser.IsRational() && parser.GetLength() == 3) { - parser.Fetch(GeoLocation.LatComponents.degrees, 0); - parser.Fetch(GeoLocation.LatComponents.minutes, 1); - parser.Fetch(GeoLocation.LatComponents.seconds, 2); + SetFieldIf(FIELD_ID_GeoLocation_Latitude, parser.Fetch(GeoLocation.LatComponents.degrees, 0)); + SetFieldIf(FIELD_ID_GeoLocation_Latitude, parser.Fetch(GeoLocation.LatComponents.minutes, 1)); + SetFieldIf(FIELD_ID_GeoLocation_Latitude, parser.Fetch(GeoLocation.LatComponents.seconds, 2)); } break; case 3: // GPS east or west - parser.Fetch(GeoLocation.LonComponents.direction); + SetFieldIf(FIELD_ID_GeoLocation_LonComponents_direction, 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); + SetFieldIf(FIELD_ID_GeoLocation_Longitude, parser.Fetch(GeoLocation.LonComponents.degrees, 0)); + SetFieldIf(FIELD_ID_GeoLocation_Longitude, parser.Fetch(GeoLocation.LonComponents.minutes, 1)); + SetFieldIf(FIELD_ID_GeoLocation_Longitude, parser.Fetch(GeoLocation.LonComponents.seconds, 2)); } break; case 5: // GPS altitude reference (below or above sea level) - parser.Fetch((uint8_t&)GeoLocation.AltitudeRef); + uint8_t altitudeRef; + if (SetFieldIf(FIELD_ID_GeoLocation_AltitudeRef, parser.Fetch(altitudeRef))) + GeoLocation.AltitudeRef = (int8_t)altitudeRef; break; case 6: // GPS altitude - parser.Fetch(GeoLocation.Altitude); + SetFieldIf(FIELD_ID_GeoLocation_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; + // Fetch() leaves its out-parameter untouched when the bounds check + // rejects the offset, so the three are initialized and all three must + // succeed: hour, minute and second are one logical value and a partial + // timestamp is not a timestamp + double h(0), m(0), s(0); + if (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; + SetField(FIELD_ID_GeoLocation_GPSTimeStamp); + } } break; case 11: // Indicates the GPS DOP (data degree of precision) - parser.Fetch(GeoLocation.GPSDOP); + SetFieldIf(FIELD_ID_GeoLocation_GPSDOP, parser.Fetch(GeoLocation.GPSDOP)); break; case 18: // GPS geodetic survey data - parser.Fetch(GeoLocation.GPSMapDatum); + SetFieldIf(FIELD_ID_GeoLocation_GPSMapDatum, parser.Fetch(GeoLocation.GPSMapDatum)); break; case 29: // GPS date-stamp - parser.Fetch(GeoLocation.GPSDateStamp); + SetFieldIf(FIELD_ID_GeoLocation_GPSDateStamp, parser.Fetch(GeoLocation.GPSDateStamp)); break; case 30: // GPS differential indicates whether differential correction is applied to the GPS receiver - parser.Fetch(GeoLocation.GPSDifferential); + SetFieldIf(FIELD_ID_GeoLocation_GPSDifferential, parser.Fetch(GeoLocation.GPSDifferential)); break; } } @@ -943,23 +1122,31 @@ int EXIFInfo::parseFromEXIFSegment(const uint8_t* buf, unsigned len) { // 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 = true; // 1: Intel byte alignment + alignIntel = IS_LITTLE_ENDIAN; // 1: Intel byte alignment else if (buf[offs] == 'M' && buf[offs+1] == 'M') - alignIntel = false; // 0: Motorola byte alignment + alignIntel = !IS_LITTLE_ENDIAN; // 0: Motorola byte alignment else return PARSE_UNKNOWN_BYTEALIGN; - EntryParser parser(buf, len, offs, alignIntel); + const unsigned tiff_header_start = offs; + EntryParser parser(buf, len, tiff_header_start, 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) + // the first IFD offset is relative to the TIFF header start and it is stored in the + // 4 bytes it has to skip, so anything below 4 points back into the TIFF header + const uint32_t first_ifd_offset = EntryParser::parse32(buf + offs, alignIntel); + if (first_ifd_offset < 4) return PARSE_CORRUPT_DATA; + const uint64_t first_ifd = (uint64_t)tiff_header_start + first_ifd_offset; + if (first_ifd >= len) + return PARSE_CORRUPT_DATA; + offs = (unsigned)first_ifd; // 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 @@ -967,16 +1154,19 @@ int EXIFInfo::parseFromEXIFSegment(const uint8_t* buf, unsigned len) { // 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. - if (offs + 2 > len) + // Note that it's possible that the next IFD offset doesn't exist, + // so here the last 4 bytes are considered optional. + if (!parser.InBounds(offs, 2)) return PARSE_CORRUPT_DATA; - int num_entries = EntryParser::parse16(buf + offs, alignIntel); - if (offs + 6 + 12 * num_entries > len) + unsigned num_entries = EntryParser::parse16(buf + offs, alignIntel); + if (!parser.InBounds((uint64_t)offs + 2, 12 * num_entries)) return PARSE_CORRUPT_DATA; - unsigned exif_sub_ifd_offset = len; - unsigned gps_sub_ifd_offset = len; + uint64_t exif_sub_ifd_offset = len; + uint64_t gps_sub_ifd_offset = len; parser.Init(offs+2); - while (--num_entries >= 0) { - parser.ParseTag(); + while (num_entries-- > 0) { + if (!parser.ParseTag()) + break; parseIFDImage(parser, exif_sub_ifd_offset, gps_sub_ifd_offset); } @@ -984,28 +1174,30 @@ int EXIFInfo::parseFromEXIFSegment(const uint8_t* buf, unsigned len) { // 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; + if (parser.InBounds(exif_sub_ifd_offset, 4)) { + offs = (unsigned)exif_sub_ifd_offset; num_entries = EntryParser::parse16(buf + offs, alignIntel); - if (offs + 6 + 12 * num_entries > len) + if (!parser.InBounds((uint64_t)offs + 2, 12 * num_entries)) return PARSE_CORRUPT_DATA; parser.Init(offs+2); - while (--num_entries >= 0) { - parser.ParseTag(); + while (num_entries-- > 0) { + if (!parser.ParseTag()) + break; 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; + if (parser.InBounds(gps_sub_ifd_offset, 4)) { + offs = (unsigned)gps_sub_ifd_offset; num_entries = EntryParser::parse16(buf + offs, alignIntel); - if (offs + 6 + 12 * num_entries > len) + if (!parser.InBounds((uint64_t)offs + 2, 12 * num_entries)) return PARSE_CORRUPT_DATA; parser.Init(offs+2); - while (--num_entries >= 0) { - parser.ParseTag(); + while (num_entries-- > 0) { + if (!parser.ParseTag()) + break; parseIFDGPS(parser); } GeoLocation.parseCoords(); @@ -1051,40 +1243,26 @@ int EXIFInfo::parseFromXMPSegmentXML(const char* szXML, unsigned len) { return PARSE_ABSENT_DATA; // Try parsing the XMP content for tiff details. + // these fill the same fields as their EXIF counterparts, so either source + // finding one counts as present if (Orientation == 0) { uint32_t _Orientation(0); - document->QueryUnsignedAttribute("tiff:Orientation", &_Orientation); + SetFieldIf(FIELD_ID_Orientation, document->QueryUnsignedAttribute("tiff:Orientation", &_Orientation) == tinyxml2::XML_SUCCESS); 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) ; + SetFieldIf(FIELD_ID_ImageWidth, document->QueryUnsignedAttribute("tiff:ImageWidth", &ImageWidth) == tinyxml2::XML_SUCCESS); + if (!SetFieldIf(FIELD_ID_ImageHeight, document->QueryUnsignedAttribute("tiff:ImageHeight", &ImageHeight) == tinyxml2::XML_SUCCESS)) + SetFieldIf(FIELD_ID_ImageHeight, document->QueryUnsignedAttribute("tiff:ImageLength", &ImageHeight) == tinyxml2::XML_SUCCESS); } if (XResolution == 0 && YResolution == 0 && ResolutionUnit == 0) { - document->QueryDoubleAttribute("tiff:XResolution", &XResolution); - document->QueryDoubleAttribute("tiff:YResolution", &YResolution); + SetFieldIf(FIELD_ID_XResolution, document->QueryDoubleAttribute("tiff:XResolution", &XResolution) == tinyxml2::XML_SUCCESS); + SetFieldIf(FIELD_ID_YResolution, document->QueryDoubleAttribute("tiff:YResolution", &YResolution) == tinyxml2::XML_SUCCESS); uint32_t _ResolutionUnit(0); - document->QueryUnsignedAttribute("tiff:ResolutionUnit", &_ResolutionUnit); + SetFieldIf(FIELD_ID_ResolutionUnit, document->QueryUnsignedAttribute("tiff:ResolutionUnit", &_ResolutionUnit) == tinyxml2::XML_SUCCESS); 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 @@ -1104,7 +1282,28 @@ int EXIFInfo::parseFromXMPSegmentXML(const char* szXML, unsigned len) { } return false; } - // same as previous function but with unsigned int results + // same as previous function but with unsigned int results; + // values too large for uint32_t (a video item longer than 4GiB, for example) saturate + // at UINT32_MAX instead of silently wrapping, and text that starts with no digits at + // all is reported as absent instead of as a zero; strtoull, not strtoul, is used so + // the range check is meaningful where unsigned long is only 32 bits wide. + // note UINT32_MAX doubles as the "absent" sentinel of seven fields fed from here - + // Distortion.DewarpFlag and the six GPano pixel counts - so for those a saturated + // value reads back as absent through their hasXxx(); fail-safe, but not distinguishable. + // + // The double overload above has the same collision with its own sentinel: strtod + // parses "1.7976931348623157e308" to exactly DBL_MAX, which clear() uses to mean + // absent, so an XMP file carrying that literal sets the field and still reads back + // as absent through hasAltitude(), hasRelativeAltitude(), hasOrientation() (Roll, + // Pitch and Yaw), hasPosePitchDegrees(), hasPoseRollDegrees() and + // hasPoseHeadingDegrees() - every DBL_MAX-sentinelled accessor this path can feed; + // hasLatLon() and hasSpeed() also use DBL_MAX but are fed from EXIF rationals and + // floats, neither of which can produce it. HasField()/GetFields() report it present, + // so the two presence APIs disagree on this one input. The sentinels are part of + // the public contract of those accessors and cannot be changed without breaking + // every existing caller, so this is documented rather than fixed; HasField() is + // the accurate answer where the two differ. Both collisions need a value at the + // very edge of the type to trigger and both fail towards "absent". static bool Value(const tinyxml2::XMLElement* document, const char* name, uint32_t& value) { const char* szAttribute = document->Attribute(name); if (szAttribute == NULL) { @@ -1112,57 +1311,221 @@ int EXIFInfo::parseFromXMPSegmentXML(const char* szXML, unsigned len) { if (element == NULL || (szAttribute = element->GetText()) == NULL) return false; } - value = strtoul(szAttribute, NULL, 0); return true; + // strtoull negates a negative input rather than rejecting it, so "-1" would + // come back as ULLONG_MAX and saturate onto the UINT32_MAX absence sentinel; + // a pixel count or a flag is never negative, so reject the sign outright + const char* szValue(szAttribute); + while (isspace((unsigned char)*szValue)) + ++szValue; + if (*szValue == '-') + return false; + char* szEnd(NULL); + errno = 0; + // base 10, not 0: these are decimal XMP integers, not C literals, so neither + // a 0x prefix nor a leading zero should change how they are read + const unsigned long long ullValue(strtoull(szValue, &szEnd, 10)); + if (szEnd == szValue) + return false; + value = (errno == ERANGE || ullValue > UINT32_MAX ? UINT32_MAX : (uint32_t)ullValue); + 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; + } + // true if the given mime type names a video; mime types are case insensitive + static bool IsVideoMime(const std::string& mime) { + std::string lower(mime); + for (std::string::iterator it=lower.begin(); it!=lower.end(); ++it) + *it = (char)tolower((unsigned char)*it); + return lower.compare(0, 6, "video/") == 0; + } + // fetch the mime type and the byte length of the first video item listed in the + // GCamera:MotionPhoto container directory: + // Container:Directory / rdf:Seq / rdf:li / Container:Item[Item:Mime, Item:Length] + // some writers spell the container namespace "GContainer" and fold the item + // namespace into the attribute name, so both spellings are tried; + // this is XMP from an untrusted file, so every step of the walk may be missing; + // 'hasLength' reports whether the item carried a length, which the caller needs + // as this being a static of a local struct keeps it from marking the field itself + static bool VideoItem(const tinyxml2::XMLElement* document, std::string& mime, uint32_t& length, bool& hasLength) { + const char* const szDirectories[2] = {"Container:Directory", "GContainer:Directory"}; + const char* const szItems[2] = {"Container:Item", "GContainer:Item"}; + for (unsigned i=0; i<2; ++i) { + const tinyxml2::XMLElement* const directory(document->FirstChildElement(szDirectories[i])); + if (directory == NULL) + continue; + const tinyxml2::XMLElement* const seq(directory->FirstChildElement("rdf:Seq")); + if (seq == NULL) + continue; + for (const tinyxml2::XMLElement* li(seq->FirstChildElement("rdf:li")); li != NULL; li=li->NextSiblingElement("rdf:li")) { + const tinyxml2::XMLElement* const item(li->FirstChildElement(szItems[i])); + if (item == NULL) + continue; + std::string itemMime; + if (!Value(item, "Item:Mime", itemMime) && + !Value(item, "GContainer:ItemMime", itemMime)) + continue; + if (!IsVideoMime(itemMime)) + continue; + mime = itemMime; + // a container item may legitimately omit its length + hasLength = + Value(item, "Item:Length", length) || + Value(item, "GContainer:ItemLength", length); + return true; + } + } return false; } }; 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); + SetFieldIf(FIELD_ID_GeoLocation_Altitude, ParseXMP::Value(document, "drone-dji:AbsoluteAltitude", GeoLocation.Altitude)); + SetFieldIf(FIELD_ID_GeoLocation_RelativeAltitude, ParseXMP::Value(document, "drone-dji:RelativeAltitude", GeoLocation.RelativeAltitude)); + SetFieldIf(FIELD_ID_GeoLocation_RollDegree, ParseXMP::Value(document, "drone-dji:GimbalRollDegree", GeoLocation.RollDegree)); + SetFieldIf(FIELD_ID_GeoLocation_PitchDegree, ParseXMP::Value(document, "drone-dji:GimbalPitchDegree", GeoLocation.PitchDegree)); + SetFieldIf(FIELD_ID_GeoLocation_YawDegree, ParseXMP::Value(document, "drone-dji:GimbalYawDegree", GeoLocation.YawDegree)); + SetFieldIf(FIELD_ID_Calibration_FocalLength, ParseXMP::Value(document, "drone-dji:CalibratedFocalLength", Calibration.FocalLength)); + SetFieldIf(FIELD_ID_Calibration_OpticalCenterX, ParseXMP::Value(document, "drone-dji:CalibratedOpticalCenterX", Calibration.OpticalCenterX)); + SetFieldIf(FIELD_ID_Calibration_OpticalCenterY, ParseXMP::Value(document, "drone-dji:CalibratedOpticalCenterY", Calibration.OpticalCenterY)); + std::string dewarpData; + SetFieldIf(FIELD_ID_Distortion_DewarpFlag, ParseXMP::Value(document, "drone-dji:DewarpFlag", Distortion.DewarpFlag)); + // DewarpData lands in a local: the fields it feeds are marked below, where they are written + 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, ',')) { + // strtod, not std::stod: dewarpData is attacker controlled XMP text and + // std::stod throws on a non-numeric or an out-of-range item, an exception + // nothing between here and parseFrom() catches - it would abort the + // process instead of returning one of the documented error codes + const char* const szItem(item.c_str()); + char* szEnd(NULL); + errno = 0; + const double value(strtod(szItem, &szEnd)); + if (szEnd == szItem || errno == ERANGE) { + // one malformed item invalidates the whole list, so that the + // distortion fields stay absent instead of half populated + distortionParams.clear(); + break; + } + distortionParams.push_back(value); + } + } + // 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]; + SetField(FIELD_ID_Distortion_K1); + SetField(FIELD_ID_Distortion_K2); + SetField(FIELD_ID_Distortion_P1); + SetField(FIELD_ID_Distortion_P2); + SetField(FIELD_ID_Distortion_K3); + } } 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)) { + SetFieldIf(FIELD_ID_GeoLocation_RollDegree, ParseXMP::Value(document, "Camera:Roll", GeoLocation.RollDegree)); + if (SetFieldIf(FIELD_ID_GeoLocation_PitchDegree, 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); + SetFieldIf(FIELD_ID_GeoLocation_YawDegree, ParseXMP::Value(document, "Camera:Yaw", GeoLocation.YawDegree)); + SetFieldIf(FIELD_ID_GeoLocation_AccuracyXY, ParseXMP::Value(document, "Camera:GPSXYAccuracy", GeoLocation.AccuracyXY)); + SetFieldIf(FIELD_ID_GeoLocation_AccuracyZ, 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)) { + SetFieldIf(FIELD_ID_GeoLocation_RollDegree, ParseXMP::Value(document, "Camera:Roll", GeoLocation.RollDegree)) || + SetFieldIf(FIELD_ID_GeoLocation_RollDegree, ParseXMP::Value(document, "drone-parrot:CameraRollDegree", GeoLocation.RollDegree)); + if (SetFieldIf(FIELD_ID_GeoLocation_PitchDegree, ParseXMP::Value(document, "Camera:Pitch", GeoLocation.PitchDegree)) || + SetFieldIf(FIELD_ID_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); + SetFieldIf(FIELD_ID_GeoLocation_YawDegree, ParseXMP::Value(document, "Camera:Yaw", GeoLocation.YawDegree)) || + SetFieldIf(FIELD_ID_GeoLocation_YawDegree, ParseXMP::Value(document, "drone-parrot:CameraYawDegree", GeoLocation.YawDegree)); + SetFieldIf(FIELD_ID_GeoLocation_RelativeAltitude, ParseXMP::Value(document, "Camera:AboveGroundAltitude", GeoLocation.RelativeAltitude)); } - ParseXMP::Value(document, "GPano:PosePitchDegrees", GPano.PosePitchDegrees); - ParseXMP::Value(document, "GPano:PoseRollDegrees", GPano.PoseRollDegrees); + // Try parsing the XMP content for spherical (GPano) metadata. + // GPano:ProjectionType is parsed once, into the raw spec string; the existing numeric + // ProjectionType is derived from it below instead of being parsed independently, so the + // two fields can never drift out of sync with each other. + if (SetFieldIf(FIELD_ID_GPano_ProjectionType, ParseXMP::Value(document, "GPano:ProjectionType", GPano.ProjectionType))) { + if (0 == strcasecmp(GPano.ProjectionType.c_str(), "perspective")) { + ProjectionType = 1; + SetField(FIELD_ID_ProjectionType); + } else + if (GPano.isEquirectangular()) { + ProjectionType = 2; + SetField(FIELD_ID_ProjectionType); + } + } + SetFieldIf(FIELD_ID_GPano_PoseHeadingDegrees, ParseXMP::Value(document, "GPano:PoseHeadingDegrees", GPano.PoseHeadingDegrees)); + SetFieldIf(FIELD_ID_GPano_PosePitchDegrees, ParseXMP::Value(document, "GPano:PosePitchDegrees", GPano.PosePitchDegrees)); + SetFieldIf(FIELD_ID_GPano_PoseRollDegrees, ParseXMP::Value(document, "GPano:PoseRollDegrees", GPano.PoseRollDegrees)); + SetFieldIf(FIELD_ID_GPano_CroppedAreaImageWidthPixels, ParseXMP::Value(document, "GPano:CroppedAreaImageWidthPixels", GPano.CroppedAreaImageWidthPixels)); + SetFieldIf(FIELD_ID_GPano_CroppedAreaImageHeightPixels, ParseXMP::Value(document, "GPano:CroppedAreaImageHeightPixels", GPano.CroppedAreaImageHeightPixels)); + SetFieldIf(FIELD_ID_GPano_FullPanoWidthPixels, ParseXMP::Value(document, "GPano:FullPanoWidthPixels", GPano.FullPanoWidthPixels)); + SetFieldIf(FIELD_ID_GPano_FullPanoHeightPixels, ParseXMP::Value(document, "GPano:FullPanoHeightPixels", GPano.FullPanoHeightPixels)); + SetFieldIf(FIELD_ID_GPano_CroppedAreaLeftPixels, ParseXMP::Value(document, "GPano:CroppedAreaLeftPixels", GPano.CroppedAreaLeftPixels)); + SetFieldIf(FIELD_ID_GPano_CroppedAreaTopPixels, ParseXMP::Value(document, "GPano:CroppedAreaTopPixels", GPano.CroppedAreaTopPixels)); // 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); + SetFieldIf(FIELD_ID_MicroVideo_HasMicroVideo, ParseXMP::Value(document, "GCamera:MicroVideo", MicroVideo.HasMicroVideo)); + SetFieldIf(FIELD_ID_MicroVideo_MicroVideoVersion, ParseXMP::Value(document, "GCamera:MicroVideoVersion", MicroVideo.MicroVideoVersion)); + SetFieldIf(FIELD_ID_MicroVideo_MicroVideoOffset, ParseXMP::Value(document, "GCamera:MicroVideoOffset", MicroVideo.MicroVideoOffset)); + } + // parse GCamera:MotionPhoto, the container format that supersedes GCamera:MicroVideo; + // deliberately not an "else" of the block above: the two write to disjoint fields, so a + // transitional file declaring both attributes reports both instead of losing one of them, + // and neither can overwrite the other's data + if (document->Attribute("GCamera:MotionPhoto")) { + SetFieldIf(FIELD_ID_MicroVideo_HasMotionPhoto, ParseXMP::Value(document, "GCamera:MotionPhoto", MicroVideo.HasMotionPhoto)); + // the container gives the video item's *length*; it is deliberately not stored in + // MicroVideoOffset, which is an offset from the end of the file - a different + // quantity as soon as the container lists any item after the video. + // it is only walked when the file actually claims a motion photo: one saying + // GCamera:MotionPhoto="0" while carrying a container for something else (an Ultra HDR + // gain map, say) must not come back with the payload fields filled in, or + // HasMotionPhoto would no longer tell "no motion photo" from "length unknown" + if (MicroVideo.HasMotionPhoto) { + bool hasLength(false); + if (SetFieldIf(FIELD_ID_MicroVideo_MotionPhotoMime, ParseXMP::VideoItem(document, MicroVideo.MotionPhotoMime, MicroVideo.MotionPhotoLength, hasLength))) + SetFieldIf(FIELD_ID_MicroVideo_MotionPhotoLength, hasLength); + } } 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 || @@ -1188,8 +1551,8 @@ void EXIFInfo::Geolocation_t::parseCoords() { } // Convert GPS altitude if (hasAltitude() && - AltitudeRef == 1) { - Altitude = -Altitude; + (AltitudeRef == 1 || AltitudeRef == 3)) { + Altitude = -std::abs(Altitude); } } @@ -1208,6 +1571,9 @@ bool EXIFInfo::Geolocation_t::hasOrientation() const { 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; @@ -1217,9 +1583,45 @@ bool EXIFInfo::GPano_t::hasPoseRollDegrees() const { return PoseRollDegrees != DBL_MAX; } +bool EXIFInfo::GPano_t::hasPoseHeadingDegrees() const { + return PoseHeadingDegrees != DBL_MAX; +} + +bool EXIFInfo::GPano_t::hasCroppedAreaImageWidthPixels() const { + return CroppedAreaImageWidthPixels != UINT32_MAX; +} + +bool EXIFInfo::GPano_t::hasCroppedAreaImageHeightPixels() const { + return CroppedAreaImageHeightPixels != UINT32_MAX; +} + +bool EXIFInfo::GPano_t::hasFullPanoWidthPixels() const { + return FullPanoWidthPixels != UINT32_MAX; +} + +bool EXIFInfo::GPano_t::hasFullPanoHeightPixels() const { + return FullPanoHeightPixels != UINT32_MAX; +} + +bool EXIFInfo::GPano_t::hasCroppedAreaLeftPixels() const { + return CroppedAreaLeftPixels != UINT32_MAX; +} + +bool EXIFInfo::GPano_t::hasCroppedAreaTopPixels() const { + return CroppedAreaTopPixels != UINT32_MAX; +} + +bool EXIFInfo::GPano_t::isEquirectangular() const { + return 0 == strcasecmp(ProjectionType.c_str(), "equirectangular") || + 0 == strcasecmp(ProjectionType.c_str(), "spherical"); +} + void EXIFInfo::clear() { Fields = FIELD_NA; + // Presence bits: nothing was found yet + FieldsPresent.assign(FIELD_ID_WORDS, 0); + // Strings ImageDescription = ""; Make = ""; @@ -1304,14 +1706,33 @@ void EXIFInfo::clear() { 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; + GPano.PoseHeadingDegrees = DBL_MAX; + GPano.ProjectionType = ""; + GPano.CroppedAreaImageWidthPixels = UINT32_MAX; + GPano.CroppedAreaImageHeightPixels = UINT32_MAX; + GPano.FullPanoWidthPixels = UINT32_MAX; + GPano.FullPanoHeightPixels = UINT32_MAX; + GPano.CroppedAreaLeftPixels = UINT32_MAX; + GPano.CroppedAreaTopPixels = UINT32_MAX; // Video metadata MicroVideo.HasMicroVideo = 0; MicroVideo.MicroVideoVersion = 0; MicroVideo.MicroVideoOffset = 0; + MicroVideo.HasMotionPhoto = 0; + MicroVideo.MotionPhotoLength = 0; + MicroVideo.MotionPhotoMime = ""; } } // namespace TinyEXIF diff --git a/lib/TinyEXIF/TinyEXIF.h b/lib/TinyEXIF/TinyEXIF.h index b03d36b..cc4da63 100644 --- a/lib/TinyEXIF/TinyEXIF.h +++ b/lib/TinyEXIF/TinyEXIF.h @@ -2,33 +2,9 @@ TinyEXIF.h -- A simple ISO C++ library to parse basic EXIF and XMP information from a JPEG file. - Copyright (c) 2015-2017 Seacave + Copyright (c) 2015-2025 Seacave cdc.seacave@gmail.com - All rights reserved. - - Based on the easyexif library (2013 version) - https://github.com/mayanklahiri/easyexif - of Mayank Lahiri (mlahiri@gmail.com). - - Redistribution and use in source and binary forms, with or without - modification, are permitted provided that the following conditions are met: - - - Redistributions of source code must retain the above copyright notice, - this list of conditions and the following disclaimer. - - Redistributions in binary form must reproduce the above copyright notice, - this list of conditions and the following disclaimer in the documentation - and/or other materials provided with the distribution. - - THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS ``AS IS'' AND ANY EXPRESS - OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES - OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN - NO EVENT SHALL THE FREEBSD PROJECT OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, - INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, - BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, - DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY - OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING - NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, - EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + MIT License */ #ifndef __TINYEXIF_H__ @@ -39,8 +15,28 @@ #include #define TINYEXIF_MAJOR_VERSION 1 -#define TINYEXIF_MINOR_VERSION 0 -#define TINYEXIF_PATCH_VERSION 1 +#define TINYEXIF_MINOR_VERSION 1 +#define TINYEXIF_PATCH_VERSION 0 + +// TINYEXIF_VERSION_STRING and TINYEXIF_VERSION are derived from the three +// macros above -- change only MAJOR/MINOR/PATCH above, never these directly. +// CMakeLists.txt also reads MAJOR/MINOR/PATCH out of this header, so it is +// the single source of truth for the library version. +#define TINYEXIF_STRINGIZE_(x) #x +#define TINYEXIF_STRINGIZE(x) TINYEXIF_STRINGIZE_(x) +#define TINYEXIF_VERSION_STRING \ + TINYEXIF_STRINGIZE(TINYEXIF_MAJOR_VERSION) "." \ + TINYEXIF_STRINGIZE(TINYEXIF_MINOR_VERSION) "." \ + TINYEXIF_STRINGIZE(TINYEXIF_PATCH_VERSION) + +// Numeric version for #if comparisons (macro values, not strings, are +// required in preprocessor conditions). Encodes MAJOR/MINOR/PATCH as +// MAJOR*10000 + MINOR*100 + PATCH, leaving room for two-digit MINOR/PATCH +// components. Example: gate use of an API added in 1.1.0 like this: +// #if TINYEXIF_VERSION >= 10100 +// // use TinyEXIF::EXIFInfo::HasField(), added in 1.1.0 +// #endif +#define TINYEXIF_VERSION (TINYEXIF_MAJOR_VERSION*10000 + TINYEXIF_MINOR_VERSION*100 + TINYEXIF_PATCH_VERSION) #ifdef _MSC_VER # ifdef TINYEXIF_EXPORT @@ -73,6 +69,121 @@ enum FieldCode { FIELD_ALL = FIELD_EXIF|FIELD_XMP }; +// Identifies one data field of EXIFInfo, for HasField()/GetFields(); it tells +// "tag absent" apart from "tag present and legitimately zero", which the value +// of a zero-initialized field alone can not. +// There is one enumerator per destination field, named FIELD_ID_ after the +// member it fills, not after the EXIF tag number: several EXIF tags and their +// XMP equivalents write the same member, and either source setting it counts +// as present. Note that FieldCode above (FIELD_EXIF/FIELD_XMP) is a different +// thing: it says which segment was found, not which tags were parsed. +// New enumerators are appended before FIELD_ID_COUNT, so the numeric value of +// an existing one never changes. +enum FieldID { + // EXIFInfo + FIELD_ID_ImageWidth = 0, + FIELD_ID_ImageHeight, + FIELD_ID_RelatedImageWidth, + FIELD_ID_RelatedImageHeight, + FIELD_ID_ImageDescription, + FIELD_ID_Make, + FIELD_ID_Model, + FIELD_ID_SerialNumber, + FIELD_ID_Orientation, + FIELD_ID_XResolution, + FIELD_ID_YResolution, + FIELD_ID_ResolutionUnit, + FIELD_ID_BitsPerSample, + FIELD_ID_Software, + FIELD_ID_DateTime, + FIELD_ID_DateTimeOriginal, + FIELD_ID_DateTimeDigitized, + FIELD_ID_SubSecTimeOriginal, + FIELD_ID_Copyright, + FIELD_ID_ExposureTime, + FIELD_ID_FNumber, + FIELD_ID_ExposureProgram, + FIELD_ID_ISOSpeedRatings, // also set by the ExposureIndex tag, used interchangeably + FIELD_ID_ShutterSpeedValue, + FIELD_ID_ApertureValue, + FIELD_ID_BrightnessValue, + FIELD_ID_ExposureBiasValue, + FIELD_ID_SubjectDistance, + FIELD_ID_FocalLength, + FIELD_ID_Flash, + FIELD_ID_MeteringMode, + FIELD_ID_LightSource, + FIELD_ID_ProjectionType, // the numeric value derived from GPano:ProjectionType + FIELD_ID_SubjectArea, // set if at least one of its components was read + // EXIFInfo::Calibration + FIELD_ID_Calibration_FocalLength, + FIELD_ID_Calibration_OpticalCenterX, + FIELD_ID_Calibration_OpticalCenterY, + // EXIFInfo::Distortion + FIELD_ID_Distortion_DewarpFlag, + FIELD_ID_Distortion_K1, + FIELD_ID_Distortion_K2, + FIELD_ID_Distortion_P1, + FIELD_ID_Distortion_P2, + FIELD_ID_Distortion_K3, + // EXIFInfo::LensInfo + FIELD_ID_LensInfo_FStopMin, + FIELD_ID_LensInfo_FStopMax, + FIELD_ID_LensInfo_FocalLengthMin, + FIELD_ID_LensInfo_FocalLengthMax, + FIELD_ID_LensInfo_DigitalZoomRatio, + FIELD_ID_LensInfo_FocalLengthIn35mm, + FIELD_ID_LensInfo_FocalPlaneXResolution, + FIELD_ID_LensInfo_FocalPlaneYResolution, + FIELD_ID_LensInfo_FocalPlaneResolutionUnit, + FIELD_ID_LensInfo_Make, + FIELD_ID_LensInfo_Model, + // EXIFInfo::GeoLocation + FIELD_ID_GeoLocation_Latitude, // set by the GPSLatitude components feeding LatComponents + FIELD_ID_GeoLocation_Longitude, // set by the GPSLongitude components feeding LonComponents + FIELD_ID_GeoLocation_Altitude, + FIELD_ID_GeoLocation_AltitudeRef, + FIELD_ID_GeoLocation_RelativeAltitude, + FIELD_ID_GeoLocation_RollDegree, + FIELD_ID_GeoLocation_PitchDegree, + FIELD_ID_GeoLocation_YawDegree, + FIELD_ID_GeoLocation_SpeedX, + FIELD_ID_GeoLocation_SpeedY, + FIELD_ID_GeoLocation_SpeedZ, + FIELD_ID_GeoLocation_AccuracyXY, + FIELD_ID_GeoLocation_AccuracyZ, + FIELD_ID_GeoLocation_GPSDOP, + FIELD_ID_GeoLocation_GPSDifferential, + FIELD_ID_GeoLocation_GPSMapDatum, + FIELD_ID_GeoLocation_GPSTimeStamp, + FIELD_ID_GeoLocation_GPSDateStamp, + FIELD_ID_GeoLocation_LatComponents_direction,// GPSLatitudeRef, the N/S hemisphere of Latitude + FIELD_ID_GeoLocation_LonComponents_direction,// GPSLongitudeRef, the E/W hemisphere of Longitude + // EXIFInfo::GPano + FIELD_ID_GPano_PosePitchDegrees, + FIELD_ID_GPano_PoseRollDegrees, + FIELD_ID_GPano_PoseHeadingDegrees, + FIELD_ID_GPano_ProjectionType, + FIELD_ID_GPano_CroppedAreaImageWidthPixels, + FIELD_ID_GPano_CroppedAreaImageHeightPixels, + FIELD_ID_GPano_FullPanoWidthPixels, + FIELD_ID_GPano_FullPanoHeightPixels, + FIELD_ID_GPano_CroppedAreaLeftPixels, + FIELD_ID_GPano_CroppedAreaTopPixels, + // EXIFInfo::MicroVideo + FIELD_ID_MicroVideo_HasMicroVideo, + FIELD_ID_MicroVideo_MicroVideoVersion, + FIELD_ID_MicroVideo_MicroVideoOffset, + FIELD_ID_MicroVideo_HasMotionPhoto, + FIELD_ID_MicroVideo_MotionPhotoLength, + FIELD_ID_MicroVideo_MotionPhotoMime, + FIELD_ID_COUNT // number of known fields; not a field itself +}; + +// Return the name of the given field as the path of the member it fills, +// e.g. "GeoLocation.Altitude"; empty string if the ID is not a known field. +TINYEXIF_LIB const char* FieldName(FieldID id); + class EntryParser; // @@ -132,9 +243,18 @@ class TINYEXIF_LIB EXIFInfo { // Should be called before parsing a new stream. void clear(); + // Return true if the given field was actually present in the parsed data; + // this is what distinguishes a tag that was absent from a tag that was + // present and legitimately zero, which the field value alone can not. + bool HasField(FieldID id) const; + // Return the ID of every field that was found, in FieldID order; + // see FieldName() to turn one into a printable name. + std::vector GetFields() const; + private: - // Parse tag as Image IFD. - void parseIFDImage(EntryParser&, unsigned&, unsigned&); + // Parse tag as Image IFD; the sub-IFD offsets are 64bit as they are read from + // attacker controlled data and must be range checked before being used. + void parseIFDImage(EntryParser&, uint64_t&, uint64_t&); // Parse tag as Exif IFD. void parseIFDExif(EntryParser&); // Parse tag as GPS IFD. @@ -142,6 +262,17 @@ class TINYEXIF_LIB EXIFInfo { // Parse tag as MakerNote IFD. void parseIFDMakerNote(EntryParser&); + // Mark the given field as present. + void SetField(FieldID id); + // Mark the given field as present if 'fetched' says its fetch succeeded, and + // return 'fetched' unchanged; every fetch site is instrumented through this, + // so it neither hides which field it marks nor alters the control flow around it. + bool SetFieldIf(FieldID id, bool fetched); + + // Presence bit per FieldID, queried by HasField(); a vector rather than a + // fixed-size array so that adding fields later does not change this layout. + std::vector FieldsPresent; + public: // Data fields uint32_t Fields; // Store if EXIF and/or XMP data fields are available @@ -251,7 +382,21 @@ class TINYEXIF_LIB EXIFInfo { double FocalLength; // Focal length (pixels) double OpticalCenterX; // Principal point X (pixels) double OpticalCenterY; // Principal point Y (pixels) + bool hasCalibration() const; // Return true if FocalLength, OpticalCenterX, OpticalCenterY are available (nonzero) } Calibration; + struct TINYEXIF_LIB Distortion_t { // Lens distortion information + uint32_t DewarpFlag; // Dewarp flag - indicates whether undistortion has been applied to the image + // UINT32_MAX: flag missing from EXIF data + // 0: no dewarp (raw image) - the image is distorted, should also have coefficients + // 1: dewarp applied (undistorted image) + double K1; + double K2; + double P1; + double P2; + double K3; + bool hasDewarpFlag() const; // Return true if DewarpFlag is available + bool hasDistortion() const; // Return true if any of K1, K2, P1, P2, K3 are available + } Distortion; struct TINYEXIF_LIB LensInfo_t { // Lens information double FStopMin; // Min aperture (f-stop) double FStopMax; // Max aperture (f-stop) @@ -273,7 +418,10 @@ class TINYEXIF_LIB EXIFInfo { double Latitude; // Image latitude expressed as decimal double Longitude; // Image longitude expressed as decimal double Altitude; // Altitude in meters, relative to sea level - int8_t AltitudeRef; // 0: above sea level, -1: below sea level + int8_t AltitudeRef; // 0: above sea level + // 1: below sea level + // 2: positive sea-level reference + // 3: negative sea-level reference double RelativeAltitude; // Relative altitude in meters double RollDegree; // Flight roll in degrees double PitchDegree; // Flight pitch in degrees @@ -302,17 +450,37 @@ class TINYEXIF_LIB EXIFInfo { bool hasRelativeAltitude()const;// Return true if (rel_alt) is available bool hasOrientation() const; // Return true if (roll,yaw,pitch) is available bool hasSpeed() const; // Return true if (speedX,speedY,speedZ) is available + bool hasAccuracy() const; // Return true if (accuracyXY,accuracyZ) is available } GeoLocation; - struct TINYEXIF_LIB GPano_t { // Spherical metadata. https://developers.google.com/streetview/spherical-metadata + struct TINYEXIF_LIB GPano_t { // Spherical metadata. https://developers.google.com/streetview/spherical-metadata double PosePitchDegrees; // Pitch, measured in degrees above the horizon, for the center in the image. Value must be >= -90 and <= 90. double PoseRollDegrees; // Roll, measured in degrees, of the image where level with the horizon is 0. As roll increases, the horizon rotates counterclockwise in the image. Value must be > -180 and <= 180. + double PoseHeadingDegrees; // Compass heading, measured in degrees, for the center in the image. Value must be >= 0 and < 360. + std::string ProjectionType; // Type of image projection, e.g. "equirectangular" (raw spec string; see also EXIFInfo::ProjectionType for the derived numeric value) + uint32_t CroppedAreaImageWidthPixels;// Width of the image after cropping to the panoramic field-of-view + uint32_t CroppedAreaImageHeightPixels;// Height of the image after cropping to the panoramic field-of-view + uint32_t FullPanoWidthPixels; // Width of the full panorama the cropped image represents (may exceed CroppedAreaImageWidthPixels) + uint32_t FullPanoHeightPixels; // Height of the full panorama the cropped image represents (may exceed CroppedAreaImageHeightPixels) + uint32_t CroppedAreaLeftPixels; // Column where the left edge of the cropped image was cropped from the full panorama + uint32_t CroppedAreaTopPixels; // Row where the top edge of the cropped image was cropped from the full panorama bool hasPosePitchDegrees() const; // Return true if PosePitchDegrees is available bool hasPoseRollDegrees() const; // Return true if PoseRollDegrees is available + bool hasPoseHeadingDegrees() const; // Return true if PoseHeadingDegrees is available + bool hasCroppedAreaImageWidthPixels() const; // Return true if CroppedAreaImageWidthPixels is available + bool hasCroppedAreaImageHeightPixels() const; // Return true if CroppedAreaImageHeightPixels is available + bool hasFullPanoWidthPixels() const; // Return true if FullPanoWidthPixels is available + bool hasFullPanoHeightPixels() const; // Return true if FullPanoHeightPixels is available + bool hasCroppedAreaLeftPixels() const; // Return true if CroppedAreaLeftPixels is available + bool hasCroppedAreaTopPixels() const; // Return true if CroppedAreaTopPixels is available + bool isEquirectangular() const; // Return true if ProjectionType is "equirectangular" or "spherical" (case-insensitive) } GPano; - struct TINYEXIF_LIB MicroVideo_t { // Google camera video file in metadata + struct TINYEXIF_LIB MicroVideo_t { // Google camera video file in metadata uint32_t HasMicroVideo; // not zero if exists uint32_t MicroVideoVersion; // just regularinfo - uint32_t MicroVideoOffset; // offset from end of file + uint32_t MicroVideoOffset; // legacy GCamera:MicroVideo - offset from end of file + uint32_t HasMotionPhoto; // GCamera:MotionPhoto - not zero if exists (newer container format, supersedes GCamera:MicroVideo) + uint32_t MotionPhotoLength; // GCamera:MotionPhoto - length in bytes of the trailing video item, saturated at UINT32_MAX; a length, not an offset: it differs from MicroVideoOffset as soon as the container holds items after the video + std::string MotionPhotoMime; // GCamera:MotionPhoto - mime type of the trailing video item, e.g. "video/mp4" (empty if the container declared none) } MicroVideo; }; diff --git a/lib/kodi-TinyEXIF-note.txt b/lib/kodi-TinyEXIF-note.txt index dbaf35e..b2b9067 100644 --- a/lib/kodi-TinyEXIF-note.txt +++ b/lib/kodi-TinyEXIF-note.txt @@ -1,4 +1,4 @@ Source from https://github.com/cdcseacave/TinyEXIF -Sync to 6e56015 (10 Apr. 2021) +Sync to 8c22aff (23 Aug. 2026) Used to read EXIF information from HEIF pictures.