Skip to content

Commit

Permalink
AnalyzeView: Add Exiv2 for writing exif data
Browse files Browse the repository at this point in the history
  • Loading branch information
HTRamsey committed Sep 21, 2024
1 parent 008ae6e commit b1b1e01
Show file tree
Hide file tree
Showing 5 changed files with 168 additions and 185 deletions.
2 changes: 1 addition & 1 deletion .github/workflows/macos.yml
Original file line number Diff line number Diff line change
Expand Up @@ -56,7 +56,7 @@ jobs:
- name: Install Dependencies
run: |
brew update
brew install cmake ninja ccache geographiclib SDL2
brew install cmake ninja ccache geographiclib SDL2 exiv2
- name: Install Gstreamer
run: |
Expand Down
60 changes: 59 additions & 1 deletion src/AnalyzeView/CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,64 @@ target_link_libraries(AnalyzeView
QmlControls
)

set(MINIMUM_EXIV2_VERSION 0.28.3)

if(NOT QGC_BUILD_DEPENDENCIES)
find_package(Exiv2 ${MINIMUM_EXIV2_VERSION} CONFIG)
if(Exiv2_FOUND)
message(STATUS "Found Exiv2 ${Exiv2_VERSION_STRING}")
target_link_libraries(AnalyzeView PRIVATE Exiv2::exiv2lib)
else()
find_package(PkgConfig)
if(PkgConfig_FOUND)
pkg_check_modules(Exiv2 IMPORTED_TARGET exiv2>=${MINIMUM_EXIV2_VERSION})
if(Exiv2_FOUND)
message(STATUS "Found Exiv2 ${Exiv2_VERSION}")
target_link_libraries(AnalyzeView PRIVATE PkgConfig::Exiv2)
endif()
endif()
endif()
endif()

if(NOT Exiv2_FOUND)
message(STATUS "Building Exiv2")
include(FetchContent)
FetchContent_Declare(EXIV2
GIT_REPOSITORY https://github.com/Exiv2/exiv2.git
GIT_TAG v0.28.3
GIT_SHALLOW TRUE
GIT_PROGRESS TRUE
)
set(EXIV2_ENABLE_XMP OFF CACHE INTERNAL "" FORCE)
set(EXIV2_ENABLE_EXTERNAL_XMP OFF CACHE INTERNAL "" FORCE)
set(EXIV2_ENABLE_PNG OFF CACHE INTERNAL "" FORCE)
set(EXIV2_ENABLE_NLS OFF CACHE INTERNAL "" FORCE)
set(EXIV2_ENABLE_LENSDATA OFF CACHE INTERNAL "" FORCE)
set(EXIV2_ENABLE_DYNAMIC_RUNTIME ON CACHE INTERNAL "" FORCE)
set(EXIV2_ENABLE_WEBREADY OFF CACHE INTERNAL "" FORCE)
set(EXIV2_ENABLE_CURL OFF CACHE INTERNAL "" FORCE)
set(EXIV2_ENABLE_BMFF OFF CACHE INTERNAL "" FORCE)
set(EXIV2_ENABLE_BROTLI OFF CACHE INTERNAL "" FORCE)
set(EXIV2_ENABLE_VIDEO OFF CACHE INTERNAL "" FORCE)
set(EXIV2_ENABLE_INIH OFF CACHE INTERNAL "" FORCE)
set(EXIV2_ENABLE_FILESYSTEM_ACCESS ON CACHE INTERNAL "" FORCE)
set(EXIV2_BUILD_SAMPLES OFF CACHE INTERNAL "" FORCE)
set(EXIV2_BUILD_EXIV2_COMMAND OFF CACHE INTERNAL "" FORCE)
set(EXIV2_BUILD_UNIT_TESTS OFF CACHE INTERNAL "" FORCE)
set(EXIV2_BUILD_FUZZ_TESTS OFF CACHE INTERNAL "" FORCE)
set(EXIV2_BUILD_DOC OFF CACHE INTERNAL "" FORCE)
set(BUILD_WITH_CCACHE ON CACHE INTERNAL "" FORCE)
FetchContent_MakeAvailable(EXIV2)

target_link_libraries(AnalyzeView PRIVATE exiv2lib)
target_include_directories(AnalyzeView
PRIVATE
${CMAKE_BINARY_DIR}
${exiv2_SOURCE_DIR}/include
${exiv2_SOURCE_DIR}/include/exiv2
)
endif()

include(FetchContent)
FetchContent_Declare(easyexif
GIT_REPOSITORY https://github.com/mayanklahiri/easyexif.git
Expand All @@ -69,7 +127,7 @@ target_sources(AnalyzeView
${easyexif_SOURCE_DIR}/exif.h
)

target_include_directories(AnalyzeView
target_include_directories(AnalyzeView
PUBLIC
${CMAKE_CURRENT_SOURCE_DIR}
${easyexif_SOURCE_DIR}
Expand Down
289 changes: 106 additions & 183 deletions src/AnalyzeView/ExifParser.cc
Original file line number Diff line number Diff line change
Expand Up @@ -12,66 +12,12 @@

#include <QtCore/QByteArray>
#include <QtCore/QDateTime>
#include <QtCore/QtEndian>

#include <exif.h>
#include <exiv2/exiv2.hpp>

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

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;
};
} // namespace

namespace ExifParser {

double readTime(const QByteArray &buf)
Expand Down Expand Up @@ -110,136 +56,113 @@ double readTime(const QByteArray &buf)
return (tagTime.toMSecsSinceEpoch() / 1000.0);
}

double readTime2(const QByteArray &buf)
{
try {
// Convert QByteArray to std::string for Exiv2
const Exiv2::Image::UniquePtr image = Exiv2::ImageFactory::open(reinterpret_cast<const Exiv2::byte*>(buf.constData()), buf.size());
image->readMetadata();

Exiv2::ExifData &exifData = image->exifData();
if (exifData.empty()) {
qCWarning(ExifParserLog) << "No EXIF data found in the image.";
return -1.0;
}

// Read DateTimeOriginal
// Exiv2::ExifData::const_iterator it = dateTimeOriginal(exifData);
const Exiv2::ExifKey key("Exif.Photo.DateTimeOriginal");
const Exiv2::ExifData::iterator pos = exifData.findKey(key);
if (pos == exifData.end()) {
qCWarning(ExifParserLog) << "No DateTimeOriginal found.";
return -1.0;
}

const std::string dateTimeOriginal = pos->toString();
const QString createDate = QString::fromStdString(dateTimeOriginal);
const QStringList createDateList = createDate.split(' ');

if (createDateList.size() < 2) {
qCWarning(ExifParserLog) << "Invalid date/time format: " << createDateList;
return -1.0;
}

const QStringList dateList = createDateList[0].split(':');
const QStringList timeList = createDateList[1].split(':');

if (dateList.size() < 3 || timeList.size() < 3) {
qCWarning(ExifParserLog) << "Could not parse creation date/time: " << dateList << " " << timeList;
return -1.0;
}

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

const QDateTime tagTime(date, time);

return (tagTime.toMSecsSinceEpoch() / 1000.0);
} catch (Exiv2::Error& e) {
qCWarning(ExifParserLog) << "Error reading EXIF data:" << e.what();
return -1.0;
}
}

bool write(QByteArray &buf, const GeoTagWorker::cameraFeedbackPacket &geotag)
{
static const QByteArray app1Header("\xff\xe1", 2);

const uint32_t app1HeaderInd = buf.indexOf(app1Header);
const uint16_t *conversionPointer = reinterpret_cast<const uint16_t*>(buf.mid(app1HeaderInd + 2, 2).constData());
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<const uint16_t*>(buf.mid(tiffHeaderInd + 8, 2).constData());
const uint16_t numberOfTiffFields = *conversionPointer;

const uint32_t nextIfdOffsetInd = tiffHeaderInd + 10 + (12 * numberOfTiffFields);
conversionPointer = reinterpret_cast<const uint16_t*>(buf.mid(nextIfdOffsetInd, 2).constData());
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);
(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
const uint32_t gpsDataExtInd = gpsIFDInd.i + 2 + sizeof(fields_s);

gpsData_u gpsData;

// Filling up the fields with the corresponding values
gpsData.readable.fields.gpsVersion.tagID = 0;
gpsData.readable.fields.gpsVersion.type = 1;
gpsData.readable.fields.gpsVersion.size = 4;
gpsData.readable.fields.gpsVersion.content = 2;

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.gpsLat.tagID = 2;
gpsData.readable.fields.gpsLat.type = 5;
gpsData.readable.fields.gpsLat.size = 3;
gpsData.readable.fields.gpsLat.content = gpsDataExtInd;

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.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.gpsAltRef.tagID = 5;
gpsData.readable.fields.gpsAltRef.type = 1;
gpsData.readable.fields.gpsAltRef.size = 1;
gpsData.readable.fields.gpsAltRef.content = 0x00;

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.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.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[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[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[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[5] = 1000;

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';
gpsData.readable.extendedData.mapDatum[2] = 'S';
gpsData.readable.extendedData.mapDatum[3] = '-';
gpsData.readable.extendedData.mapDatum[4] = '8';
gpsData.readable.extendedData.mapDatum[5] = '4';
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
(void) buf.remove(nextIfdOffsetInd + 4, 12);

// TODO correct size in image description

// insert Gps Info to image file
(void) buf.insert(nextIfdOffsetInd, gpsInfo, 12);

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

// insert the gps data
(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);
(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;
(void) buf.replace(tiffHeaderInd + 8, 2, converter.c, 2);

return true;
try {
// Convert QByteArray to std::string for Exiv2
const Exiv2::Image::UniquePtr image = Exiv2::ImageFactory::open(reinterpret_cast<const Exiv2::byte*>(buf.constData()), buf.size());
image->readMetadata();

Exiv2::ExifData &exifData = image->exifData();

// Set GPSVersionID
exifData["Exif.GPSInfo.GPSVersionID"] = "2 2 0 0";

// Set GPS map datum
exifData["Exif.GPSInfo.GPSMapDatum"] = "WGS-84";

// Latitude in degrees, minutes, seconds
const double latitude = std::fabs(geotag.latitude); // Absolute value for conversion
const int latDegrees = static_cast<int>(latitude);
const int latMinutes = static_cast<int>((latitude - latDegrees) * 60);
const double latSeconds = (latitude - latDegrees - latMinutes / 60.0) * 3600.0;

// Set GPS latitude
exifData["Exif.GPSInfo.GPSLatitudeRef"] = (geotag.latitude > 0) ? "N" : "S";
exifData["Exif.GPSInfo.GPSLatitude"] =
std::to_string(latDegrees) + "/1 " +
std::to_string(latMinutes) + "/1 " +
std::to_string(static_cast<int>(latSeconds * 1000)) + "/1000";

// Longitude in degrees, minutes, seconds
const double longitude = std::fabs(geotag.longitude);
const int lonDegrees = static_cast<int>(longitude);
const int lonMinutes = static_cast<int>((longitude - lonDegrees) * 60);
const double lonSeconds = (longitude - lonDegrees - lonMinutes / 60.0) * 3600.0;

// Set GPS longitude
exifData["Exif.GPSInfo.GPSLongitudeRef"] = (geotag.longitude > 0) ? "E" : "W";
exifData["Exif.GPSInfo.GPSLongitude"] =
std::to_string(lonDegrees) + "/1 " +
std::to_string(lonMinutes) + "/1 " +
std::to_string(static_cast<int>(lonSeconds * 1000)) + "/1000";

// Set GPS altitude
exifData["Exif.GPSInfo.GPSAltitudeRef"] = (geotag.altitude < 0) ? 1 : 0;
exifData["Exif.GPSInfo.GPSAltitude"] = std::to_string(static_cast<uint32_t>(geotag.altitude * 100)) + "/100";

// Write the updated metadata back to the buffer
image->setExifData(exifData);
image->writeMetadata();

// Update the buffer with new image data
buf = QByteArray(reinterpret_cast<const char*>(image->io().mmap()), image->io().size());
return true;
} catch (Exiv2::Error& e) {
qCWarning(ExifParserLog) << "Error writing EXIF GPS data:" << e.what();
return false;
}
}

} // namespace ExifParser
1 change: 1 addition & 0 deletions src/AnalyzeView/ExifParser.h
Original file line number Diff line number Diff line change
Expand Up @@ -19,5 +19,6 @@ Q_DECLARE_LOGGING_CATEGORY(ExifParserLog)

namespace ExifParser {
double readTime(const QByteArray &buf);
double readTime2(const QByteArray &buf);
bool write(QByteArray &buf, const GeoTagWorker::cameraFeedbackPacket &geotag);
} // namespace ExifParser
Loading

0 comments on commit b1b1e01

Please sign in to comment.