From c083d7ce1583459382891e5827ca8b8a759ece80 Mon Sep 17 00:00:00 2001
From: Urs Fleisch <ufleisch@users.sourceforge.net>
Date: Thu, 23 Nov 2023 16:39:57 +0100
Subject: [PATCH] Clients can control supported MP4 atoms using an ItemFactory
 (#1175)

---
 taglib/CMakeLists.txt         |   2 +
 taglib/mp4/mp4file.cpp        |  23 +-
 taglib/mp4/mp4file.h          |  12 +-
 taglib/mp4/mp4item.h          |   2 +
 taglib/mp4/mp4itemfactory.cpp | 776 ++++++++++++++++++++++++++++++++++
 taglib/mp4/mp4itemfactory.h   | 256 +++++++++++
 taglib/mp4/mp4tag.cpp         | 606 ++------------------------
 taglib/mp4/mp4tag.h           |  35 +-
 tests/test_mp4.cpp            | 101 +++++
 9 files changed, 1195 insertions(+), 618 deletions(-)
 create mode 100644 taglib/mp4/mp4itemfactory.cpp
 create mode 100644 taglib/mp4/mp4itemfactory.h

diff --git a/taglib/CMakeLists.txt b/taglib/CMakeLists.txt
index 911ce058..59f6527e 100644
--- a/taglib/CMakeLists.txt
+++ b/taglib/CMakeLists.txt
@@ -126,6 +126,7 @@ set(tag_HDRS
   mp4/mp4item.h
   mp4/mp4properties.h
   mp4/mp4coverart.h
+  mp4/mp4itemfactory.h
   mod/modfilebase.h
   mod/modfile.h
   mod/modtag.h
@@ -221,6 +222,7 @@ set(mp4_SRCS
   mp4/mp4item.cpp
   mp4/mp4properties.cpp
   mp4/mp4coverart.cpp
+  mp4/mp4itemfactory.cpp
 )
 
 set(ape_SRCS
diff --git a/taglib/mp4/mp4file.cpp b/taglib/mp4/mp4file.cpp
index 02353718..f8e87fdb 100644
--- a/taglib/mp4/mp4file.cpp
+++ b/taglib/mp4/mp4file.cpp
@@ -29,6 +29,8 @@
 #include "tpropertymap.h"
 #include "tagutils.h"
 
+#include "mp4itemfactory.h"
+
 using namespace TagLib;
 
 namespace
@@ -43,6 +45,15 @@ namespace
 class MP4::File::FilePrivate
 {
 public:
+  FilePrivate(MP4::ItemFactory *mp4ItemFactory)
+        : itemFactory(mp4ItemFactory ? mp4ItemFactory
+                                     : MP4::ItemFactory::instance())
+  {
+  }
+
+  ~FilePrivate() = default;
+
+  const ItemFactory *itemFactory;
   std::unique_ptr<MP4::Tag> tag;
   std::unique_ptr<MP4::Atoms> atoms;
   std::unique_ptr<MP4::Properties> properties;
@@ -64,17 +75,19 @@ bool MP4::File::isSupported(IOStream *stream)
 // public members
 ////////////////////////////////////////////////////////////////////////////////
 
-MP4::File::File(FileName file, bool readProperties, AudioProperties::ReadStyle) :
+MP4::File::File(FileName file, bool readProperties, AudioProperties::ReadStyle,
+                ItemFactory *itemFactory) :
   TagLib::File(file),
-  d(std::make_unique<FilePrivate>())
+  d(std::make_unique<FilePrivate>(itemFactory))
 {
   if(isOpen())
     read(readProperties);
 }
 
-MP4::File::File(IOStream *stream, bool readProperties, AudioProperties::ReadStyle) :
+MP4::File::File(IOStream *stream, bool readProperties, AudioProperties::ReadStyle,
+                ItemFactory *itemFactory) :
   TagLib::File(stream),
-  d(std::make_unique<FilePrivate>())
+  d(std::make_unique<FilePrivate>(itemFactory))
 {
   if(isOpen())
     read(readProperties);
@@ -125,7 +138,7 @@ MP4::File::read(bool readProperties)
     return;
   }
 
-  d->tag = std::make_unique<Tag>(this, d->atoms.get());
+  d->tag = std::make_unique<Tag>(this, d->atoms.get(), d->itemFactory);
   if(readProperties) {
     d->properties = std::make_unique<Properties>(this, d->atoms.get());
   }
diff --git a/taglib/mp4/mp4file.h b/taglib/mp4/mp4file.h
index 76cd95cf..ad708909 100644
--- a/taglib/mp4/mp4file.h
+++ b/taglib/mp4/mp4file.h
@@ -36,6 +36,8 @@ namespace TagLib {
   //! An implementation of MP4 (AAC, ALAC, ...) metadata
   namespace MP4 {
     class Atoms;
+    class ItemFactory;
+
 
     /*!
      * This implements and provides an interface for MP4 files to the
@@ -64,9 +66,12 @@ namespace TagLib {
        * file's audio properties will also be read.
        *
        * \note In the current implementation, \a propertiesStyle is ignored.
+       *
+       * The items will be created using \a itemFactory (default if null).
        */
       File(FileName file, bool readProperties = true,
-           Properties::ReadStyle audioPropertiesStyle = Properties::Average);
+           Properties::ReadStyle audioPropertiesStyle = Properties::Average,
+           ItemFactory *itemFactory = nullptr);
 
       /*!
        * Constructs an MP4 file from \a stream.  If \a readProperties is true the
@@ -76,9 +81,12 @@ namespace TagLib {
        * responsible for deleting it after the File object.
        *
        * \note In the current implementation, \a propertiesStyle is ignored.
+       *
+       * The items will be created using \a itemFactory (default if null).
        */
       File(IOStream *stream, bool readProperties = true,
-           Properties::ReadStyle audioPropertiesStyle = Properties::Average);
+           Properties::ReadStyle audioPropertiesStyle = Properties::Average,
+           ItemFactory *itemFactory = nullptr);
 
       /*!
        * Destroys this instance of the File.
diff --git a/taglib/mp4/mp4item.h b/taglib/mp4/mp4item.h
index b54985be..1c6de582 100644
--- a/taglib/mp4/mp4item.h
+++ b/taglib/mp4/mp4item.h
@@ -83,6 +83,8 @@ namespace TagLib {
       class ItemPrivate;
       std::shared_ptr<ItemPrivate> d;
     };
+
+    using ItemMap = TagLib::Map<String, Item>;
   }  // namespace MP4
 }  // namespace TagLib
 #endif
diff --git a/taglib/mp4/mp4itemfactory.cpp b/taglib/mp4/mp4itemfactory.cpp
new file mode 100644
index 00000000..06127c8b
--- /dev/null
+++ b/taglib/mp4/mp4itemfactory.cpp
@@ -0,0 +1,776 @@
+/***************************************************************************
+    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 "mp4itemfactory.h"
+
+#include <utility>
+
+#include "tbytevector.h"
+#include "tdebug.h"
+
+#include "id3v1genres.h"
+
+using namespace TagLib;
+using namespace MP4;
+
+class ItemFactory::ItemFactoryPrivate
+{
+public:
+  NameHandlerMap handlerTypeForName;
+  Map<ByteVector, String> propertyKeyForName;
+  Map<String, ByteVector> nameForPropertyKey;
+};
+
+ItemFactory ItemFactory::factory;
+
+////////////////////////////////////////////////////////////////////////////////
+// public members
+////////////////////////////////////////////////////////////////////////////////
+
+ItemFactory *ItemFactory::instance()
+{
+  return &factory;
+}
+
+std::pair<String, Item> ItemFactory::parseItem(
+  const Atom *atom, const ByteVector &data) const
+{
+  auto handlerType = handlerTypeForName(atom->name);
+  switch(handlerType) {
+  case ItemHandlerType::Unknown:
+    break;
+  case ItemHandlerType::FreeForm:
+    return parseFreeForm(atom, data);
+  case ItemHandlerType::IntPair:
+  case ItemHandlerType::IntPairNoTrailing:
+    return parseIntPair(atom, data);
+  case ItemHandlerType::Bool:
+    return parseBool(atom, data);
+  case ItemHandlerType::Int:
+    return parseInt(atom, data);
+  case ItemHandlerType::TextOrInt:
+    return parseTextOrInt(atom, data);
+  case ItemHandlerType::UInt:
+    return parseUInt(atom, data);
+  case ItemHandlerType::LongLong:
+    return parseLongLong(atom, data);
+  case ItemHandlerType::Byte:
+    return parseByte(atom, data);
+  case ItemHandlerType::Gnre:
+    return parseGnre(atom, data);
+  case ItemHandlerType::Covr:
+    return parseCovr(atom, data);
+  case ItemHandlerType::TextImplicit:
+    return parseText(atom, data, -1);
+  case ItemHandlerType::Text:
+    return parseText(atom, data);
+  }
+  return {atom->name, Item()};
+}
+
+ByteVector ItemFactory::renderItem(
+  const String &itemName, const Item &item) const
+{
+  if(itemName.startsWith("----")) {
+    return renderFreeForm(itemName, item);
+  }
+  else {
+    const ByteVector name = itemName.data(String::Latin1);
+    auto handlerType = handlerTypeForName(name);
+    switch(handlerType) {
+    case ItemHandlerType::Unknown:
+      debug("MP4: Unknown item name \"" + name + "\"");
+      break;
+    case ItemHandlerType::FreeForm:
+      return renderFreeForm(name, item);
+    case ItemHandlerType::IntPair:
+      return renderIntPair(name, item);
+    case ItemHandlerType::IntPairNoTrailing:
+      return renderIntPairNoTrailing(name, item);
+    case ItemHandlerType::Bool:
+      return renderBool(name, item);
+    case ItemHandlerType::Int:
+      return renderInt(name, item);
+    case ItemHandlerType::TextOrInt:
+      return renderTextOrInt(name, item);
+    case ItemHandlerType::UInt:
+      return renderUInt(name, item);
+    case ItemHandlerType::LongLong:
+      return renderLongLong(name, item);
+    case ItemHandlerType::Byte:
+      return renderByte(name, item);
+    case ItemHandlerType::Gnre:
+      return renderInt(name, item);
+    case ItemHandlerType::Covr:
+      return renderCovr(name, item);
+    case ItemHandlerType::TextImplicit:
+      return renderText(name, item, TypeImplicit);
+    case ItemHandlerType::Text:
+      return renderText(name, item);
+    }
+  }
+  return ByteVector();
+}
+
+std::pair<ByteVector, Item> ItemFactory::itemFromProperty(
+  const String &key, const StringList &values) const
+{
+  ByteVector name = nameForPropertyKey(key);
+  if(!name.isEmpty()) {
+    if(values.isEmpty()) {
+      return {name, values};
+    }
+    auto handlerType = name.startsWith("----")
+      ? ItemHandlerType::FreeForm
+      : handlerTypeForName(name);
+    switch(handlerType) {
+    case ItemHandlerType::IntPair:
+    case ItemHandlerType::IntPairNoTrailing:
+      if(StringList parts = StringList::split(values.front(), "/");
+         !parts.isEmpty()) {
+        int first = parts[0].toInt();
+        int second = 0;
+        if(parts.size() > 1) {
+          second = parts[1].toInt();
+        }
+        return {name, Item(first, second)};
+      }
+      break;
+    case ItemHandlerType::Int:
+    case ItemHandlerType::Gnre:
+      return {name, Item(values.front().toInt())};
+    case ItemHandlerType::UInt:
+      return {name, Item(static_cast<unsigned int>(values.front().toInt()))};
+    case ItemHandlerType::LongLong:
+      return {name, Item(static_cast<long long>(values.front().toInt()))};
+    case ItemHandlerType::Byte:
+      return {name, Item(static_cast<unsigned char>(values.front().toInt()))};
+    case ItemHandlerType::Bool:
+      return {name, Item(values.front().toInt() != 0)};
+    case ItemHandlerType::FreeForm:
+    case ItemHandlerType::TextOrInt:
+    case ItemHandlerType::TextImplicit:
+    case ItemHandlerType::Text:
+      return {name, values};
+
+    case ItemHandlerType::Covr:
+      debug("MP4: Invalid item \"" + name + "\" for property");
+      break;
+    case ItemHandlerType::Unknown:
+      debug("MP4: Unknown item name \"" + name + "\" for property");
+      break;
+    }
+  }
+  return {name, Item()};
+}
+
+std::pair<String, StringList> ItemFactory::itemToProperty(
+  const ByteVector &itemName, const Item &item) const
+{
+  const String key = propertyKeyForName(itemName);
+  if(!key.isEmpty()) {
+    auto handlerType = itemName.startsWith("----")
+      ? ItemHandlerType::FreeForm
+      : handlerTypeForName(itemName);
+    switch(handlerType) {
+    case ItemHandlerType::IntPair:
+    case ItemHandlerType::IntPairNoTrailing:
+    {
+      auto [vn, tn] = item.toIntPair();
+      String value  = String::number(vn);
+      if(tn) {
+        value += "/" + String::number(tn);
+      }
+      return {key, value};
+    }
+    case ItemHandlerType::Int:
+    case ItemHandlerType::Gnre:
+      return {key, String::number(item.toInt())};
+    case ItemHandlerType::UInt:
+      return {key, String::number(item.toUInt())};
+    case ItemHandlerType::LongLong:
+      return {key, String::number(item.toLongLong())};
+    case ItemHandlerType::Byte:
+      return {key, String::number(item.toByte())};
+    case ItemHandlerType::Bool:
+      return {key, String::number(item.toBool())};
+    case ItemHandlerType::FreeForm:
+    case ItemHandlerType::TextOrInt:
+    case ItemHandlerType::TextImplicit:
+    case ItemHandlerType::Text:
+      return {key, item.toStringList()};
+
+    case ItemHandlerType::Covr:
+      debug("MP4: Invalid item \"" + itemName + "\" for property");
+      break;
+    case ItemHandlerType::Unknown:
+      debug("MP4: Unknown item name \"" + itemName + "\" for property");
+      break;
+    }
+  }
+  return {String(), StringList()};
+}
+
+String ItemFactory::propertyKeyForName(const ByteVector &name) const
+{
+  if(d->propertyKeyForName.isEmpty()) {
+    d->propertyKeyForName = namePropertyMap();
+  }
+  return d->propertyKeyForName.value(name);
+}
+
+ByteVector ItemFactory::nameForPropertyKey(const String &key) const
+{
+  if(d->nameForPropertyKey.isEmpty()) {
+    if(d->propertyKeyForName.isEmpty()) {
+      d->propertyKeyForName = namePropertyMap();
+    }
+    for(const auto &[k, t] : std::as_const(d->propertyKeyForName)) {
+      d->nameForPropertyKey[t] = k;
+    }
+  }
+  return d->nameForPropertyKey.value(key);
+}
+
+////////////////////////////////////////////////////////////////////////////////
+// protected members
+////////////////////////////////////////////////////////////////////////////////
+
+ItemFactory::ItemFactory() :
+  d(std::make_unique<ItemFactoryPrivate>())
+{
+}
+
+ItemFactory::~ItemFactory() = default;
+
+ItemFactory::NameHandlerMap ItemFactory::nameHandlerMap() const
+{
+  return {
+    {"----", ItemHandlerType::FreeForm},
+    {"trkn", ItemHandlerType::IntPair},
+    {"disk", ItemHandlerType::IntPairNoTrailing},
+    {"cpil", ItemHandlerType::Bool},
+    {"pgap", ItemHandlerType::Bool},
+    {"pcst", ItemHandlerType::Bool},
+    {"shwm", ItemHandlerType::Bool},
+    {"tmpo", ItemHandlerType::Int},
+    {"\251mvi", ItemHandlerType::Int},
+    {"\251mvc", ItemHandlerType::Int},
+    {"hdvd", ItemHandlerType::Int},
+    {"rate", ItemHandlerType::TextOrInt},
+    {"tvsn", ItemHandlerType::UInt},
+    {"tves", ItemHandlerType::UInt},
+    {"cnID", ItemHandlerType::UInt},
+    {"sfID", ItemHandlerType::UInt},
+    {"atID", ItemHandlerType::UInt},
+    {"geID", ItemHandlerType::UInt},
+    {"cmID", ItemHandlerType::UInt},
+    {"plID", ItemHandlerType::LongLong},
+    {"stik", ItemHandlerType::Byte},
+    {"rtng", ItemHandlerType::Byte},
+    {"akID", ItemHandlerType::Byte},
+    {"gnre", ItemHandlerType::Gnre},
+    {"covr", ItemHandlerType::Covr},
+    {"purl", ItemHandlerType::TextImplicit},
+    {"egid", ItemHandlerType::TextImplicit},
+  };
+}
+
+ItemFactory::ItemHandlerType ItemFactory::handlerTypeForName(
+  const ByteVector &name) const
+{
+  if(d->handlerTypeForName.isEmpty()) {
+    d->handlerTypeForName = nameHandlerMap();
+  }
+  auto type = d->handlerTypeForName.value(name, ItemHandlerType::Unknown);
+  if (type == ItemHandlerType::Unknown && name.size() == 4) {
+    type = ItemHandlerType::Text;
+  }
+  return type;
+}
+
+Map<ByteVector, String> ItemFactory::namePropertyMap() const
+{
+  return {
+    {"\251nam", "TITLE"},
+    {"\251ART", "ARTIST"},
+    {"\251alb", "ALBUM"},
+    {"\251cmt", "COMMENT"},
+    {"\251gen", "GENRE"},
+    {"\251day", "DATE"},
+    {"\251wrt", "COMPOSER"},
+    {"\251grp", "GROUPING"},
+    {"aART", "ALBUMARTIST"},
+    {"trkn", "TRACKNUMBER"},
+    {"disk", "DISCNUMBER"},
+    {"cpil", "COMPILATION"},
+    {"tmpo", "BPM"},
+    {"cprt", "COPYRIGHT"},
+    {"\251lyr", "LYRICS"},
+    {"\251too", "ENCODEDBY"},
+    {"soal", "ALBUMSORT"},
+    {"soaa", "ALBUMARTISTSORT"},
+    {"soar", "ARTISTSORT"},
+    {"sonm", "TITLESORT"},
+    {"soco", "COMPOSERSORT"},
+    {"sosn", "SHOWSORT"},
+    {"shwm", "SHOWWORKMOVEMENT"},
+    {"pgap", "GAPLESSPLAYBACK"},
+    {"pcst", "PODCAST"},
+    {"catg", "PODCASTCATEGORY"},
+    {"desc", "PODCASTDESC"},
+    {"egid", "PODCASTID"},
+    {"purl", "PODCASTURL"},
+    {"tves", "TVEPISODE"},
+    {"tven", "TVEPISODEID"},
+    {"tvnn", "TVNETWORK"},
+    {"tvsn", "TVSEASON"},
+    {"tvsh", "TVSHOW"},
+    {"\251wrk", "WORK"},
+    {"\251mvn", "MOVEMENTNAME"},
+    {"\251mvi", "MOVEMENTNUMBER"},
+    {"\251mvc", "MOVEMENTCOUNT"},
+    {"----:com.apple.iTunes:MusicBrainz Track Id", "MUSICBRAINZ_TRACKID"},
+    {"----:com.apple.iTunes:MusicBrainz Artist Id", "MUSICBRAINZ_ARTISTID"},
+    {"----:com.apple.iTunes:MusicBrainz Album Id", "MUSICBRAINZ_ALBUMID"},
+    {"----:com.apple.iTunes:MusicBrainz Album Artist Id", "MUSICBRAINZ_ALBUMARTISTID"},
+    {"----:com.apple.iTunes:MusicBrainz Release Group Id", "MUSICBRAINZ_RELEASEGROUPID"},
+    {"----:com.apple.iTunes:MusicBrainz Release Track Id", "MUSICBRAINZ_RELEASETRACKID"},
+    {"----:com.apple.iTunes:MusicBrainz Work Id", "MUSICBRAINZ_WORKID"},
+    {"----:com.apple.iTunes:MusicBrainz Album Release Country", "RELEASECOUNTRY"},
+    {"----:com.apple.iTunes:MusicBrainz Album Status", "RELEASESTATUS"},
+    {"----:com.apple.iTunes:MusicBrainz Album Type", "RELEASETYPE"},
+    {"----:com.apple.iTunes:ARTISTS", "ARTISTS"},
+    {"----:com.apple.iTunes:originaldate", "ORIGINALDATE"},
+    {"----:com.apple.iTunes:ASIN", "ASIN"},
+    {"----:com.apple.iTunes:LABEL", "LABEL"},
+    {"----:com.apple.iTunes:LYRICIST", "LYRICIST"},
+    {"----:com.apple.iTunes:CONDUCTOR", "CONDUCTOR"},
+    {"----:com.apple.iTunes:REMIXER", "REMIXER"},
+    {"----:com.apple.iTunes:ENGINEER", "ENGINEER"},
+    {"----:com.apple.iTunes:PRODUCER", "PRODUCER"},
+    {"----:com.apple.iTunes:DJMIXER", "DJMIXER"},
+    {"----:com.apple.iTunes:MIXER", "MIXER"},
+    {"----:com.apple.iTunes:SUBTITLE", "SUBTITLE"},
+    {"----:com.apple.iTunes:DISCSUBTITLE", "DISCSUBTITLE"},
+    {"----:com.apple.iTunes:MOOD", "MOOD"},
+    {"----:com.apple.iTunes:ISRC", "ISRC"},
+    {"----:com.apple.iTunes:CATALOGNUMBER", "CATALOGNUMBER"},
+    {"----:com.apple.iTunes:BARCODE", "BARCODE"},
+    {"----:com.apple.iTunes:SCRIPT", "SCRIPT"},
+    {"----:com.apple.iTunes:LANGUAGE", "LANGUAGE"},
+    {"----:com.apple.iTunes:LICENSE", "LICENSE"},
+    {"----:com.apple.iTunes:MEDIA", "MEDIA"}
+  };
+}
+
+MP4::AtomDataList ItemFactory::parseData2(
+  const MP4::Atom *atom, const ByteVector &data, int expectedFlags,
+  bool freeForm)
+{
+  AtomDataList result;
+  int i = 0;
+  unsigned int pos = 0;
+  while(pos < data.size()) {
+    const auto length = static_cast<int>(data.toUInt(pos));
+    if(length < 12) {
+      debug("MP4: Too short atom");
+      return result;
+    }
+
+    const ByteVector name = data.mid(pos + 4, 4);
+    const auto flags = static_cast<int>(data.toUInt(pos + 8));
+    if(freeForm && i < 2) {
+      if(i == 0 && name != "mean") {
+        debug("MP4: Unexpected atom \"" + name + "\", expecting \"mean\"");
+        return result;
+      }
+      if(i == 1 && name != "name") {
+        debug("MP4: Unexpected atom \"" + name + "\", expecting \"name\"");
+        return result;
+      }
+      result.append(AtomData(static_cast<AtomDataType>(flags),
+                    data.mid(pos + 12, length - 12)));
+    }
+    else {
+      if(name != "data") {
+        debug("MP4: Unexpected atom \"" + name + "\", expecting \"data\"");
+        return result;
+      }
+      if(expectedFlags == -1 || flags == expectedFlags) {
+        result.append(AtomData(static_cast<AtomDataType>(flags),
+                      data.mid(pos + 16, length - 16)));
+      }
+    }
+    pos += length;
+    i++;
+  }
+  return result;
+}
+
+ByteVectorList ItemFactory::parseData(
+  const MP4::Atom *atom, const ByteVector &bytes, int expectedFlags,
+  bool freeForm)
+{
+  const AtomDataList data = parseData2(atom, bytes, expectedFlags, freeForm);
+  ByteVectorList result;
+  for(const auto &atom : data) {
+    result.append(atom.data);
+  }
+  return result;
+}
+
+std::pair<String, Item> ItemFactory::parseInt(
+  const MP4::Atom *atom, const ByteVector &bytes)
+{
+  ByteVectorList data = parseData(atom, bytes);
+  return {
+    atom->name,
+    !data.isEmpty() ? Item(static_cast<int>(data[0].toShort())) : Item()
+  };
+}
+
+std::pair<String, Item> ItemFactory::parseTextOrInt(
+  const MP4::Atom *atom, const ByteVector &bytes)
+{
+  AtomDataList data = parseData2(atom, bytes);
+  if(!data.isEmpty()) {
+    AtomData val = data[0];
+    return {
+      atom->name,
+       val.type == TypeUTF8 ? Item(StringList(String(val.data, String::UTF8)))
+                            : Item(static_cast<int>(val.data.toShort()))
+    };
+  }
+  return {atom->name, Item()};
+}
+
+std::pair<String, Item> ItemFactory::parseUInt(
+  const MP4::Atom *atom, const ByteVector &bytes)
+{
+  ByteVectorList data = parseData(atom, bytes);
+  return {
+    atom->name,
+    !data.isEmpty() ? Item(data[0].toUInt()) : Item()
+  };
+}
+
+std::pair<String, Item> ItemFactory::parseLongLong(
+  const MP4::Atom *atom, const ByteVector &bytes)
+{
+  ByteVectorList data = parseData(atom, bytes);
+  return {
+    atom->name,
+    !data.isEmpty() ?Item (data[0].toLongLong()) : Item()
+  };
+}
+
+std::pair<String, Item> ItemFactory::parseByte(
+  const MP4::Atom *atom, const ByteVector &bytes)
+{
+  ByteVectorList data = parseData(atom, bytes);
+  return {
+    atom->name,
+    !data.isEmpty() ? Item(static_cast<unsigned char>(data[0].at(0))) : Item()
+  };
+}
+
+std::pair<String, Item> ItemFactory::parseGnre(
+  const MP4::Atom *atom, const ByteVector &bytes)
+{
+  ByteVectorList data = parseData(atom, bytes);
+  if(!data.isEmpty()) {
+    int idx = static_cast<int>(data[0].toShort());
+    if(idx > 0) {
+      return {
+        "\251gen",
+        Item(StringList(ID3v1::genre(idx - 1)))
+      };
+    }
+  }
+  return {"\251gen", Item()};
+}
+
+std::pair<String, Item> ItemFactory::parseIntPair(
+  const MP4::Atom *atom, const ByteVector &bytes)
+{
+  ByteVectorList data = parseData(atom, bytes);
+  if(!data.isEmpty()) {
+    const int a = data[0].toShort(2U);
+    const int b = data[0].toShort(4U);
+    return {atom->name, Item(a, b)};
+  }
+  return {atom->name, Item()};
+}
+
+std::pair<String, Item> ItemFactory::parseBool(
+  const MP4::Atom *atom, const ByteVector &bytes)
+{
+  ByteVectorList data = parseData(atom, bytes);
+  if(!data.isEmpty()) {
+    bool value = !data[0].isEmpty() && data[0][0] != '\0';
+    return {atom->name, Item(value)};
+  }
+  return {atom->name, Item()};
+}
+
+std::pair<String, Item> ItemFactory::parseText(
+  const MP4::Atom *atom, const ByteVector &bytes, int expectedFlags)
+{
+  const ByteVectorList data = parseData(atom, bytes, expectedFlags);
+  if(!data.isEmpty()) {
+    StringList value;
+    for(const auto &byte : data) {
+      value.append(String(byte, String::UTF8));
+    }
+    return {atom->name, Item(value)};
+  }
+  return {atom->name, Item()};
+}
+
+std::pair<String, Item> ItemFactory::parseFreeForm(
+  const MP4::Atom *atom, const ByteVector &bytes)
+{
+  const AtomDataList data = parseData2(atom, bytes, -1, true);
+  if(data.size() > 2) {
+    auto itBegin = data.begin();
+
+    String name = "----:";
+    name += String((itBegin++)->data, String::UTF8);  // data[0].data
+    name += ':';
+    name += String((itBegin++)->data, String::UTF8);  // data[1].data
+
+    AtomDataType type = itBegin->type; // data[2].type
+
+    for(auto it = itBegin; it != data.end(); ++it) {
+      if(it->type != type) {
+        debug("MP4: We currently don't support values with multiple types");
+        break;
+      }
+    }
+    if(type == TypeUTF8) {
+      StringList value;
+      for(auto it = itBegin; it != data.end(); ++it) {
+        value.append(String(it->data, String::UTF8));
+      }
+      Item item(value);
+      item.setAtomDataType(type);
+      return {name, item};
+    }
+    else {
+      ByteVectorList value;
+      for(auto it = itBegin; it != data.end(); ++it) {
+        value.append(it->data);
+      }
+      Item item(value);
+      item.setAtomDataType(type);
+      return {name, item};
+    }
+  }
+  return {atom->name, Item()};
+}
+
+std::pair<String, Item> ItemFactory::parseCovr(
+  const MP4::Atom *atom, const ByteVector &data)
+{
+  MP4::CoverArtList value;
+  unsigned int pos = 0;
+  while(pos < data.size()) {
+    const int length = static_cast<int>(data.toUInt(pos));
+    if(length < 12) {
+      debug("MP4: Too short atom");
+      break;
+    }
+
+    const ByteVector name = data.mid(pos + 4, 4);
+    const int flags = static_cast<int>(data.toUInt(pos + 8));
+    if(name != "data") {
+      debug("MP4: Unexpected atom \"" + name + "\", expecting \"data\"");
+      break;
+    }
+    if(flags == TypeJPEG || flags == TypePNG || flags == TypeBMP ||
+       flags == TypeGIF || flags == TypeImplicit) {
+      value.append(MP4::CoverArt(static_cast<MP4::CoverArt::Format>(flags),
+                                 data.mid(pos + 16, length - 16)));
+    }
+    else {
+      debug("MP4: Unknown covr format " + String::number(flags));
+    }
+    pos += length;
+  }
+  return {
+    atom->name,
+    !value.isEmpty() ? Item(value) : Item()
+  };
+}
+
+
+ByteVector ItemFactory::renderAtom(
+  const ByteVector &name, const ByteVector &data)
+{
+  return ByteVector::fromUInt(data.size() + 8) + name + data;
+}
+
+ByteVector ItemFactory::renderData(
+  const ByteVector &name, int flags, const ByteVectorList &data)
+{
+  ByteVector result;
+  for(const auto &byte : data) {
+    result.append(renderAtom("data", ByteVector::fromUInt(flags) +
+                                     ByteVector(4, '\0') + byte));
+  }
+  return renderAtom(name, result);
+}
+
+ByteVector ItemFactory::renderBool(
+  const ByteVector &name, const MP4::Item &item)
+{
+  ByteVectorList data;
+  data.append(ByteVector(1, item.toBool() ? '\1' : '\0'));
+  return renderData(name, TypeInteger, data);
+}
+
+ByteVector ItemFactory::renderInt(
+  const ByteVector &name, const MP4::Item &item)
+{
+  ByteVectorList data;
+  data.append(ByteVector::fromShort(item.toInt()));
+  return renderData(name, TypeInteger, data);
+}
+
+ByteVector ItemFactory::renderTextOrInt(
+  const ByteVector &name, const MP4::Item &item)
+{
+  StringList value = item.toStringList();
+  return value.isEmpty() ? renderInt(name, item) : renderText(name, item);
+}
+
+ByteVector ItemFactory::renderUInt(
+  const ByteVector &name, const MP4::Item &item)
+{
+  ByteVectorList data;
+  data.append(ByteVector::fromUInt(item.toUInt()));
+  return renderData(name, TypeInteger, data);
+}
+
+ByteVector ItemFactory::renderLongLong(
+  const ByteVector &name, const MP4::Item &item)
+{
+  ByteVectorList data;
+  data.append(ByteVector::fromLongLong(item.toLongLong()));
+  return renderData(name, TypeInteger, data);
+}
+
+ByteVector ItemFactory::renderByte(
+  const ByteVector &name, const MP4::Item &item)
+{
+  ByteVectorList data;
+  data.append(ByteVector(1, item.toByte()));
+  return renderData(name, TypeInteger, data);
+}
+
+ByteVector ItemFactory::renderIntPair(
+  const ByteVector &name, const MP4::Item &item)
+{
+  ByteVectorList data;
+  data.append(ByteVector(2, '\0') +
+              ByteVector::fromShort(item.toIntPair().first) +
+              ByteVector::fromShort(item.toIntPair().second) +
+              ByteVector(2, '\0'));
+  return renderData(name, TypeImplicit, data);
+}
+
+ByteVector ItemFactory::renderIntPairNoTrailing(
+  const ByteVector &name, const MP4::Item &item)
+{
+  ByteVectorList data;
+  data.append(ByteVector(2, '\0') +
+              ByteVector::fromShort(item.toIntPair().first) +
+              ByteVector::fromShort(item.toIntPair().second));
+  return renderData(name, TypeImplicit, data);
+}
+
+ByteVector ItemFactory::renderText(
+  const ByteVector &name, const MP4::Item &item, int flags)
+{
+  ByteVectorList data;
+  const StringList values = item.toStringList();
+  for(const auto &value : values) {
+    data.append(value.data(String::UTF8));
+  }
+  return renderData(name, flags, data);
+}
+
+ByteVector ItemFactory::renderCovr(
+  const ByteVector &name, const MP4::Item &item)
+{
+  ByteVector data;
+  const MP4::CoverArtList values = item.toCoverArtList();
+  for(const auto &value : values) {
+    data.append(renderAtom("data", ByteVector::fromUInt(value.format()) +
+                                   ByteVector(4, '\0') + value.data()));
+  }
+  return renderAtom(name, data);
+}
+
+ByteVector ItemFactory::renderFreeForm(
+  const String &name, const MP4::Item &item)
+{
+  StringList header = StringList::split(name, ":");
+  if(header.size() != 3) {
+    debug("MP4: Invalid free-form item name \"" + name + "\"");
+    return ByteVector();
+  }
+  ByteVector data;
+  data.append(renderAtom("mean", ByteVector::fromUInt(0) +
+                                 header[1].data(String::UTF8)));
+  data.append(renderAtom("name", ByteVector::fromUInt(0) +
+                                 header[2].data(String::UTF8)));
+  AtomDataType type = item.atomDataType();
+  if(type == TypeUndefined) {
+    if(!item.toStringList().isEmpty()) {
+      type = TypeUTF8;
+    }
+    else {
+      type = TypeImplicit;
+    }
+  }
+  if(type == TypeUTF8) {
+    const StringList values = item.toStringList();
+    for(const auto &value : values) {
+      data.append(renderAtom("data", ByteVector::fromUInt(type) +
+                                     ByteVector(4, '\0') +
+                                     value.data(String::UTF8)));
+    }
+  }
+  else {
+    const ByteVectorList values = item.toByteVectorList();
+    for(const auto &value : values) {
+      data.append(renderAtom("data", ByteVector::fromUInt(type) +
+                                     ByteVector(4, '\0') + value));
+    }
+  }
+  return renderAtom("----", data);
+}
diff --git a/taglib/mp4/mp4itemfactory.h b/taglib/mp4/mp4itemfactory.h
new file mode 100644
index 00000000..e908b347
--- /dev/null
+++ b/taglib/mp4/mp4itemfactory.h
@@ -0,0 +1,256 @@
+/***************************************************************************
+    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_MP4ITEMFACTORY_H
+#define TAGLIB_MP4ITEMFACTORY_H
+
+#include <memory>
+#include "taglib_export.h"
+#include "mp4item.h"
+
+namespace TagLib {
+
+  namespace MP4 {
+
+    //! A factory for creating MP4 items during parsing
+
+    /*!
+     * This factory abstracts away the parsing and rendering between atom data
+     * and MP4 items.
+     *
+     * Reimplementing this factory is the key to adding support for atom types
+     * not directly supported by TagLib to your application.  To do so you would
+     * subclass this factory and reimplement nameHandlerMap() to add support
+     * for new atoms.  If the new atoms do not have the same behavior as
+     * other supported atoms, it may be necessary to reimplement parseItem() and
+     * renderItem().  Then by setting your factory in the MP4::Tag constructor
+     * you can implement behavior that will allow for new atom types to be used.
+     *
+     * A custom item factory adding support for a "tsti" integer atom and a
+     * "tstt" text atom can be implemented like this:
+     *
+     * \code
+     * class CustomItemFactory : public MP4::ItemFactory {
+     * protected:
+     *   NameHandlerMap nameHandlerMap() const override
+     *   {
+     *     return MP4::ItemFactory::nameHandlerMap()
+     *       .insert("tsti", ItemHandlerType::Int)
+     *       .insert("tstt", ItemHandlerType::Text);
+     *   }
+     * };
+     * \endcode
+     *
+     * If the custom item shall also be accessible via a property,
+     * namePropertyMap() can be overridden in the same way.
+     */
+    class TAGLIB_EXPORT ItemFactory
+    {
+    public:
+      ItemFactory(const ItemFactory &) = delete;
+      ItemFactory &operator=(const ItemFactory &) = delete;
+
+      static ItemFactory *instance();
+
+      /*!
+       * Create an MP4 item from the \a data bytes of an \a atom.
+       * Returns the name (in most cases atom->name) and an item,
+       * an invalid item on failure.
+       * The default implementation uses the map returned by nameHandlerMap().
+       */
+      virtual std::pair<String, Item> parseItem(
+        const Atom *atom, const ByteVector &data) const;
+
+      /*!
+       * Render an MP4 \a item to the data bytes of an atom \a itemName.
+       * An empty byte vector is returned if the item is invalid or unknown.
+       * The default implementation uses the map returned by nameHandlerMap().
+       */
+      virtual ByteVector renderItem(
+        const String &itemName, const Item &item) const;
+
+      /*!
+       * Create an MP4 item from a property with \a key and \a values.
+       * If the property is not supported, an invalid item is returned.
+       * The default implementation uses the map returned by namePropertyMap().
+       */
+      virtual std::pair<ByteVector, Item> itemFromProperty(
+        const String &key, const StringList &values) const;
+
+      /*!
+       * Get an MP4 item as a property.
+       * If no property exists for \a itemName, an empty string is returned as
+       * the property key.
+       * The default implementation uses the map returned by namePropertyMap().
+       */
+      virtual std::pair<String, StringList> itemToProperty(
+        const ByteVector &itemName, const Item &item) const;
+
+      /*!
+       * Returns property key for atom \a name, empty if no property exists for
+       * this atom.
+       * The default method looks up the map created by namePropertyMap() and
+       * should be enough for most uses.
+       */
+      virtual String propertyKeyForName(const ByteVector &name) const;
+
+      /*!
+       * Returns atom name for property \a key, empty if no property is
+       * supported for this key.
+       * The default method uses the reverse mapping of propertyKeyForName()
+       * and should be enough for most uses.
+       */
+      virtual ByteVector nameForPropertyKey(const String &key) const;
+
+    protected:
+      /*!
+       * Type that determines the parsing and rendering between the data and
+       * the item representation of an atom.
+       */
+      enum class ItemHandlerType {
+        Unknown,
+        FreeForm,
+        IntPair,
+        IntPairNoTrailing,
+        Bool,
+        Int,
+        TextOrInt,
+        UInt,
+        LongLong,
+        Byte,
+        Gnre,
+        Covr,
+        TextImplicit,
+        Text
+      };
+
+      /*! Mapping of atom name to handler type. */
+      using NameHandlerMap = Map<ByteVector, ItemHandlerType>;
+
+      /*!
+       * Constructs an item factory.  Because this is a singleton this method is
+       * protected, but may be used for subclasses.
+       */
+      ItemFactory();
+
+      /*!
+       * Destroys the frame factory.
+       */
+      virtual ~ItemFactory();
+
+      /*!
+       * Returns mapping between atom names and handler types.
+       * This method is called once by handlerTypeForName() to initialize its
+       * internal cache.
+       * To add support for a new atom, it is sufficient in most cases to just
+       * reimplement this method and add new entries.
+       */
+      virtual NameHandlerMap nameHandlerMap() const;
+
+      /*!
+       * Returns handler type for atom \a name.
+       * The default method looks up the map created by nameHandlerMap() and
+       * should be enough for most uses.
+       */
+      virtual ItemHandlerType handlerTypeForName(const ByteVector &name) const;
+
+      /*!
+       * Returns mapping between atom names and property keys.
+       * This method is called once by propertyKeyForName() to initialize its
+       * internal cache.
+       * To add support for a new atom with a property, it is sufficient in most
+       * cases to just reimplement this method and add new entries.
+       */
+      virtual Map<ByteVector, String> namePropertyMap() const;
+
+      // Functions used by parseItem() to create items from atom data.
+      static MP4::AtomDataList parseData2(
+        const MP4::Atom *atom, const ByteVector &data, int expectedFlags = -1,
+        bool freeForm = false);
+      static ByteVectorList parseData(
+        const MP4::Atom *atom, const ByteVector &bytes, int expectedFlags = -1,
+        bool freeForm = false);
+      static std::pair<String, Item> parseText(
+        const MP4::Atom *atom, const ByteVector &bytes, int expectedFlags = 1);
+      static std::pair<String, Item> parseFreeForm(
+        const MP4::Atom *atom, const ByteVector &bytes);
+      static std::pair<String, Item> parseInt(
+        const MP4::Atom *atom, const ByteVector &bytes);
+      static std::pair<String, Item> parseByte(
+        const MP4::Atom *atom, const ByteVector &bytes);
+      static std::pair<String, Item> parseTextOrInt(
+        const MP4::Atom *atom, const ByteVector &bytes);
+      static std::pair<String, Item> parseUInt(
+        const MP4::Atom *atom, const ByteVector &bytes);
+      static std::pair<String, Item> parseLongLong(
+        const MP4::Atom *atom, const ByteVector &bytes);
+      static std::pair<String, Item> parseGnre(
+        const MP4::Atom *atom, const ByteVector &bytes);
+      static std::pair<String, Item> parseIntPair(
+        const MP4::Atom *atom, const ByteVector &bytes);
+      static std::pair<String, Item> parseBool(
+        const MP4::Atom *atom, const ByteVector &bytes);
+      static std::pair<String, Item> parseCovr(
+        const MP4::Atom *atom, const ByteVector &data);
+
+      // Functions used by renderItem() to render atom data for items.
+      static ByteVector renderAtom(
+        const ByteVector &name, const ByteVector &data);
+      static ByteVector renderData(
+        const ByteVector &name, int flags, const ByteVectorList &data);
+      static ByteVector renderText(
+        const ByteVector &name, const MP4::Item &item, int flags = TypeUTF8);
+      static ByteVector renderFreeForm(
+        const String &name, const MP4::Item &item);
+      static ByteVector renderBool(
+        const ByteVector &name, const MP4::Item &item);
+      static ByteVector renderInt(
+        const ByteVector &name, const MP4::Item &item);
+      static ByteVector renderTextOrInt(
+        const ByteVector &name, const MP4::Item &item);
+      static ByteVector renderByte(
+        const ByteVector &name, const MP4::Item &item);
+      static ByteVector renderUInt(
+        const ByteVector &name, const MP4::Item &item);
+      static ByteVector renderLongLong(
+        const ByteVector &name, const MP4::Item &item);
+      static ByteVector renderIntPair(
+        const ByteVector &name, const MP4::Item &item);
+      static ByteVector renderIntPairNoTrailing(
+        const ByteVector &name, const MP4::Item &item);
+      static ByteVector renderCovr(
+        const ByteVector &name, const MP4::Item &item);
+
+    private:
+      static ItemFactory factory;
+
+      class ItemFactoryPrivate;
+      std::unique_ptr<ItemFactoryPrivate> d;
+    };
+
+  }  // namespace MP4
+}  // namespace TagLib
+
+#endif
diff --git a/taglib/mp4/mp4tag.cpp b/taglib/mp4/mp4tag.cpp
index ce69e018..eef637a0 100644
--- a/taglib/mp4/mp4tag.cpp
+++ b/taglib/mp4/mp4tag.cpp
@@ -30,7 +30,7 @@
 
 #include "tdebug.h"
 #include "tpropertymap.h"
-#include "id3v1genres.h"
+#include "mp4itemfactory.h"
 #include "mp4atom.h"
 #include "mp4coverart.h"
 
@@ -39,18 +39,28 @@ using namespace TagLib;
 class MP4::Tag::TagPrivate
 {
 public:
+  TagPrivate(const ItemFactory *itemFactory) :
+    factory(itemFactory ? itemFactory
+                        : ItemFactory::instance())
+  {
+  }
+
+  ~TagPrivate() = default;
+
+  const ItemFactory *factory;
   TagLib::File *file { nullptr };
   Atoms *atoms { nullptr };
   ItemMap items;
 };
 
 MP4::Tag::Tag() :
-  d(std::make_unique<TagPrivate>())
+  d(std::make_unique<TagPrivate>(ItemFactory::instance()))
 {
 }
 
-MP4::Tag::Tag(TagLib::File *file, MP4::Atoms *atoms) :
-  d(std::make_unique<TagPrivate>())
+MP4::Tag::Tag(TagLib::File *file, MP4::Atoms *atoms,
+              const MP4::ItemFactory *factory) :
+  d(std::make_unique<TagPrivate>(factory))
 {
   d->file = file;
   d->atoms = atoms;
@@ -63,268 +73,16 @@ MP4::Tag::Tag(TagLib::File *file, MP4::Atoms *atoms) :
 
   for(const auto &atom : std::as_const(ilst->children)) {
     file->seek(atom->offset + 8);
-    if(atom->name == "----") {
-      parseFreeForm(atom);
-    }
-    else if(atom->name == "trkn" || atom->name == "disk") {
-      parseIntPair(atom);
-    }
-    else if(atom->name == "cpil" || atom->name == "pgap" || atom->name == "pcst" ||
-            atom->name == "shwm") {
-      parseBool(atom);
-    }
-    else if(atom->name == "tmpo" || atom->name == "\251mvi" || atom->name == "\251mvc" ||
-            atom->name == "hdvd") {
-      parseInt(atom);
-    }
-    else if(atom->name == "rate") {
-      AtomDataList data = parseData2(atom);
-      if(!data.isEmpty()) {
-        AtomData val = data[0];
-        if (val.type == TypeUTF8) {
-          addItem(atom->name, StringList(String(val.data, String::UTF8)));
-        } else {
-          addItem(atom->name, static_cast<int>(val.data.toShort()));
-        }
-      }
-    }
-    else if(atom->name == "tvsn" || atom->name == "tves" || atom->name == "cnID" ||
-            atom->name == "sfID" || atom->name == "atID" || atom->name == "geID" ||
-            atom->name == "cmID") {
-      parseUInt(atom);
-    }
-    else if(atom->name == "plID") {
-      parseLongLong(atom);
-    }
-    else if(atom->name == "stik" || atom->name == "rtng" || atom->name == "akID") {
-      parseByte(atom);
-    }
-    else if(atom->name == "gnre") {
-      parseGnre(atom);
-    }
-    else if(atom->name == "covr") {
-      parseCovr(atom);
-    }
-    else if(atom->name == "purl" || atom->name == "egid") {
-      parseText(atom, -1);
-    }
-    else {
-      parseText(atom);
+    ByteVector data = d->file->readBlock(atom->length - 8);
+    const auto &[name, item] = d->factory->parseItem(atom, data);
+    if (item.isValid()) {
+      addItem(name, item);
     }
   }
 }
 
 MP4::Tag::~Tag() = default;
 
-MP4::AtomDataList
-MP4::Tag::parseData2(const MP4::Atom *atom, int expectedFlags, bool freeForm)
-{
-  AtomDataList result;
-  ByteVector data = d->file->readBlock(atom->length - 8);
-  int i = 0;
-  unsigned int pos = 0;
-  while(pos < data.size()) {
-    const auto length = static_cast<int>(data.toUInt(pos));
-    if(length < 12) {
-      debug("MP4: Too short atom");
-      return result;
-    }
-
-    const ByteVector name = data.mid(pos + 4, 4);
-    const auto flags = static_cast<int>(data.toUInt(pos + 8));
-    if(freeForm && i < 2) {
-      if(i == 0 && name != "mean") {
-        debug("MP4: Unexpected atom \"" + name + "\", expecting \"mean\"");
-        return result;
-      }
-      if(i == 1 && name != "name") {
-        debug("MP4: Unexpected atom \"" + name + "\", expecting \"name\"");
-        return result;
-      }
-      result.append(AtomData(static_cast<AtomDataType>(flags), data.mid(pos + 12, length - 12)));
-    }
-    else {
-      if(name != "data") {
-        debug("MP4: Unexpected atom \"" + name + "\", expecting \"data\"");
-        return result;
-      }
-      if(expectedFlags == -1 || flags == expectedFlags) {
-        result.append(AtomData(static_cast<AtomDataType>(flags), data.mid(pos + 16, length - 16)));
-      }
-    }
-    pos += length;
-    i++;
-  }
-  return result;
-}
-
-ByteVectorList
-MP4::Tag::parseData(const MP4::Atom *atom, int expectedFlags, bool freeForm)
-{
-  const AtomDataList data = parseData2(atom, expectedFlags, freeForm);
-  ByteVectorList result;
-  for(const auto &atom : data) {
-    result.append(atom.data);
-  }
-  return result;
-}
-
-void
-MP4::Tag::parseInt(const MP4::Atom *atom)
-{
-  ByteVectorList data = parseData(atom);
-  if(!data.isEmpty()) {
-    addItem(atom->name, static_cast<int>(data[0].toShort()));
-  }
-}
-
-void
-MP4::Tag::parseUInt(const MP4::Atom *atom)
-{
-  ByteVectorList data = parseData(atom);
-  if(!data.isEmpty()) {
-    addItem(atom->name, data[0].toUInt());
-  }
-}
-
-void
-MP4::Tag::parseLongLong(const MP4::Atom *atom)
-{
-  ByteVectorList data = parseData(atom);
-  if(!data.isEmpty()) {
-    addItem(atom->name, data[0].toLongLong());
-  }
-}
-
-void
-MP4::Tag::parseByte(const MP4::Atom *atom)
-{
-  ByteVectorList data = parseData(atom);
-  if(!data.isEmpty()) {
-    addItem(atom->name, static_cast<unsigned char>(data[0].at(0)));
-  }
-}
-
-void
-MP4::Tag::parseGnre(const MP4::Atom *atom)
-{
-  ByteVectorList data = parseData(atom);
-  if(!data.isEmpty()) {
-    int idx = static_cast<int>(data[0].toShort());
-    if(idx > 0) {
-      addItem("\251gen", StringList(ID3v1::genre(idx - 1)));
-    }
-  }
-}
-
-void
-MP4::Tag::parseIntPair(const MP4::Atom *atom)
-{
-  ByteVectorList data = parseData(atom);
-  if(!data.isEmpty()) {
-    const int a = data[0].toShort(2U);
-    const int b = data[0].toShort(4U);
-    addItem(atom->name, MP4::Item(a, b));
-  }
-}
-
-void
-MP4::Tag::parseBool(const MP4::Atom *atom)
-{
-  ByteVectorList data = parseData(atom);
-  if(!data.isEmpty()) {
-    bool value = !data[0].isEmpty() && data[0][0] != '\0';
-    addItem(atom->name, value);
-  }
-}
-
-void
-MP4::Tag::parseText(const MP4::Atom *atom, int expectedFlags)
-{
-  const ByteVectorList data = parseData(atom, expectedFlags);
-  if(!data.isEmpty()) {
-    StringList value;
-    for(const auto &byte : data) {
-      value.append(String(byte, String::UTF8));
-    }
-    addItem(atom->name, value);
-  }
-}
-
-void
-MP4::Tag::parseFreeForm(const MP4::Atom *atom)
-{
-  const AtomDataList data = parseData2(atom, -1, true);
-  if(data.size() > 2) {
-    auto itBegin = data.begin();
-
-    String name = "----:";
-    name += String((itBegin++)->data, String::UTF8);  // data[0].data
-    name += ':';
-    name += String((itBegin++)->data, String::UTF8);  // data[1].data
-
-    AtomDataType type = itBegin->type; // data[2].type
-
-    for(auto it = itBegin; it != data.end(); ++it) {
-      if(it->type != type) {
-        debug("MP4: We currently don't support values with multiple types");
-        break;
-      }
-    }
-    if(type == TypeUTF8) {
-      StringList value;
-      for(auto it = itBegin; it != data.end(); ++it) {
-        value.append(String(it->data, String::UTF8));
-      }
-      Item item(value);
-      item.setAtomDataType(type);
-      addItem(name, item);
-    }
-    else {
-      ByteVectorList value;
-      for(auto it = itBegin; it != data.end(); ++it) {
-        value.append(it->data);
-      }
-      Item item(value);
-      item.setAtomDataType(type);
-      addItem(name, item);
-    }
-  }
-}
-
-void
-MP4::Tag::parseCovr(const MP4::Atom *atom)
-{
-  MP4::CoverArtList value;
-  ByteVector data = d->file->readBlock(atom->length - 8);
-  unsigned int pos = 0;
-  while(pos < data.size()) {
-    const int length = static_cast<int>(data.toUInt(pos));
-    if(length < 12) {
-      debug("MP4: Too short atom");
-      break;
-    }
-
-    const ByteVector name = data.mid(pos + 4, 4);
-    const int flags = static_cast<int>(data.toUInt(pos + 8));
-    if(name != "data") {
-      debug("MP4: Unexpected atom \"" + name + "\", expecting \"data\"");
-      break;
-    }
-    if(flags == TypeJPEG || flags == TypePNG || flags == TypeBMP ||
-       flags == TypeGIF || flags == TypeImplicit) {
-      value.append(MP4::CoverArt(static_cast<MP4::CoverArt::Format>(flags),
-                                 data.mid(pos + 16, length - 16)));
-    }
-    else {
-      debug("MP4: Unknown covr format " + String::number(flags));
-    }
-    pos += length;
-  }
-  if(!value.isEmpty())
-    addItem(atom->name, value);
-}
-
 ByteVector
 MP4::Tag::padIlst(const ByteVector &data, int length) const
 {
@@ -340,191 +98,12 @@ MP4::Tag::renderAtom(const ByteVector &name, const ByteVector &data) const
   return ByteVector::fromUInt(data.size() + 8) + name + data;
 }
 
-ByteVector
-MP4::Tag::renderData(const ByteVector &name, int flags, const ByteVectorList &data) const
-{
-  ByteVector result;
-  for(const auto &byte : data) {
-    result.append(renderAtom("data", ByteVector::fromUInt(flags) + ByteVector(4, '\0') + byte));
-  }
-  return renderAtom(name, result);
-}
-
-ByteVector
-MP4::Tag::renderBool(const ByteVector &name, const MP4::Item &item) const
-{
-  ByteVectorList data;
-  data.append(ByteVector(1, item.toBool() ? '\1' : '\0'));
-  return renderData(name, TypeInteger, data);
-}
-
-ByteVector
-MP4::Tag::renderInt(const ByteVector &name, const MP4::Item &item) const
-{
-  ByteVectorList data;
-  data.append(ByteVector::fromShort(item.toInt()));
-  return renderData(name, TypeInteger, data);
-}
-
-ByteVector
-MP4::Tag::renderUInt(const ByteVector &name, const MP4::Item &item) const
-{
-  ByteVectorList data;
-  data.append(ByteVector::fromUInt(item.toUInt()));
-  return renderData(name, TypeInteger, data);
-}
-
-ByteVector
-MP4::Tag::renderLongLong(const ByteVector &name, const MP4::Item &item) const
-{
-  ByteVectorList data;
-  data.append(ByteVector::fromLongLong(item.toLongLong()));
-  return renderData(name, TypeInteger, data);
-}
-
-ByteVector
-MP4::Tag::renderByte(const ByteVector &name, const MP4::Item &item) const
-{
-  ByteVectorList data;
-  data.append(ByteVector(1, item.toByte()));
-  return renderData(name, TypeInteger, data);
-}
-
-ByteVector
-MP4::Tag::renderIntPair(const ByteVector &name, const MP4::Item &item) const
-{
-  ByteVectorList data;
-  data.append(ByteVector(2, '\0') +
-              ByteVector::fromShort(item.toIntPair().first) +
-              ByteVector::fromShort(item.toIntPair().second) +
-              ByteVector(2, '\0'));
-  return renderData(name, TypeImplicit, data);
-}
-
-ByteVector
-MP4::Tag::renderIntPairNoTrailing(const ByteVector &name, const MP4::Item &item) const
-{
-  ByteVectorList data;
-  data.append(ByteVector(2, '\0') +
-              ByteVector::fromShort(item.toIntPair().first) +
-              ByteVector::fromShort(item.toIntPair().second));
-  return renderData(name, TypeImplicit, data);
-}
-
-ByteVector
-MP4::Tag::renderText(const ByteVector &name, const MP4::Item &item, int flags) const
-{
-  ByteVectorList data;
-  const StringList values = item.toStringList();
-  for(const auto &value : values) {
-    data.append(value.data(String::UTF8));
-  }
-  return renderData(name, flags, data);
-}
-
-ByteVector
-MP4::Tag::renderCovr(const ByteVector &name, const MP4::Item &item) const
-{
-  ByteVector data;
-  const MP4::CoverArtList values = item.toCoverArtList();
-  for(const auto &value : values) {
-    data.append(renderAtom("data", ByteVector::fromUInt(value.format()) +
-                                   ByteVector(4, '\0') + value.data()));
-  }
-  return renderAtom(name, data);
-}
-
-ByteVector
-MP4::Tag::renderFreeForm(const String &name, const MP4::Item &item) const
-{
-  StringList header = StringList::split(name, ":");
-  if(header.size() != 3) {
-    debug("MP4: Invalid free-form item name \"" + name + "\"");
-    return ByteVector();
-  }
-  ByteVector data;
-  data.append(renderAtom("mean", ByteVector::fromUInt(0) + header[1].data(String::UTF8)));
-  data.append(renderAtom("name", ByteVector::fromUInt(0) + header[2].data(String::UTF8)));
-  AtomDataType type = item.atomDataType();
-  if(type == TypeUndefined) {
-    if(!item.toStringList().isEmpty()) {
-      type = TypeUTF8;
-    }
-    else {
-      type = TypeImplicit;
-    }
-  }
-  if(type == TypeUTF8) {
-    const StringList values = item.toStringList();
-    for(const auto &value : values) {
-      data.append(renderAtom("data", ByteVector::fromUInt(type) +
-                                     ByteVector(4, '\0') + value.data(String::UTF8)));
-    }
-  }
-  else {
-    const ByteVectorList values = item.toByteVectorList();
-    for(const auto &value : values) {
-      data.append(renderAtom("data", ByteVector::fromUInt(type) +
-                                     ByteVector(4, '\0') + value));
-    }
-  }
-  return renderAtom("----", data);
-}
-
 bool
 MP4::Tag::save()
 {
   ByteVector data;
   for(const auto &[name, item] : std::as_const(d->items)) {
-    if(name.startsWith("----")) {
-      data.append(renderFreeForm(name, item));
-    }
-    else if(name == "trkn") {
-      data.append(renderIntPair(name.data(String::Latin1), item));
-    }
-    else if(name == "disk") {
-      data.append(renderIntPairNoTrailing(name.data(String::Latin1), item));
-    }
-    else if(name == "cpil" || name == "pgap" || name == "pcst" ||
-            name == "shwm") {
-      data.append(renderBool(name.data(String::Latin1), item));
-    }
-    else if(name == "tmpo" || name == "\251mvi" || name == "\251mvc" ||
-            name == "hdvd") {
-      data.append(renderInt(name.data(String::Latin1), item));
-    }
-    else if(name == "rate") {
-      StringList value = item.toStringList();
-      if (value.isEmpty()) {
-        data.append(renderInt(name.data(String::Latin1), item));
-      }
-      else {
-        data.append(renderText(name.data(String::Latin1), item));
-      }
-    }
-    else if(name == "tvsn" || name == "tves" || name == "cnID" ||
-            name == "sfID" || name == "atID" || name == "geID" ||
-            name == "cmID") {
-      data.append(renderUInt(name.data(String::Latin1), item));
-    }
-    else if(name == "plID") {
-      data.append(renderLongLong(name.data(String::Latin1), item));
-    }
-    else if(name == "stik" || name == "rtng" || name == "akID") {
-      data.append(renderByte(name.data(String::Latin1), item));
-    }
-    else if(name == "covr") {
-      data.append(renderCovr(name.data(String::Latin1), item));
-    }
-    else if(name == "purl" || name == "egid") {
-      data.append(renderText(name.data(String::Latin1), item, TypeImplicit));
-    }
-    else if(name.size() == 4){
-      data.append(renderText(name.data(String::Latin1), item));
-    }
-    else {
-      debug("MP4: Unknown item name \"" + name + "\"");
-    }
+    data.append(d->factory->renderItem(name, item));
   }
   data = renderAtom("ilst", data);
 
@@ -890,116 +469,13 @@ bool MP4::Tag::contains(const String &key) const
   return d->items.contains(key);
 }
 
-namespace
-{
-  constexpr std::array keyTranslation {
-    std::pair("\251nam", "TITLE"),
-    std::pair("\251ART", "ARTIST"),
-    std::pair("\251alb", "ALBUM"),
-    std::pair("\251cmt", "COMMENT"),
-    std::pair("\251gen", "GENRE"),
-    std::pair("\251day", "DATE"),
-    std::pair("\251wrt", "COMPOSER"),
-    std::pair("\251grp", "GROUPING"),
-    std::pair("aART", "ALBUMARTIST"),
-    std::pair("trkn", "TRACKNUMBER"),
-    std::pair("disk", "DISCNUMBER"),
-    std::pair("cpil", "COMPILATION"),
-    std::pair("tmpo", "BPM"),
-    std::pair("cprt", "COPYRIGHT"),
-    std::pair("\251lyr", "LYRICS"),
-    std::pair("\251too", "ENCODEDBY"),
-    std::pair("soal", "ALBUMSORT"),
-    std::pair("soaa", "ALBUMARTISTSORT"),
-    std::pair("soar", "ARTISTSORT"),
-    std::pair("sonm", "TITLESORT"),
-    std::pair("soco", "COMPOSERSORT"),
-    std::pair("sosn", "SHOWSORT"),
-    std::pair("shwm", "SHOWWORKMOVEMENT"),
-    std::pair("pgap", "GAPLESSPLAYBACK"),
-    std::pair("pcst", "PODCAST"),
-    std::pair("catg", "PODCASTCATEGORY"),
-    std::pair("desc", "PODCASTDESC"),
-    std::pair("egid", "PODCASTID"),
-    std::pair("purl", "PODCASTURL"),
-    std::pair("tves", "TVEPISODE"),
-    std::pair("tven", "TVEPISODEID"),
-    std::pair("tvnn", "TVNETWORK"),
-    std::pair("tvsn", "TVSEASON"),
-    std::pair("tvsh", "TVSHOW"),
-    std::pair("\251wrk", "WORK"),
-    std::pair("\251mvn", "MOVEMENTNAME"),
-    std::pair("\251mvi", "MOVEMENTNUMBER"),
-    std::pair("\251mvc", "MOVEMENTCOUNT"),
-    std::pair("----:com.apple.iTunes:MusicBrainz Track Id", "MUSICBRAINZ_TRACKID"),
-    std::pair("----:com.apple.iTunes:MusicBrainz Artist Id", "MUSICBRAINZ_ARTISTID"),
-    std::pair("----:com.apple.iTunes:MusicBrainz Album Id", "MUSICBRAINZ_ALBUMID"),
-    std::pair("----:com.apple.iTunes:MusicBrainz Album Artist Id", "MUSICBRAINZ_ALBUMARTISTID"),
-    std::pair("----:com.apple.iTunes:MusicBrainz Release Group Id", "MUSICBRAINZ_RELEASEGROUPID"),
-    std::pair("----:com.apple.iTunes:MusicBrainz Release Track Id", "MUSICBRAINZ_RELEASETRACKID"),
-    std::pair("----:com.apple.iTunes:MusicBrainz Work Id", "MUSICBRAINZ_WORKID"),
-    std::pair("----:com.apple.iTunes:MusicBrainz Album Release Country", "RELEASECOUNTRY"),
-    std::pair("----:com.apple.iTunes:MusicBrainz Album Status", "RELEASESTATUS"),
-    std::pair("----:com.apple.iTunes:MusicBrainz Album Type", "RELEASETYPE"),
-    std::pair("----:com.apple.iTunes:ARTISTS", "ARTISTS"),
-    std::pair("----:com.apple.iTunes:originaldate", "ORIGINALDATE"),
-    std::pair("----:com.apple.iTunes:ASIN", "ASIN"),
-    std::pair("----:com.apple.iTunes:LABEL", "LABEL"),
-    std::pair("----:com.apple.iTunes:LYRICIST", "LYRICIST"),
-    std::pair("----:com.apple.iTunes:CONDUCTOR", "CONDUCTOR"),
-    std::pair("----:com.apple.iTunes:REMIXER", "REMIXER"),
-    std::pair("----:com.apple.iTunes:ENGINEER", "ENGINEER"),
-    std::pair("----:com.apple.iTunes:PRODUCER", "PRODUCER"),
-    std::pair("----:com.apple.iTunes:DJMIXER", "DJMIXER"),
-    std::pair("----:com.apple.iTunes:MIXER", "MIXER"),
-    std::pair("----:com.apple.iTunes:SUBTITLE", "SUBTITLE"),
-    std::pair("----:com.apple.iTunes:DISCSUBTITLE", "DISCSUBTITLE"),
-    std::pair("----:com.apple.iTunes:MOOD", "MOOD"),
-    std::pair("----:com.apple.iTunes:ISRC", "ISRC"),
-    std::pair("----:com.apple.iTunes:CATALOGNUMBER", "CATALOGNUMBER"),
-    std::pair("----:com.apple.iTunes:BARCODE", "BARCODE"),
-    std::pair("----:com.apple.iTunes:SCRIPT", "SCRIPT"),
-    std::pair("----:com.apple.iTunes:LANGUAGE", "LANGUAGE"),
-    std::pair("----:com.apple.iTunes:LICENSE", "LICENSE"),
-    std::pair("----:com.apple.iTunes:MEDIA", "MEDIA"),
-  };
-
-  String translateKey(const String &key)
-  {
-    for(const auto &[k, t] : keyTranslation) {
-      if(key == k)
-        return t;
-    }
-
-    return String();
-  }
-}  // namespace
-
 PropertyMap MP4::Tag::properties() const
 {
   PropertyMap props;
   for(const auto &[k, t] : std::as_const(d->items)) {
-    const String key = translateKey(k);
+    auto [key, value] = d->factory->itemToProperty(k.data(String::Latin1), t);
     if(!key.isEmpty()) {
-      if(key == "TRACKNUMBER" || key == "DISCNUMBER") {
-        auto [vn, tn] = t.toIntPair();
-        String value  = String::number(vn);
-        if(tn) {
-          value += "/" + String::number(tn);
-        }
-        props[key] = value;
-      }
-      else if(key == "BPM" || key == "MOVEMENTNUMBER" || key == "MOVEMENTCOUNT" ||
-              key == "TVEPISODE" || key == "TVSEASON") {
-        props[key] = String::number(t.toInt());
-      }
-      else if(key == "COMPILATION" || key == "SHOWWORKMOVEMENT" ||
-              key == "GAPLESSPLAYBACK" || key == "PODCAST") {
-        props[key] = String::number(t.toBool());
-      }
-      else {
-        props[key] = t.toStringList();
-      }
+      props[key] = value;
     }
     else {
       props.unsupportedData().append(k);
@@ -1016,50 +492,18 @@ void MP4::Tag::removeUnsupportedProperties(const StringList &props)
 
 PropertyMap MP4::Tag::setProperties(const PropertyMap &props)
 {
-  static Map<String, String> reverseKeyMap;
-  if(reverseKeyMap.isEmpty()) {
-    for(const auto &[k, t] : keyTranslation) {
-      reverseKeyMap[t] = k;
-    }
-  }
-
   const PropertyMap origProps = properties();
   for(const auto &[prop, _] : origProps) {
     if(!props.contains(prop) || props[prop].isEmpty()) {
-      d->items.erase(reverseKeyMap[prop]);
+      d->items.erase(d->factory->nameForPropertyKey(prop));
     }
   }
 
   PropertyMap ignoredProps;
   for(const auto &[prop, val] : props) {
-    if(reverseKeyMap.contains(prop)) {
-      String name = reverseKeyMap[prop];
-      if((prop == "TRACKNUMBER" || prop == "DISCNUMBER") && !val.isEmpty()) {
-        StringList parts = StringList::split(val.front(), "/");
-        if(!parts.isEmpty()) {
-          int first = parts[0].toInt();
-          int second = 0;
-          if(parts.size() > 1) {
-            second = parts[1].toInt();
-          }
-          d->items[name] = MP4::Item(first, second);
-        }
-      }
-      else if((prop == "BPM" || prop == "MOVEMENTNUMBER" ||
-               prop == "MOVEMENTCOUNT" || prop == "TVEPISODE" ||
-               prop == "TVSEASON") && !val.isEmpty()) {
-        int value = val.front().toInt();
-        d->items[name] = MP4::Item(value);
-      }
-      else if((prop == "COMPILATION" || prop == "SHOWWORKMOVEMENT" ||
-               prop == "GAPLESSPLAYBACK" || prop == "PODCAST") &&
-              !val.isEmpty()) {
-        bool value = (val.front().toInt() != 0);
-        d->items[name] = MP4::Item(value);
-      }
-      else {
-        d->items[name] = val;
-      }
+    auto [name, item] = d->factory->itemFromProperty(prop, val);
+    if(item.isValid()) {
+      d->items[name] = item;
     }
     else {
       ignoredProps.insert(prop, val);
diff --git a/taglib/mp4/mp4tag.h b/taglib/mp4/mp4tag.h
index 226ce019..e0e9f7ca 100644
--- a/taglib/mp4/mp4tag.h
+++ b/taglib/mp4/mp4tag.h
@@ -37,13 +37,15 @@
 
 namespace TagLib {
   namespace MP4 {
-    using ItemMap = TagLib::Map<String, Item>;
+
+    class ItemFactory;
 
     class TAGLIB_EXPORT Tag: public TagLib::Tag
     {
     public:
         Tag();
-        Tag(TagLib::File *file, Atoms *atoms);
+        Tag(TagLib::File *file, Atoms *atoms,
+            const ItemFactory *factory = nullptr);
         ~Tag() override;
         Tag(const Tag &) = delete;
         Tag &operator=(const Tag &) = delete;
@@ -114,36 +116,9 @@ namespace TagLib {
         void setTextItem(const String &key, const String &value);
 
     private:
-        AtomDataList parseData2(const Atom *atom, int expectedFlags = -1,
-                                bool freeForm = false);
-        ByteVectorList parseData(const Atom *atom, int expectedFlags = -1,
-                                 bool freeForm = false);
-        void parseText(const Atom *atom, int expectedFlags = 1);
-        void parseFreeForm(const Atom *atom);
-        void parseInt(const Atom *atom);
-        void parseByte(const Atom *atom);
-        void parseUInt(const Atom *atom);
-        void parseLongLong(const Atom *atom);
-        void parseGnre(const Atom *atom);
-        void parseIntPair(const Atom *atom);
-        void parseBool(const Atom *atom);
-        void parseCovr(const Atom *atom);
-
         ByteVector padIlst(const ByteVector &data, int length = -1) const;
         ByteVector renderAtom(const ByteVector &name, const ByteVector &data) const;
-        ByteVector renderData(const ByteVector &name, int flags,
-                              const ByteVectorList &data) const;
-        ByteVector renderText(const ByteVector &name, const Item &item,
-                              int flags = TypeUTF8) const;
-        ByteVector renderFreeForm(const String &name, const Item &item) const;
-        ByteVector renderBool(const ByteVector &name, const Item &item) const;
-        ByteVector renderInt(const ByteVector &name, const Item &item) const;
-        ByteVector renderByte(const ByteVector &name, const Item &item) const;
-        ByteVector renderUInt(const ByteVector &name, const Item &item) const;
-        ByteVector renderLongLong(const ByteVector &name, const Item &item) const;
-        ByteVector renderIntPair(const ByteVector &name, const Item &item) const;
-        ByteVector renderIntPairNoTrailing(const ByteVector &name, const Item &item) const;
-        ByteVector renderCovr(const ByteVector &name, const Item &item) const;
+
 
         void updateParents(const AtomList &path, offset_t delta, int ignore = 0);
         void updateOffsets(offset_t delta, offset_t offset);
diff --git a/tests/test_mp4.cpp b/tests/test_mp4.cpp
index 5d8909da..77f8823d 100644
--- a/tests/test_mp4.cpp
+++ b/tests/test_mp4.cpp
@@ -33,6 +33,7 @@
 #include "mp4tag.h"
 #include "mp4atom.h"
 #include "mp4file.h"
+#include "mp4itemfactory.h"
 #include "plainfile.h"
 #include <cppunit/extensions/HelperMacros.h>
 #include "utils.h"
@@ -69,6 +70,7 @@ class TestMP4 : public CppUnit::TestFixture
   CPPUNIT_TEST(testEmptyValuesRemoveItems);
   CPPUNIT_TEST(testRemoveMetadata);
   CPPUNIT_TEST(testNonFullMetaAtom);
+  CPPUNIT_TEST(testItemFactory);
   CPPUNIT_TEST_SUITE_END();
 
 public:
@@ -735,6 +737,105 @@ public:
       CPPUNIT_ASSERT_EQUAL(StringList("FAAC 1.24"), properties["ENCODEDBY"]);
     }
   }
+
+  void testItemFactory()
+  {
+    class CustomItemFactory : public MP4::ItemFactory {
+    protected:
+      NameHandlerMap nameHandlerMap() const override
+      {
+        return MP4::ItemFactory::nameHandlerMap()
+          .insert("tsti", ItemHandlerType::Int)
+          .insert("tstt", ItemHandlerType::Text);
+      }
+
+      Map<ByteVector, String> namePropertyMap() const override
+      {
+        return MP4::ItemFactory::namePropertyMap()
+          .insert("tsti", "TESTINTEGER");
+      }
+    };
+
+    CustomItemFactory factory;
+
+    ScopedFileCopy copy("no-tags", ".m4a");
+    {
+      MP4::File f(copy.fileName().c_str());
+      CPPUNIT_ASSERT(f.isValid());
+      CPPUNIT_ASSERT(!f.hasMP4Tag());
+      MP4::Tag *tag = f.tag();
+      tag->setItem("tsti", MP4::Item(123));
+      tag->setItem("tstt", MP4::Item(StringList("Test text")));
+      f.save();
+    }
+    {
+      MP4::File f(copy.fileName().c_str());
+      CPPUNIT_ASSERT(f.isValid());
+      CPPUNIT_ASSERT(f.hasMP4Tag());
+      MP4::Tag *tag = f.tag();
+      // Without a custom item factory, only custom text atoms with four
+      // letter names are possible.
+      MP4::Item item = tag->item("tsti");
+      CPPUNIT_ASSERT(!item.isValid());
+      CPPUNIT_ASSERT(item.toInt() != 123);
+      item = tag->item("tstt");
+      CPPUNIT_ASSERT(item.isValid());
+      CPPUNIT_ASSERT_EQUAL(StringList("Test text"), item.toStringList());
+      f.strip();
+    }
+    {
+      MP4::File f(copy.fileName().c_str(),
+                  true, MP4::Properties::Average, &factory);
+      CPPUNIT_ASSERT(f.isValid());
+      CPPUNIT_ASSERT(!f.hasMP4Tag());
+      MP4::Tag *tag = f.tag();
+      tag->setItem("tsti", MP4::Item(123));
+      tag->setItem("tstt", MP4::Item(StringList("Test text")));
+      tag->setItem("trkn", MP4::Item(2, 10));
+      tag->setItem("rate", MP4::Item(80));
+      tag->setItem("plID", MP4::Item(1540934238LL));
+      tag->setItem("rtng", MP4::Item(static_cast<unsigned char>(2)));
+      f.save();
+    }
+    {
+      MP4::File f(copy.fileName().c_str(),
+                  true, MP4::Properties::Average, &factory);
+      CPPUNIT_ASSERT(f.isValid());
+      CPPUNIT_ASSERT(f.hasMP4Tag());
+      MP4::Tag *tag = f.tag();
+      MP4::Item item = tag->item("tsti");
+      CPPUNIT_ASSERT(item.isValid());
+      CPPUNIT_ASSERT_EQUAL(123, item.toInt());
+      item = tag->item("tstt");
+      CPPUNIT_ASSERT(item.isValid());
+      CPPUNIT_ASSERT_EQUAL(StringList("Test text"), item.toStringList());
+      item = tag->item("trkn");
+      CPPUNIT_ASSERT(item.isValid());
+      CPPUNIT_ASSERT_EQUAL(2, item.toIntPair().first);
+      CPPUNIT_ASSERT_EQUAL(10, item.toIntPair().second);
+      CPPUNIT_ASSERT_EQUAL(80, tag->item("rate").toInt());
+      CPPUNIT_ASSERT_EQUAL(1540934238LL, tag->item("plID").toLongLong());
+      CPPUNIT_ASSERT_EQUAL(static_cast<unsigned char>(2), tag->item("rtng").toByte());
+      PropertyMap properties = tag->properties();
+      CPPUNIT_ASSERT_EQUAL(StringList("123"), properties.value("TESTINTEGER"));
+      CPPUNIT_ASSERT_EQUAL(StringList("2/10"), properties.value("TRACKNUMBER"));
+      properties["TESTINTEGER"] = StringList("456");
+      tag->setProperties(properties);
+      f.save();
+    }
+    {
+      MP4::File f(copy.fileName().c_str(),
+                  true, MP4::Properties::Average, &factory);
+      CPPUNIT_ASSERT(f.isValid());
+      CPPUNIT_ASSERT(f.hasMP4Tag());
+      MP4::Tag *tag = f.tag();
+      MP4::Item item = tag->item("tsti");
+      CPPUNIT_ASSERT(item.isValid());
+      CPPUNIT_ASSERT_EQUAL(456, item.toInt());
+      PropertyMap properties = tag->properties();
+      CPPUNIT_ASSERT_EQUAL(StringList("456"), properties.value("TESTINTEGER"));
+    }
+  }
 };
 
 CPPUNIT_TEST_SUITE_REGISTRATION(TestMP4);