QGIS API Documentation  3.26.3-Buenos Aires (65e4edfdad)
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 
27 double 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 
33 double 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 
45 QVariant 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 
161 QString 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 
172 QVariant 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 
199 QVariantMap 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 
232 bool QgsExifTools::hasGeoTag( const QString &imagePath )
233 {
234  bool ok = false;
235  QgsExifTools::getGeoTag( imagePath, ok );
236  return ok;
237 }
238 
239 QgsPoint 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 
307 bool 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 }
readCoordinate
double readCoordinate(const Exiv2::Value &value)
Definition: qgsexiftools.cpp:33
QgsExifTools::getGeoTag
static QgsPoint getGeoTag(const QString &imagePath, bool &ok)
Returns the geotagged coordinate stored in the image at imagePath.
Definition: qgsexiftools.cpp:239
QgsPointXY::y
double y
Definition: qgspointxy.h:63
QgsPoint
Point geometry type, with support for z-dimension and m-values.
Definition: qgspoint.h:48
QgsExifTools::readTag
static QVariant readTag(const QString &imagePath, const QString &key)
Returns the value of of an exif tag key stored in the image at imagePath.
Definition: qgsexiftools.cpp:172
decodeExifData
QVariant decodeExifData(const QString &key, Exiv2::ExifData::const_iterator &it)
Definition: qgsexiftools.cpp:45
qgspoint.h
qgsexiftools.h
QgsExifTools::geoTagImage
static bool geoTagImage(const QString &imagePath, const QgsPointXY &location, const GeoTagDetails &details=QgsExifTools::GeoTagDetails())
Writes geotags to the image at imagePath.
Definition: qgsexiftools.cpp:307
QgsPointXY
A class to represent a 2D point.
Definition: qgspointxy.h:58
QgsExifTools::readTags
static QVariantMap readTags(const QString &imagePath)
Returns a map object containing all exif tags stored in the image at imagePath.
Definition: qgsexiftools.cpp:199
QgsExifTools::hasGeoTag
static Q_INVOKABLE bool hasGeoTag(const QString &imagePath)
Returns true if the image at imagePath contains a valid geotag.
Definition: qgsexiftools.cpp:232
readRationale
double readRationale(const Exiv2::Value &value, long n=0)
Definition: qgsexiftools.cpp:27
QgsPointXY::x
double x
Definition: qgspointxy.h:62
QgsExifTools::GeoTagDetails::elevation
double elevation
GPS elevation, or NaN if elevation is not available.
Definition: qgsexiftools.h:99
doubleToExifCoordinateString
QString doubleToExifCoordinateString(const double val)
Definition: qgsexiftools.cpp:161
QgsExifTools::GeoTagDetails
Extended image geotag details.
Definition: qgsexiftools.h:87