Skip to content

Commit

Permalink
ExifParser Unit Test
Browse files Browse the repository at this point in the history
  • Loading branch information
HTRamsey committed May 17, 2024
1 parent 3f5d83e commit e602fd9
Show file tree
Hide file tree
Showing 7 changed files with 197 additions and 120 deletions.
186 changes: 130 additions & 56 deletions src/AnalyzeView/ExifParser.cc
Original file line number Diff line number Diff line change
Expand Up @@ -6,76 +6,141 @@

QGC_LOGGING_CATEGORY(ExifParserLog, "qgc.analyzeview.exifparser")

double ExifParser::readTime(QByteArray& buf)
{
QByteArray tiffHeader("\x49\x49\x2A", 3);
QByteArray createDateHeader("\x04\x90\x02", 3);
namespace {

union char2uint32_u {
char c[4];
uint32_t i;
};

union char2uint16_u {
char c[2];
uint16_t i;
};

// This struct describes a standart field used in exif files
struct field_s {
uint16_t tagID; // Describes which information is added here, e.g. GPS Lat
uint16_t type; // Describes the data type, e.g. string, uint8_t,...
uint32_t size; // Describes the size
uint32_t content; // Either contains the information, or the offset to the exif header where the information is stored (if 32 bits is not enough)
};

// This struct contains all the fields that we want to add to the image
struct fields_s {
field_s gpsVersion;
field_s gpsLatRef;
field_s gpsLat;
field_s gpsLonRef;
field_s gpsLon;
field_s gpsAltRef;
field_s gpsAlt;
field_s gpsMapDatum;
uint32_t finishedDataField;
};

// These are the additional information that can not be put into a single uin32_t
struct extended_s {
uint32_t gpsLat[6];
uint32_t gpsLon[6];
uint32_t gpsAlt[2];
char mapDatum[7];// = {0x57,0x47,0x53,0x2D,0x38,0x34,0x00};
};

// This struct contains all the information we want to add to the image
struct readable_s {
fields_s fields;
extended_s extendedData;
};

// This union is used because for writing the information we have to use a char array, but we still want the information to be available in a more descriptive way
union gpsData_u {
char c[0xa3];
readable_s readable;
};
}

// find header position
uint32_t tiffHeaderIndex = buf.indexOf(tiffHeader);
namespace ExifParser {

double readTime(const QByteArray &buf)
{
// find creation date header index
uint32_t createDateHeaderIndex = buf.indexOf(createDateHeader);
static const QByteArray createDateHeader("\x04\x90\x02", 3);
const uint32_t createDateHeaderIndex = buf.indexOf(createDateHeader);

// extract size of date-time string, -1 accounting for null-termination
uint32_t* sizeString = reinterpret_cast<uint32_t*>(buf.mid(createDateHeaderIndex + 4, 4).data());
uint32_t createDateStringSize = qFromLittleEndian(*sizeString) - 1;
const uint32_t* const sizeString = reinterpret_cast<const uint32_t*>(buf.mid(createDateHeaderIndex + 4, 4).data());
const uint32_t createDateStringSize = qFromLittleEndian(*sizeString) - 1;

// find header position
static const QByteArray tiffHeaderIntel("\x49\x49\x2A\x00", 4);
// static const QByteArray tiffHeaderMotorola("\x4D\x4D\x00\x2A", 4);
const uint32_t tiffHeaderIndex = buf.indexOf(tiffHeaderIntel);

// extract location of date-time string
uint32_t* dataIndex = reinterpret_cast<uint32_t*>(buf.mid(createDateHeaderIndex + 8, 4).data());
uint32_t createDateStringDataIndex = qFromLittleEndian(*dataIndex) + tiffHeaderIndex;
const uint32_t* const dataIndex = reinterpret_cast<const uint32_t*>(buf.mid(createDateHeaderIndex + 8, 4).data());
const uint32_t createDateStringDataIndex = qFromLittleEndian(*dataIndex) + tiffHeaderIndex;

// read out data of create date-time field
QString createDate = buf.mid(createDateStringDataIndex, createDateStringSize);
const QString createDate = buf.mid(createDateStringDataIndex, createDateStringSize);

QStringList createDateList = createDate.split(' ');
const QStringList createDateList = createDate.split(' ');
if (createDateList.count() < 2) {
qCWarning(ExifParserLog) << "Could not decode creation time and date: " << createDateList;
return -1.0;
}
QStringList dateList = createDateList[0].split(':');

const QStringList dateList = createDateList.at(0).split(':');
if (dateList.count() < 3) {
qCWarning(ExifParserLog) << "Could not decode creation date: " << dateList;
return -1.0;
}
QStringList timeList = createDateList[1].split(':');

const QStringList timeList = createDateList.at(1).split(':');
if (timeList.count() < 3) {
qCWarning(ExifParserLog) << "Could not decode creation time: " << timeList;
return -1.0;
}
QDate date(dateList[0].toInt(), dateList[1].toInt(), dateList[2].toInt());
QTime time(timeList[0].toInt(), timeList[1].toInt(), timeList[2].toInt());
QDateTime tagTime(date, time);
return tagTime.toMSecsSinceEpoch()/1000.0;

const QDate date(dateList.at(0).toInt(), dateList.at(1).toInt(), dateList.at(2).toInt());
const QTime time(timeList.at(0).toInt(), timeList.at(1).toInt(), timeList.at(2).toInt());

const QDateTime tagTime(date, time);

return (tagTime.toMSecsSinceEpoch() / 1000.0);
}

bool ExifParser::write(QByteArray& buf, GeoTagWorker::cameraFeedbackPacket& geotag)
bool write(QByteArray& buf, const GeoTagWorker::cameraFeedbackPacket& geotag)
{
QByteArray app1Header("\xff\xe1", 2);
uint32_t app1HeaderInd = buf.indexOf(app1Header);
uint16_t *conversionPointer = reinterpret_cast<uint16_t *>(buf.mid(app1HeaderInd + 2, 2).data());
uint16_t app1Size = *conversionPointer;
uint16_t app1SizeEndian = qFromBigEndian(app1Size) + 0xa5; // change wrong endian
QByteArray tiffHeader("\x49\x49\x2A", 3);
uint32_t tiffHeaderInd = buf.indexOf(tiffHeader);
conversionPointer = reinterpret_cast<uint16_t *>(buf.mid(tiffHeaderInd + 8, 2).data());
uint16_t numberOfTiffFields = *conversionPointer;
uint32_t nextIfdOffsetInd = tiffHeaderInd + 10 + 12 * (numberOfTiffFields);
conversionPointer = reinterpret_cast<uint16_t *>(buf.mid(nextIfdOffsetInd, 2).data());
uint16_t nextIfdOffset = *conversionPointer;
static const QByteArray app1Header("\xff\xe1", 2);

const uint32_t app1HeaderInd = buf.indexOf(app1Header);
const uint16_t *conversionPointer = reinterpret_cast<uint16_t*>(buf.mid(app1HeaderInd + 2, 2).data());
const uint16_t app1Size = *conversionPointer;
const uint16_t app1SizeEndian = qFromBigEndian(app1Size) + 0xA5; // change wrong endian

static const QByteArray tiffHeader("\x49\x49\x2A", 3);

const uint32_t tiffHeaderInd = buf.indexOf(tiffHeader);
conversionPointer = reinterpret_cast<uint16_t*>(buf.mid(tiffHeaderInd + 8, 2).data());
const uint16_t numberOfTiffFields = *conversionPointer;

const uint32_t nextIfdOffsetInd = tiffHeaderInd + 10 + (12 * numberOfTiffFields);
conversionPointer = reinterpret_cast<uint16_t*>(buf.mid(nextIfdOffsetInd, 2).data());
const uint16_t nextIfdOffset = *conversionPointer;

char2uint32_u gpsIFDInd;
gpsIFDInd.i = nextIfdOffset;

// this will stay constant
QByteArray gpsInfo("\x25\x88\x04\x00\x01\x00\x00\x00", 8);
gpsInfo.append(gpsIFDInd.c[0]);
gpsInfo.append(gpsIFDInd.c[1]);
gpsInfo.append(gpsIFDInd.c[2]);
gpsInfo.append(gpsIFDInd.c[3]);
(void) gpsInfo.append(gpsIFDInd.c[0]);
(void) gpsInfo.append(gpsIFDInd.c[1]);
(void) gpsInfo.append(gpsIFDInd.c[2]);
(void) gpsInfo.append(gpsIFDInd.c[3]);

// filling values to gpsData
uint32_t gpsDataExtInd = gpsIFDInd.i + 2 + sizeof(fields_s);
const uint32_t gpsDataExtInd = gpsIFDInd.i + 2 + sizeof(fields_s);

gpsData_u gpsData;

Expand All @@ -88,7 +153,7 @@ bool ExifParser::write(QByteArray& buf, GeoTagWorker::cameraFeedbackPacket& geot
gpsData.readable.fields.gpsLatRef.tagID = 1;
gpsData.readable.fields.gpsLatRef.type = 2;
gpsData.readable.fields.gpsLatRef.size = 2;
gpsData.readable.fields.gpsLatRef.content = geotag.latitude > 0 ? 'N' : 'S';
gpsData.readable.fields.gpsLatRef.content = (geotag.latitude > 0) ? 'N' : 'S';

gpsData.readable.fields.gpsLat.tagID = 2;
gpsData.readable.fields.gpsLat.type = 5;
Expand All @@ -98,12 +163,12 @@ bool ExifParser::write(QByteArray& buf, GeoTagWorker::cameraFeedbackPacket& geot
gpsData.readable.fields.gpsLonRef.tagID = 3;
gpsData.readable.fields.gpsLonRef.type = 2;
gpsData.readable.fields.gpsLonRef.size = 2;
gpsData.readable.fields.gpsLonRef.content = geotag.longitude > 0 ? 'E' : 'W';
gpsData.readable.fields.gpsLonRef.content = (geotag.longitude > 0) ? 'E' : 'W';

gpsData.readable.fields.gpsLon.tagID = 4;
gpsData.readable.fields.gpsLon.type = 5;
gpsData.readable.fields.gpsLon.size = 3;
gpsData.readable.fields.gpsLon.content = gpsDataExtInd + 6 * 4;
gpsData.readable.fields.gpsLon.content = gpsDataExtInd + (6 * 4);

gpsData.readable.fields.gpsAltRef.tagID = 5;
gpsData.readable.fields.gpsAltRef.type = 1;
Expand All @@ -113,31 +178,31 @@ bool ExifParser::write(QByteArray& buf, GeoTagWorker::cameraFeedbackPacket& geot
gpsData.readable.fields.gpsAlt.tagID = 6;
gpsData.readable.fields.gpsAlt.type = 5;
gpsData.readable.fields.gpsAlt.size = 1;
gpsData.readable.fields.gpsAlt.content = gpsDataExtInd + 6 * 4 * 2;
gpsData.readable.fields.gpsAlt.content = gpsDataExtInd + (6 * 4 * 2);

gpsData.readable.fields.gpsMapDatum.tagID = 18;
gpsData.readable.fields.gpsMapDatum.type = 2;
gpsData.readable.fields.gpsMapDatum.size = 7;
gpsData.readable.fields.gpsMapDatum.content = gpsDataExtInd + 6 * 4 * 2 + 2 * 4;
gpsData.readable.fields.gpsMapDatum.content = gpsDataExtInd + (6 * 4 * 2) + (2 * 4);

gpsData.readable.fields.finishedDataField = 0;

// Filling up the additional information that does not fit into the fields
gpsData.readable.extendedData.gpsLat[0] = abs(static_cast<int>(geotag.latitude));
gpsData.readable.extendedData.gpsLat[1] = 1;
gpsData.readable.extendedData.gpsLat[2] = static_cast<int>((fabs(geotag.latitude) - floor(fabs(geotag.latitude))) * 60.0);
gpsData.readable.extendedData.gpsLat[2] = static_cast<int>((abs(geotag.latitude) - (floor(abs(geotag.latitude))) * 60.0));
gpsData.readable.extendedData.gpsLat[3] = 1;
gpsData.readable.extendedData.gpsLat[4] = static_cast<int>((fabs(geotag.latitude) * 60.0 - floor(fabs(geotag.latitude) * 60.0)) * 60000.0);
gpsData.readable.extendedData.gpsLat[4] = static_cast<int>(((abs(geotag.latitude) * 60.0) - (floor(abs(geotag.latitude) * 60.0)) * 60000.0));
gpsData.readable.extendedData.gpsLat[5] = 1000;

gpsData.readable.extendedData.gpsLon[0] = abs(static_cast<int>(geotag.longitude));
gpsData.readable.extendedData.gpsLon[1] = 1;
gpsData.readable.extendedData.gpsLon[2] = static_cast<int>((fabs(geotag.longitude) - floor(fabs(geotag.longitude))) * 60.0);
gpsData.readable.extendedData.gpsLon[2] = static_cast<int>((abs(geotag.longitude) - (floor(abs(geotag.longitude))) * 60.0));
gpsData.readable.extendedData.gpsLon[3] = 1;
gpsData.readable.extendedData.gpsLon[4] = static_cast<int>((fabs(geotag.longitude) * 60.0 - floor(fabs(geotag.longitude) * 60.0)) * 60000.0);
gpsData.readable.extendedData.gpsLon[4] = static_cast<int>(((abs(geotag.longitude) * 60.0) - (floor(abs(geotag.longitude) * 60.0)) * 60000.0));
gpsData.readable.extendedData.gpsLon[5] = 1000;

gpsData.readable.extendedData.gpsAlt[0] = geotag.altitude * 100;
gpsData.readable.extendedData.gpsAlt[0] = geotag.altitude * 100.f;
gpsData.readable.extendedData.gpsAlt[1] = 100;
gpsData.readable.extendedData.mapDatum[0] = 'W';
gpsData.readable.extendedData.mapDatum[1] = 'G';
Expand All @@ -148,24 +213,33 @@ bool ExifParser::write(QByteArray& buf, GeoTagWorker::cameraFeedbackPacket& geot
gpsData.readable.extendedData.mapDatum[6] = 0x00;

// remove 12 spaces from image description, as otherwise we need to loop through every field and correct the new address values
buf.remove(nextIfdOffsetInd + 4, 12);
(void) buf.remove(nextIfdOffsetInd + 4, 12);

// TODO correct size in image description

// insert Gps Info to image file
buf.insert(nextIfdOffsetInd, gpsInfo, 12);
char numberOfFields[2] = {0x08, 0x00};
(void) buf.insert(nextIfdOffsetInd, gpsInfo, 12);

// insert number of gps specific fields that we want to add
buf.insert(gpsIFDInd.i + tiffHeaderInd, numberOfFields, 2);
const char numberOfFields[2] = {0x08, 0x00};
(void) buf.insert(gpsIFDInd.i + tiffHeaderInd, numberOfFields, 2);

// insert the gps data
buf.insert(gpsIFDInd.i + 2 + tiffHeaderInd, gpsData.c, 0xa3);
(void) buf.insert(gpsIFDInd.i + 2 + tiffHeaderInd, gpsData.c, 0xA3);

// update the new file size and exif offsets
char2uint16_u converter;

converter.i = qToBigEndian(app1SizeEndian);
buf.replace(app1HeaderInd + 2, 2, converter.c, 2);
converter.i = nextIfdOffset + 12 + 0xa5;
buf.replace(nextIfdOffsetInd + 12, 2, converter.c, 2);
(void) buf.replace(app1HeaderInd + 2, 2, converter.c, 2);

converter.i = nextIfdOffset + 12 + 0xA5;
(void) buf.replace(nextIfdOffsetInd + 12, 2, converter.c, 2);

converter.i = (numberOfTiffFields) + 1;
buf.replace(tiffHeaderInd + 8, 2, converter.c, 2);
(void) buf.replace(tiffHeaderInd + 8, 2, converter.c, 2);

return true;
}

}
63 changes: 3 additions & 60 deletions src/AnalyzeView/ExifParser.h
Original file line number Diff line number Diff line change
Expand Up @@ -7,64 +7,7 @@

Q_DECLARE_LOGGING_CATEGORY(ExifParserLog)

class ExifParser
{
public:
ExifParser() = default;
~ExifParser() = default;

double readTime(QByteArray& buf);
bool write(QByteArray& buf, GeoTagWorker::cameraFeedbackPacket& geotag);

private:
union char2uint32_u {
char c[4];
uint32_t i;
};

union char2uint16_u {
char c[2];
uint16_t i;
};

// This struct describes a standart field used in exif files
struct field_s {
uint16_t tagID; // Describes which information is added here, e.g. GPS Lat
uint16_t type; // Describes the data type, e.g. string, uint8_t,...
uint32_t size; // Describes the size
uint32_t content; // Either contains the information, or the offset to the exif header where the information is stored (if 32 bits is not enough)
};

// This struct contains all the fields that we want to add to the image
struct fields_s {
field_s gpsVersion;
field_s gpsLatRef;
field_s gpsLat;
field_s gpsLonRef;
field_s gpsLon;
field_s gpsAltRef;
field_s gpsAlt;
field_s gpsMapDatum;
uint32_t finishedDataField;
};

// These are the additional information that can not be put into a single uin32_t
struct extended_s {
uint32_t gpsLat[6];
uint32_t gpsLon[6];
uint32_t gpsAlt[2];
char mapDatum[7];// = {0x57,0x47,0x53,0x2D,0x38,0x34,0x00};
};

// This struct contains all the information we want to add to the image
struct readable_s {
fields_s fields;
extended_s extendedData;
};

// This union is used because for writing the information we have to use a char array, but we still want the information to be available in a more descriptive way
union gpsData_u {
char c[0xa3];
readable_s readable;
};
namespace ExifParser {
double readTime(const QByteArray& buf);
bool write(QByteArray& buf, const GeoTagWorker::cameraFeedbackPacket& geotag);
};
5 changes: 2 additions & 3 deletions src/AnalyzeView/GeoTagWorker.cc
Original file line number Diff line number Diff line change
Expand Up @@ -45,7 +45,6 @@ void GeoTagWorker::run()
emit progressChanged((100/nSteps));

// Parse EXIF
ExifParser exifParser;
_imageTime.clear();
for (int i = 0; i < _imageList.size(); ++i) {
QFile file(_imageList.at(i).absoluteFilePath());
Expand All @@ -56,7 +55,7 @@ void GeoTagWorker::run()
QByteArray imageBuffer = file.readAll();
file.close();

_imageTime.append(exifParser.readTime(imageBuffer));
_imageTime.append(ExifParser::readTime(imageBuffer));

emit progressChanged((100/nSteps) + ((100/nSteps) / _imageList.size())*i);

Expand Down Expand Up @@ -144,7 +143,7 @@ void GeoTagWorker::run()
QByteArray imageBuffer = fileRead.readAll();
fileRead.close();

if (!exifParser.write(imageBuffer, _triggerList[_triggerIndices[i]])) {
if (!ExifParser::write(imageBuffer, _triggerList[_triggerIndices[i]])) {
emit error(tr("Geotagging failed. Couldn't write to image."));
return;
} else {
Expand Down
Loading

0 comments on commit e602fd9

Please sign in to comment.