From 33a19e17de9ae83fc8e025d95fe156d6a92c9b7b Mon Sep 17 00:00:00 2001 From: cinemaONE <35371193+cinema-ONE@users.noreply.github.com> Date: Sun, 30 Aug 2026 08:15:36 +0200 Subject: [PATCH] Update vendored TinyEXIF to 8c22aff The copy here was pinned at 6e56015 (April 2021) and is missing two upstream memory-safety fixes: 841e2929, a heap buffer overflow in EntryParser::Fetch, and b6ea1b7b, an integer overflow in the parseString bounds check. The first is reachable. Fuzzing the pinned copy under ASan segfaulted after 18 executions: EntryParser::parse32 TinyEXIF.cpp:283 EntryParser::parseRational TinyEXIF.cpp:297 EntryParser::Fetch(double&, unsigned) TinyEXIF.cpp:256 EXIFInfo::parseIFDExif TinyEXIF.cpp:615 which is exactly the function 841e2929 adds a bounds check to. EXIF parsing runs whenever Kodi reads tags for a picture, so the input is attacker controlled. The upstream proof-of-concept samples do not reproduce it - the older parser rejects them as corrupt before reaching that code - so this was found by fuzzing the pinned version rather than by replaying published cases. After the update that input parses cleanly and 3.3M fuzz executions found nothing. Only parseFrom(), parseFromEXIFSegment(), ImageWidth, ImageHeight and Orientation are used here, all unchanged upstream. Co-Authored-By: Claude Opus 5 --- lib/TinyEXIF/README.md | 67 ++- lib/TinyEXIF/TinyEXIF.cpp | 821 ++++++++++++++++++++++++++++--------- lib/TinyEXIF/TinyEXIF.h | 236 +++++++++-- lib/kodi-TinyEXIF-note.txt | 2 +- 4 files changed, 870 insertions(+), 256 deletions(-) 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.