From 98bc68d16efe2538f8ea27b09550a45e9030efb2 Mon Sep 17 00:00:00 2001 From: Urs Fleisch Date: Tue, 19 Aug 2025 16:47:04 +0200 Subject: [PATCH] Implement complex properties for Matroska --- taglib/matroska/matroskaattachedfile.h | 4 +- taglib/matroska/matroskaattachments.cpp | 5 + taglib/matroska/matroskaattachments.h | 5 +- taglib/matroska/matroskacues.h | 4 +- taglib/matroska/matroskaelement.h | 4 +- taglib/matroska/matroskafile.cpp | 129 +++++++++++++++++++++++- taglib/matroska/matroskafile.h | 107 ++++++++++++++++++-- taglib/matroska/matroskasimpletag.h | 4 +- taglib/matroska/matroskatag.cpp | 67 ++++++++++++ taglib/matroska/matroskatag.h | 24 +++++ 10 files changed, 336 insertions(+), 17 deletions(-) diff --git a/taglib/matroska/matroskaattachedfile.h b/taglib/matroska/matroskaattachedfile.h index 4eeaaa78..c28e889e 100644 --- a/taglib/matroska/matroskaattachedfile.h +++ b/taglib/matroska/matroskaattachedfile.h @@ -18,8 +18,8 @@ * http://www.mozilla.org/MPL/ * ***************************************************************************/ -#ifndef HAS_MATROSKAATTACHEDFILE_H -#define HAS_MATROSKAATTACHEDFILE_H +#ifndef TAGLIB_MATROSKAATTACHEDFILE_H +#define TAGLIB_MATROSKAATTACHEDFILE_H #include "taglib_export.h" diff --git a/taglib/matroska/matroskaattachments.cpp b/taglib/matroska/matroskaattachments.cpp index 8fb34700..b04ace77 100644 --- a/taglib/matroska/matroskaattachments.cpp +++ b/taglib/matroska/matroskaattachments.cpp @@ -53,6 +53,11 @@ const Matroska::Attachments::AttachedFileList &Matroska::Attachments::attachedFi return d->files; } +Matroska::Attachments::AttachedFileList &Matroska::Attachments::attachedFiles() +{ + return d->files; +} + bool Matroska::Attachments::render() { EBML::MkAttachments attachments; diff --git a/taglib/matroska/matroskaattachments.h b/taglib/matroska/matroskaattachments.h index a49e7b61..4489cabb 100644 --- a/taglib/matroska/matroskaattachments.h +++ b/taglib/matroska/matroskaattachments.h @@ -18,8 +18,8 @@ * http://www.mozilla.org/MPL/ * ***************************************************************************/ -#ifndef HAS_MATROSKAATTACHMENTS_H -#define HAS_MATROSKAATTACHMENTS_H +#ifndef TAGLIB_MATROSKAATTACHMENTS_H +#define TAGLIB_MATROSKAATTACHMENTS_H #include #include "taglib_export.h" @@ -56,6 +56,7 @@ namespace TagLib { // private Element implementation bool render() override; + AttachedFileList &attachedFiles(); TAGLIB_MSVC_SUPPRESS_WARNING_NEEDS_TO_HAVE_DLL_INTERFACE std::unique_ptr d; diff --git a/taglib/matroska/matroskacues.h b/taglib/matroska/matroskacues.h index 61539413..192599f3 100644 --- a/taglib/matroska/matroskacues.h +++ b/taglib/matroska/matroskacues.h @@ -18,8 +18,8 @@ * http://www.mozilla.org/MPL/ * ***************************************************************************/ -#ifndef HAS_MATROSKACUES_H -#define HAS_MATROSKACUES_H +#ifndef TAGLIB_MATROSKACUES_H +#define TAGLIB_MATROSKACUES_H #ifndef DO_NOT_DOCUMENT #include "tlist.h" diff --git a/taglib/matroska/matroskaelement.h b/taglib/matroska/matroskaelement.h index c5c3aec0..9c1f19c3 100644 --- a/taglib/matroska/matroskaelement.h +++ b/taglib/matroska/matroskaelement.h @@ -18,8 +18,8 @@ * http://www.mozilla.org/MPL/ * ***************************************************************************/ -#ifndef HAS_MATROSKAELEMENT_H -#define HAS_MATROSKAELEMENT_H +#ifndef TAGLIB_MATROSKAELEMENT_H +#define TAGLIB_MATROSKAELEMENT_H #ifndef DO_NOT_DOCUMENT #include diff --git a/taglib/matroska/matroskafile.cpp b/taglib/matroska/matroskafile.cpp index 9651a1a2..ac9c7fca 100644 --- a/taglib/matroska/matroskafile.cpp +++ b/taglib/matroska/matroskafile.cpp @@ -21,6 +21,7 @@ #include "matroskafile.h" #include "matroskatag.h" #include "matroskaattachments.h" +#include "matroskaattachedfile.h" #include "matroskaseekhead.h" #include "matroskasegment.h" #include "ebmlutils.h" @@ -29,9 +30,9 @@ #include "tlist.h" #include "tdebug.h" #include "tagutils.h" +#include "tpropertymap.h" #include -#include using namespace TagLib; @@ -110,6 +111,132 @@ Matroska::Tag *Matroska::File::tag(bool create) const return d->tag.get(); } +PropertyMap Matroska::File::properties() const +{ + return d->tag ? d->tag->properties() : PropertyMap(); +} + +void Matroska::File::removeUnsupportedProperties(const StringList &properties) +{ + if(d->tag) { + d->tag->removeUnsupportedProperties(properties); + } +} + +PropertyMap Matroska::File::setProperties(const PropertyMap &properties) +{ + if(!d->tag) { + d->tag = std::make_unique(); + } + return d->tag->setProperties(properties); +} + +namespace { + + String keyForAttachedFile(const Matroska::AttachedFile *attachedFile) + { + if(!attachedFile) { + return {}; + } + if(attachedFile->mediaType().startsWith("image/")) { + return "PICTURE"; + } + if(!attachedFile->mediaType().isEmpty()) { + return attachedFile->mediaType(); + } + if(!attachedFile->fileName().isEmpty()) { + return attachedFile->fileName(); + } + return String::fromLongLong(attachedFile->uid()); + } + + unsigned long long stringToULongLong(const String &str, bool *ok) + { + const wchar_t *beginPtr = str.toCWString(); + wchar_t *endPtr; + errno = 0; + const unsigned long long value = ::wcstoull(beginPtr, &endPtr, 10); + if(ok) { + *ok = errno == 0 && endPtr > beginPtr && *endPtr == L'\0'; + } + return value; + } + +} + +StringList Matroska::File::complexPropertyKeys() const +{ + StringList keys = TagLib::File::complexPropertyKeys(); + if(d->attachments) { + const auto &attachedFiles = d->attachments->attachedFileList(); + for(const auto &attachedFile : attachedFiles) { + String key = keyForAttachedFile(attachedFile); + if(!key.isEmpty() && !keys.contains(key)) { + keys.append(key); + } + } + } + return keys; +} + +List Matroska::File::complexProperties(const String &key) const +{ + List props = TagLib::File::complexProperties(key); + if(d->attachments) { + const auto &attachedFiles = d->attachments->attachedFileList(); + for(const auto &attachedFile : attachedFiles) { + if(keyForAttachedFile(attachedFile) == key) { + VariantMap property; + property.insert("data", attachedFile->data()); + property.insert("mimeType", attachedFile->mediaType()); + property.insert("description", attachedFile->description()); + property.insert("fileName", attachedFile->fileName()); + property.insert("uid", attachedFile->uid()); + props.append(property); + } + } + } + return props; +} + +bool Matroska::File::setComplexProperties(const String &key, const List &value) +{ + if(TagLib::File::setComplexProperties(key, value)) { + return true; + } + + attachments(true)->clear(); + for(const auto &property : value) { + auto mimeType = property.value("mimeType").value(); + auto data = property.value("data").value(); + auto fileName = property.value("fileName").value(); + auto uid = property.value("uid").value(); + bool ok; + unsigned long long uidKey; + if(key.upper() == "PICTURE" && !mimeType.startsWith("image/")) { + mimeType = data.startsWith("\x89PNG\x0d\x0a\x1a\x0a") + ? "image/png" : "image/jpeg"; + } + else if(mimeType.isEmpty() && key.find("/") != -1) { + mimeType = key; + } + else if(fileName.isEmpty() && key.find(".") != -1) { + fileName = key; + } + else if(!uid && ((uidKey = stringToULongLong(key, &ok))) && ok) { + uid = uidKey; + } + auto attachedFile = new AttachedFile; + attachedFile->setData(data); + attachedFile->setMediaType(mimeType); + attachedFile->setDescription(property.value("description").value()); + attachedFile->setFileName(fileName); + attachedFile->setUID(uid); + d->attachments->addAttachedFile(attachedFile); + } + return true; +} + Matroska::Attachments *Matroska::File::attachments(bool create) const { if(!d->attachments && create) diff --git a/taglib/matroska/matroskafile.h b/taglib/matroska/matroskafile.h index cb9088b9..7ac23fa3 100644 --- a/taglib/matroska/matroskafile.h +++ b/taglib/matroska/matroskafile.h @@ -18,8 +18,8 @@ * http://www.mozilla.org/MPL/ * ***************************************************************************/ -#ifndef HAS_MATROSKAFILE_H -#define HAS_MATROSKAFILE_H +#ifndef TAGLIB_MATROSKAFILE_H +#define TAGLIB_MATROSKAFILE_H #include "taglib_export.h" #include "tfile.h" @@ -30,23 +30,118 @@ namespace TagLib::Matroska { class Properties; class Tag; class Attachments; + + /*! + * Implementation of TagLib::File for Matroska. + */ class TAGLIB_EXPORT File : public TagLib::File { public: + /*! + * Constructs a Matroska file from \a file. If \a readProperties is \c true the + * file's audio properties will also be read. + * + * The \a readStyle parameter is currently unused. + */ explicit File(FileName file, bool readProperties = true, Properties::ReadStyle readStyle = Properties::Average); + + /*! + * Constructs a Matroska file from \a stream. If \a readProperties is \c true the + * file's audio properties will also be read. + * + * The \a readStyle parameter is currently unused. + */ explicit File(IOStream *stream, bool readProperties = true, Properties::ReadStyle readStyle = Properties::Average); + + /*! + * Destroys this instance of the File. + */ ~File() override; + File(const File &) = delete; File &operator=(const File &) = delete; - AudioProperties *audioProperties() const override; + + /*! + * Returns a pointer to the tag of the file. + * + * It will create a tag if one does not exist and returns a valid pointer. + * + * \note The tag is still owned by the Matroska::File and should not + * be deleted by the user. It will be deleted when the file (object) is + * destroyed. + */ TagLib::Tag *tag() const override; - Attachments *attachments(bool create = false) const; + + /*! + * Returns a pointer to the Matroska tag of the file. + * + * If \a create is \c false this may return a null pointer if there is no tag. + * If \a create is \c true it will create a tag if one does not exist and + * returns a valid pointer. + * + * \note The tag is still owned by the Matroska::File and should not + * be deleted by the user. It will be deleted when the file (object) + * destroyed. + */ Tag *tag(bool create) const; + + /*! + * Implements the reading part of the unified property interface. + */ + PropertyMap properties() const override; + + void removeUnsupportedProperties(const StringList &properties) override; + + /*! + * Implements the writing part of the unified tag dictionary interface. + */ + PropertyMap setProperties(const PropertyMap &) override; + + /*! + * Returns the keys for attached files, "PICTURE" for images, the media + * type, file name or UID for other attached files. + * The names of the binary simple tags are included too. + */ + StringList complexPropertyKeys() const override; + + /*! + * Get the pictures stored in the attachments as complex properties + * for \a key "PICTURE". Other attached files can be retrieved, by + * media type, file name or UID. + * The attached files are returned as maps with keys "data", "mimeType", + * "description", "fileName, "uid". + * Binary simple tags can be retrieved as maps with keys "data", "name", + * "targetTypeValue", "language", "defaultLanguage". + */ + List complexProperties(const String &key) const override; + + /*! + * Set attached files as complex properties \a value, e.g. pictures for + * \a key "PICTURE" with the maps in \a value having keys "data", "mimeType", + * "description", "fileName, "uid". For other attached files, the mime type, + * file name or UID can be used as the \a key. + * Maps with keys "name" (with the same value as \a key) and "data" are + * stored as binary simple tags with additional keys "targetTypeValue", + * "language", "defaultLanguage". + */ + bool setComplexProperties(const String &key, const List &value) override; + + /*! + * Returns the Matroska::Properties for this file. If no audio properties + * were read then this will return a null pointer. + */ + AudioProperties *audioProperties() const override; + + /*! + * Save the file. + * + * This returns \c true if the save was successful. + */ bool save() override; - //PropertyMap properties() const override { return PropertyMap(); } - //void removeUnsupportedProperties(const StringList &properties) override { } + + Attachments *attachments(bool create = false) const; /*! * Returns whether or not the given \a stream can be opened as a Matroska diff --git a/taglib/matroska/matroskasimpletag.h b/taglib/matroska/matroskasimpletag.h index 326b25d3..5026f8fb 100644 --- a/taglib/matroska/matroskasimpletag.h +++ b/taglib/matroska/matroskasimpletag.h @@ -18,8 +18,8 @@ * http://www.mozilla.org/MPL/ * ***************************************************************************/ -#ifndef HAS_MATROSKASIMPLETAG_H -#define HAS_MATROSKASIMPLETAG_H +#ifndef TAGLIB_MATROSKASIMPLETAG_H +#define TAGLIB_MATROSKASIMPLETAG_H #include #include "tag.h" diff --git a/taglib/matroska/matroskatag.cpp b/taglib/matroska/matroskatag.cpp index d7982b8a..b70a45c8 100644 --- a/taglib/matroska/matroskatag.cpp +++ b/taglib/matroska/matroskatag.cpp @@ -430,6 +430,73 @@ PropertyMap Matroska::Tag::setProperties(const PropertyMap &propertyMap) return unsupportedProperties; } +void Matroska::Tag::removeUnsupportedProperties(const StringList& properties) +{ + d->removeSimpleTags( + [&properties](const SimpleTag* t) { + return properties.contains(t->name()); + } + ); +} + +StringList Matroska::Tag::complexPropertyKeys() const +{ + StringList keys; + for(const SimpleTag *t : std::as_const(d->tags)) { + if(auto tBinary = dynamic_cast(t)) { + keys.append(tBinary->name()); + } + } + return keys; +} + +List Matroska::Tag::complexProperties(const String& key) const +{ + List props; + if(key.upper() != "PICTURE") { // Pictures are handled at the file level + for(const SimpleTag *t : std::as_const(d->tags)) { + if(auto tBinary = dynamic_cast(t)) { + VariantMap property; + property.insert("data", tBinary->value()); + property.insert("name", tBinary->name()); + property.insert("targetTypeValue", tBinary->targetTypeValue()); + property.insert("language", tBinary->language()); + property.insert("defaultLanguage", tBinary->defaultLanguageFlag()); + props.append(property); + } + } + } + return TagLib::Tag::complexProperties(key); +} + +bool Matroska::Tag::setComplexProperties(const String& key, const List& value) +{ + if(key.upper() == "PICTURE") { + // Pictures are handled at the file level + return false; + } + d->removeSimpleTags( + [&key](const SimpleTag* t) { + return t->name() == key && dynamic_cast(t) != nullptr; + } + ); + bool result = false; + for(const auto &property : value) { + if(property.value("name").value() == key && property.contains("data")) { + auto *t = new SimpleTagBinary; + t->setTargetTypeValue(static_cast( + property.value("targetTypeValue", 0).value())); + t->setName(key); + t->setValue(property.value("data").value()); + t->setLanguage(property.value("language").value()); + t->setDefaultLanguageFlag(property.value("defaultLanguage", true).value()); + d->tags.append(t); + result = true; + } + } + return result; +} + PropertyMap Matroska::Tag::properties() const { PropertyMap properties; diff --git a/taglib/matroska/matroskatag.h b/taglib/matroska/matroskatag.h index 5f4c0db2..5c90a986 100644 --- a/taglib/matroska/matroskatag.h +++ b/taglib/matroska/matroskatag.h @@ -63,6 +63,30 @@ namespace TagLib { bool isEmpty() const override; PropertyMap properties() const override; PropertyMap setProperties(const PropertyMap &propertyMap) override; + void removeUnsupportedProperties(const StringList& properties) override; + + /*! + * Returns the names of the binary simple tags. + */ + StringList complexPropertyKeys() const override; + + /*! + * Get the binary simple tags as maps with keys "data", "name", + * "targetTypeValue", "language", "defaultLanguage". + * The attached files such as pictures with key "PICTURE" are available + * with Matroska::File::complexProperties(). + */ + List complexProperties(const String& key) const override; + + /*! + * Set the binary simple tags as maps with keys "data", "name", + * "targetTypeValue", "language", "defaultLanguage". + * The attached files such as pictures with key "PICTURE" can be set + * with Matroska::File::setComplexProperties(). + * + * Returns \c true if \c key can be stored as binary simple tags. + */ + bool setComplexProperties(const String& key, const List& value) override; void addSimpleTag(SimpleTag *tag); void removeSimpleTag(SimpleTag *tag);