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  Exiv2::ExifData::iterator 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, desExists = false;
453 
454  std::string comment = GetTag(EXIF_TAG_USERCOMMENT, &comExists);
455 
456  if (comment.empty())
457  comment = GetTag(EXIF_TAG_IMAGEDESCRIPTION, &desExists);
458 
459  if (exists)
460  *exists = comExists || desExists;
461 
462  return DecodeComment(comment);
463 }
464 
465 
471 QString PictureMetaData::DecodeComment(std::string rawValue)
472 {
473  // Decode charset
474  Exiv2::CommentValue comVal = Exiv2::CommentValue(rawValue);
475  if (comVal.charsetId() != Exiv2::CommentValue::undefined)
476  rawValue = comVal.comment();
477  return QString::fromStdString(rawValue);
478 }
479 
480 
487 {
488 public:
489  explicit VideoMetaData(const QString &filePath);
490  ~VideoMetaData() override;
491 
492  bool IsValid() override // ImageMetaData
493  { return m_dict; }
494  QStringList GetAllTags() override; // ImageMetaData
495  int GetOrientation(bool *exists = nullptr) override; // ImageMetaData
496  QDateTime GetOriginalDateTime(bool *exists = nullptr) override; // ImageMetaData
497  QString GetComment(bool *exists = nullptr) override; // ImageMetaData
498 
499 protected:
500  QString GetTag(const QString &key, bool *exists = nullptr);
501 
502  AVFormatContext *m_context;
504  AVDictionary *m_dict;
505 };
506 
507 
512 VideoMetaData::VideoMetaData(const QString &filePath)
513  : ImageMetaData(filePath), m_context(nullptr), m_dict(nullptr)
514 {
515  AVInputFormat* p_inputformat = nullptr;
516 
517  // Open file
518  if (avformat_open_input(&m_context, filePath.toLatin1().constData(),
519  p_inputformat, nullptr) < 0)
520  return;
521 
522  // Locate video stream
523  int vidStream = av_find_best_stream(m_context, AVMEDIA_TYPE_VIDEO, -1, -1, nullptr, 0);
524  if (vidStream >= 0)
525  m_dict = m_context->streams[vidStream]->metadata;
526 
527  if (!VideoMetaData::IsValid())
528  avformat_close_input(&m_context);
529 }
530 
531 
536 {
538  avformat_close_input(&m_context);
539 }
540 
541 
553 {
554  QStringList tags;
555  if (!IsValid())
556  return tags;
557 
558  // Only extract interesting fields:
559  // For list use: mythffprobe -show_format -show_streams <file>
560  QString cmd = GetAppBinDir() + MYTH_APPNAME_MYTHFFPROBE;
561  QStringList args;
562  args << "-loglevel quiet"
563  << "-print_format compact" // Returns "section|key=value|key=value..."
564  << "-pretty" // Add units etc
565  << "-show_entries "
566  "format=format_long_name,duration,bit_rate:format_tags:"
567  "stream=codec_long_name,codec_type,width,height,pix_fmt,color_space,avg_frame_rate"
568  ",codec_tag_string,sample_rate,channels,channel_layout,bit_rate:stream_tags"
569  << m_filePath;
570 
571  MythSystemLegacy ffprobe(cmd, args, kMSRunShell | kMSStdOut);
572 
573  ffprobe.Run(5);
574 
575  if (ffprobe.Wait() != GENERIC_EXIT_OK)
576  {
577  LOG(VB_GENERAL, LOG_ERR, LOC +
578  QString("Timeout or Failed: %2 %3").arg(cmd, args.join(" ")));
579  return tags;
580  }
581 
582  QByteArray result = ffprobe.ReadAll();
583  QTextStream ostream(result);
584  int stream = 0;
585  while (!ostream.atEnd())
586  {
587  QStringList fields = ostream.readLine().split('|');
588 
589  if (fields.size() <= 1)
590  // Empty section
591  continue;
592 
593  // First fields should be "format" or "stream"
594  QString prefix = "";
595  QString group = fields.takeFirst();
596  if (group == "stream")
597  {
598  // Streams use index as group
599  prefix = QString::number(stream++) + ":";
600  group.append(prefix);
601  }
602 
603  foreach (const QString &field, fields)
604  {
605  // Expect label=value
606  QStringList parts = field.split('=');
607  if (parts.size() != 2)
608  continue;
609 
610  // Remove ffprobe "tag:" prefix
611  QString label = parts[0].remove("tag:");
612  QString value = parts[1];
613 
614  // Construct a pseudo-key for FFMPEG tags
615  QString key = QString("FFmpeg.%1.%2").arg(group, label);
616 
617  // Add stream id to labels
618  QString str = ToString(key, prefix + label, value);
619  tags << str;
620 
621 #ifdef DUMP_METADATA_TAGS
622  LOG(VB_FILE, LOG_DEBUG, LOC + str);
623 #endif
624  }
625  }
626  return tags;
627 }
628 
629 
636 QString VideoMetaData::GetTag(const QString &key, bool *exists)
637 {
638  if (m_dict)
639  {
640  AVDictionaryEntry *tag = nullptr;
641  while ((tag = av_dict_get(m_dict, "\0", tag, AV_DICT_IGNORE_SUFFIX)))
642  {
643  if (QString(tag->key) == key)
644  {
645  if (exists)
646  *exists = true;
647  return QString::fromUtf8(tag->value);
648  }
649  }
650  }
651  if (exists)
652  *exists = false;
653  return QString();
654 }
655 
656 
663 {
664  QString angle = GetTag(FFMPEG_TAG_ORIENTATION, exists);
665  return Orientation::FromRotation(angle);
666 }
667 
668 
674 QDateTime VideoMetaData::GetOriginalDateTime(bool *exists)
675 {
676  QString dt = GetTag(FFMPEG_TAG_DATETIME, exists);
677 
678  // Video time has no timezone
680 }
681 
682 
689 QString VideoMetaData::GetComment(bool *exists)
690 {
691  if (exists)
692  *exists = false;
693  return QString();
694 }
695 
696 
702 ImageMetaData* ImageMetaData::FromPicture(const QString &filePath)
703 { return new PictureMetaData(filePath); }
704 
705 
711 ImageMetaData* ImageMetaData::FromVideo(const QString &filePath)
712 { return new VideoMetaData(filePath); }
713 
714 
715 const QString ImageMetaData::kSeparator = "|-|";
716 
717 
723 ImageMetaData::TagMap ImageMetaData::ToMap(const QStringList &tagStrings)
724 {
725  TagMap tags;
726  foreach (const QString &token, tagStrings)
727  {
728  QStringList parts = FromString(token);
729  // Expect Key, Label, Value.
730  if (parts.size() == 3)
731  {
732  // Map tags by Family.Group to keep them together
733  // Within each group they will preserve list ordering
734  QString group = parts[0].section('.', 0, 1);
735  tags.insertMulti(group, parts);
736 
737 #ifdef DUMP_METADATA_TAGS
738  LOG(VB_FILE, LOG_DEBUG, LOC + QString("%1 = %2").arg(group, token));
739 #endif
740  }
741  }
742  return tags;
743 }
void Run(time_t timeout=0)
Runs a command inside the /bin/sh shell. Returns immediately.
#define EXIF_TAG_DATETIME
Definition: imagemetadata.h:22
int m_file
The orientation of the raw file image, as specified by the camera.
Definition: imagemetadata.h:90
int Composite()
Encode original & current orientation to a single Db field.
Definition: imagemetadata.h:67
Abstract class for image metadata.
Definition: imagemetadata.h:95
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:26
allow access to stdout
Definition: mythsystem.h:39
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:43
bool IsValid() override
static const bool krunningQt541
True when using Qt 5.4.1 with its deviant orientation behaviour.
Definition: imagemetadata.h:82
int Apply(int)
Adjust current orientation code to apply a transform to an image.
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:21
AVFormatContext * m_context
#define EXIF_TAG_USERCOMMENT
Definition: imagemetadata.h:25
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
run process through shell
Definition: mythsystem.h:41
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()
int Transform(int)
Adjust orientation to apply a transform to an image.
Reflect about horizontal axis.
Definition: imagemetadata.h:47
std::string GetTag(const QString &key, bool *exists=nullptr)
Read a single Exif metadata tag.
#define EXIF_TAG_IMAGEDESCRIPTION
Definition: imagemetadata.h:24
Exiv2::Image::AutoPtr m_image
bool IsValid() override
QStringList GetAllTags() override
Returns all metadata tags.
#define EXIF_TAG_DATE_FORMAT
Definition: imagemetadata.h:23
uint Wait(time_t timeout=0)
static TagMap ToMap(const QStringList &tags)
Creates a map of metadata tags as.
#define FFMPEG_TAG_ORIENTATION
Definition: imagemetadata.h:29
#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
Reads video metadata tags using FFmpeg Raw values for Orientation & Date are read quickly via FFmpeg ...
QHash< int, QHash< int, int > > Matrix
Definition: imagemetadata.h:79
Rotate anti-clockwise.
Definition: imagemetadata.h:45
int m_current
The orientation to use: the file orientation with user transformations applied.
Definition: imagemetadata.h:88
Reflect about vertical axis.
Definition: imagemetadata.h:46
~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:31
AVDictionary * m_dict
FFmpeg tag dictionary.
QString m_filePath
Image filepath.
QDateTime fromString(const QString &dtstr)
Converts kFilename && kISODate formats to QDateTime.
Definition: mythdate.cpp:30
static Matrix InitOrientationMatrix()
Initialises conversion matrix for Qt 5.4.1.
Rotate clockwise.
Definition: imagemetadata.h:44
#define FFMPEG_TAG_DATETIME
Definition: imagemetadata.h:30
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:84
QDateTime GetOriginalDateTime(bool *exists=nullptr) override
Read video datestamp.
QMap< QString, QStringList > TagMap