2019-03-13 00:25:46 +01:00
|
|
|
#include <QDataStream>
|
2019-03-15 19:39:52 +01:00
|
|
|
#include <QImageReader>
|
2019-03-13 00:25:46 +01:00
|
|
|
#include "common/tifffile.h"
|
2021-01-17 19:33:06 +01:00
|
|
|
#include "common/util.h"
|
2019-03-13 00:25:46 +01:00
|
|
|
#include "exifparser.h"
|
|
|
|
|
|
|
|
|
2019-03-14 18:52:15 +01:00
|
|
|
#define SOI_MARKER 0xFFD8
|
|
|
|
#define APP1_MARKER 0xFFE1
|
|
|
|
|
|
|
|
#define GPSIFDTag 34853
|
|
|
|
#define ImageDescription 270
|
|
|
|
|
|
|
|
#define GPSLatitudeRef 1
|
|
|
|
#define GPSLatitude 2
|
|
|
|
#define GPSLongitudeRef 3
|
|
|
|
#define GPSLongitude 4
|
|
|
|
#define GPSAltitudeRef 5
|
|
|
|
#define GPSAltitude 6
|
|
|
|
#define GPSTimeStamp 7
|
|
|
|
#define GPSDateStamp 29
|
|
|
|
|
|
|
|
|
|
|
|
QString EXIFParser::text(TIFFFile &file, const IFDEntry &e) const
|
|
|
|
{
|
|
|
|
if (e.type != TIFF_ASCII || !e.count)
|
|
|
|
return QString();
|
|
|
|
|
|
|
|
if (e.count <= sizeof(e.offset))
|
|
|
|
return QString(QByteArray((const char *)&e.offset, sizeof(e.offset)));
|
|
|
|
|
|
|
|
if (!file.seek(e.offset))
|
|
|
|
return QString();
|
|
|
|
|
|
|
|
QByteArray str(file.read(e.count));
|
|
|
|
if (str.size() < (int)e.count)
|
|
|
|
return QString();
|
|
|
|
|
|
|
|
return QString(str);
|
|
|
|
}
|
2019-03-13 00:25:46 +01:00
|
|
|
|
2019-03-13 20:48:25 +01:00
|
|
|
QTime EXIFParser::time(TIFFFile &file, const IFDEntry &ts) const
|
2019-03-13 00:25:46 +01:00
|
|
|
{
|
2019-03-13 20:48:25 +01:00
|
|
|
if (!(ts.type == TIFF_RATIONAL && ts.count == 3))
|
|
|
|
return QTime();
|
|
|
|
|
|
|
|
if (!file.seek(ts.offset))
|
|
|
|
return QTime();
|
|
|
|
|
|
|
|
double hms[3];
|
|
|
|
for (int i = 0; i < 3; i++) {
|
|
|
|
quint32 num, den;
|
|
|
|
if (!file.readValue(num))
|
|
|
|
return QTime();
|
|
|
|
if (!file.readValue(den))
|
|
|
|
return QTime();
|
|
|
|
|
|
|
|
hms[i] = num/(double)den;
|
|
|
|
}
|
|
|
|
|
|
|
|
return QTime((int)hms[0], (int)hms[1], (int)hms[2]);
|
|
|
|
}
|
|
|
|
|
|
|
|
double EXIFParser::altitude(TIFFFile &file, const IFDEntry &alt,
|
|
|
|
const IFDEntry &altRef) const
|
|
|
|
{
|
2019-03-13 22:01:05 +01:00
|
|
|
if (!(alt.type == TIFF_RATIONAL && alt.count == 1))
|
2019-03-13 20:48:25 +01:00
|
|
|
return NAN;
|
|
|
|
|
|
|
|
if (!file.seek(alt.offset))
|
2019-03-13 00:25:46 +01:00
|
|
|
return NAN;
|
|
|
|
|
|
|
|
quint32 num, den;
|
|
|
|
if (!file.readValue(num))
|
2019-03-13 20:48:25 +01:00
|
|
|
return NAN;
|
2019-03-13 00:25:46 +01:00
|
|
|
if (!file.readValue(den))
|
2019-03-13 20:48:25 +01:00
|
|
|
return NAN;
|
2019-03-13 00:25:46 +01:00
|
|
|
|
2019-03-13 22:01:05 +01:00
|
|
|
return (altRef.type == TIFF_BYTE && altRef.count == 1 && altRef.offset)
|
2019-03-16 19:29:30 +01:00
|
|
|
? -(num/(double)den) : num/(double)den;
|
2019-03-13 00:25:46 +01:00
|
|
|
}
|
|
|
|
|
2019-03-13 20:48:25 +01:00
|
|
|
double EXIFParser::coordinate(TIFFFile &file, const IFDEntry &ll) const
|
2019-03-13 00:25:46 +01:00
|
|
|
{
|
2020-01-16 22:54:12 +01:00
|
|
|
// Some broken image creators like NOKIA phones use a wrong (SRATIONAL)
|
|
|
|
// data type
|
|
|
|
if (!((ll.type == TIFF_RATIONAL || ll.type == TIFF_SRATIONAL)
|
|
|
|
&& ll.count == 3))
|
2019-03-13 00:25:46 +01:00
|
|
|
return NAN;
|
|
|
|
|
2019-03-13 20:48:25 +01:00
|
|
|
if (!file.seek(ll.offset))
|
|
|
|
return NAN;
|
2019-03-13 00:25:46 +01:00
|
|
|
|
2019-03-13 20:48:25 +01:00
|
|
|
double dms[3];
|
2019-03-13 00:25:46 +01:00
|
|
|
for (int i = 0; i < 3; i++) {
|
|
|
|
quint32 num, den;
|
|
|
|
if (!file.readValue(num))
|
2019-03-13 20:48:25 +01:00
|
|
|
return NAN;
|
2019-03-13 00:25:46 +01:00
|
|
|
if (!file.readValue(den))
|
2019-03-13 20:48:25 +01:00
|
|
|
return NAN;
|
2019-03-13 00:25:46 +01:00
|
|
|
|
2019-03-13 20:48:25 +01:00
|
|
|
dms[i] = num/(double)den;
|
2019-03-13 00:25:46 +01:00
|
|
|
}
|
|
|
|
|
|
|
|
return dms[0] + dms[1]/60 + dms[2]/3600;
|
|
|
|
}
|
|
|
|
|
2019-03-13 20:48:25 +01:00
|
|
|
Coordinates EXIFParser::coordinates(TIFFFile &file, const IFDEntry &lon,
|
|
|
|
const IFDEntry &lonRef, const IFDEntry &lat, const IFDEntry &latRef) const
|
|
|
|
{
|
|
|
|
if (!(latRef.type == TIFF_ASCII && latRef.count == 2
|
|
|
|
&& lonRef.type == TIFF_ASCII && lonRef.count == 2))
|
|
|
|
return Coordinates();
|
|
|
|
|
|
|
|
Coordinates c(coordinate(file, lon), coordinate(file, lat));
|
|
|
|
if (!c.isValid())
|
|
|
|
return Coordinates();
|
|
|
|
|
2019-08-11 09:26:54 +02:00
|
|
|
char ew = file.isBE() ? lonRef.offset >> 24 : lonRef.offset;
|
|
|
|
char ns = file.isBE() ? latRef.offset >> 24 : latRef.offset;
|
|
|
|
|
|
|
|
if (ew == 'W')
|
2019-03-13 20:48:25 +01:00
|
|
|
c.rlon() = -c.lon();
|
2019-08-11 09:26:54 +02:00
|
|
|
if (ns == 'S')
|
2019-03-13 20:48:25 +01:00
|
|
|
c.rlat() = -c.lat();
|
|
|
|
|
|
|
|
return c;
|
|
|
|
}
|
|
|
|
|
2019-03-13 00:25:46 +01:00
|
|
|
bool EXIFParser::readEntry(TIFFFile &file, const QSet<quint16> &tags,
|
|
|
|
QMap<quint16, IFDEntry> &entries) const
|
|
|
|
{
|
|
|
|
IFDEntry entry;
|
|
|
|
quint16 tag;
|
|
|
|
|
|
|
|
if (!file.readValue(tag))
|
|
|
|
return false;
|
|
|
|
if (!file.readValue(entry.type))
|
|
|
|
return false;
|
|
|
|
if (!file.readValue(entry.count))
|
|
|
|
return false;
|
|
|
|
if (!file.readValue(entry.offset))
|
|
|
|
return false;
|
|
|
|
|
|
|
|
if (tags.contains(tag))
|
|
|
|
entries.insert(tag, entry);
|
|
|
|
|
|
|
|
return true;
|
|
|
|
}
|
|
|
|
|
|
|
|
bool EXIFParser::readIFD(TIFFFile &file, quint32 offset,
|
|
|
|
const QSet<quint16> &tags, QMap<quint16, IFDEntry> &entries) const
|
|
|
|
{
|
|
|
|
quint16 count;
|
|
|
|
|
|
|
|
if (!file.seek(offset))
|
|
|
|
return false;
|
|
|
|
if (!file.readValue(count))
|
|
|
|
return false;
|
|
|
|
|
|
|
|
for (quint16 i = 0; i < count; i++)
|
|
|
|
if (!readEntry(file, tags, entries))
|
|
|
|
return false;
|
|
|
|
|
|
|
|
return true;
|
|
|
|
}
|
|
|
|
|
2019-03-14 08:27:01 +01:00
|
|
|
bool EXIFParser::parseTIFF(QFile *file, QVector<Waypoint> &waypoints)
|
2019-03-13 00:25:46 +01:00
|
|
|
{
|
2019-03-14 08:27:01 +01:00
|
|
|
TIFFFile tiff(file);
|
2019-03-13 00:25:46 +01:00
|
|
|
if (!tiff.isValid()) {
|
|
|
|
_errorString = "Invalid EXIF data";
|
|
|
|
return false;
|
|
|
|
}
|
|
|
|
|
2019-03-14 18:52:15 +01:00
|
|
|
QSet<quint16> IFD0Tags;
|
|
|
|
IFD0Tags << GPSIFDTag << ImageDescription;
|
|
|
|
QMap<quint16, IFDEntry> IFD0;
|
2019-03-13 00:25:46 +01:00
|
|
|
for (quint32 ifd = tiff.ifd(); ifd; ) {
|
2019-03-14 18:52:15 +01:00
|
|
|
if (!readIFD(tiff, ifd, IFD0Tags, IFD0) || !tiff.readValue(ifd)) {
|
|
|
|
_errorString = "Invalid IFD0";
|
2019-03-13 00:25:46 +01:00
|
|
|
return false;
|
|
|
|
}
|
|
|
|
}
|
2019-03-14 18:52:15 +01:00
|
|
|
if (!IFD0.contains(GPSIFDTag)) {
|
2019-03-13 00:25:46 +01:00
|
|
|
_errorString = "GPS IFD not found";
|
|
|
|
return false;
|
|
|
|
}
|
|
|
|
|
2019-03-14 18:52:15 +01:00
|
|
|
QSet<quint16> GPSIFDTags;
|
|
|
|
GPSIFDTags << GPSLatitude << GPSLongitude << GPSLatitudeRef
|
|
|
|
<< GPSLongitudeRef << GPSAltitude << GPSAltitudeRef << GPSDateStamp
|
|
|
|
<< GPSTimeStamp;
|
|
|
|
QMap<quint16, IFDEntry> GPSIFD;
|
|
|
|
for (quint32 ifd = IFD0.value(GPSIFDTag).offset; ifd; ) {
|
|
|
|
if (!readIFD(tiff, ifd, GPSIFDTags, GPSIFD) || !tiff.readValue(ifd)) {
|
2019-03-13 00:25:46 +01:00
|
|
|
_errorString = "Invalid GPS IFD";
|
|
|
|
return false;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2019-03-14 18:52:15 +01:00
|
|
|
Coordinates c(coordinates(tiff, GPSIFD.value(GPSLongitude),
|
|
|
|
GPSIFD.value(GPSLongitudeRef), GPSIFD.value(GPSLatitude),
|
|
|
|
GPSIFD.value(GPSLatitudeRef)));
|
2019-03-13 00:25:46 +01:00
|
|
|
if (!c.isValid()) {
|
2019-03-13 20:48:25 +01:00
|
|
|
_errorString = "Invalid/missing GPS coordinates";
|
2019-03-13 00:25:46 +01:00
|
|
|
return false;
|
|
|
|
}
|
|
|
|
|
2019-03-15 19:39:52 +01:00
|
|
|
file->reset();
|
|
|
|
ImageInfo img(file->fileName(), QImageReader(file).size());
|
|
|
|
|
2019-03-13 00:25:46 +01:00
|
|
|
Waypoint wp(c);
|
2021-01-17 19:33:06 +01:00
|
|
|
wp.setName(Util::file2name(file->fileName()));
|
2019-11-15 22:10:55 +01:00
|
|
|
wp.addImage(img);
|
2019-03-14 18:52:15 +01:00
|
|
|
wp.setElevation(altitude(tiff, GPSIFD.value(GPSAltitude),
|
|
|
|
GPSIFD.value(GPSAltitudeRef)));
|
|
|
|
wp.setTimestamp(QDateTime(QDate::fromString(text(tiff,
|
|
|
|
GPSIFD.value(GPSDateStamp)), "yyyy:MM:dd"), time(tiff,
|
|
|
|
GPSIFD.value(GPSTimeStamp)), Qt::UTC));
|
|
|
|
wp.setDescription(text(tiff, IFD0.value(ImageDescription)).trimmed());
|
2019-03-13 00:25:46 +01:00
|
|
|
|
|
|
|
waypoints.append(wp);
|
|
|
|
|
|
|
|
return true;
|
|
|
|
}
|
|
|
|
|
|
|
|
bool EXIFParser::parse(QFile *file, QList<TrackData> &tracks,
|
|
|
|
QList<RouteData> &routes, QList<Area> &polygons,
|
|
|
|
QVector<Waypoint> &waypoints)
|
|
|
|
{
|
|
|
|
Q_UNUSED(tracks);
|
|
|
|
Q_UNUSED(routes);
|
|
|
|
Q_UNUSED(polygons);
|
2019-03-14 18:52:15 +01:00
|
|
|
quint16 marker;
|
2019-03-13 00:25:46 +01:00
|
|
|
|
|
|
|
QDataStream stream(file);
|
|
|
|
stream.setByteOrder(QDataStream::BigEndian);
|
2019-03-14 18:52:15 +01:00
|
|
|
stream >> marker;
|
|
|
|
if (marker != SOI_MARKER) {
|
|
|
|
_errorString = "Not a JPEG file";
|
|
|
|
return false;
|
|
|
|
}
|
2019-03-13 00:25:46 +01:00
|
|
|
|
|
|
|
while (!stream.atEnd()) {
|
|
|
|
stream >> marker;
|
2019-03-14 18:52:15 +01:00
|
|
|
if (marker == APP1_MARKER) {
|
2019-03-14 08:27:01 +01:00
|
|
|
quint16 size;
|
|
|
|
char magic[6];
|
|
|
|
stream >> size;
|
|
|
|
if (stream.readRawData(magic, sizeof(magic)) == sizeof(magic) &&
|
|
|
|
!memcmp(magic, "Exif\0\0", sizeof(magic)))
|
|
|
|
return parseTIFF(file, waypoints);
|
|
|
|
else
|
|
|
|
break;
|
|
|
|
}
|
2019-03-13 00:25:46 +01:00
|
|
|
}
|
|
|
|
|
|
|
|
_errorString = "No EXIF data found";
|
|
|
|
return false;
|
|
|
|
}
|