From a4021129d2abb8464c02ee4eb5c0242536248052 Mon Sep 17 00:00:00 2001 From: Mathieu Pellerin Date: Fri, 30 Aug 2024 13:38:09 +0700 Subject: [PATCH 1/3] Add basic text stamping functionality on photos taken by QField camera --- images/images.qrc | 1 + .../qfield/nodpi/ic_text_black_24dp.svg | 4 ++ src/core/utils/fileutils.cpp | 43 +++++++++++++++- src/core/utils/fileutils.h | 2 + src/core/utils/positioningutils.cpp | 5 ++ src/core/utils/positioningutils.h | 5 ++ src/qml/QFieldCamera.qml | 49 ++++++++++++++++--- 7 files changed, 100 insertions(+), 9 deletions(-) create mode 100644 images/themes/qfield/nodpi/ic_text_black_24dp.svg diff --git a/images/images.qrc b/images/images.qrc index 8a6f8658b9..0cb5c552dc 100644 --- a/images/images.qrc +++ b/images/images.qrc @@ -11,6 +11,7 @@ pictures/qfield-love.png + themes/qfield/nodpi/ic_text_black_24dp.svg themes/qfield/nodpi/ic_3x3_grid_white_24dp.svg themes/qfield/nodpi/ic_digitizing_settings_black_24dp.svg themes/qfield/nodpi/ic_undo_black_24dp.svg diff --git a/images/themes/qfield/nodpi/ic_text_black_24dp.svg b/images/themes/qfield/nodpi/ic_text_black_24dp.svg new file mode 100644 index 0000000000..9841849a35 --- /dev/null +++ b/images/themes/qfield/nodpi/ic_text_black_24dp.svg @@ -0,0 +1,4 @@ + + + + diff --git a/src/core/utils/fileutils.cpp b/src/core/utils/fileutils.cpp index b69f0e8350..c8d96c1998 100644 --- a/src/core/utils/fileutils.cpp +++ b/src/core/utils/fileutils.cpp @@ -24,6 +24,8 @@ #include #include #include +#include +#include #include #include #include @@ -183,7 +185,7 @@ void FileUtils::restrictImageSize( const QString &imagePath, int maximumWidthHei QImage scaledImage = img.width() > img.height() ? img.scaledToWidth( maximumWidthHeight, Qt::SmoothTransformation ) : img.scaledToHeight( maximumWidthHeight, Qt::SmoothTransformation ); - scaledImage.save( imagePath ); + scaledImage.save( imagePath, nullptr, 90 ); for ( const QString &key : metadata.keys() ) { @@ -236,3 +238,42 @@ void FileUtils::addImageMetadata( const QString &imagePath, const GnssPositionIn QgsExifTools::tagImage( imagePath, key, metadata[key] ); } } + +void FileUtils::addImageStamp( const QString &imagePath, const QString &text ) +{ + if ( !QFileInfo::exists( imagePath ) || text.isEmpty() ) + { + return; + } + + QVariantMap metadata = QgsExifTools::readTags( imagePath ); + QImage img( imagePath ); + if ( !img.isNull() ) + { + QPainter painter( &img ); + painter.setRenderHint( QPainter::Antialiasing ); + + QFont font = painter.font(); + const int pixelSize = std::min( img.width(), img.height() ) / 45; + font.setPixelSize( pixelSize ); + font.setBold( true ); + + QPainterPath path; + QStringList lines = text.split( QStringLiteral( "\n" ) ); + for ( int i = 0; i < lines.size(); i++ ) + { + path.addText( QPointF( 10, img.height() - ( ( pixelSize + pixelSize / 4 ) * ( lines.size() - i - 1 ) ) - 10 ), font, lines.at( i ) ); + } + + painter.setPen( Qt::black ); + painter.setBrush( Qt::white ); + painter.drawPath( path ); + + img.save( imagePath, nullptr, 90 ); + + for ( const QString &key : metadata.keys() ) + { + QgsExifTools::tagImage( imagePath, key, metadata[key] ); + } + } +} diff --git a/src/core/utils/fileutils.h b/src/core/utils/fileutils.h index 470d199dbd..45226f9841 100644 --- a/src/core/utils/fileutils.h +++ b/src/core/utils/fileutils.h @@ -55,6 +55,8 @@ class QFIELD_CORE_EXPORT FileUtils : public QObject Q_INVOKABLE void addImageMetadata( const QString &imagePath, const GnssPositionInformation &positionInformation ); + Q_INVOKABLE void addImageStamp( const QString &imagePath, const QString &text ); + static bool copyRecursively( const QString &sourceFolder, const QString &destFolder, QgsFeedback *feedback = nullptr, bool wipeDestFolder = true ); /** * Creates checksum of a file. Returns null QByteArray if cannot be calculated. diff --git a/src/core/utils/positioningutils.cpp b/src/core/utils/positioningutils.cpp index 9de0a0ef32..727d1462ab 100644 --- a/src/core/utils/positioningutils.cpp +++ b/src/core/utils/positioningutils.cpp @@ -35,6 +35,11 @@ GnssPositionInformation PositioningUtils::createGnssPositionInformation( double verticalSpeed, magneticVariation, 0, sourceName ); } +GnssPositionInformation PositioningUtils::createEmptyGnssPositionInformation() +{ + return GnssPositionInformation(); +} + GnssPositionInformation PositioningUtils::averagedPositionInformation( const QList &positionsInformation ) { QList convertedList; diff --git a/src/core/utils/positioningutils.h b/src/core/utils/positioningutils.h index 966763943f..e7b9407618 100644 --- a/src/core/utils/positioningutils.h +++ b/src/core/utils/positioningutils.h @@ -37,6 +37,11 @@ class QFIELD_CORE_EXPORT PositioningUtils : public QObject */ static Q_INVOKABLE GnssPositionInformation createGnssPositionInformation( double latitude, double longitude, double altitude, double speed, double direction, double horizontalAccuracy, double verticalAcurracy, double verticalSpeed, double magneticVariation, const QDateTime ×tamp, const QString &sourceName ); + /** + * Creates an empty GnssPositionInformation. + */ + static Q_INVOKABLE GnssPositionInformation createEmptyGnssPositionInformation(); + /** * Returns an average GnssPositionInformation from a list of position information */ diff --git a/src/qml/QFieldCamera.qml b/src/qml/QFieldCamera.qml index cad5e49078..62ec1ba9bd 100644 --- a/src/qml/QFieldCamera.qml +++ b/src/qml/QFieldCamera.qml @@ -14,8 +14,8 @@ Popup { property bool isCapturing: state == "PhotoCapture" || state == "VideoCapture" property bool isPortraitMode: mainWindow.height > mainWindow.width - property string currentPath - property var currentPosition + property string currentPath: '' + property var currentPosition: PositioningUtils.createEmptyGnssPositionInformation() signal finished(string path) signal canceled @@ -75,6 +75,7 @@ Popup { Settings { id: cameraSettings + property bool stamping: false property bool geoTagging: true property bool showGrid: false property string deviceId: '' @@ -82,6 +83,16 @@ Popup { property int pixelFormat: 0 } + ExpressionEvaluator { + id: stampExpressionEvaluator + + mode: ExpressionEvaluator.ExpressionMode + expressionText: "format_date(now(), 'yyyy-MM-dd @ HH:mm') || if(@gnss_coordinate is not null, format('\nLatitude %1 | Longitude %2 | Altitude %3 | Speed %4', coalesce(y(@gnss_coordinate), 'N/A'), coalesce(x(@gnss_coordinate), 'N/A'), coalesce(z(@gnss_coordinate), 'N/A'), if(@gnss_ground_speed != 'nan', @gnss_ground_speed || ' m/s', 'N/A')), '')" + + project: qgisProject + positionInformation: currentPosition + } + Page { width: parent.width height: parent.height @@ -354,12 +365,13 @@ Popup { onClicked: { if (cameraItem.state == "PhotoCapture") { captureSession.imageCapture.captureToFile(qgisProject.homePath + '/DCIM/'); - if (cameraSettings.geoTagging) { - if (positionSource.active) { - currentPosition = positionSource.positionInformation; - } else { - displayToast(qsTr("Image geotagging requires positioning to be turned on"), "warning"); - } + if (positionSource.active) { + currentPosition = positionSource.positionInformation; + } else { + currentPosition = PositioningUtils.createEmptyGnssPositionInformation(); + } + if (cameraSettings.geoTagging && !positionSource.active) { + displayToast(qsTr("Image geotagging requires positioning to be turned on"), "warning"); } } else if (cameraItem.state == "VideoCapture") { if (captureSession.recorder.recorderState === MediaRecorder.StoppedState) { @@ -376,6 +388,9 @@ Popup { if (cameraSettings.geoTagging && positionSource.active) { FileUtils.addImageMetadata(currentPath, currentPosition); } + if (cameraSettings.stamping) { + FileUtils.addImageStamp(currentPath, stampExpressionEvaluator.evaluate()); + } } cameraItem.finished(currentPath); } @@ -552,6 +567,24 @@ Popup { } } + QfToolButton { + id: stampingButton + + width: 40 + height: 40 + padding: 2 + + iconSource: Theme.getThemeVectorIcon("ic_text_black_24dp") + iconColor: cameraSettings.stamping ? Theme.mainColor : "white" + bgcolor: Theme.darkGraySemiOpaque + round: true + + onClicked: { + cameraSettings.stamping = !cameraSettings.stamping; + displayToast(cameraSettings.stamping ? qsTr("Details stamping enabled") : qsTr("Details tamping disabled")); + } + } + QfToolButton { id: geotagButton From 62c970f5b635053464b3d4e26e2d0e8a35b4f559 Mon Sep 17 00:00:00 2001 From: Mathieu Pellerin Date: Fri, 30 Aug 2024 13:42:42 +0700 Subject: [PATCH 2/3] Our camera is now in many ways superior to native camera, let's use it by default --- src/qml/QFieldSettings.qml | 10 +++++----- src/qml/editorwidgets/ExternalResource.qml | 4 ++-- 2 files changed, 7 insertions(+), 7 deletions(-) diff --git a/src/qml/QFieldSettings.qml b/src/qml/QFieldSettings.qml index 4606309e36..35c191704f 100644 --- a/src/qml/QFieldSettings.qml +++ b/src/qml/QFieldSettings.qml @@ -15,7 +15,7 @@ Page { property alias locatorKeepScale: registry.locatorKeepScale property alias numericalDigitizingInformation: registry.numericalDigitizingInformation property alias showBookmarks: registry.showBookmarks - property alias nativeCamera: registry.nativeCamera + property alias nativeCamera2: registry.nativeCamera2 property alias digitizingVolumeKeys: registry.digitizingVolumeKeys property alias autoSave: registry.autoSave property alias fingerTapDigitizing: registry.fingerTapDigitizing @@ -27,7 +27,7 @@ Page { Component.onCompleted: { if (settings.valueBool('nativeCameraLaunched', false)) { // a crash occured while the native camera was launched, disable it - nativeCamera = false; + nativeCamera2 = false; } } @@ -42,7 +42,7 @@ Page { property bool locatorKeepScale: false property bool numericalDigitizingInformation: false property bool showBookmarks: true - property bool nativeCamera: platformUtilities.capabilities & PlatformUtilities.NativeCamera + property bool nativeCamera2: false property bool digitizingVolumeKeys: platformUtilities.capabilities & PlatformUtilities.VolumeKeys property bool autoSave: false property bool fingerTapDigitizing: false @@ -154,7 +154,7 @@ Page { ListElement { title: qsTr("Use native camera") description: qsTr("If disabled, QField will use a minimalist internal camera instead of the camera app on the device.
Tip: Enable this option and install the open camera app to create geo tagged photos.") - settingAlias: "nativeCamera" + settingAlias: "nativeCamera2" isVisible: true } ListElement { @@ -165,7 +165,7 @@ Page { } Component.onCompleted: { for (var i = 0; i < count; i++) { - if (get(i).settingAlias === 'nativeCamera') { + if (get(i).settingAlias === 'nativeCamera2') { setProperty(i, 'isVisible', platformUtilities.capabilities & PlatformUtilities.NativeCamera ? true : false); } else if (get(i).settingAlias === 'enableInfoCollection') { setProperty(i, 'isVisible', platformUtilities.capabilities & PlatformUtilities.SentryFramework ? true : false); diff --git a/src/qml/editorwidgets/ExternalResource.qml b/src/qml/editorwidgets/ExternalResource.qml index 0ecd5fa4f8..fb2cd712db 100644 --- a/src/qml/editorwidgets/ExternalResource.qml +++ b/src/qml/editorwidgets/ExternalResource.qml @@ -647,7 +647,7 @@ EditorWidgetBase { function capturePhoto() { Qt.inputMethod.hide(); - if (platformUtilities.capabilities & PlatformUtilities.NativeCamera && settings.valueBool("nativeCamera", true)) { + if (platformUtilities.capabilities & PlatformUtilities.NativeCamera && settings.valueBool("nativeCamera2", true)) { var filepath = getResourceFilePath(); // Pictures taken by cameras will always be JPG filepath = filepath.replace('{extension}', 'JPG'); @@ -661,7 +661,7 @@ EditorWidgetBase { function captureVideo() { Qt.inputMethod.hide(); - if (platformUtilities.capabilities & PlatformUtilities.NativeCamera && settings.valueBool("nativeCamera", true)) { + if (platformUtilities.capabilities & PlatformUtilities.NativeCamera && settings.valueBool("nativeCamera2", true)) { var filepath = getResourceFilePath(); // Video taken by cameras will always be MP4 filepath = filepath.replace('{extension}', 'MP4'); From 8f6687429d18b38df36712a1cf258f0753e09db5 Mon Sep 17 00:00:00 2001 From: Mathieu Pellerin Date: Fri, 30 Aug 2024 14:17:15 +0700 Subject: [PATCH 3/3] Be clever, use text renderer --- src/core/utils/fileutils.cpp | 20 ++++++++++---------- 1 file changed, 10 insertions(+), 10 deletions(-) diff --git a/src/core/utils/fileutils.cpp b/src/core/utils/fileutils.cpp index c8d96c1998..d34ebbf8d8 100644 --- a/src/core/utils/fileutils.cpp +++ b/src/core/utils/fileutils.cpp @@ -29,6 +29,9 @@ #include #include #include +#include +#include +#include FileUtils::FileUtils( QObject *parent ) : QObject( parent ) @@ -258,16 +261,13 @@ void FileUtils::addImageStamp( const QString &imagePath, const QString &text ) font.setPixelSize( pixelSize ); font.setBold( true ); - QPainterPath path; - QStringList lines = text.split( QStringLiteral( "\n" ) ); - for ( int i = 0; i < lines.size(); i++ ) - { - path.addText( QPointF( 10, img.height() - ( ( pixelSize + pixelSize / 4 ) * ( lines.size() - i - 1 ) ) - 10 ), font, lines.at( i ) ); - } - - painter.setPen( Qt::black ); - painter.setBrush( Qt::white ); - painter.drawPath( path ); + QgsRenderContext context = QgsRenderContext::fromQPainter( &painter ); + QgsTextFormat format; + format.setFont( font ); + format.setColor( Qt::white ); + format.buffer().setColor( Qt::black ); + format.buffer().setEnabled( true ); + QgsTextRenderer::drawText( QPointF( 10, img.height() - 10 ), 0, Qgis::TextHorizontalAlignment::Left, text.split( QStringLiteral( "\n" ) ), context, format ); img.save( imagePath, nullptr, 90 );