KFileMetaData

embeddedimagedata.cpp
1 /*
2  * Copyright (C) 2018 Alexander Stippich <[email protected]>
3  *
4  * This library is free software; you can redistribute it and/or
5  * modify it under the terms of the GNU Lesser General Public
6  * License as published by the Free Software Foundation; either
7  * version 2.1 of the License, or (at your option) any later version.
8  *
9  * This library is distributed in the hope that it will be useful,
10  * but WITHOUT ANY WARRANTY; without even the implied warranty of
11  * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
12  * Lesser General Public License for more details.
13  *
14  * You should have received a copy of the GNU Lesser General Public
15  * License along with this library; if not, write to the Free Software
16  * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
17  *
18  */
19 
20 #include "embeddedimagedata.h"
21 #include "kfilemetadata_debug.h"
22 // Taglib includes
23 #include <taglib.h>
24 #include <tag.h>
25 #include <tfilestream.h>
26 #include <mpegfile.h>
27 #include <oggfile.h>
28 #include <mp4file.h>
29 #include <flacfile.h>
30 #include <vorbisfile.h>
31 #include <opusfile.h>
32 #include <mpcfile.h>
33 #include <id3v2tag.h>
34 #include <attachedpictureframe.h>
35 #include <mp4tag.h>
36 #include <xiphcomment.h>
37 #include <apefile.h>
38 #include <apetag.h>
39 #include <wavpackfile.h>
40 #include <speexfile.h>
41 #include <wavfile.h>
42 #include <aifffile.h>
43 #include <asffile.h>
44 #include <asfattribute.h>
45 #include <asfpicture.h>
46 
47 #include <QByteArray>
48 #include <QMimeDatabase>
49 
50 using namespace KFileMetaData;
51 
52 class Q_DECL_HIDDEN EmbeddedImageData::Private
53 {
54 public:
55  QMimeDatabase mMimeDatabase;
56  QByteArray getFrontCover(const QString &fileUrl, const QString &mimeType) const;
57  QByteArray getFrontCoverFromID3(TagLib::ID3v2::Tag* Id3Tags) const;
58  QByteArray getFrontCoverFromFlacPicture(TagLib::List<TagLib::FLAC::Picture *> lstPic) const;
59  QByteArray getFrontCoverFromMp4(TagLib::MP4::Tag* mp4Tags) const;
60  QByteArray getFrontCoverFromApe(TagLib::APE::Tag* apeTags) const;
61  QByteArray getFrontCoverFromAsf(TagLib::ASF::Tag* asfTags) const;
62  void writeFrontCover(const QString &fileUrl, const QString &mimeType, const QByteArray &pictureData);
63  void writeFrontCoverToID3(TagLib::ID3v2::Tag* Id3Tags, const QByteArray &pictureData);
64  void writeFrontCoverToFlacPicture(TagLib::List<TagLib::FLAC::Picture *> lstPic, const QByteArray &pictureData);
65  void writeFrontCoverToMp4(TagLib::MP4::Tag* mp4Tags, const QByteArray &pictureData);
66  void writeFrontCoverToApe(TagLib::APE::Tag* apeTags, const QByteArray &pictureData);
67  void writeFrontCoverToAsf(TagLib::ASF::Tag* asfTags, const QByteArray &pictureData);
68  TagLib::String determineMimeType(const QByteArray &pictureData);
69  static const QStringList mMimetypes;
70 };
71 
72 const QStringList EmbeddedImageData::Private::mMimetypes =
73 {
74  QStringLiteral("audio/flac"),
75  QStringLiteral("audio/mp4"),
76  QStringLiteral("audio/mpeg"),
77  QStringLiteral("audio/mpeg3"),
78  QStringLiteral("audio/ogg"),
79  QStringLiteral("audio/opus"),
80  QStringLiteral("audio/speex"),
81  QStringLiteral("audio/wav"),
82  QStringLiteral("audio/x-aiff"),
83  QStringLiteral("audio/x-ape"),
84  QStringLiteral("audio/x-mpeg"),
85  QStringLiteral("audio/x-ms-wma"),
86  QStringLiteral("audio/x-musepack"),
87  QStringLiteral("audio/x-opus+ogg"),
88  QStringLiteral("audio/x-speex"),
89  QStringLiteral("audio/x-vorbis+ogg"),
90  QStringLiteral("audio/x-wav"),
91  QStringLiteral("audio/x-wavpack"),
92 };
93 
94 EmbeddedImageData::EmbeddedImageData()
95  : d(std::unique_ptr<Private>(new Private()))
96 {
97 }
98 
99 EmbeddedImageData::~EmbeddedImageData()
100 = default;
101 
102 QStringList EmbeddedImageData::mimeTypes() const
103 {
104  return d->mMimetypes;
105 }
106 
109  const EmbeddedImageData::ImageTypes types) const
110 {
112 
113  const auto fileMimeType = d->mMimeDatabase.mimeTypeForFile(fileUrl);
114  if (fileMimeType.name().startsWith(QLatin1String("audio/"))) {
115  if (types & EmbeddedImageData::FrontCover) {
116  imageData.insert(EmbeddedImageData::FrontCover, d->getFrontCover(fileUrl, fileMimeType.name()));
117  }
118  }
119 
120  return imageData;
121 }
122 
123 void
126 {
127  const auto fileMimeType = d->mMimeDatabase.mimeTypeForFile(fileUrl);
128  QMap<EmbeddedImageData::ImageType, QByteArray>::iterator frontCover = imageData.find(EmbeddedImageData::FrontCover);
129  if (fileMimeType.name().startsWith(QLatin1String("audio/"))) {
130  if (frontCover != imageData.end()) {
131  d->writeFrontCover(fileUrl, fileMimeType.name(), frontCover.value());
132  }
133  }
134 }
135 
137 EmbeddedImageData::Private::getFrontCover(const QString &fileUrl,
138  const QString &mimeType) const
139 {
140  TagLib::FileStream stream(fileUrl.toUtf8().constData(), true);
141  if (!stream.isOpen()) {
142  qCWarning(KFILEMETADATA_LOG) << "Unable to open file readonly: " << fileUrl;
143  return QByteArray();
144  }
145  if ((mimeType == QLatin1String("audio/mpeg"))
146  || (mimeType == QLatin1String("audio/mpeg3"))
147  || (mimeType == QLatin1String("audio/x-mpeg"))) {
148 
149  // Handling multiple tags in mpeg files.
150  TagLib::MPEG::File mpegFile(&stream, TagLib::ID3v2::FrameFactory::instance(), true);
151  if (mpegFile.ID3v2Tag()) {
152  return getFrontCoverFromID3(mpegFile.ID3v2Tag());
153  }
154 
155  } else if (mimeType == QLatin1String("audio/x-aiff")) {
156 
157  TagLib::RIFF::AIFF::File aiffFile(&stream, true);
158  if (aiffFile.hasID3v2Tag()) {
159  return getFrontCoverFromID3(aiffFile.tag());
160  }
161 
162  } else if ((mimeType == QLatin1String("audio/wav"))
163  || (mimeType == QLatin1String("audio/x-wav"))) {
164 
165  TagLib::RIFF::WAV::File wavFile(&stream, true);
166  if (wavFile.hasID3v2Tag()) {
167  return getFrontCoverFromID3(wavFile.ID3v2Tag());
168  }
169 
170  } else if (mimeType == QLatin1String("audio/mp4")) {
171 
172  TagLib::MP4::File mp4File(&stream, true);
173  if (mp4File.tag()) {
174  return getFrontCoverFromMp4(mp4File.tag());
175  }
176 
177  } else if (mimeType == QLatin1String("audio/x-musepack")) {
178 
179  TagLib::MPC::File mpcFile(&stream, true);
180  if (mpcFile.APETag()) {
181  return getFrontCoverFromApe(mpcFile.APETag());
182  }
183 
184  } else if (mimeType == QLatin1String("audio/x-ape")) {
185 
186  TagLib::APE::File apeFile(&stream, true);
187  if (apeFile.hasAPETag()) {
188  return getFrontCoverFromApe(apeFile.APETag());
189  }
190 
191  } else if (mimeType == QLatin1String("audio/x-wavpack")) {
192 
193  TagLib::WavPack::File wavpackFile(&stream, true);
194  if (wavpackFile.hasAPETag()) {
195  return getFrontCoverFromApe(wavpackFile.APETag());
196  }
197 
198  } else if (mimeType == QLatin1String("audio/x-ms-wma")) {
199 
200  TagLib::ASF::File asfFile(&stream, true);
201  TagLib::ASF::Tag* asfTags = dynamic_cast<TagLib::ASF::Tag*>(asfFile.tag());
202  if (asfTags) {
203  return getFrontCoverFromAsf(asfTags);
204  }
205 
206  } else if (mimeType == QLatin1String("audio/flac")) {
207 
208  TagLib::FLAC::File flacFile(&stream, TagLib::ID3v2::FrameFactory::instance(), true);
209  return getFrontCoverFromFlacPicture(flacFile.pictureList());
210 
211  } else if ((mimeType == QLatin1String("audio/ogg"))
212  || (mimeType == QLatin1String("audio/x-vorbis+ogg"))) {
213 
214  TagLib::Ogg::Vorbis::File oggFile(&stream, true);
215  if (oggFile.tag()) {
216  return getFrontCoverFromFlacPicture(oggFile.tag()->pictureList());
217  }
218 
219  }
220  else if ((mimeType == QLatin1String("audio/opus"))
221  || (mimeType == QLatin1String("audio/x-opus+ogg"))) {
222 
223  TagLib::Ogg::Opus::File opusFile(&stream, true);
224  if (opusFile.tag()) {
225  return getFrontCoverFromFlacPicture(opusFile.tag()->pictureList());
226  }
227 
228  } else if (mimeType == QLatin1String("audio/speex") || mimeType == QLatin1String("audio/x-speex")) {
229 
230  TagLib::Ogg::Speex::File speexFile(&stream, true);
231  if (speexFile.tag()) {
232  return getFrontCoverFromFlacPicture(speexFile.tag()->pictureList());
233  }
234 
235  }
236  return QByteArray();
237 }
238 
239 void
240 EmbeddedImageData::Private::writeFrontCover(const QString &fileUrl,
241  const QString &mimeType, const QByteArray &pictureData)
242 {
243  TagLib::FileStream stream(fileUrl.toUtf8().constData(), false);
244  if (!stream.isOpen()) {
245  qWarning() << "Unable to open file: " << fileUrl;
246  return;
247  }
248 
249  if ((mimeType == QLatin1String("audio/mpeg"))
250  || (mimeType == QLatin1String("audio/mpeg3"))
251  || (mimeType == QLatin1String("audio/x-mpeg"))) {
252 
253  // Handling multiple tags in mpeg files.
254  TagLib::MPEG::File mpegFile(&stream, TagLib::ID3v2::FrameFactory::instance(), false);
255  if (mpegFile.ID3v2Tag()) {
256  writeFrontCoverToID3(mpegFile.ID3v2Tag(), pictureData);
257  }
258  mpegFile.save();
259  } else if (mimeType == QLatin1String("audio/x-aiff")) {
260 
261  TagLib::RIFF::AIFF::File aiffFile(&stream, false);
262  if (aiffFile.hasID3v2Tag()) {
263  writeFrontCoverToID3(aiffFile.tag(), pictureData);
264  }
265  aiffFile.save();
266  } else if ((mimeType == QLatin1String("audio/wav"))
267  || (mimeType == QLatin1String("audio/x-wav"))) {
268 
269  TagLib::RIFF::WAV::File wavFile(&stream, false);
270  if (wavFile.hasID3v2Tag()) {
271  writeFrontCoverToID3(wavFile.ID3v2Tag(), pictureData);
272  }
273  wavFile.save();
274  } else if (mimeType == QLatin1String("audio/mp4")) {
275 
276  TagLib::MP4::File mp4File(&stream, false);
277  if (mp4File.tag()) {
278  writeFrontCoverToMp4(mp4File.tag(), pictureData);
279  }
280  mp4File.save();
281  } else if (mimeType == QLatin1String("audio/x-musepack")) {
282 
283  TagLib::MPC::File mpcFile(&stream, false);
284  if (mpcFile.APETag()) {
285  writeFrontCoverToApe(mpcFile.APETag(), pictureData);
286  }
287  mpcFile.save();
288  } else if (mimeType == QLatin1String("audio/x-ape")) {
289 
290  TagLib::APE::File apeFile(&stream, false);
291  if (apeFile.hasAPETag()) {
292  writeFrontCoverToApe(apeFile.APETag(), pictureData);
293  }
294  apeFile.save();
295  } else if (mimeType == QLatin1String("audio/x-wavpack")) {
296 
297  TagLib::WavPack::File wavpackFile(&stream, false);
298  if (wavpackFile.hasAPETag()) {
299  writeFrontCoverToApe(wavpackFile.APETag(), pictureData);
300  }
301  wavpackFile.save();
302  } else if (mimeType == QLatin1String("audio/x-ms-wma")) {
303 
304  TagLib::ASF::File asfFile(&stream, false);
305  TagLib::ASF::Tag* asfTags = dynamic_cast<TagLib::ASF::Tag*>(asfFile.tag());
306  if (asfTags) {
307  writeFrontCoverToAsf(asfTags, pictureData);
308  }
309  asfFile.save();
310  } else if (mimeType == QLatin1String("audio/flac")) {
311 
312  TagLib::FLAC::File flacFile(&stream, TagLib::ID3v2::FrameFactory::instance(), false);
313  writeFrontCoverToFlacPicture(flacFile.pictureList(), pictureData);
314  flacFile.save();
315  } else if ((mimeType == QLatin1String("audio/ogg"))
316  || (mimeType == QLatin1String("audio/x-vorbis+ogg"))) {
317 
318  TagLib::Ogg::Vorbis::File oggFile(&stream, false);
319  if (oggFile.tag()) {
320  writeFrontCoverToFlacPicture(oggFile.tag()->pictureList(), pictureData);
321  }
322  oggFile.save();
323  }
324  else if ((mimeType == QLatin1String("audio/opus"))
325  || (mimeType == QLatin1String("audio/x-opus+ogg"))) {
326 
327  TagLib::Ogg::Opus::File opusFile(&stream, false);
328  if (opusFile.tag()) {
329  writeFrontCoverToFlacPicture(opusFile.tag()->pictureList(), pictureData);
330  }
331  opusFile.save();
332  } else if (mimeType == QLatin1String("audio/speex") || mimeType == QLatin1String("audio/x-speex")) {
333 
334  TagLib::Ogg::Speex::File speexFile(&stream, false);
335  if (speexFile.tag()) {
336  writeFrontCoverToFlacPicture(speexFile.tag()->pictureList(), pictureData);
337  }
338  speexFile.save();
339  }
340 }
341 
343 EmbeddedImageData::Private::getFrontCoverFromID3(TagLib::ID3v2::Tag* Id3Tags) const
344 {
345  TagLib::ID3v2::FrameList lstID3v2;
346  // Attached Front Picture.
347  lstID3v2 = Id3Tags->frameListMap()["APIC"];
348  for (const auto& frame : qAsConst(lstID3v2))
349  {
350  const auto *frontCoverFrame = static_cast<TagLib::ID3v2::AttachedPictureFrame *>(frame);
351  if (frontCoverFrame->type() == frontCoverFrame->FrontCover) {
352  return QByteArray(frontCoverFrame->picture().data(), frontCoverFrame->picture().size());
353  }
354  }
355  return QByteArray();
356 }
357 
358 void
359 EmbeddedImageData::Private::writeFrontCoverToID3(TagLib::ID3v2::Tag* Id3Tags, const QByteArray &pictureData)
360 {
361  // Try to update existing front cover frame first
362  TagLib::ID3v2::FrameList lstID3v2;
363  lstID3v2 = Id3Tags->frameListMap()["APIC"];
364  for (auto& frame : qAsConst(lstID3v2))
365  {
366  auto *frontCoverFrame = static_cast<TagLib::ID3v2::AttachedPictureFrame *>(frame);
367  if (frontCoverFrame->type() == frontCoverFrame->FrontCover) {
368  frontCoverFrame->setPicture(TagLib::ByteVector(static_cast<const char *>(pictureData.data()), pictureData.size()));
369  frontCoverFrame->setMimeType(determineMimeType(pictureData));
370  return;
371  }
372  }
373  auto pictureFrame = new TagLib::ID3v2::AttachedPictureFrame;
374  pictureFrame->setPicture(TagLib::ByteVector(pictureData.data(), pictureData.size()));
375  pictureFrame->setType(pictureFrame->FrontCover);
376  pictureFrame->setMimeType(determineMimeType(pictureData));
377  Id3Tags->addFrame(pictureFrame);
378 }
379 
381 EmbeddedImageData::Private::getFrontCoverFromFlacPicture(TagLib::List<TagLib::FLAC::Picture *> lstPic) const
382 {
383  for (const auto &picture : qAsConst(lstPic)) {
384  if (picture->type() == picture->FrontCover) {
385  return QByteArray(picture->data().data(), picture->data().size());
386  }
387  }
388  return QByteArray();
389 }
390 
391 void
392 EmbeddedImageData::Private::writeFrontCoverToFlacPicture(TagLib::List<TagLib::FLAC::Picture *> lstPic, const QByteArray &pictureData)
393 {
394  for (const auto &picture : qAsConst(lstPic)) {
395  if (picture->type() == picture->FrontCover) {
396  picture->setData(TagLib::ByteVector(pictureData.data(), pictureData.size()));
397  picture->setMimeType(determineMimeType(pictureData));
398  return ;
399  }
400  }
401  auto flacPicture = new TagLib::FLAC::Picture;
402  flacPicture->setMimeType(determineMimeType(pictureData));
403  flacPicture->setData(TagLib::ByteVector(pictureData.data(), pictureData.size()));
404  lstPic.append(flacPicture);
405 }
406 
408 EmbeddedImageData::Private::getFrontCoverFromMp4(TagLib::MP4::Tag* mp4Tags) const
409 {
410  TagLib::MP4::Item coverArtItem = mp4Tags->item("covr");
411  if (coverArtItem.isValid())
412  {
413  TagLib::MP4::CoverArtList coverArtList = coverArtItem.toCoverArtList();
414  TagLib::MP4::CoverArt& frontCover = coverArtList.front();
415  return QByteArray(frontCover.data().data(), frontCover.data().size());
416  }
417  return QByteArray();
418 }
419 
420 void
421 EmbeddedImageData::Private::writeFrontCoverToMp4(TagLib::MP4::Tag* mp4Tags, const QByteArray &pictureData)
422 {
423  TagLib::MP4::Item coverArtItem = mp4Tags->item("covr");
424  TagLib::MP4::CoverArtList coverArtList;
425  TagLib::MP4::CoverArt coverArt(TagLib::MP4::CoverArt::Format::Unknown, TagLib::ByteVector(pictureData.data(), pictureData.size()));
426  if (coverArtItem.isValid())
427  {
428  coverArtList = coverArtItem.toCoverArtList();
429  coverArtList.clear();
430  }
431  coverArtList.append(coverArt);
432  mp4Tags->setItem("covr", coverArtList);
433 }
434 
436 EmbeddedImageData::Private::getFrontCoverFromApe(TagLib::APE::Tag* apeTags) const
437 {
438  TagLib::APE::ItemListMap lstApe = apeTags->itemListMap();
439  TagLib::APE::ItemListMap::ConstIterator itApe;
440 
441  /* The cover art tag for APEv2 tags starts with the filename as string
442  * with zero termination followed by the actual picture data */
443  itApe = lstApe.find("COVER ART (FRONT)");
444  if (itApe != lstApe.end()) {
445  TagLib::ByteVector pictureData = (*itApe).second.binaryData();
446  int position = pictureData.find('\0');
447  if (position >= 0) {
448  position += 1;
449  return QByteArray(pictureData.data() + position, pictureData.size() - position);
450  }
451  }
452  return QByteArray();
453 }
454 
455 void
456 EmbeddedImageData::Private::writeFrontCoverToApe(TagLib::APE::Tag* apeTags, const QByteArray &pictureData)
457 {
458  /* The cover art tag for APEv2 tags starts with the filename as string
459  * with zero termination followed by the actual picture data */
460  TagLib::ByteVector imageData;
461  TagLib::String name;
462  if (determineMimeType(pictureData) == TagLib::String("image/png")) {
463  name = "frontCover.png";
464  } else {
465  name = "frontCover.jpeg";
466  }
467  imageData.append(name.data(TagLib::String::UTF8));
468  imageData.append('\0');
469  imageData.append(TagLib::ByteVector(pictureData.constData(), pictureData.size()));
470  apeTags->setData("COVER ART (FRONT)", imageData);
471 }
472 
474 EmbeddedImageData::Private::getFrontCoverFromAsf(TagLib::ASF::Tag* asfTags) const
475 {
476  TagLib::ASF::AttributeList lstASF = asfTags->attribute("WM/Picture");
477  for (const auto& attribute : qAsConst(lstASF)) {
478  TagLib::ASF::Picture picture = attribute.toPicture();
479  if (picture.type() == TagLib::ASF::Picture::FrontCover) {
480  TagLib::ByteVector pictureData = picture.picture();
481  return QByteArray(pictureData.data(), pictureData.size());
482  }
483  }
484  return QByteArray();
485 }
486 
487 void
488 EmbeddedImageData::Private::writeFrontCoverToAsf(TagLib::ASF::Tag* asfTags, const QByteArray &pictureData)
489 {
490  TagLib::ASF::AttributeList lstASF = asfTags->attribute("WM/Picture");
491  for (const auto& attribute : qAsConst(lstASF)) {
492  TagLib::ASF::Picture picture = attribute.toPicture();
493  if (picture.type() == TagLib::ASF::Picture::FrontCover) {
494  picture.setPicture(TagLib::ByteVector(pictureData.constData(), pictureData.size()));
495  picture.setMimeType(determineMimeType(pictureData));
496  TagLib::ByteVector pictureData = picture.picture();
497  return;
498  }
499  }
500  TagLib::ASF::Picture picture;
501  picture.setPicture(TagLib::ByteVector(pictureData.constData(), pictureData.size()));
502  picture.setType(TagLib::ASF::Picture::FrontCover);
503  lstASF.append(picture);
504 }
505 
506 
507 TagLib::String EmbeddedImageData::Private::determineMimeType(const QByteArray &pictureData)
508 {
509  if (pictureData.startsWith(QByteArray::fromHex("89504E470D0A1A0A"))) {
510  return TagLib::String("image/png");
511  } else if (pictureData.startsWith(QByteArray::fromHex("FFD8FFDB")) ||
512  pictureData.startsWith(QByteArray::fromHex("FFD8FFE000104A4649460001")) ||
513  pictureData.startsWith(QByteArray::fromHex("FFD8FFEE")) ||
514  pictureData.startsWith(QByteArray::fromHex("FFD8FFE1"))) {
515  return TagLib::String("image/jpeg");
516  } else {
517  return TagLib::String();
518  }
519 }
520 
QString & append(QChar ch)
bool startsWith(const QByteArray &ba) const const
void writeImageData(const QString &fileUrl, QMap< ImageType, QByteArray > &imageData)
Write the images to the metadata tags in a file.
The EmbeddedImageData is a class which extracts and writes the images stored in the metadata tags of ...
const char * constData() const const
QMap::iterator end()
QByteArray & append(char ch)
QByteArray fromHex(const QByteArray &hexEncoded)
char * data()
QMap::iterator insert(const Key &key, const T &value)
char front() const const
int size() const const
QMap< ImageType, QByteArray > imageData(const QString &fileUrl, const EmbeddedImageData::ImageTypes types=FrontCover) const
Extracts the images stored in the metadata tags from a file.
QMap::iterator find(const Key &key)
QByteArray toUtf8() const const
This file is part of the KDE documentation.
Documentation copyright © 1996-2020 The KDE developers.
Generated on Mon Jun 1 2020 22:55:49 by doxygen 1.8.11 written by Dimitri van Heesch, © 1997-2006

KDE's Doxygen guidelines are available online.