Skip to content

Commit

Permalink
Browse files Browse the repository at this point in the history
[feature][exiftools] Implement tags reading, allow for individual tag…
… value read (#44076)
  • Loading branch information
nirvn committed Jul 8, 2021
1 parent aaff7ce commit 8a4683f
Show file tree
Hide file tree
Showing 4 changed files with 179 additions and 62 deletions.
14 changes: 14 additions & 0 deletions python/core/auto_generated/raster/qgsexiftools.sip.in
Expand Up @@ -25,6 +25,19 @@ Contains utilities for working with EXIF tags in images.

public:

static QVariantMap readTags( const QString &imagePath );
%Docstring
Returns a map object containing all exif tags stored in the image at ``imagePath``.

.. versionadded:: 3.22
%End

static QVariant readTag( const QString &imagePath, const QString &key );
%Docstring
Returns the value of of an exif tag ``key`` stored in the image at ``imagePath``.

.. versionadded:: 3.22
%End

static QgsPoint getGeoTag( const QString &imagePath, bool &ok /Out/ );
%Docstring
Expand Down Expand Up @@ -75,6 +88,7 @@ Returns ``True`` if writing was successful.

.. seealso:: :py:func:`getGeoTag`
%End

};

/************************************************************************
Expand Down
201 changes: 142 additions & 59 deletions src/core/raster/qgsexiftools.cpp
Expand Up @@ -18,80 +18,147 @@

#include <exiv2/exiv2.hpp>

#include <QDate>
#include <QRegularExpression>
#include <QFileInfo>
#include <QTime>

#if 0 // needs further work on the correct casting of tag values to QVariant values!
QVariantMap QgsExifTools::readTags( const QString &imagePath )

double readRationale( const Exiv2::Value &value, long n = 0 )
{
std::unique_ptr< Exiv2::Image > image( Exiv2::ImageFactory::open( imagePath.toStdString() ) );
if ( !image )
return QVariantMap();
Exiv2::Rational rational = value.toRational( n );
return static_cast< double >( rational.first ) / rational.second;
};

image->readMetadata();
Exiv2::ExifData &exifData = image->exifData();
if ( exifData.empty() )
double readCoordinate( const Exiv2::Value &value )
{
double res = 0;
double div = 1;
for ( int i = 0; i < 3; i++ )
{
return QVariantMap();
res += readRationale( value, i ) / div;
div *= 60;
}
return res;
};

QVariantMap res;
Exiv2::ExifData::const_iterator end = exifData.end();
for ( Exiv2::ExifData::const_iterator i = exifData.begin(); i != end; ++i )
QVariant decodeExifData( const QString &key, Exiv2::ExifData::const_iterator &it )
{
QVariant val;

if ( key == QLatin1String( "Exif.GPSInfo.GPSLatitude" ) ||
key == QLatin1String( "Exif.GPSInfo.GPSLongitude" ) ||
key == QLatin1String( "Exif.GPSInfo.GPSDestLatitude" ) ||
key == QLatin1String( "Exif.GPSInfo.GPSDestLongitude" ) )
{
const QString key = QString::fromStdString( i->key() );
QVariant val;
switch ( i->typeId() )
val = readCoordinate( it->value() );
}
else if ( key == QLatin1String( "Exif.GPSInfo.GPSTimeStamp" ) )
{
const QStringList parts = QString::fromStdString( it->toString() ).split( QRegularExpression( QStringLiteral( "\\s+" ) ) );
if ( parts.size() == 3 )
{
const int hour = readRationale( it->value(), 0 );
const int minute = readRationale( it->value(), 1 );
const int second = readRationale( it->value(), 2 );
val = QVariant::fromValue( QTime::fromString( QStringLiteral( "%1:%2:%3" )
.arg( QString::number( hour ).rightJustified( 2, '0' ) )
.arg( QString::number( minute ).rightJustified( 2, '0' ) )
.arg( QString::number( second ).rightJustified( 2, '0' ) ), QLatin1String( "hh:mm:ss" ) ) );
}
}
else if ( key == QLatin1String( "Exif.GPSInfo.GPSDateStamp" ) )
{
val = QVariant::fromValue( QDate::fromString( QString::fromStdString( it->toString() ), QLatin1String( "yyyy:MM:dd" ) ) );
}
else if ( key == QLatin1String( "Exif.Image.DateTime" ) ||
key == QLatin1String( "Exif.Image.DateTime" ) ||
key == QLatin1String( "Exif.Photo.DateTimeDigitized" ) ||
key == QLatin1String( "Exif.Photo.DateTimeOriginal" ) )
{
val = QVariant::fromValue( QDateTime::fromString( QString::fromStdString( it->toString() ), QLatin1String( "yyyy:MM:dd hh:mm:ss" ) ) );
}
else
{
switch ( it->typeId() )
{
case Exiv2::asciiString:
case Exiv2::string:
case Exiv2::comment:
case Exiv2::directory:
case Exiv2::xmpText:
val = QString::fromStdString( i->toString() );
val = QString::fromStdString( it->toString() );
break;

case Exiv2::unsignedLong:
case Exiv2::signedLong:
val = QVariant::fromValue( i->toLong() );
case Exiv2::unsignedLongLong:
case Exiv2::signedLongLong:
val = QVariant::fromValue( it->toLong() );
break;

case Exiv2::tiffDouble:
case Exiv2::tiffFloat:
val = QVariant::fromValue( i->toFloat() );
val = QVariant::fromValue( it->toFloat() );
break;

case Exiv2::unsignedShort:
case Exiv2::signedShort:
val = QVariant::fromValue( static_cast< int >( i->toLong() ) );
break;

case Exiv2::unsignedRational:
case Exiv2::signedRational:
case Exiv2::unsignedByte:
case Exiv2::signedByte:
case Exiv2::undefined:
case Exiv2::tiffIfd:
case Exiv2::tiffIfd8:
val = QVariant::fromValue( static_cast< int >( it->toLong() ) );
break;

case Exiv2::date:
{
Exiv2::DateValue::Date date = static_cast< const Exiv2::DateValue *>( &it->value() )->getDate();
val = QVariant::fromValue( QDate::fromString( QStringLiteral( "%1-%2-%3" ).arg( date.year )
.arg( QString::number( date.month ).rightJustified( 2, '0' ) )
.arg( QString::number( date.day ).rightJustified( 2, '0' ) ), QLatin1String( "yyyy-MM-dd" ) ) );
break;
}

case Exiv2::time:
{
Exiv2::TimeValue::Time time = static_cast< const Exiv2::TimeValue *>( &it->value() )->getTime();
val = QVariant::fromValue( QTime::fromString( QStringLiteral( "%1:%2:%3" ).arg( QString::number( time.hour ).rightJustified( 2, '0' ) )
.arg( QString::number( time.minute ).rightJustified( 2, '0' ) )
.arg( QString::number( time.second ).rightJustified( 2, '0' ) ), QLatin1String( "hh:mm:ss" ) ) );
break;
}

case Exiv2::unsignedRational:
case Exiv2::signedRational:
{
if ( it->count() == 1 )
{
val = QVariant::fromValue( readRationale( it->value() ) );
}
else
{
val = QString::fromStdString( it->toString() );
}
break;
}

case Exiv2::undefined:
case Exiv2::xmpAlt:
case Exiv2::xmpBag:
case Exiv2::xmpSeq:
case Exiv2::langAlt:
case Exiv2::invalidTypeId:
case Exiv2::lastTypeId:
val = QString::fromStdString( i->toString() );
val = QString::fromStdString( it->toString() );
break;

}

res.insert( key, val );
}
return res;
return val;
}
#endif

QString doubleToExifCoordinate( const double val )
QString doubleToExifCoordinateString( const double val )
{
double d = std::abs( val );
int degrees = static_cast< int >( std::floor( d ) );
Expand All @@ -102,6 +169,46 @@ QString doubleToExifCoordinate( const double val )
return QStringLiteral( "%1/1 %2/1 %3/1000" ).arg( degrees ).arg( minutes ).arg( seconds );
}

QVariant QgsExifTools::readTag( const QString &imagePath, const QString &key )
{
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();
}

QVariantMap QgsExifTools::readTags( const QString &imagePath )
{
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 )
{
const QString key = QString::fromStdString( i->key() );
res.insert( key, decodeExifData( key, i ) );
}
return res;
}

bool QgsExifTools::hasGeoTag( const QString &imagePath )
{
bool ok = false;
Expand Down Expand Up @@ -135,32 +242,8 @@ QgsPoint QgsExifTools::getGeoTag( const QString &imagePath, bool &ok )
itLonRef == exifData.end() || itLonVal == exifData.end() )
return QgsPoint();

auto readCoord = []( const QString & coord )->double
{
double res = 0;
double div = 1;
const QStringList parts = coord.split( QRegularExpression( QStringLiteral( "\\s+" ) ) );
for ( const QString &rational : parts )
{
const QStringList pair = rational.split( '/' );
if ( pair.size() != 2 )
break;
res += ( pair[0].toDouble() / pair[1].toDouble() ) / div;
div *= 60;
}
return res;
};

auto readRationale = []( const QString & rational )->double
{
const QStringList pair = rational.split( '/' );
if ( pair.size() != 2 )
return std::numeric_limits< double >::quiet_NaN();
return pair[0].toDouble() / pair[1].toDouble();
};

double lat = readCoord( QString::fromStdString( itLatVal->value().toString() ) );
double lon = readCoord( QString::fromStdString( itLonVal->value().toString() ) );
double lat = readCoordinate( itLatVal->value() );
double lon = readCoordinate( itLonVal->value() );

const QString latRef = QString::fromStdString( itLatRef->value().toString() );
const QString lonRef = QString::fromStdString( itLonRef->value().toString() );
Expand All @@ -179,7 +262,7 @@ QgsPoint QgsExifTools::getGeoTag( const QString &imagePath, bool &ok )
Exiv2::ExifData::iterator itElevRefVal = exifData.findKey( Exiv2::ExifKey( "Exif.GPSInfo.GPSAltitudeRef" ) );
if ( itElevVal != exifData.end() )
{
double elev = readRationale( QString::fromStdString( itElevVal->value().toString() ) );
double elev = readRationale( itElevVal->value() );
if ( itElevRefVal != exifData.end() )
{
const QString elevRef = QString::fromStdString( itElevRefVal->value().toString() );
Expand Down Expand Up @@ -214,8 +297,8 @@ bool QgsExifTools::geoTagImage( const QString &imagePath, const QgsPointXY &loca

exifData["Exif.GPSInfo.GPSVersionID"] = "2 0 0 0";
exifData["Exif.GPSInfo.GPSMapDatum"] = "WGS-84";
exifData["Exif.GPSInfo.GPSLatitude"] = doubleToExifCoordinate( location.y() ).toStdString();
exifData["Exif.GPSInfo.GPSLongitude"] = doubleToExifCoordinate( location.x() ).toStdString();
exifData["Exif.GPSInfo.GPSLatitude"] = doubleToExifCoordinateString( location.y() ).toStdString();
exifData["Exif.GPSInfo.GPSLongitude"] = doubleToExifCoordinateString( location.x() ).toStdString();
if ( !std::isnan( details.elevation ) )
{
const QString elevationString = QStringLiteral( "%1/1000" ).arg( static_cast< int>( std::floor( std::abs( details.elevation ) * 1000 ) ) );
Expand Down
13 changes: 11 additions & 2 deletions src/core/raster/qgsexiftools.h
Expand Up @@ -35,9 +35,17 @@ class CORE_EXPORT QgsExifTools

public:

#if 0
/**
* Returns a map object containing all exif tags stored in the image at \a imagePath.
* \since QGIS 3.22
*/
static QVariantMap readTags( const QString &imagePath );
#endif

/**
* Returns the value of of an exif tag \a key stored in the image at \a imagePath.
* \since QGIS 3.22
*/
static QVariant readTag( const QString &imagePath, const QString &key );

/**
* Returns the geotagged coordinate stored in the image at \a imagePath.
Expand Down Expand Up @@ -90,6 +98,7 @@ class CORE_EXPORT QgsExifTools
* \see getGeoTag()
*/
static bool geoTagImage( const QString &imagePath, const QgsPointXY &location, const GeoTagDetails &details = QgsExifTools::GeoTagDetails() );

};

#endif // QGSEXIFTOOLS_H
13 changes: 12 additions & 1 deletion tests/src/python/test_qgsexiftools.py
Expand Up @@ -13,7 +13,7 @@
import qgis # NOQA switch sip api
import os
import shutil
from qgis.PyQt.QtCore import QTemporaryFile
from qgis.PyQt.QtCore import QTemporaryFile, QDateTime
from qgis.core import QgsPointXY, QgsExifTools
from qgis.testing import start_app, unittest
from utilities import unitTestDataPath
Expand All @@ -25,6 +25,17 @@

class TestQgsExifUtils(unittest.TestCase):

def testReadTags(self):
photos_folder = os.path.join(TEST_DATA_DIR, 'photos')

# test a convnerted rational value
elevation = QgsExifTools.readTag(os.path.join(photos_folder, '0997.JPG'), 'Exif.GPSInfo.GPSAltitude')
self.assertEqual(elevation, 422.19101123595505)

# test a converted datetime value
dt = QgsExifTools.readTag(os.path.join(photos_folder, '0997.JPG'), 'Exif.Image.DateTime')
self.assertEqual(dt, QDateTime(2018, 3, 16, 12, 19, 19))

def testGeoTags(self):
photos_folder = os.path.join(TEST_DATA_DIR, 'photos')

Expand Down

0 comments on commit 8a4683f

Please sign in to comment.