diff --git a/app/input.pro b/app/input.pro index 46d2fa7cc..dc0abccf9 100644 --- a/app/input.pro +++ b/app/input.pro @@ -14,6 +14,13 @@ QT += network svg sql QT += opengl QT += core +# Exiv still uses std::auto_ptr +# remove when https://github.com/lutraconsulting/input-sdk/issues/68 is fixed +equals(QMAKE_CXX, clang++) +{ + DEFINES += "_LIBCPP_ENABLE_CXX17_REMOVED_AUTO_PTR" +} + include(android.pri) include(ios.pri) include(linux.pri) diff --git a/app/inpututils.cpp b/app/inpututils.cpp index f77b878f8..034ce74ba 100644 --- a/app/inpututils.cpp +++ b/app/inpututils.cpp @@ -38,6 +38,8 @@ #include "qgsunittypes.h" #include "qgsfeatureid.h" +#include "imageutils.h" + #include #include #include @@ -46,7 +48,6 @@ #include #include #include - #include static const QString DATE_TIME_FORMAT = QStringLiteral( "yyMMdd-hhmmss" ); @@ -1625,3 +1626,9 @@ QString InputUtils::iconFromGeometry( const QgsWkbTypes::GeometryType &geometry default: return QString( "qrc:/mIconTableLayer.svg" ); } } + +bool InputUtils::rescaleImage( const QString &path, QgsProject *activeProject ) +{ + int quality = activeProject->readNumEntry( QStringLiteral( "Mergin" ), QStringLiteral( "PhotoQuality" ), 0 ); + return ImageUtils::rescale( path, quality ); +} diff --git a/app/inpututils.h b/app/inpututils.h index 30c25dd20..552af7827 100644 --- a/app/inpututils.h +++ b/app/inpututils.h @@ -450,6 +450,11 @@ class InputUtils: public QObject */ Q_INVOKABLE void zoomToProject( QgsProject *qgsProject, QgsQuickMapSettings *mapSettings ); + /** + * Rescales image according to the project photo quality setting. + */ + Q_INVOKABLE static bool rescaleImage( const QString &path, QgsProject *activeProject ); + signals: Q_INVOKABLE void showNotificationRequested( const QString &message ); diff --git a/app/qml/ExternalResourceBundle.qml b/app/qml/ExternalResourceBundle.qml index fb562a80c..2888e0928 100644 --- a/app/qml/ExternalResourceBundle.qml +++ b/app/qml/ExternalResourceBundle.qml @@ -97,6 +97,7 @@ Item { */ property var confirmImage: function confirmImage(itemWidget, prefixToRelativePath, value) { if (value) { + __inputUtils.rescaleImage(value, __activeProject.qgsProject) var newCurrentValue = __inputUtils.getRelativePath(value, prefixToRelativePath) itemWidget.editorValueChanged(newCurrentValue, newCurrentValue === "" || newCurrentValue === null) } diff --git a/app/sources.pri b/app/sources.pri index 415d0d91f..71251fe7c 100644 --- a/app/sources.pri +++ b/app/sources.pri @@ -134,7 +134,8 @@ contains(DEFINES, INPUT_TEST) { test/testvariablesmanager.cpp \ test/testformeditors.cpp \ test/testmodels.cpp \ - test/testcoreutils.cpp + test/testcoreutils.cpp \ + test/testimageutils.cpp HEADERS += \ test/inputtests.h \ @@ -153,7 +154,8 @@ contains(DEFINES, INPUT_TEST) { test/testvariablesmanager.h \ test/testformeditors.h \ test/testmodels.h \ - test/testcoreutils.h + test/testcoreutils.h \ + test/testimageutils.h } contains(DEFINES, APPLE_PURCHASING) { diff --git a/app/test/inputtests.cpp b/app/test/inputtests.cpp index 354822770..aa082defc 100644 --- a/app/test/inputtests.cpp +++ b/app/test/inputtests.cpp @@ -25,6 +25,7 @@ #include "test/testformeditors.h" #include "test/testmodels.h" #include "test/testcoreutils.h" +#include "test/testimageutils.h" #if not defined APPLE_PURCHASING #include "test/testpurchasing.h" @@ -160,6 +161,11 @@ int InputTests::runTest() const TestCoreUtils coreUtilsTest; nFailed = QTest::qExec( &coreUtilsTest, mTestArgs ); } + else if ( mTestRequested == "--testImageUtils" ) + { + TestImageUtils imageUtilsTest; + nFailed = QTest::qExec( &imageUtilsTest, mTestArgs ); + } #if not defined APPLE_PURCHASING else if ( mTestRequested == "--testPurchasing" ) { diff --git a/app/test/testimageutils.cpp b/app/test/testimageutils.cpp new file mode 100644 index 000000000..6a263f394 --- /dev/null +++ b/app/test/testimageutils.cpp @@ -0,0 +1,50 @@ +/*************************************************************************** + * * + * This program is free software; you can redistribute it and/or modify * + * it under the terms of the GNU General Public License as published by * + * the Free Software Foundation; either version 2 of the License, or * + * (at your option) any later version. * + * * + ***************************************************************************/ + +#include "testimageutils.h" +#include "testutils.h" +#include "imageutils.h" + +#include +#include +#include + +#include + +void TestImageUtils::init() +{ +} + +void TestImageUtils::cleanup() +{ +} + +void TestImageUtils::testRescale() +{ + QTemporaryDir dir; + QString testPhotoName = QStringLiteral( "photo.jpg" ); + QFile::copy( TestUtils::testDataDir() + '/' + testPhotoName, dir.filePath( testPhotoName ) ); + + QVERIFY( ImageUtils::rescale( dir.filePath( testPhotoName ), 3 ) ); + + QImage img( dir.filePath( testPhotoName ) ); + QCOMPARE( img.height(), 1000 ); + + // check EXIF tags + std::unique_ptr< Exiv2::Image > image( Exiv2::ImageFactory::open( dir.filePath( testPhotoName ).toStdString() ) ); + + image->readMetadata(); + Exiv2::ExifData &exifData = image->exifData(); + QVERIFY( !exifData.empty() ); + + const Exiv2::ExifData::iterator itElevVal = exifData.findKey( Exiv2::ExifKey( "Exif.GPSInfo.GPSAltitude" ) ); + const Exiv2::Rational rational = itElevVal->value().toRational( 0 ); + double val = static_cast< double >( rational.first ) / rational.second; + QCOMPARE( val, 133 ); +} diff --git a/app/test/testimageutils.h b/app/test/testimageutils.h new file mode 100644 index 000000000..6e7f4450d --- /dev/null +++ b/app/test/testimageutils.h @@ -0,0 +1,26 @@ +/*************************************************************************** + * * + * This program is free software; you can redistribute it and/or modify * + * it under the terms of the GNU General Public License as published by * + * the Free Software Foundation; either version 2 of the License, or * + * (at your option) any later version. * + * * + ***************************************************************************/ + +#ifndef TESTIMAGEUTILS_H +#define TESTIMAGEUTILS_H + +#include + +class TestImageUtils : public QObject +{ + Q_OBJECT + + private slots: + void init(); // will be called before each testfunction is executed. + void cleanup(); // will be called after every testfunction. + + void testRescale(); +}; + +#endif // TESTIMAGEUTILS_H diff --git a/core/core.pri b/core/core.pri index 6667237b4..270c5e972 100644 --- a/core/core.pri +++ b/core/core.pri @@ -12,7 +12,8 @@ SOURCES += \ $$PWD/localprojectsmanager.cpp \ $$PWD/merginprojectmetadata.cpp \ $$PWD/project.cpp \ - $$PWD/geodiffutils.cpp + $$PWD/geodiffutils.cpp \ + $$PWD/imageutils.cpp HEADERS += \ $$PWD/coreutils.h \ @@ -27,7 +28,8 @@ HEADERS += \ $$PWD/localprojectsmanager.h \ $$PWD/merginprojectmetadata.h \ $$PWD/project.h \ - $$PWD/geodiffutils.h + $$PWD/geodiffutils.h \ + $$PWD/imageutils.h exists($$PWD/merginsecrets.cpp) { message("Using production Mergin API_KEYS") diff --git a/core/imageutils.cpp b/core/imageutils.cpp new file mode 100644 index 000000000..1ab5a59d1 --- /dev/null +++ b/core/imageutils.cpp @@ -0,0 +1,135 @@ +/*************************************************************************** + * * + * This program is free software; you can redistribute it and/or modify * + * it under the terms of the GNU General Public License as published by * + * the Free Software Foundation; either version 2 of the License, or * + * (at your option) any later version. * + * * + ***************************************************************************/ + +#include "imageutils.h" + +#include "coreutils.h" + +#include +#include +#include + +#include + +bool ImageUtils::copyExifMetadata( const QString &sourceImage, const QString &targetImage ) +{ + if ( !QFileInfo::exists( sourceImage ) || !QFileInfo::exists( targetImage ) ) + return false; + + try + { + std::unique_ptr< Exiv2::Image > srcImage( Exiv2::ImageFactory::open( sourceImage.toStdString() ) ); + if ( !srcImage ) + return false; + + std::unique_ptr< Exiv2::Image > dstImage( Exiv2::ImageFactory::open( targetImage.toStdString() ) ); + if ( !dstImage ) + return false; + + srcImage->readMetadata(); + Exiv2::ExifData &exifData = srcImage->exifData(); + if ( exifData.empty() ) + { + return true; + } + + dstImage->setExifData( exifData ); + dstImage->writeMetadata(); + return true; + } + catch ( ... ) + { + CoreUtils::log( "copying EXIF", QStringLiteral( "Failed to copy EXIF metadata" ) ); + return false; + } +} + +bool ImageUtils::rescale( const QString &path, int quality ) +{ + + QImage sourceImage( path ); + bool isPortrait = sourceImage.height() > sourceImage.width(); + int size = isPortrait ? sourceImage.width() : sourceImage.height(); + + int newSize = size; + switch ( quality ) + { + case 0: // original quality, no rescaling needed + { + break; + } + case 1: // high quality, output image size ~2-4 Mb + { + newSize = 3000; + break; + } + case 2: // medium quality, output image size ~1-2 Mb + { + newSize = 1500; + break; + } + case 3: // low quality, output image size ~0.5 Mb + { + newSize = 1000; + break; + } + } + + // if image width or height (depending on the orientation) is smaller + // than new size we keep original image + if ( size <= newSize ) + { + return true; + } + + // rescale + QImage rescaledImage; + if ( isPortrait ) + { + rescaledImage = sourceImage.scaledToWidth( newSize, Qt::SmoothTransformation ); + } + else + { + rescaledImage = sourceImage.scaledToHeight( newSize, Qt::SmoothTransformation ); + } + + if ( rescaledImage.isNull() ) + { + CoreUtils::log( "rescaling image", QStringLiteral( "Failed to rescale %1" ).arg( path ) ); + return false; + } + + QFileInfo fi( path ); + QString newPath = QStringLiteral( "%1/%2_rescaled.%3" ).arg( fi.path(), fi.baseName(), fi.completeSuffix() ); + + if ( !rescaledImage.save( newPath ) ) + { + CoreUtils::log( "rescaling image", QStringLiteral( "Failed to save rescaled image" ) ); + return false; + } + + // copy EXIF from source image to rescaled image + if ( !copyExifMetadata( path, newPath ) ) + { + CoreUtils::log( "rescaling image", QStringLiteral( "Failed to copy EXIF metadata from original image" ) ); + return false; + } + + // remove original file and rename rescaled version + if ( QFile::remove( path ) ) + { + if ( QFile::rename( newPath, path ) ) + { + return true; + } + } + + CoreUtils::log( "rescaling image", QStringLiteral( "Can not replace original file with rescaled version" ) ); + return false; +} diff --git a/core/imageutils.h b/core/imageutils.h new file mode 100644 index 000000000..541d4a737 --- /dev/null +++ b/core/imageutils.h @@ -0,0 +1,33 @@ +/*************************************************************************** + * * + * This program is free software; you can redistribute it and/or modify * + * it under the terms of the GNU General Public License as published by * + * the Free Software Foundation; either version 2 of the License, or * + * (at your option) any later version. * + * * + ***************************************************************************/ + +#ifndef IMAGEUTILS_H +#define IMAGEUTILS_H + +#include + +class ImageUtils +{ + public: + explicit ImageUtils( ) = default; + ~ImageUtils() = default; + + /** + * Copies EXIF metadata from sourceImage to targetImage. + */ + static bool copyExifMetadata( const QString &sourceImage, const QString &targetImage ); + + /** + * Rescales image to the given quality taking into account its orientation + * and preserving EXIF metadata. + */ + static bool rescale( const QString &path, int quality ); +}; + +#endif // IMAGEUTILS_H diff --git a/scripts/run_all_tests.bash b/scripts/run_all_tests.bash index 7a77ce7aa..1c71d6e7f 100755 --- a/scripts/run_all_tests.bash +++ b/scripts/run_all_tests.bash @@ -51,6 +51,9 @@ NFAILURES=$(($NFAILURES+$?)) $INPUT_EXECUTABLE --testCoreUtils NFAILURES=$(($NFAILURES+$?)) +$INPUT_EXECUTABLE --testImageUtils +NFAILURES=$(($NFAILURES+$?)) + echo "Total $NFAILURES failures found in testing" exit $NFAILURES diff --git a/test/test_data/photo.jpg b/test/test_data/photo.jpg new file mode 100644 index 000000000..e4b7f3d3b Binary files /dev/null and b/test/test_data/photo.jpg differ