MythTV master
imagemetadata.cpp
Go to the documentation of this file.
1#include "imagemetadata.h"
2
3#include <QTextStream>
4
6#include "libmythbase/mythdirs.h" // for GetAppBinDir
8#include "libmythbase/mythsystemlegacy.h" // for ffprobe
9
10// libexiv2 for Exif metadata
11#include <exiv2/exiv2.hpp>
12
13// To read FFMPEG Metadata
14extern "C" {
15#include "libavformat/avformat.h"
16}
17
18// Uncomment this to log raw metadata from exif/ffmpeg
19//#define DUMP_METADATA_TAGS yes
20
21#define LOC QString("ImageMetaData: ")
22
23
29int Orientation::Transform(int transform)
30{
31 m_current = Apply(transform);
32 return Composite();
33}
34
41{
42 return m_current;
43}
44
45
55int Orientation::Apply(int transform) const
56{
57 if (transform == kResetToExif)
58 return m_file;
59
60 // https://github.com/recurser/exif-orientation-examples is a useful resource.
61 switch (m_current)
62 {
63 case 0: // The image has no orientation info
64 case 1: // The image is in its original state
65 switch (transform)
66 {
67 case kRotateCW: return 6;
68 case kRotateCCW: return 8;
69 case kFlipHorizontal: return 2;
70 case kFlipVertical: return 4;
71 }
72 break;
73
74 case 2: // The image is horizontally flipped
75 switch (transform)
76 {
77 case kRotateCW: return 7;
78 case kRotateCCW: return 5;
79 case kFlipHorizontal: return 1;
80 case kFlipVertical: return 3;
81 }
82 break;
83
84 case 3: // The image is rotated 180°
85 switch (transform)
86 {
87 case kRotateCW: return 8;
88 case kRotateCCW: return 6;
89 case kFlipHorizontal: return 4;
90 case kFlipVertical: return 2;
91 }
92 break;
93
94 case 4: // The image is vertically flipped
95 switch (transform)
96 {
97 case kRotateCW: return 5;
98 case kRotateCCW: return 7;
99 case kFlipHorizontal: return 3;
100 case kFlipVertical: return 1;
101 }
102 break;
103
104 case 5: // The image is rotated 90° CW and flipped horizontally
105 switch (transform)
106 {
107 case kRotateCW: return 2;
108 case kRotateCCW: return 4;
109 case kFlipHorizontal: return 6;
110 case kFlipVertical: return 8;
111 }
112 break;
113
114 case 6: // The image is rotated 90° CCW
115 switch (transform)
116 {
117 case kRotateCW: return 3;
118 case kRotateCCW: return 1;
119 case kFlipHorizontal: return 5;
120 case kFlipVertical: return 7;
121 }
122 break;
123
124 case 7: // The image is rotated 90° CW and flipped vertically
125 switch (transform)
126 {
127 case kRotateCW: return 4;
128 case kRotateCCW: return 2;
129 case kFlipHorizontal: return 8;
130 case kFlipVertical: return 6;
131 }
132 break;
133
134 case 8: // The image is rotated 90° CW
135 switch (transform)
136 {
137 case kRotateCW: return 1;
138 case kRotateCCW: return 3;
139 case kFlipHorizontal: return 7;
140 case kFlipVertical: return 5;
141 }
142 break;
143 }
144 return m_current;
145}
146
147
153int Orientation::FromRotation(const QString &degrees)
154{
155 if (degrees == "0") return 1;
156 if (degrees == "90") return 6;
157 if (degrees == "180") return 3;
158 if (degrees == "270") return 8;
159 return 0;
160}
161
162
170{
171 return (m_file == m_current)
172 ? AsText(m_file)
173 : tr("File: %1, Db: %2").arg(AsText(m_file),
175}
176
177
183QString Orientation::AsText(int orientation)
184{
185 switch (orientation)
186 {
187 case 1: return tr("1 (Normal)");
188 case 2: return tr("2 (H Mirror)");
189 case 3: return tr("3 (Rotate 180°)");
190 case 4: return tr("4 (V Mirror)");
191 case 5: return tr("5 (H Mirror, Rot 270°)");
192 case 6: return tr("6 (Rotate 90°)");
193 case 7: return tr("7 (H Mirror, Rot 90°)");
194 case 8: return tr("8 (Rotate 270°)");
195 default: return tr("%1 (Undefined)").arg(orientation);
196 }
197}
198
199
202{
203public:
204 explicit PictureMetaData(const QString &filePath);
205 ~PictureMetaData() override = default; // libexiv2 closes file, cleans up via autoptrs
206
207 bool IsValid() override // ImageMetaData
208 { return m_image.get(); }
209 QStringList GetAllTags() override; // ImageMetaData
210 int GetOrientation(bool *exists = nullptr) override; // ImageMetaData
211 QDateTime GetOriginalDateTime(bool *exists = nullptr) override; // ImageMetaData
212 QString GetComment(bool *exists = nullptr) override; // ImageMetaData
213
214protected:
215 static QString DecodeComment(std::string rawValue);
216
217 std::string GetTag(const QString &key, bool *exists = nullptr);
218
219 Exiv2::Image::UniquePtr m_image;
220 Exiv2::ExifData m_exifData;
221};
222
223
228PictureMetaData::PictureMetaData(const QString &filePath)
229 : ImageMetaData(filePath), m_image(nullptr)
230{
231 try
232 {
233 m_image = Exiv2::ImageFactory::open(filePath.toStdString());
234
236 {
237 m_image->readMetadata();
238 m_exifData = m_image->exifData();
239 }
240 else
241 {
242 LOG(VB_GENERAL, LOG_ERR, LOC +
243 QString("Exiv2 error: Could not open file %1").arg(filePath));
244 }
245 }
246 catch (Exiv2::Error &e)
247 {
248 LOG(VB_GENERAL, LOG_ERR, LOC + QString("Exiv2 exception %1").arg(e.what()));
249 }
250}
251
252
261{
262 QStringList tags;
263 if (!IsValid())
264 return tags;
265
266 LOG(VB_FILE, LOG_DEBUG, LOC + QString("Found %1 tag(s) for file %2")
267 .arg(m_exifData.count()).arg(m_filePath));
268
269 Exiv2::ExifData::const_iterator i;
270 for (i = m_exifData.begin(); i != m_exifData.end(); ++i)
271 {
272 QString label = QString::fromStdString(i->tagLabel());
273
274 // Ignore empty labels
275 if (label.isEmpty())
276 continue;
277
278 QString key = QString::fromStdString(i->key());
279
280 // Ignore large values (binary/private tags)
281 if (i->size() >= 256)
282 {
283 LOG(VB_FILE, LOG_DEBUG, LOC +
284 QString("Ignoring %1 (%2, %3) : Too big")
285 .arg(key, i->typeName()).arg(i->size()));
286 }
287 // Ignore 'Print Image Matching'
288 else if (i->tag() == EXIF_PRINT_IMAGE_MATCHING)
289 {
290 LOG(VB_FILE, LOG_DEBUG, LOC +
291 QString("Ignoring %1 (%2, %3) : Undecodable")
292 .arg(key, i->typeName()).arg(i->size()));
293 }
294 else
295 {
296 // Use interpreted values
297 std::string val = i->print(&m_exifData);
298
299 // Comment needs charset decoding
300 QString value = (key == EXIF_TAG_USERCOMMENT)
301 ? DecodeComment(val) : QString::fromStdString(val);
302
303 // Nulls can arise from corrupt metadata (MakerNote)
304 // Remove them as they disrupt socket comms between BE & remote FE's
305 if (value.contains(QChar::Null))
306 {
307 LOG(VB_GENERAL, LOG_NOTICE, LOC +
308 QString("Corrupted Exif detected in %1").arg(m_filePath));
309 value = "????";
310 }
311
312 // Encode tag
313 QString str = ToString(key, label, value);
314 tags << str;
315
316#ifdef DUMP_METADATA_TAGS
317 LOG(VB_FILE, LOG_DEBUG, LOC + QString("%1 (%2, %3)")
318 .arg(str, i->typeName()).arg(i->size()));
319#endif
320 }
321 }
322 return tags;
323}
324
325
333std::string PictureMetaData::GetTag(const QString &key, bool *exists)
334{
335 std::string value;
336 if (exists)
337 *exists = false;
338
339 if (!IsValid())
340 return value;
341
342 Exiv2::ExifKey exifKey = Exiv2::ExifKey(key.toStdString());
343 auto exifIt = m_exifData.findKey(exifKey);
344
345 if (exifIt == m_exifData.end())
346 return value;
347
348 if (exists)
349 *exists = true;
350
351 // Use raw value
352 return exifIt->value().toString();
353}
354
355
362{
363 std::string value = GetTag(EXIF_TAG_ORIENTATION, exists);
364 return QString::fromStdString(value).toInt();
365}
366
367
374{
375 std::string value = GetTag(EXIF_TAG_DATETIME, exists);
376 QString dt = QString::fromStdString(value);
377
378 // Exif time has no timezone
380}
381
382
390{
391 // Use User Comment or else Image Description
392 bool comExists = false;
393 bool desExists = false;
394
395 std::string comment = GetTag(EXIF_TAG_USERCOMMENT, &comExists);
396
397 if (comment.empty())
398 comment = GetTag(EXIF_TAG_IMAGEDESCRIPTION, &desExists);
399
400 if (exists)
401 *exists = comExists || desExists;
402
403 return DecodeComment(comment);
404}
405
406
412QString PictureMetaData::DecodeComment(std::string rawValue)
413{
414 // Decode charset
415 Exiv2::CommentValue comVal = Exiv2::CommentValue(rawValue);
416 if (comVal.charsetId() != Exiv2::CommentValue::undefined)
417 rawValue = comVal.comment();
418 return QString::fromStdString(rawValue);
419}
420
421
428{
429public:
430 explicit VideoMetaData(const QString &filePath);
431 ~VideoMetaData() override;
432
433 bool IsValid() override // ImageMetaData
434 { return m_dict; }
435 QStringList GetAllTags() override; // ImageMetaData
436 int GetOrientation(bool *exists = nullptr) override; // ImageMetaData
437 QDateTime GetOriginalDateTime(bool *exists = nullptr) override; // ImageMetaData
438 QString GetComment(bool *exists = nullptr) override; // ImageMetaData
439
440protected:
441 QString GetTag(const QString &key, bool *exists = nullptr);
442
443 AVFormatContext *m_context { nullptr };
445 AVDictionary *m_dict { nullptr };
446};
447
448
453VideoMetaData::VideoMetaData(const QString &filePath)
454 : ImageMetaData(filePath)
455{
456 AVInputFormat* p_inputformat = nullptr;
457
458 // Open file
459 if (avformat_open_input(&m_context, filePath.toLatin1().constData(),
460 p_inputformat, nullptr) < 0)
461 return;
462
463 // Locate video stream
464 int vidStream = av_find_best_stream(m_context, AVMEDIA_TYPE_VIDEO, -1, -1, nullptr, 0);
465 if (vidStream >= 0)
466 m_dict = m_context->streams[vidStream]->metadata;
467
469 avformat_close_input(&m_context);
470}
471
472
477{
479 avformat_close_input(&m_context);
480}
481
482
494{
495 QStringList tags;
496 if (!IsValid())
497 return tags;
498
499 // Only extract interesting fields:
500 // For list use: mythffprobe -show_format -show_streams <file>
501 QString cmd = GetAppBinDir() + MYTH_APPNAME_MYTHFFPROBE;
502 QStringList args;
503 args << "-loglevel quiet"
504 << "-print_format compact" // Returns "section|key=value|key=value..."
505 << "-pretty" // Add units etc
506 << "-show_entries "
507 "format=format_long_name,duration,bit_rate:format_tags:"
508 "stream=codec_long_name,codec_type,width,height,pix_fmt,color_space,avg_frame_rate"
509 ",codec_tag_string,sample_rate,channels,channel_layout,bit_rate:stream_tags"
510 << m_filePath;
511
513
514 ffprobe.Run(5s);
515
516 if (ffprobe.Wait() != GENERIC_EXIT_OK)
517 {
518 LOG(VB_GENERAL, LOG_ERR, LOC +
519 QString("Timeout or Failed: %2 %3").arg(cmd, args.join(" ")));
520 return tags;
521 }
522
523 QByteArray result = ffprobe.ReadAll();
524 QTextStream ostream(result);
525 int stream = 0;
526 while (!ostream.atEnd())
527 {
528 QStringList fields = ostream.readLine().split('|');
529
530 if (fields.size() <= 1)
531 // Empty section
532 continue;
533
534 // First fields should be "format" or "stream"
535 QString prefix = "";
536 QString group = fields.takeFirst();
537 if (group == "stream")
538 {
539 // Streams use index as group
540 prefix = QString::number(stream++) + ":";
541 group.append(prefix);
542 }
543
544 for (const auto& field : std::as_const(fields))
545 {
546 // Expect label=value
547 QStringList parts = field.split('=');
548 if (parts.size() != 2)
549 continue;
550
551 // Remove ffprobe "tag:" prefix
552 QString label = parts[0].remove("tag:");
553 QString value = parts[1];
554
555 // Construct a pseudo-key for FFMPEG tags
556 QString key = QString("FFmpeg.%1.%2").arg(group, label);
557
558 // Add stream id to labels
559 QString str = ToString(key, prefix + label, value);
560 tags << str;
561
562#ifdef DUMP_METADATA_TAGS
563 LOG(VB_FILE, LOG_DEBUG, LOC + str);
564#endif
565 }
566 }
567 return tags;
568}
569
570
577QString VideoMetaData::GetTag(const QString &key, bool *exists)
578{
579 if (m_dict)
580 {
581 AVDictionaryEntry *tag = nullptr;
582 while ((tag = av_dict_get(m_dict, "\0", tag, AV_DICT_IGNORE_SUFFIX)))
583 {
584 if (QString(tag->key) == key)
585 {
586 if (exists)
587 *exists = true;
588 return QString::fromUtf8(tag->value);
589 }
590 }
591 }
592 if (exists)
593 *exists = false;
594 return {};
595}
596
597
604{
605 QString angle = GetTag(FFMPEG_TAG_ORIENTATION, exists);
606 return Orientation::FromRotation(angle);
607}
608
609
616{
617 QString dt = GetTag(FFMPEG_TAG_DATETIME, exists);
618
619 // Video time has no timezone
621}
622
623
631{
632 if (exists)
633 *exists = false;
634 return {};
635}
636
637
644{ return new PictureMetaData(filePath); }
645
646
653{ return new VideoMetaData(filePath); }
654
655
656const QString ImageMetaData::kSeparator = "|-|";
657
658
664ImageMetaData::TagMap ImageMetaData::ToMap(const QStringList &tagStrings)
665{
666 TagMap tags;
667 for (const auto& token : std::as_const(tagStrings))
668 {
669 QStringList parts = FromString(token);
670 // Expect Key, Label, Value.
671 if (parts.size() == 3)
672 {
673 // Map tags by Family.Group to keep them together
674 // Within each group they will preserve list ordering
675 QString group = parts[0].section('.', 0, 1);
676 tags.insert(group, parts);
677
678#ifdef DUMP_METADATA_TAGS
679 LOG(VB_FILE, LOG_DEBUG, LOC + QString("%1 = %2").arg(group, token));
680#endif
681 }
682 }
683 return tags;
684}
Abstract class for image metadata.
Definition: imagemetadata.h:92
QMultiMap< QString, QStringList > TagMap
static ImageMetaData * FromPicture(const QString &filePath)
Factory to retrieve metadata from pictures.
static QString ToString(const QString &name, const QString &label, const QString &value)
Encodes metadata into a string as <tag name><tag label><tag value>
static TagMap ToMap(const QStringList &tags)
Creates a map of metadata tags as.
static const QString kSeparator
Unique separator to delimit fields within a string.
QString m_filePath
Image filepath.
static ImageMetaData * FromVideo(const QString &filePath)
Factory to retrieve metadata from videos.
static QStringList FromString(const QString &str)
Decodes metadata name, label, value from a string.
uint Wait(std::chrono::seconds timeout=0s)
void Run(std::chrono::seconds timeout=0s)
Runs a command inside the /bin/sh shell. Returns immediately.
QByteArray & ReadAll()
static QString AsText(int orientation)
Converts orientation code to text description for info display.
int Transform(int transform)
Adjust orientation to apply a transform to an image.
int Composite() const
Encode original & current orientation to a single Db field.
Definition: imagemetadata.h:71
int m_current
The orientation to use: the file orientation with user transformations applied.
Definition: imagemetadata.h:84
static int FromRotation(const QString &degrees)
Convert degrees of rotation into Exif orientation code.
int Apply(int transform) const
Adjust current orientation code to apply a transform to an image.
int m_file
The orientation of the raw file image, as specified by the camera.
Definition: imagemetadata.h:86
int GetCurrent() const
Determines orientation required for an image.
QString Description() const
Generate text description of orientation.
Reads Exif metadata from a picture using libexiv2.
int GetOrientation(bool *exists=nullptr) override
Read Exif orientation.
QStringList GetAllTags() override
Returns all metadata tags.
static QString DecodeComment(std::string rawValue)
Decodes charset of UserComment.
PictureMetaData(const QString &filePath)
Constructor. Reads metadata from image.
Exiv2::Image::UniquePtr m_image
Exiv2::ExifData m_exifData
bool IsValid() override
QDateTime GetOriginalDateTime(bool *exists=nullptr) override
Read Exif timestamp of image capture.
std::string GetTag(const QString &key, bool *exists=nullptr)
Read a single Exif metadata tag.
~PictureMetaData() override=default
QString GetComment(bool *exists=nullptr) override
Read Exif comments from metadata.
Reads video metadata tags using FFmpeg Raw values for Orientation & Date are read quickly via FFmpeg ...
QDateTime GetOriginalDateTime(bool *exists=nullptr) override
Read video datestamp.
AVDictionary * m_dict
FFmpeg tag dictionary.
VideoMetaData(const QString &filePath)
Constructor. Opens best video stream from video.
QString GetComment(bool *exists=nullptr) override
Read Video comment from metadata.
AVFormatContext * m_context
QStringList GetAllTags() override
Reads relevant video metadata by running mythffprobe.
bool IsValid() override
~VideoMetaData() override
Destructor. Closes file.
QString GetTag(const QString &key, bool *exists=nullptr)
Read a single video tag.
int GetOrientation(bool *exists=nullptr) override
Read FFmpeg video orientation tag.
@ GENERIC_EXIT_OK
Exited with no error.
Definition: exitcodes.h:13
#define LOC
Handles Exif/FFMpeg metadata tags for images.
static constexpr const char * FFMPEG_TAG_DATETIME
Definition: imagemetadata.h:34
static constexpr const char * FFMPEG_TAG_ORIENTATION
Definition: imagemetadata.h:33
static constexpr const char * EXIF_TAG_ORIENTATION
Definition: imagemetadata.h:25
static constexpr const char * EXIF_TAG_USERCOMMENT
Definition: imagemetadata.h:29
static constexpr const char * EXIF_TAG_DATE_FORMAT
Definition: imagemetadata.h:27
static constexpr const char * EXIF_TAG_DATETIME
Definition: imagemetadata.h:26
static constexpr uint16_t EXIF_PRINT_IMAGE_MATCHING
Definition: imagemetadata.h:30
@ kFlipVertical
Reflect about horizontal axis.
Definition: imagemetadata.h:51
@ kRotateCCW
Rotate anti-clockwise.
Definition: imagemetadata.h:49
@ kFlipHorizontal
Reflect about vertical axis.
Definition: imagemetadata.h:50
@ kRotateCW
Rotate clockwise.
Definition: imagemetadata.h:48
@ kResetToExif
Reset to Exif value.
Definition: imagemetadata.h:47
static constexpr const char * EXIF_TAG_IMAGEDESCRIPTION
Definition: imagemetadata.h:28
static constexpr const char * FFMPEG_TAG_DATE_FORMAT
Definition: imagemetadata.h:35
static constexpr const char * MYTH_APPNAME_MYTHFFPROBE
Definition: mythappname.h:21
QString GetAppBinDir(void)
Definition: mythdirs.cpp:260
#define LOG(_MASK_, _LEVEL_, _QSTRING_)
Definition: mythlogging.h:39
@ kMSStdOut
allow access to stdout
Definition: mythsystem.h:41
@ kMSRunShell
run process through shell
Definition: mythsystem.h:43
QDateTime fromString(const QString &dtstr)
Converts kFilename && kISODate formats to QDateTime.
Definition: mythdate.cpp:39
bool exists(str path)
Definition: xbmcvfs.py:51