Repository: tinytag/tinytag

Issue:
Ability to get all images
I would like the ability to get all of the images in a file.  Currently, I think tinytag is coded to just pull the cover image or maybe it is the first image it comes across.  Ideally, I'd like to have each image and its location (e.g., cover, insert, etc) accessible.

Thanks!

Discussion:
@mathiascode: What would the ideal API for this look like? I'm thinking something similar to the `extra` dict could work here, with standardized keys and image data as the value, but are we missing any important information this way? Is there anything else than the image and its description that's important to retain?@aw-was-here: I can't think of any other info that would be useful that would be in the purview of TinyTag. e.g., image size, image type, etc, are better handled by something like Pillow and would need to be handled by the downstream caller anyway.@mathiascode: Here's a quick stab at a dictionary structure. Would be accessed as such: `tag.images["front_cover"]["image_data"]`

mime_type and picture_type are included because ID3 provides them, but I haven't looked into what other formats do.

### tag.images:
- other
- icon
- other_icon
- front_cover
  - image_data
  - description
  - mime_type
  - picture_type
- back_cover
- leaflet
- media
- lead_artist
- artist
- conductor
- band
- composer
- lyricist
- recording_location
- during_recording
- during_performance
- video
- bright_colored_fish
- illustration
- band_logo
- publisher_logo@aw-was-here: > mime_type and picture_type are included because ID3 provides them, but I haven't looked into what other formats do.

I wonder how reliable those fields actually are.  In my particular case I'll likely just pass the raw data to pillow to convert to a known format.@mathiascode: PR for testing: https://github.com/devsnd/tinytag/pull/202

I changed my mind and used classes instead of dictionaries. Allows you to easily access images as such:
```
image = tag.images.front_cover
data = image.data
description = image.description
```

`tag.get_image()` remains as a way to get something that most likely resembles a cover image (sometimes it's stored in `other` instead of `front_cover`).

> I wonder how reliable those fields actually are. In my particular case I'll likely just pass the raw data to pillow to convert to a known format.

I haven't had time to look into them, so I left them out for now. The format allows us to easily expose them in the future though, if necessary.@mathiascode: The image MIME type is now available under `tag.images.front_cover.mime_type`.@mathiascode: I didn't think about the case where multiple images of the same kind can exist, so the API has been changed. You can check the README.md file for more details, but it essentially boils down to:

- `tag.images.any` to get any image available, prioritizing the cover art. `tag.images.any.data` to get the image as bytes.
- 'images' attributes now contain lists of images. If you want to e.g. get a list of leaflet images, you can do so with `tag.images.leaflet`. To get the first leaflet image, `tag.images.leaflet[0]`.
- More uncommon image types were moved to `tag.images.extra`. If you want to get e.g. a list of artist images, `tag.images.extra.get('artist')`.@aw-was-here: OK I'll try to make some time today/tomorrow to play around with this feature and see how it works out in my workflow.  Thanks again!@aw-was-here: Sorry @mathiascode I got distracted with some family stuff. This one is back on my radar and I'll try it out.@aw-was-here: The new code seems to be working ok for me with some quick, preliminary tests.  Got a [track with 9 front_cover images](https://musicbrainz.org/release/29cc7eff-fd8b-4cee-89a2-f36c923086f7/cover-art) and I'm getting the proper data for all 9.

Great job! Thanks!

Solution patch:
diff --git a/README.md b/README.md
index e78b522..ab55b06 100644
--- a/README.md
+++ b/README.md
@@ -16,7 +16,7 @@ tinytag is a library for reading music meta data of most common audio files in p
 
 ## Features
 
-  * Read tags, length and cover images of audio files
+  * Read tags, length and images of audio files
   * Supported formats:
     * MP3 / MP2 / MP1 (ID3 v1, v1.1, v2.2, v2.3+)
     * M4A (AAC / ALAC)
@@ -28,15 +28,17 @@ tinytag is a library for reading music meta data of most common audio files in p
   * Pure Python, no dependencies
   * Supports Python 3.7 or higher
   * High test coverage
-  * Just a few hundred lines of code (just include it in your project!) 
+  * A few hundred lines of code (just include it in your project!)
 
-tinytag only provides the minimum needed for _reading_ meta-data.
-It can determine track number, total tracks, title, artist, album, year, duration and any more.
+## Usage
+
+tinytag only provides the minimum needed for _reading_ metadata, and presents it in a simple format.
+It can determine track number, total tracks, title, artist, album, year, duration and more.
 
     from tinytag import TinyTag
     tag = TinyTag.get('/some/music.mp3')
-    print('This track is by %s.' % tag.artist)
-    print('It is %f seconds long.' % tag.duration)
+    print(f'This track is by {tag.artist}.')
+    print(f'It is {tag.duration:.2f} seconds long.')
     
 Alternatively you can use tinytag directly on the command line:
 
@@ -45,14 +47,18 @@ Alternatively you can use tinytag directly on the command line:
 
 Check `python -m tinytag --help` for all CLI options, for example other output formats.
 
-To receive a list of file extensions tinytag supports, use the `SUPPORTED_FILE_EXTENSIONS` constant:
+### Supported Files
+
+To receive a tuple of file extensions tinytag supports, use the `SUPPORTED_FILE_EXTENSIONS` constant:
 
     TinyTag.SUPPORTED_FILE_EXTENSIONS
 
-Alternatively, check if a file is supported:
+Alternatively, check if a file is supported by providing its path:
 
     is_supported = TinyTag.is_supported('/some/music.mp3')
 
+### Attributes
+
 List of common attributes tinytag provides:
 
     tag.album         # album as string
@@ -82,23 +88,78 @@ For non-common fields and fields specific to certain file formats, use `extra`:
 
 The following standard `extra` field names are used when file formats provide relevant data:
 
-    `bpm`
-    `composer`
-    `copyright`
-    `director`
-    `initial_key`
-    `isrc`
-    `language`
-    `lyrics`
-    `publisher`
-    `url`
+    bpm
+    composer
+    copyright
+    director
+    initial_key
+    isrc
+    language
+    lyrics
+    publisher
+    url
 
 Any other `extra` field names are not guaranteed to be consistent across audio formats.
 
-Additionally you can also get cover images from ID3 tags:
+Additionally, you can also get images from ID3 tags. To receive an image most likely suitable as the front cover:
+
+    tag: TinyTag = TinyTag.get('/some/music.mp3', image=True)
+    image_data: bytes = tag.get_image()
+
+If you need to receive an image of a specific type, including its description, use `images`:
+
+    tag.images        # image types and their data
+
+The following common image types exist:
+
+    front_cover
+    back_cover
+    leaflet
+    media
+    other
+
+The following less common image types are provided in an `extra` dict:
 
-    tag = TinyTag.get('/some/music.mp3', image=True)
-    image_data = tag.get_image()
+    icon
+    other_icon
+    lead_artist
+    artist
+    conductor
+    band
+    composer
+    lyricist
+    recording_location
+    during_recording
+    during_performance
+    video
+    bright_colored_fish
+    illustration
+    band_logo
+    publisher_logo
+
+The following image attributes are available:
+
+    data           # image data as bytes
+    description    # image description as string
+
+To receive a common image, e.g. `front_cover`:
+
+    from tinytag import TinyTag, TagImage, TagImages
+
+    tag: TinyTag = TinyTag.get('/some/music.ogg')
+    images: TagImages = tag.images
+    image: TagImage = images.front_cover
+    data: bytes = image.data
+    description: str = image.description
+
+To receive an extra image, e.g. `bright_colored_fish`:
+
+    image = tag.images.extra.get('bright_colored_fish')
+    if image is not None:
+        data = image.data
+        description = image.description
+
+### Encoding
 
 To open files using a specific encoding, you can use the `encoding` parameter.
 This parameter is however only used for formats where the encoding isn't explicitly
@@ -106,6 +167,8 @@ specified.
 
     TinyTag.get('a_file_with_gbk_encoding.mp3', encoding='gbk')
 
+### File-like Objects
+
 To use a file-like object (e.g. BytesIO) instead of a file path, pass a
 `file_obj` keyword argument:
 
diff --git a/tinytag/__init__.py b/tinytag/__init__.py
index b4d6291..26c8208 100644
--- a/tinytag/__init__.py
+++ b/tinytag/__init__.py
@@ -1,2 +1,2 @@
 # pylint: disable=missing-module-docstring
-from .tinytag import TinyTag, TinyTagException
+from .tinytag import TinyTag, TagImage, TagImages, TinyTagException
diff --git a/tinytag/tinytag.py b/tinytag/tinytag.py
index aede9a8..3d1bde4 100644
--- a/tinytag/tinytag.py
+++ b/tinytag/tinytag.py
@@ -27,7 +27,7 @@
 # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
 # SOFTWARE.
 
-# pylint: disable=missing-module-docstring,missing-class-docstring,missing-function-docstring
+# pylint: disable=missing-module-docstring,missing-class-docstring
 # pylint: disable=invalid-name,protected-access
 # pylint: disable=too-many-lines,too-many-arguments,too-many-boolean-expressions
 # pylint: disable=too-many-branches,too-many-instance-attributes,too-many-locals
@@ -62,6 +62,7 @@ class TinyTag:
         '.m4b', '.m4a', '.m4r', '.m4v', '.mp4', '.aax', '.aaxc',
         '.aiff', '.aifc', '.aif', '.afc'
     )
+    _EXTRA_PREFIX = 'extra.'
     _file_extension_mapping: dict[tuple[bytes, ...], type[TinyTag]] | None = None
     _magic_bytes_mapping: dict[bytes, type[TinyTag]] | None = None
 
@@ -75,9 +76,10 @@ class TinyTag:
         self.disc: int | None = None
         self.disc_total: int | None = None
         self.duration: float | None = None
-        self.extra: dict[str, str | int | float] = {}
+        self.extra: dict[str, str | float | int] = {}
         self.filesize = 0
         self.genre: str | None = None
+        self.images = TagImages()
         self.samplerate: int | None = None
         self.bitdepth: int | None = None
         self.title: str | None = None
@@ -86,7 +88,6 @@ class TinyTag:
         self.year: str | None = None
         self._filehandler: BinaryIO | None = None
         self._filename: bytes | str | PathLike[Any] | None = None  # for debugging
-        self._image_data: bytes | None = None
         self._default_encoding: str | None = None  # allow override for some file formats
         self._ignore_errors = False
         self._parse_duration = True
@@ -103,6 +104,7 @@ class TinyTag:
             ignore_errors: bool = False,
             encoding: str | None = None,
             file_obj: BinaryIO | None = None) -> TinyTag:
+        """Create a tag object for a file path or a file-like object."""
         should_close_file = file_obj is None
         if filename and should_close_file:
             file_obj = open(filename, 'rb')  # pylint: disable=consider-using-with
@@ -130,17 +132,31 @@ class TinyTag:
                 file_obj.close()
 
     def get_image(self) -> bytes | None:
-        return self._image_data
+        """Return the closest equivalent to a cover image as bytes."""
+        front_cover = self.images.front_cover.data  # check the obvious location
+        if front_cover is not None:
+            return front_cover
+        other = self.images.other.data  # cover images are sometimes stored here
+        if other is not None:
+            return other
+        back_cover = self.images.back_cover.data  # last resort, maybe there's something here...
+        if back_cover is not None:
+            return back_cover
+        return None
 
     @classmethod
     def is_supported(cls, filename: bytes | str | PathLike[Any]) -> bool:
+        """Check if a specific file is supported based on the file extension."""
         return cls._get_parser_for_filename(filename) is not None
 
     def __repr__(self) -> str:
         return str(self._as_dict())
 
     def _as_dict(self) -> dict[str, Any]:
-        return {k: v for k, v in sorted(self.__dict__.items()) if not k.startswith('_')}
+        return {
+            k: v for k, v in sorted(self.__dict__.items())
+            if not k.startswith('_') and k != 'images'
+        }
 
     @classmethod
     def _get_parser_for_filename(
@@ -224,22 +240,34 @@ class TinyTag:
 
     def _set_field(self, fieldname: str, value: str | int | float) -> None:
         """convenience function to set fields of the tinytag by name"""
-        write_dest = self.__dict__  # write into the TinyTag by default
         is_str = isinstance(value, str)
         if is_str and not value:
             # don't set empty value
             return
-        if fieldname.startswith('extra.'):
-            fieldname = fieldname[6:]
-            write_dest = self.extra  # write into the extra field instead
+        write_dest = self.__dict__
+        if fieldname.startswith(self._EXTRA_PREFIX):
+            fieldname = fieldname[len(self._EXTRA_PREFIX):]
+            write_dest = self.extra
         old_value = write_dest.get(fieldname)
-        if is_str and old_value and old_value != value:
-            # Combine same field with a null character
-            value = old_value + '\x00' + value
+        if is_str:
+            if old_value and old_value != value:
+                # Combine same field with a null character
+                value = old_value + '\x00' + value
+        elif not value and old_value:
+            return
         if DEBUG:
             print(f'Setting field "{fieldname}" to "{value!r}"')
         write_dest[fieldname] = value
 
+    def _set_image_field(self, fieldname: str, value: bytes | str | TagImage) -> None:
+        write_dest = self.images.__dict__
+        if fieldname.startswith(self._EXTRA_PREFIX):
+            fieldname = fieldname[len(self._EXTRA_PREFIX):]
+            write_dest = self.images.extra
+        if DEBUG:
+            print(f'Setting image field "{fieldname}"')
+        write_dest[fieldname] = value
+
     def _determine_duration(self, fh: BinaryIO) -> None:
         raise NotImplementedError
 
@@ -248,16 +276,16 @@ class TinyTag:
 
     def _update(self, other: TinyTag) -> None:
         # update the values of this tag with the values from another tag
-        for key in ('track', 'track_total', 'title', 'artist',
-                    'album', 'albumartist', 'year', 'duration',
-                    'genre', 'disc', 'disc_total', 'comment',
-                    'bitdepth', 'bitrate', 'channels', 'samplerate',
-                    '_image_data'):
-            new_value = getattr(other, key)
-            if new_value:
-                self._set_field(key, new_value)
-        for key, value in other.extra.items():
-            self._set_field("extra." + key, value)
+        for standard_key, standard_value in other.__dict__.items():
+            if (not standard_key.startswith('_') and standard_key != 'filesize'
+                    and standard_value is not None):
+                self._set_field(standard_key, standard_value)
+        for extra_key, extra_value in other.extra.items():
+            self._set_field(self._EXTRA_PREFIX + extra_key, extra_value)
+        for image_key, image_value in other.images.__dict__.items():
+            self._set_image_field(image_key, image_value)
+        for image_extra_key, image_extra_value in other.images.extra.items():
+            self._set_image_field(self._EXTRA_PREFIX + image_extra_key, image_extra_value)
 
     @staticmethod
     def _bytes_to_int_le(b: bytes) -> int:
@@ -275,12 +303,40 @@ class TinyTag:
         return s.strip('\x00')
 
 
+class TagImages:
+    def __init__(self) -> None:
+        image = TagImage()
+        self.front_cover: TagImage = image
+        self.back_cover: TagImage = image
+        self.leaflet: TagImage = image
+        self.media: TagImage = image
+        self.other: TagImage = image
+        self.extra: dict[str, bytes | str] = {}
+
+    def __repr__(self) -> str:
+        return str(vars(self))
+
+
+class TagImage:
+    def __init__(self, data: bytes | None = None) -> None:
+        self.data = data
+        self.description: str | None = None
+
+    def __repr__(self) -> str:
+        variables = vars(self).copy()
+        data = variables.get("data")
+        if data is not None:
+            variables["data"] = (data[:45] + b'..') if len(data) > 45 else data
+        return str(variables)
+
+
 class _MP4(TinyTag):
     # https://developer.apple.com/library/mac/documentation/QuickTime/QTFF/Metadata/Metadata.html
     # https://developer.apple.com/library/mac/documentation/QuickTime/QTFF/QTFFChap2/qtff2.html
 
     class _Parser:
-        atom_decoder_by_type: dict[int, Callable[[bytes], int | str | bytes]] | None = None
+        atom_decoder_by_type: dict[
+            int, Callable[[bytes], int | str | bytes | TagImage]] | None = None
 
         @classmethod
         def _unpack_integer(cls, value: bytes, signed: bool = True) -> int:
@@ -302,8 +358,8 @@ class _MP4(TinyTag):
 
         @classmethod
         def _make_data_atom_parser(
-                cls, fieldname: str) -> Callable[[bytes], dict[str, int | str | bytes]]:
-            def _parse_data_atom(data_atom: bytes) -> dict[str, int | str | bytes]:
+                cls, fieldname: str) -> Callable[[bytes], dict[str, int | str | bytes | TagImage]]:
+            def _parse_data_atom(data_atom: bytes) -> dict[str, int | str | bytes | TagImage]:
                 data_type = struct.unpack('>I', data_atom[:4])[0]
                 if cls.atom_decoder_by_type is None:
                     # https://developer.apple.com/library/mac/documentation/QuickTime/QTFF/Metadata/Metadata.html#//apple_ref/doc/uid/TP40000939-CH1-SW34
@@ -313,8 +369,8 @@ class _MP4(TinyTag):
                         2: lambda x: x.decode('utf-16', 'replace'),  # UTF-16
                         3: lambda x: x.decode('s/jis', 'replace'),   # S/JIS
                         # 16: duration in millis
-                        13: lambda x: x,  # JPEG
-                        14: lambda x: x,  # PNG
+                        13: TagImage,                                  # JPEG
+                        14: TagImage,                                  # PNG
                         21: cls._unpack_integer,                    # BE Signed int
                         22: cls._unpack_integer_unsigned,           # BE Unsigned int
                         # 23: lambda x: struct.unpack('>f', x)[0],  # BE Float32
@@ -365,7 +421,7 @@ class _MP4(TinyTag):
                     break
 
         @classmethod
-        def _parse_custom_field(cls, data: bytes) -> dict[str, int | str | bytes]:
+        def _parse_custom_field(cls, data: bytes) -> dict[str, int | str | bytes | TagImage]:
             fh = io.BytesIO(data)
             header_size = 8
             field_name = None
@@ -376,7 +432,7 @@ class _MP4(TinyTag):
                 atom_type = atom_header[4:]
                 if atom_type == b'name':
                     atom_value = fh.read(atom_size)[4:].lower()
-                    field_name = 'extra.' + atom_value.decode('utf-8', 'replace')
+                    field_name = TinyTag._EXTRA_PREFIX + atom_value.decode('utf-8', 'replace')
                 elif atom_type == b'data':
                     data_atom = fh.read(atom_size)
                 else:
@@ -472,7 +528,7 @@ class _MP4(TinyTag):
         b'gnre': {b'data': _Parser._parse_id3v1_genre},
         b'trkn': {b'data': _Parser._make_number_parser('track', 'track_total')},
         b'tmpo': {b'data': _Parser._make_data_atom_parser('extra.bpm')},
-        b'covr': {b'data': _Parser._make_data_atom_parser('_image_data')},
+        b'covr': {b'data': _Parser._make_data_atom_parser('images.front_cover')},
         b'----': _Parser._parse_custom_field,
     }}}}}
 
@@ -527,9 +583,10 @@ class _MP4(TinyTag):
                 for fieldname, value in sub_path(fh.read(atom_size)).items():
                     if DEBUG:
                         print(' ' * 4 * len(curr_path), 'FIELD: ', fieldname)
-                    if fieldname == '_image_data' and not self._load_image:
-                        continue
-                    if fieldname:
+                    if fieldname.startswith('images.'):
+                        if self._load_image:
+                            self._set_image_field(fieldname[len('images.'):], value)
+                    elif fieldname:
                         self._set_field(fieldname, value)
             # if no action was specified using dict or callable, jump over atom
             else:
@@ -614,6 +671,29 @@ class _ID3(TinyTag):
         'Neoclassical', 'Audiobook', 'Audio Theatre', 'Neue Deutsche Welle',
         'Podcast', 'Indie Rock', 'G-Funk', 'Dubstep', 'Garage Rock', 'Psybient',
     )
+    _IMAGE_TYPES = (
+        'other',
+        'extra.icon',
+        'extra.other_icon',
+        'front_cover',
+        'back_cover',
+        'leaflet',
+        'media',
+        'extra.lead_artist',
+        'extra.artist',
+        'extra.conductor',
+        'extra.band',
+        'extra.composer',
+        'extra.lyricist',
+        'extra.recording_location',
+        'extra.during_recording',
+        'extra.during_performance',
+        'extra.video',
+        'extra.bright_colored_fish',
+        'extra.illustration',
+        'extra.band_logo',
+        'extra.publisher_logo',
+    )
 
     # see this page for the magic values used in mp3:
     # http://www.mpgedit.org/mpgedit/mpeg_format/mpeghdr.htm
@@ -824,7 +904,7 @@ class _ID3(TinyTag):
     def __parse_custom_field(self, content: str) -> bool:
         custom_field_name, separator, value = content.partition('\x00')
         if custom_field_name and separator:
-            self._set_field('extra.' + custom_field_name.lower(), value.lstrip('\ufeff'))
+            self._set_field(self._EXTRA_PREFIX + custom_field_name.lower(), value.lstrip('\ufeff'))
             return True
         return False
 
@@ -898,15 +978,23 @@ class _ID3(TinyTag):
                         desc_start_pos = 1 + 3 + 1  # skip encoding (1), imgformat (3), pictype(1)
                     else:  # ID3 v2.3+
                         desc_start_pos = content.index(b'\x00', 1) + 1 + 1  # skip mtype, pictype(1)
+                    pictype = content[desc_start_pos - 1]
                     # latin1 and utf-8 are 1 byte
                     termination = b'\x00' if encoding in {b'\x00', b'\x03'} else b'\x00\x00'
                     desc_length = self._index_utf16(content[desc_start_pos:], termination)
                     desc_end_pos = desc_start_pos + desc_length + len(termination)
-                    self._image_data = content[desc_end_pos:]
+                    description = self._decode_string(content[desc_start_pos:desc_end_pos])
+                    image = TagImage(content[desc_end_pos:])
+                    if description:
+                        image.description = description
+                    if 0 <= pictype <= len(self._IMAGE_TYPES):
+                        pictype = 0  # fall back to 'other' type
+                    self._set_image_field(self._IMAGE_TYPES[pictype], image)
             elif frame_id not in self._DISALLOWED_FRAME_IDS:
                 # unknown, try to add to extra dict
                 if self._parse_tags:
-                    self._set_field('extra.' + frame_id.lower(), self._decode_string(content))
+                    self._set_field(
+                        self._EXTRA_PREFIX + frame_id.lower(), self._decode_string(content))
             return frame_size
         return 0
 
@@ -1095,13 +1183,14 @@ class _Ogg(TinyTag):
 
                 if key_lowercase == "metadata_block_picture" and self._load_image:
                     if DEBUG:
-                        print('Found Vorbis Image', key, value[:64])
-                    self._image_data = _Flac._parse_image(io.BytesIO(base64.b64decode(value)))
+                        print('Found Vorbis TagImage', key, value[:64])
+                    fieldname, fieldvalue = _Flac._parse_image(io.BytesIO(base64.b64decode(value)))
+                    self._set_image_field(fieldname, fieldvalue)
                 else:
                     if DEBUG:
                         print('Found Vorbis Comment', key, value[:64])
                     fieldname = self._VORBIS_MAPPING.get(
-                        key_lowercase, 'extra.' + key_lowercase)  # custom fields go in 'extra'
+                        key_lowercase, self._EXTRA_PREFIX + key_lowercase)  # custom field
                     if fieldname in {'track', 'disc', 'track_total', 'disc_total'}:
                         if fieldname in {'track', 'disc'} and '/' in value:
                             value, total = value.split('/')[:2]
@@ -1303,7 +1392,8 @@ class _Flac(TinyTag):
                 oggtag._parse_vorbis_comment(fh)
                 self._update(oggtag)
             elif block_type == self.METADATA_PICTURE and self._load_image:
-                self._image_data = self._parse_image(fh)
+                fieldname, value = self._parse_image(fh)
+                self._set_image_field(fieldname, value)
             elif block_type >= 127:
                 break  # invalid block type
             else:
@@ -1318,15 +1408,21 @@ class _Flac(TinyTag):
             self._update(id3)
         self._tags_parsed = True
 
-    @staticmethod
-    def _parse_image(fh: BinaryIO) -> bytes:
+    @classmethod
+    def _parse_image(cls, fh: BinaryIO) -> tuple[str, TagImage]:
         # https://xiph.org/flac/format.html#metadata_block_picture
-        _pic_type, mime_len = struct.unpack('>2I', fh.read(8))
+        pic_type, mime_len = struct.unpack('>2I', fh.read(8))
         fh.read(mime_len)
         description_len = struct.unpack('>I', fh.read(4))[0]
-        fh.read(description_len)
+        description = fh.read(description_len).decode('utf-8')
         _width, _height, _depth, _colors, pic_len = struct.unpack('>5I', fh.read(20))
-        return fh.read(pic_len)
+        if 0 <= pic_type <= len(_ID3._IMAGE_TYPES):
+            pic_type = 0  # fall back to 'other' type
+        field_name = _ID3._IMAGE_TYPES[pic_type]
+        image = TagImage(fh.read(pic_len))
+        if description:
+            image.description = description
+        return field_name, image
 
 
 class _Wma(TinyTag):
@@ -1416,7 +1512,7 @@ class _Wma(TinyTag):
                     if field_name is None:  # custom field
                         if name.startswith('WM/'):
                             name = name[3:]
-                        field_name = 'extra.' + name.lower()
+                        field_name = self._EXTRA_PREFIX + name.lower()
                     field_value = self._decode_ext_desc(value_type, fh.read(value_len))
                     if field_value is not None:
                         if field_name in {'track', 'disc'}:


Test patch:
diff --git a/tinytag/tests/test_all.py b/tinytag/tests/test_all.py
index 2c13bd0..34c5181 100644
--- a/tinytag/tests/test_all.py
+++ b/tinytag/tests/test_all.py
@@ -393,7 +393,7 @@ testfiles = dict([
          'artist': 'artist\x00群星',
          'title': 'title\x00A 梦 哆啦 机器猫 短信铃声', 'track': 1, 'bitrate': 1143.72468, 'channels': 1,
          'duration': 0.45351473922902497, 'genre': 'genre', 'samplerate': 44100, 'bitdepth': 16,
-         'year': '2018', 'comment': 'comment'}),
+         'year': '2018', 'comment': 'comment', 'disc': 0}),
     ('samples/with_padded_id3_header.flac',
         {'extra': {}, 'filesize': 16070, 'album': 'album', 'artist': 'artist',
          'bitrate': 283.4748, 'channels': 1,
@@ -614,10 +614,10 @@ def test_file_reading_tags_duration(testfile: str, expected: dict[str, dict[str,
     filename = os.path.join(testfolder, testfile)
     tag = TinyTag.get(filename, tags=True, duration=True)
     results = {
-        key: val for key, val in tag._as_dict().items() if val is not None
+        key: val for key, val in tag._as_dict().items() if val is not None and key != 'images'
     }
     compare_tag(results, expected, filename)
-    assert tag._image_data is None
+    assert tag.images.front_cover.data is None
 
 
 @pytest.mark.parametrize("testfile,expected", testfiles.items())
@@ -626,13 +626,13 @@ def test_file_reading_tags(testfile: str, expected: dict[str, dict[str, Any]]) -
     excluded_attrs = {"bitdepth", "bitrate", "channels", "duration", "samplerate"}
     tag = TinyTag.get(filename, tags=True, duration=False)
     results = {
-        key: val for key, val in tag._as_dict().items() if val is not None
+        key: val for key, val in tag._as_dict().items() if val is not None and key != 'images'
     }
     expected = {
         key: val for key, val in expected.items() if key not in excluded_attrs
     }
     compare_tag(results, expected, filename)
-    assert tag._image_data is None
+    assert tag.images.front_cover.data is None
 
 
 @pytest.mark.parametrize("testfile,expected", testfiles.items())
@@ -641,14 +641,14 @@ def test_file_reading_duration(testfile: str, expected: dict[str, dict[str, Any]
     allowed_attrs = {"bitdepth", "bitrate", "channels", "duration", "filesize", "samplerate"}
     tag = TinyTag.get(filename, tags=False, duration=True)
     results = {
-        key: val for key, val in tag._as_dict().items() if val is not None
+        key: val for key, val in tag._as_dict().items() if val is not None and key != 'images'
     }
     expected = {
         key: val for key, val in expected.items() if key in allowed_attrs
     }
     expected["extra"] = {}
     compare_tag(results, expected, filename)
-    assert tag._image_data is None
+    assert tag.images.front_cover.data is None
 
 
 def test_pathlib_compatibility() -> None: