diff --git a/.github/workflows/macos.yml b/.github/workflows/macos.yml index fb4d8fcf33b..893b72f343e 100644 --- a/.github/workflows/macos.yml +++ b/.github/workflows/macos.yml @@ -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: | diff --git a/CMakeLists.txt b/CMakeLists.txt index 2b38735430a..acb3e7f2f02 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -85,16 +85,16 @@ elseif(WIN32) message(STATUS "Using SCCache") set(CMAKE_C_COMPILER_LAUNCHER ${SCCACHE_PROGRAM}) set(CMAKE_CXX_COMPILER_LAUNCHER ${SCCACHE_PROGRAM}) - if(MSVC) - if(CMAKE_BUILD_TYPE STREQUAL "Debug") - string(REPLACE "/Zi" "/Z7" CMAKE_CXX_FLAGS_DEBUG "${CMAKE_CXX_FLAGS_DEBUG}") - string(REPLACE "/Zi" "/Z7" CMAKE_C_FLAGS_DEBUG "${CMAKE_C_FLAGS_DEBUG}") - elseif(CMAKE_BUILD_TYPE STREQUAL "Release") - string(REPLACE "/Zi" "/Z7" CMAKE_CXX_FLAGS_RELEASE "${CMAKE_CXX_FLAGS_RELEASE}") - string(REPLACE "/Zi" "/Z7" CMAKE_C_FLAGS_RELEASE "${CMAKE_C_FLAGS_RELEASE}") - endif() - set(CMAKE_MSVC_DEBUG_INFORMATION_FORMAT Embedded) + endif() + if(MSVC) + if(CMAKE_BUILD_TYPE STREQUAL "Debug") + string(REPLACE "/Zi" "/Z7" CMAKE_CXX_FLAGS_DEBUG "${CMAKE_CXX_FLAGS_DEBUG}") + string(REPLACE "/Zi" "/Z7" CMAKE_C_FLAGS_DEBUG "${CMAKE_C_FLAGS_DEBUG}") + elseif(CMAKE_BUILD_TYPE STREQUAL "Release") + string(REPLACE "/Zi" "/Z7" CMAKE_CXX_FLAGS_RELEASE "${CMAKE_CXX_FLAGS_RELEASE}") + string(REPLACE "/Zi" "/Z7" CMAKE_C_FLAGS_RELEASE "${CMAKE_C_FLAGS_RELEASE}") endif() + set(CMAKE_MSVC_DEBUG_INFORMATION_FORMAT Embedded) endif() endif() diff --git a/src/AnalyzeView/CMakeLists.txt b/src/AnalyzeView/CMakeLists.txt index cfdeeeb252e..4531ad9295b 100644 --- a/src/AnalyzeView/CMakeLists.txt +++ b/src/AnalyzeView/CMakeLists.txt @@ -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 @@ -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} diff --git a/src/AnalyzeView/ExifParser.cc b/src/AnalyzeView/ExifParser.cc index f2a2f9f0876..d4cf08c37db 100644 --- a/src/AnalyzeView/ExifParser.cc +++ b/src/AnalyzeView/ExifParser.cc @@ -3,66 +3,12 @@ #include #include -#include #include +#include 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) @@ -101,136 +47,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(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(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(buf.mid(tiffHeaderInd + 8, 2).constData()); - const uint16_t numberOfTiffFields = *conversionPointer; - - const uint32_t nextIfdOffsetInd = tiffHeaderInd + 10 + (12 * numberOfTiffFields); - conversionPointer = reinterpret_cast(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(geotag.latitude)); - gpsData.readable.extendedData.gpsLat[1] = 1; - gpsData.readable.extendedData.gpsLat[2] = static_cast((fabs(geotag.latitude) - (floor(fabs(geotag.latitude))) * 60.0)); - gpsData.readable.extendedData.gpsLat[3] = 1; - gpsData.readable.extendedData.gpsLat[4] = static_cast(((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(geotag.longitude)); - gpsData.readable.extendedData.gpsLon[1] = 1; - gpsData.readable.extendedData.gpsLon[2] = static_cast((fabs(geotag.longitude) - (floor(fabs(geotag.longitude))) * 60.0)); - gpsData.readable.extendedData.gpsLon[3] = 1; - gpsData.readable.extendedData.gpsLon[4] = static_cast(((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(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(latitude); + const int latMinutes = static_cast((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(latSeconds * 1000)) + "/1000"; + + // Longitude in degrees, minutes, seconds + const double longitude = std::fabs(geotag.longitude); + const int lonDegrees = static_cast(longitude); + const int lonMinutes = static_cast((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(lonSeconds * 1000)) + "/1000"; + + // Set GPS altitude + exifData["Exif.GPSInfo.GPSAltitudeRef"] = (geotag.altitude < 0) ? 1 : 0; + exifData["Exif.GPSInfo.GPSAltitude"] = std::to_string(static_cast(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(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 diff --git a/src/AnalyzeView/ExifParser.h b/src/AnalyzeView/ExifParser.h index 10bc94bb6a6..ce104025923 100644 --- a/src/AnalyzeView/ExifParser.h +++ b/src/AnalyzeView/ExifParser.h @@ -10,5 +10,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 diff --git a/tools/setup/install-dependencies-debian.sh b/tools/setup/install-dependencies-debian.sh index 027a33fee8e..5213a1a04ed 100755 --- a/tools/setup/install-dependencies-debian.sh +++ b/tools/setup/install-dependencies-debian.sh @@ -85,6 +85,7 @@ DEBIAN_FRONTEND=noninteractive apt -y --quiet install \ libass-dev \ libdrm-dev \ libegl1-mesa-dev \ + libexiv2-dev \ libgbm-dev \ libgl1-mesa-dev \ libgles2-mesa-dev \