mirror of
https://github.com/qgis/QGIS.git
synced 2025-04-14 00:07:35 -04:00
[FEATURE][API] New class QgsExifTools
Contains utilities for retrieving the geotag from images and for setting an image's geotag. Working with geotags (before this class!) is super-annoying and fiddly and relies on either parsing command line tools or depending on non-standard Python libraries which are not available everywhere, and often very difficult for users on certain platforms to get installed and working correctly. With this class we have stable methods for geotag getting/setting which are universally available and can be used safely by plugins and scripts.
This commit is contained in:
parent
62495c0699
commit
8703fb219b
@ -3,6 +3,7 @@
|
||||
%Include auto_generated/raster/qgsalignraster.sip
|
||||
%Include auto_generated/raster/qgsaspectfilter.sip
|
||||
%Include auto_generated/raster/qgsderivativefilter.sip
|
||||
%Include auto_generated/raster/qgsexiftools.sip
|
||||
%Include auto_generated/raster/qgshillshadefilter.sip
|
||||
%Include auto_generated/raster/qgskde.sip
|
||||
%Include auto_generated/raster/qgsninecellfilter.sip
|
||||
|
71
python/analysis/auto_generated/raster/qgsexiftools.sip.in
Normal file
71
python/analysis/auto_generated/raster/qgsexiftools.sip.in
Normal file
@ -0,0 +1,71 @@
|
||||
/************************************************************************
|
||||
* This file has been generated automatically from *
|
||||
* *
|
||||
* src/analysis/raster/qgsexiftools.h *
|
||||
* *
|
||||
* Do not edit manually ! Edit header and run scripts/sipify.pl again *
|
||||
************************************************************************/
|
||||
|
||||
|
||||
|
||||
class QgsExifTools
|
||||
{
|
||||
%Docstring
|
||||
Contains utilities for working with EXIF tags in images.
|
||||
|
||||
.. versionadded:: 3.6
|
||||
%End
|
||||
|
||||
%TypeHeaderCode
|
||||
#include "qgsexiftools.h"
|
||||
%End
|
||||
public:
|
||||
|
||||
|
||||
static QgsPoint getGeoTag( const QString &imagePath, bool &ok /Out/ );
|
||||
%Docstring
|
||||
Returns the geotagged coordinate stored in the image at ``imagePath``.
|
||||
|
||||
If a geotag was found, ``ok`` will be set to true.
|
||||
|
||||
If the image contains an elevation tag then the returned point will contain
|
||||
the elevation as a z value.
|
||||
|
||||
.. seealso:: :py:func:`geoTagImage`
|
||||
%End
|
||||
|
||||
class GeoTagDetails
|
||||
{
|
||||
%Docstring
|
||||
Extended image geotag details.
|
||||
%End
|
||||
|
||||
%TypeHeaderCode
|
||||
#include "qgsexiftools.h"
|
||||
%End
|
||||
public:
|
||||
|
||||
GeoTagDetails();
|
||||
|
||||
double elevation;
|
||||
};
|
||||
|
||||
static bool geotagImage( const QString &imagePath, const QgsPointXY &location, const GeoTagDetails &details = QgsExifTools::GeoTagDetails() );
|
||||
%Docstring
|
||||
Writes geotags to the image at ``imagePath``.
|
||||
|
||||
The ``location`` argument indicates the GPS location to write to the image, as a WGS84 latitude/longitude coordinate.
|
||||
|
||||
If desired, extended GPS tags (such as elevation) can be specified via the ``details`` argument.
|
||||
|
||||
Returns true if writing was successful.
|
||||
%End
|
||||
};
|
||||
|
||||
/************************************************************************
|
||||
* This file has been generated automatically from *
|
||||
* *
|
||||
* src/analysis/raster/qgsexiftools.h *
|
||||
* *
|
||||
* Do not edit manually ! Edit header and run scripts/sipify.pl again *
|
||||
************************************************************************/
|
@ -114,6 +114,7 @@ SET(QGIS_ANALYSIS_SRCS
|
||||
processing/qgsreclassifyutils.cpp
|
||||
|
||||
raster/qgsalignraster.cpp
|
||||
raster/qgsexiftools.cpp
|
||||
raster/qgsninecellfilter.cpp
|
||||
raster/qgsruggednessfilter.cpp
|
||||
raster/qgsderivativefilter.cpp
|
||||
@ -239,6 +240,7 @@ SET(QGIS_ANALYSIS_HDRS
|
||||
raster/qgsalignraster.h
|
||||
raster/qgsaspectfilter.h
|
||||
raster/qgsderivativefilter.h
|
||||
raster/qgsexiftools.h
|
||||
raster/qgshillshadefilter.h
|
||||
raster/qgskde.h
|
||||
raster/qgsninecellfilter.h
|
||||
|
226
src/analysis/raster/qgsexiftools.cpp
Normal file
226
src/analysis/raster/qgsexiftools.cpp
Normal file
@ -0,0 +1,226 @@
|
||||
/***************************************************************************
|
||||
qgisexiftools.cpp
|
||||
-----------------
|
||||
Date : November 2018
|
||||
Copyright : (C) 2018 by Nyall Dawson
|
||||
Email : nyall dot dawson at gmail dot com
|
||||
***************************************************************************
|
||||
* *
|
||||
* This program is free software; you can redistribute it and/or modify *
|
||||
* it under the terms of the GNU General Public License as published by *
|
||||
* the Free Software Foundation; either version 2 of the License, or *
|
||||
* (at your option) any later version. *
|
||||
* *
|
||||
***************************************************************************/
|
||||
|
||||
#include "qgsexiftools.h"
|
||||
#include "qgspoint.h"
|
||||
#include <exiv2/exiv2.hpp>
|
||||
#include <QRegularExpression>
|
||||
#include <QFileInfo>
|
||||
|
||||
#if 0 // needs further work on the correct casting of tag values to QVariant values!
|
||||
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() );
|
||||
QVariant val;
|
||||
switch ( i->typeId() )
|
||||
{
|
||||
case Exiv2::asciiString:
|
||||
case Exiv2::string:
|
||||
case Exiv2::comment:
|
||||
case Exiv2::directory:
|
||||
case Exiv2::xmpText:
|
||||
val = QString::fromStdString( i->toString() );
|
||||
break;
|
||||
|
||||
case Exiv2::unsignedLong:
|
||||
case Exiv2::signedLong:
|
||||
val = QVariant::fromValue( i->toLong() );
|
||||
break;
|
||||
|
||||
case Exiv2::tiffDouble:
|
||||
case Exiv2::tiffFloat:
|
||||
val = QVariant::fromValue( i->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::date:
|
||||
case Exiv2::time:
|
||||
case Exiv2::xmpAlt:
|
||||
case Exiv2::xmpBag:
|
||||
case Exiv2::xmpSeq:
|
||||
case Exiv2::langAlt:
|
||||
case Exiv2::invalidTypeId:
|
||||
case Exiv2::lastTypeId:
|
||||
val = QString::fromStdString( i->toString() );
|
||||
break;
|
||||
|
||||
}
|
||||
|
||||
res.insert( key, val );
|
||||
}
|
||||
return res;
|
||||
}
|
||||
#endif
|
||||
|
||||
QString doubleToExifCoordinate( const double val )
|
||||
{
|
||||
double d = std::abs( val );
|
||||
int degrees = static_cast< int >( std::floor( d ) );
|
||||
double m = 60 * ( d - degrees );
|
||||
int minutes = static_cast< int >( std::floor( m ) );
|
||||
double s = 60 * ( m - minutes );
|
||||
int seconds = static_cast< int >( std::floor( s * 1000 ) );
|
||||
return QStringLiteral( "%1/1 %2/1 %3/1000" ).arg( degrees ).arg( minutes ).arg( seconds );
|
||||
}
|
||||
|
||||
QgsPoint QgsExifTools::getGeoTag( const QString &imagePath, bool &ok )
|
||||
{
|
||||
ok = false;
|
||||
if ( !QFileInfo::exists( imagePath ) )
|
||||
return QgsPoint();
|
||||
try
|
||||
{
|
||||
std::unique_ptr< Exiv2::Image > image( Exiv2::ImageFactory::open( imagePath.toStdString() ) );
|
||||
if ( !image )
|
||||
return QgsPoint();
|
||||
|
||||
image->readMetadata();
|
||||
Exiv2::ExifData &exifData = image->exifData();
|
||||
|
||||
if ( exifData.empty() )
|
||||
return QgsPoint();
|
||||
|
||||
Exiv2::ExifData::iterator itLatRef = exifData.findKey( Exiv2::ExifKey( "Exif.GPSInfo.GPSLatitudeRef" ) );
|
||||
Exiv2::ExifData::iterator itLatVal = exifData.findKey( Exiv2::ExifKey( "Exif.GPSInfo.GPSLatitude" ) );
|
||||
Exiv2::ExifData::iterator itLonRef = exifData.findKey( Exiv2::ExifKey( "Exif.GPSInfo.GPSLongitudeRef" ) );
|
||||
Exiv2::ExifData::iterator itLonVal = exifData.findKey( Exiv2::ExifKey( "Exif.GPSInfo.GPSLongitude" ) );
|
||||
|
||||
if ( itLatRef == exifData.end() || itLatVal == exifData.end() ||
|
||||
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() ) );
|
||||
|
||||
const QString latRef = QString::fromStdString( itLatRef->value().toString() );
|
||||
const QString lonRef = QString::fromStdString( itLonRef->value().toString() );
|
||||
if ( latRef.compare( QLatin1String( "S" ), Qt::CaseInsensitive ) == 0 )
|
||||
{
|
||||
lat *= -1;
|
||||
}
|
||||
if ( lonRef.compare( QLatin1String( "W" ), Qt::CaseInsensitive ) == 0 )
|
||||
{
|
||||
lon *= -1;
|
||||
}
|
||||
|
||||
ok = true;
|
||||
|
||||
Exiv2::ExifData::iterator itElevVal = exifData.findKey( Exiv2::ExifKey( "Exif.GPSInfo.GPSAltitude" ) );
|
||||
Exiv2::ExifData::iterator itElevRefVal = exifData.findKey( Exiv2::ExifKey( "Exif.GPSInfo.GPSAltitudeRef" ) );
|
||||
if ( itElevVal != exifData.end() )
|
||||
{
|
||||
double elev = readRationale( QString::fromStdString( itElevVal->value().toString() ) );
|
||||
if ( itElevRefVal != exifData.end() )
|
||||
{
|
||||
const QString elevRef = QString::fromStdString( itElevRefVal->value().toString() );
|
||||
if ( elevRef.compare( QLatin1String( "1" ), Qt::CaseInsensitive ) == 0 )
|
||||
{
|
||||
elev *= -1;
|
||||
}
|
||||
}
|
||||
return QgsPoint( lon, lat, elev );
|
||||
}
|
||||
else
|
||||
{
|
||||
return QgsPoint( lon, lat );
|
||||
}
|
||||
}
|
||||
catch ( ... )
|
||||
{
|
||||
return QgsPoint();
|
||||
}
|
||||
}
|
||||
|
||||
bool QgsExifTools::geotagImage( const QString &imagePath, const QgsPointXY &location, const GeoTagDetails &details )
|
||||
{
|
||||
try
|
||||
{
|
||||
std::unique_ptr< Exiv2::Image > image( Exiv2::ImageFactory::open( imagePath.toStdString() ) );
|
||||
if ( !image )
|
||||
return false;
|
||||
|
||||
image->readMetadata();
|
||||
Exiv2::ExifData &exifData = image->exifData();
|
||||
|
||||
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();
|
||||
if ( !std::isnan( details.elevation ) )
|
||||
{
|
||||
const QString elevationString = QStringLiteral( "%1/1000" ).arg( static_cast< int>( std::floor( std::abs( details.elevation ) * 1000 ) ) );
|
||||
exifData["Exif.GPSInfo.GPSAltitude"] = elevationString.toStdString();
|
||||
exifData["Exif.GPSInfo.GPSAltitudeRef"] = details.elevation < 0.0 ? "1" : "0";
|
||||
}
|
||||
exifData["Exif.GPSInfo.GPSLatitudeRef"] = location.y() > 0 ? "N" : "S";
|
||||
exifData["Exif.GPSInfo.GPSLongitudeRef"] = location.x() > 0 ? "E" : "W";
|
||||
exifData["Exif.Image.GPSTag"] = 4908;
|
||||
image->writeMetadata();
|
||||
}
|
||||
catch ( ... )
|
||||
{
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
80
src/analysis/raster/qgsexiftools.h
Normal file
80
src/analysis/raster/qgsexiftools.h
Normal file
@ -0,0 +1,80 @@
|
||||
/***************************************************************************
|
||||
qgisexiftools.h
|
||||
---------------
|
||||
Date : November 2018
|
||||
Copyright : (C) 2018 by Nyall Dawson
|
||||
Email : nyall dot dawson at gmail dot com
|
||||
***************************************************************************
|
||||
* *
|
||||
* This program is free software; you can redistribute it and/or modify *
|
||||
* it under the terms of the GNU General Public License as published by *
|
||||
* the Free Software Foundation; either version 2 of the License, or *
|
||||
* (at your option) any later version. *
|
||||
* *
|
||||
***************************************************************************/
|
||||
|
||||
#ifndef QGSEXIFTOOLS_H
|
||||
#define QGSEXIFTOOLS_H
|
||||
|
||||
#include "qgis_analysis.h"
|
||||
#include "qgspointxy.h"
|
||||
#include <QString>
|
||||
#include <QVariant>
|
||||
#include <QVariantMap>
|
||||
|
||||
/**
|
||||
* Contains utilities for working with EXIF tags in images.
|
||||
* \ingroup analysis
|
||||
* \since QGIS 3.6
|
||||
*/
|
||||
class ANALYSIS_EXPORT QgsExifTools
|
||||
{
|
||||
public:
|
||||
|
||||
#if 0
|
||||
static QVariantMap readTags( const QString &imagePath );
|
||||
#endif
|
||||
|
||||
/**
|
||||
* Returns the geotagged coordinate stored in the image at \a imagePath.
|
||||
*
|
||||
* If a geotag was found, \a ok will be set to true.
|
||||
*
|
||||
* If the image contains an elevation tag then the returned point will contain
|
||||
* the elevation as a z value.
|
||||
*
|
||||
* \see geoTagImage()
|
||||
*/
|
||||
static QgsPoint getGeoTag( const QString &imagePath, bool &ok SIP_OUT );
|
||||
|
||||
/**
|
||||
* Extended image geotag details.
|
||||
*/
|
||||
class GeoTagDetails
|
||||
{
|
||||
public:
|
||||
|
||||
GeoTagDetails()
|
||||
: elevation( std::numeric_limits< double >::quiet_NaN() )
|
||||
{
|
||||
}
|
||||
|
||||
/**
|
||||
* GPS elevation, or NaN if elevation is not available.
|
||||
*/
|
||||
double elevation = 0;
|
||||
};
|
||||
|
||||
/**
|
||||
* Writes geotags to the image at \a imagePath.
|
||||
*
|
||||
* The \a location argument indicates the GPS location to write to the image, as a WGS84 latitude/longitude coordinate.
|
||||
*
|
||||
* If desired, extended GPS tags (such as elevation) can be specified via the \a details argument.
|
||||
*
|
||||
* Returns true if writing was successful.
|
||||
*/
|
||||
static bool geotagImage( const QString &imagePath, const QgsPointXY &location, const GeoTagDetails &details = QgsExifTools::GeoTagDetails() );
|
||||
};
|
||||
|
||||
#endif // QGSEXIFTOOLS_H
|
@ -44,6 +44,7 @@ ADD_PYTHON_TEST(PyQgsEditFormConfig test_qgseditformconfig.py)
|
||||
ADD_PYTHON_TEST(PyQgsEditWidgets test_qgseditwidgets.py)
|
||||
ADD_PYTHON_TEST(PyQgsEllipsoidUtils test_qgsellipsoidutils.py)
|
||||
ADD_PYTHON_TEST(PyQgsEncodingSelectionDialog test_qgsencodingselectiondialog.py)
|
||||
ADD_PYTHON_TEST(PyQgsExifTools test_qgsexiftools.py)
|
||||
ADD_PYTHON_TEST(PyQgsExpression test_qgsexpression.py)
|
||||
ADD_PYTHON_TEST(PyQgsExpressionBuilderWidget test_qgsexpressionbuilderwidget.py)
|
||||
ADD_PYTHON_TEST(PyQgsExpressionLineEdit test_qgsexpressionlineedit.py)
|
||||
|
96
tests/src/python/test_qgsexiftools.py
Normal file
96
tests/src/python/test_qgsexiftools.py
Normal file
@ -0,0 +1,96 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
"""QGIS Unit tests for QgsExifTools.
|
||||
|
||||
.. note:: This program is free software; you can redistribute it and/or modify
|
||||
it under the terms of the GNU General Public License as published by
|
||||
the Free Software Foundation; either version 2 of the License, or
|
||||
(at your option) any later version.
|
||||
"""
|
||||
__author__ = 'Nyall Dawson'
|
||||
__date__ = '09/11/2018'
|
||||
__copyright__ = 'Copyright 2018, The QGIS Project'
|
||||
# This will get replaced with a git SHA1 when you do a git archive
|
||||
__revision__ = '$Format:%H$'
|
||||
|
||||
import qgis # NOQA switch sip api
|
||||
import os
|
||||
import shutil
|
||||
from qgis.PyQt.QtCore import QTemporaryFile
|
||||
from qgis.core import QgsPointXY
|
||||
from qgis.analysis import (QgsExifTools)
|
||||
from qgis.testing import start_app, unittest
|
||||
from utilities import unitTestDataPath
|
||||
|
||||
TEST_DATA_DIR = unitTestDataPath()
|
||||
|
||||
start_app()
|
||||
|
||||
|
||||
class TestQgsExifUtils(unittest.TestCase):
|
||||
|
||||
def testGeoTags(self):
|
||||
photos_folder = os.path.join(TEST_DATA_DIR, 'photos')
|
||||
|
||||
tag, ok = QgsExifTools.getGeoTag('')
|
||||
self.assertFalse(ok)
|
||||
|
||||
tag, ok = QgsExifTools.getGeoTag(os.path.join(photos_folder, '0997.JPG'))
|
||||
self.assertTrue(ok)
|
||||
self.assertEqual(tag.asWkt(6), 'PointZ (149.275167 -37.2305 422.191011)')
|
||||
|
||||
tag, ok = QgsExifTools.getGeoTag(os.path.join(photos_folder, 'geotagged.jpg'))
|
||||
self.assertTrue(ok)
|
||||
self.assertEqual(tag.asWkt(6), 'PointZ (149.131878 -36.220892 867)')
|
||||
|
||||
tag, ok = QgsExifTools.getGeoTag(os.path.join(photos_folder, 'notags.JPG'))
|
||||
self.assertFalse(ok)
|
||||
|
||||
tag, ok = QgsExifTools.getGeoTag(os.path.join(photos_folder, 'not_photo.jpg'))
|
||||
self.assertFalse(ok)
|
||||
|
||||
def testTagging(self):
|
||||
self.assertFalse(QgsExifTools.geotagImage('', QgsPointXY(1, 2)))
|
||||
self.assertFalse(QgsExifTools.geotagImage('not a path', QgsPointXY(1, 2)))
|
||||
|
||||
src_photo = os.path.join(TEST_DATA_DIR, 'photos', 'notags.JPG')
|
||||
|
||||
tmpFile = QTemporaryFile()
|
||||
tmpFile.open()
|
||||
tmpName = tmpFile.fileName()
|
||||
tmpFile.close()
|
||||
|
||||
shutil.copy(src_photo, tmpName)
|
||||
self.assertTrue(QgsExifTools.geotagImage(tmpName, QgsPointXY(1.1, 3.3)))
|
||||
tag, ok = QgsExifTools.getGeoTag(tmpName)
|
||||
self.assertTrue(ok)
|
||||
self.assertEqual(tag.asWkt(6), 'Point (1.1 3.3)')
|
||||
os.remove(tmpName)
|
||||
|
||||
shutil.copy(src_photo, tmpName)
|
||||
self.assertTrue(QgsExifTools.geotagImage(tmpName, QgsPointXY(-1.1, -3.3)))
|
||||
tag, ok = QgsExifTools.getGeoTag(tmpName)
|
||||
self.assertTrue(ok)
|
||||
self.assertEqual(tag.asWkt(6), 'Point (-1.1 -3.3)')
|
||||
os.remove(tmpName)
|
||||
|
||||
shutil.copy(src_photo, tmpName)
|
||||
deets = QgsExifTools.GeoTagDetails()
|
||||
deets.elevation = 110.1
|
||||
self.assertTrue(QgsExifTools.geotagImage(tmpName, QgsPointXY(1.1, 3.3), deets))
|
||||
tag, ok = QgsExifTools.getGeoTag(tmpName)
|
||||
self.assertTrue(ok)
|
||||
self.assertEqual(tag.asWkt(6), 'PointZ (1.1 3.3 110.1)')
|
||||
os.remove(tmpName)
|
||||
|
||||
shutil.copy(src_photo, tmpName)
|
||||
deets = QgsExifTools.GeoTagDetails()
|
||||
deets.elevation = -110.1
|
||||
self.assertTrue(QgsExifTools.geotagImage(tmpName, QgsPointXY(1.1, 3.3), deets))
|
||||
tag, ok = QgsExifTools.getGeoTag(tmpName)
|
||||
self.assertTrue(ok)
|
||||
self.assertEqual(tag.asWkt(6), 'PointZ (1.1 3.3 -110.1)')
|
||||
os.remove(tmpName)
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
unittest.main()
|
BIN
tests/testdata/photos/0997.JPG
vendored
Executable file
BIN
tests/testdata/photos/0997.JPG
vendored
Executable file
Binary file not shown.
After Width: | Height: | Size: 5.0 KiB |
BIN
tests/testdata/photos/geotagged.jpg
vendored
Normal file
BIN
tests/testdata/photos/geotagged.jpg
vendored
Normal file
Binary file not shown.
After Width: | Height: | Size: 24 KiB |
1
tests/testdata/photos/not_photo.jpg
vendored
Executable file
1
tests/testdata/photos/not_photo.jpg
vendored
Executable file
@ -0,0 +1 @@
|
||||
a
|
BIN
tests/testdata/photos/notags.JPG
vendored
Normal file
BIN
tests/testdata/photos/notags.JPG
vendored
Normal file
Binary file not shown.
After Width: | Height: | Size: 536 B |
Loading…
x
Reference in New Issue
Block a user