15 Commits
1.0.0 ... 1.0.2

Author SHA1 Message Date
cDc
c57a5fec1a fix tinyxml2 namespace 2019-01-16 15:26:30 +02:00
cDc
5bbfe8f70c add Sentera camera support to XMP 2019-01-11 15:28:25 +02:00
cDc
cb40ffa537 add test images 2019-01-11 15:24:23 +02:00
cDc
405b8a1693 parse XMP stored inside EXIF 2018-10-09 12:20:43 +03:00
cDc
22ba2704c9 parse XMP for more DJI cameras 2018-09-24 13:18:40 +03:00
cDc
8b383a2809 fix rational/s-rational EXIF parsing 2018-09-24 13:18:05 +03:00
cDc
ac39905f11 add maker check for DJI too 2018-09-24 12:33:57 +03:00
cDc
98dc256e0a add PARROT camera support to XMP 2018-08-15 13:44:36 +03:00
cDc
e9c19435c3 parse GPS accuracy 2018-07-18 15:41:35 +03:00
cDc
04969097a4 add MakerNote support for DJI 2018-07-10 10:00:24 +03:00
cDc
8e3316cb41 extract calibration information 2018-07-04 18:30:26 +03:00
cDc
c02203795b add senseFly support 2018-07-04 18:29:53 +03:00
cDc
87c343b4f1 fix linux compile and increase version 2017-12-09 14:53:52 +02:00
cDc
af81c87222 add input stream interface (with override ex. for memory and file input) 2017-12-09 13:38:32 +02:00
cDc
36f7056799 try recover missing EXIF filds from XMP 2017-12-09 11:59:09 +02:00
39 changed files with 577 additions and 204 deletions

40
.gitignore vendored Normal file
View File

@@ -0,0 +1,40 @@
# Compiled Object files
*.slo
*.lo
*.o
*.obj
# Precompiled Headers
*.gch
*.pch
# Compiled Dynamic libraries
*.so
*.dylib
*.dll
# Fortran module files
*.mod
# Compiled Static libraries
*.lai
*.la
*.a
*.lib
# Executables
*.exe
*.out
*.app
# Custom
*.tmp
*.exif.txt
*.exif.txt.txt
.DS_Store
CMakeSettings.json
.vs/
.idea/
.vscode/
bin/
binaries/

View File

@@ -1,8 +1,4 @@
cmake_minimum_required(VERSION 2.6)
cmake_policy(VERSION 2.6)
if(POLICY CMP0063)
cmake_policy(SET CMP0063 OLD)
endif()
cmake_minimum_required(VERSION 3.1)
project(TinyEXIF)
include(GNUInstallDirs)
@@ -14,12 +10,13 @@ find_package(tinyxml2 REQUIRED)
################################
# set lib version here
set(GENERIC_LIB_VERSION "1.0.0")
set(GENERIC_LIB_VERSION "1.0.1")
set(GENERIC_LIB_SOVERSION "1")
################################
# Add definitions
set(CMAKE_CXX_STANDARD 11)
set(CMAKE_CXX_FLAGS_DEBUG "${CMAKE_CXX_FLAGS_DEBUG} -DDEBUG")
################################
@@ -87,7 +84,7 @@ if(BUILD_SHARED_LIBS)
set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} /wd4251") # needs to have dll-interface
endif()
target_link_libraries(TinyEXIF tinyxml2)
target_link_libraries(TinyEXIF tinyxml2::tinyxml2)
set_target_properties(TinyEXIF PROPERTIES
COMPILE_DEFINITIONS "TINYEXIF_EXPORT"
VERSION "${GENERIC_LIB_VERSION}"
@@ -124,7 +121,7 @@ endif()
if(BUILD_STATIC_LIBS)
add_library(TinyEXIFstatic STATIC TinyEXIF.cpp TinyEXIF.h)
target_link_libraries(TinyEXIFstatic tinyxml2)
target_link_libraries(TinyEXIFstatic tinyxml2::tinyxml2)
set_target_properties(TinyEXIFstatic PROPERTIES
OUTPUT_NAME TinyEXIF
VERSION "${GENERIC_LIB_VERSION}"
@@ -165,7 +162,7 @@ if(BUILD_DEMO)
target_compile_definitions(TinyEXIFdemo PRIVATE TINYEXIF_IMPORT)
else(BUILD_STATIC_LIBS)
add_dependencies(TinyEXIFdemo TinyEXIFstatic)
target_link_libraries(TinyEXIFdemo TinyEXIFstatic tinyxml2)
target_link_libraries(TinyEXIFdemo TinyEXIFstatic tinyxml2::tinyxml2)
endif()
endif()

View File

@@ -38,3 +38,25 @@ 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.

Binary file not shown.

After

Width:  |  Height:  |  Size: 693 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 194 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 404 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 289 KiB

BIN
Samples/1103806289718.jpg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 245 KiB

BIN
Samples/20160108-162501.jpg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 116 KiB

BIN
Samples/ARTstorXMP.jpg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 137 KiB

BIN
Samples/Anafi.jpg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 707 KiB

BIN
Samples/Bebop_2.jpg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 220 KiB

BIN
Samples/Bebop_2_1.jpg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 204 KiB

BIN
Samples/GettyVilla0001.jpg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 404 KiB

BIN
Samples/IPTCpanel.jpg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 85 KiB

BIN
Samples/VRAexample012.jpg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 118 KiB

BIN
Samples/bb-android.jpg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 12 KiB

BIN
Samples/calib.jpg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 259 KiB

BIN
Samples/crosa.jpg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 230 KiB

BIN
Samples/down-mirrored.jpg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 15 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 355 KiB

BIN
Samples/evil1.jpg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 84 KiB

BIN
Samples/example005.jpg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 154 KiB

BIN
Samples/lens_info.jpg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 897 B

BIN
Samples/lukas12p.jpg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 157 KiB

BIN
Samples/ok.jpg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 123 KiB

BIN
Samples/problem.jpg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 967 KiB

BIN
Samples/right.jpg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.8 KiB

BIN
Samples/sensefly.jpg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 298 KiB

BIN
Samples/short-ascii-II.jpg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 872 KiB

BIN
Samples/short-ascii-MM.jpg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 31 KiB

BIN
Samples/sony-alpha-6000.jpg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 457 KiB

BIN
Samples/test1.jpg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 27 KiB

BIN
Samples/test2.jpg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 11 KiB

BIN
Samples/test3.jpg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 26 KiB

18
TestSamples.py Normal file
View File

@@ -0,0 +1,18 @@
import os
import sys
import subprocess
source = os.path.join(os.path.dirname(sys.argv[0]), 'Samples')
TinyEXIF = os.path.join(source, 'TinyEXIF')
exiftool = 'exiftool'
ext = 'exif.txt'
if len(sys.argv) > 1:
ext = sys.argv[1]
for root, dirs, filenames in os.walk(source):
for f in filenames:
if f[-4:].lower() == '.jpg':
fullpath = os.path.join(source, f)
imgexif = fullpath[:-3] + ext
print('parse ' + fullpath + ' to ' + imgexif)
subprocess.Popen(TinyEXIF + ' ' + fullpath + ' > ' + imgexif, shell=True)
subprocess.Popen(exiftool + ' -n -s -G ' + fullpath + ' > ' + imgexif + '.txt', shell=True)

View File

@@ -13,9 +13,9 @@
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,
- 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,
- 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.
@@ -49,6 +49,43 @@
#endif
namespace Tools {
// search string inside a string, case sensitive
static const char* strrnstr(const char* haystack, const char* needle, size_t len) {
const size_t needle_len(strlen(needle));
if (0 == needle_len)
return haystack;
if (len <= needle_len)
return NULL;
for (size_t i=len-needle_len; i-- > 0; ) {
if (haystack[0] == needle[0] &&
0 == _tcsncmp(haystack, needle, needle_len))
return haystack;
haystack++;
}
return NULL;
}
// split an input string with a delimiter and fill a string vector
static void strSplit(const std::string& str, char delim, std::vector<std::string>& values) {
values.clear();
std::string::size_type start(0), end(0);
while (end != std::string::npos) {
end = str.find(delim, start);
values.emplace_back(str.substr(start, end-start));
start = end + 1;
}
}
// make sure the given degrees value is between -180 and 180
static double NormD180(double d) {
return (d = fmod(d+180.0, 360.0)) < 0 ? d+180.0 : d-180.0;
}
} // namespace Tools
namespace TinyEXIF {
enum JPEG_MARKERS {
@@ -145,6 +182,10 @@ public:
length = parse32(buf + offs + 4, alignIntel);
}
const uint8_t* GetBuffer() const { return buf; }
unsigned GetOffset() const { return offs; }
bool IsIntelAligned() const { return alignIntel; }
uint16_t GetTag() const { return tag; }
uint32_t GetLength() const { return length; }
uint32_t GetData() const { return parse32(buf + offs + 8, alignIntel); }
@@ -153,11 +194,17 @@ public:
bool IsShort() const { return format == 3; }
bool IsLong() const { return format == 4; }
bool IsRational() const { return format == 5 || format == 10; }
bool IsSRational() const { return format == 10; }
bool IsFloat() const { return format == 11; }
bool IsUndefined() const { return format == 7; }
std::string FetchString() const {
return parseString(buf, length, GetData(), tiff_header_start, len, alignIntel);
}
bool Fetch(std::string& val) const {
if (format != 2 || length == 0)
return false;
val = parseEXIFString(buf, length, GetData(), tiff_header_start, len, alignIntel);
val = FetchString();
return true;
}
bool Fetch(uint8_t& val) const {
@@ -184,16 +231,30 @@ public:
val = parse32(buf + offs + 8, alignIntel);
return true;
}
bool Fetch(float& val) const {
if (!IsFloat() || length == 0)
return false;
val = parseFloat(buf + offs + 8, alignIntel);
return true;
}
bool Fetch(double& val) const {
if (!IsRational() || length == 0)
return false;
val = parseEXIFRational(buf + GetSubIFD(), alignIntel);
val = parseRational(buf + GetSubIFD(), alignIntel, IsSRational());
return true;
}
bool Fetch(double& val, uint32_t idx) const {
if (!IsRational() || length <= idx)
return false;
val = parseEXIFRational(buf + GetSubIFD() + idx*8, alignIntel);
val = parseRational(buf + GetSubIFD() + idx*8, alignIntel, IsSRational());
return true;
}
bool FetchFloat(double& val) const {
float _val;
if (!Fetch(_val))
return false;
val = _val;
return true;
}
@@ -217,14 +278,24 @@ public:
((uint32_t)buf[2]<<8) |
buf[3];
}
static double parseEXIFRational(const uint8_t* buf, bool intel) {
static float parseFloat(const uint8_t* buf, bool intel) {
union {
uint32_t i;
float f;
} i2f;
i2f.i = parse32(buf, intel);
return i2f.f;
}
static double parseRational(const uint8_t* buf, bool intel, bool isSigned) {
const uint32_t denominator = parse32(buf+4, intel);
if (denominator == 0)
return 0.0;
const uint32_t numerator = parse32(buf, intel);
return (double)(int32_t)numerator/(double)(int32_t)denominator;
return isSigned ?
(double)(int32_t)numerator/(double)(int32_t)denominator :
(double)numerator/(double)denominator;
}
static std::string parseEXIFString(const uint8_t* buf,
static std::string parseString(const uint8_t* buf,
unsigned num_components,
unsigned data,
unsigned base,
@@ -258,12 +329,12 @@ public:
// Constructors
EXIFInfo::EXIFInfo() : Fields(FIELD_NA) {
}
EXIFInfo::EXIFInfo(EXIFStream& stream) {
parseFrom(stream);
}
EXIFInfo::EXIFInfo(const uint8_t* data, unsigned length) {
parseFrom(data, length);
}
EXIFInfo::EXIFInfo(const std::string& data) {
parseFrom(data);
}
// Parse tag as Image IFD
@@ -362,6 +433,14 @@ void EXIFInfo::parseIFDImage(EntryParser& parser, unsigned& exif_sub_ifd_offset,
// Parse tag as Exif IFD
void EXIFInfo::parseIFDExif(EntryParser& parser) {
switch (parser.GetTag()) {
case 0x02bc:
// XMP Metadata (Adobe technote 9-14-02)
if (parser.IsUndefined()) {
const std::string strXML(parser.FetchString());
parseFromXMPSegmentXML(strXML.c_str(), (unsigned)strXML.length());
}
break;
case 0x829a:
// Exposure time in seconds
parser.Fetch(ExposureTime);
@@ -448,6 +527,11 @@ void EXIFInfo::parseIFDExif(EntryParser& parser) {
}
break;
case 0x927c:
// MakerNote
parseIFDMakerNote(parser);
break;
case 0x9291:
// Fractions of seconds for DateTimeOriginal
parser.Fetch(SubSecTimeOriginal);
@@ -534,6 +618,60 @@ void EXIFInfo::parseIFDExif(EntryParser& parser) {
}
}
// Parse tag as MakerNote IFD
void EXIFInfo::parseIFDMakerNote(EntryParser& parser) {
const unsigned startOff = parser.GetOffset();
const uint32_t off = parser.GetSubIFD();
if (0 != _tcsicmp(Make.c_str(), "DJI"))
return;
int num_entries = EntryParser::parse16(parser.GetBuffer()+off, parser.IsIntelAligned());
if (uint32_t(2 + 12 * num_entries) > parser.GetLength())
return;
parser.Init(off+2);
parser.ParseTag();
--num_entries;
std::string maker;
if (parser.GetTag() == 1 && parser.Fetch(maker)) {
if (0 == _tcsicmp(maker.c_str(), "DJI")) {
while (--num_entries >= 0) {
parser.ParseTag();
switch (parser.GetTag()) {
case 3:
// SpeedX
parser.FetchFloat(GeoLocation.SpeedX);
break;
case 4:
// SpeedY
parser.FetchFloat(GeoLocation.SpeedY);
break;
case 5:
// SpeedZ
parser.FetchFloat(GeoLocation.SpeedZ);
break;
case 9:
// Camera Pitch
parser.FetchFloat(GeoLocation.PitchDegree);
break;
case 10:
// Camera Yaw
parser.FetchFloat(GeoLocation.YawDegree);
break;
case 11:
// Camera Roll
parser.FetchFloat(GeoLocation.RollDegree);
break;
}
}
}
}
parser.Init(startOff+12);
}
// Parse tag as GPS IFD
void EXIFInfo::parseIFDGPS(EntryParser& parser) {
switch (parser.GetTag()) {
@@ -615,18 +753,16 @@ void EXIFInfo::parseIFDGPS(EntryParser& parser) {
// Locates the JM_APP1 segment and parses it using
// parseFromEXIFSegment() or parseFromXMPSegment()
//
int EXIFInfo::parseFrom(const uint8_t* buf, unsigned len) {
int EXIFInfo::parseFrom(EXIFStream& stream) {
clear();
if (!stream.IsValid())
return PARSE_INVALID_JPEG;
// Sanity check: all JPEG files start with 0xFFD8 and end with 0xFFD9
// This check also ensures that the user has supplied a correct value for len.
if (!buf || len < 16)
const uint8_t* buf(stream.GetBuffer(2));
if (buf == NULL || buf[0] != JM_START || buf[1] != JM_SOI)
return PARSE_INVALID_JPEG;
if (buf[0] != JM_START || buf[1] != JM_SOI)
return PARSE_INVALID_JPEG;
// not always valid, sometimes 0xFF is added for padding
//if (buf[len-2] != JM_START || buf[len-1] != JM_EOI)
// return PARSE_INVALID_JPEG;
// Scan for JM_APP1 header (bytes 0xFF 0xE1) and parse its length.
// Exit if both EXIF and XMP sections were parsed.
@@ -637,19 +773,20 @@ int EXIFInfo::parseFrom(const uint8_t* buf, unsigned len) {
inline operator uint32_t& () { return val; }
inline int operator () (int code=PARSE_ABSENT_DATA) const { return val&FIELD_ALL ? (int)PARSE_SUCCESS : code; }
} app1s(Fields);
for (unsigned pos=2; pos<len; ) {
// find next marker
uint8_t marker, prev(0);
do {
marker = buf[pos++];
if (marker != JM_START && prev == JM_START)
while ((buf=stream.GetBuffer(2)) != NULL) {
// find next marker;
// in cases of markers appended after the compressed data,
// optional JM_START fill bytes may precede the marker
if (*buf++ != JM_START)
break;
prev = marker;
} while (pos<len);
uint8_t marker;
while ((marker=buf[0]) == JM_START && (buf=stream.GetBuffer(1)) != NULL);
// select marker
uint16_t sectionLength;
switch (marker) {
case 0x00:
case 0x01:
case JM_START:
case JM_RST0:
case JM_RST1:
case JM_RST2:
@@ -663,12 +800,15 @@ int EXIFInfo::parseFrom(const uint8_t* buf, unsigned len) {
case JM_SOS: // start of stream: and we're done
case JM_EOI: // no data? not good
return app1s();
case JM_APP1: {
const uint16_t section_length(EntryParser::parse16(buf + pos, false));
int ret;
switch (ret=parseFromEXIFSegment(buf + pos + 2, section_length - 2)) {
case JM_APP1:
if ((buf=stream.GetBuffer(2)) == NULL)
return app1s(PARSE_INVALID_JPEG);
sectionLength = EntryParser::parse16(buf, false);
if (sectionLength <= 2 || (buf=stream.GetBuffer(sectionLength-=2)) == NULL)
return app1s(PARSE_INVALID_JPEG);
switch (int ret=parseFromEXIFSegment(buf, sectionLength)) {
case PARSE_ABSENT_DATA:
switch (ret=parseFromXMPSegment(buf + pos + 2, section_length - 2)) {
switch (ret=parseFromXMPSegment(buf, sectionLength)) {
case PARSE_ABSENT_DATA:
break;
case PARSE_SUCCESS:
@@ -686,22 +826,42 @@ int EXIFInfo::parseFrom(const uint8_t* buf, unsigned len) {
default:
return app1s(ret); // some error
}
}
default: {
// read section length
const uint16_t section_length(EntryParser::parse16(buf + pos, false));
if (pos + section_length > len)
return app1s(PARSE_INVALID_JPEG);
break;
default:
// skip the section
pos += section_length;
}
if ((buf=stream.GetBuffer(2)) == NULL ||
(sectionLength=EntryParser::parse16(buf, false)) <= 2 ||
!stream.SkipBuffer(sectionLength-2))
return app1s(PARSE_INVALID_JPEG);
}
}
return app1s();
}
int EXIFInfo::parseFrom(const std::string& data) {
return parseFrom((const uint8_t*)data.data(), (unsigned)data.length());
int EXIFInfo::parseFrom(const uint8_t* buf, unsigned len) {
class EXIFStreamBuffer : public EXIFStream {
public:
explicit EXIFStreamBuffer(const uint8_t* buf, unsigned len)
: it(buf), end(buf+len) {}
bool IsValid() const override {
return it != NULL;
}
const uint8_t* GetBuffer(unsigned desiredLength) override {
const uint8_t* const itNext(it+desiredLength);
if (itNext >= end)
return NULL;
const uint8_t* const begin(it);
it = itNext;
return begin;
}
bool SkipBuffer(unsigned desiredLength) override {
return GetBuffer(desiredLength) != NULL;
}
private:
const uint8_t* it, * const end;
};
EXIFStreamBuffer stream(buf, len);
return parseFrom(stream);
}
//
@@ -820,23 +980,6 @@ int EXIFInfo::parseFromEXIFSegment(const uint8_t* buf, unsigned len) {
// PARAM: 'len' length of buffer
//
int EXIFInfo::parseFromXMPSegment(const uint8_t* buf, unsigned len) {
struct Tools {
static const char* strrnstr(const char* haystack, const char* needle, size_t len) {
const size_t needle_len(strlen(needle));
if (0 == needle_len)
return haystack;
if (len <= needle_len)
return NULL;
for (size_t i=len-needle_len; i-- > 0; ) {
if (haystack[0] == needle[0] &&
0 == _tcsncmp(haystack, needle, needle_len))
return haystack;
haystack++;
}
return NULL;
}
};
unsigned offs = 29; // current offset into buffer
if (!buf || len < offs)
return PARSE_ABSENT_DATA;
@@ -844,23 +987,43 @@ int EXIFInfo::parseFromXMPSegment(const uint8_t* buf, unsigned len) {
return PARSE_ABSENT_DATA;
if (offs >= len)
return PARSE_CORRUPT_DATA;
len -= offs;
return parseFromXMPSegmentXML((const char*)(buf + offs), len - offs);
}
int EXIFInfo::parseFromXMPSegmentXML(const char* szXML, unsigned len) {
// Skip xpacket end section so that tinyxml2 lib parses the section correctly.
const char* const strXMP((const char*)(buf + offs)), *strEnd;
if ((strEnd=Tools::strrnstr(strXMP, "<?xpacket end=", len)) != NULL)
len = (unsigned)(strEnd - strXMP);
const char* szEnd(Tools::strrnstr(szXML, "<?xpacket end=", len));
if (szEnd != NULL)
len = (unsigned)(szEnd - szXML);
// Now try parsing the XML packet.
// Try parsing the XML packet.
tinyxml2::XMLDocument doc;
const tinyxml2::XMLElement* document;
if (doc.Parse(strXMP, len) != tinyxml2::XML_SUCCESS ||
if (doc.Parse(szXML, len) != tinyxml2::XML_SUCCESS ||
((document=doc.FirstChildElement("x:xmpmeta")) == NULL && (document=doc.FirstChildElement("xmp:xmpmeta")) == NULL) ||
(document=document->FirstChildElement("rdf:RDF")) == NULL ||
(document=document->FirstChildElement("rdf:Description")) == NULL)
return PARSE_ABSENT_DATA;
// Now try parsing the XMP content for projection type.
// Try parsing the XMP content for tiff details.
if (Orientation == 0) {
uint32_t _Orientation(0);
document->QueryUnsignedAttribute("tiff:Orientation", &_Orientation);
Orientation = (uint16_t)_Orientation;
}
if (ImageWidth == 0 && ImageHeight == 0) {
document->QueryUnsignedAttribute("tiff:ImageWidth", &ImageWidth);
if (document->QueryUnsignedAttribute("tiff:ImageHeight", &ImageHeight) != tinyxml2::XML_SUCCESS)
document->QueryUnsignedAttribute("tiff:ImageLength", &ImageHeight) ;
}
if (XResolution == 0 && YResolution == 0 && ResolutionUnit == 0) {
document->QueryDoubleAttribute("tiff:XResolution", &XResolution);
document->QueryDoubleAttribute("tiff:YResolution", &YResolution);
uint32_t _ResolutionUnit(0);
document->QueryUnsignedAttribute("tiff:ResolutionUnit", &_ResolutionUnit);
ResolutionUnit = (uint16_t)_ResolutionUnit;
}
// Try parsing the XMP content for projection type.
{
const tinyxml2::XMLElement* const element(document->FirstChildElement("GPano:ProjectionType"));
if (element != NULL) {
@@ -868,26 +1031,74 @@ int EXIFInfo::parseFromXMPSegment(const uint8_t* buf, unsigned len) {
if (szProjectionType != NULL) {
if (0 == _tcsicmp(szProjectionType, "perspective"))
ProjectionType = 1;
else if (0 == _tcsicmp(szProjectionType, "equirectangular") ||
else
if (0 == _tcsicmp(szProjectionType, "equirectangular") ||
0 == _tcsicmp(szProjectionType, "spherical"))
ProjectionType = 2;
}
}
}
// Now try parsing the XMP content for DJI info.
document->QueryDoubleAttribute("drone-dji:AbsoluteAltitude", &GeoLocation.Altitude);
document->QueryDoubleAttribute("drone-dji:RelativeAltitude", &GeoLocation.RelativeAltitude);
document->QueryDoubleAttribute("drone-dji:GimbalRollDegree", &GeoLocation.RollDegree);
document->QueryDoubleAttribute("drone-dji:GimbalPitchDegree", &GeoLocation.PitchDegree);
document->QueryDoubleAttribute("drone-dji:GimbalYawDegree", &GeoLocation.YawDegree);
// Try parsing the XMP content for supported maker's info.
struct ParseXMP {
// try yo fetch the value both from the attribute and child element
// and parse if needed rational numbers stored as string fraction
static bool Value(const tinyxml2::XMLElement* document, const char* name, double& value) {
const char* szAttribute = document->Attribute(name);
if (szAttribute == NULL) {
const tinyxml2::XMLElement* const element(document->FirstChildElement(name));
if (element == NULL || (szAttribute=element->GetText()) == NULL)
return false;
}
std::vector<std::string> values;
Tools::strSplit(szAttribute, '/', values);
switch (values.size()) {
case 1: value = strtod(values.front().c_str(), NULL); return true;
case 2: value = strtod(values.front().c_str(), NULL)/strtod(values.back().c_str(), NULL); return true;
}
return false;
}
};
const char* szAbout(document->Attribute("rdf:about"));
if (0 == _tcsicmp(Make.c_str(), "DJI") || (szAbout != NULL && 0 == _tcsicmp(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);
} else
if (0 == _tcsicmp(Make.c_str(), "senseFly") || 0 == _tcsicmp(Make.c_str(), "Sentera")) {
ParseXMP::Value(document, "Camera:Roll", GeoLocation.RollDegree);
if (ParseXMP::Value(document, "Camera:Pitch", GeoLocation.PitchDegree)) {
// convert to DJI format: senseFly uses pitch 0 as NADIR, whereas DJI -90
GeoLocation.PitchDegree = Tools::NormD180(GeoLocation.PitchDegree-90.0);
}
ParseXMP::Value(document, "Camera:Yaw", GeoLocation.YawDegree);
ParseXMP::Value(document, "Camera:GPSXYAccuracy", GeoLocation.AccuracyXY);
ParseXMP::Value(document, "Camera:GPSZAccuracy", GeoLocation.AccuracyZ);
} else
if (0 == _tcsicmp(Make.c_str(), "PARROT")) {
ParseXMP::Value(document, "Camera:Roll", GeoLocation.RollDegree) ||
ParseXMP::Value(document, "drone-parrot:CameraRollDegree", GeoLocation.RollDegree);
if (ParseXMP::Value(document, "Camera:Pitch", GeoLocation.PitchDegree) ||
ParseXMP::Value(document, "drone-parrot:CameraPitchDegree", GeoLocation.PitchDegree)) {
// convert to DJI format: senseFly uses pitch 0 as NADIR, whereas DJI -90
GeoLocation.PitchDegree = Tools::NormD180(GeoLocation.PitchDegree-90.0);
}
ParseXMP::Value(document, "Camera:Yaw", GeoLocation.YawDegree) ||
ParseXMP::Value(document, "drone-parrot:CameraYawDegree", GeoLocation.YawDegree);
ParseXMP::Value(document, "Camera:AboveGroundAltitude", GeoLocation.RelativeAltitude);
}
return PARSE_SUCCESS;
}
void EXIFInfo::Geolocation_t::parseCoords() {
// convert GPS latitude
// Convert GPS latitude
if (LatComponents.degrees != DBL_MAX ||
LatComponents.minutes != 0 ||
LatComponents.seconds != 0) {
@@ -898,7 +1109,7 @@ void EXIFInfo::Geolocation_t::parseCoords() {
if ('S' == LatComponents.direction)
Latitude = -Latitude;
}
// convert GPS longitude
// Convert GPS longitude
if (LonComponents.degrees != DBL_MAX ||
LonComponents.minutes != 0 ||
LonComponents.seconds != 0) {
@@ -909,7 +1120,7 @@ void EXIFInfo::Geolocation_t::parseCoords() {
if ('W' == LonComponents.direction)
Longitude = -Longitude;
}
// convert GPS altitude
// Convert GPS altitude
if (hasAltitude() &&
AltitudeRef == 1) {
Altitude = -Altitude;
@@ -928,6 +1139,9 @@ bool EXIFInfo::Geolocation_t::hasRelativeAltitude() const {
bool EXIFInfo::Geolocation_t::hasOrientation() const {
return RollDegree != DBL_MAX && PitchDegree != DBL_MAX && YawDegree != DBL_MAX;
}
bool EXIFInfo::Geolocation_t::hasSpeed() const {
return SpeedX != DBL_MAX && SpeedY != DBL_MAX && SpeedZ != DBL_MAX;
}
void EXIFInfo::clear() {
@@ -951,6 +1165,9 @@ void EXIFInfo::clear() {
RelatedImageWidth = 0;
RelatedImageHeight= 0;
Orientation = 0;
XResolution = 0;
YResolution = 0;
ResolutionUnit = 0;
BitsPerSample = 0;
ExposureTime = 0;
FNumber = 0;
@@ -968,6 +1185,11 @@ void EXIFInfo::clear() {
ProjectionType = 0;
SubjectArea.clear();
// Calibration
Calibration.FocalLength = 0;
Calibration.OpticalCenterX = 0;
Calibration.OpticalCenterY = 0;
// LensInfo
LensInfo.FocalLengthMax = 0;
LensInfo.FocalLengthMin = 0;
@@ -990,6 +1212,11 @@ void EXIFInfo::clear() {
GeoLocation.RollDegree = DBL_MAX;
GeoLocation.PitchDegree = DBL_MAX;
GeoLocation.YawDegree = DBL_MAX;
GeoLocation.SpeedX = DBL_MAX;
GeoLocation.SpeedY = DBL_MAX;
GeoLocation.SpeedZ = DBL_MAX;
GeoLocation.AccuracyXY = 0;
GeoLocation.AccuracyZ = 0;
GeoLocation.GPSDOP = 0;
GeoLocation.GPSDifferential = 0;
GeoLocation.GPSMapDatum = "";

View File

@@ -13,9 +13,9 @@
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,
- 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,
- 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.
@@ -39,7 +39,7 @@
#define TINYEXIF_MAJOR_VERSION 1
#define TINYEXIF_MINOR_VERSION 0
#define TINYEXIF_PATCH_VERSION 0
#define TINYEXIF_PATCH_VERSION 1
#ifdef _MSC_VER
# ifdef TINYEXIF_EXPORT
@@ -75,22 +75,42 @@ enum FieldCode {
class EntryParser;
//
// Class responsible for storing and parsing EXIF information from a JPEG blob
// Interface class responsible for fetching stream data to be parsed
//
class TINYEXIF_LIB EXIFStream {
public:
virtual ~EXIFStream() {}
// Check the state of the stream.
virtual bool IsValid() const = 0;
// Return the pointer to the beginning of the desired size buffer
// following current buffer position.
virtual const uint8_t* GetBuffer(unsigned desiredLength) = 0;
// Advance current buffer position with the desired size;
// return false if stream ends in less than the desired size.
virtual bool SkipBuffer(unsigned desiredLength) = 0;
};
//
// Class responsible for storing and parsing EXIF & XMP metadata from a JPEG stream
//
class TINYEXIF_LIB EXIFInfo {
public:
EXIFInfo();
EXIFInfo(EXIFStream& stream);
EXIFInfo(const uint8_t* data, unsigned length);
EXIFInfo(const std::string& data);
// Parsing function for an entire JPEG image stream.
//
// PARAM 'stream': Interface to fetch JPEG image stream.
// PARAM 'data': A pointer to a JPEG image.
// PARAM 'length': The length of the JPEG image.
// RETURN: PARSE_SUCCESS (0) on success with 'result' filled out
// error code otherwise, as defined by the PARSE_* macros
int parseFrom(EXIFStream& stream);
int parseFrom(const uint8_t* data, unsigned length);
int parseFrom(const std::string& data);
// Parsing function for an EXIF segment. This is used internally by parseFrom()
// but can be called for special cases where only the EXIF section is
@@ -101,6 +121,7 @@ public:
// but can be called for special cases where only the XMP section is
// available (i.e., a blob starting with the bytes "http://ns.adobe.com/xap/1.0/\0").
int parseFromXMPSegment(const uint8_t* buf, unsigned len);
int parseFromXMPSegmentXML(const char* szXML, unsigned len);
// Set all data members to default values.
// Should be called before parsing a new stream.
@@ -113,6 +134,8 @@ private:
void parseIFDExif(EntryParser&);
// Parse tag as GPS IFD.
void parseIFDGPS(EntryParser&);
// Parse tag as MakerNote IFD.
void parseIFDMakerNote(EntryParser&);
public:
// Data fields
@@ -219,6 +242,11 @@ public:
// 2: location of the main subject as coordinates (first value is the X coordinate and second is the Y coordinate)
// 3: area of the main subject as a circle (first value is the center X coordinate, second is the center Y coordinate, and third is the diameter)
// 4: area of the main subject as a rectangle (first value is the center X coordinate, second is the center Y coordinate, third is the width of the area, and fourth is the height of the area)
struct TINYEXIF_LIB Calibration_t { // Camera calibration information
double FocalLength; // Focal length (pixels)
double OpticalCenterX; // Principal point X (pixels)
double OpticalCenterY; // Principal point Y (pixels)
} Calibration;
struct TINYEXIF_LIB LensInfo_t { // Lens information
double FStopMin; // Min aperture (f-stop)
double FStopMax; // Max aperture (f-stop)
@@ -245,6 +273,11 @@ public:
double RollDegree; // Flight roll in degrees
double PitchDegree; // Flight pitch in degrees
double YawDegree; // Flight yaw in degrees
double SpeedX; // Flight speed on X in meters/second
double SpeedY; // Flight speed on Y in meters/second
double SpeedZ; // Flight speed on Z in meters/second
double AccuracyXY; // GPS accuracy on XY in meters
double AccuracyZ; // GPS accuracy on Z in meters
double GPSDOP; // GPS DOP (data degree of precision)
uint16_t GPSDifferential; // Differential correction applied to the GPS receiver (may not exist)
// 0: measurement without differential correction
@@ -263,6 +296,7 @@ public:
bool hasAltitude() const; // Return true if (alt) is available
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
} GeoLocation;
};

View File

@@ -9,28 +9,49 @@
#include <vector> // std::vector
#include <iomanip> // std::setprecision
class EXIFStreamFile : public TinyEXIF::EXIFStream {
public:
explicit EXIFStreamFile(const char* fileName)
: file(fileName, std::ifstream::in|std::ifstream::binary) {}
bool IsValid() const override {
return file.is_open();
}
const uint8_t* GetBuffer(unsigned desiredLength) override {
buffer.resize(desiredLength);
if (!file.read((char*)buffer.data(), desiredLength))
return NULL;
return buffer.data();
}
bool SkipBuffer(unsigned desiredLength) override {
return (bool)file.seekg(desiredLength,std::ios::cur);
}
private:
std::ifstream file;
std::vector<uint8_t> buffer;
};
int main(int argc, const char** argv)
{
if (argc != 2) {
std::cout << "Usage: TinyEXIF <image_file>" << "\n";
std::cout << "Usage: TinyEXIF <image_file>\n";
return -1;
}
// read entire image file
std::ifstream file(argv[1], std::ifstream::in|std::ifstream::binary);
if (!file.is_open()) {
EXIFStreamFile stream(argv[1]);
if (!stream.IsValid()) {
std::cout << "error: can not open '" << argv[1] << "'\n";
return -2;
}
file.seekg(0,std::ios::end);
std::streampos length = file.tellg();
file.seekg(0,std::ios::beg);
std::vector<uint8_t> data(length);
file.read((char*)data.data(), length);
// parse image EXIF and XMP metadata
TinyEXIF::EXIFInfo imageEXIF(data.data(), (unsigned)length);
if (imageEXIF.Fields) {
TinyEXIF::EXIFInfo imageEXIF(stream);
if (!imageEXIF.Fields) {
std::cout << "error: no EXIF or XMP metadata\n";
return -3;
}
// print extracted metadata
if (imageEXIF.ImageWidth || imageEXIF.ImageHeight)
std::cout << "ImageResolution " << imageEXIF.ImageWidth << "x" << imageEXIF.ImageHeight << " pixels" << "\n";
if (imageEXIF.RelatedImageWidth || imageEXIF.RelatedImageHeight)
@@ -41,9 +62,11 @@ int main(int argc, const char** argv)
std::cout << "CameraModel " << imageEXIF.Make << " - " << imageEXIF.Model << "\n";
if (!imageEXIF.SerialNumber.empty())
std::cout << "SerialNumber " << imageEXIF.SerialNumber << "\n";
if (imageEXIF.Orientation)
std::cout << "Orientation " << imageEXIF.Orientation << "\n";
std::cout << "Resolution " << imageEXIF.XResolution << "x" << imageEXIF.YResolution << "\n";
std::cout << "ResolutionUnit " << imageEXIF.ResolutionUnit << "\n";
if (imageEXIF.XResolution || imageEXIF.YResolution || imageEXIF.ResolutionUnit)
std::cout << "Resolution " << imageEXIF.XResolution << "x" << imageEXIF.YResolution << " (" << imageEXIF.ResolutionUnit << ")\n";
if (imageEXIF.BitsPerSample)
std::cout << "BitsPerSample " << imageEXIF.BitsPerSample << "\n";
if (!imageEXIF.Software.empty())
std::cout << "Software " << imageEXIF.Software << "\n";
@@ -77,6 +100,12 @@ int main(int argc, const char** argv)
std::cout << "MeteringMode " << imageEXIF.MeteringMode << "\n";
std::cout << "LightSource " << imageEXIF.LightSource << "\n";
std::cout << "ProjectionType " << imageEXIF.ProjectionType << "\n";
if (imageEXIF.Calibration.FocalLength != 0)
std::cout << "Calibration.FocalLength " << imageEXIF.Calibration.FocalLength << " pixels" << "\n";
if (imageEXIF.Calibration.OpticalCenterX != 0)
std::cout << "Calibration.OpticalCenterX " << imageEXIF.Calibration.OpticalCenterX << " pixels" << "\n";
if (imageEXIF.Calibration.OpticalCenterY != 0)
std::cout << "Calibration.OpticalCenterY " << imageEXIF.Calibration.OpticalCenterY << " pixels" << "\n";
std::cout << "LensInfo.FStopMin " << imageEXIF.LensInfo.FStopMin << "\n";
std::cout << "LensInfo.FStopMax " << imageEXIF.LensInfo.FStopMax << "\n";
std::cout << "LensInfo.FocalLengthMin " << imageEXIF.LensInfo.FocalLengthMin << " mm" << "\n";
@@ -97,12 +126,19 @@ int main(int argc, const char** argv)
std::cout << "GeoLocation.AltitudeRef " << (int)imageEXIF.GeoLocation.AltitudeRef << "\n";
}
if (imageEXIF.GeoLocation.hasRelativeAltitude())
std::cout << "GeoLocation.RelativeAltitude " << imageEXIF.GeoLocation.RelativeAltitude << "\n";
std::cout << "GeoLocation.RelativeAltitude " << imageEXIF.GeoLocation.RelativeAltitude << " m" << "\n";
if (imageEXIF.GeoLocation.hasOrientation()) {
std::cout << "GeoLocation.RollDegree " << imageEXIF.GeoLocation.RollDegree << "\n";
std::cout << "GeoLocation.PitchDegree " << imageEXIF.GeoLocation.PitchDegree << "\n";
std::cout << "GeoLocation.YawDegree " << imageEXIF.GeoLocation.YawDegree << "\n";
}
if (imageEXIF.GeoLocation.hasSpeed()) {
std::cout << "GeoLocation.SpeedX " << imageEXIF.GeoLocation.SpeedX << " m/s" << "\n";
std::cout << "GeoLocation.SpeedY " << imageEXIF.GeoLocation.SpeedY << " m/s" << "\n";
std::cout << "GeoLocation.SpeedZ " << imageEXIF.GeoLocation.SpeedZ << " m/s" << "\n";
}
if (imageEXIF.GeoLocation.AccuracyXY > 0 || imageEXIF.GeoLocation.AccuracyZ > 0)
std::cout << "GeoLocation.GPSAccuracy XY " << imageEXIF.GeoLocation.AccuracyXY << " m" << " Z " << imageEXIF.GeoLocation.AccuracyZ << " m" << "\n";
std::cout << "GeoLocation.GPSDOP " << imageEXIF.GeoLocation.GPSDOP << "\n";
std::cout << "GeoLocation.GPSDifferential " << imageEXIF.GeoLocation.GPSDifferential << "\n";
if (!imageEXIF.GeoLocation.GPSMapDatum.empty())
@@ -111,6 +147,5 @@ int main(int argc, const char** argv)
std::cout << "GeoLocation.GPSTimeStamp " << imageEXIF.GeoLocation.GPSTimeStamp << "\n";
if (!imageEXIF.GeoLocation.GPSDateStamp.empty())
std::cout << "GeoLocation.GPSDateStamp " << imageEXIF.GeoLocation.GPSDateStamp << "\n";
}
return 0;
return EXIT_SUCCESS;
}