MythTV  master
imagemetadata.cpp
Go to the documentation of this file.
1 #include "imagemetadata.h"
2 
3 #include "mythlogging.h"
4 #include "mythcorecontext.h" // for avcodeclock
5 #include "mythdirs.h" // for ffprobe
6 #include "mythsystemlegacy.h" // for ffprobe
7 #include "exitcodes.h" // for ffprobe
8 
9 // libexiv2 for Exif metadata
10 #include <exiv2/exiv2.hpp>
11 
12 // To read FFMPEG Metadata
13 extern "C" {
14 #include "libavformat/avformat.h"
15 }
16 
17 // Uncomment this to log raw metadata from exif/ffmpeg
18 //#define DUMP_METADATA_TAGS yes
19 
20 #define LOC QString("ImageMetaData: ")
21 
22 
28 int Orientation::Transform(int transform)
29 {
30  m_current = Apply(transform);
31  return Composite();
32 }
33 
34 
41 {
43 
44  if (krunningQt541)
45  {
46  // Each row/string defines codes for a single file orientation
47  // Each col/value defines applicable code for corresponding current orientation
48  // As current orientation is applicable to raw camera image, these codes
49  // define the current orientation relative to 1/Normal (as Qt 5.4.1 has already
50  // applied the file orientation)
51  QStringList vals = QStringList()
52  << "0 1 2 3 4 5 6 7 8"
53  << "0 1 2 3 4 5 6 7 8"
54  << "0 2 1 4 3 8 7 6 5"
55  << "0 3 4 1 2 7 8 5 6"
56  << "0 4 3 2 1 6 5 8 7"
57  << "0 5 6 7 8 1 2 3 4"
58  << "0 8 7 6 5 2 1 4 3"
59  << "0 7 8 5 6 3 4 1 2"
60  << "0 6 5 8 7 4 3 2 1";
61 
62  for (int row = 0; row < vals.size(); ++row)
63  {
64  QStringList rowVals = vals.at(row).split(' ');
65  for (int col = 0; col < rowVals.size(); ++col)
66  matrix[row][col] = rowVals.at(col).toInt();
67  }
68  }
69  return matrix;
70 }
71 
72 const bool Orientation::krunningQt541 = (strcmp(qVersion(), "5.4.1") == 0);
75 
76 
85 int Orientation::GetCurrent(bool compensate)
86 {
87  // Qt 5.4.1 automatically applies the file orientation when loading images
88  // Ref: https://codereview.qt-project.org/#/c/111398/
89  // Ref: https://codereview.qt-project.org/#/c/110685/
90  // https://bugreports.qt.io/browse/QTBUG-37946
91  if (compensate && krunningQt541)
92  {
93  // Deduce orientation relative to 1/Normal from file & current orientations
94  int old = m_current;
96 
97  LOG(VB_FILE, LOG_DEBUG, LOC +
98  QString("Adjusted orientation %1 to %2 for Qt 5.4.1")
99  .arg(old).arg(m_current));
100  }
101  return m_current;
102 }
103 
104 
114 int Orientation::Apply(int transform)
115 {
116  if (transform == kResetToExif)
117  return m_file;
118 
119  // https://github.com/recurser/exif-orientation-examples is a useful resource.
120  switch (m_current)
121  {
122  case 0: // The image has no orientation info
123  case 1: // The image is in its original state
124  switch (transform)
125  {
126  case kRotateCW: return 6;
127  case kRotateCCW: return 8;
128  case kFlipHorizontal: return 2;
129  case kFlipVertical: return 4;
130  }
131  break;
132 
133  case 2: // The image is horizontally flipped
134  switch (transform)
135  {
136  case kRotateCW: return 7;
137  case kRotateCCW: return 5;
138  case kFlipHorizontal: return 1;
139  case kFlipVertical: return 3;
140  }
141  break;
142 
143  case 3: // The image is rotated 180°
144  switch (transform)
145  {
146  case kRotateCW: return 8;
147  case kRotateCCW: return 6;
148  case kFlipHorizontal: return 4;
149  case kFlipVertical: return 2;
150  }
151  break;
152 
153  case 4: // The image is vertically flipped
154  switch (transform)
155  {
156  case kRotateCW: return 5;
157  case kRotateCCW: return 7;
158  case kFlipHorizontal: return 3;
159  case kFlipVertical: return 1;
160  }
161  break;
162 
163  case 5: // The image is rotated 90° CW and flipped horizontally
164  switch (transform)
165  {
166  case kRotateCW: return 2;
167  case kRotateCCW: return 4;
168  case kFlipHorizontal: return 6;
169  case kFlipVertical: return 8;
170  }
171  break;
172 
173  case 6: // The image is rotated 90° CCW
174  switch (transform)
175  {
176  case kRotateCW: return 3;
177  case kRotateCCW: return 1;
178  case kFlipHorizontal: return 5;
179  case kFlipVertical: return 7;
180  }
181  break;
182 
183  case 7: // The image is rotated 90° CW and flipped vertically
184  switch (transform)
185  {
186  case kRotateCW: return 4;
187  case kRotateCCW: return 2;
188  case kFlipHorizontal: return 8;
189  case kFlipVertical: return 6;
190  }
191  break;
192 
193  case 8: // The image is rotated 90° CW
194  switch (transform)
195  {
196  case kRotateCW: return 1;
197  case kRotateCCW: return 3;
198  case kFlipHorizontal: return 7;
199  case kFlipVertical: return 5;
200  }
201  break;
202  }
203  return m_current;
204 }
205 
206 
212 int Orientation::FromRotation(const QString &degrees)
213 {
214  if (degrees == "0") return 1;
215  if (degrees == "90") return 6;
216  if (degrees == "180") return 3;
217  if (degrees == "270") return 8;
218  return 0;
219 }
220 
221 
229 {
230  return (m_file == m_current)
231  ? AsText(m_file)
232  : tr("File: %1, Db: %2").arg(AsText(m_file),
233  AsText(m_current));
234 }
235 
236 
242 QString Orientation::AsText(int orientation)
243 {
244  switch (orientation)
245  {
246  case 1: return tr("1 (Normal)");
247  case 2: return tr("2 (H Mirror)");
248  case 3: return tr("3 (Rotate 180°)");
249  case 4: return tr("4 (V Mirror)");
250  case 5: return tr("5 (H Mirror, Rot 270°)");
251  case 6: return tr("6 (Rotate 90°)");
252  case 7: return tr("7 (H Mirror, Rot 90°)");
253  case 8: return tr("8 (Rotate 270°)");
254  default: return tr("%1 (Undefined)").arg(orientation);
255  }
256 }
257 
258 
261 {
262 public:
263  explicit PictureMetaData(const QString &filePath);
264  ~PictureMetaData() override = default; // libexiv2 closes file, cleans up via autoptrs
265 
266  bool IsValid() override // ImageMetaData
267  { return m_image.get(); }
268  QStringList GetAllTags() override; // ImageMetaData
269  int GetOrientation(bool *exists = nullptr) override; // ImageMetaData
270  QDateTime GetOriginalDateTime(bool *exists = nullptr) override; // ImageMetaData
271  QString GetComment(bool *exists = nullptr) override; // ImageMetaData
272 
273 protected:
274  static QString DecodeComment(std::string rawValue);
275 
276  std::string GetTag(const QString &key, bool *exists = nullptr);
277 
278  // Clang8 warns that 'AutoPtr' is deprecated. It was apparently
279  // deprecated in glibc-2.27, and the exiv2 library hasn't been
280  // updated yet.
281  Exiv2::Image::AutoPtr m_image;
282  Exiv2::ExifData m_exifData;
283 };
284 
285 
290 PictureMetaData::PictureMetaData(const QString &filePath)
291  : ImageMetaData(filePath), m_image(nullptr)
292 {
293  try
294  {
295  m_image = Exiv2::ImageFactory::open(filePath.toStdString());
296 
298  {
299  m_image->readMetadata();
300  m_exifData = m_image->exifData();
301  }
302  else
303  LOG(VB_GENERAL, LOG_ERR, LOC +
304  QString("Exiv2 error: Could not open file %1").arg(filePath));
305  }
306  catch (Exiv2::Error &e)
307  {
308  LOG(VB_GENERAL, LOG_ERR, LOC + QString("Exiv2 exception %1").arg(e.what()));
309  }
310 }
311 
312 
321 {
322  QStringList tags;
323  if (!IsValid())
324  return tags;
325 
326  LOG(VB_FILE, LOG_DEBUG, LOC + QString("Found %1 tag(s) for file %2")
327  .arg(m_exifData.count()).arg(m_filePath));
328 
329  Exiv2::ExifData::const_iterator i;
330  for (i = m_exifData.begin(); i != m_exifData.end(); ++i)
331  {
332  QString label = QString::fromStdString(i->tagLabel());
333 
334  // Ignore empty labels
335  if (label.isEmpty())
336  continue;
337 
338  QString key = QString::fromStdString(i->key());
339 
340  // Ignore large values (binary/private tags)
341  if (i->size() >= 256)
342  {
343  LOG(VB_FILE, LOG_DEBUG, LOC +
344  QString("Ignoring %1 (%2, %3) : Too big")
345  .arg(key, i->typeName()).arg(i->size()));
346  }
347  // Ignore 'Print Image Matching'
348  else if (i->tag() == EXIF_PRINT_IMAGE_MATCHING)
349  {
350  LOG(VB_FILE, LOG_DEBUG, LOC +
351  QString("Ignoring %1 (%2, %3) : Undecodable")
352  .arg(key, i->typeName()).arg(i->size()));
353  }
354  else
355  {
356  // Use interpreted values
357  std::string val = i->print(&m_exifData);
358 
359  // Comment needs charset decoding
360  QString value = (key == EXIF_TAG_USERCOMMENT)
361  ? DecodeComment(val) : QString::fromStdString(val);
362 
363  // Nulls can arise from corrupt metadata (MakerNote)
364  // Remove them as they disrupt socket comms between BE & remote FE's
365  if (value.contains(QChar::Null))
366  {
367  LOG(VB_GENERAL, LOG_NOTICE, LOC +
368  QString("Corrupted Exif detected in %1").arg(m_filePath));
369  value = "????";
370  }
371 
372  // Encode tag
373  QString str = ToString(key, label, value);
374  tags << str;
375 
376 #ifdef DUMP_METADATA_TAGS
377  LOG(VB_FILE, LOG_DEBUG, LOC + QString("%1 (%2, %3)")
378  .arg(str, i->typeName()).arg(i->size()));
379 #endif
380  }
381  }
382  return tags;
383 }
384 
385 
393 std::string PictureMetaData::GetTag(const QString &key, bool *exists)
394 {
395  std::string value;
396  if (exists)
397  *exists = false;
398 
399  if (!IsValid())
400  return value;
401 
402  Exiv2::ExifKey exifKey = Exiv2::ExifKey(key.toStdString());
403  auto exifIt = m_exifData.findKey(exifKey);
404 
405  if (exifIt == m_exifData.end())
406  return value;
407 
408  if (exists)
409  *exists = true;
410 
411  // Use raw value
412  return exifIt->value().toString();
413 }
414 
415 
422 {
423  std::string value = GetTag(EXIF_TAG_ORIENTATION, exists);
424  return QString::fromStdString(value).toInt();
425 }
426 
427 
434 {
435  std::string value = GetTag(EXIF_TAG_DATETIME, exists);
436  QString dt = QString::fromStdString(value);
437 
438  // Exif time has no timezone
440 }
441 
442 
449 QString PictureMetaData::GetComment(bool *exists)
450 {
451  // Use User Comment or else Image Description
452  bool comExists = false;
453  bool desExists = false;
454 
455  std::string comment = GetTag(EXIF_TAG_USERCOMMENT, &comExists);
456 
457  if (comment.empty())
458  comment = GetTag(EXIF_TAG_IMAGEDESCRIPTION, &desExists);
459 
460  if (exists)
461  *exists = comExists || desExists;
462 
463  return DecodeComment(comment);
464 }
465 
466 
472 QString PictureMetaData::DecodeComment(std::string rawValue)
473 {
474  // Decode charset
475  Exiv2::CommentValue comVal = Exiv2::CommentValue(rawValue);
476  if (comVal.charsetId() != Exiv2::CommentValue::undefined)
477  rawValue = comVal.comment();
478  return QString::fromStdString(rawValue);
479 }
480 
481 
488 {
489 public:
490  explicit VideoMetaData(const QString &filePath);
491  ~VideoMetaData() override;
492 
493  bool IsValid() override // ImageMetaData
494  { return m_dict; }
495  QStringList GetAllTags() override; // ImageMetaData
496  int GetOrientation(bool *exists = nullptr) override; // ImageMetaData
497  QDateTime GetOriginalDateTime(bool *exists = nullptr) override; // ImageMetaData
498  QString GetComment(bool *exists = nullptr) override; // ImageMetaData
499 
500 protected:
501  QString GetTag(const QString &key, bool *exists = nullptr);
502 
503  AVFormatContext *m_context;
505  AVDictionary *m_dict;
506 };
507 
508 
513 VideoMetaData::VideoMetaData(const QString &filePath)
514  : ImageMetaData(filePath), m_context(nullptr), m_dict(nullptr)
515 {
516  AVInputFormat* p_inputformat = nullptr;
517 
518  // Open file
519  if (avformat_open_input(&m_context, filePath.toLatin1().constData(),
520  p_inputformat, nullptr) < 0)
521  return;
522 
523  // Locate video stream
524  int vidStream = av_find_best_stream(m_context, AVMEDIA_TYPE_VIDEO, -1, -1, nullptr, 0);
525  if (vidStream >= 0)
526  m_dict = m_context->streams[vidStream]->metadata;
527 
528  if (!VideoMetaData::IsValid())
529  avformat_close_input(&m_context);
530 }
531 
532 
537 {
539  avformat_close_input(&m_context);
540 }
541 
542 
554 {
555  QStringList tags;
556  if (!IsValid())
557  return tags;
558 
559  // Only extract interesting fields:
560  // For list use: mythffprobe -show_format -show_streams <file>
561  QString cmd = GetAppBinDir() + MYTH_APPNAME_MYTHFFPROBE;
562  QStringList args;
563  args << "-loglevel quiet"
564  << "-print_format compact" // Returns "section|key=value|key=value..."
565  << "-pretty" // Add units etc
566  << "-show_entries "
567  "format=format_long_name,duration,bit_rate:format_tags:"
568  "stream=codec_long_name,codec_type,width,height,pix_fmt,color_space,avg_frame_rate"
569  ",codec_tag_string,sample_rate,channels,channel_layout,bit_rate:stream_tags"
570  << m_filePath;
571 
572  MythSystemLegacy ffprobe(cmd, args, kMSRunShell | kMSStdOut);
573 
574  ffprobe.Run(5);
575 
576  if (ffprobe.Wait() != GENERIC_EXIT_OK)
577  {
578  LOG(VB_GENERAL, LOG_ERR, LOC +
579  QString("Timeout or Failed: %2 %3").arg(cmd, args.join(" ")));
580  return tags;
581  }
582 
583  QByteArray result = ffprobe.ReadAll();
584  QTextStream ostream(result);
585  int stream = 0;
586  while (!ostream.atEnd())
587  {
588  QStringList fields = ostream.readLine().split('|');
589 
590  if (fields.size() <= 1)
591  // Empty section
592  continue;
593 
594  // First fields should be "format" or "stream"
595  QString prefix = "";
596  QString group = fields.takeFirst();
597  if (group == "stream")
598  {
599  // Streams use index as group
600  prefix = QString::number(stream++) + ":";
601  group.append(prefix);
602  }
603 
604  foreach (const QString &field, fields)
605  {
606  // Expect label=value
607  QStringList parts = field.split('=');
608  if (parts.size() != 2)
609  continue;
610 
611  // Remove ffprobe "tag:" prefix
612  QString label = parts[0].remove("tag:");
613  QString value = parts[1];
614 
615  // Construct a pseudo-key for FFMPEG tags
616  QString key = QString("FFmpeg.%1.%2").arg(group, label);
617 
618  // Add stream id to labels
619  QString str = ToString(key, prefix + label, value);
620  tags << str;
621 
622 #ifdef DUMP_METADATA_TAGS
623  LOG(VB_FILE, LOG_DEBUG, LOC + str);
624 #endif
625  }
626  }
627  return tags;
628 }
629 
630 
637 QString VideoMetaData::GetTag(const QString &key, bool *exists)
638 {
639  if (m_dict)
640  {
641  AVDictionaryEntry *tag = nullptr;
642  while ((tag = av_dict_get(m_dict, "\0", tag, AV_DICT_IGNORE_SUFFIX)))
643  {
644  if (QString(tag->key) == key)
645  {
646  if (exists)
647  *exists = true;
648  return QString::fromUtf8(tag->value);
649  }
650  }
651  }
652  if (exists)
653  *exists = false;
654  return QString();
655 }
656 
657 
664 {
665  QString angle = GetTag(FFMPEG_TAG_ORIENTATION, exists);
666  return Orientation::FromRotation(angle);
667 }
668 
669 
675 QDateTime VideoMetaData::GetOriginalDateTime(bool *exists)
676 {
677  QString dt = GetTag(FFMPEG_TAG_DATETIME, exists);
678 
679  // Video time has no timezone
681 }
682 
683 
690 QString VideoMetaData::GetComment(bool *exists)
691 {
692  if (exists)
693  *exists = false;
694  return QString();
695 }
696 
697 
703 ImageMetaData* ImageMetaData::FromPicture(const QString &filePath)
704 { return new PictureMetaData(filePath); }
705 
706 
712 ImageMetaData* ImageMetaData::FromVideo(const QString &filePath)
713 { return new VideoMetaData(filePath); }
714 
715 
716 const QString ImageMetaData::kSeparator = "|-|";
717 
718 
724 ImageMetaData::TagMap ImageMetaData::ToMap(const QStringList &tagStrings)
725 {
726  TagMap tags;
727  foreach (const QString &token, tagStrings)
728  {
729  QStringList parts = FromString(token);
730  // Expect Key, Label, Value.
731  if (parts.size() == 3)
732  {
733  // Map tags by Family.Group to keep them together
734  // Within each group they will preserve list ordering
735  QString group = parts[0].section('.', 0, 1);
736  tags.insertMulti(group, parts);
737 
738 #ifdef DUMP_METADATA_TAGS
739  LOG(VB_FILE, LOG_DEBUG, LOC + QString("%1 = %2").arg(group, token));
740 #endif
741  }
742  }
743  return tags;
744 }
void Run(time_t timeout=0)
Runs a command inside the /bin/sh shell. Returns immediately.
#define EXIF_TAG_DATETIME
Definition: imagemetadata.h:26
int m_file
The orientation of the raw file image, as specified by the camera.
Definition: imagemetadata.h:94
int Composite()
Encode original & current orientation to a single Db field.
Definition: imagemetadata.h:71
Abstract class for image metadata.
Definition: imagemetadata.h:99
static int FromRotation(const QString &degrees)
Convert degrees of rotation into Exif orientation code.
#define MYTH_APPNAME_MYTHFFPROBE
#define EXIF_PRINT_IMAGE_MATCHING
Definition: imagemetadata.h:30
QString Description()
Generate text description of orientation.
#define GENERIC_EXIT_OK
Exited with no error.
Definition: exitcodes.h:10
QDateTime GetOriginalDateTime(bool *exists=nullptr) override
Read Exif timestamp of image capture.
static QStringList FromString(const QString &str)
Decodes metadata name, label, value from a string.
QString GetTag(const QString &key, bool *exists=nullptr)
Read a single video tag.
Reset to Exif value.
Definition: imagemetadata.h:47
bool IsValid() override
QDateTime fromString(const QString &dtstr)
Converts kFilename && kISODate formats to QDateTime.
Definition: mythdate.cpp:30
int Transform(int transform)
Adjust orientation to apply a transform to an image.
static const bool krunningQt541
True when using Qt 5.4.1 with its deviant orientation behaviour.
Definition: imagemetadata.h:86
int GetOrientation(bool *exists=nullptr) override
Read Exif orientation.
int GetOrientation(bool *exists=nullptr) override
Read FFmpeg video orientation tag.
int matrix[4][2]
static float * vals
Definition: tentacle3d.c:16
Handles Exif/FFMpeg metadata tags for images.
static ImageMetaData * FromPicture(const QString &filePath)
Factory to retrieve metadata from pictures.
QStringList GetAllTags() override
Reads relevant video metadata by running mythffprobe.
#define EXIF_TAG_ORIENTATION
Definition: imagemetadata.h:25
AVFormatContext * m_context
#define EXIF_TAG_USERCOMMENT
Definition: imagemetadata.h:29
static const QString kSeparator
Unique separator to delimit fields within a string.
#define LOC
Reads Exif metadata from a picture using libexiv2.
QString GetComment(bool *exists=nullptr) override
Read Video comment from metadata.
int GetCurrent(bool compensate)
Determines orientation required for an image.
QString GetAppBinDir(void)
Definition: mythdirs.cpp:221
static QString ToString(const QString &name, const QString &label, const QString &value)
Encodes metadata into a string as <tag name><tag label><tag value>
QByteArray & ReadAll()
Reflect about horizontal axis.
Definition: imagemetadata.h:51
std::string GetTag(const QString &key, bool *exists=nullptr)
Read a single Exif metadata tag.
#define EXIF_TAG_IMAGEDESCRIPTION
Definition: imagemetadata.h:28
Exiv2::Image::AutoPtr m_image
bool IsValid() override
QStringList GetAllTags() override
Returns all metadata tags.
#define EXIF_TAG_DATE_FORMAT
Definition: imagemetadata.h:27
uint Wait(time_t timeout=0)
run process through shell
Definition: mythsystem.h:41
static TagMap ToMap(const QStringList &tags)
Creates a map of metadata tags as.
#define FFMPEG_TAG_ORIENTATION
Definition: imagemetadata.h:33
#define LOG(_MASK_, _LEVEL_, _STRING_)
Definition: mythlogging.h:41
static QString DecodeComment(std::string rawValue)
Decodes charset of UserComment.
QString GetComment(bool *exists=nullptr) override
Read Exif comments from metadata.
~PictureMetaData() override=default
int Apply(int transform)
Adjust current orientation code to apply a transform to an image.
Reads video metadata tags using FFmpeg Raw values for Orientation & Date are read quickly via FFmpeg ...
Rotate anti-clockwise.
Definition: imagemetadata.h:49
int m_current
The orientation to use: the file orientation with user transformations applied.
Definition: imagemetadata.h:92
Reflect about vertical axis.
Definition: imagemetadata.h:50
~VideoMetaData() override
Destructor. Closes file.
static QString AsText(int orientation)
Converts orientation code to text description for info display.
PictureMetaData(const QString &filePath)
Constructor. Reads metadata from image.
Exiv2::ExifData m_exifData
#define FFMPEG_TAG_DATE_FORMAT
Definition: imagemetadata.h:35
QHash< int, QHash< int, int > > Matrix
Definition: imagemetadata.h:83
AVDictionary * m_dict
FFmpeg tag dictionary.
QString m_filePath
Image filepath.
QMap< QString, QStringList > TagMap
static Matrix InitOrientationMatrix()
Initialises conversion matrix for Qt 5.4.1.
Rotate clockwise.
Definition: imagemetadata.h:48
#define FFMPEG_TAG_DATETIME
Definition: imagemetadata.h:34
VideoMetaData(const QString &filePath)
Constructor. Opens best video stream from video.
static ImageMetaData * FromVideo(const QString &filePath)
Factory to retrieve metadata from videos.
static const Matrix kQt541_orientation
Orientation conversions for proper display on Qt 5.4.1.
Definition: imagemetadata.h:88
QDateTime GetOriginalDateTime(bool *exists=nullptr) override
Read video datestamp.
allow access to stdout
Definition: mythsystem.h:39