This is an automated email from the ASF dual-hosted git repository.

tballison pushed a commit to branch main
in repository https://gitbox.apache.org/repos/asf/tika.git


The following commit(s) were added to refs/heads/main by this push:
     new 8bce0bc1b4 TIKA-4801: Extract cover art from MP3, MP4, Vorbis and FLAC 
audio (#2983)
8bce0bc1b4 is described below

commit 8bce0bc1b4af5446a2d35bc101f2ce7a9011bd4a
Author: Dominik Schmidt <[email protected]>
AuthorDate: Tue Aug 4 23:17:58 2026 +0200

    TIKA-4801: Extract cover art from MP3, MP4, Vorbis and FLAC audio (#2983)
    
    * [TIKA-4801] Extract cover art from ID3v2 APIC frames
    
    Pictures embedded in ID3v2 tags (APIC in v2.3/v2.4, PIC in v2.2) are
    handed to the standard EmbeddedDocumentExtractor as inline embedded
    documents. The declared mime type becomes the embedded Content-Type,
    the picture description becomes dc:title and the APIC picture type is
    mapped to dc:description via the spec's label list. No new metadata
    properties are recorded on the audio document itself: consumers that
    only want to know whether cover art exists can use the embedded
    document count.
    
    Malformed picture frames (missing encoding, unterminated mime type or
    description, empty picture data) are skipped silently, matching the
    existing handling of malformed COMM frames. Linked v2.2 pictures
    (image format "-->") are skipped as they carry a URL, not image data.
    
    The fixture is a copy of testMP3id3v24.mp3 with a self-crafted APIC
    frame (minimal 1x1 PNG) inserted before the existing frames and the
    synchsafe tag size rewritten accordingly.
    
    * [TIKA-4801] Extract cover art from the MP4 covr atom
    
    The covr ilst entry, previously skipped explicitly, is handed to the
    standard EmbeddedDocumentExtractor as an inline embedded document, one
    per data atom (the atom may repeat for multiple images). The data
    atom's well-known value type declares the image format (13 = jpeg,
    14 = png, 27 = bmp); any other type is left for auto-detection. As in
    the ID3v2 commit, no new metadata properties are recorded on the audio
    document itself.
    
    The ParseContext needed for the embedded document extractor is plumbed
    from MP4Parser through TikaMp4BoxHandler into TikaUserDataBox.
    
    The fixture is a copy of testMP4.m4a with a self-crafted covr entry
    (minimal 1x1 PNG, type 14) appended to the ilst box and the ancestor
    box sizes patched.
    
    * [TIKA-4801] Extract cover art from Vorbis metadata block pictures
    
    METADATA_BLOCK_PICTURE comments, base64 encoded FLAC picture blocks as
    used by Vorbis and Opus (and shared by all Ogg family parsers through
    OggAudioParser.extractComments), are handed to the standard
    EmbeddedDocumentExtractor as inline embedded documents. The declared
    mime type becomes the embedded Content-Type, the description becomes
    dc:title and the picture type is mapped to dc:description through the
    ID3v2 APIC label list, which the FLAC picture spec reuses. The raw
    comment is excluded from the generic vorbis:* passthrough: a giant
    base64 metadata value helps nobody once the picture is an embedded
    document. Malformed or truncated blocks and linked pictures (mime
    "-->") are skipped silently.
    
    Native FLAC PICTURE blocks remain out of scope: vorbis-java 0.8 keeps
    them in FlacFile.otherMetadata without a public accessor.
    
    The fixture is a copy of testVORBIS.ogg with a self-crafted
    metadata_block_picture comment (minimal 1x1 PNG) inserted into the
    comment header packet, with the Ogg page lacing values and CRC
    rewritten.
    
    * [TIKA-4801] Extract cover art from native FLAC PICTURE blocks
    
    vorbis-java 0.8 parses the native FLAC metadata blocks but offers no
    accessor for them (FlacFile.getOtherMetadata() exists on their master,
    unreleased since 0.8 in 2016), so FlacParser walks the metadata region
    itself through a second stream over the spooled file: fLaC magic, then
    per block 1 header byte (last-block flag plus type) and a 24 bit BE
    length, stopping at the last-block flag, at end of stream or at a
    block that declares more data than is left. The PICTURE block payload
    (type 6) is byte-identical to the metadata_block_picture comment, so
    the parsing and inline embedded-document emission are shared with
    OggAudioParser through the extracted extractPictureBlock helper,
    keeping the semantics of the previous commit unchanged.
    
    Scope is native FLAC only: FLAC-in-Ogg carries its pictures in
    metadata_block_picture comments, which the previous commit already
    handles, and its PICTURE packets stay out of scope.
    
    The fixture is a copy of testFLAC.flac from the standard integration
    tests with a self-crafted PICTURE block (minimal 1x1 PNG) appended
    after the previous last metadata block, whose last-block flag is
    cleared.
    
    * [TIKA-4801] Emit all embedded pictures, not just the first
    
    An ID3v2.4 tag with several APIC frames only yielded the first
    picture. ID3v2.4 encodes frame sizes as synchsafe integers, but
    RawTag read them as plain integers, so any frame above 127 bytes,
    which every real cover picture is, made the frame walk overshoot
    into the middle of the following frames: with two covers the walk
    consumed the second APIC as trailing bytes of the first and stopped
    in the padding. The pictures were also emitted with that trailing
    garbage attached.
    
    RawTag now reads v2.4 frame sizes as synchsafe. Because widespread
    taggers (e.g. older iTunes) wrote plain integers into v2.4 tags, the
    two readings are disambiguated when they disagree: a size byte with
    the high bit set forces plain, otherwise the reading wins that lands
    the walk on a plausible next frame (frame id of capitals and digits,
    padding, or the tag end). Sizes below 128 bytes, like all frames in
    the existing v2.4 fixtures, are identical in both encodings.
    
    The ID3v2.3 (plain sizes) path, the MP4 covr repeats, the FLAC
    PICTURE walk and the Vorbis metadata_block_picture loop were checked
    against the same loss pattern and were already correct; multi-cover
    fixtures and tests for all of them are added as regression coverage.
    The MP3 fixtures were generated with ffmpeg -id3v2_version 4 / 3
    (front 64x40 png, back 30x30 png, both above 127 bytes); the MP4,
    FLAC and Ogg fixtures extend the single-cover fixtures with a second
    image (MP4: one covr record with a png and a jpeg data atom).
---
 .../java/org/apache/tika/parser/mp3/ID3Tags.java   |  86 +++++++++
 .../org/apache/tika/parser/mp3/ID3v22Handler.java  |  12 ++
 .../org/apache/tika/parser/mp3/ID3v23Handler.java  |  12 ++
 .../org/apache/tika/parser/mp3/ID3v24Handler.java  |  15 +-
 .../org/apache/tika/parser/mp3/ID3v2Frame.java     | 207 ++++++++++++++++++++-
 .../java/org/apache/tika/parser/mp3/Mp3Parser.java |  45 +++++
 .../java/org/apache/tika/parser/mp4/MP4Parser.java |   2 +-
 .../apache/tika/parser/mp4/TikaMp4BoxHandler.java  |   8 +-
 .../tika/parser/mp4/boxes/TikaUserDataBox.java     |  74 +++++++-
 .../org/apache/tika/parser/ogg/FlacParser.java     |  81 +++++++-
 .../org/apache/tika/parser/ogg/OggAudioParser.java | 131 ++++++++++++-
 .../org/apache/tika/parser/ogg/OpusParser.java     |   2 +-
 .../org/apache/tika/parser/ogg/SpeexParser.java    |   2 +-
 .../org/apache/tika/parser/ogg/TheoraParser.java   |   2 +-
 .../org/apache/tika/parser/ogg/VorbisParser.java   |   2 +-
 .../org/apache/tika/parser/mp3/Mp3ParserTest.java  |  64 +++++++
 .../org/apache/tika/parser/mp4/MP4ParserTest.java  |  37 ++++
 .../org/apache/tika/parser/ogg/FlacParserTest.java |  73 ++++++++
 .../apache/tika/parser/ogg/OggAudioParserTest.java |  66 ++++++-
 .../apache/tika/parser/ogg/VorbisParserTest.java   |  46 +++++
 .../test-documents/testFLAC_coverArt.flac          | Bin 0 -> 10726 bytes
 .../test-documents/testFLAC_twoCovers.flac         | Bin 0 -> 10988 bytes
 .../resources/test-documents/testMP3_coverArt.mp3  | Bin 0 -> 39611 bytes
 .../resources/test-documents/testMP3_twoCovers.mp3 | Bin 0 -> 40065 bytes
 .../test-documents/testMP3v23_twoCovers.mp3        | Bin 0 -> 40065 bytes
 .../resources/test-documents/testMP4_coverArt.m4a  | Bin 0 -> 4861 bytes
 .../resources/test-documents/testMP4_twoCovers.m4a | Bin 0 -> 5186 bytes
 .../test-documents/testVORBIS_coverArt.ogg         | Bin 0 -> 4429 bytes
 .../test-documents/testVORBIS_twoCovers.ogg        | Bin 0 -> 4802 bytes
 29 files changed, 945 insertions(+), 22 deletions(-)

diff --git 
a/tika-parsers/tika-parsers-standard/tika-parsers-standard-modules/tika-parser-audiovideo-module/src/main/java/org/apache/tika/parser/mp3/ID3Tags.java
 
b/tika-parsers/tika-parsers-standard/tika-parsers-standard-modules/tika-parser-audiovideo-module/src/main/java/org/apache/tika/parser/mp3/ID3Tags.java
index c314734881..a19a5cf072 100644
--- 
a/tika-parsers/tika-parsers-standard/tika-parsers-standard-modules/tika-parser-audiovideo-module/src/main/java/org/apache/tika/parser/mp3/ID3Tags.java
+++ 
b/tika-parsers/tika-parsers-standard/tika-parsers-standard-modules/tika-parser-audiovideo-module/src/main/java/org/apache/tika/parser/mp3/ID3Tags.java
@@ -16,6 +16,7 @@
  */
 package org.apache.tika.parser.mp3;
 
+import java.util.Collections;
 import java.util.List;
 
 /**
@@ -163,6 +164,36 @@ public interface ID3Tags {
             /* 125 */ "Dance Hall",
             /* sentinel */ ""};
 
+    /**
+     * List of predefined picture types for embedded pictures, indexed
+     * by the type byte of the ID3v2 APIC / PIC frames. The FLAC picture
+     * block, as also embedded in Vorbis comments, reuses the same list.
+     * <p>
+     * See <a 
href="https://id3.org/id3v2.4.0-frames";>http://id3.org/id3v2.4.0-frames</a>
+     */
+    String[] PICTURE_TYPES = new String[]{
+            /*  0 */ "Other",
+            /*  1 */ "32x32 pixels 'file icon' (PNG only)",
+            /*  2 */ "Other file icon",
+            /*  3 */ "Cover (front)",
+            /*  4 */ "Cover (back)",
+            /*  5 */ "Leaflet page",
+            /*  6 */ "Media (e.g. label side of CD)",
+            /*  7 */ "Lead artist/lead performer/soloist",
+            /*  8 */ "Artist/performer",
+            /*  9 */ "Conductor",
+            /* 10 */ "Band/Orchestra",
+            /* 11 */ "Composer",
+            /* 12 */ "Lyricist/text writer",
+            /* 13 */ "Recording Location",
+            /* 14 */ "During recording",
+            /* 15 */ "During performance",
+            /* 16 */ "Movie/video screen capture",
+            /* 17 */ "A bright coloured fish",
+            /* 18 */ "Illustration",
+            /* 19 */ "Band/artist logotype",
+            /* 20 */ "Publisher/Studio logotype"};
+
     /**
      * Does the file contain this kind of tags?
      */
@@ -198,6 +229,15 @@ public interface ID3Tags {
      */
     List<ID3Comment> getComments();
 
+    /**
+     * Retrieves the embedded pictures (e.g. cover art), if any.
+     * Only ID3v2 tags can carry pictures, so this defaults to
+     * an empty list.
+     */
+    default List<ID3Picture> getPictures() {
+        return Collections.emptyList();
+    }
+
     String getGenre();
 
     String getYear();
@@ -258,4 +298,50 @@ public interface ID3Tags {
             return text;
         }
     }
+
+    /**
+     * Represents an embedded picture in ID3 v2, such as cover art,
+     * as carried by the APIC (v2.3/v2.4) and PIC (v2.2) frames
+     */
+    class ID3Picture {
+        private final String mimeType;
+        private final String description;
+        private final int pictureType;
+        private final byte[] data;
+
+        public ID3Picture(String mimeType, String description, int 
pictureType, byte[] data) {
+            this.mimeType = mimeType;
+            this.description = description;
+            this.pictureType = pictureType;
+            this.data = data;
+        }
+
+        /**
+         * Gets the declared mime type, if present
+         */
+        public String getMimeType() {
+            return mimeType;
+        }
+
+        /**
+         * Gets the description, if present
+         */
+        public String getDescription() {
+            return description;
+        }
+
+        /**
+         * Gets the picture type byte, which indexes {@link #PICTURE_TYPES}
+         */
+        public int getPictureType() {
+            return pictureType;
+        }
+
+        /**
+         * Gets the raw picture data
+         */
+        public byte[] getData() {
+            return data;
+        }
+    }
 }
diff --git 
a/tika-parsers/tika-parsers-standard/tika-parsers-standard-modules/tika-parser-audiovideo-module/src/main/java/org/apache/tika/parser/mp3/ID3v22Handler.java
 
b/tika-parsers/tika-parsers-standard/tika-parsers-standard-modules/tika-parser-audiovideo-module/src/main/java/org/apache/tika/parser/mp3/ID3v22Handler.java
index 25bd4afc34..7d1dea0656 100644
--- 
a/tika-parsers/tika-parsers-standard/tika-parsers-standard-modules/tika-parser-audiovideo-module/src/main/java/org/apache/tika/parser/mp3/ID3v22Handler.java
+++ 
b/tika-parsers/tika-parsers-standard/tika-parsers-standard-modules/tika-parser-audiovideo-module/src/main/java/org/apache/tika/parser/mp3/ID3v22Handler.java
@@ -44,6 +44,7 @@ public class ID3v22Handler implements ID3Tags {
     private String albumArtist;
     private String disc;
     private List<ID3Comment> comments = new ArrayList<>();
+    private List<ID3Picture> pictures = new ArrayList<>();
 
     public ID3v22Handler(ID3v2Frame frame) throws IOException, SAXException, 
TikaException {
         RawTagIterator tags = new RawV22TagIterator(frame);
@@ -78,6 +79,13 @@ public class ID3v22Handler implements ID3Tags {
                     }
                     break;
                 }
+                case "PIC": {
+                    ID3Picture picture = ID3v2Frame.getV22Picture(tag.data, 0, 
tag.data.length);
+                    if (picture != null) {
+                        pictures.add(picture);
+                    }
+                    break;
+                }
                 case "TRK":
                     trackNumber = getTagString(tag.data, 0, tag.data.length);
                     break;
@@ -149,6 +157,10 @@ public class ID3v22Handler implements ID3Tags {
         return comments;
     }
 
+    public List<ID3Picture> getPictures() {
+        return pictures;
+    }
+
     public String getGenre() {
         return genre;
     }
diff --git 
a/tika-parsers/tika-parsers-standard/tika-parsers-standard-modules/tika-parser-audiovideo-module/src/main/java/org/apache/tika/parser/mp3/ID3v23Handler.java
 
b/tika-parsers/tika-parsers-standard/tika-parsers-standard-modules/tika-parser-audiovideo-module/src/main/java/org/apache/tika/parser/mp3/ID3v23Handler.java
index be3fd4fb11..cf1622fc11 100644
--- 
a/tika-parsers/tika-parsers-standard/tika-parsers-standard-modules/tika-parser-audiovideo-module/src/main/java/org/apache/tika/parser/mp3/ID3v23Handler.java
+++ 
b/tika-parsers/tika-parsers-standard/tika-parsers-standard-modules/tika-parser-audiovideo-module/src/main/java/org/apache/tika/parser/mp3/ID3v23Handler.java
@@ -45,6 +45,7 @@ public class ID3v23Handler implements ID3Tags {
     private String disc;
     private String compilation;
     private List<ID3Comment> comments = new ArrayList<>();
+    private List<ID3Picture> pictures = new ArrayList<>();
 
     public ID3v23Handler(ID3v2Frame frame) throws IOException, SAXException, 
TikaException {
         RawTagIterator tags = new RawV23TagIterator(frame);
@@ -79,6 +80,13 @@ public class ID3v23Handler implements ID3Tags {
                     }
                     break;
                 }
+                case "APIC": {
+                    ID3Picture picture = ID3v2Frame.getPicture(tag.data, 0, 
tag.data.length);
+                    if (picture != null) {
+                        pictures.add(picture);
+                    }
+                    break;
+                }
                 case "TRCK":
                     trackNumber = getTagString(tag.data, 0, tag.data.length);
                     break;
@@ -135,6 +143,10 @@ public class ID3v23Handler implements ID3Tags {
         return comments;
     }
 
+    public List<ID3Picture> getPictures() {
+        return pictures;
+    }
+
     public String getGenre() {
         return genre;
     }
diff --git 
a/tika-parsers/tika-parsers-standard/tika-parsers-standard-modules/tika-parser-audiovideo-module/src/main/java/org/apache/tika/parser/mp3/ID3v24Handler.java
 
b/tika-parsers/tika-parsers-standard/tika-parsers-standard-modules/tika-parser-audiovideo-module/src/main/java/org/apache/tika/parser/mp3/ID3v24Handler.java
index cb7fc38165..055ae9bcd4 100644
--- 
a/tika-parsers/tika-parsers-standard/tika-parsers-standard-modules/tika-parser-audiovideo-module/src/main/java/org/apache/tika/parser/mp3/ID3v24Handler.java
+++ 
b/tika-parsers/tika-parsers-standard/tika-parsers-standard-modules/tika-parser-audiovideo-module/src/main/java/org/apache/tika/parser/mp3/ID3v24Handler.java
@@ -46,6 +46,7 @@ public class ID3v24Handler implements ID3Tags {
     private String disc;
     private String compilation;
     private List<ID3Comment> comments = new ArrayList<>();
+    private List<ID3Picture> pictures = new ArrayList<>();
 
     public ID3v24Handler(ID3v2Frame frame) throws IOException, SAXException, 
TikaException {
         RawTagIterator tags = new RawV24TagIterator(frame);
@@ -85,6 +86,13 @@ public class ID3v24Handler implements ID3Tags {
                     }
                     break;
                 }
+                case "APIC": {
+                    ID3Picture picture = ID3v2Frame.getPicture(tag.data, 0, 
tag.data.length);
+                    if (picture != null) {
+                        pictures.add(picture);
+                    }
+                    break;
+                }
                 case "TRCK":
                     trackNumber = getTagString(tag.data, 0, tag.data.length);
                     break;
@@ -141,6 +149,10 @@ public class ID3v24Handler implements ID3Tags {
         return comments;
     }
 
+    public List<ID3Picture> getPictures() {
+        return pictures;
+    }
+
     public String getGenre() {
         return genre;
     }
@@ -163,7 +175,8 @@ public class ID3v24Handler implements ID3Tags {
 
     private class RawV24TagIterator extends RawTagIterator {
         private RawV24TagIterator(ID3v2Frame frame) {
-            frame.super(4, 4, 1, 2);
+            //v2.4 frame sizes are synchsafe integers
+            frame.super(4, 4, 1, 2, true);
         }
     }
 }
diff --git 
a/tika-parsers/tika-parsers-standard/tika-parsers-standard-modules/tika-parser-audiovideo-module/src/main/java/org/apache/tika/parser/mp3/ID3v2Frame.java
 
b/tika-parsers/tika-parsers-standard/tika-parsers-standard-modules/tika-parser-audiovideo-module/src/main/java/org/apache/tika/parser/mp3/ID3v2Frame.java
index 3b40a5344b..b8403deff0 100644
--- 
a/tika-parsers/tika-parsers-standard/tika-parsers-standard-modules/tika-parser-audiovideo-module/src/main/java/org/apache/tika/parser/mp3/ID3v2Frame.java
+++ 
b/tika-parsers/tika-parsers-standard/tika-parsers-standard-modules/tika-parser-audiovideo-module/src/main/java/org/apache/tika/parser/mp3/ID3v2Frame.java
@@ -25,6 +25,7 @@ import java.io.UnsupportedEncodingException;
 import java.util.Iterator;
 
 import org.apache.tika.parser.mp3.ID3Tags.ID3Comment;
+import org.apache.tika.parser.mp3.ID3Tags.ID3Picture;
 
 /**
  * A frame of ID3v2 data, which is then passed to a handler to
@@ -354,6 +355,137 @@ public class ID3v2Frame implements MP3Frame {
         }
     }
 
+    /**
+     * Parses the picture parts from an ID3v2.3/v2.4 APIC frame, or null
+     * if the frame is too short or malformed to hold a picture.
+     */
+    protected static ID3Picture getPicture(byte[] data, int offset, int 
length) {
+        // encoding flag + empty mime terminator + picture type
+        if (length < 3) {
+            return null;
+        }
+
+        // Pictures must have an encoding
+        int encodingFlag = data[offset];
+        if (encodingFlag < 0 || encodingFlag >= encodings.length) {
+            // Invalid picture
+            return null;
+        }
+        TextEncoding encoding = encodings[encodingFlag];
+
+        int end = offset + length;
+
+        // First is the mime type, always ISO-8859-1 and null terminated
+        int mimeStart = offset + 1;
+        int mimeEnd = -1;
+        for (int i = mimeStart; i < end; i++) {
+            if (data[i] == 0) {
+                mimeEnd = i;
+                break;
+            }
+        }
+        if (mimeEnd == -1 || mimeEnd + 1 >= end) {
+            return null;
+        }
+        String mimeType = getString(data, mimeStart, mimeEnd - mimeStart);
+        if (mimeType.isEmpty()) {
+            // Leave the type for auto-detection
+            mimeType = null;
+        }
+
+        // Then one byte of picture type
+        int pictureType = data[mimeEnd + 1] & 0xFF;
+
+        // Then the description and the picture data
+        return getPictureWithDescription(data, mimeEnd + 2, end, encoding, 
mimeType, pictureType);
+    }
+
+    /**
+     * Parses the picture parts from an ID3v2.2 PIC frame, which declares
+     * a three letter image format instead of a mime type. Linked pictures
+     * (format "-->") are skipped, they hold a URL rather than image data.
+     */
+    protected static ID3Picture getV22Picture(byte[] data, int offset, int 
length) {
+        // encoding flag + 3 byte image format + picture type
+        if (length < 5) {
+            return null;
+        }
+
+        // Pictures must have an encoding
+        int encodingFlag = data[offset];
+        if (encodingFlag < 0 || encodingFlag >= encodings.length) {
+            // Invalid picture
+            return null;
+        }
+        TextEncoding encoding = encodings[encodingFlag];
+
+        String format = getString(data, offset + 1, 3);
+        String mimeType;
+        if ("PNG".equals(format)) {
+            mimeType = "image/png";
+        } else if ("JPG".equals(format)) {
+            mimeType = "image/jpeg";
+        } else if ("-->".equals(format)) {
+            // A link to a picture, not an embedded one
+            return null;
+        } else {
+            // Leave the type for auto-detection
+            mimeType = null;
+        }
+
+        // Then one byte of picture type
+        int pictureType = data[offset + 4] & 0xFF;
+
+        // Then the description and the picture data
+        return getPictureWithDescription(data, offset + 5, offset + length, 
encoding, mimeType,
+                pictureType);
+    }
+
+    /**
+     * Reads a picture frame's description, terminated per the text encoding,
+     * and the picture data following it. Returns null when the terminator or
+     * the picture data is missing.
+     */
+    private static ID3Picture getPictureWithDescription(byte[] data, int 
descStart, int end,
+                                                        TextEncoding encoding, 
String mimeType,
+                                                        int pictureType) {
+        int dataStart = -1;
+        String description = null;
+        try {
+            if (encoding.doubleByte) {
+                // a double byte terminator needs both bytes present, and sits
+                // on a two byte boundary relative to the description start
+                for (int i = descStart; i + 1 < end; i += 2) {
+                    if (data[i] == 0 && data[i + 1] == 0) {
+                        description = decodeText(data, descStart, i - 
descStart, encoding);
+                        dataStart = i + 2;
+                        break;
+                    }
+                }
+            } else {
+                for (int i = descStart; i < end; i++) {
+                    if (data[i] == 0) {
+                        description = decodeText(data, descStart, i - 
descStart, encoding);
+                        dataStart = i + 1;
+                        break;
+                    }
+                }
+            }
+        } catch (UnsupportedEncodingException e) {
+            throw new RuntimeException("Core encoding " + encoding.encoding + 
" is not available",
+                    e);
+        }
+
+        // Without a terminated description there is no picture data
+        if (dataStart == -1 || dataStart >= end) {
+            return null;
+        }
+
+        byte[] picture = new byte[end - dataStart];
+        System.arraycopy(data, dataStart, picture, 0, picture.length);
+        return new ID3Picture(mimeType, description, pictureType, picture);
+    }
+
     /**
      * Returns the String at the given
      * offset and length. Strings are ISO-8859-1
@@ -407,7 +539,7 @@ public class ID3v2Frame implements MP3Frame {
         private int headerSize;
 
         private RawTag(int nameLength, int sizeLength, int sizeMultiplier, int 
flagLength,
-                       byte[] frameData, int offset) {
+                       boolean synchsafeSize, byte[] frameData, int offset) {
             headerSize = nameLength + sizeLength + flagLength;
 
             // Name, normally 3 or 4 bytes
@@ -417,6 +549,8 @@ public class ID3v2Frame implements MP3Frame {
             int rawSize;
             if (sizeLength == 3) {
                 rawSize = getInt3(frameData, offset + nameLength);
+            } else if (synchsafeSize) {
+                rawSize = getV24FrameSize(frameData, offset, headerSize, 
nameLength);
             } else {
                 rawSize = getInt(frameData, offset + nameLength);
             }
@@ -443,6 +577,66 @@ public class ID3v2Frame implements MP3Frame {
             return headerSize + data.length;
         }
 
+        /**
+         * Returns the size of an ID3v2.4 frame. The spec encodes frame
+         * sizes as synchsafe integers, but widespread taggers (e.g. older
+         * iTunes) wrote plain integers instead. Reading a synchsafe size
+         * as a plain integer (or the other way around) makes the frame
+         * walk skip into the middle of the following frames, losing them,
+         * so when the two readings disagree, pick the one that lands the
+         * walk on a plausible next frame.
+         */
+        private static int getV24FrameSize(byte[] frameData, int offset, int 
headerSize,
+                                           int nameLength) {
+            int plain = getInt(frameData, offset + nameLength);
+            // A size byte with the high bit set cannot be synchsafe
+            if (((frameData[offset + nameLength] | frameData[offset + 
nameLength + 1] |
+                    frameData[offset + nameLength + 2] | frameData[offset + 
nameLength + 3]) &
+                    0x80) != 0) {
+                return plain;
+            }
+            int synchsafe = get7BitsInt(frameData, offset + nameLength);
+            if (synchsafe == plain) {
+                return synchsafe;
+            }
+            if (isPlausibleFrameStart(frameData, offset + headerSize + 
synchsafe)) {
+                return synchsafe;
+            }
+            if (isPlausibleFrameStart(frameData, offset + headerSize + plain)) 
{
+                return plain;
+            }
+            // Neither reading looks right, go with the spec
+            return synchsafe;
+        }
+
+        /**
+         * Checks whether the given offset is a plausible place for the
+         * next frame to start: the end of the tag, padding, or a frame id
+         * made of capital letters and digits.
+         */
+        private static boolean isPlausibleFrameStart(byte[] frameData, int 
nextOffset) {
+            if (nextOffset < 0 || nextOffset > frameData.length) {
+                return false;
+            }
+            if (nextOffset == frameData.length) {
+                return true;
+            }
+            if (frameData[nextOffset] == 0) {
+                // Padding
+                return true;
+            }
+            if (nextOffset + 4 > frameData.length) {
+                return false;
+            }
+            for (int i = nextOffset; i < nextOffset + 4; i++) {
+                byte b = frameData[i];
+                if (!((b >= 'A' && b <= 'Z') || (b >= '0' && b <= '9'))) {
+                    return false;
+                }
+            }
+            return true;
+        }
+
     }
 
     /**
@@ -455,15 +649,22 @@ public class ID3v2Frame implements MP3Frame {
         private int sizeLength;
         private int sizeMultiplier;
         private int flagLength;
+        private boolean synchsafeSize;
 
         private int offset = 0;
 
         protected RawTagIterator(int nameLength, int sizeLength, int 
sizeMultiplier,
                                  int flagLength) {
+            this(nameLength, sizeLength, sizeMultiplier, flagLength, false);
+        }
+
+        protected RawTagIterator(int nameLength, int sizeLength, int 
sizeMultiplier,
+                                 int flagLength, boolean synchsafeSize) {
             this.nameLength = nameLength;
             this.sizeLength = sizeLength;
             this.sizeMultiplier = sizeMultiplier;
             this.flagLength = flagLength;
+            this.synchsafeSize = synchsafeSize;
         }
 
         public boolean hasNext() {
@@ -472,8 +673,8 @@ public class ID3v2Frame implements MP3Frame {
         }
 
         public RawTag next() {
-            RawTag tag =
-                    new RawTag(nameLength, sizeLength, sizeMultiplier, 
flagLength, data, offset);
+            RawTag tag = new RawTag(nameLength, sizeLength, sizeMultiplier, 
flagLength,
+                    synchsafeSize, data, offset);
             offset += tag.getSize();
             return tag;
         }
diff --git 
a/tika-parsers/tika-parsers-standard/tika-parsers-standard-modules/tika-parser-audiovideo-module/src/main/java/org/apache/tika/parser/mp3/Mp3Parser.java
 
b/tika-parsers/tika-parsers-standard/tika-parsers-standard-modules/tika-parser-audiovideo-module/src/main/java/org/apache/tika/parser/mp3/Mp3Parser.java
index 82fb40dd4f..113e6370ab 100644
--- 
a/tika-parsers/tika-parsers-standard/tika-parsers-standard-modules/tika-parser-audiovideo-module/src/main/java/org/apache/tika/parser/mp3/Mp3Parser.java
+++ 
b/tika-parsers/tika-parsers-standard/tika-parsers-standard-modules/tika-parser-audiovideo-module/src/main/java/org/apache/tika/parser/mp3/Mp3Parser.java
@@ -28,6 +28,8 @@ import org.xml.sax.SAXException;
 
 import org.apache.tika.annotation.TikaComponent;
 import org.apache.tika.exception.TikaException;
+import org.apache.tika.extractor.EmbeddedDocumentExtractor;
+import org.apache.tika.extractor.EmbeddedDocumentUtil;
 import org.apache.tika.io.TailStream;
 import org.apache.tika.io.TikaInputStream;
 import org.apache.tika.metadata.Audio;
@@ -39,6 +41,7 @@ import org.apache.tika.parser.ParseContext;
 import org.apache.tika.parser.Parser;
 import org.apache.tika.parser.audio.NumberAndTotal;
 import org.apache.tika.parser.mp3.ID3Tags.ID3Comment;
+import org.apache.tika.parser.mp3.ID3Tags.ID3Picture;
 import org.apache.tika.sax.XHTMLContentHandler;
 
 /**
@@ -291,9 +294,51 @@ public class Mp3Parser implements Parser {
             xhtml.endElement("p");
         }
 
+        // Any embedded pictures, such as cover art, become
+        //  embedded documents of the audio file
+        extractPictures(audioAndTags.tags, xhtml, context);
+
         xhtml.endDocument();
     }
 
+    /**
+     * Sends the embedded pictures, such as cover art, from the ID3v2 tags
+     * to the embedded document extractor. The pictures only become embedded
+     * documents, no metadata is recorded on the audio document itself.
+     */
+    private static void extractPictures(ID3Tags[] tags, XHTMLContentHandler 
xhtml,
+                                        ParseContext context)
+            throws IOException, SAXException {
+        EmbeddedDocumentExtractor extractor = null;
+        for (ID3Tags tag : tags) {
+            for (ID3Picture picture : tag.getPictures()) {
+                if (extractor == null) {
+                    extractor = 
EmbeddedDocumentUtil.getEmbeddedDocumentExtractor(context);
+                }
+                Metadata pictureMetadata = Metadata.newInstance(context);
+                pictureMetadata.set(TikaCoreProperties.EMBEDDED_RESOURCE_TYPE,
+                        
TikaCoreProperties.EmbeddedResourceType.INLINE.toString());
+                if (picture.getMimeType() != null) {
+                    pictureMetadata.set(Metadata.CONTENT_TYPE, 
picture.getMimeType());
+                }
+                if (picture.getDescription() != null && 
!picture.getDescription().isEmpty()) {
+                    pictureMetadata.set(TikaCoreProperties.TITLE, 
picture.getDescription());
+                }
+                if (picture.getPictureType() >= 0 &&
+                        picture.getPictureType() < 
ID3Tags.PICTURE_TYPES.length) {
+                    pictureMetadata.set(TikaCoreProperties.DESCRIPTION,
+                            ID3Tags.PICTURE_TYPES[picture.getPictureType()]);
+                }
+                if (extractor.shouldParseEmbedded(pictureMetadata)) {
+                    try (TikaInputStream pictureStream = 
TikaInputStream.get(picture.getData())) {
+                        extractor.parseEmbedded(pictureStream, xhtml, 
pictureMetadata, context,
+                                true);
+                    }
+                }
+            }
+        }
+    }
+
     /**
      * This statically sets the max record size in {@link ID3v2Frame}
      *
diff --git 
a/tika-parsers/tika-parsers-standard/tika-parsers-standard-modules/tika-parser-audiovideo-module/src/main/java/org/apache/tika/parser/mp4/MP4Parser.java
 
b/tika-parsers/tika-parsers-standard/tika-parsers-standard-modules/tika-parser-audiovideo-module/src/main/java/org/apache/tika/parser/mp4/MP4Parser.java
index 0e8a46b396..e450c1675b 100644
--- 
a/tika-parsers/tika-parsers-standard/tika-parsers-standard-modules/tika-parser-audiovideo-module/src/main/java/org/apache/tika/parser/mp4/MP4Parser.java
+++ 
b/tika-parsers/tika-parsers-standard/tika-parsers-standard-modules/tika-parser-audiovideo-module/src/main/java/org/apache/tika/parser/mp4/MP4Parser.java
@@ -100,7 +100,7 @@ public class MP4Parser implements Parser {
         XHTMLContentHandler xhtml = new XHTMLContentHandler(handler, metadata, 
context);
         xhtml.startDocument();
         com.drew.metadata.Metadata mp4Metadata = new 
com.drew.metadata.Metadata();
-        Mp4BoxHandler boxHandler = new TikaMp4BoxHandler(mp4Metadata, 
metadata, xhtml);
+        Mp4BoxHandler boxHandler = new TikaMp4BoxHandler(mp4Metadata, 
metadata, xhtml, context);
         //we used to spool to disk and then read from that with sannies parser.
         //we think that drewnoakes' parser streams the data so we don't need 
to spool
         try {
diff --git 
a/tika-parsers/tika-parsers-standard/tika-parsers-standard-modules/tika-parser-audiovideo-module/src/main/java/org/apache/tika/parser/mp4/TikaMp4BoxHandler.java
 
b/tika-parsers/tika-parsers-standard/tika-parsers-standard-modules/tika-parser-audiovideo-module/src/main/java/org/apache/tika/parser/mp4/TikaMp4BoxHandler.java
index 9d15d3afeb..0dfc66681f 100644
--- 
a/tika-parsers/tika-parsers-standard/tika-parsers-standard-modules/tika-parser-audiovideo-module/src/main/java/org/apache/tika/parser/mp4/TikaMp4BoxHandler.java
+++ 
b/tika-parsers/tika-parsers-standard/tika-parsers-standard-modules/tika-parser-audiovideo-module/src/main/java/org/apache/tika/parser/mp4/TikaMp4BoxHandler.java
@@ -34,6 +34,7 @@ import com.drew.metadata.mp4.Mp4Directory;
 import org.xml.sax.SAXException;
 
 import org.apache.tika.metadata.TikaCoreProperties;
+import org.apache.tika.parser.ParseContext;
 import org.apache.tika.parser.mp4.boxes.ISO6709;
 import org.apache.tika.parser.mp4.boxes.TikaUserDataBox;
 import org.apache.tika.sax.XHTMLContentHandler;
@@ -53,6 +54,7 @@ public class TikaMp4BoxHandler extends Mp4BoxHandler {
 
     org.apache.tika.metadata.Metadata tikaMetadata;
     final XHTMLContentHandler xhtml;
+    private final ParseContext parseContext;
 
     //key names for the current 'meta' box, filled from its 'keys' box and 
consumed
     //by the following 'ilst' box (e.g. com.apple.quicktime.content.identifier)
@@ -63,10 +65,11 @@ public class TikaMp4BoxHandler extends Mp4BoxHandler {
     private long emptyEditDuration = -1;
 
     public TikaMp4BoxHandler(Metadata metadata, 
org.apache.tika.metadata.Metadata tikaMetadata,
-                             XHTMLContentHandler xhtml) {
+                             XHTMLContentHandler xhtml, ParseContext 
parseContext) {
         super(metadata);
         this.tikaMetadata = tikaMetadata;
         this.xhtml = xhtml;
+        this.parseContext = parseContext;
     }
 
     @Override
@@ -134,7 +137,8 @@ public class TikaMp4BoxHandler extends Mp4BoxHandler {
             return this;
         }
         try {
-            new TikaUserDataBox(box, payload, tikaMetadata, 
xhtml).addMetadata(directory);
+            new TikaUserDataBox(box, payload, tikaMetadata, xhtml, 
parseContext)
+                    .addMetadata(directory);
         } catch (SAXException e) {
             throw new IOException(e);
         }
diff --git 
a/tika-parsers/tika-parsers-standard/tika-parsers-standard-modules/tika-parser-audiovideo-module/src/main/java/org/apache/tika/parser/mp4/boxes/TikaUserDataBox.java
 
b/tika-parsers/tika-parsers-standard/tika-parsers-standard-modules/tika-parser-audiovideo-module/src/main/java/org/apache/tika/parser/mp4/boxes/TikaUserDataBox.java
index 9d6e293073..72fd54ca4b 100644
--- 
a/tika-parsers/tika-parsers-standard/tika-parsers-standard-modules/tika-parser-audiovideo-module/src/main/java/org/apache/tika/parser/mp4/boxes/TikaUserDataBox.java
+++ 
b/tika-parsers/tika-parsers-standard/tika-parsers-standard-modules/tika-parser-audiovideo-module/src/main/java/org/apache/tika/parser/mp4/boxes/TikaUserDataBox.java
@@ -27,11 +27,15 @@ import com.drew.metadata.mp4.Mp4Directory;
 import org.xml.sax.SAXException;
 
 import org.apache.tika.exception.RuntimeSAXException;
+import org.apache.tika.extractor.EmbeddedDocumentExtractor;
+import org.apache.tika.extractor.EmbeddedDocumentUtil;
+import org.apache.tika.io.TikaInputStream;
 import org.apache.tika.metadata.Audio;
 import org.apache.tika.metadata.Metadata;
 import org.apache.tika.metadata.TikaCoreProperties;
 import org.apache.tika.metadata.XMP;
 import org.apache.tika.metadata.XMPDM;
+import org.apache.tika.parser.ParseContext;
 import org.apache.tika.sax.XHTMLContentHandler;
 
 public class TikaUserDataBox {
@@ -49,10 +53,13 @@ public class TikaUserDataBox {
     private boolean isQuickTime = false;
     private final Metadata metadata;
     private final XHTMLContentHandler xhtml;
+    private final ParseContext parseContext;
     public TikaUserDataBox(@NotNull String box, byte[] payload, Metadata 
metadata,
-                           XHTMLContentHandler xhtml) throws IOException, 
SAXException {
+                           XHTMLContentHandler xhtml, ParseContext 
parseContext)
+            throws IOException, SAXException {
         this.metadata = metadata;
         this.xhtml = xhtml;
+        this.parseContext = parseContext;
         int length = payload.length;
         SequentialReader reader = new SequentialByteArrayReader(payload);
         while (reader.getPosition() < (long) length) {
@@ -139,7 +146,10 @@ public class TikaUserDataBox {
             String typeName = reader.getString(4, 
StandardCharsets.ISO_8859_1);//data
             totalRead += 16;
             if ("data".equals(typeName)) {
-                reader.skip(8);//not sure what these are
+                //1 byte version and 3 bytes flags; for the "well-known"
+                //types the flags hold the value type
+                long valueType = reader.getUInt32() & 0xFFFFFF;
+                reader.skip(4L);//locale
                 totalRead += 8;
                 int toRead = (int) fieldLen - 16;
                 if (toRead <= 0) {
@@ -147,9 +157,31 @@ public class TikaUserDataBox {
                     return;
                 }
                 if ("covr".equals(fieldName)) {
-                    //covr can be an image file, e.g. png or jpeg
-                    //skip this for now
-                    reader.skip(toRead);
+                    //covr holds one image file (e.g. png or jpeg) per data
+                    //atom, and may repeat the data atom for further images
+                    handleCoverArt(reader, valueType, toRead);
+                    long remaining = recordLen - 8 - fieldLen;
+                    while (remaining >= 16) {
+                        long extraLen = reader.getUInt32();
+                        String extraTypeName = reader.getString(4, 
StandardCharsets.ISO_8859_1);
+                        long extraValueType = reader.getUInt32() & 0xFFFFFF;
+                        reader.skip(4L);//locale
+                        totalRead += 16;
+                        remaining -= 16;
+                        int extraToRead = (int) extraLen - 16;
+                        if (!"data".equals(extraTypeName) || extraToRead <= 0 
||
+                                extraToRead > remaining) {
+                            //malformed, skip the rest of the record
+                            break;
+                        }
+                        handleCoverArt(reader, extraValueType, extraToRead);
+                        totalRead += extraToRead;
+                        remaining -= extraToRead;
+                    }
+                    if (remaining > 0) {
+                        reader.skip(remaining);
+                        totalRead += remaining;
+                    }
                 } else if ("cpil".equals(fieldName)) {
                     int compilationId = (int)reader.getByte();
                     metadata.set(XMPDM.COMPILATION, compilationId);
@@ -205,6 +237,38 @@ public class TikaUserDataBox {
     }
 
 
+    /**
+     * Sends one embedded cover image to the embedded document extractor.
+     * The image only becomes an embedded document, no metadata is recorded
+     * on the audio document itself.
+     */
+    private void handleCoverArt(SequentialReader reader, long valueType, int 
length)
+            throws IOException {
+        byte[] picture = reader.getBytes(length);
+        Metadata pictureMetadata = Metadata.newInstance(parseContext);
+        pictureMetadata.set(TikaCoreProperties.EMBEDDED_RESOURCE_TYPE,
+                TikaCoreProperties.EmbeddedResourceType.INLINE.toString());
+        //the data atom's well-known value type declares the image format;
+        //for any other type leave the content type for auto-detection
+        if (valueType == 13) {
+            pictureMetadata.set(Metadata.CONTENT_TYPE, "image/jpeg");
+        } else if (valueType == 14) {
+            pictureMetadata.set(Metadata.CONTENT_TYPE, "image/png");
+        } else if (valueType == 27) {
+            pictureMetadata.set(Metadata.CONTENT_TYPE, "image/bmp");
+        }
+        EmbeddedDocumentExtractor extractor =
+                
EmbeddedDocumentUtil.getEmbeddedDocumentExtractor(parseContext);
+        if (extractor.shouldParseEmbedded(pictureMetadata)) {
+            try (TikaInputStream tis = TikaInputStream.get(picture)) {
+                extractor.parseEmbedded(tis, xhtml, pictureMetadata, 
parseContext, true);
+            } catch (SAXException e) {
+                //need to punch through IOException catching in MP4Reader
+                throw new RuntimeSAXException(e);
+            }
+        }
+    }
+
     private void addMetadata(String key, String value) throws SAXException {
         switch (key) {
             case "\u00A9nam":
diff --git 
a/tika-parsers/tika-parsers-standard/tika-parsers-standard-modules/tika-parser-audiovideo-module/src/main/java/org/apache/tika/parser/ogg/FlacParser.java
 
b/tika-parsers/tika-parsers-standard/tika-parsers-standard-modules/tika-parser-audiovideo-module/src/main/java/org/apache/tika/parser/ogg/FlacParser.java
index a8592564e9..e251682c4b 100644
--- 
a/tika-parsers/tika-parsers-standard/tika-parsers-standard-modules/tika-parser-audiovideo-module/src/main/java/org/apache/tika/parser/ogg/FlacParser.java
+++ 
b/tika-parsers/tika-parsers-standard/tika-parsers-standard-modules/tika-parser-audiovideo-module/src/main/java/org/apache/tika/parser/ogg/FlacParser.java
@@ -16,7 +16,12 @@
  */
 package org.apache.tika.parser.ogg;
 
+import java.io.BufferedInputStream;
+import java.io.EOFException;
 import java.io.IOException;
+import java.io.InputStream;
+import java.nio.file.Files;
+import java.nio.file.Path;
 import java.util.Arrays;
 import java.util.HashSet;
 import java.util.List;
@@ -31,6 +36,8 @@ import org.xml.sax.SAXException;
 
 import org.apache.tika.annotation.TikaComponent;
 import org.apache.tika.exception.TikaException;
+import org.apache.tika.extractor.EmbeddedDocumentExtractor;
+import org.apache.tika.extractor.EmbeddedDocumentUtil;
 import org.apache.tika.io.TikaInputStream;
 import org.apache.tika.metadata.Metadata;
 import org.apache.tika.metadata.XMPDM;
@@ -53,6 +60,11 @@ public class FlacParser extends AbstractParser {
 
     private static List<MediaType> TYPES = Arrays.asList(NATIVE_FLAC, 
OGG_FLAC);
 
+    /**
+     * The metadata block type of a native FLAC PICTURE block
+     */
+    private static final int PICTURE_BLOCK_TYPE = 6;
+
     @Override
     public Set<MediaType> getSupportedTypes(ParseContext context) {
         return new HashSet<>(TYPES);
@@ -64,6 +76,11 @@ public class FlacParser extends AbstractParser {
             throws IOException, TikaException, SAXException {
         metadata.set(XMPDM.AUDIO_COMPRESSOR, "FLAC");
 
+        // Spool to a file first: FlacFile.open consumes the stream, but
+        //  native FLAC PICTURE blocks are read through a second stream
+        //  over the file later on
+        Path path = tis.getPath();
+
         // Open the FLAC file
         FlacFile flac = FlacFile.open(tis);
 
@@ -83,7 +100,14 @@ public class FlacParser extends AbstractParser {
         }
 
         // Extract any Vorbis-style comments
-        OggAudioParser.extractComments(metadata, xhtml, flac.getTags());
+        OggAudioParser.extractComments(metadata, xhtml, flac.getTags(), 
context);
+
+        // Extract any embedded pictures, such as cover art, from native
+        //  FLAC PICTURE metadata blocks (Ogg-contained FLAC carries its
+        //  pictures in metadata_block_picture comments instead)
+        if (!(flac instanceof FlacOggFile)) {
+            extractNativePictures(path, xhtml, context);
+        }
 
         // Extract duration if available from header
         FlacInfo info = flac.getInfo();
@@ -101,4 +125,59 @@ public class FlacParser extends AbstractParser {
         metadata.set(XMPDM.AUDIO_SAMPLE_RATE, (int) info.getSampleRate());
         OggAudioParser.extractChannelInfo(metadata, info.getNumChannels());
     }
+
+    /**
+     * Walks the metadata blocks of a native FLAC file and sends any
+     * PICTURE blocks to the embedded document extractor. Their payload is
+     * identical to the metadata_block_picture comments handled by
+     * {@link OggAudioParser}. vorbis-java parses these blocks but keeps
+     * them without a public accessor, so they are read through a second
+     * stream over the spooled file, leaving the main parse untouched.
+     * The walk stops at the block flagged as last, at the end of the
+     * stream, or at a block that declares more data than is left.
+     * TODO: remove this block walk once a vorbis-java release ships
+     * FlacFile.getOtherMetadata(), present on their master but unreleased
+     * as of 0.8, see https://github.com/Gagravarr/VorbisJava/issues/46
+     */
+    private static void extractNativePictures(Path path, XHTMLContentHandler 
xhtml,
+            ParseContext context) throws IOException, SAXException {
+        EmbeddedDocumentExtractor extractor = null;
+        try (InputStream stream = new 
BufferedInputStream(Files.newInputStream(path))) {
+            byte[] magic = stream.readNBytes(4);
+            if (magic.length != 4 || magic[0] != 'f' || magic[1] != 'L'
+                    || magic[2] != 'a' || magic[3] != 'C') {
+                return;
+            }
+            boolean lastBlock = false;
+            while (!lastBlock) {
+                // 1 byte of last-block flag and block type, then a
+                //  24 bit BE block length
+                byte[] header = stream.readNBytes(4);
+                if (header.length != 4) {
+                    return;
+                }
+                lastBlock = (header[0] & 0x80) != 0;
+                int blockType = header[0] & 0x7F;
+                int blockLength = ((header[1] & 0xFF) << 16) | ((header[2] & 
0xFF) << 8)
+                        | (header[3] & 0xFF);
+                if (blockType == PICTURE_BLOCK_TYPE) {
+                    byte[] block = stream.readNBytes(blockLength);
+                    if (block.length != blockLength) {
+                        return;
+                    }
+                    if (extractor == null) {
+                        extractor = 
EmbeddedDocumentUtil.getEmbeddedDocumentExtractor(context);
+                    }
+                    OggAudioParser.extractPictureBlock(block, xhtml, context, 
extractor);
+                } else {
+                    try {
+                        stream.skipNBytes(blockLength);
+                    } catch (EOFException e) {
+                        //truncated block, stop the walk
+                        return;
+                    }
+                }
+            }
+        }
+    }
 }
diff --git 
a/tika-parsers/tika-parsers-standard/tika-parsers-standard-modules/tika-parser-audiovideo-module/src/main/java/org/apache/tika/parser/ogg/OggAudioParser.java
 
b/tika-parsers/tika-parsers-standard/tika-parsers-standard-modules/tika-parser-audiovideo-module/src/main/java/org/apache/tika/parser/ogg/OggAudioParser.java
index c590e43400..7d0b043889 100644
--- 
a/tika-parsers/tika-parsers-standard/tika-parsers-standard-modules/tika-parser-audiovideo-module/src/main/java/org/apache/tika/parser/ogg/OggAudioParser.java
+++ 
b/tika-parsers/tika-parsers-standard/tika-parsers-standard-modules/tika-parser-audiovideo-module/src/main/java/org/apache/tika/parser/ogg/OggAudioParser.java
@@ -17,9 +17,14 @@
 package org.apache.tika.parser.ogg;
 
 import java.io.IOException;
+import java.nio.BufferUnderflowException;
+import java.nio.ByteBuffer;
+import java.nio.charset.Charset;
+import java.nio.charset.StandardCharsets;
 import java.text.DecimalFormat;
 import java.text.NumberFormat;
 import java.util.Arrays;
+import java.util.Base64;
 import java.util.List;
 import java.util.Locale;
 
@@ -32,6 +37,9 @@ import org.gagravarr.vorbis.VorbisStyleComments;
 import org.xml.sax.SAXException;
 
 import org.apache.tika.exception.TikaException;
+import org.apache.tika.extractor.EmbeddedDocumentExtractor;
+import org.apache.tika.extractor.EmbeddedDocumentUtil;
+import org.apache.tika.io.TikaInputStream;
 import org.apache.tika.metadata.Audio;
 import org.apache.tika.metadata.Metadata;
 import org.apache.tika.metadata.PassthroughPrefix;
@@ -39,7 +47,9 @@ import org.apache.tika.metadata.TikaCoreProperties;
 import org.apache.tika.metadata.XMP;
 import org.apache.tika.metadata.XMPDM;
 import org.apache.tika.parser.AbstractParser;
+import org.apache.tika.parser.ParseContext;
 import org.apache.tika.parser.audio.NumberAndTotal;
+import org.apache.tika.parser.mp3.ID3Tags;
 import org.apache.tika.sax.XHTMLContentHandler;
 
 /**
@@ -52,6 +62,12 @@ public abstract class OggAudioParser extends AbstractParser {
     private static final PassthroughPrefix VORBIS =
             PassthroughPrefix.file("vorbis:", "Vorbis comment field names");
 
+    /**
+     * Comment holding an embedded picture (e.g. cover art) as a base64
+     * encoded FLAC picture block
+     */
+    private static final String METADATA_BLOCK_PICTURE = 
"metadata_block_picture";
+
 
     /**
      * Returns the first positive integer found under the given comment keys,
@@ -90,7 +106,8 @@ public abstract class OggAudioParser extends AbstractParser {
     }
 
     protected static void extractComments(Metadata metadata, 
XHTMLContentHandler xhtml,
-            VorbisStyleComments comments) throws TikaException, SAXException {
+            VorbisStyleComments comments, ParseContext context)
+            throws IOException, TikaException, SAXException {
         // Get the specific known comments
         metadata.set(TikaCoreProperties.TITLE, comments.getTitle());
         metadata.set(TikaCoreProperties.CREATOR, comments.getArtist());
@@ -112,12 +129,13 @@ public abstract class OggAudioParser extends 
AbstractParser {
             metadata.add(XMPDM.LOG_COMMENT.getName(), comment);
         }
 
-        // Grab the rest just in case
+        // Grab the rest just in case; the pictures become embedded
+        //  documents instead, their raw base64 blocks help nobody
         List<String> done = Arrays.asList(
                 VorbisComments.KEY_TITLE, VorbisComments.KEY_ARTIST,
                 VorbisComments.KEY_ALBUM, VorbisComments.KEY_GENRE,
                 VorbisComments.KEY_DATE, VorbisComments.KEY_TRACKNUMBER,
-                "vendor", "comment"
+                "vendor", "comment", METADATA_BLOCK_PICTURE
         );
         for (String key : comments.getAllComments().keySet()) {
             if (!done.contains(key)) {
@@ -175,6 +193,113 @@ public abstract class OggAudioParser extends 
AbstractParser {
             xhtml.element("p", comment);
         }
         xhtml.element("p", comments.getGenre());
+
+        // Any embedded pictures, such as cover art, become
+        //  embedded documents of the audio file
+        extractPictures(xhtml, comments, context);
+    }
+
+    /**
+     * Sends the embedded pictures, such as cover art, from the comments to
+     * the embedded document extractor. The pictures are carried as base64
+     * encoded FLAC picture blocks; malformed blocks are skipped silently.
+     * The pictures only become embedded documents, no metadata is recorded
+     * on the audio document itself.
+     */
+    private static void extractPictures(XHTMLContentHandler xhtml,
+            VorbisStyleComments comments, ParseContext context)
+            throws IOException, SAXException {
+        EmbeddedDocumentExtractor extractor = null;
+        for (String block : comments.getComments(METADATA_BLOCK_PICTURE)) {
+            byte[] decoded;
+            try {
+                decoded = Base64.getMimeDecoder().decode(block);
+            } catch (IllegalArgumentException e) {
+                //not valid base64, skip
+                continue;
+            }
+            if (extractor == null) {
+                extractor = 
EmbeddedDocumentUtil.getEmbeddedDocumentExtractor(context);
+            }
+            extractPictureBlock(decoded, xhtml, context, extractor);
+        }
+    }
+
+    /**
+     * Parses one FLAC picture block and sends the picture it holds to the
+     * embedded document extractor. Native FLAC PICTURE metadata blocks use
+     * the very same structure, so {@link FlacParser} shares this method.
+     * Malformed or truncated blocks are skipped silently.
+     */
+    static void extractPictureBlock(byte[] block, XHTMLContentHandler xhtml,
+            ParseContext context, EmbeddedDocumentExtractor extractor)
+            throws IOException, SAXException {
+        // The picture block holds a 32 bit BE picture type, the mime
+        // type, the description, the image geometry and the picture
+        // data, with mime type, description and data length prefixed
+        int pictureType;
+        String mimeType;
+        String description;
+        byte[] picture;
+        try {
+            ByteBuffer buffer = ByteBuffer.wrap(block);
+            pictureType = buffer.getInt();
+            mimeType = getPrefixedString(buffer, StandardCharsets.ISO_8859_1);
+            if (mimeType == null || "-->".equals(mimeType)) {
+                // Malformed, or a link to a picture rather than an
+                // embedded one
+                return;
+            }
+            description = getPrefixedString(buffer, StandardCharsets.UTF_8);
+            if (description == null) {
+                return;
+            }
+            // Width, height, color depth and number of colors
+            buffer.position(buffer.position() + 16);
+            int dataLength = buffer.getInt();
+            if (dataLength <= 0 || dataLength > buffer.remaining()) {
+                return;
+            }
+            picture = new byte[dataLength];
+            buffer.get(picture);
+        } catch (BufferUnderflowException | IllegalArgumentException e) {
+            //truncated picture block, skip
+            return;
+        }
+
+        Metadata pictureMetadata = Metadata.newInstance(context);
+        pictureMetadata.set(TikaCoreProperties.EMBEDDED_RESOURCE_TYPE,
+                TikaCoreProperties.EmbeddedResourceType.INLINE.toString());
+        if (!mimeType.isEmpty()) {
+            pictureMetadata.set(Metadata.CONTENT_TYPE, mimeType);
+        }
+        if (!description.isEmpty()) {
+            pictureMetadata.set(TikaCoreProperties.TITLE, description);
+        }
+        //the FLAC picture block reuses the ID3v2 APIC picture types
+        if (pictureType >= 0 && pictureType < ID3Tags.PICTURE_TYPES.length) {
+            pictureMetadata.set(TikaCoreProperties.DESCRIPTION,
+                    ID3Tags.PICTURE_TYPES[pictureType]);
+        }
+        if (extractor.shouldParseEmbedded(pictureMetadata)) {
+            try (TikaInputStream pictureStream = TikaInputStream.get(picture)) 
{
+                extractor.parseEmbedded(pictureStream, xhtml, pictureMetadata, 
context, true);
+            }
+        }
+    }
+
+    /**
+     * Reads a 32 bit length prefixed string from the buffer, or null if the
+     * declared length is invalid for the remaining data.
+     */
+    private static String getPrefixedString(ByteBuffer buffer, Charset 
charset) {
+        int length = buffer.getInt();
+        if (length < 0 || length > buffer.remaining()) {
+            return null;
+        }
+        byte[] bytes = new byte[length];
+        buffer.get(bytes);
+        return new String(bytes, charset);
     }
 
     protected static void extractDuration(Metadata metadata, 
XHTMLContentHandler xhtml,
diff --git 
a/tika-parsers/tika-parsers-standard/tika-parsers-standard-modules/tika-parser-audiovideo-module/src/main/java/org/apache/tika/parser/ogg/OpusParser.java
 
b/tika-parsers/tika-parsers-standard/tika-parsers-standard-modules/tika-parser-audiovideo-module/src/main/java/org/apache/tika/parser/ogg/OpusParser.java
index f475193b93..e710cb3abb 100644
--- 
a/tika-parsers/tika-parsers-standard/tika-parsers-standard-modules/tika-parser-audiovideo-module/src/main/java/org/apache/tika/parser/ogg/OpusParser.java
+++ 
b/tika-parsers/tika-parsers-standard/tika-parsers-standard-modules/tika-parser-audiovideo-module/src/main/java/org/apache/tika/parser/ogg/OpusParser.java
@@ -76,7 +76,7 @@ public class OpusParser extends OggAudioParser {
         extractInfo(metadata, opus.getInfo());
 
         // Extract any Vorbis comments
-        extractComments(metadata, xhtml, opus.getTags());
+        extractComments(metadata, xhtml, opus.getTags(), context);
 
         // Extract the audio length
         extractDuration(metadata, xhtml, opus, opus);
diff --git 
a/tika-parsers/tika-parsers-standard/tika-parsers-standard-modules/tika-parser-audiovideo-module/src/main/java/org/apache/tika/parser/ogg/SpeexParser.java
 
b/tika-parsers/tika-parsers-standard/tika-parsers-standard-modules/tika-parser-audiovideo-module/src/main/java/org/apache/tika/parser/ogg/SpeexParser.java
index 7a8d17091e..d12fc38487 100644
--- 
a/tika-parsers/tika-parsers-standard/tika-parsers-standard-modules/tika-parser-audiovideo-module/src/main/java/org/apache/tika/parser/ogg/SpeexParser.java
+++ 
b/tika-parsers/tika-parsers-standard/tika-parsers-standard-modules/tika-parser-audiovideo-module/src/main/java/org/apache/tika/parser/ogg/SpeexParser.java
@@ -76,7 +76,7 @@ public class SpeexParser extends OggAudioParser {
         extractInfo(metadata, speex.getInfo());
 
         // Extract any Vorbis comments
-        extractComments(metadata, xhtml, speex.getTags());
+        extractComments(metadata, xhtml, speex.getTags(), context);
 
         // Extract the audio length
         extractDuration(metadata, xhtml, speex, speex);
diff --git 
a/tika-parsers/tika-parsers-standard/tika-parsers-standard-modules/tika-parser-audiovideo-module/src/main/java/org/apache/tika/parser/ogg/TheoraParser.java
 
b/tika-parsers/tika-parsers-standard/tika-parsers-standard-modules/tika-parser-audiovideo-module/src/main/java/org/apache/tika/parser/ogg/TheoraParser.java
index cef86f5137..ffa7be177e 100644
--- 
a/tika-parsers/tika-parsers-standard/tika-parsers-standard-modules/tika-parser-audiovideo-module/src/main/java/org/apache/tika/parser/ogg/TheoraParser.java
+++ 
b/tika-parsers/tika-parsers-standard/tika-parsers-standard-modules/tika-parser-audiovideo-module/src/main/java/org/apache/tika/parser/ogg/TheoraParser.java
@@ -77,7 +77,7 @@ public class TheoraParser extends AbstractParser {
         extractInfo(metadata, theora.getInfo());
 
         // Extract the common Theora comments
-        OggAudioParser.extractComments(metadata, xhtml, theora.getComments());
+        OggAudioParser.extractComments(metadata, xhtml, theora.getComments(), 
context);
 
         // Extract any soundtracks
         for (OggAudioHeaders audio : theora.getSoundtracks()) {
diff --git 
a/tika-parsers/tika-parsers-standard/tika-parsers-standard-modules/tika-parser-audiovideo-module/src/main/java/org/apache/tika/parser/ogg/VorbisParser.java
 
b/tika-parsers/tika-parsers-standard/tika-parsers-standard-modules/tika-parser-audiovideo-module/src/main/java/org/apache/tika/parser/ogg/VorbisParser.java
index dc6b78f995..50fe8fd2a7 100644
--- 
a/tika-parsers/tika-parsers-standard/tika-parsers-standard-modules/tika-parser-audiovideo-module/src/main/java/org/apache/tika/parser/ogg/VorbisParser.java
+++ 
b/tika-parsers/tika-parsers-standard/tika-parsers-standard-modules/tika-parser-audiovideo-module/src/main/java/org/apache/tika/parser/ogg/VorbisParser.java
@@ -75,7 +75,7 @@ public class VorbisParser extends OggAudioParser {
         extractInfo(metadata, vorbis.getInfo());
 
         // Extract any Vorbis comments
-        extractComments(metadata, xhtml, vorbis.getComment());
+        extractComments(metadata, xhtml, vorbis.getComment(), context);
 
         // Extract the audio length
         extractDuration(metadata, xhtml, vorbis, vorbis);
diff --git 
a/tika-parsers/tika-parsers-standard/tika-parsers-standard-modules/tika-parser-audiovideo-module/src/test/java/org/apache/tika/parser/mp3/Mp3ParserTest.java
 
b/tika-parsers/tika-parsers-standard/tika-parsers-standard-modules/tika-parser-audiovideo-module/src/test/java/org/apache/tika/parser/mp3/Mp3ParserTest.java
index af1c03f55d..7290977e52 100644
--- 
a/tika-parsers/tika-parsers-standard/tika-parsers-standard-modules/tika-parser-audiovideo-module/src/test/java/org/apache/tika/parser/mp3/Mp3ParserTest.java
+++ 
b/tika-parsers/tika-parsers-standard/tika-parsers-standard-modules/tika-parser-audiovideo-module/src/test/java/org/apache/tika/parser/mp3/Mp3ParserTest.java
@@ -21,6 +21,7 @@ import static org.junit.jupiter.api.Assumptions.assumeTrue;
 
 import java.io.ByteArrayOutputStream;
 import java.nio.charset.StandardCharsets;
+import java.util.List;
 
 import org.junit.jupiter.api.Test;
 
@@ -130,6 +131,69 @@ public class Mp3ParserTest extends TikaTest {
         checkDuration(metadata, 2);
     }
 
+    /**
+     * Test that cover art in an ID3v2 APIC frame becomes an embedded
+     * document, with no extra metadata on the audio document itself
+     */
+    @Test
+    public void testMp3ParsingID3v2CoverArt() throws Exception {
+        List<Metadata> metadataList = 
getRecursiveMetadata("testMP3_coverArt.mp3");
+
+        assertEquals(2, metadataList.size());
+        assertEquals("audio/mpeg", 
metadataList.get(0).get(Metadata.CONTENT_TYPE));
+
+        Metadata pictureMetadata = metadataList.get(1);
+        assertEquals("image/png", pictureMetadata.get(Metadata.CONTENT_TYPE));
+        assertEquals(TikaCoreProperties.EmbeddedResourceType.INLINE.toString(),
+                
pictureMetadata.get(TikaCoreProperties.EMBEDDED_RESOURCE_TYPE));
+        assertEquals("Test Cover", 
pictureMetadata.get(TikaCoreProperties.TITLE));
+        assertEquals("Cover (front)", 
pictureMetadata.get(TikaCoreProperties.DESCRIPTION));
+    }
+
+    /**
+     * Test that a file with several APIC frames yields one embedded
+     * document per picture, in file order. The first frame is larger
+     * than 127 bytes, so this also exercises the synchsafe frame sizes
+     * of ID3v2.4: reading them as plain integers desyncs the frame walk
+     * and loses every picture after the first
+     */
+    @Test
+    public void testMp3ParsingID3v24MultipleCovers() throws Exception {
+        assertTwoCovers("testMP3_twoCovers.mp3");
+    }
+
+    /**
+     * Test the same two pictures in an ID3v2.3 tag, whose frame sizes
+     * are plain integers
+     */
+    @Test
+    public void testMp3ParsingID3v23MultipleCovers() throws Exception {
+        assertTwoCovers("testMP3v23_twoCovers.mp3");
+    }
+
+    private void assertTwoCovers(String fileName) throws Exception {
+        List<Metadata> metadataList = getRecursiveMetadata(fileName);
+
+        assertEquals(3, metadataList.size());
+        assertEquals("audio/mpeg", 
metadataList.get(0).get(Metadata.CONTENT_TYPE));
+
+        //the 64x40 front cover comes first in the file,
+        //the 30x30 back cover second
+        Metadata front = metadataList.get(1);
+        assertEquals("image/png", front.get(Metadata.CONTENT_TYPE));
+        assertEquals(TikaCoreProperties.EmbeddedResourceType.INLINE.toString(),
+                front.get(TikaCoreProperties.EMBEDDED_RESOURCE_TYPE));
+        assertEquals("Front Cover", front.get(TikaCoreProperties.TITLE));
+        assertEquals("Cover (front)", 
front.get(TikaCoreProperties.DESCRIPTION));
+
+        Metadata back = metadataList.get(2);
+        assertEquals("image/png", back.get(Metadata.CONTENT_TYPE));
+        assertEquals(TikaCoreProperties.EmbeddedResourceType.INLINE.toString(),
+                back.get(TikaCoreProperties.EMBEDDED_RESOURCE_TYPE));
+        assertEquals("Back Cover", back.get(TikaCoreProperties.TITLE));
+        assertEquals("Cover (back)", back.get(TikaCoreProperties.DESCRIPTION));
+    }
+
     /**
      * Test that metadata is added before xhtml content
      * is written...so that more metadata shows up in the xhtml
diff --git 
a/tika-parsers/tika-parsers-standard/tika-parsers-standard-modules/tika-parser-audiovideo-module/src/test/java/org/apache/tika/parser/mp4/MP4ParserTest.java
 
b/tika-parsers/tika-parsers-standard/tika-parsers-standard-modules/tika-parser-audiovideo-module/src/test/java/org/apache/tika/parser/mp4/MP4ParserTest.java
index fd26092d15..2a8f9fcecb 100644
--- 
a/tika-parsers/tika-parsers-standard/tika-parsers-standard-modules/tika-parser-audiovideo-module/src/test/java/org/apache/tika/parser/mp4/MP4ParserTest.java
+++ 
b/tika-parsers/tika-parsers-standard/tika-parsers-standard-modules/tika-parser-audiovideo-module/src/test/java/org/apache/tika/parser/mp4/MP4ParserTest.java
@@ -142,6 +142,43 @@ public class MP4ParserTest extends TikaTest {
         //TODO: why don't we check the output here?
     }
 
+    /**
+     * Test that cover art in the covr atom becomes an embedded document,
+     * with no extra metadata on the audio document itself
+     */
+    @Test
+    public void testMP4CoverArt() throws Exception {
+        List<Metadata> metadataList = 
getRecursiveMetadata("testMP4_coverArt.m4a");
+
+        assertEquals(2, metadataList.size());
+        assertEquals("audio/mp4", 
metadataList.get(0).get(Metadata.CONTENT_TYPE));
+
+        Metadata pictureMetadata = metadataList.get(1);
+        assertEquals("image/png", pictureMetadata.get(Metadata.CONTENT_TYPE));
+        assertEquals(TikaCoreProperties.EmbeddedResourceType.INLINE.toString(),
+                
pictureMetadata.get(TikaCoreProperties.EMBEDDED_RESOURCE_TYPE));
+    }
+
+    /**
+     * Test that a covr entry with several data atoms, one image each,
+     * yields one embedded document per image, in file order
+     */
+    @Test
+    public void testMP4MultipleCovers() throws Exception {
+        List<Metadata> metadataList = 
getRecursiveMetadata("testMP4_twoCovers.m4a");
+
+        assertEquals(3, metadataList.size());
+        //a png data atom (well-known type 14) followed by a jpeg one (13)
+        Metadata front = metadataList.get(1);
+        assertEquals("image/png", front.get(Metadata.CONTENT_TYPE));
+        assertEquals(TikaCoreProperties.EmbeddedResourceType.INLINE.toString(),
+                front.get(TikaCoreProperties.EMBEDDED_RESOURCE_TYPE));
+        Metadata back = metadataList.get(2);
+        assertEquals("image/jpeg", back.get(Metadata.CONTENT_TYPE));
+        assertEquals(TikaCoreProperties.EmbeddedResourceType.INLINE.toString(),
+                back.get(TikaCoreProperties.EMBEDDED_RESOURCE_TYPE));
+    }
+
     // TODO Test a MP4 Video file
     // TODO Test an old QuickTime Video File
     @Test
diff --git 
a/tika-parsers/tika-parsers-standard/tika-parsers-standard-modules/tika-parser-audiovideo-module/src/test/java/org/apache/tika/parser/ogg/FlacParserTest.java
 
b/tika-parsers/tika-parsers-standard/tika-parsers-standard-modules/tika-parser-audiovideo-module/src/test/java/org/apache/tika/parser/ogg/FlacParserTest.java
new file mode 100644
index 0000000000..9341033f2f
--- /dev/null
+++ 
b/tika-parsers/tika-parsers-standard/tika-parsers-standard-modules/tika-parser-audiovideo-module/src/test/java/org/apache/tika/parser/ogg/FlacParserTest.java
@@ -0,0 +1,73 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.tika.parser.ogg;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+
+import java.util.List;
+
+import org.junit.jupiter.api.Test;
+
+import org.apache.tika.TikaTest;
+import org.apache.tika.metadata.Metadata;
+import org.apache.tika.metadata.TikaCoreProperties;
+
+/**
+ * Tests the parsing of native FLAC files.
+ */
+public class FlacParserTest extends TikaTest {
+
+    /**
+     * Cover art in a native PICTURE metadata block becomes an embedded
+     * document, with no extra metadata on the audio document itself.
+     */
+    @Test
+    public void testCoverArt() throws Exception {
+        List<Metadata> metadataList = 
getRecursiveMetadata("testFLAC_coverArt.flac");
+
+        assertEquals(2, metadataList.size());
+        assertEquals("audio/x-flac", 
metadataList.get(0).get(Metadata.CONTENT_TYPE));
+
+        Metadata pictureMetadata = metadataList.get(1);
+        assertEquals("image/png", pictureMetadata.get(Metadata.CONTENT_TYPE));
+        assertEquals(TikaCoreProperties.EmbeddedResourceType.INLINE.toString(),
+                
pictureMetadata.get(TikaCoreProperties.EMBEDDED_RESOURCE_TYPE));
+        assertEquals("Test Cover", 
pictureMetadata.get(TikaCoreProperties.TITLE));
+        assertEquals("Cover (front)", 
pictureMetadata.get(TikaCoreProperties.DESCRIPTION));
+    }
+
+    /**
+     * A file with several PICTURE blocks yields one embedded document
+     * per picture, in file order.
+     */
+    @Test
+    public void testMultipleCovers() throws Exception {
+        List<Metadata> metadataList = 
getRecursiveMetadata("testFLAC_twoCovers.flac");
+
+        assertEquals(3, metadataList.size());
+
+        Metadata front = metadataList.get(1);
+        assertEquals("image/png", front.get(Metadata.CONTENT_TYPE));
+        assertEquals("Front Cover", front.get(TikaCoreProperties.TITLE));
+        assertEquals("Cover (front)", 
front.get(TikaCoreProperties.DESCRIPTION));
+
+        Metadata back = metadataList.get(2);
+        assertEquals("image/png", back.get(Metadata.CONTENT_TYPE));
+        assertEquals("Back Cover", back.get(TikaCoreProperties.TITLE));
+        assertEquals("Cover (back)", back.get(TikaCoreProperties.DESCRIPTION));
+    }
+}
diff --git 
a/tika-parsers/tika-parsers-standard/tika-parsers-standard-modules/tika-parser-audiovideo-module/src/test/java/org/apache/tika/parser/ogg/OggAudioParserTest.java
 
b/tika-parsers/tika-parsers-standard/tika-parsers-standard-modules/tika-parser-audiovideo-module/src/test/java/org/apache/tika/parser/ogg/OggAudioParserTest.java
index a0fd0d7e03..4132477313 100644
--- 
a/tika-parsers/tika-parsers-standard/tika-parsers-standard-modules/tika-parser-audiovideo-module/src/test/java/org/apache/tika/parser/ogg/OggAudioParserTest.java
+++ 
b/tika-parsers/tika-parsers-standard/tika-parsers-standard-modules/tika-parser-audiovideo-module/src/test/java/org/apache/tika/parser/ogg/OggAudioParserTest.java
@@ -20,13 +20,24 @@ import static 
org.junit.jupiter.api.Assertions.assertArrayEquals;
 import static org.junit.jupiter.api.Assertions.assertEquals;
 import static org.junit.jupiter.api.Assertions.assertNull;
 
+import java.nio.ByteBuffer;
+import java.nio.charset.StandardCharsets;
+import java.util.ArrayList;
+import java.util.Base64;
+import java.util.List;
+
 import org.gagravarr.vorbis.VorbisComments;
 import org.junit.jupiter.api.Test;
+import org.xml.sax.ContentHandler;
 import org.xml.sax.helpers.DefaultHandler;
 
+import org.apache.tika.extractor.EmbeddedDocumentExtractor;
+import org.apache.tika.io.TikaInputStream;
 import org.apache.tika.metadata.Audio;
 import org.apache.tika.metadata.Metadata;
+import org.apache.tika.metadata.TikaCoreProperties;
 import org.apache.tika.metadata.XMPDM;
+import org.apache.tika.parser.ParseContext;
 import org.apache.tika.sax.XHTMLContentHandler;
 
 /**
@@ -94,11 +105,62 @@ public class OggAudioParserTest {
         assertEquals("14", metadata.get(Audio.TRACK_COUNT));
     }
 
+    /**
+     * A metadata_block_picture comment becomes an embedded document with
+     * the declared mime type, description and picture type, while the raw
+     * base64 block stays out of the vorbis passthrough metadata.
+     */
+    @Test
+    public void testMetadataBlockPictureBecomesEmbeddedDocument() throws 
Exception {
+        byte[] pictureData = new byte[]{1, 2, 3, 4};
+        byte[] mime = "image/jpeg".getBytes(StandardCharsets.ISO_8859_1);
+        byte[] description = "Back cover".getBytes(StandardCharsets.UTF_8);
+        ByteBuffer buffer = ByteBuffer.allocate(4 + 4 + mime.length + 4 + 
description.length +
+                16 + 4 + pictureData.length);
+        buffer.putInt(4);//picture type: cover (back)
+        buffer.putInt(mime.length).put(mime);
+        buffer.putInt(description.length).put(description);
+        buffer.putInt(1).putInt(1).putInt(24).putInt(0);//width, height, 
depth, colors
+        buffer.putInt(pictureData.length).put(pictureData);
+        String block = Base64.getEncoder().encodeToString(buffer.array());
+
+        List<Metadata> pictures = new ArrayList<>();
+        ParseContext context = new ParseContext();
+        context.set(EmbeddedDocumentExtractor.class, new 
EmbeddedDocumentExtractor() {
+            @Override
+            public boolean shouldParseEmbedded(Metadata metadata) {
+                return true;
+            }
+
+            @Override
+            public void parseEmbedded(TikaInputStream stream, ContentHandler 
handler,
+                    Metadata metadata, ParseContext parseContext, boolean 
outputHtml) {
+                pictures.add(metadata);
+            }
+        });
+
+        Metadata metadata = extractComments(context, "metadata_block_picture", 
block);
+
+        assertEquals(1, pictures.size());
+        Metadata pictureMetadata = pictures.get(0);
+        assertEquals("image/jpeg", pictureMetadata.get(Metadata.CONTENT_TYPE));
+        assertEquals(TikaCoreProperties.EmbeddedResourceType.INLINE.toString(),
+                
pictureMetadata.get(TikaCoreProperties.EMBEDDED_RESOURCE_TYPE));
+        assertEquals("Back cover", 
pictureMetadata.get(TikaCoreProperties.TITLE));
+        assertEquals("Cover (back)", 
pictureMetadata.get(TikaCoreProperties.DESCRIPTION));
+        assertNull(metadata.get("vorbis:metadata_block_picture"));
+    }
+
+    private static Metadata extractComments(String... keysAndValues) throws 
Exception {
+        return extractComments(new ParseContext(), keysAndValues);
+    }
+
     /**
      * Runs the given key/value comment pairs through the shared comment
      * extraction and returns the resulting metadata.
      */
-    private static Metadata extractComments(String... keysAndValues) throws 
Exception {
+    private static Metadata extractComments(ParseContext context, String... 
keysAndValues)
+            throws Exception {
         VorbisComments comments = new VorbisComments();
         comments.addComment("title", "Test Title");
         comments.addComment("artist", "Test Artist");
@@ -110,7 +172,7 @@ public class OggAudioParserTest {
         Metadata metadata = new Metadata();
         XHTMLContentHandler xhtml = new XHTMLContentHandler(new 
DefaultHandler(), metadata);
         xhtml.startDocument();
-        OggAudioParser.extractComments(metadata, xhtml, comments);
+        OggAudioParser.extractComments(metadata, xhtml, comments, context);
         xhtml.endDocument();
         return metadata;
     }
diff --git 
a/tika-parsers/tika-parsers-standard/tika-parsers-standard-modules/tika-parser-audiovideo-module/src/test/java/org/apache/tika/parser/ogg/VorbisParserTest.java
 
b/tika-parsers/tika-parsers-standard/tika-parsers-standard-modules/tika-parser-audiovideo-module/src/test/java/org/apache/tika/parser/ogg/VorbisParserTest.java
index 142d202da2..5a2ddb2d29 100644
--- 
a/tika-parsers/tika-parsers-standard/tika-parsers-standard-modules/tika-parser-audiovideo-module/src/test/java/org/apache/tika/parser/ogg/VorbisParserTest.java
+++ 
b/tika-parsers/tika-parsers-standard/tika-parsers-standard-modules/tika-parser-audiovideo-module/src/test/java/org/apache/tika/parser/ogg/VorbisParserTest.java
@@ -19,6 +19,8 @@ package org.apache.tika.parser.ogg;
 import static org.junit.jupiter.api.Assertions.assertEquals;
 import static org.junit.jupiter.api.Assertions.assertNull;
 
+import java.util.List;
+
 import org.gagravarr.vorbis.VorbisInfo;
 import org.junit.jupiter.api.Test;
 import org.xml.sax.helpers.DefaultHandler;
@@ -27,6 +29,7 @@ import org.apache.tika.TikaTest;
 import org.apache.tika.io.TikaInputStream;
 import org.apache.tika.metadata.Audio;
 import org.apache.tika.metadata.Metadata;
+import org.apache.tika.metadata.TikaCoreProperties;
 import org.apache.tika.metadata.XMPDM;
 import org.apache.tika.parser.ParseContext;
 
@@ -51,6 +54,49 @@ public class VorbisParserTest extends TikaTest {
         assertEquals("44100", metadata.get(XMPDM.AUDIO_SAMPLE_RATE));
     }
 
+    /**
+     * Cover art in a metadata_block_picture comment becomes an embedded
+     * document; neither the raw base64 block nor any other new metadata
+     * lands on the audio document itself.
+     */
+    @Test
+    public void testCoverArt() throws Exception {
+        List<Metadata> metadataList = 
getRecursiveMetadata("testVORBIS_coverArt.ogg");
+
+        assertEquals(2, metadataList.size());
+        assertEquals("audio/vorbis", 
metadataList.get(0).get(Metadata.CONTENT_TYPE));
+        assertNull(metadataList.get(0).get("vorbis:metadata_block_picture"));
+
+        Metadata pictureMetadata = metadataList.get(1);
+        assertEquals("image/png", pictureMetadata.get(Metadata.CONTENT_TYPE));
+        assertEquals(TikaCoreProperties.EmbeddedResourceType.INLINE.toString(),
+                
pictureMetadata.get(TikaCoreProperties.EMBEDDED_RESOURCE_TYPE));
+        assertEquals("Test Cover", 
pictureMetadata.get(TikaCoreProperties.TITLE));
+        assertEquals("Cover (front)", 
pictureMetadata.get(TikaCoreProperties.DESCRIPTION));
+    }
+
+    /**
+     * A file with several metadata_block_picture comments yields one
+     * embedded document per picture, in file order.
+     */
+    @Test
+    public void testMultipleCovers() throws Exception {
+        List<Metadata> metadataList = 
getRecursiveMetadata("testVORBIS_twoCovers.ogg");
+
+        assertEquals(3, metadataList.size());
+        assertNull(metadataList.get(0).get("vorbis:metadata_block_picture"));
+
+        Metadata front = metadataList.get(1);
+        assertEquals("image/png", front.get(Metadata.CONTENT_TYPE));
+        assertEquals("Front Cover", front.get(TikaCoreProperties.TITLE));
+        assertEquals("Cover (front)", 
front.get(TikaCoreProperties.DESCRIPTION));
+
+        Metadata back = metadataList.get(2);
+        assertEquals("image/png", back.get(Metadata.CONTENT_TYPE));
+        assertEquals("Back Cover", back.get(TikaCoreProperties.TITLE));
+        assertEquals("Cover (back)", back.get(TikaCoreProperties.DESCRIPTION));
+    }
+
     private static Metadata extractInfo(int upper, int nominal, int lower) 
throws Exception {
         VorbisInfo info = new VorbisInfo();
         info.setRate(44100);
diff --git 
a/tika-parsers/tika-parsers-standard/tika-parsers-standard-modules/tika-parser-audiovideo-module/src/test/resources/test-documents/testFLAC_coverArt.flac
 
b/tika-parsers/tika-parsers-standard/tika-parsers-standard-modules/tika-parser-audiovideo-module/src/test/resources/test-documents/testFLAC_coverArt.flac
new file mode 100644
index 0000000000..2f1698a3cd
Binary files /dev/null and 
b/tika-parsers/tika-parsers-standard/tika-parsers-standard-modules/tika-parser-audiovideo-module/src/test/resources/test-documents/testFLAC_coverArt.flac
 differ
diff --git 
a/tika-parsers/tika-parsers-standard/tika-parsers-standard-modules/tika-parser-audiovideo-module/src/test/resources/test-documents/testFLAC_twoCovers.flac
 
b/tika-parsers/tika-parsers-standard/tika-parsers-standard-modules/tika-parser-audiovideo-module/src/test/resources/test-documents/testFLAC_twoCovers.flac
new file mode 100644
index 0000000000..53c793f0fd
Binary files /dev/null and 
b/tika-parsers/tika-parsers-standard/tika-parsers-standard-modules/tika-parser-audiovideo-module/src/test/resources/test-documents/testFLAC_twoCovers.flac
 differ
diff --git 
a/tika-parsers/tika-parsers-standard/tika-parsers-standard-modules/tika-parser-audiovideo-module/src/test/resources/test-documents/testMP3_coverArt.mp3
 
b/tika-parsers/tika-parsers-standard/tika-parsers-standard-modules/tika-parser-audiovideo-module/src/test/resources/test-documents/testMP3_coverArt.mp3
new file mode 100644
index 0000000000..fe81c5c794
Binary files /dev/null and 
b/tika-parsers/tika-parsers-standard/tika-parsers-standard-modules/tika-parser-audiovideo-module/src/test/resources/test-documents/testMP3_coverArt.mp3
 differ
diff --git 
a/tika-parsers/tika-parsers-standard/tika-parsers-standard-modules/tika-parser-audiovideo-module/src/test/resources/test-documents/testMP3_twoCovers.mp3
 
b/tika-parsers/tika-parsers-standard/tika-parsers-standard-modules/tika-parser-audiovideo-module/src/test/resources/test-documents/testMP3_twoCovers.mp3
new file mode 100644
index 0000000000..db86376738
Binary files /dev/null and 
b/tika-parsers/tika-parsers-standard/tika-parsers-standard-modules/tika-parser-audiovideo-module/src/test/resources/test-documents/testMP3_twoCovers.mp3
 differ
diff --git 
a/tika-parsers/tika-parsers-standard/tika-parsers-standard-modules/tika-parser-audiovideo-module/src/test/resources/test-documents/testMP3v23_twoCovers.mp3
 
b/tika-parsers/tika-parsers-standard/tika-parsers-standard-modules/tika-parser-audiovideo-module/src/test/resources/test-documents/testMP3v23_twoCovers.mp3
new file mode 100644
index 0000000000..85926a204d
Binary files /dev/null and 
b/tika-parsers/tika-parsers-standard/tika-parsers-standard-modules/tika-parser-audiovideo-module/src/test/resources/test-documents/testMP3v23_twoCovers.mp3
 differ
diff --git 
a/tika-parsers/tika-parsers-standard/tika-parsers-standard-modules/tika-parser-audiovideo-module/src/test/resources/test-documents/testMP4_coverArt.m4a
 
b/tika-parsers/tika-parsers-standard/tika-parsers-standard-modules/tika-parser-audiovideo-module/src/test/resources/test-documents/testMP4_coverArt.m4a
new file mode 100644
index 0000000000..afed2997ce
Binary files /dev/null and 
b/tika-parsers/tika-parsers-standard/tika-parsers-standard-modules/tika-parser-audiovideo-module/src/test/resources/test-documents/testMP4_coverArt.m4a
 differ
diff --git 
a/tika-parsers/tika-parsers-standard/tika-parsers-standard-modules/tika-parser-audiovideo-module/src/test/resources/test-documents/testMP4_twoCovers.m4a
 
b/tika-parsers/tika-parsers-standard/tika-parsers-standard-modules/tika-parser-audiovideo-module/src/test/resources/test-documents/testMP4_twoCovers.m4a
new file mode 100644
index 0000000000..9269826c0c
Binary files /dev/null and 
b/tika-parsers/tika-parsers-standard/tika-parsers-standard-modules/tika-parser-audiovideo-module/src/test/resources/test-documents/testMP4_twoCovers.m4a
 differ
diff --git 
a/tika-parsers/tika-parsers-standard/tika-parsers-standard-modules/tika-parser-audiovideo-module/src/test/resources/test-documents/testVORBIS_coverArt.ogg
 
b/tika-parsers/tika-parsers-standard/tika-parsers-standard-modules/tika-parser-audiovideo-module/src/test/resources/test-documents/testVORBIS_coverArt.ogg
new file mode 100644
index 0000000000..d7b769ac33
Binary files /dev/null and 
b/tika-parsers/tika-parsers-standard/tika-parsers-standard-modules/tika-parser-audiovideo-module/src/test/resources/test-documents/testVORBIS_coverArt.ogg
 differ
diff --git 
a/tika-parsers/tika-parsers-standard/tika-parsers-standard-modules/tika-parser-audiovideo-module/src/test/resources/test-documents/testVORBIS_twoCovers.ogg
 
b/tika-parsers/tika-parsers-standard/tika-parsers-standard-modules/tika-parser-audiovideo-module/src/test/resources/test-documents/testVORBIS_twoCovers.ogg
new file mode 100644
index 0000000000..dafbed1969
Binary files /dev/null and 
b/tika-parsers/tika-parsers-standard/tika-parsers-standard-modules/tika-parser-audiovideo-module/src/test/resources/test-documents/testVORBIS_twoCovers.ogg
 differ

Reply via email to