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