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..d34ebbf8d8 100644
--- a/src/core/utils/fileutils.cpp
+++ b/src/core/utils/fileutils.cpp
@@ -24,9 +24,14 @@
#include
#include
#include
+#include
+#include
#include
#include
#include
+#include
+#include
+#include
FileUtils::FileUtils( QObject *parent )
: QObject( parent )
@@ -183,7 +188,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 +241,39 @@ 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 );
+
+ 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 );
+
+ 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
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');