Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add basic text stamping functionality on photos taken by QField camera #5596

Merged
merged 3 commits into from
Aug 30, 2024
Merged
Show file tree
Hide file tree
Changes from 2 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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,6 +24,8 @@
#include <QImage>
#include <QImageReader>
#include <QMimeDatabase>
#include <QPainter>
#include <QPainterPath>
#include <qgis.h>
#include <qgsexiftools.h>
#include <qgsfileutils.h>
Expand Down Expand Up @@ -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() )
{
Expand Down Expand Up @@ -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 ) );
nirvn marked this conversation as resolved.
Show resolved Hide resolved
}

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] );
}
}
}
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
mohsenD98 marked this conversation as resolved.
Show resolved Hide resolved
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
Loading