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"}
      }
    });
This commit is contained in:
Urs Fleisch 2023-10-07 09:42:25 +02:00
parent 75d4252480
commit 6be03b7ae1
28 changed files with 1300 additions and 135 deletions

View File

@ -27,6 +27,8 @@
#include <cstdio>
#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<TagLib::ByteVector>().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<TagLib::ByteVector>();
picture.close();
*/
}
else {
cout << value << endl;
}
}
}
}
}
if(!f.isNull() && f.audioProperties()) {

View File

@ -24,6 +24,8 @@
#include <iostream>
#include <iomanip>
#include <fstream>
#include <sstream>
#include <cstring>
#include <cstdio>
#include <cstdlib>
@ -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 <tagname> <tagvalue>" << endl;
cout << " -I <tagname> <tagvalue>" << endl;
cout << " -D <tagname>" << endl;
cout << " -p <picturefile> <description> (\"\" \"\" 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<TagLib::FileRef>::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();

View File

@ -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

View File

@ -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<VariantMap> APE::Tag::complexProperties(const String &key) const
{
List<VariantMap> 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<VariantMap> &value)
{
const String uppercaseKey = key.upper();
if(uppercaseKey == "PICTURE") {
removeItem(FRONT_COVER);
removeItem(BACK_COVER);
auto frontItems = List<Item>();
auto backItems = List<Item>();
for(auto property : value) {
ByteVector data = property.value("description").value<String>().data(String::UTF8)
.append('\0')
.append(property.value("data").value<ByteVector>());
String pictureType = property.value("pictureType").value<String>();
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)

View File

@ -130,6 +130,10 @@ namespace TagLib {
*/
PropertyMap setProperties(const PropertyMap &) override;
StringList complexPropertyKeys() const override;
List<VariantMap> complexProperties(const String &key) const override;
bool setComplexProperties(const String &key, const List<VariantMap> &value) override;
/*!
* Check if the given String is a valid APE tag key.
*/

View File

@ -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.

View File

@ -29,6 +29,8 @@
#include <utility>
#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<VariantMap> ASF::Tag::complexProperties(const String &key) const
{
List<VariantMap> 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<VariantMap> &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<ByteVector>());
picture.setMimeType(property.value("mimeType").value<String>());
picture.setDescription(property.value("description").value<String>());
picture.setType(ASF::Picture::typeFromString(
property.value("pictureType").value<String>()));
addAttribute("WM/Picture", Attribute(picture));
}
}
else {
return false;
}
return true;
}

View File

@ -204,6 +204,10 @@ namespace TagLib {
void removeUnsupportedProperties(const StringList &props) override;
PropertyMap setProperties(const PropertyMap &props) override;
StringList complexPropertyKeys() const override;
List<VariantMap> complexProperties(const String &key) const override;
bool setComplexProperties(const String &key, const List<VariantMap> &value) override;
private:
class TagPrivate;

View File

@ -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<Picture *>(block) != nullptr) {
keys.append("PICTURE");
break;
}
}
}
return keys;
}
List<VariantMap> FLAC::File::complexProperties(const String &key) const
{
const String uppercaseKey = key.upper();
if(uppercaseKey == "PICTURE") {
List<VariantMap> properties;
for(const auto &block : std::as_const(d->blocks)) {
if(auto picture = dynamic_cast<Picture *>(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<VariantMap> &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<ByteVector>());
picture->setMimeType(property.value("mimeType").value<String>());
picture->setDescription(property.value("description").value<String>());
picture->setType(FLAC::Picture::typeFromString(
property.value("pictureType").value<String>()));
picture->setWidth(property.value("width").value<int>());
picture->setHeight(property.value("height").value<int>());
picture->setNumColors(property.value("numColors").value<int>());
picture->setColorDepth(property.value("colorDepth").value<int>());
addPicture(picture);
}
}
else {
return TagLib::File::setComplexProperties(key, value);
}
return true;
}
FLAC::Properties *FLAC::File::audioProperties() const
{
return d->properties.get();

View File

@ -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<VariantMap> 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<VariantMap> &value) override;
/*!
* Returns the FLAC::Properties for this file. If no audio properties
* were read then this will return a null pointer.

View File

@ -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);

View File

@ -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<VariantMap> MP4::Tag::complexProperties(const String &key) const
{
List<VariantMap> 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<VariantMap> &value)
{
const String uppercaseKey = key.upper();
if(uppercaseKey == "PICTURE") {
CoverArtList pictures;
for(auto property : value) {
String mimeType = property.value("mimeType").value<String>();
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<ByteVector>()));
}
d->items["covr"] = pictures;
}
else {
return false;
}
return true;
}
void MP4::Tag::addItem(const String &name, const Item &value)
{
if(!d->items.contains(name)) {

View File

@ -102,6 +102,10 @@ namespace TagLib {
void removeUnsupportedProperties(const StringList &props) override;
PropertyMap setProperties(const PropertyMap &props) override;
StringList complexPropertyKeys() const override;
List<VariantMap> complexProperties(const String &key) const override;
bool setComplexProperties(const String &key, const List<VariantMap> &value) override;
protected:
/*!
* Sets the value of \a key to \a value, overwriting any previous value.

View File

@ -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

View File

@ -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<VariantMap> ID3v2::Tag::complexProperties(const String &key) const
{
List<VariantMap> 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<const AttachedPictureFrame *>(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<const GeneralEncapsulatedObjectFrame *>(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<VariantMap> &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<ByteVector>());
picture->setMimeType(property.value("mimeType").value<String>());
picture->setDescription(property.value("description").value<String>());
picture->setType(AttachedPictureFrame::typeFromString(
property.value("pictureType").value<String>()));
addFrame(picture);
}
}
else if(uppercaseKey == "GENERALOBJECT") {
removeFrames("GEOB");
for(auto property : value) {
auto geob = new GeneralEncapsulatedObjectFrame;
geob->setObject(property.value("data").value<ByteVector>());
geob->setMimeType(property.value("mimeType").value<String>());
geob->setDescription(property.value("description").value<String>());
geob->setFileName(property.value("fileName").value<String>());
addFrame(geob);
}
}
else {
return false;
}
return true;
}
ByteVector ID3v2::Tag::render() const
{
return render(ID3v2::v4);

View File

@ -329,6 +329,10 @@ namespace TagLib {
*/
PropertyMap setProperties(const PropertyMap &) override;
StringList complexPropertyKeys() const override;
List<VariantMap> complexProperties(const String &key) const override;
bool setComplexProperties(const String &key, const List<VariantMap> &value) override;
/*!
* Render the tag back to binary data, suitable to be written to disk.
*/

View File

@ -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<VariantMap> Ogg::XiphComment::complexProperties(const String &key) const
{
List<VariantMap> 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<VariantMap> &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<ByteVector>());
picture->setMimeType(property.value("mimeType").value<String>());
picture->setDescription(property.value("description").value<String>());
picture->setType(FLAC::Picture::typeFromString(
property.value("pictureType").value<String>()));
picture->setWidth(property.value("width").value<int>());
picture->setHeight(property.value("height").value<int>());
picture->setNumColors(property.value("numColors").value<int>());
picture->setColorDepth(property.value("colorDepth").value<int>());
addPicture(picture);
}
}
else {
return false;
}
return true;
}
bool Ogg::XiphComment::checkKey(const String &key)
{
if(key.size() < 1)

View File

@ -167,6 +167,10 @@ namespace TagLib {
*/
PropertyMap setProperties(const PropertyMap&) override;
StringList complexPropertyKeys() const override;
List<VariantMap> complexProperties(const String &key) const override;
bool setComplexProperties(const String &key, const List<VariantMap> &value) override;
/*!
* Check if the given String is a valid Xiph comment key.
*/

View File

@ -146,6 +146,21 @@ PropertyMap Tag::setProperties(const PropertyMap &origProps)
return properties;
}
StringList Tag::complexPropertyKeys() const
{
return StringList();
}
List<VariantMap> Tag::complexProperties(const String &) const
{
return {};
}
bool Tag::setComplexProperties(const String &, const List<VariantMap> &)
{
return false;
}
void Tag::duplicate(const Tag *source, Tag *target, bool overwrite) // static
{
if(overwrite) {

View File

@ -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<VariantMap> 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<VariantMap> &value);
/*!
* Returns the track name; if no track name is present in the tag
* String::null will be returned.

View File

@ -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<VariantMap> TagUnion::complexProperties(const String &key) const
{
for(const auto &tag : d->tags) {
if(tag) {
const List<VariantMap> props = tag->complexProperties(key);
if(!props.isEmpty()) {
return props;
}
}
}
return List<VariantMap>();
}
bool TagUnion::setComplexProperties(const String &key, const List<VariantMap> &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);

View File

@ -62,6 +62,10 @@ namespace TagLib {
PropertyMap properties() const override;
void removeUnsupportedProperties(const StringList &unsupported) override;
StringList complexPropertyKeys() const override;
List<VariantMap> complexProperties(const String &key) const override;
bool setComplexProperties(const String &key, const List<VariantMap> &value) override;
String title() const override;
String artist() const override;
String album() const override;

View File

@ -106,6 +106,21 @@ PropertyMap File::setProperties(const PropertyMap &properties)
return tag()->setProperties(properties);
}
StringList File::complexPropertyKeys() const
{
return tag()->complexPropertyKeys();
}
List<VariantMap> File::complexProperties(const String &key) const
{
return tag()->complexProperties(key);
}
bool File::setComplexProperties(const String &key, const List<VariantMap> &value)
{
return tag()->setComplexProperties(key, value);
}
ByteVector File::readBlock(size_t length)
{
return d->stream->readBlock(length);

View File

@ -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<VariantMap> 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<VariantMap> &value);
/*!
* Returns a pointer to this file's audio properties. This should be
* reimplemented in the concrete subclasses. If no audio properties were

View File

@ -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<int>(std::size(typeStrs))) {
return typeStrs[type];
}
return "";
}
int Utils::pictureTypeFromString(String str)
{
for(int i = 0; i < static_cast<int>(std::size(typeStrs)); ++i) {
if(str == typeStrs[i]) {
return i;
}
}
return 0;
}

View File

@ -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<name>( \
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

View File

@ -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

View File

@ -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 <cppunit/extensions/HelperMacros.h>
#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<ByteVector>().size());
CPPUNIT_ASSERT_EQUAL(String(""),
picture.value("description").value<String>());
CPPUNIT_ASSERT_EQUAL(String("image/bmp"),
picture.value("mimeType").value<String>());
CPPUNIT_ASSERT_EQUAL(String("Other"),
picture.value("pictureType").value<String>());
}
}
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<ByteVector>());
CPPUNIT_ASSERT_EQUAL(String("image/png"),
picture.value("mimeType").value<String>());
picture = pictures.back();
CPPUNIT_ASSERT_EQUAL(expectedData2,
picture.value("data").value<ByteVector>());
CPPUNIT_ASSERT_EQUAL(String("image/jpeg"),
picture.value("mimeType").value<String>());
}
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<ByteVector>());
CPPUNIT_ASSERT_EQUAL(String("image/jpeg"),
picture.value("mimeType").value<String>());
CPPUNIT_ASSERT_EQUAL(String("Back Cover"),
picture.value("pictureType").value<String>());
CPPUNIT_ASSERT_EQUAL(String("new image"),
picture.value("description").value<String>());
CPPUNIT_ASSERT_EQUAL(16, picture.value("colorDepth").value<int>());
CPPUNIT_ASSERT_EQUAL(7, picture.value("numColors").value<int>());
CPPUNIT_ASSERT_EQUAL(5, picture.value("width").value<int>());
CPPUNIT_ASSERT_EQUAL(6, picture.value("height").value<int>());
}
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<ByteVector>(),
flacPicture->data());
CPPUNIT_ASSERT_EQUAL(picture.value("mimeType").value<String>(),
flacPicture->mimeType());
CPPUNIT_ASSERT_EQUAL(FLAC::Picture::FrontCover, flacPicture->type());
CPPUNIT_ASSERT_EQUAL(picture.value("description").value<String>(),
flacPicture->description());
CPPUNIT_ASSERT_EQUAL(picture.value("colorDepth").value<int>(),
flacPicture->colorDepth());
CPPUNIT_ASSERT_EQUAL(picture.value("numColors").value<int>(),
flacPicture->numColors());
CPPUNIT_ASSERT_EQUAL(picture.value("width").value<int>(),
flacPicture->width());
CPPUNIT_ASSERT_EQUAL(picture.value("height").value<int>(),
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<VariantMap>({TEST_PICTURE, picture2}) ==
f.file()->complexProperties(PICTURE_KEY));
CPPUNIT_ASSERT(List<VariantMap>({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<ID3v2::GeneralEncapsulatedObjectFrame *>(frames.front());
CPPUNIT_ASSERT(frame);
CPPUNIT_ASSERT_EQUAL(geob.value("data").value<ByteVector>(), frame->object());
CPPUNIT_ASSERT_EQUAL(geob.value("mimeType").value<String>(), frame->mimeType());
CPPUNIT_ASSERT_EQUAL(geob.value("description").value<String>(), frame->description());
CPPUNIT_ASSERT_EQUAL(geob.value("fileName").value<String>(), 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<ID3v2::AttachedPictureFrame *>(frames.front());
CPPUNIT_ASSERT(frame);
CPPUNIT_ASSERT_EQUAL(picture.value("data").value<ByteVector>(), frame->picture());
CPPUNIT_ASSERT_EQUAL(picture.value("mimeType").value<String>(), frame->mimeType());
CPPUNIT_ASSERT_EQUAL(picture.value("description").value<String>(), 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<String>().data(String::UTF8)
.append('\0')
.append(picture.value("data").value<ByteVector>()),
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<ByteVector>(),
asfPicture.picture());
CPPUNIT_ASSERT_EQUAL(picture.value("mimeType").value<String>(),
asfPicture.mimeType());
CPPUNIT_ASSERT_EQUAL(picture.value("description").value<String>(),
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<ByteVector>(),
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<ByteVector>(),
pic->data());
CPPUNIT_ASSERT_EQUAL(picture.value("mimeType").value<String>(),
pic->mimeType());
CPPUNIT_ASSERT_EQUAL(picture.value("description").value<String>(),
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);