Skip to content
Permalink
Browse files
[feature][expression] Add exif() and exif_geotag() functions to the Q…
…GIS expression engine (#44081)
  • Loading branch information
nirvn committed Jul 8, 2021
1 parent 7341492 commit 754328cbd0a4e5251f03c444221988a7031f4cef
@@ -0,0 +1,13 @@
{
"name": "exif",
"type": "function",
"groups": ["Files and Paths"],
"description": "Retrieves exif tag values from an image file.",
"arguments": [
{"arg":"path","description":"An image file path."},
{"arg":"tag", "optional": true, "description":"The tag to return. If empty, a map with all exif tag values will be returned."}
],
"examples": [
{ "expression":"exif('/my/photo.jpg','Exif.Image.Orientation')", "returns":"0"}
]
}
@@ -0,0 +1,12 @@
{
"name": "exif_geotag",
"type": "function",
"groups": ["GeometryGroup"],
"description": "Creates a point geometry from the exif geotags of an image file.",
"arguments": [
{"arg":"path","description":"An image file path."}
],
"examples": [
{ "expression":"geom_to_wkt(exif_geotag('/my/photo.jpg'))", "returns":"'Point (2 4)'"}
]
}
@@ -21,6 +21,7 @@
#include "qgsexpressionfunction.h"
#include "qgsexpressionutils.h"
#include "qgsexpressionnodeimpl.h"
#include "qgsexiftools.h"
#include "qgsfeaturerequest.h"
#include "qgsstringutils.h"
#include "qgsmultipoint.h"
@@ -2358,6 +2359,20 @@ static QVariant fcnDateTimeFromEpoch( const QVariantList &values, const QgsExpre
return QVariant( QDateTime::fromMSecsSinceEpoch( millisecs_since_epoch ) );
}

static QVariant fcnExif( const QVariantList &values, const QgsExpressionContext *, QgsExpression *parent, const QgsExpressionNodeFunction * )
{
QString filepath = QgsExpressionUtils::getStringValue( values.at( 0 ), parent );
QString tag = QgsExpressionUtils::getStringValue( values.at( 1 ), parent );
return !tag.isNull() ? QgsExifTools::readTag( filepath, tag ) : QVariant( QgsExifTools::readTags( filepath ) );
}

static QVariant fcnExifGeoTag( const QVariantList &values, const QgsExpressionContext *, QgsExpression *parent, const QgsExpressionNodeFunction * )
{
QString filepath = QgsExpressionUtils::getStringValue( values.at( 0 ), parent );
bool ok;
return QVariant::fromValue( QgsGeometry( new QgsPoint( QgsExifTools::getGeoTag( filepath, ok ) ) ) );
}

#define ENSURE_GEOM_TYPE(f, g, geomtype) \
if ( !(f).hasGeometry() ) \
return QVariant(); \
@@ -6672,6 +6687,11 @@ const QList<QgsExpressionFunction *> &QgsExpression::Functions()
<< new QgsStaticExpressionFunction( QStringLiteral( "file_size" ), QgsExpressionFunction::ParameterList() << QgsExpressionFunction::Parameter( QStringLiteral( "path" ) ),
fcnFileSize, QStringLiteral( "Files and Paths" ) )

<< new QgsStaticExpressionFunction( QStringLiteral( "exif" ), QgsExpressionFunction::ParameterList() << QgsExpressionFunction::Parameter( QStringLiteral( "path" ) ) << QgsExpressionFunction::Parameter( QStringLiteral( "tag" ), true ),
fcnExif, QStringLiteral( "Files and Paths" ) )
<< new QgsStaticExpressionFunction( QStringLiteral( "exif_geotag" ), QgsExpressionFunction::ParameterList() << QgsExpressionFunction::Parameter( QStringLiteral( "path" ) ),
fcnExifGeoTag, QStringLiteral( "Geometry" ) )

// hash
<< new QgsStaticExpressionFunction( QStringLiteral( "hash" ), QgsExpressionFunction::ParameterList() << QgsExpressionFunction::Parameter( QStringLiteral( "string" ) ) << QgsExpressionFunction::Parameter( QStringLiteral( "method" ) ),
fcnGenericHash, QStringLiteral( "Conversions" ) )
@@ -171,42 +171,62 @@ QString doubleToExifCoordinateString( const double val )

QVariant QgsExifTools::readTag( const QString &imagePath, const QString &key )
{
std::unique_ptr< Exiv2::Image > image( Exiv2::ImageFactory::open( imagePath.toStdString() ) );
if ( !image || key.isEmpty() )
if ( !QFileInfo::exists( imagePath ) )
return QVariant();

image->readMetadata();
Exiv2::ExifData &exifData = image->exifData();
if ( exifData.empty() )
try
{
std::unique_ptr< Exiv2::Image > image( Exiv2::ImageFactory::open( imagePath.toStdString() ) );
if ( !image || key.isEmpty() )
return QVariant();

image->readMetadata();
Exiv2::ExifData &exifData = image->exifData();
if ( exifData.empty() )
{
return QVariant();
}

Exiv2::ExifData::const_iterator i = exifData.findKey( Exiv2::ExifKey( key.toUtf8().constData() ) );
return i != exifData.end() ? decodeExifData( key, i ) : QVariant();
}
catch ( ... )
{
return QVariant();
}

Exiv2::ExifData::const_iterator i = exifData.findKey( Exiv2::ExifKey( key.toUtf8().constData() ) );
return i != exifData.end() ? decodeExifData( key, i ) : QVariant();
}

QVariantMap QgsExifTools::readTags( const QString &imagePath )
{
std::unique_ptr< Exiv2::Image > image( Exiv2::ImageFactory::open( imagePath.toStdString() ) );
if ( !image )
if ( !QFileInfo::exists( imagePath ) )
return QVariantMap();

image->readMetadata();
Exiv2::ExifData &exifData = image->exifData();
if ( exifData.empty() )
try
{
return QVariantMap();
}
std::unique_ptr< Exiv2::Image > image( Exiv2::ImageFactory::open( imagePath.toStdString() ) );
if ( !image )
return QVariantMap();

image->readMetadata();
Exiv2::ExifData &exifData = image->exifData();
if ( exifData.empty() )
{
return QVariantMap();
}

QVariantMap res;
Exiv2::ExifData::const_iterator end = exifData.end();
for ( Exiv2::ExifData::const_iterator i = exifData.begin(); i != end; ++i )
QVariantMap res;
Exiv2::ExifData::const_iterator end = exifData.end();
for ( Exiv2::ExifData::const_iterator i = exifData.begin(); i != end; ++i )
{
const QString key = QString::fromStdString( i->key() );
res.insert( key, decodeExifData( key, i ) );
}
return res;
}
catch ( ... )
{
const QString key = QString::fromStdString( i->key() );
res.insert( key, decodeExifData( key, i ) );
return QVariantMap();
}
return res;
}

bool QgsExifTools::hasGeoTag( const QString &imagePath )
@@ -1806,8 +1806,18 @@ class TestQgsExpression: public QObject
QTest::newRow( "uuid('WithoutBraces')" ) << QStringLiteral( "regexp_match( uuid('WithoutBraces'), '([a-zA-Z\\\\d]{8}\\\\-[a-zA-Z\\\\d]{4}\\\\-[a-zA-Z\\\\d]{4}\\\\-[a-zA-Z\\\\d]{4}\\\\-[a-zA-Z\\\\d]{12})')" ) << false << QVariant( 1 );
QTest::newRow( "uuid('Id128')" ) << QStringLiteral( "regexp_match( uuid('Id128'), '([a-zA-Z\\\\d]{32})')" ) << false << QVariant( 1 );
QTest::newRow( "uuid('invalid-format')" ) << QStringLiteral( "regexp_match( uuid('invalid-format'), '({[a-zA-Z\\\\d]{8}\\\\-[a-zA-Z\\\\d]{4}\\\\-[a-zA-Z\\\\d]{4}\\\\-[a-zA-Z\\\\d]{4}\\\\-[a-zA-Z\\\\d]{12}})')" ) << false << QVariant( 1 );
}

//exif functions
QString testDataDir = QStringLiteral( TEST_DATA_DIR ) + '/';
QTest::newRow( "exif number" ) << QStringLiteral( "exif('%1photos/0997.JPG','Exif.GPSInfo.GPSAltitude')" ).arg( testDataDir ) << false << QVariant( 422.19101123595505 );
QTest::newRow( "exif number from map" ) << QStringLiteral( "exif('%1photos/0997.JPG')['Exif.GPSInfo.GPSAltitude']" ).arg( testDataDir ) << false << QVariant( 422.19101123595505 );
QTest::newRow( "exif date" ) << QStringLiteral( "exif('%1photos/0997.JPG','Exif.Image.DateTime')" ).arg( testDataDir ) << false << QVariant( QDateTime( QDate( 2018, 3, 16 ), QTime( 12, 19, 19 ) ) );
QTest::newRow( "exif date from map" ) << QStringLiteral( "exif('%1photos/0997.JPG')['Exif.Image.DateTime']" ).arg( testDataDir ) << false << QVariant( QDateTime( QDate( 2018, 3, 16 ), QTime( 12, 19, 19 ) ) );
QTest::newRow( "exif bad tag" ) << QStringLiteral( "exif('%1photos/0997.JPG','bad tag')" ).arg( testDataDir ) << false << QVariant();
QTest::newRow( "exif bad file path" ) << QStringLiteral( "exif('bad path','Exif.Image.DateTime')" ) << false << QVariant();
QTest::newRow( "exif_geotag" ) << QStringLiteral( "geom_to_wkt(exif_geotag('%1photos/0997.JPG'))" ).arg( testDataDir ) << false << QVariant( "PointZ (149.27516667 -37.2305 422.19101124)" );
QTest::newRow( "exif_geotag bad file path" ) << QStringLiteral( "geom_to_wkt(exif_geotag('bad path'))" ).arg( testDataDir ) << false << QVariant( "Point EMPTY" );
}

void run_evaluation_test( QgsExpression &exp, bool evalError, QVariant &expected )
{

0 comments on commit 754328c

Please sign in to comment.