QGIS API Documentation 3.28.0-Firenze (ed3ad0430f)
qgsexiftools.cpp
Go to the documentation of this file.
1/***************************************************************************
2 qgisexiftools.cpp
3 -----------------
4 Date : November 2018
5 Copyright : (C) 2018 by Nyall Dawson
6 Email : nyall dot dawson at gmail dot com
7 ***************************************************************************
8 * *
9 * This program is free software; you can redistribute it and/or modify *
10 * it under the terms of the GNU General Public License as published by *
11 * the Free Software Foundation; either version 2 of the License, or *
12 * (at your option) any later version. *
13 * *
14 ***************************************************************************/
15
16#include "qgsexiftools.h"
17#include "qgspoint.h"
18
19#include <exiv2/exiv2.hpp>
20
21#include <QDate>
22#include <QRegularExpression>
23#include <QFileInfo>
24#include <QTime>
25
26
27double readRationale( const Exiv2::Value &value, long n = 0 )
28{
29 const Exiv2::Rational rational = value.toRational( n );
30 return static_cast< double >( rational.first ) / rational.second;
31};
32
33double readCoordinate( const Exiv2::Value &value )
34{
35 double res = 0;
36 double div = 1;
37 for ( int i = 0; i < 3; i++ )
38 {
39 res += readRationale( value, i ) / div;
40 div *= 60;
41 }
42 return res;
43};
44
45QVariant decodeExifData( const QString &key, Exiv2::ExifData::const_iterator &it )
46{
47 QVariant val;
48
49 if ( key == QLatin1String( "Exif.GPSInfo.GPSLatitude" ) ||
50 key == QLatin1String( "Exif.GPSInfo.GPSLongitude" ) ||
51 key == QLatin1String( "Exif.GPSInfo.GPSDestLatitude" ) ||
52 key == QLatin1String( "Exif.GPSInfo.GPSDestLongitude" ) )
53 {
54 val = readCoordinate( it->value() );
55 }
56 else if ( key == QLatin1String( "Exif.GPSInfo.GPSTimeStamp" ) )
57 {
58 const QStringList parts = QString::fromStdString( it->toString() ).split( QRegularExpression( QStringLiteral( "\\s+" ) ) );
59 if ( parts.size() == 3 )
60 {
61 const int hour = readRationale( it->value(), 0 );
62 const int minute = readRationale( it->value(), 1 );
63 const int second = readRationale( it->value(), 2 );
64 val = QVariant::fromValue( QTime::fromString( QStringLiteral( "%1:%2:%3" )
65 .arg( QString::number( hour ).rightJustified( 2, '0' ) )
66 .arg( QString::number( minute ).rightJustified( 2, '0' ) )
67 .arg( QString::number( second ).rightJustified( 2, '0' ) ), QLatin1String( "hh:mm:ss" ) ) );
68 }
69 }
70 else if ( key == QLatin1String( "Exif.GPSInfo.GPSDateStamp" ) )
71 {
72 val = QVariant::fromValue( QDate::fromString( QString::fromStdString( it->toString() ), QLatin1String( "yyyy:MM:dd" ) ) );
73 }
74 else if ( key == QLatin1String( "Exif.Image.DateTime" ) ||
75 key == QLatin1String( "Exif.Image.DateTime" ) ||
76 key == QLatin1String( "Exif.Photo.DateTimeDigitized" ) ||
77 key == QLatin1String( "Exif.Photo.DateTimeOriginal" ) )
78 {
79 val = QVariant::fromValue( QDateTime::fromString( QString::fromStdString( it->toString() ), QLatin1String( "yyyy:MM:dd hh:mm:ss" ) ) );
80 }
81 else
82 {
83 switch ( it->typeId() )
84 {
85 case Exiv2::asciiString:
86 case Exiv2::string:
87 case Exiv2::comment:
88 case Exiv2::directory:
89 case Exiv2::xmpText:
90 val = QString::fromStdString( it->toString() );
91 break;
92
93 case Exiv2::unsignedLong:
94 case Exiv2::signedLong:
95 case Exiv2::unsignedLongLong:
96 case Exiv2::signedLongLong:
97 val = QVariant::fromValue( it->toLong() );
98 break;
99
100 case Exiv2::tiffDouble:
101 case Exiv2::tiffFloat:
102 val = QVariant::fromValue( it->toFloat() );
103 break;
104
105 case Exiv2::unsignedShort:
106 case Exiv2::signedShort:
107 case Exiv2::unsignedByte:
108 case Exiv2::signedByte:
109 case Exiv2::tiffIfd:
110 case Exiv2::tiffIfd8:
111 val = QVariant::fromValue( static_cast< int >( it->toLong() ) );
112 break;
113
114 case Exiv2::date:
115 {
116 const Exiv2::DateValue::Date date = static_cast< const Exiv2::DateValue *>( &it->value() )->getDate();
117 val = QVariant::fromValue( QDate::fromString( QStringLiteral( "%1-%2-%3" ).arg( date.year )
118 .arg( QString::number( date.month ).rightJustified( 2, '0' ) )
119 .arg( QString::number( date.day ).rightJustified( 2, '0' ) ), QLatin1String( "yyyy-MM-dd" ) ) );
120 break;
121 }
122
123 case Exiv2::time:
124 {
125 const Exiv2::TimeValue::Time time = static_cast< const Exiv2::TimeValue *>( &it->value() )->getTime();
126 val = QVariant::fromValue( QTime::fromString( QStringLiteral( "%1:%2:%3" ).arg( QString::number( time.hour ).rightJustified( 2, '0' ) )
127 .arg( QString::number( time.minute ).rightJustified( 2, '0' ) )
128 .arg( QString::number( time.second ).rightJustified( 2, '0' ) ), QLatin1String( "hh:mm:ss" ) ) );
129 break;
130 }
131
132 case Exiv2::unsignedRational:
133 case Exiv2::signedRational:
134 {
135 if ( it->count() == 1 )
136 {
137 val = QVariant::fromValue( readRationale( it->value() ) );
138 }
139 else
140 {
141 val = QString::fromStdString( it->toString() );
142 }
143 break;
144 }
145
146 case Exiv2::undefined:
147 case Exiv2::xmpAlt:
148 case Exiv2::xmpBag:
149 case Exiv2::xmpSeq:
150 case Exiv2::langAlt:
151 case Exiv2::invalidTypeId:
152 case Exiv2::lastTypeId:
153 val = QString::fromStdString( it->toString() );
154 break;
155
156 }
157 }
158 return val;
159}
160
161QString doubleToExifCoordinateString( const double val )
162{
163 const double d = std::abs( val );
164 const int degrees = static_cast< int >( std::floor( d ) );
165 const double m = 60 * ( d - degrees );
166 const int minutes = static_cast< int >( std::floor( m ) );
167 const double s = 60 * ( m - minutes );
168 const int seconds = static_cast< int >( std::floor( s * 1000 ) );
169 return QStringLiteral( "%1/1 %2/1 %3/1000" ).arg( degrees ).arg( minutes ).arg( seconds );
170}
171
172QVariant QgsExifTools::readTag( const QString &imagePath, const QString &key )
173{
174 if ( !QFileInfo::exists( imagePath ) )
175 return QVariant();
176
177 try
178 {
179 std::unique_ptr< Exiv2::Image > image( Exiv2::ImageFactory::open( imagePath.toStdString() ) );
180 if ( !image || key.isEmpty() )
181 return QVariant();
182
183 image->readMetadata();
184 Exiv2::ExifData &exifData = image->exifData();
185 if ( exifData.empty() )
186 {
187 return QVariant();
188 }
189
190 Exiv2::ExifData::const_iterator i = exifData.findKey( Exiv2::ExifKey( key.toUtf8().constData() ) );
191 return i != exifData.end() ? decodeExifData( key, i ) : QVariant();
192 }
193 catch ( ... )
194 {
195 return QVariant();
196 }
197}
198
199QVariantMap QgsExifTools::readTags( const QString &imagePath )
200{
201 if ( !QFileInfo::exists( imagePath ) )
202 return QVariantMap();
203
204 try
205 {
206 std::unique_ptr< Exiv2::Image > image( Exiv2::ImageFactory::open( imagePath.toStdString() ) );
207 if ( !image )
208 return QVariantMap();
209
210 image->readMetadata();
211 Exiv2::ExifData &exifData = image->exifData();
212 if ( exifData.empty() )
213 {
214 return QVariantMap();
215 }
216
217 QVariantMap res;
218 const Exiv2::ExifData::const_iterator end = exifData.end();
219 for ( Exiv2::ExifData::const_iterator i = exifData.begin(); i != end; ++i )
220 {
221 const QString key = QString::fromStdString( i->key() );
222 res.insert( key, decodeExifData( key, i ) );
223 }
224 return res;
225 }
226 catch ( ... )
227 {
228 return QVariantMap();
229 }
230}
231
232bool QgsExifTools::hasGeoTag( const QString &imagePath )
233{
234 bool ok = false;
235 QgsExifTools::getGeoTag( imagePath, ok );
236 return ok;
237}
238
239QgsPoint QgsExifTools::getGeoTag( const QString &imagePath, bool &ok )
240{
241 ok = false;
242 if ( !QFileInfo::exists( imagePath ) )
243 return QgsPoint();
244 try
245 {
246 std::unique_ptr< Exiv2::Image > image( Exiv2::ImageFactory::open( imagePath.toStdString() ) );
247 if ( !image )
248 return QgsPoint();
249
250 image->readMetadata();
251 Exiv2::ExifData &exifData = image->exifData();
252
253 if ( exifData.empty() )
254 return QgsPoint();
255
256 const Exiv2::ExifData::iterator itLatRef = exifData.findKey( Exiv2::ExifKey( "Exif.GPSInfo.GPSLatitudeRef" ) );
257 const Exiv2::ExifData::iterator itLatVal = exifData.findKey( Exiv2::ExifKey( "Exif.GPSInfo.GPSLatitude" ) );
258 const Exiv2::ExifData::iterator itLonRef = exifData.findKey( Exiv2::ExifKey( "Exif.GPSInfo.GPSLongitudeRef" ) );
259 const Exiv2::ExifData::iterator itLonVal = exifData.findKey( Exiv2::ExifKey( "Exif.GPSInfo.GPSLongitude" ) );
260
261 if ( itLatRef == exifData.end() || itLatVal == exifData.end() ||
262 itLonRef == exifData.end() || itLonVal == exifData.end() )
263 return QgsPoint();
264
265 double lat = readCoordinate( itLatVal->value() );
266 double lon = readCoordinate( itLonVal->value() );
267
268 const QString latRef = QString::fromStdString( itLatRef->value().toString() );
269 const QString lonRef = QString::fromStdString( itLonRef->value().toString() );
270 if ( latRef.compare( QLatin1String( "S" ), Qt::CaseInsensitive ) == 0 )
271 {
272 lat *= -1;
273 }
274 if ( lonRef.compare( QLatin1String( "W" ), Qt::CaseInsensitive ) == 0 )
275 {
276 lon *= -1;
277 }
278
279 ok = true;
280
281 const Exiv2::ExifData::iterator itElevVal = exifData.findKey( Exiv2::ExifKey( "Exif.GPSInfo.GPSAltitude" ) );
282 const Exiv2::ExifData::iterator itElevRefVal = exifData.findKey( Exiv2::ExifKey( "Exif.GPSInfo.GPSAltitudeRef" ) );
283 if ( itElevVal != exifData.end() )
284 {
285 double elev = readRationale( itElevVal->value() );
286 if ( itElevRefVal != exifData.end() )
287 {
288 const QString elevRef = QString::fromStdString( itElevRefVal->value().toString() );
289 if ( elevRef.compare( QLatin1String( "1" ), Qt::CaseInsensitive ) == 0 )
290 {
291 elev *= -1;
292 }
293 }
294 return QgsPoint( lon, lat, elev );
295 }
296 else
297 {
298 return QgsPoint( lon, lat );
299 }
300 }
301 catch ( ... )
302 {
303 return QgsPoint();
304 }
305}
306
307bool QgsExifTools::geoTagImage( const QString &imagePath, const QgsPointXY &location, const GeoTagDetails &details )
308{
309 try
310 {
311 std::unique_ptr< Exiv2::Image > image( Exiv2::ImageFactory::open( imagePath.toStdString() ) );
312 if ( !image )
313 return false;
314
315 image->readMetadata();
316 Exiv2::ExifData &exifData = image->exifData();
317
318 exifData["Exif.GPSInfo.GPSVersionID"] = "2 0 0 0";
319 exifData["Exif.GPSInfo.GPSMapDatum"] = "WGS-84";
320 exifData["Exif.GPSInfo.GPSLatitude"] = doubleToExifCoordinateString( location.y() ).toStdString();
321 exifData["Exif.GPSInfo.GPSLongitude"] = doubleToExifCoordinateString( location.x() ).toStdString();
322 if ( !std::isnan( details.elevation ) )
323 {
324 const QString elevationString = QStringLiteral( "%1/1000" ).arg( static_cast< int>( std::floor( std::abs( details.elevation ) * 1000 ) ) );
325 exifData["Exif.GPSInfo.GPSAltitude"] = elevationString.toStdString();
326 exifData["Exif.GPSInfo.GPSAltitudeRef"] = details.elevation < 0.0 ? "1" : "0";
327 }
328 exifData["Exif.GPSInfo.GPSLatitudeRef"] = location.y() > 0 ? "N" : "S";
329 exifData["Exif.GPSInfo.GPSLongitudeRef"] = location.x() > 0 ? "E" : "W";
330 exifData["Exif.Image.GPSTag"] = 4908;
331 image->writeMetadata();
332 }
333 catch ( ... )
334 {
335 return false;
336 }
337 return true;
338}
Extended image geotag details.
Definition: qgsexiftools.h:75
double elevation
GPS elevation, or NaN if elevation is not available.
Definition: qgsexiftools.h:86
static QVariantMap readTags(const QString &imagePath)
Returns a map object containing all exif tags stored in the image at imagePath.
static QgsPoint getGeoTag(const QString &imagePath, bool &ok)
Returns the geotagged coordinate stored in the image at imagePath.
static bool geoTagImage(const QString &imagePath, const QgsPointXY &location, const GeoTagDetails &details=QgsExifTools::GeoTagDetails())
Writes geotags to the image at imagePath.
static Q_INVOKABLE bool hasGeoTag(const QString &imagePath)
Returns true if the image at imagePath contains a valid geotag.
static QVariant readTag(const QString &imagePath, const QString &key)
Returns the value of of an exif tag key stored in the image at imagePath.
A class to represent a 2D point.
Definition: qgspointxy.h:59
double y
Definition: qgspointxy.h:63
Q_GADGET double x
Definition: qgspointxy.h:62
Point geometry type, with support for z-dimension and m-values.
Definition: qgspoint.h:49
QString doubleToExifCoordinateString(const double val)
double readCoordinate(const Exiv2::Value &value)
QVariant decodeExifData(const QString &key, Exiv2::ExifData::const_iterator &it)
double readRationale(const Exiv2::Value &value, long n=0)