QGIS API Documentation 3.30.0-'s-Hertogenbosch (f186b8efe0)
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
26double readRationale( const Exiv2::Value &value, long n = 0 )
27{
28 const Exiv2::Rational rational = value.toRational( n );
29 return static_cast< double >( rational.first ) / rational.second;
30};
31
32double readCoordinate( const Exiv2::Value &value )
33{
34 double res = 0;
35 double div = 1;
36 for ( int i = 0; i < 3; i++ )
37 {
38 res += readRationale( value, i ) / div;
39 div *= 60;
40 }
41 return res;
42};
43
44QVariant decodeXmpData( const QString &key, Exiv2::XmpData::const_iterator &it )
45{
46 QVariant val;
47 if ( key == QLatin1String( "Xmp.xmp.MetadataDate" ) ||
48 key == QLatin1String( "Xmp.xmp.CreateDate" ) ||
49 key == QLatin1String( "Xmp.xmp.ModifyDate" ) )
50 {
51 val = QVariant::fromValue( QDateTime::fromString( QString::fromStdString( it->toString() ), Qt::ISODate ) );
52 }
53 else
54 {
55 switch ( it->typeId() )
56 {
57 case Exiv2::asciiString:
58 case Exiv2::string:
59 case Exiv2::comment:
60 case Exiv2::directory:
61 case Exiv2::xmpText:
62 val = QString::fromStdString( it->toString() );
63 break;
64
65 case Exiv2::unsignedLong:
66 case Exiv2::signedLong:
67 case Exiv2::unsignedLongLong:
68 case Exiv2::signedLongLong:
69 val = QVariant::fromValue( it->toLong() );
70 break;
71
72 case Exiv2::tiffDouble:
73 case Exiv2::tiffFloat:
74 val = QVariant::fromValue( it->toFloat() );
75 break;
76
77 case Exiv2::unsignedShort:
78 case Exiv2::signedShort:
79 case Exiv2::unsignedByte:
80 case Exiv2::signedByte:
81 case Exiv2::tiffIfd:
82 case Exiv2::tiffIfd8:
83 val = QVariant::fromValue( static_cast< int >( it->toLong() ) );
84 break;
85
86 case Exiv2::date:
87 {
88 const Exiv2::DateValue::Date date = static_cast< const Exiv2::DateValue *>( &it->value() )->getDate();
89 val = QVariant::fromValue( QDate::fromString( QStringLiteral( "%1-%2-%3" ).arg( date.year )
90 .arg( QString::number( date.month ).rightJustified( 2, '0' ) )
91 .arg( QString::number( date.day ).rightJustified( 2, '0' ) ), QLatin1String( "yyyy-MM-dd" ) ) );
92 break;
93 }
94
95 case Exiv2::time:
96 {
97 const Exiv2::TimeValue::Time time = static_cast< const Exiv2::TimeValue *>( &it->value() )->getTime();
98 val = QVariant::fromValue( QTime::fromString( QStringLiteral( "%1:%2:%3" ).arg( QString::number( time.hour ).rightJustified( 2, '0' ) )
99 .arg( QString::number( time.minute ).rightJustified( 2, '0' ) )
100 .arg( QString::number( time.second ).rightJustified( 2, '0' ) ), QLatin1String( "hh:mm:ss" ) ) );
101 break;
102 }
103
104 case Exiv2::unsignedRational:
105 case Exiv2::signedRational:
106 {
107 if ( it->count() == 1 )
108 {
109 val = QVariant::fromValue( readRationale( it->value() ) );
110 }
111 else
112 {
113 val = QString::fromStdString( it->toString() );
114 }
115 break;
116 }
117
118 case Exiv2::undefined:
119 case Exiv2::xmpAlt:
120 case Exiv2::xmpBag:
121 case Exiv2::xmpSeq:
122 case Exiv2::langAlt:
123 case Exiv2::invalidTypeId:
124 case Exiv2::lastTypeId:
125 val = QString::fromStdString( it->toString() );
126 break;
127
128 }
129 }
130 return val;
131}
132
133QVariant decodeExifData( const QString &key, Exiv2::ExifData::const_iterator &it )
134{
135 QVariant val;
136
137 if ( key == QLatin1String( "Exif.GPSInfo.GPSLatitude" ) ||
138 key == QLatin1String( "Exif.GPSInfo.GPSLongitude" ) ||
139 key == QLatin1String( "Exif.GPSInfo.GPSDestLatitude" ) ||
140 key == QLatin1String( "Exif.GPSInfo.GPSDestLongitude" ) )
141 {
142 val = readCoordinate( it->value() );
143 }
144 else if ( key == QLatin1String( "Exif.GPSInfo.GPSTimeStamp" ) )
145 {
146 const QStringList parts = QString::fromStdString( it->toString() ).split( QRegularExpression( QStringLiteral( "\\s+" ) ) );
147 if ( parts.size() == 3 )
148 {
149 const int hour = readRationale( it->value(), 0 );
150 const int minute = readRationale( it->value(), 1 );
151 const int second = readRationale( it->value(), 2 );
152 val = QVariant::fromValue( QTime::fromString( QStringLiteral( "%1:%2:%3" )
153 .arg( QString::number( hour ).rightJustified( 2, '0' ) )
154 .arg( QString::number( minute ).rightJustified( 2, '0' ) )
155 .arg( QString::number( second ).rightJustified( 2, '0' ) ), QLatin1String( "hh:mm:ss" ) ) );
156 }
157 }
158 else if ( key == QLatin1String( "Exif.GPSInfo.GPSDateStamp" ) )
159 {
160 val = QVariant::fromValue( QDate::fromString( QString::fromStdString( it->toString() ), QLatin1String( "yyyy:MM:dd" ) ) );
161 }
162 else if ( key == QLatin1String( "Exif.Image.DateTime" ) ||
163 key == QLatin1String( "Exif.Image.DateTime" ) ||
164 key == QLatin1String( "Exif.Photo.DateTimeDigitized" ) ||
165 key == QLatin1String( "Exif.Photo.DateTimeOriginal" ) )
166 {
167 val = QVariant::fromValue( QDateTime::fromString( QString::fromStdString( it->toString() ), QLatin1String( "yyyy:MM:dd hh:mm:ss" ) ) );
168 }
169 else
170 {
171 switch ( it->typeId() )
172 {
173 case Exiv2::asciiString:
174 case Exiv2::string:
175 case Exiv2::comment:
176 case Exiv2::directory:
177 case Exiv2::xmpText:
178 val = QString::fromStdString( it->toString() );
179 break;
180
181 case Exiv2::unsignedLong:
182 case Exiv2::signedLong:
183 case Exiv2::unsignedLongLong:
184 case Exiv2::signedLongLong:
185 val = QVariant::fromValue( it->toLong() );
186 break;
187
188 case Exiv2::tiffDouble:
189 case Exiv2::tiffFloat:
190 val = QVariant::fromValue( it->toFloat() );
191 break;
192
193 case Exiv2::unsignedShort:
194 case Exiv2::signedShort:
195 case Exiv2::unsignedByte:
196 case Exiv2::signedByte:
197 case Exiv2::tiffIfd:
198 case Exiv2::tiffIfd8:
199 val = QVariant::fromValue( static_cast< int >( it->toLong() ) );
200 break;
201
202 case Exiv2::date:
203 {
204 const Exiv2::DateValue::Date date = static_cast< const Exiv2::DateValue *>( &it->value() )->getDate();
205 val = QVariant::fromValue( QDate::fromString( QStringLiteral( "%1-%2-%3" ).arg( date.year )
206 .arg( QString::number( date.month ).rightJustified( 2, '0' ) )
207 .arg( QString::number( date.day ).rightJustified( 2, '0' ) ), QLatin1String( "yyyy-MM-dd" ) ) );
208 break;
209 }
210
211 case Exiv2::time:
212 {
213 const Exiv2::TimeValue::Time time = static_cast< const Exiv2::TimeValue *>( &it->value() )->getTime();
214 val = QVariant::fromValue( QTime::fromString( QStringLiteral( "%1:%2:%3" ).arg( QString::number( time.hour ).rightJustified( 2, '0' ) )
215 .arg( QString::number( time.minute ).rightJustified( 2, '0' ) )
216 .arg( QString::number( time.second ).rightJustified( 2, '0' ) ), QLatin1String( "hh:mm:ss" ) ) );
217 break;
218 }
219
220 case Exiv2::unsignedRational:
221 case Exiv2::signedRational:
222 {
223 if ( it->count() == 1 )
224 {
225 val = QVariant::fromValue( readRationale( it->value() ) );
226 }
227 else
228 {
229 val = QString::fromStdString( it->toString() );
230 }
231 break;
232 }
233
234 case Exiv2::undefined:
235 case Exiv2::xmpAlt:
236 case Exiv2::xmpBag:
237 case Exiv2::xmpSeq:
238 case Exiv2::langAlt:
239 case Exiv2::invalidTypeId:
240 case Exiv2::lastTypeId:
241 val = QString::fromStdString( it->toString() );
242 break;
243 }
244 }
245 return val;
246}
247
248QString doubleToExifCoordinateString( const double val )
249{
250 const double d = std::abs( val );
251 const int degrees = static_cast< int >( std::floor( d ) );
252 const double m = 60 * ( d - degrees );
253 const int minutes = static_cast< int >( std::floor( m ) );
254 const double s = 60 * ( m - minutes );
255 const int seconds = static_cast< int >( std::floor( s * 1000 ) );
256 return QStringLiteral( "%1/1 %2/1 %3/1000" ).arg( degrees ).arg( minutes ).arg( seconds );
257}
258
259QVariant QgsExifTools::readTag( const QString &imagePath, const QString &key )
260{
261 if ( !QFileInfo::exists( imagePath ) )
262 return QVariant();
263
264 try
265 {
266 std::unique_ptr< Exiv2::Image > image( Exiv2::ImageFactory::open( imagePath.toStdString() ) );
267 if ( !image || key.isEmpty() )
268 return QVariant();
269
270 image->readMetadata();
271
272 if ( key.startsWith( QLatin1String( "Xmp." ) ) )
273 {
274 Exiv2::XmpData &xmpData = image->xmpData();
275 if ( xmpData.empty() )
276 {
277 return QVariant();
278 }
279 Exiv2::XmpData::const_iterator i = xmpData.findKey( Exiv2::XmpKey( key.toUtf8().constData() ) );
280 return i != xmpData.end() ? decodeXmpData( key, i ) : QVariant();
281 }
282 else
283 {
284 Exiv2::ExifData &exifData = image->exifData();
285 if ( exifData.empty() )
286 {
287 return QVariant();
288 }
289 Exiv2::ExifData::const_iterator i = exifData.findKey( Exiv2::ExifKey( key.toUtf8().constData() ) );
290 return i != exifData.end() ? decodeExifData( key, i ) : QVariant();
291 }
292 }
293 catch ( ... )
294 {
295 return QVariant();
296 }
297}
298
299QVariantMap QgsExifTools::readTags( const QString &imagePath )
300{
301 if ( !QFileInfo::exists( imagePath ) )
302 return QVariantMap();
303
304 try
305 {
306 QVariantMap res;
307 std::unique_ptr< Exiv2::Image > image( Exiv2::ImageFactory::open( imagePath.toStdString() ) );
308 if ( !image )
309 return QVariantMap();
310 image->readMetadata();
311
312 Exiv2::ExifData &exifData = image->exifData();
313 if ( !exifData.empty() )
314 {
315 const Exiv2::ExifData::const_iterator end = exifData.end();
316 for ( Exiv2::ExifData::const_iterator i = exifData.begin(); i != end; ++i )
317 {
318 const QString key = QString::fromStdString( i->key() );
319 res.insert( key, decodeExifData( key, i ) );
320 }
321 }
322
323 Exiv2::XmpData &xmpData = image->xmpData();
324 if ( !xmpData.empty() )
325 {
326 const Exiv2::XmpData::const_iterator end = xmpData.end();
327 for ( Exiv2::XmpData::const_iterator i = xmpData.begin(); i != end; ++i )
328 {
329 const QString key = QString::fromStdString( i->key() );
330 res.insert( key, decodeXmpData( key, i ) );
331 }
332 }
333
334 return res;
335 }
336 catch ( ... )
337 {
338 return QVariantMap();
339 }
340}
341
342bool QgsExifTools::hasGeoTag( const QString &imagePath )
343{
344 bool ok = false;
345 QgsExifTools::getGeoTag( imagePath, ok );
346 return ok;
347}
348
349QgsPoint QgsExifTools::getGeoTag( const QString &imagePath, bool &ok )
350{
351 ok = false;
352 if ( !QFileInfo::exists( imagePath ) )
353 return QgsPoint();
354 try
355 {
356 std::unique_ptr< Exiv2::Image > image( Exiv2::ImageFactory::open( imagePath.toStdString() ) );
357 if ( !image )
358 return QgsPoint();
359
360 image->readMetadata();
361 Exiv2::ExifData &exifData = image->exifData();
362
363 if ( exifData.empty() )
364 return QgsPoint();
365
366 const Exiv2::ExifData::iterator itLatRef = exifData.findKey( Exiv2::ExifKey( "Exif.GPSInfo.GPSLatitudeRef" ) );
367 const Exiv2::ExifData::iterator itLatVal = exifData.findKey( Exiv2::ExifKey( "Exif.GPSInfo.GPSLatitude" ) );
368 const Exiv2::ExifData::iterator itLonRef = exifData.findKey( Exiv2::ExifKey( "Exif.GPSInfo.GPSLongitudeRef" ) );
369 const Exiv2::ExifData::iterator itLonVal = exifData.findKey( Exiv2::ExifKey( "Exif.GPSInfo.GPSLongitude" ) );
370
371 if ( itLatRef == exifData.end() || itLatVal == exifData.end() ||
372 itLonRef == exifData.end() || itLonVal == exifData.end() )
373 return QgsPoint();
374
375 double lat = readCoordinate( itLatVal->value() );
376 double lon = readCoordinate( itLonVal->value() );
377
378 const QString latRef = QString::fromStdString( itLatRef->value().toString() );
379 const QString lonRef = QString::fromStdString( itLonRef->value().toString() );
380 if ( latRef.compare( QLatin1String( "S" ), Qt::CaseInsensitive ) == 0 )
381 {
382 lat *= -1;
383 }
384 if ( lonRef.compare( QLatin1String( "W" ), Qt::CaseInsensitive ) == 0 )
385 {
386 lon *= -1;
387 }
388
389 ok = true;
390
391 const Exiv2::ExifData::iterator itElevVal = exifData.findKey( Exiv2::ExifKey( "Exif.GPSInfo.GPSAltitude" ) );
392 const Exiv2::ExifData::iterator itElevRefVal = exifData.findKey( Exiv2::ExifKey( "Exif.GPSInfo.GPSAltitudeRef" ) );
393 if ( itElevVal != exifData.end() )
394 {
395 double elev = readRationale( itElevVal->value() );
396 if ( itElevRefVal != exifData.end() )
397 {
398 const QString elevRef = QString::fromStdString( itElevRefVal->value().toString() );
399 if ( elevRef.compare( QLatin1String( "1" ), Qt::CaseInsensitive ) == 0 )
400 {
401 elev *= -1;
402 }
403 }
404 return QgsPoint( lon, lat, elev );
405 }
406 else
407 {
408 return QgsPoint( lon, lat );
409 }
410 }
411 catch ( ... )
412 {
413 return QgsPoint();
414 }
415}
416
417bool QgsExifTools::geoTagImage( const QString &imagePath, const QgsPointXY &location, const GeoTagDetails &details )
418{
419 try
420 {
421 std::unique_ptr< Exiv2::Image > image( Exiv2::ImageFactory::open( imagePath.toStdString() ) );
422 if ( !image )
423 return false;
424
425 image->readMetadata();
426 Exiv2::ExifData &exifData = image->exifData();
427
428 exifData["Exif.GPSInfo.GPSVersionID"] = "2 0 0 0";
429 exifData["Exif.GPSInfo.GPSMapDatum"] = "WGS-84";
430 exifData["Exif.GPSInfo.GPSLatitude"] = doubleToExifCoordinateString( location.y() ).toStdString();
431 exifData["Exif.GPSInfo.GPSLongitude"] = doubleToExifCoordinateString( location.x() ).toStdString();
432 if ( !std::isnan( details.elevation ) )
433 {
434 const QString elevationString = QStringLiteral( "%1/1000" ).arg( static_cast< int>( std::floor( std::abs( details.elevation ) * 1000 ) ) );
435 exifData["Exif.GPSInfo.GPSAltitude"] = elevationString.toStdString();
436 exifData["Exif.GPSInfo.GPSAltitudeRef"] = details.elevation < 0.0 ? "1" : "0";
437 }
438 exifData["Exif.GPSInfo.GPSLatitudeRef"] = location.y() > 0 ? "N" : "S";
439 exifData["Exif.GPSInfo.GPSLongitudeRef"] = location.x() > 0 ? "E" : "W";
440 exifData["Exif.Image.GPSTag"] = 4908;
441 image->writeMetadata();
442 }
443 catch ( ... )
444 {
445 return false;
446 }
447 return true;
448}
449
450bool QgsExifTools::tagImage( const QString &imagePath, const QString &tag, const QVariant &value )
451{
452 try
453 {
454 std::unique_ptr< Exiv2::Image > image( Exiv2::ImageFactory::open( imagePath.toStdString() ) );
455 if ( !image )
456 return false;
457
458 QVariant actualValue;
459 if ( tag == QLatin1String( "Exif.GPSInfo.GPSLatitude" ) ||
460 tag == QLatin1String( "Exif.GPSInfo.GPSLongitude" ) ||
461 tag == QLatin1String( "Exif.GPSInfo.GPSDestLatitude" ) ||
462 tag == QLatin1String( "Exif.GPSInfo.GPSDestLongitude" ) )
463 {
464 actualValue = doubleToExifCoordinateString( value.toDouble() );
465 }
466 else if ( tag == QLatin1String( "Exif.GPSInfo.GPSAltitude" ) )
467 {
468 actualValue = QStringLiteral( "%1/1000" ).arg( static_cast< int>( std::floor( std::abs( value.toDouble() ) * 1000 ) ) );
469 }
470 else if ( value.type() == QVariant::DateTime )
471 {
472 const QDateTime dateTime = value.toDateTime();
473 if ( tag == QLatin1String( "Exif.Image.DateTime" ) ||
474 tag == QLatin1String( "Exif.Image.DateTime" ) ||
475 tag == QLatin1String( "Exif.Photo.DateTimeDigitized" ) ||
476 tag == QLatin1String( "Exif.Photo.DateTimeOriginal" ) )
477 {
478 actualValue = dateTime.toString( QStringLiteral( "yyyy:MM:dd hh:mm:ss" ) );
479 }
480 else
481 {
482 actualValue = dateTime.toString( Qt::ISODate );
483 }
484 }
485 else if ( value.type() == QVariant::Date )
486 {
487 const QDate date = value.toDate();
488 if ( tag == QLatin1String( "Exif.GPSInfo.GPSDateStamp" ) )
489 {
490 actualValue = date.toString( QStringLiteral( "yyyy:MM:dd" ) );
491 }
492 else
493 {
494 actualValue = date.toString( QStringLiteral( "yyyy-MM-dd" ) );
495 }
496 }
497 else if ( value.type() == QVariant::Time )
498 {
499 const QTime time = value.toTime();
500 if ( tag == QLatin1String( "Exif.GPSInfo.GPSTimeStamp" ) )
501 {
502 actualValue = QStringLiteral( "%1/1 %2/1 %3/1" ).arg( time.hour() ).arg( time.minute() ).arg( time.second() );
503 }
504 else
505 {
506 actualValue = time.toString( QStringLiteral( "HH:mm:ss" ) );
507 }
508 }
509 else
510 {
511 actualValue = value;
512 }
513
514 const bool isXmp = tag.startsWith( QLatin1String( "Xmp." ) );
515 image->readMetadata();
516 if ( actualValue.type() == QVariant::Int ||
517 actualValue.type() == QVariant::LongLong )
518 {
519 if ( isXmp )
520 {
521 Exiv2::XmpData &xmpData = image->xmpData();
522 xmpData[tag.toStdString()] = static_cast<uint32_t>( actualValue.toLongLong() );
523 }
524 else
525 {
526 Exiv2::ExifData &exifData = image->exifData();
527 exifData[tag.toStdString()] = static_cast<uint32_t>( actualValue.toLongLong() );
528 }
529 }
530 if ( actualValue.type() == QVariant::UInt ||
531 actualValue.type() == QVariant::ULongLong )
532 {
533 if ( isXmp )
534 {
535 Exiv2::XmpData &xmpData = image->xmpData();
536 xmpData[tag.toStdString()] = static_cast<int32_t>( actualValue.toULongLong() );
537 }
538 else
539 {
540 Exiv2::ExifData &exifData = image->exifData();
541 exifData[tag.toStdString()] = static_cast<int32_t>( actualValue.toULongLong() );
542 }
543 }
544 else if ( actualValue.type() == QVariant::Double )
545 {
546 if ( isXmp )
547 {
548 Exiv2::XmpData &xmpData = image->xmpData();
549 xmpData[tag.toStdString()] = Exiv2::floatToRationalCast( actualValue.toFloat() );
550 }
551 else
552 {
553 Exiv2::ExifData &exifData = image->exifData();
554 exifData[tag.toStdString()] = Exiv2::floatToRationalCast( actualValue.toFloat() );
555 }
556 }
557 else
558 {
559 if ( isXmp )
560 {
561 Exiv2::XmpData &xmpData = image->xmpData();
562 xmpData[tag.toStdString()] = actualValue.toString().toStdString();
563 }
564 else
565 {
566 Exiv2::ExifData &exifData = image->exifData();
567 exifData[tag.toStdString()] = actualValue.toString().toStdString();
568 }
569 }
570 image->writeMetadata();
571 }
572 catch ( ... )
573 {
574 return false;
575 }
576 return true;
577}
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 bool tagImage(const QString &imagePath, const QString &tag, const QVariant &value)
Writes a tag to the image at imagePath.
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)
QVariant decodeXmpData(const QString &key, Exiv2::XmpData::const_iterator &it)