Skip to content

Commit 8bce0bc

Browse files
authored
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).
1 parent c18f268 commit 8bce0bc

29 files changed

Lines changed: 945 additions & 22 deletions

tika-parsers/tika-parsers-standard/tika-parsers-standard-modules/tika-parser-audiovideo-module/src/main/java/org/apache/tika/parser/mp3/ID3Tags.java

Lines changed: 86 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@
1616
*/
1717
package org.apache.tika.parser.mp3;
1818

19+
import java.util.Collections;
1920
import java.util.List;
2021

2122
/**
@@ -163,6 +164,36 @@ public interface ID3Tags {
163164
/* 125 */ "Dance Hall",
164165
/* sentinel */ ""};
165166

167+
/**
168+
* List of predefined picture types for embedded pictures, indexed
169+
* by the type byte of the ID3v2 APIC / PIC frames. The FLAC picture
170+
* block, as also embedded in Vorbis comments, reuses the same list.
171+
* <p>
172+
* See <a href="https://id3.org/id3v2.4.0-frames">http://id3.org/id3v2.4.0-frames</a>
173+
*/
174+
String[] PICTURE_TYPES = new String[]{
175+
/* 0 */ "Other",
176+
/* 1 */ "32x32 pixels 'file icon' (PNG only)",
177+
/* 2 */ "Other file icon",
178+
/* 3 */ "Cover (front)",
179+
/* 4 */ "Cover (back)",
180+
/* 5 */ "Leaflet page",
181+
/* 6 */ "Media (e.g. label side of CD)",
182+
/* 7 */ "Lead artist/lead performer/soloist",
183+
/* 8 */ "Artist/performer",
184+
/* 9 */ "Conductor",
185+
/* 10 */ "Band/Orchestra",
186+
/* 11 */ "Composer",
187+
/* 12 */ "Lyricist/text writer",
188+
/* 13 */ "Recording Location",
189+
/* 14 */ "During recording",
190+
/* 15 */ "During performance",
191+
/* 16 */ "Movie/video screen capture",
192+
/* 17 */ "A bright coloured fish",
193+
/* 18 */ "Illustration",
194+
/* 19 */ "Band/artist logotype",
195+
/* 20 */ "Publisher/Studio logotype"};
196+
166197
/**
167198
* Does the file contain this kind of tags?
168199
*/
@@ -198,6 +229,15 @@ public interface ID3Tags {
198229
*/
199230
List<ID3Comment> getComments();
200231

232+
/**
233+
* Retrieves the embedded pictures (e.g. cover art), if any.
234+
* Only ID3v2 tags can carry pictures, so this defaults to
235+
* an empty list.
236+
*/
237+
default List<ID3Picture> getPictures() {
238+
return Collections.emptyList();
239+
}
240+
201241
String getGenre();
202242

203243
String getYear();
@@ -258,4 +298,50 @@ public String getText() {
258298
return text;
259299
}
260300
}
301+
302+
/**
303+
* Represents an embedded picture in ID3 v2, such as cover art,
304+
* as carried by the APIC (v2.3/v2.4) and PIC (v2.2) frames
305+
*/
306+
class ID3Picture {
307+
private final String mimeType;
308+
private final String description;
309+
private final int pictureType;
310+
private final byte[] data;
311+
312+
public ID3Picture(String mimeType, String description, int pictureType, byte[] data) {
313+
this.mimeType = mimeType;
314+
this.description = description;
315+
this.pictureType = pictureType;
316+
this.data = data;
317+
}
318+
319+
/**
320+
* Gets the declared mime type, if present
321+
*/
322+
public String getMimeType() {
323+
return mimeType;
324+
}
325+
326+
/**
327+
* Gets the description, if present
328+
*/
329+
public String getDescription() {
330+
return description;
331+
}
332+
333+
/**
334+
* Gets the picture type byte, which indexes {@link #PICTURE_TYPES}
335+
*/
336+
public int getPictureType() {
337+
return pictureType;
338+
}
339+
340+
/**
341+
* Gets the raw picture data
342+
*/
343+
public byte[] getData() {
344+
return data;
345+
}
346+
}
261347
}

tika-parsers/tika-parsers-standard/tika-parsers-standard-modules/tika-parser-audiovideo-module/src/main/java/org/apache/tika/parser/mp3/ID3v22Handler.java

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -44,6 +44,7 @@ public class ID3v22Handler implements ID3Tags {
4444
private String albumArtist;
4545
private String disc;
4646
private List<ID3Comment> comments = new ArrayList<>();
47+
private List<ID3Picture> pictures = new ArrayList<>();
4748

4849
public ID3v22Handler(ID3v2Frame frame) throws IOException, SAXException, TikaException {
4950
RawTagIterator tags = new RawV22TagIterator(frame);
@@ -78,6 +79,13 @@ public ID3v22Handler(ID3v2Frame frame) throws IOException, SAXException, TikaExc
7879
}
7980
break;
8081
}
82+
case "PIC": {
83+
ID3Picture picture = ID3v2Frame.getV22Picture(tag.data, 0, tag.data.length);
84+
if (picture != null) {
85+
pictures.add(picture);
86+
}
87+
break;
88+
}
8189
case "TRK":
8290
trackNumber = getTagString(tag.data, 0, tag.data.length);
8391
break;
@@ -149,6 +157,10 @@ public List<ID3Comment> getComments() {
149157
return comments;
150158
}
151159

160+
public List<ID3Picture> getPictures() {
161+
return pictures;
162+
}
163+
152164
public String getGenre() {
153165
return genre;
154166
}

tika-parsers/tika-parsers-standard/tika-parsers-standard-modules/tika-parser-audiovideo-module/src/main/java/org/apache/tika/parser/mp3/ID3v23Handler.java

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -45,6 +45,7 @@ public class ID3v23Handler implements ID3Tags {
4545
private String disc;
4646
private String compilation;
4747
private List<ID3Comment> comments = new ArrayList<>();
48+
private List<ID3Picture> pictures = new ArrayList<>();
4849

4950
public ID3v23Handler(ID3v2Frame frame) throws IOException, SAXException, TikaException {
5051
RawTagIterator tags = new RawV23TagIterator(frame);
@@ -79,6 +80,13 @@ public ID3v23Handler(ID3v2Frame frame) throws IOException, SAXException, TikaExc
7980
}
8081
break;
8182
}
83+
case "APIC": {
84+
ID3Picture picture = ID3v2Frame.getPicture(tag.data, 0, tag.data.length);
85+
if (picture != null) {
86+
pictures.add(picture);
87+
}
88+
break;
89+
}
8290
case "TRCK":
8391
trackNumber = getTagString(tag.data, 0, tag.data.length);
8492
break;
@@ -135,6 +143,10 @@ public List<ID3Comment> getComments() {
135143
return comments;
136144
}
137145

146+
public List<ID3Picture> getPictures() {
147+
return pictures;
148+
}
149+
138150
public String getGenre() {
139151
return genre;
140152
}

tika-parsers/tika-parsers-standard/tika-parsers-standard-modules/tika-parser-audiovideo-module/src/main/java/org/apache/tika/parser/mp3/ID3v24Handler.java

Lines changed: 14 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -46,6 +46,7 @@ public class ID3v24Handler implements ID3Tags {
4646
private String disc;
4747
private String compilation;
4848
private List<ID3Comment> comments = new ArrayList<>();
49+
private List<ID3Picture> pictures = new ArrayList<>();
4950

5051
public ID3v24Handler(ID3v2Frame frame) throws IOException, SAXException, TikaException {
5152
RawTagIterator tags = new RawV24TagIterator(frame);
@@ -85,6 +86,13 @@ public ID3v24Handler(ID3v2Frame frame) throws IOException, SAXException, TikaExc
8586
}
8687
break;
8788
}
89+
case "APIC": {
90+
ID3Picture picture = ID3v2Frame.getPicture(tag.data, 0, tag.data.length);
91+
if (picture != null) {
92+
pictures.add(picture);
93+
}
94+
break;
95+
}
8896
case "TRCK":
8997
trackNumber = getTagString(tag.data, 0, tag.data.length);
9098
break;
@@ -141,6 +149,10 @@ public List<ID3Comment> getComments() {
141149
return comments;
142150
}
143151

152+
public List<ID3Picture> getPictures() {
153+
return pictures;
154+
}
155+
144156
public String getGenre() {
145157
return genre;
146158
}
@@ -163,7 +175,8 @@ public String getCompilation() {
163175

164176
private class RawV24TagIterator extends RawTagIterator {
165177
private RawV24TagIterator(ID3v2Frame frame) {
166-
frame.super(4, 4, 1, 2);
178+
//v2.4 frame sizes are synchsafe integers
179+
frame.super(4, 4, 1, 2, true);
167180
}
168181
}
169182
}

0 commit comments

Comments
 (0)