From 6be03b7ae135eb34c279b078f5d5bb296165fdc2 Mon Sep 17 00:00:00 2001 From: Urs Fleisch Date: Sat, 7 Oct 2023 09:42:25 +0200 Subject: [PATCH] Unified interface for complex properties like pictures (#94) Provides a dynamic interface for properties which cannot be represented with simple strings, e.g. pictures. The keys of such properties can be queried using `complexPropertyKeys()`, which could return for example ["PICTURE"]. The property can then be read using `complexProperties("PICTURE")`, which will return a list of variant maps containing the picture data and attributes. Adding a picture is as easy as t->setComplexProperties("PICTURE", { { {"data", data}, {"pictureType", "Front Cover"}, {"mimeType", "image/jpeg"} } }); --- examples/tagreader.cpp | 33 +- examples/tagwriter.cpp | 48 +- taglib/CMakeLists.txt | 2 + taglib/ape/apetag.cpp | 93 ++++ taglib/ape/apetag.h | 4 + taglib/asf/asfpicture.h | 46 +- taglib/asf/asftag.cpp | 53 +++ taglib/asf/asftag.h | 4 + taglib/flac/flacfile.cpp | 65 +++ taglib/flac/flacfile.h | 17 + taglib/flac/flacpicture.h | 46 +- taglib/mp4/mp4tag.cpp | 73 +++ taglib/mp4/mp4tag.h | 4 + .../mpeg/id3v2/frames/attachedpictureframe.h | 46 +- taglib/mpeg/id3v2/id3v2tag.cpp | 80 ++++ taglib/mpeg/id3v2/id3v2tag.h | 4 + taglib/ogg/xiphcomment.cpp | 57 +++ taglib/ogg/xiphcomment.h | 4 + taglib/tag.cpp | 15 + taglib/tag.h | 45 ++ taglib/tagunion.cpp | 39 ++ taglib/tagunion.h | 4 + taglib/toolkit/tfile.cpp | 15 + taglib/toolkit/tfile.h | 22 + taglib/toolkit/tpicturetype.cpp | 76 ++++ taglib/toolkit/tpicturetype.h | 122 +++++ tests/CMakeLists.txt | 1 + tests/test_complexproperties.cpp | 417 ++++++++++++++++++ 28 files changed, 1300 insertions(+), 135 deletions(-) create mode 100644 taglib/toolkit/tpicturetype.cpp create mode 100644 taglib/toolkit/tpicturetype.h create mode 100644 tests/test_complexproperties.cpp diff --git a/examples/tagreader.cpp b/examples/tagreader.cpp index 3d18f4d5..1fe825d2 100644 --- a/examples/tagreader.cpp +++ b/examples/tagreader.cpp @@ -27,6 +27,8 @@ #include #include "tpropertymap.h" +#include "tstringlist.h" +#include "tvariant.h" #include "fileref.h" #include "tag.h" @@ -65,10 +67,39 @@ int main(int argc, char *argv[]) cout << "-- TAG (properties) --" << endl; for(auto i = tags.cbegin(); i != tags.cend(); ++i) { for(auto j = i->second.begin(); j != i->second.end(); ++j) { - cout << left << std::setw(longest) << i->first << " - " << '"' << *j << '"' << endl; + cout << left << std::setfill(' ') << std::setw(longest) << i->first << " - " << '"' << *j << '"' << endl; } } + TagLib::StringList names = f.file()->complexPropertyKeys(); + for(const auto &name : names) { + const auto& properties = f.file()->complexProperties(name); + for(const auto &property : properties) { + cout << name << ":" << endl; + for(const auto &[key, value] : property) { + cout << " " << left << std::setfill(' ') << std::setw(11) << key << " - "; + if(value.type() == TagLib::Variant::ByteVector) { + cout << "(" << value.value().size() << " bytes)" << endl; + /* The picture could be extracted using: + ofstream picture; + TagLib::String fn(argv[i]); + int slashPos = fn.rfind('/'); + int dotPos = fn.rfind('.'); + if(slashPos >= 0 && dotPos > slashPos) { + fn = fn.substr(slashPos + 1, dotPos - slashPos - 1); + } + fn += ".jpg"; + picture.open(fn.toCString(), ios_base::out | ios_base::binary); + picture << value.value(); + picture.close(); + */ + } + else { + cout << value << endl; + } + } + } + } } if(!f.isNull() && f.audioProperties()) { diff --git a/examples/tagwriter.cpp b/examples/tagwriter.cpp index 85885c0c..796bc42e 100644 --- a/examples/tagwriter.cpp +++ b/examples/tagwriter.cpp @@ -24,6 +24,8 @@ #include #include +#include +#include #include #include #include @@ -33,6 +35,7 @@ #include "tlist.h" #include "tfile.h" #include "tpropertymap.h" +#include "tvariant.h" #include "fileref.h" #include "tag.h" @@ -69,6 +72,7 @@ void usage() cout << " -R " << endl; cout << " -I " << endl; cout << " -D " << endl; + cout << " -p (\"\" \"\" to remove)" << endl; cout << endl; exit(1); @@ -109,12 +113,14 @@ int main(int argc, char *argv[]) if(fileList.isEmpty()) usage(); - for(int i = 1; i < argc - 1; i += 2) { + int i = 1; + while(i < argc - 1) { if(isArgument(argv[i]) && i + 1 < argc && !isArgument(argv[i + 1])) { char field = argv[i][1]; TagLib::String value = argv[i + 1]; + int numArgsConsumed = 2; TagLib::List::ConstIterator it; for(it = fileList.cbegin(); it != fileList.cend(); ++it) { @@ -153,7 +159,7 @@ int main(int argc, char *argv[]) else { map.insert(value, TagLib::String(argv[i + 2])); } - ++i; + numArgsConsumed = 3; checkForRejectedProperties((*it).file()->setProperties(map)); } else { @@ -166,11 +172,49 @@ int main(int argc, char *argv[]) checkForRejectedProperties((*it).file()->setProperties(map)); break; } + case 'p': { + if(i + 2 < argc) { + numArgsConsumed = 3; + if(!value.isEmpty()) { + if(!isFile(value.toCString())) { + cout << value.toCString() << " not found." << endl; + return 1; + } + ifstream picture; + picture.open(value.toCString()); + stringstream buffer; + buffer << picture.rdbuf(); + picture.close(); + TagLib::String buf(buffer.str()); + TagLib::ByteVector data(buf.data(TagLib::String::Latin1)); + TagLib::String mimeType = data.startsWith("\x89PNG\x0d\x0a\x1a\x0a") + ? "image/png" : "image/jpeg"; + TagLib::String description(argv[i + 2]); + it->file()->setComplexProperties("PICTURE", { + { + {"data", data}, + {"pictureType", "Front Cover"}, + {"mimeType", mimeType}, + {"description", description} + } + }); + } + else { + // empty value, remove pictures + it->file()->setComplexProperties("PICTURE", {}); + } + } + else { + usage(); + } + break; + } default: usage(); break; } } + i += numArgsConsumed; } else usage(); diff --git a/taglib/CMakeLists.txt b/taglib/CMakeLists.txt index 3ffe978a..9f4c2143 100644 --- a/taglib/CMakeLists.txt +++ b/taglib/CMakeLists.txt @@ -47,6 +47,7 @@ set(tag_HDRS toolkit/tfilestream.h toolkit/tmap.h toolkit/tmap.tcc + toolkit/tpicturetype.h toolkit/tpropertymap.h toolkit/tdebuglistener.h toolkit/tversionnumber.h @@ -307,6 +308,7 @@ set(toolkit_SRCS toolkit/tfile.cpp toolkit/tfilestream.cpp toolkit/tdebug.cpp + toolkit/tpicturetype.cpp toolkit/tpropertymap.cpp toolkit/tdebuglistener.cpp toolkit/tzlib.cpp diff --git a/taglib/ape/apetag.cpp b/taglib/ape/apetag.cpp index a7b77c09..98aff334 100644 --- a/taglib/ape/apetag.cpp +++ b/taglib/ape/apetag.cpp @@ -50,6 +50,9 @@ namespace const unsigned int MinKeyLength = 2; const unsigned int MaxKeyLength = 255; + const String FRONT_COVER("COVER ART (FRONT)"); + const String BACK_COVER("COVER ART (BACK)"); + bool isKeyValid(const ByteVector &key) { static constexpr std::array invalidKeys { "ID3", "TAG", "OGGS", "MP+" }; @@ -265,6 +268,96 @@ PropertyMap APE::Tag::setProperties(const PropertyMap &origProps) return invalid; } +StringList APE::Tag::complexPropertyKeys() const +{ + StringList keys; + if(d->itemListMap.contains(FRONT_COVER) || + d->itemListMap.contains(BACK_COVER)) { + keys.append("PICTURE"); + } + return keys; +} + +List APE::Tag::complexProperties(const String &key) const +{ + List properties; + const String uppercaseKey = key.upper(); + if(uppercaseKey == "PICTURE") { + const StringList itemNames = StringList(FRONT_COVER).append(BACK_COVER); + for(const auto &itemName: itemNames) { + if(d->itemListMap.contains(itemName)) { + Item picture = d->itemListMap.value(itemName); + if(picture.type() == Item::Binary) { + ByteVector data = picture.binaryData(); + // Do not search for a description if the first byte could start JPG or PNG + // data. + int index = data.isEmpty() || data.at(0) == '\xff' || data.at(0) == '\x89' + ? -1 : data.find('\0'); + String description; + if(index >= 0) { + description = String(data.mid(0, index), String::UTF8); + data = data.mid(index + 1); + } + + VariantMap property; + property.insert("data", data); + if(!description.isEmpty()) { + property.insert("description", description); + } + property.insert("pictureType", + itemName == BACK_COVER ? "Back Cover" : "Front Cover"); + properties.append(property); + } + } + } + } + return properties; +} + +bool APE::Tag::setComplexProperties(const String &key, const List &value) +{ + const String uppercaseKey = key.upper(); + if(uppercaseKey == "PICTURE") { + removeItem(FRONT_COVER); + removeItem(BACK_COVER); + + auto frontItems = List(); + auto backItems = List(); + for(auto property : value) { + ByteVector data = property.value("description").value().data(String::UTF8) + .append('\0') + .append(property.value("data").value()); + String pictureType = property.value("pictureType").value(); + Item item; + item.setType(Item::Binary); + item.setBinaryData(data); + if(pictureType == "Back Cover") { + item.setKey(BACK_COVER); + backItems.append(item); + } + else if(pictureType == "Front Cover") { + item.setKey(FRONT_COVER); + // prioritize pictures with correct type + frontItems.prepend(item); + } + else { + item.setKey(FRONT_COVER); + frontItems.append(item); + } + } + if(!frontItems.isEmpty()) { + setItem(FRONT_COVER, frontItems.front()); + } + if(!backItems.isEmpty()) { + setItem(BACK_COVER, backItems.front()); + } + } + else { + return false; + } + return true; +} + bool APE::Tag::checkKey(const String &key) { if(key.size() < MinKeyLength || key.size() > MaxKeyLength) diff --git a/taglib/ape/apetag.h b/taglib/ape/apetag.h index d6bf8579..485f5035 100644 --- a/taglib/ape/apetag.h +++ b/taglib/ape/apetag.h @@ -130,6 +130,10 @@ namespace TagLib { */ PropertyMap setProperties(const PropertyMap &) override; + StringList complexPropertyKeys() const override; + List complexProperties(const String &key) const override; + bool setComplexProperties(const String &key, const List &value) override; + /*! * Check if the given String is a valid APE tag key. */ diff --git a/taglib/asf/asfpicture.h b/taglib/asf/asfpicture.h index 40e88795..c379359e 100644 --- a/taglib/asf/asfpicture.h +++ b/taglib/asf/asfpicture.h @@ -28,6 +28,7 @@ #include "tstring.h" #include "tbytevector.h" +#include "tpicturetype.h" #include "taglib_export.h" #include "attachedpictureframe.h" @@ -52,50 +53,7 @@ namespace TagLib /*! * This describes the function or content of the picture. */ - enum Type { - //! A type not enumerated below - Other = 0x00, - //! 32x32 PNG image that should be used as the file icon - FileIcon = 0x01, - //! File icon of a different size or format - OtherFileIcon = 0x02, - //! Front cover image of the album - FrontCover = 0x03, - //! Back cover image of the album - BackCover = 0x04, - //! Inside leaflet page of the album - LeafletPage = 0x05, - //! Image from the album itself - Media = 0x06, - //! Picture of the lead artist or soloist - LeadArtist = 0x07, - //! Picture of the artist or performer - Artist = 0x08, - //! Picture of the conductor - Conductor = 0x09, - //! Picture of the band or orchestra - Band = 0x0A, - //! Picture of the composer - Composer = 0x0B, - //! Picture of the lyricist or text writer - Lyricist = 0x0C, - //! Picture of the recording location or studio - RecordingLocation = 0x0D, - //! Picture of the artists during recording - DuringRecording = 0x0E, - //! Picture of the artists during performance - DuringPerformance = 0x0F, - //! Picture from a movie or video related to the track - MovieScreenCapture = 0x10, - //! Picture of a large, coloured fish - ColouredFish = 0x11, - //! Illustration related to the track - Illustration = 0x12, - //! Logo of the band or performer - BandLogo = 0x13, - //! Logo of the publisher (record company) - PublisherLogo = 0x14 - }; + DECLARE_PICTURE_TYPE_ENUM(Type) /*! * Constructs an empty picture. diff --git a/taglib/asf/asftag.cpp b/taglib/asf/asftag.cpp index f2a1648e..783c3217 100644 --- a/taglib/asf/asftag.cpp +++ b/taglib/asf/asftag.cpp @@ -29,6 +29,8 @@ #include #include "tpropertymap.h" +#include "asfattribute.h" +#include "asfpicture.h" using namespace TagLib; @@ -373,3 +375,54 @@ PropertyMap ASF::Tag::setProperties(const PropertyMap &props) return ignoredProps; } + +StringList ASF::Tag::complexPropertyKeys() const +{ + StringList keys; + if(d->attributeListMap.contains("WM/Picture")) { + keys.append("PICTURE"); + } + return keys; +} + +List ASF::Tag::complexProperties(const String &key) const +{ + List properties; + const String uppercaseKey = key.upper(); + if(uppercaseKey == "PICTURE") { + const AttributeList pictures = d->attributeListMap.value("WM/Picture"); + for(const Attribute &attribute : pictures) { + ASF::Picture picture = attribute.toPicture(); + VariantMap property; + property.insert("data", picture.picture()); + property.insert("mimeType", picture.mimeType()); + property.insert("description", picture.description()); + property.insert("pictureType", + ASF::Picture::typeToString(picture.type())); + properties.append(property); + } + } + return properties; +} + +bool ASF::Tag::setComplexProperties(const String &key, const List &value) +{ + const String uppercaseKey = key.upper(); + if(uppercaseKey == "PICTURE") { + removeItem("WM/Picture");; + + for(auto property : value) { + ASF::Picture picture; + picture.setPicture(property.value("data").value()); + picture.setMimeType(property.value("mimeType").value()); + picture.setDescription(property.value("description").value()); + picture.setType(ASF::Picture::typeFromString( + property.value("pictureType").value())); + addAttribute("WM/Picture", Attribute(picture)); + } + } + else { + return false; + } + return true; +} diff --git a/taglib/asf/asftag.h b/taglib/asf/asftag.h index ad1f95dd..35ffe81d 100644 --- a/taglib/asf/asftag.h +++ b/taglib/asf/asftag.h @@ -204,6 +204,10 @@ namespace TagLib { void removeUnsupportedProperties(const StringList &props) override; PropertyMap setProperties(const PropertyMap &props) override; + StringList complexPropertyKeys() const override; + List complexProperties(const String &key) const override; + bool setComplexProperties(const String &key, const List &value) override; + private: class TagPrivate; diff --git a/taglib/flac/flacfile.cpp b/taglib/flac/flacfile.cpp index 6b17acc4..13a4de89 100644 --- a/taglib/flac/flacfile.cpp +++ b/taglib/flac/flacfile.cpp @@ -140,6 +140,71 @@ PropertyMap FLAC::File::setProperties(const PropertyMap &properties) return xiphComment(true)->setProperties(properties); } +StringList FLAC::File::complexPropertyKeys() const +{ + StringList keys = TagLib::File::complexPropertyKeys(); + if(!keys.contains("PICTURE")) { + for(const auto &block : std::as_const(d->blocks)) { + if(dynamic_cast(block) != nullptr) { + keys.append("PICTURE"); + break; + } + } + } + return keys; +} + +List FLAC::File::complexProperties(const String &key) const +{ + const String uppercaseKey = key.upper(); + if(uppercaseKey == "PICTURE") { + List properties; + for(const auto &block : std::as_const(d->blocks)) { + if(auto picture = dynamic_cast(block)) { + VariantMap property; + property.insert("data", picture->data()); + property.insert("mimeType", picture->mimeType()); + property.insert("description", picture->description()); + property.insert("pictureType", + FLAC::Picture::typeToString(picture->type())); + property.insert("width", picture->width()); + property.insert("height", picture->height()); + property.insert("numColors", picture->numColors()); + property.insert("colorDepth", picture->colorDepth()); + properties.append(property); + } + } + return properties; + } + return TagLib::File::complexProperties(key); +} + +bool FLAC::File::setComplexProperties(const String &key, const List &value) +{ + const String uppercaseKey = key.upper(); + if(uppercaseKey == "PICTURE") { + removePictures(); + + for(auto property : value) { + FLAC::Picture *picture = new FLAC::Picture; + picture->setData(property.value("data").value()); + picture->setMimeType(property.value("mimeType").value()); + picture->setDescription(property.value("description").value()); + picture->setType(FLAC::Picture::typeFromString( + property.value("pictureType").value())); + picture->setWidth(property.value("width").value()); + picture->setHeight(property.value("height").value()); + picture->setNumColors(property.value("numColors").value()); + picture->setColorDepth(property.value("colorDepth").value()); + addPicture(picture); + } + } + else { + return TagLib::File::setComplexProperties(key, value); + } + return true; +} + FLAC::Properties *FLAC::File::audioProperties() const { return d->properties.get(); diff --git a/taglib/flac/flacfile.h b/taglib/flac/flacfile.h index 22586d45..b311a495 100644 --- a/taglib/flac/flacfile.h +++ b/taglib/flac/flacfile.h @@ -161,6 +161,23 @@ namespace TagLib { */ PropertyMap setProperties(const PropertyMap &) override; + /*! + * Returns ["PICTURE"] if any picture is stored in METADATA_BLOCK_PICTURE. + */ + StringList complexPropertyKeys() const override; + + /*! + * Get the pictures stored in METADATA_BLOCK_PICTURE as complex properties + * for \a key "PICTURE". + */ + List complexProperties(const String &key) const override; + + /*! + * Set the complex properties \a value as pictures in METADATA_BLOCK_PICTURE + * for \a key "PICTURE". + */ + bool setComplexProperties(const String &key, const List &value) override; + /*! * Returns the FLAC::Properties for this file. If no audio properties * were read then this will return a null pointer. diff --git a/taglib/flac/flacpicture.h b/taglib/flac/flacpicture.h index 24f5edc4..665af875 100644 --- a/taglib/flac/flacpicture.h +++ b/taglib/flac/flacpicture.h @@ -29,6 +29,7 @@ #include "tlist.h" #include "tstring.h" #include "tbytevector.h" +#include "tpicturetype.h" #include "taglib_export.h" #include "flacmetadatablock.h" @@ -41,50 +42,7 @@ namespace TagLib { /*! * This describes the function or content of the picture. */ - enum Type { - //! A type not enumerated below - Other = 0x00, - //! 32x32 PNG image that should be used as the file icon - FileIcon = 0x01, - //! File icon of a different size or format - OtherFileIcon = 0x02, - //! Front cover image of the album - FrontCover = 0x03, - //! Back cover image of the album - BackCover = 0x04, - //! Inside leaflet page of the album - LeafletPage = 0x05, - //! Image from the album itself - Media = 0x06, - //! Picture of the lead artist or soloist - LeadArtist = 0x07, - //! Picture of the artist or performer - Artist = 0x08, - //! Picture of the conductor - Conductor = 0x09, - //! Picture of the band or orchestra - Band = 0x0A, - //! Picture of the composer - Composer = 0x0B, - //! Picture of the lyricist or text writer - Lyricist = 0x0C, - //! Picture of the recording location or studio - RecordingLocation = 0x0D, - //! Picture of the artists during recording - DuringRecording = 0x0E, - //! Picture of the artists during performance - DuringPerformance = 0x0F, - //! Picture from a movie or video related to the track - MovieScreenCapture = 0x10, - //! Picture of a large, coloured fish - ColouredFish = 0x11, - //! Illustration related to the track - Illustration = 0x12, - //! Logo of the band or performer - BandLogo = 0x13, - //! Logo of the publisher (record company) - PublisherLogo = 0x14 - }; + DECLARE_PICTURE_TYPE_ENUM(Type) Picture(); Picture(const ByteVector &data); diff --git a/taglib/mp4/mp4tag.cpp b/taglib/mp4/mp4tag.cpp index bec6b2ab..dc8f8a9d 100644 --- a/taglib/mp4/mp4tag.cpp +++ b/taglib/mp4/mp4tag.cpp @@ -32,6 +32,7 @@ #include "tpropertymap.h" #include "id3v1genres.h" #include "mp4atom.h" +#include "mp4coverart.h" using namespace TagLib; @@ -1066,6 +1067,78 @@ PropertyMap MP4::Tag::setProperties(const PropertyMap &props) return ignoredProps; } +StringList MP4::Tag::complexPropertyKeys() const +{ + StringList keys; + if(d->items.contains("covr")) { + keys.append("PICTURE"); + } + return keys; +} + +List MP4::Tag::complexProperties(const String &key) const +{ + List properties; + const String uppercaseKey = key.upper(); + if(uppercaseKey == "PICTURE") { + const CoverArtList pictures = d->items.value("covr").toCoverArtList(); + for(const CoverArt &picture : pictures) { + String mimeType = "image/"; + switch(picture.format()) { + case CoverArt::BMP: + mimeType.append("bmp"); + break; + case CoverArt::JPEG: + mimeType.append("jpeg"); + break; + case CoverArt::GIF: + mimeType.append("gif"); + break; + case CoverArt::PNG: + mimeType.append("png"); + break; + case CoverArt::Unknown: + break; + } + + VariantMap property; + property.insert("data", picture.data()); + property.insert("mimeType", mimeType); + properties.append(property); + } + } + return properties; +} + +bool MP4::Tag::setComplexProperties(const String &key, const List &value) +{ + const String uppercaseKey = key.upper(); + if(uppercaseKey == "PICTURE") { + CoverArtList pictures; + for(auto property : value) { + String mimeType = property.value("mimeType").value(); + CoverArt::Format format; + if(mimeType == "image/bmp") { + format = CoverArt::BMP; + } else if(mimeType == "image/png") { + format = CoverArt::PNG; + } else if(mimeType == "image/gif") { + format = CoverArt::GIF; + } else if(mimeType == "image/jpeg") { + format = CoverArt::JPEG; + } else { + format = CoverArt::Unknown; + } + pictures.append(CoverArt(format, property.value("data").value())); + } + d->items["covr"] = pictures; + } + else { + return false; + } + return true; +} + void MP4::Tag::addItem(const String &name, const Item &value) { if(!d->items.contains(name)) { diff --git a/taglib/mp4/mp4tag.h b/taglib/mp4/mp4tag.h index 186c8ab8..226ce019 100644 --- a/taglib/mp4/mp4tag.h +++ b/taglib/mp4/mp4tag.h @@ -102,6 +102,10 @@ namespace TagLib { void removeUnsupportedProperties(const StringList &props) override; PropertyMap setProperties(const PropertyMap &props) override; + StringList complexPropertyKeys() const override; + List complexProperties(const String &key) const override; + bool setComplexProperties(const String &key, const List &value) override; + protected: /*! * Sets the value of \a key to \a value, overwriting any previous value. diff --git a/taglib/mpeg/id3v2/frames/attachedpictureframe.h b/taglib/mpeg/id3v2/frames/attachedpictureframe.h index 3d062e76..37ca5c5f 100644 --- a/taglib/mpeg/id3v2/frames/attachedpictureframe.h +++ b/taglib/mpeg/id3v2/frames/attachedpictureframe.h @@ -27,6 +27,7 @@ #define TAGLIB_ATTACHEDPICTUREFRAME_H #include "taglib_export.h" +#include "tpicturetype.h" #include "id3v2frame.h" #include "id3v2header.h" @@ -52,50 +53,7 @@ namespace TagLib { /*! * This describes the function or content of the picture. */ - enum Type { - //! A type not enumerated below - Other = 0x00, - //! 32x32 PNG image that should be used as the file icon - FileIcon = 0x01, - //! File icon of a different size or format - OtherFileIcon = 0x02, - //! Front cover image of the album - FrontCover = 0x03, - //! Back cover image of the album - BackCover = 0x04, - //! Inside leaflet page of the album - LeafletPage = 0x05, - //! Image from the album itself - Media = 0x06, - //! Picture of the lead artist or soloist - LeadArtist = 0x07, - //! Picture of the artist or performer - Artist = 0x08, - //! Picture of the conductor - Conductor = 0x09, - //! Picture of the band or orchestra - Band = 0x0A, - //! Picture of the composer - Composer = 0x0B, - //! Picture of the lyricist or text writer - Lyricist = 0x0C, - //! Picture of the recording location or studio - RecordingLocation = 0x0D, - //! Picture of the artists during recording - DuringRecording = 0x0E, - //! Picture of the artists during performance - DuringPerformance = 0x0F, - //! Picture from a movie or video related to the track - MovieScreenCapture = 0x10, - //! Picture of a large, coloured fish - ColouredFish = 0x11, - //! Illustration related to the track - Illustration = 0x12, - //! Logo of the band or performer - BandLogo = 0x13, - //! Logo of the publisher (record company) - PublisherLogo = 0x14 - }; + DECLARE_PICTURE_TYPE_ENUM(Type) /*! * Constructs an empty picture frame. The description, content and text diff --git a/taglib/mpeg/id3v2/id3v2tag.cpp b/taglib/mpeg/id3v2/id3v2tag.cpp index f3492b36..3dc28011 100644 --- a/taglib/mpeg/id3v2/id3v2tag.cpp +++ b/taglib/mpeg/id3v2/id3v2tag.cpp @@ -37,6 +37,8 @@ #include "id3v2footer.h" #include "id3v2synchdata.h" #include "id3v1genres.h" +#include "frames/attachedpictureframe.h" +#include "frames/generalencapsulatedobjectframe.h" #include "frames/textidentificationframe.h" #include "frames/commentsframe.h" #include "frames/urllinkframe.h" @@ -451,6 +453,84 @@ PropertyMap ID3v2::Tag::setProperties(const PropertyMap &origProps) return PropertyMap(); // ID3 implements the complete PropertyMap interface, so an empty map is returned } +StringList ID3v2::Tag::complexPropertyKeys() const +{ + StringList keys; + if(d->frameListMap.contains("APIC")) { + keys.append("PICTURE"); + } + if(d->frameListMap.contains("GEOB")) { + keys.append("GENERALOBJECT"); + } + return keys; +} + +List ID3v2::Tag::complexProperties(const String &key) const +{ + List properties; + const String uppercaseKey = key.upper(); + if(uppercaseKey == "PICTURE") { + const FrameList pictures = d->frameListMap.value("APIC"); + for(const Frame *frame : pictures) { + auto picture = static_cast(frame); + VariantMap property; + property.insert("data", picture->picture()); + property.insert("mimeType", picture->mimeType()); + property.insert("description", picture->description()); + property.insert("pictureType", + AttachedPictureFrame::typeToString(picture->type())); + properties.append(property); + } + } + else if(uppercaseKey == "GENERALOBJECT") { + const FrameList geobs = d->frameListMap.value("GEOB"); + for(const Frame *frame : geobs) { + auto geob = static_cast(frame); + VariantMap property; + property.insert("data", geob->object()); + property.insert("mimeType", geob->mimeType()); + property.insert("description", geob->description()); + property.insert("fileName", geob->fileName()); + properties.append(property); + } + } + return properties; +} + +bool ID3v2::Tag::setComplexProperties(const String &key, const List &value) +{ + const String uppercaseKey = key.upper(); + if(uppercaseKey == "PICTURE") { + removeFrames("APIC"); + + for(auto property : value) { + auto picture = new AttachedPictureFrame; + picture->setPicture(property.value("data").value()); + picture->setMimeType(property.value("mimeType").value()); + picture->setDescription(property.value("description").value()); + picture->setType(AttachedPictureFrame::typeFromString( + property.value("pictureType").value())); + addFrame(picture); + } + } + else if(uppercaseKey == "GENERALOBJECT") { + removeFrames("GEOB"); + + for(auto property : value) { + auto geob = new GeneralEncapsulatedObjectFrame; + geob->setObject(property.value("data").value()); + geob->setMimeType(property.value("mimeType").value()); + geob->setDescription(property.value("description").value()); + geob->setFileName(property.value("fileName").value()); + addFrame(geob); + } + } + else { + return false; + } + return true; +} + ByteVector ID3v2::Tag::render() const { return render(ID3v2::v4); diff --git a/taglib/mpeg/id3v2/id3v2tag.h b/taglib/mpeg/id3v2/id3v2tag.h index 8d23d3e3..d3f9b81b 100644 --- a/taglib/mpeg/id3v2/id3v2tag.h +++ b/taglib/mpeg/id3v2/id3v2tag.h @@ -329,6 +329,10 @@ namespace TagLib { */ PropertyMap setProperties(const PropertyMap &) override; + StringList complexPropertyKeys() const override; + List complexProperties(const String &key) const override; + bool setComplexProperties(const String &key, const List &value) override; + /*! * Render the tag back to binary data, suitable to be written to disk. */ diff --git a/taglib/ogg/xiphcomment.cpp b/taglib/ogg/xiphcomment.cpp index fbe7ff05..f02831d0 100644 --- a/taglib/ogg/xiphcomment.cpp +++ b/taglib/ogg/xiphcomment.cpp @@ -235,6 +235,63 @@ PropertyMap Ogg::XiphComment::setProperties(const PropertyMap &properties) return invalid; } +StringList Ogg::XiphComment::complexPropertyKeys() const +{ + StringList keys; + if(!d->pictureList.isEmpty()) { + keys.append("PICTURE"); + } + return keys; +} + +List Ogg::XiphComment::complexProperties(const String &key) const +{ + List properties; + const String uppercaseKey = key.upper(); + if(uppercaseKey == "PICTURE") { + for(const FLAC::Picture *picture : std::as_const(d->pictureList)) { + VariantMap property; + property.insert("data", picture->data()); + property.insert("mimeType", picture->mimeType()); + property.insert("description", picture->description()); + property.insert("pictureType", + FLAC::Picture::typeToString(picture->type())); + property.insert("width", picture->width()); + property.insert("height", picture->height()); + property.insert("numColors", picture->numColors()); + property.insert("colorDepth", picture->colorDepth()); + properties.append(property); + } + } + return properties; +} + +bool Ogg::XiphComment::setComplexProperties(const String &key, const List &value) +{ + const String uppercaseKey = key.upper(); + if(uppercaseKey == "PICTURE") { + removeAllPictures(); + + for(auto property : value) { + FLAC::Picture *picture = new FLAC::Picture; + picture->setData(property.value("data").value()); + picture->setMimeType(property.value("mimeType").value()); + picture->setDescription(property.value("description").value()); + picture->setType(FLAC::Picture::typeFromString( + property.value("pictureType").value())); + picture->setWidth(property.value("width").value()); + picture->setHeight(property.value("height").value()); + picture->setNumColors(property.value("numColors").value()); + picture->setColorDepth(property.value("colorDepth").value()); + addPicture(picture); + } + } + else { + return false; + } + return true; +} + bool Ogg::XiphComment::checkKey(const String &key) { if(key.size() < 1) diff --git a/taglib/ogg/xiphcomment.h b/taglib/ogg/xiphcomment.h index e28e13f8..ca70a71f 100644 --- a/taglib/ogg/xiphcomment.h +++ b/taglib/ogg/xiphcomment.h @@ -167,6 +167,10 @@ namespace TagLib { */ PropertyMap setProperties(const PropertyMap&) override; + StringList complexPropertyKeys() const override; + List complexProperties(const String &key) const override; + bool setComplexProperties(const String &key, const List &value) override; + /*! * Check if the given String is a valid Xiph comment key. */ diff --git a/taglib/tag.cpp b/taglib/tag.cpp index 394715fc..e06477d9 100644 --- a/taglib/tag.cpp +++ b/taglib/tag.cpp @@ -146,6 +146,21 @@ PropertyMap Tag::setProperties(const PropertyMap &origProps) return properties; } +StringList Tag::complexPropertyKeys() const +{ + return StringList(); +} + +List Tag::complexProperties(const String &) const +{ + return {}; +} + +bool Tag::setComplexProperties(const String &, const List &) +{ + return false; +} + void Tag::duplicate(const Tag *source, Tag *target, bool overwrite) // static { if(overwrite) { diff --git a/taglib/tag.h b/taglib/tag.h index dbddfa98..0e5b4049 100644 --- a/taglib/tag.h +++ b/taglib/tag.h @@ -28,6 +28,8 @@ #include "taglib_export.h" #include "tstring.h" +#include "tlist.h" +#include "tvariant.h" namespace TagLib { @@ -78,6 +80,49 @@ namespace TagLib { */ virtual PropertyMap setProperties(const PropertyMap &origProps); + /*! + * Get the keys of complex properties, i.e. properties which cannot be + * represented simply by a string. + * Because such properties might be expensive to fetch, there are separate + * operations to get the available keys - which is expected to be cheap - + * and getting and setting the property values. + * The default implementation returns only an empty list. Reimplementations + * should provide "PICTURE" if embedded cover art is present, and optionally + * support other properties. + */ + virtual StringList complexPropertyKeys() const; + + /*! + * Get the complex properties for a given \a key. + * In order to be flexible for different metadata formats, the properties + * are represented as variant maps. Despite this dynamic nature, some + * degree of standardization should be achieved between formats: + * + * - PICTURE + * - data: ByteVector with picture data + * - description: String with description + * - pictureType: String with type as specified for ID3v2, + * e.g. "Front Cover", "Back Cover", "Band" + * - mimeType: String with image format, e.g. "image/jpeg" + * - optionally more information found in the tag, such as + * "width", "height", "numColors", "colorDepth" int values + * in FLAC pictures + * - GENERALOBJECT + * - data: ByteVector with object data + * - description: String with description + * - fileName: String with file name + * - mimeType: String with MIME type + * - this is currently only implemented for ID3v2 GEOB frames + */ + virtual List complexProperties(const String &key) const; + + /*! + * Set all complex properties for a given \a key using variant maps as + * \a value with the same format as returned by complexProperties(). + * An empty list as \a value to removes all complex properties for \a key. + */ + virtual bool setComplexProperties(const String &key, const List &value); + /*! * Returns the track name; if no track name is present in the tag * String::null will be returned. diff --git a/taglib/tagunion.cpp b/taglib/tagunion.cpp index ee79b2be..54b7ccbe 100644 --- a/taglib/tagunion.cpp +++ b/taglib/tagunion.cpp @@ -124,6 +124,45 @@ void TagUnion::removeUnsupportedProperties(const StringList &unsupported) } } +StringList TagUnion::complexPropertyKeys() const +{ + for(const auto &tag : d->tags) { + if(tag) { + const StringList keys = tag->complexPropertyKeys(); + if(!keys.isEmpty()) { + return keys; + } + } + } + return StringList(); +} + +List TagUnion::complexProperties(const String &key) const +{ + for(const auto &tag : d->tags) { + if(tag) { + const List props = tag->complexProperties(key); + if(!props.isEmpty()) { + return props; + } + } + } + return List(); +} + +bool TagUnion::setComplexProperties(const String &key, const List &value) +{ + bool combinedResult = false; + for(const auto &tag : d->tags) { + if(tag) { + if(tag->setComplexProperties(key, value)) { + combinedResult = true; + } + } + } + return combinedResult; +} + String TagUnion::title() const { stringUnion(title); diff --git a/taglib/tagunion.h b/taglib/tagunion.h index a6b4cfc4..d6301a34 100644 --- a/taglib/tagunion.h +++ b/taglib/tagunion.h @@ -62,6 +62,10 @@ namespace TagLib { PropertyMap properties() const override; void removeUnsupportedProperties(const StringList &unsupported) override; + StringList complexPropertyKeys() const override; + List complexProperties(const String &key) const override; + bool setComplexProperties(const String &key, const List &value) override; + String title() const override; String artist() const override; String album() const override; diff --git a/taglib/toolkit/tfile.cpp b/taglib/toolkit/tfile.cpp index 2ca4a1b2..340c9216 100644 --- a/taglib/toolkit/tfile.cpp +++ b/taglib/toolkit/tfile.cpp @@ -106,6 +106,21 @@ PropertyMap File::setProperties(const PropertyMap &properties) return tag()->setProperties(properties); } +StringList File::complexPropertyKeys() const +{ + return tag()->complexPropertyKeys(); +} + +List File::complexProperties(const String &key) const +{ + return tag()->complexProperties(key); +} + +bool File::setComplexProperties(const String &key, const List &value) +{ + return tag()->setComplexProperties(key, value); +} + ByteVector File::readBlock(size_t length) { return d->stream->readBlock(length); diff --git a/taglib/toolkit/tfile.h b/taglib/toolkit/tfile.h index f6e7a8f9..e5fe6faf 100644 --- a/taglib/toolkit/tfile.h +++ b/taglib/toolkit/tfile.h @@ -133,6 +133,28 @@ namespace TagLib { */ virtual PropertyMap setProperties(const PropertyMap &properties); + /*! + * Get the keys of complex properties, i.e. properties which cannot be + * represented simply by a string. + * The default implementation calls Tag::complexPropertyKeys(). + * \see Tag::complexPropertyKeys() + */ + virtual StringList complexPropertyKeys() const; + + /*! + * Get the complex properties for a given \a key. + * The default implementation calls Tag::complexProperties(). + * \see Tag::complexProperties() + */ + virtual List complexProperties(const String &key) const; + + /*! + * Set all complex properties for \a key using the variant maps \a value. + * The default implementation calls Tag::setComplexProperties(). + * \see Tag::setComplexProperties() + */ + virtual bool setComplexProperties(const String &key, const List &value); + /*! * Returns a pointer to this file's audio properties. This should be * reimplemented in the concrete subclasses. If no audio properties were diff --git a/taglib/toolkit/tpicturetype.cpp b/taglib/toolkit/tpicturetype.cpp new file mode 100644 index 00000000..33442b1d --- /dev/null +++ b/taglib/toolkit/tpicturetype.cpp @@ -0,0 +1,76 @@ +/*************************************************************************** + copyright : (C) 2023 by Urs Fleisch + email : ufleisch@users.sourceforge.net + ***************************************************************************/ + +/*************************************************************************** + * This library is free software; you can redistribute it and/or modify * + * it under the terms of the GNU Lesser General Public License version * + * 2.1 as published by the Free Software Foundation. * + * * + * This library is distributed in the hope that it will be useful, but * + * WITHOUT ANY WARRANTY; without even the implied warranty of * + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU * + * Lesser General Public License for more details. * + * * + * You should have received a copy of the GNU Lesser General Public * + * License along with this library; if not, write to the Free Software * + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA * + * 02110-1301 USA * + * * + * Alternatively, this file is available under the Mozilla Public * + * License Version 1.1. You may obtain a copy of the License at * + * http://www.mozilla.org/MPL/ * + ***************************************************************************/ + +#include "tpicturetype.h" + +#include "tstring.h" + +using namespace TagLib; + +namespace { + + static const char *const typeStrs[] = { + "Other", + "File Icon", + "Other File Icon", + "Front Cover", + "Back Cover", + "Leaflet Page", + "Media", + "Lead Artist", + "Artist", + "Conductor", + "Band", + "Composer", + "Lyricist", + "Recording Location", + "During Recording", + "During Performance", + "Movie Screen Capture", + "Coloured Fish", + "Illustration", + "Band Logo", + "Publisher Logo" + }; + +} // namespace + +String Utils::pictureTypeToString(int type) +{ + if(type >= 0 && type < static_cast(std::size(typeStrs))) { + return typeStrs[type]; + } + return ""; +} + +int Utils::pictureTypeFromString(String str) +{ + for(int i = 0; i < static_cast(std::size(typeStrs)); ++i) { + if(str == typeStrs[i]) { + return i; + } + } + return 0; +} diff --git a/taglib/toolkit/tpicturetype.h b/taglib/toolkit/tpicturetype.h new file mode 100644 index 00000000..a9cadf19 --- /dev/null +++ b/taglib/toolkit/tpicturetype.h @@ -0,0 +1,122 @@ +/*************************************************************************** + copyright : (C) 2023 by Urs Fleisch + email : ufleisch@users.sourceforge.net + ***************************************************************************/ + +/*************************************************************************** + * This library is free software; you can redistribute it and/or modify * + * it under the terms of the GNU Lesser General Public License version * + * 2.1 as published by the Free Software Foundation. * + * * + * This library is distributed in the hope that it will be useful, but * + * WITHOUT ANY WARRANTY; without even the implied warranty of * + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU * + * Lesser General Public License for more details. * + * * + * You should have received a copy of the GNU Lesser General Public * + * License along with this library; if not, write to the Free Software * + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA * + * 02110-1301 USA * + * * + * Alternatively, this file is available under the Mozilla Public * + * License Version 1.1. You may obtain a copy of the License at * + * http://www.mozilla.org/MPL/ * + ***************************************************************************/ + +#ifndef TAGLIB_PICTURETYPE_H +#define TAGLIB_PICTURETYPE_H + +// THIS FILE IS NOT A PART OF THE TAGLIB API + +#ifndef DO_NOT_DOCUMENT // tell Doxygen not to document this header + +#include "taglib_export.h" + +/*! + * Declare a picture type \a name enumeration inside a class. + * Declares a picture type enum according to the ID3v2 specification and + * adds methods \c typeToString() and \c typeFromString(). + * + * \code {.cpp} + * class MyClass { + * public: + * DECLARE_PICTURE_TYPE_ENUM(Type) + * (..) + * } + * \endcode + */ +#define DECLARE_PICTURE_TYPE_ENUM(name) \ +enum name { \ + /*! A type not enumerated below */ \ + Other = 0x00, \ + /*! 32x32 PNG image that should be used as the file icon */ \ + FileIcon = 0x01, \ + /*! File icon of a different size or format */ \ + OtherFileIcon = 0x02, \ + /*! Front cover image of the album */ \ + FrontCover = 0x03, \ + /*! Back cover image of the album */ \ + BackCover = 0x04, \ + /*! Inside leaflet page of the album */ \ + LeafletPage = 0x05, \ + /*! Image from the album itself */ \ + Media = 0x06, \ + /*! Picture of the lead artist or soloist */ \ + LeadArtist = 0x07, \ + /*! Picture of the artist or performer */ \ + Artist = 0x08, \ + /*! Picture of the conductor */ \ + Conductor = 0x09, \ + /*! Picture of the band or orchestra */ \ + Band = 0x0A, \ + /*! Picture of the composer */ \ + Composer = 0x0B, \ + /*! Picture of the lyricist or text writer */ \ + Lyricist = 0x0C, \ + /*! Picture of the recording location or studio */ \ + RecordingLocation = 0x0D, \ + /*! Picture of the artists during recording */ \ + DuringRecording = 0x0E, \ + /*! Picture of the artists during performance */ \ + DuringPerformance = 0x0F, \ + /*! Picture from a movie or video related to the track */ \ + MovieScreenCapture = 0x10, \ + /*! Picture of a large, coloured fish */ \ + ColouredFish = 0x11, \ + /*! Illustration related to the track */ \ + Illustration = 0x12, \ + /*! Logo of the band or performer */ \ + BandLogo = 0x13, \ + /*! Logo of the publisher (record company) */ \ + PublisherLogo = 0x14 \ +}; \ +static TagLib::String typeToString(name type) { \ + return TagLib::Utils::pictureTypeToString(type); \ +} \ +static name typeFromString(TagLib::String str) { \ + return static_cast( \ + TagLib::Utils::pictureTypeFromString(str)); \ +} + +namespace TagLib { + + class String; + + namespace Utils { + + /*! + * Get string representation of picture type. + */ + String TAGLIB_EXPORT pictureTypeToString(int type); + + /*! + * Get picture type from string representation. + */ + int TAGLIB_EXPORT pictureTypeFromString(String str); + + } // namespace Utils +} // namespace TagLib + +#endif + +#endif diff --git a/tests/CMakeLists.txt b/tests/CMakeLists.txt index 8ee72478..415ae1c9 100644 --- a/tests/CMakeLists.txt +++ b/tests/CMakeLists.txt @@ -40,6 +40,7 @@ SET(test_runner_SRCS test_string.cpp test_propertymap.cpp test_variant.cpp + test_complexproperties.cpp test_file.cpp test_fileref.cpp test_id3v1.cpp diff --git a/tests/test_complexproperties.cpp b/tests/test_complexproperties.cpp new file mode 100644 index 00000000..cd8d78c4 --- /dev/null +++ b/tests/test_complexproperties.cpp @@ -0,0 +1,417 @@ +/*************************************************************************** + copyright : (C) 2023 by Urs Fleisch + email : ufleisch@users.sourceforge.net + ***************************************************************************/ + +/*************************************************************************** + * This library is free software; you can redistribute it and/or modify * + * it under the terms of the GNU Lesser General Public License version * + * 2.1 as published by the Free Software Foundation. * + * * + * This library is distributed in the hope that it will be useful, but * + * WITHOUT ANY WARRANTY; without even the implied warranty of * + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU * + * Lesser General Public License for more details. * + * * + * You should have received a copy of the GNU Lesser General Public * + * License along with this library; if not, write to the Free Software * + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA * + * 02110-1301 USA * + * * + * Alternatively, this file is available under the Mozilla Public * + * License Version 1.1. You may obtain a copy of the License at * + * http://www.mozilla.org/MPL/ * + ***************************************************************************/ + +#include "asfpicture.h" +#include "flacpicture.h" +#include "flacfile.h" +#include "tbytevector.h" +#include "tvariant.h" +#include "tzlib.h" +#include "fileref.h" +#include "apetag.h" +#include "asftag.h" +#include "mp4tag.h" +#include "xiphcomment.h" +#include "id3v1tag.h" +#include "id3v2tag.h" +#include "attachedpictureframe.h" +#include "generalencapsulatedobjectframe.h" +#include +#include "utils.h" + +using namespace TagLib; + +namespace { + +const String GEOB_KEY("GENERALOBJECT"); +const String PICTURE_KEY("PICTURE"); + +const VariantMap TEST_PICTURE { + {"data", ByteVector( + "\xff\xd8\xff\xe0\x00\x10\x4a\x46\x49\x46\x00\x01\x01\x01\x00\x48\x00\x48" + "\x00\x00\xff\xdb\x00\x43\x00\x03\x02\x02\x02\x02\x02\x03\x02\x02\x02\x03" + "\x03\x03\x03\x04\x06\x04\x04\x04\x04\x04\x08\x06\x06\x05\x06\x09\x08\x0a" + "\x0a\x09\x08\x09\x09\x0a\x0c\x0f\x0c\x0a\x0b\x0e\x0b\x09\x09\x0d\x11\x0d" + "\x0e\x0f\x10\x10\x11\x10\x0a\x0c\x12\x13\x12\x10\x13\x0f\x10\x10\x10\xff" + "\xc9\x00\x0b\x08\x00\x01\x00\x01\x01\x01\x11\x00\xff\xcc\x00\x06\x00\x10" + "\x10\x05\xff\xda\x00\x08\x01\x01\x00\x00\x3f\x00\xd2\xcf\x20\xff\xd9", + 125)}, + {"mimeType", "image/jpeg"}, + {"description", "Embedded cover"}, + {"pictureType", "Front Cover"} +}; + +} // namespace + +class TestComplexProperties : public CppUnit::TestFixture +{ + CPPUNIT_TEST_SUITE(TestComplexProperties); + CPPUNIT_TEST(testReadMp3Picture); + CPPUNIT_TEST(testReadM4aPicture); + CPPUNIT_TEST(testReadOggPicture); + CPPUNIT_TEST(testReadWriteFlacPicture); + CPPUNIT_TEST(testReadWriteMultipleProperties); + CPPUNIT_TEST(testSetGetId3Geob); + CPPUNIT_TEST(testSetGetId3Picture); + CPPUNIT_TEST(testSetGetApePicture); + CPPUNIT_TEST(testSetGetAsfPicture); + CPPUNIT_TEST(testSetGetMp4Picture); + CPPUNIT_TEST(testSetGetXiphPicture); + CPPUNIT_TEST(testNonExistent); + CPPUNIT_TEST_SUITE_END(); + +public: + void testReadMp3Picture() + { + if(zlib::isAvailable()) { + FileRef f(TEST_FILE_PATH_C("compressed_id3_frame.mp3"), false); + CPPUNIT_ASSERT_EQUAL(StringList(PICTURE_KEY), + f.file()->complexPropertyKeys()); + auto pictures = f.file()->complexProperties(PICTURE_KEY); + CPPUNIT_ASSERT_EQUAL(1U, pictures.size()); + auto picture = pictures.front(); + CPPUNIT_ASSERT_EQUAL(86414U, + picture.value("data").value().size()); + CPPUNIT_ASSERT_EQUAL(String(""), + picture.value("description").value()); + CPPUNIT_ASSERT_EQUAL(String("image/bmp"), + picture.value("mimeType").value()); + CPPUNIT_ASSERT_EQUAL(String("Other"), + picture.value("pictureType").value()); + } + } + + void testReadM4aPicture() + { + const ByteVector expectedData1( + "\x89\x50\x4e\x47\x0d\x0a\x1a\x0a\x00\x00\x00\x0d\x49\x48\x44\x52\x00\x00" + "\x00\x02\x00\x00\x00\x02\x08\x02\x00\x00\x00\xfd\xd4\x9a\x73\x00\x00\x00" + "\x16\x49\x44\x41\x54\x78\x9c\x63\x7c\x9f\xca\xc0\xc0\xc0\xc0\xc4\xc0\xc0" + "\xc0\xc0\xc0\x00\x00\x11\x09\x01\x58\xab\x88\xdb\x6f\x00\x00\x00\x00\x49" + "\x45\x4e\x44\xae\x42\x60\x82", 79); + const ByteVector expectedData2( + "\xff\xd8\xff\xe0\x00\x10\x4a\x46\x49\x46\x00\x01\x01\x01\x00\x64\x00\x64" + "\x00\x00\xff\xdb\x00\x43\x00\x09\x06\x07\x08\x07\x06\x09\x08\x08\x08\x0a" + "\x0a\x09\x0b\x0e\x17\x0f\x0e\x0d\x0d\x0e\x1c\x14\x15\x11\x17\x22\x1e\x23" + "\x23\x21\x1e\x20\x20\x25\x2a\x35\x2d\x25\x27\x32\x28\x20\x20\x2e\x3f\x2f" + "\x32\x37\x39\x3c\x3c\x3c\x24\x2d\x42\x46\x41\x3a\x46\x35\x3b\x3c\x39\xff" + "\xdb\x00\x43\x01\x0a\x0a\x0a\x0e\x0c\x0e\x1b\x0f\x0f\x1b\x39\x26\x20\x26" + "\x39\x39\x39\x39\x39\x39\x39\x39\x39\x39\x39\x39\x39\x39\x39\x39\x39\x39" + "\x39\x39\x39\x39\x39\x39\x39\x39\x39\x39\x39\x39\x39\x39\x39\x39\x39\x39" + "\x39\x39\x39\x39\x39\x39\x39\x39\x39\x39\x39\x39\x39\x39\xff\xc0\x00\x11" + "\x08\x00\x02\x00\x02\x03\x01\x22\x00\x02\x11\x01\x03\x11\x01\xff\xc4\x00" + "\x15\x00\x01\x01\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00" + "\x00\x07\xff\xc4\x00\x14\x10\x01\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00" + "\x00\x00\x00\x00\x00\x00\xff\xc4\x00\x15\x01\x01\x01\x00\x00\x00\x00\x00" + "\x00\x00\x00\x00\x00\x00\x00\x00\x00\x04\x06\xff\xc4\x00\x14\x11\x01\x00" + "\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\xff\xda\x00" + "\x0c\x03\x01\x00\x02\x11\x03\x11\x00\x3f\x00\x8d\x80\xb8\x19\xff\xd9", 287); + + FileRef f(TEST_FILE_PATH_C("has-tags.m4a"), false); + CPPUNIT_ASSERT_EQUAL(StringList(PICTURE_KEY), + f.file()->complexPropertyKeys()); + auto pictures = f.file()->complexProperties(PICTURE_KEY); + CPPUNIT_ASSERT_EQUAL(2U, pictures.size()); + auto picture = pictures.front(); + CPPUNIT_ASSERT_EQUAL(expectedData1, + picture.value("data").value()); + CPPUNIT_ASSERT_EQUAL(String("image/png"), + picture.value("mimeType").value()); + picture = pictures.back(); + CPPUNIT_ASSERT_EQUAL(expectedData2, + picture.value("data").value()); + CPPUNIT_ASSERT_EQUAL(String("image/jpeg"), + picture.value("mimeType").value()); + } + + void testReadOggPicture() + { + FileRef f(TEST_FILE_PATH_C("lowercase-fields.ogg"), false); + CPPUNIT_ASSERT_EQUAL(StringList(PICTURE_KEY), + f.file()->complexPropertyKeys()); + auto pictures = f.file()->complexProperties(PICTURE_KEY); + CPPUNIT_ASSERT_EQUAL(1U, pictures.size()); + auto picture = pictures.front(); + CPPUNIT_ASSERT_EQUAL(ByteVector("JPEG data"), + picture.value("data").value()); + CPPUNIT_ASSERT_EQUAL(String("image/jpeg"), + picture.value("mimeType").value()); + CPPUNIT_ASSERT_EQUAL(String("Back Cover"), + picture.value("pictureType").value()); + CPPUNIT_ASSERT_EQUAL(String("new image"), + picture.value("description").value()); + CPPUNIT_ASSERT_EQUAL(16, picture.value("colorDepth").value()); + CPPUNIT_ASSERT_EQUAL(7, picture.value("numColors").value()); + CPPUNIT_ASSERT_EQUAL(5, picture.value("width").value()); + CPPUNIT_ASSERT_EQUAL(6, picture.value("height").value()); + } + + void testReadWriteFlacPicture() + { + VariantMap picture(TEST_PICTURE); + picture.insert("colorDepth", 8); + picture.insert("numColors", 1); + picture.insert("width", 1); + picture.insert("height", 1); + + ScopedFileCopy copy("no-tags", ".flac"); + + { + FLAC::File f(copy.fileName().c_str(), false); + CPPUNIT_ASSERT(f.complexPropertyKeys().isEmpty()); + CPPUNIT_ASSERT(f.pictureList().isEmpty()); + CPPUNIT_ASSERT(f.setComplexProperties(PICTURE_KEY, {picture})); + f.save(); + } + { + FLAC::File f(copy.fileName().c_str(), false); + CPPUNIT_ASSERT_EQUAL(StringList(PICTURE_KEY), f.complexPropertyKeys()); + CPPUNIT_ASSERT_EQUAL(picture, f.complexProperties(PICTURE_KEY).front()); + auto flacPictures = f.pictureList(); + CPPUNIT_ASSERT_EQUAL(1U, flacPictures.size()); + auto flacPicture = flacPictures.front(); + CPPUNIT_ASSERT_EQUAL(picture.value("data").value(), + flacPicture->data()); + CPPUNIT_ASSERT_EQUAL(picture.value("mimeType").value(), + flacPicture->mimeType()); + CPPUNIT_ASSERT_EQUAL(FLAC::Picture::FrontCover, flacPicture->type()); + CPPUNIT_ASSERT_EQUAL(picture.value("description").value(), + flacPicture->description()); + CPPUNIT_ASSERT_EQUAL(picture.value("colorDepth").value(), + flacPicture->colorDepth()); + CPPUNIT_ASSERT_EQUAL(picture.value("numColors").value(), + flacPicture->numColors()); + CPPUNIT_ASSERT_EQUAL(picture.value("width").value(), + flacPicture->width()); + CPPUNIT_ASSERT_EQUAL(picture.value("height").value(), + flacPicture->height()); + + CPPUNIT_ASSERT(f.setComplexProperties(PICTURE_KEY, {})); + f.save(); + } + { + FLAC::File f(copy.fileName().c_str(), false); + CPPUNIT_ASSERT(f.complexPropertyKeys().isEmpty()); + CPPUNIT_ASSERT(f.pictureList().isEmpty()); + } + } + + void testReadWriteMultipleProperties() + { + const VariantMap picture2 { + {"data", ByteVector("PNG data")}, + {"mimeType", "image/png"}, + {"description", ""}, + {"pictureType", "Back Cover"} + }; + const VariantMap geob1 { + {"data", ByteVector("First")}, + {"mimeType", "text/plain"}, + {"description", "Object 1"}, + {"fileName", "test1.txt"} + }; + const VariantMap geob2 { + {"data", ByteVector("Second")}, + {"mimeType", "text/plain"}, + {"description", "Object 2"}, + {"fileName", "test2.txt"} + }; + + ScopedFileCopy copy("xing", ".mp3"); + + { + FileRef f(copy.fileName().c_str(), false); + CPPUNIT_ASSERT(f.file()->complexPropertyKeys().isEmpty()); + f.file()->setComplexProperties(PICTURE_KEY, {TEST_PICTURE, picture2}); + f.file()->setComplexProperties(GEOB_KEY, {geob1, geob2}); + f.file()->save(); + } + { + FileRef f(copy.fileName().c_str(), false); + CPPUNIT_ASSERT_EQUAL(StringList({PICTURE_KEY, GEOB_KEY}), + f.file()->complexPropertyKeys()); + CPPUNIT_ASSERT(List({TEST_PICTURE, picture2}) == + f.file()->complexProperties(PICTURE_KEY)); + CPPUNIT_ASSERT(List({geob1, geob2}) == + f.file()->complexProperties(GEOB_KEY)); + } + } + + void testSetGetId3Geob() + { + const VariantMap geob { + {"data", ByteVector("Just a test")}, + {"mimeType", "text/plain"}, + {"description", "Embedded object"}, + {"fileName", "test.txt"} + }; + ID3v2::Tag tag; + CPPUNIT_ASSERT(!tag.frameListMap().contains("GEOB")); + CPPUNIT_ASSERT(tag.complexPropertyKeys().isEmpty()); + CPPUNIT_ASSERT(tag.complexProperties(GEOB_KEY).isEmpty()); + CPPUNIT_ASSERT(tag.setComplexProperties(GEOB_KEY, {geob})); + CPPUNIT_ASSERT_EQUAL(StringList(GEOB_KEY), tag.complexPropertyKeys()); + CPPUNIT_ASSERT_EQUAL(geob, tag.complexProperties(GEOB_KEY).front()); + auto frames = tag.frameListMap().value("GEOB"); + CPPUNIT_ASSERT_EQUAL(1U, frames.size()); + auto frame = + dynamic_cast(frames.front()); + CPPUNIT_ASSERT(frame); + CPPUNIT_ASSERT_EQUAL(geob.value("data").value(), frame->object()); + CPPUNIT_ASSERT_EQUAL(geob.value("mimeType").value(), frame->mimeType()); + CPPUNIT_ASSERT_EQUAL(geob.value("description").value(), frame->description()); + CPPUNIT_ASSERT_EQUAL(geob.value("fileName").value(), frame->fileName()); + } + + void tagSetGetPicture(Tag &tag, const VariantMap &picture) + { + CPPUNIT_ASSERT(tag.complexPropertyKeys().isEmpty()); + CPPUNIT_ASSERT(tag.complexProperties(PICTURE_KEY).isEmpty()); + CPPUNIT_ASSERT(tag.setComplexProperties(PICTURE_KEY, {picture})); + CPPUNIT_ASSERT_EQUAL(StringList(PICTURE_KEY), tag.complexPropertyKeys()); + CPPUNIT_ASSERT_EQUAL(picture, tag.complexProperties(PICTURE_KEY).front()); + } + + void testSetGetId3Picture() + { + const VariantMap picture(TEST_PICTURE); + ID3v2::Tag tag; + CPPUNIT_ASSERT(!tag.frameListMap().contains("APIC")); + tagSetGetPicture(tag, picture); + auto frames = tag.frameListMap().value("APIC"); + CPPUNIT_ASSERT_EQUAL(1U, frames.size()); + auto frame = + dynamic_cast(frames.front()); + CPPUNIT_ASSERT(frame); + CPPUNIT_ASSERT_EQUAL(picture.value("data").value(), frame->picture()); + CPPUNIT_ASSERT_EQUAL(picture.value("mimeType").value(), frame->mimeType()); + CPPUNIT_ASSERT_EQUAL(picture.value("description").value(), frame->description()); + CPPUNIT_ASSERT_EQUAL(ID3v2::AttachedPictureFrame::FrontCover, frame->type()); + } + + void testSetGetApePicture() + { + const String FRONT_COVER("COVER ART (FRONT)"); + VariantMap picture(TEST_PICTURE); + picture.erase("mimeType"); + APE::Tag tag; + CPPUNIT_ASSERT(!tag.itemListMap().contains(FRONT_COVER)); + tagSetGetPicture(tag, picture); + auto item = tag.itemListMap().value(FRONT_COVER); + CPPUNIT_ASSERT_EQUAL( + picture.value("description").value().data(String::UTF8) + .append('\0') + .append(picture.value("data").value()), + item.binaryData()); + } + + void testSetGetAsfPicture() + { + VariantMap picture(TEST_PICTURE); + ASF::Tag tag; + CPPUNIT_ASSERT(!tag.attributeListMap().contains("WM/Picture")); + tagSetGetPicture(tag, picture); + auto attributes = tag.attribute("WM/Picture"); + CPPUNIT_ASSERT_EQUAL(1U, attributes.size()); + auto asfPicture = attributes.front().toPicture(); + CPPUNIT_ASSERT_EQUAL(picture.value("data").value(), + asfPicture.picture()); + CPPUNIT_ASSERT_EQUAL(picture.value("mimeType").value(), + asfPicture.mimeType()); + CPPUNIT_ASSERT_EQUAL(picture.value("description").value(), + asfPicture.description()); + CPPUNIT_ASSERT_EQUAL(ASF::Picture::FrontCover, asfPicture.type()); + } + + void testSetGetMp4Picture() + { + VariantMap picture(TEST_PICTURE); + picture.erase("description"); + picture.erase("pictureType"); + MP4::Tag tag; + CPPUNIT_ASSERT(!tag.itemMap().contains("covr")); + tagSetGetPicture(tag, picture); + auto covrs = tag.item("covr").toCoverArtList(); + CPPUNIT_ASSERT_EQUAL(1U, covrs.size()); + auto covr = covrs.front(); + CPPUNIT_ASSERT_EQUAL(picture.value("data").value(), + covr.data()); + CPPUNIT_ASSERT_EQUAL(MP4::CoverArt::JPEG, covr.format()); + } + + void testSetGetXiphPicture() + { + VariantMap picture(TEST_PICTURE); + picture.insert("colorDepth", 8); + picture.insert("numColors", 1); + picture.insert("width", 1); + picture.insert("height", 1); + Ogg::XiphComment tag; + CPPUNIT_ASSERT(tag.pictureList().isEmpty()); + tagSetGetPicture(tag, picture); + auto pics = tag.pictureList(); + CPPUNIT_ASSERT_EQUAL(1U, pics.size()); + auto pic = pics.front(); + CPPUNIT_ASSERT_EQUAL(picture.value("data").value(), + pic->data()); + CPPUNIT_ASSERT_EQUAL(picture.value("mimeType").value(), + pic->mimeType()); + CPPUNIT_ASSERT_EQUAL(picture.value("description").value(), + pic->description()); + CPPUNIT_ASSERT_EQUAL(FLAC::Picture::FrontCover, pic->type()); + CPPUNIT_ASSERT_EQUAL(8, pic->colorDepth()); + CPPUNIT_ASSERT_EQUAL(1, pic->numColors()); + CPPUNIT_ASSERT_EQUAL(1, pic->width()); + CPPUNIT_ASSERT_EQUAL(1, pic->height()); + } + + void testNonExistent() + { + { + ID3v2::Tag tag; + CPPUNIT_ASSERT(tag.complexPropertyKeys().isEmpty()); + CPPUNIT_ASSERT(tag.complexProperties(PICTURE_KEY).isEmpty()); + CPPUNIT_ASSERT(tag.complexProperties(GEOB_KEY).isEmpty()); + CPPUNIT_ASSERT(tag.complexProperties("NONEXISTENT").isEmpty()); + CPPUNIT_ASSERT(!tag.setComplexProperties("NONEXISTENT", {{{"description", "test"}}})); + CPPUNIT_ASSERT(tag.complexProperties("NONEXISTENT").isEmpty()); + CPPUNIT_ASSERT(tag.setComplexProperties(PICTURE_KEY, {TEST_PICTURE})); + CPPUNIT_ASSERT(!tag.complexProperties(PICTURE_KEY).isEmpty()); + } + { + ID3v1::Tag tag; + CPPUNIT_ASSERT(tag.complexPropertyKeys().isEmpty()); + CPPUNIT_ASSERT(tag.complexProperties(PICTURE_KEY).isEmpty()); + CPPUNIT_ASSERT(tag.complexProperties(GEOB_KEY).isEmpty()); + CPPUNIT_ASSERT(tag.complexProperties("NONEXISTENT").isEmpty()); + CPPUNIT_ASSERT(!tag.setComplexProperties("NONEXISTENT", {{{"description", "test"}}})); + CPPUNIT_ASSERT(tag.complexProperties("NONEXISTENT").isEmpty()); + CPPUNIT_ASSERT(!tag.setComplexProperties(PICTURE_KEY, {TEST_PICTURE})); + CPPUNIT_ASSERT(tag.complexProperties(PICTURE_KEY).isEmpty()); + } + } +}; + +CPPUNIT_TEST_SUITE_REGISTRATION(TestComplexProperties);