Skip to content

Commit

Permalink
Merge pull request #5596 from opengisch/stamps
Browse files Browse the repository at this point in the history
Add basic text stamping functionality on photos taken by QField camera
  • Loading branch information
nirvn authored Aug 30, 2024
2 parents 8661c38 + 8f66874 commit 2c15b46
Show file tree
Hide file tree
Showing 9 changed files with 107 additions and 16 deletions.
1 change: 1 addition & 0 deletions images/images.qrc
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@
<file alias="qfield-love.png">pictures/qfield-love.png</file>
</qresource>
<qresource prefix="/">
<file>themes/qfield/nodpi/ic_text_black_24dp.svg</file>
<file>themes/qfield/nodpi/ic_3x3_grid_white_24dp.svg</file>
<file>themes/qfield/nodpi/ic_digitizing_settings_black_24dp.svg</file>
<file>themes/qfield/nodpi/ic_undo_black_24dp.svg</file>
Expand Down
4 changes: 4 additions & 0 deletions images/themes/qfield/nodpi/ic_text_black_24dp.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
43 changes: 42 additions & 1 deletion src/core/utils/fileutils.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -24,9 +24,14 @@
#include <QImage>
#include <QImageReader>
#include <QMimeDatabase>
#include <QPainter>
#include <QPainterPath>
#include <qgis.h>
#include <qgsexiftools.h>
#include <qgsfileutils.h>
#include <qgsrendercontext.h>
#include <qgstextformat.h>
#include <qgstextrenderer.h>

FileUtils::FileUtils( QObject *parent )
: QObject( parent )
Expand Down Expand Up @@ -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() )
{
Expand Down Expand Up @@ -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] );
}
}
}
2 changes: 2 additions & 0 deletions src/core/utils/fileutils.h
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down
5 changes: 5 additions & 0 deletions src/core/utils/positioningutils.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,11 @@ GnssPositionInformation PositioningUtils::createGnssPositionInformation( double
verticalSpeed, magneticVariation, 0, sourceName );
}

GnssPositionInformation PositioningUtils::createEmptyGnssPositionInformation()
{
return GnssPositionInformation();
}

GnssPositionInformation PositioningUtils::averagedPositionInformation( const QList<QVariant> &positionsInformation )
{
QList<GnssPositionInformation> convertedList;
Expand Down
5 changes: 5 additions & 0 deletions src/core/utils/positioningutils.h
Original file line number Diff line number Diff line change
Expand Up @@ -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 &timestamp, const QString &sourceName );

/**
* Creates an empty GnssPositionInformation.
*/
static Q_INVOKABLE GnssPositionInformation createEmptyGnssPositionInformation();

/**
* Returns an average GnssPositionInformation from a list of position information
*/
Expand Down
49 changes: 41 additions & 8 deletions src/qml/QFieldCamera.qml
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -75,13 +75,24 @@ Popup {

Settings {
id: cameraSettings
property bool stamping: false
property bool geoTagging: true
property bool showGrid: false
property string deviceId: ''
property size resolution: Qt.size(0, 0)
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
Expand Down Expand Up @@ -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) {
Expand All @@ -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);
}
Expand Down Expand Up @@ -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

Expand Down
10 changes: 5 additions & 5 deletions src/qml/QFieldSettings.qml
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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;
}
}

Expand All @@ -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
Expand Down Expand Up @@ -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.<br>Tip: Enable this option and install the open camera app to create geo tagged photos.")
settingAlias: "nativeCamera"
settingAlias: "nativeCamera2"
isVisible: true
}
ListElement {
Expand All @@ -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);
Expand Down
4 changes: 2 additions & 2 deletions src/qml/editorwidgets/ExternalResource.qml
Original file line number Diff line number Diff line change
Expand Up @@ -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');
Expand All @@ -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');
Expand Down

1 comment on commit 2c15b46

@qfield-fairy
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Please sign in to comment.