MauiKit Image Tools

modules/metadata/exiv2extractor.cpp
1// SPDX-License-Identifier: LGPL-3.0-or-later
2
3/*
4 * Copyright (C) 2012-15 Vishesh Handa <vhanda@kde.org>
5 *
6 * This library is free software; you can redistribute it and/or
7 * modify it under the terms of the GNU Lesser General Public
8 * License as published by the Free Software Foundation; either
9 * version 2.1 of the License, or (at your option) any later version.
10 *
11 * This library is distributed in the hope that it will be useful,
12 * but WITHOUT ANY WARRANTY; without even the implied warranty of
13 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
14 * Lesser General Public License for more details.
15 *
16 * You should have received a copy of the GNU Lesser General Public
17 * License along with this library; if not, write to the Free Software
18 * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
19 */
20
21#include "exiv2extractor.h"
22
23#include <QGeoAddress>
24#include <QFileInfo>
25#include <QDateTime>
26#include <QDebug>
27#include <QFile>
28#include <QTextCodec>
29
30#include "geolocation/cities.h"
31#include "geolocation/city.h"
32
33Exiv2Extractor::Exiv2Extractor(const QUrl &url, QObject *parent) : QObject(parent)
34, m_error(true)
35
36{
37 this->setUrl(url);
38}
39
40Exiv2Extractor::Exiv2Extractor(QObject *parent) : QObject(parent)
41, m_error(true)
42
43{
44
45}
46
47void Exiv2Extractor::setUrl(const QUrl &url)
48{
49 m_url = url;
50 if (!QFileInfo::exists(m_url.toLocalFile()) || m_url.isEmpty() || !m_url.isValid()) {
51 m_error = true;
52 }
53
54 try {
55 m_image = Exiv2::ImageFactory::open(m_url.toLocalFile().toStdString());
56 } catch (const std::exception &) {
57 return;
58 }
59 if (!m_image.get()) {
60 return;
61 }
62
63 if (!m_image->good()) {
64 return;
65 }
66
67 try {
68 m_image->readMetadata();
69 } catch (const std::exception &) {
70 return;
71 }
72
73 m_error = false;
74}
75
76
77Exiv2::ExifData & Exiv2Extractor::exifData() const
78{
79 Exiv2::ExifData &exifData = m_image->exifData();
80// if (exifData.empty()) {
81// qWarning() << "No EXIF data in : " << m_url.toString();
82// }
83
84 return exifData;
85}
86
87Coordinates Exiv2Extractor::extractGPS() const
88{
89 double latitude = fetchGpsDouble("Exif.GPSInfo.GPSLatitude");
90 double longitude = fetchGpsDouble("Exif.GPSInfo.GPSLongitude");
91
92 QByteArray latRef = getExifTagData("Exif.GPSInfo.GPSLatitudeRef");
93 if (!latRef.isEmpty() && latRef[0] == 'S')
94 latitude *= -1;
95
96 QByteArray longRef = getExifTagData("Exif.GPSInfo.GPSLongitudeRef");
97 if (!longRef.isEmpty() && longRef[0] == 'W')
98 longitude *= -1;
99
100 return {latitude, longitude};
101}
102
103double Exiv2Extractor::fetchGpsDouble(const char *name) const
104{
105 Exiv2::ExifData &data = (exifData());
106 Exiv2::ExifData::const_iterator it = data.findKey(Exiv2::ExifKey(name));
107 if (it != data.end() && it->count() == 3) {
108 double n = 0.0;
109 double d = 0.0;
110
111 n = (*it).toRational(0).first;
112 d = (*it).toRational(0).second;
113
114 if (d == 0) {
115 return 0.0;
116 }
117
118 double deg = n / d;
119
120 n = (*it).toRational(1).first;
121 d = (*it).toRational(1).second;
122
123 if (d == 0) {
124 return deg;
125 }
126
127 double min = n / d;
128 if (min != -1.0) {
129 deg += min / 60.0;
130 }
131
132 n = (*it).toRational(2).first;
133 d = (*it).toRational(2).second;
134
135 if (d == 0) {
136 return deg;
137 }
138
139 double sec = n / d;
140 if (sec != -1.0) {
141 deg += sec / 3600.0;
142 }
143
144 return deg;
145 }
146
147 return 0.0;
148}
149
150bool Exiv2Extractor::error() const
151{
152 return m_error;
153}
154
155QString Exiv2Extractor::getExifTagString(const char* exifTagName, bool escapeCR) const
156{
157 try
158 {
159 Exiv2::ExifKey exifKey(exifTagName);
160 Exiv2::ExifData &data = (exifData());
161 Exiv2::ExifData::iterator it = data.findKey(exifKey);
162
163
164 if (it != data.end())
165 {
166 // See B.K.O #184156 comment #13
167 std::string val = it->print(&data);
169
170 if (escapeCR)
172
173 return tagValue;
174 }
175 }
176 catch( Exiv2::Error& e )
177 {
178 qWarning() << QString("Cannot find Exif key '%1' into image using Exiv2 ").arg(QString::fromLatin1(exifTagName)) << e.what();
179 }
180 catch(...)
181 {
182 qWarning() << "Default exception from Exiv2";
183 }
184
185 return QString();
186}
187
188QByteArray Exiv2Extractor::getExifTagData(const char* exifTagName) const
189{
190 try
191 {
192 Exiv2::ExifKey exifKey(exifTagName);
193 Exiv2::ExifData &data = (exifData());
194 Exiv2::ExifData::iterator it = data.findKey(exifKey);
195
196 if (it != data.end())
197 {
198 char* const s = new char[(*it).size()];
199 (*it).copy((Exiv2::byte*)s, Exiv2::bigEndian);
200 QByteArray data(s, (*it).size());
201 delete[] s;
202
203 return data;
204 }
205 }
206 catch( Exiv2::Error& e )
207 {
208 qWarning() << QString("Cannot find Exif key '%1' into image using Exiv2 ").arg(QString::fromLatin1(exifTagName)) << e.what();
209 }
210 catch(...)
211 {
212 qWarning() << "Default exception from Exiv2";
213 }
214
215 return QByteArray();
216}
217
218QVariant Exiv2Extractor::getExifTagVariant(const char* exifTagName, bool rationalAsListOfInts, bool stringEscapeCR, int component) const
219{
220 try
221 {
222 Exiv2::ExifKey exifKey(exifTagName);
223 Exiv2::ExifData &data = (exifData());
224 Exiv2::ExifData::iterator it = data.findKey(exifKey);
225
226 if (it != data.end())
227 {
228 switch (it->typeId())
229 {
230 case Exiv2::unsignedByte:
231 case Exiv2::unsignedShort:
232 case Exiv2::unsignedLong:
233 case Exiv2::signedShort:
234 case Exiv2::signedLong:
235 if (it->count() > component)
236 return QVariant((int)it->toLong(component));
237 else
238 return QVariant(QVariant::Int);
239 case Exiv2::unsignedRational:
240 case Exiv2::signedRational:
241
242 if (rationalAsListOfInts)
243 {
244 if (it->count() <= component)
245 return QVariant(QVariant::List);
246
248 list << (*it).toRational(component).first;
249 list << (*it).toRational(component).second;
250
251 return QVariant(list);
252 }
253 else
254 {
255 if (it->count() <= component)
257
258 // prefer double precision
259 double num = (*it).toRational(component).first;
260 double den = (*it).toRational(component).second;
261
262 if (den == 0.0)
264
265 return QVariant(num / den);
266 }
267 case Exiv2::date:
268 case Exiv2::time:
269 {
270 QDateTime dateTime = QDateTime::fromString(QString::fromLatin1(it->toString().c_str()), Qt::ISODate);
271 return QVariant(dateTime);
272 }
273 case Exiv2::asciiString:
274 case Exiv2::comment:
275 case Exiv2::string:
276 {
277 std::ostringstream os;
278 os << *it;
279 QString tagValue = QString::fromLocal8Bit(os.str().c_str());
280
281 if (stringEscapeCR)
283
284 return QVariant(tagValue);
285 }
286 default:
287 break;
288 }
289 }
290 }
291 catch( Exiv2::Error& e )
292 {
293 qWarning () << QString("Cannot find Exif key '%1' in the image using Exiv2 ").arg(QString::fromLatin1(exifTagName)) << e.what();
294 }
295 catch(...)
296 {
297 qWarning() << "Default exception from Exiv2";
298 }
299
300 return QVariant();
301}
302
303static bool isUtf8(const char* const buffer)
304{
305 int i, n;
306 unsigned char c;
307 bool gotone = false;
308
309 if (!buffer)
310 return true;
311
312 // character never appears in text
313 #define F 0
314 // character appears in plain ASCII text
315 #define T 1
316 // character appears in ISO-8859 text
317 #define I 2
318 // character appears in non-ISO extended ASCII (Mac, IBM PC)
319 #define X 3
320
321 static const unsigned char text_chars[256] =
322 {
323 // BEL BS HT LF FF CR
324 F, F, F, F, F, F, F, T, T, T, T, F, T, T, F, F, // 0x0X
325 // ESC
326 F, F, F, F, F, F, F, F, F, F, F, T, F, F, F, F, // 0x1X
327 T, T, T, T, T, T, T, T, T, T, T, T, T, T, T, T, // 0x2X
328 T, T, T, T, T, T, T, T, T, T, T, T, T, T, T, T, // 0x3X
329 T, T, T, T, T, T, T, T, T, T, T, T, T, T, T, T, // 0x4X
330 T, T, T, T, T, T, T, T, T, T, T, T, T, T, T, T, // 0x5X
331 T, T, T, T, T, T, T, T, T, T, T, T, T, T, T, T, // 0x6X
332 T, T, T, T, T, T, T, T, T, T, T, T, T, T, T, F, // 0x7X
333 // NEL
334 X, X, X, X, X, T, X, X, X, X, X, X, X, X, X, X, // 0x8X
335 X, X, X, X, X, X, X, X, X, X, X, X, X, X, X, X, // 0x9X
336 I, I, I, I, I, I, I, I, I, I, I, I, I, I, I, I, // 0xaX
337 I, I, I, I, I, I, I, I, I, I, I, I, I, I, I, I, // 0xbX
338 I, I, I, I, I, I, I, I, I, I, I, I, I, I, I, I, // 0xcX
339 I, I, I, I, I, I, I, I, I, I, I, I, I, I, I, I, // 0xdX
340 I, I, I, I, I, I, I, I, I, I, I, I, I, I, I, I, // 0xeX
341 I, I, I, I, I, I, I, I, I, I, I, I, I, I, I, I // 0xfX
342 };
343
344 for (i = 0; (c = buffer[i]); ++i)
345 {
346 if ((c & 0x80) == 0)
347 {
348 // 0xxxxxxx is plain ASCII
349
350 // Even if the whole file is valid UTF-8 sequences,
351 // still reject it if it uses weird control characters.
352
353 if (text_chars[c] != T)
354 return false;
355
356 }
357 else if ((c & 0x40) == 0)
358 {
359 // 10xxxxxx never 1st byte
360 return false;
361 }
362 else
363 {
364 // 11xxxxxx begins UTF-8
365 int following = 0;
366
367 if ((c & 0x20) == 0)
368 {
369 // 110xxxxx
370 following = 1;
371 }
372 else if ((c & 0x10) == 0)
373 {
374 // 1110xxxx
375 following = 2;
376 }
377 else if ((c & 0x08) == 0)
378 {
379 // 11110xxx
380 following = 3;
381 }
382 else if ((c & 0x04) == 0)
383 {
384 // 111110xx
385 following = 4;
386 }
387 else if ((c & 0x02) == 0)
388 {
389 // 1111110x
390 following = 5;
391 }
392 else
393 {
394 return false;
395 }
396
397 for (n = 0; n < following; ++n)
398 {
399 i++;
400
401 if (!(c = buffer[i]))
402 goto done;
403
404 if ((c & 0x80) == 0 || (c & 0x40))
405 return false;
406 }
407
408 gotone = true;
409 }
410 }
411
412 done:
413
414 return gotone; // don't claim it's UTF-8 if it's all 7-bit.
415}
416
417static QString detectEncodingAndDecode(const std::string& value)
418{
419 // For charset autodetection, we could use sophisticated code
420 // (Mozilla chardet, KHTML's autodetection, QTextCodec::codecForContent),
421 // but that is probably too much.
422 // We check for UTF8, Local encoding and ASCII.
423 // Look like KEncodingDetector class can provide a full implementation for encoding detection.
424
425 if (value.empty())
426 {
427 return QString();
428 }
429
430 if (isUtf8(value.c_str()))
431 {
432 return QString::fromUtf8(value.c_str());
433 }
434
435 // Utf8 has a pretty unique byte pattern.
436 // Thats not true for ASCII, it is not possible
437 // to reliably autodetect different ISO-8859 charsets.
438 // So we can use either local encoding, or latin1.
439
440 return QString::fromLocal8Bit(value.c_str());
441}
442
443static QString convertCommentValue(const Exiv2::Exifdatum& exifDatum)
444{
445 try
446 {
447 std::string comment;
448 std::string charset;
449
450 comment = exifDatum.toString();
451
452 // libexiv2 will prepend "charset=\"SomeCharset\" " if charset is specified
453 // Before conversion to QString, we must know the charset, so we stay with std::string for a while
454 if (comment.length() > 8 && comment.substr(0, 8) == "charset=")
455 {
456 // the prepended charset specification is followed by a blank
457 std::string::size_type pos = comment.find_first_of(' ');
458
459 if (pos != std::string::npos)
460 {
461 // extract string between the = and the blank
462 charset = comment.substr(8, pos-8);
463 // get the rest of the string after the charset specification
464 comment = comment.substr(pos+1);
465 }
466 }
467
468 if (charset == "\"Unicode\"")
469 {
470 return QString::fromUtf8(comment.data());
471 }
472 else if (charset == "\"Jis\"")
473 {
474 QTextCodec* const codec = QTextCodec::codecForName("JIS7");
475 return codec->toUnicode(comment.c_str());
476 }
477 else if (charset == "\"Ascii\"")
478 {
479 return QString::fromLatin1(comment.c_str());
480 }
481 else
482 {
483 return detectEncodingAndDecode(comment);
484 }
485 }
486 catch( Exiv2::Error& e )
487 {
488 qWarning() << (QString::fromLatin1("Cannot convert Comment using Exiv2 "), e.what());
489 }
490 catch(...)
491 {
492 qWarning()<< "Default exception from Exiv2";
493 }
494
495 return QString();
496}
497
498MetaDataMap Exiv2Extractor::getExifTagsDataList(const QStringList& exifKeysFilter, bool invertSelection) const
499{
500 if (exifData().empty())
501 return MetaDataMap();
502
503 try
504 {
505 Exiv2::ExifData &data = exifData();
506 data.sortByKey();
507
508 MetaDataMap metaDataMap;
509
510 for (Exiv2::ExifData::iterator md = data.begin(); md != data.end(); ++md)
511 {
512 QString key = QString::fromLatin1(md->key().c_str());
513
514 // Decode the tag value with a user friendly output.
516
517 if (key == QString::fromLatin1("Exif.Photo.UserComment"))
518 {
519 tagValue = convertCommentValue(*md);
520 }
521 else if (key == QString::fromLatin1("Exif.Image.0x935c"))
522 {
523 tagValue = QString::number(md->value().size());
524 }
525 else
526 {
527 std::ostringstream os;
528 os << *md;
529
530 // Exif tag contents can be an translated strings, no only simple ascii.
531 tagValue = QString::fromLocal8Bit(os.str().c_str());
532 }
533
535
536 // We apply a filter to get only the Exif tags that we need.
537
538 if (!exifKeysFilter.isEmpty())
539 {
540 if (!invertSelection)
541 {
542 if (exifKeysFilter.contains(key.section(QString::fromLatin1("."), 1, 1)))
543 metaDataMap.insert(key, tagValue);
544 }
545 else
546 {
547 if (!exifKeysFilter.contains(key.section(QString::fromLatin1("."), 1, 1)))
548 metaDataMap.insert(key, tagValue);
549 }
550 }
551 else // else no filter at all.
552 {
553 metaDataMap.insert(key, tagValue);
554 }
555 }
556
557 return metaDataMap;
558 }
559 catch (Exiv2::Error& e)
560 {
561 qWarning() << (QString::fromLatin1("Cannot parse EXIF metadata using Exiv2 "), e.what());
562 }
563 catch(...)
564 {
565 qWarning() << "Default exception from Exiv2";
566 }
567
568 return MetaDataMap();
569}
570
571QString Exiv2Extractor::getExifComment() const
572{
573 try
574 {
575 if (!exifData().empty())
576 {
577 Exiv2::ExifData &data(exifData());
578 Exiv2::ExifKey key("Exif.Photo.UserComment");
579 Exiv2::ExifData::iterator it = data.findKey(key);
580
581 if (it != data.end())
582 {
583 QString exifComment = convertCommentValue(*it);
584
585 // some cameras fill the UserComment with whitespace
586 if (!exifComment.isEmpty() && !exifComment.trimmed().isEmpty())
587 return exifComment;
588 }
589
590 Exiv2::ExifKey key2("Exif.Image.ImageDescription");
591 Exiv2::ExifData::iterator it2 = data.findKey(key2);
592
593 if (it2 != data.end())
594 {
595 QString exifComment = convertCommentValue(*it2);
596
597 // Some cameras fill in nonsense default values
598 QStringList blackList;
599 blackList << QString::fromLatin1("SONY DSC"); // + whitespace
600 blackList << QString::fromLatin1("OLYMPUS DIGITAL CAMERA");
601 blackList << QString::fromLatin1("MINOLTA DIGITAL CAMERA");
602
603 QString trimmedComment = exifComment.trimmed();
604
605 // some cameras fill the UserComment with whitespace
606 if (!exifComment.isEmpty() && !trimmedComment.isEmpty() && !blackList.contains(trimmedComment))
607 return exifComment;
608 }
609 }
610 }
611 catch( Exiv2::Error& e )
612 {
613 qWarning() << (QString::fromLatin1("Cannot find Exif User Comment using Exiv2 "), e.what());
614 }
615 catch(...)
616 {
617 qWarning() << "Default exception from Exiv2";
618 }
619
620 return QString();
621}
622
623QString Exiv2Extractor::GPSString() const
624{
625 if(error())
626 {
627 return QString();
628 }
629
630 std::unique_ptr<City>m_city(city());
631
632 if(!m_city)
633 {
634 return QString();
635 }
636
637 if(!m_city->isValid())
638 {
639 return QString();
640 }
641
642 return m_city->name();
643}
644
645QString Exiv2Extractor::cityId() const
646{
647 if(error())
648 {
649 return QString();
650 }
651
652 std::unique_ptr<City>m_city(city());
653
654 if(!m_city)
655 {
656 return QString();
657 }
658
659 if(!m_city->isValid())
660 {
661 return QString();
662 }
663
664 return m_city->id();
665}
666
667City* Exiv2Extractor::city() const
668{
669 if(error())
670 {
671 return nullptr;
672 }
673
674 auto c = extractGPS();
675
676 if(c.first == 0.0 || c.second == 0.0)
677 {
678 return nullptr;
679 }
680
681 return Cities::getInstance()->findCity(c.first, c.second);
682}
683
684bool Exiv2Extractor::writeTag(const char *tagName, const QVariant &value)
685{
686 try
687 {
688 qDebug() << "trying to write tag4";
689
690 Exiv2::ExifKey exifKey(tagName);
691 Exiv2::ExifData &data = (exifData());
692 Exiv2::ExifData::iterator it = data.findKey(exifKey);
693 qDebug() << "trying to write tag5";
694
695 if (it != data.end())
696 {
697 qDebug() << "trying to write tag2";
698
699 switch (it->typeId())
700 {
701 case Exiv2::unsignedByte:
702 case Exiv2::unsignedShort:
703 case Exiv2::unsignedLong:
704 case Exiv2::signedShort:
705 case Exiv2::signedLong:
706 case Exiv2::unsignedLongLong:
707 case Exiv2::signedLongLong:
708 {
709 if(!value.canConvert<QString>())
710 return false;
711
712 qDebug() << "Writting number metadata" << tagName;
713
714 Exiv2::Value::AutoPtr v = Exiv2::Value::create(Exiv2::signedLongLong);
715 v->read(value.toString().toStdString());
716 it->setValue(v.get());
717 break;
718 }
719
720 case Exiv2::unsignedRational:
721 case Exiv2::signedRational:
722 {
723 if(!value.canConvert<QString>())
724 return false;
725 qDebug() << "Writting rational metadata" << tagName;
726
727 Exiv2::RationalValue::AutoPtr rv(new Exiv2::RationalValue);
728 rv->read(value.toString().toStdString());
729 it->setValue(rv.get());
730 break;
731
732 }
733 case Exiv2::date:
734 case Exiv2::time:
735 {
736 if(!value.canConvert<QString>())
737 return false;
738
739 auto date = value.toString();
740 Exiv2::Value::AutoPtr v = Exiv2::Value::create(Exiv2::asciiString);
741 v->read(date.toStdString());
742 it->setValue(v.get());
743 break;
744
745 }
746 case Exiv2::asciiString:
747 case Exiv2::comment:
748 case Exiv2::string:
749 {
750 if(!value.canConvert<QString>())
751 return false;
752 qDebug() << "Writting ascii metadata" << tagName;
753
754 auto string = value.toString();
755 Exiv2::Value::AutoPtr v = Exiv2::Value::create(Exiv2::asciiString);
756 v->read(string.toStdString());
757 it->setValue(v.get());
758 break;
759
760
761 }
762 default:
763 qDebug() << "Writting unkown metadata" << tagName;
764
765 return false;
766 }
767
768 qDebug() << "Writting metadata EXIF tag to file" << tagName;
769 // m_image->setExifData(data);
770 m_image->writeMetadata();
771 return true;
772 }else
773 {
774 Exiv2::Exifdatum& tag = data[tagName];
775 std::string str = value.toString().toStdString();
776 tag.setValue(str);
777 m_image->writeMetadata();
778 return true;
779 }
780 }
781 catch( Exiv2::Error& e )
782 {
783 qWarning () << QString("Cannot find Exif key '%1' in the image using Exiv2 ").arg(QString::fromLatin1(tagName)) << e.what();
784 return false;
785
786 }
787 catch(...)
788 {
789 qWarning() << "Default exception from Exiv2";
790 return false;
791
792 }
793 return false;
794}
795
796bool Exiv2Extractor::removeTag(const char *tagName)
797{
798
799 try
800 {
801 Exiv2::ExifKey key = Exiv2::ExifKey(tagName);
802 Exiv2::ExifData &data = (exifData());
803
804 Exiv2::ExifData::iterator it = data.findKey(key);
805
806 if (it != data.end())
807 {
808 data.erase(it);
809 m_image->writeMetadata();
810 return true;
811 }
812 }
813 catch( Exiv2::Error& e )
814 {
815 qWarning () << QString("Cannot find Exif key '%1' in the image using Exiv2 ").arg(QString::fromLatin1(tagName)) << e.what();
816 return false;
817
818 }
819 catch(...)
820 {
821 qWarning() << "Default exception from Exiv2";
822 return false;
823
824 }
825
826 return false;
827}
828
A class for representing the GPS coordinates and information of a city.
KIOCORE_EXPORT QStringList list(const QString &fileClass)
QByteArray tagValue(const Elem &elem, const char *keyName)
bool isEmpty() const const
QByteArray & replace(QByteArrayView before, QByteArrayView after)
QDateTime fromString(QStringView string, QStringView format, QCalendar cal)
bool exists() const const
T & first()
bool isEmpty() const const
iterator insert(const Key &key, const T &value)
QString arg(Args &&... args) const const
QString fromLatin1(QByteArrayView str)
QString fromLocal8Bit(QByteArrayView str)
QString fromUtf8(QByteArrayView str)
bool isEmpty() const const
QString number(double n, char format, int precision)
QString section(QChar sep, qsizetype start, qsizetype end, SectionFlags flags) const const
std::string toStdString() const const
QString trimmed() const const
bool contains(QLatin1StringView str, Qt::CaseSensitivity cs) const const
bool isEmpty() const const
bool isValid() const const
QString toLocalFile() const const
bool canConvert() const const
QString toString() const const
This file is part of the KDE documentation.
Documentation copyright © 1996-2024 The KDE developers.
Generated on Fri Nov 22 2024 12:04:14 by doxygen 1.12.0 written by Dimitri van Heesch, © 1997-2006

KDE's Doxygen guidelines are available online.