/* * Copyright (c) 2010, 2013, Oracle and/or its affiliates. All rights reserved. * ORACLE PROPRIETARY/CONFIDENTIAL. Use is subject to license terms. * * * * * * * * * * * * * * * * * * * * */ package com.sun.media.jfxmediaimpl.platform.java; import com.sun.media.jfxmediaimpl.MetadataParserImpl; import java.io.IOException; import java.util.Arrays; import com.sun.media.jfxmedia.locator.Locator; import com.sun.media.jfxmedia.logging.Logger; import java.nio.charset.Charset; import java.nio.charset.UnsupportedCharsetException; final class ID3MetadataParser extends MetadataParserImpl { private static final int ID3_VERSION_MIN = 2; private static final int ID3_VERSION_MAX = 4; private static final String CHARSET_UTF_8 = "UTF-8"; private static final String CHARSET_ISO_8859_1 = "ISO-8859-1"; private static final String CHARSET_UTF_16 = "UTF-16"; private static final String CHARSET_UTF_16BE = "UTF-16BE"; private int COMMCount = 0; private int TXXXCount = 0; private int version = 3; // Default to 3 private boolean unsynchronized = false; public ID3MetadataParser(Locator locator) { super(locator); } protected void parse() { try { // We will need ISO-8859-1 if (!Charset.isSupported(CHARSET_ISO_8859_1)) { throw new UnsupportedCharsetException(CHARSET_ISO_8859_1); } // // An ID3v2 tag can be detected with the following pattern: // // byte 0 1 2 3 4 5 6 7 8 9 // value 0x49 0x44 0x33 yy yy xx zz zz zz zz // // Where yy is less than 0xFF, xx is the 'flags' byte and zz is // less than 0x80. We also require the version, byte 3, // to be exactly 3 to indicate ID3v2.3.0, period. // // http://id3.org/id3v2.3.0#head-697d09c50ed7fa96fb66c6b0a9d93585e2652b0b // byte[] buf = getBytes(10); version = (int)(buf[3] & 0xFF); if (buf[0] == 0x49 && buf[1] == 0x44 && buf[2] == 0x33 && (version >= ID3_VERSION_MIN && version <= ID3_VERSION_MAX)) { int flags = buf[5] & 0xFF; if ((flags & 0x80) == 0x80) { unsynchronized = true; } int tagSize = 0; for (int i = 6, shift = 21; i < 10; i++) { tagSize += (buf[i] & 0x7f) << shift; shift -= 7; } startRawMetadata(tagSize + 10); stuffRawMetadata(buf, 0, 10); // put the header back in the raw metadata blob readRawMetadata(tagSize); setParseRawMetadata(true); skipBytes(10); // reposition to past the ID3 header while (getStreamPosition() < tagSize) { // size int frameSize; byte[] idBytes; if (2 == version) { // v2 has a six byte header idBytes = getBytes(3); frameSize = getU24(); } else { idBytes = getBytes(4); frameSize = getFrameSize(); skipBytes(2); } if (0 == idBytes[0]) { // terminate on zero padding, NULL characters not allowed in frame ID if (Logger.canLog(Logger.DEBUG)) { Logger.logMsg(Logger.DEBUG, "ID3MetadataParser", "parse", "ID3 parser: zero padding detected at " +getStreamPosition()+", terminating"); } break; } String frameID = new String(idBytes, Charset.forName(CHARSET_ISO_8859_1)); if (Logger.canLog(Logger.DEBUG)) { Logger.logMsg(Logger.DEBUG, "ID3MetadataParser", "parse", getStreamPosition()+"\\"+tagSize +": frame ID "+frameID+", size "+frameSize); } if (frameID.equals("APIC") || frameID.equals("PIC")) { byte[] data = getBytes(frameSize); if (unsynchronized) { data = unsynchronizeBuffer(data); } byte[] image = frameID.equals("PIC") ? getImageFromPIC(data) : getImageFromAPIC(data); if (image != null) { addMetadataItem("image", image); } } else if (frameID.startsWith("T") && !frameID.equals("TXXX")) { String encoding = getEncoding(); byte[] data = getBytes(frameSize - 1); if (unsynchronized) { data = unsynchronizeBuffer(data); } String value = new String(data, encoding); String[] tag = getTagFromFrameID(frameID); if (tag != null) { for (int i = 0; i < tag.length; i++) { Object tagValue = convertValue(tag[i], value); if (tagValue != null) { addMetadataItem(tag[i], tagValue); } } } } else if (frameID.equals("COMM") || frameID.equals("COM")) { String encoding = getEncoding(); // Get language byte[] data = getBytes(3); if (unsynchronized) { data = unsynchronizeBuffer(data); } String language = new String(data, Charset.forName(CHARSET_ISO_8859_1)); // Get content description and comment data = getBytes(frameSize - 4); if (unsynchronized) { data = unsynchronizeBuffer(data); } String value = new String(data, encoding); if (value != null) { int index = value.indexOf(0x00); String content = ""; String comment; if (index == 0) { if (isTwoByteEncoding(encoding)) { comment = value.substring(2); } else { comment = value.substring(1); } } else { content = value.substring(0, index); if (isTwoByteEncoding(encoding)) { comment = value.substring(index + 2); } else { comment = value.substring(index + 1); } } String[] tag = getTagFromFrameID(frameID); if (tag != null) { for (int i = 0; i < tag.length; i++) { addMetadataItem(tag[i] + "-" + COMMCount, content + "[" + language + "]=" + comment); COMMCount++; } } } } else if (frameID.equals("TXX") || frameID.equals("TXXX")) { String encoding = getEncoding(); byte[] data = getBytes(frameSize-1); if (unsynchronized) { data = unsynchronizeBuffer(data); } String value = new String(data, encoding); if (null != value){ int index = value.indexOf(0x00); String description = (index != 0) ? value.substring(0, index) : ""; String text = isTwoByteEncoding(encoding) ? value.substring(index+2) : value.substring(index+1); String[] tag = getTagFromFrameID(frameID); if (tag != null) { for (int i = 0; i < tag.length; i++) { if (description.equals("")) { addMetadataItem(tag[i] + "-" + TXXXCount, text); } else { addMetadataItem(tag[i] + "-" + TXXXCount, description + "=" + text); } TXXXCount++; } } } } else { // Unknown or unsupported frame. skipBytes(frameSize); } } } } catch (Exception ex) { // Ignore all exceptions as it is preferable to play audio. // fail gracefully, probably just hit the end of the buffer if (Logger.canLog(Logger.WARNING)) { Logger.logMsg(Logger.WARNING, "ID3MetadataParser", "parse", "Exception while processing ID3v2 metadata: "+ex); } } finally { if (null != rawMetaBlob) { setParseRawMetadata(false); addRawMetadata(RAW_ID3_METADATA_NAME); disposeRawMetadata(); } done(); } } private int getFrameSize() throws IOException { if (version == 4) { byte[] buf = getBytes(4); int size = 0; for (int i = 0, shift = 21; i < 4; i++) { size += (buf[i] & 0x7f) << shift; shift -= 7; } return size; } else { return getInteger(); } } private String getEncoding() throws IOException { byte encodingType = getNextByte(); if (encodingType == 0x00) { return CHARSET_ISO_8859_1; } else if (encodingType == 0x01) { return CHARSET_UTF_16; } else if (encodingType == 0x02) { return CHARSET_UTF_16BE; } else if (encodingType == 0x03) { return CHARSET_UTF_8; } else { throw new IllegalArgumentException(); } } private boolean isTwoByteEncoding(String encoding) { if (encoding.equals(CHARSET_ISO_8859_1) || encoding.equals(CHARSET_UTF_8)) { return false; } else if (encoding.equals(CHARSET_UTF_16) || encoding.equals(CHARSET_UTF_16BE)) { return true; } else { throw new IllegalArgumentException(); } } /* Supported tags: * PIC / APIC -> image * TP2 / TPE2 -> ALBUMARTIST_TAG_NAME * TAL / TALB -> ALBUM_TAG_NAME * TP1 / TPE1 -> ARTIST_TAG_NAME * COM / COMM -> COMMENT_TAG_NAME * TCM / TCOM -> COMPOSER_TAG_NAME * TLE / TLEN -> DURATION_TAG_NAME * TCO / TCON -> GENRE_TAG_NAME * TT2 / TIT2 -> TITLE_TAG_NAME * TRK / TRCK -> TRACKNUMBER_TAG_NAME, TRACKCOUNT_TAG_NAME * TPA / TPOS -> DISCNUMBER_TAG_NAME DISCCOUNT_TAG_NAME * TYE / TYER / TDRC -> YEAR_TAG_NAME * TXX / TXXX -> TEXT_TAG_NAME */ private String[] getTagFromFrameID(String frameID) { if (frameID.equals("TPE2") || frameID.equals("TP2")) { return new String[]{MetadataParserImpl.ALBUMARTIST_TAG_NAME}; } else if (frameID.equals("TALB") || frameID.equals("TAL")) { return new String[]{MetadataParserImpl.ALBUM_TAG_NAME}; } else if (frameID.equals("TPE1") || frameID.equals("TP1")) { return new String[]{MetadataParserImpl.ARTIST_TAG_NAME}; } else if (frameID.equals("COMM") || frameID.equals("COM")) { return new String[]{MetadataParserImpl.COMMENT_TAG_NAME}; } else if (frameID.equals("TCOM") || frameID.equals("TCM")) { return new String[]{MetadataParserImpl.COMPOSER_TAG_NAME}; } else if (frameID.equals("TLEN") || frameID.equals("TLE")) { return new String[]{MetadataParserImpl.DURATION_TAG_NAME}; } else if (frameID.equals("TCON") || frameID.equals("TCO")) { return new String[]{MetadataParserImpl.GENRE_TAG_NAME}; } else if (frameID.equals("TIT2") || frameID.equals("TT2")) { return new String[]{MetadataParserImpl.TITLE_TAG_NAME}; } else if (frameID.equals("TRCK") || frameID.equals("TRK")) { return new String[]{MetadataParserImpl.TRACKNUMBER_TAG_NAME, MetadataParserImpl.TRACKCOUNT_TAG_NAME}; } else if (frameID.equals("TPOS") || frameID.equals("TPA")) { return new String[]{MetadataParserImpl.DISCNUMBER_TAG_NAME, MetadataParserImpl.DISCCOUNT_TAG_NAME}; } else if (frameID.equals("TYER") || frameID.equals("TDRC")) { return new String[]{MetadataParserImpl.YEAR_TAG_NAME}; } else if (frameID.equals("TXX") || frameID.equals("TXXX")) { return new String[]{MetadataParserImpl.TEXT_TAG_NAME}; } return null; } private byte[] getImageFromPIC(byte[] data) { /* * Attached picture "PIC" * Frame size $xx xx xx * (data byte array starts here) * Text encoding $xx * Image format $xx xx xx (PNG/JPG) * Picture type $xx * Description $00 (00) * Picture data */ // find end of description string int imgOffset = 5; while (0 != data[imgOffset] && imgOffset < data.length) { imgOffset++; } if (imgOffset == data.length) { // only description? maybe it's a URI return null; } String type = new String(data, 1, 3, Charset.forName(CHARSET_ISO_8859_1)); if (Logger.canLog(Logger.DEBUG)) { Logger.logMsg(Logger.DEBUG, "ID3MetadataParser", "getImageFromPIC", "PIC type: "+type); } if (type.equalsIgnoreCase("PNG") || type.equalsIgnoreCase("JPG")) { // image data follows description return Arrays.copyOfRange(data, imgOffset+1, data.length); } if (Logger.canLog(Logger.WARNING)) { Logger.logMsg(Logger.WARNING, "ID3MetadataParser", "getImageFromPIC", "Unsupported picture type found \""+type+"\""); } return null; } private byte[] getImageFromAPIC(byte[] data) { boolean isImageJPEG = false; boolean isImagePNG = false; // Look for string "image/". int maxIndex = data.length - 10; int offset = 0; for (int j = 0; j < maxIndex; j++) { if (data[j] == 'i' && data[j + 1] == 'm' && data[j + 2] == 'a' && data[j + 3] == 'g' && data[j + 4] == 'e' && data[j + 5] == '/') { // Found "image/"; offset by its length. j += 6; // Look for "jpeg". if (data[j] == 'j' && data[j + 1] == 'p' && data[j + 2] == 'e' && data[j + 3] == 'g') { // MIME type "image/jpeg" found; save offset. isImageJPEG = true; offset = j + 4; break; } // Look for "png". else if (data[j] == 'p' && data[j + 1] == 'n' && data[j + 2] == 'g') { // MIME type "image/png" found; save offset. isImagePNG = true; offset = j + 3; break; } } } // for j if (isImageJPEG) { // Look for JPEG signature. boolean isSignatureFound = false; int upperBound = data.length - 1; for (int j = offset; j < upperBound; j++) { // JPEG start of image (SOI) marker is 0xff 0xd8 if (-1 == data[j] && -40 == data[j + 1]) { // JPEG SOI found. isSignatureFound = true; offset = j; break; // JPEG image } } if (isSignatureFound) { // Save JPEG data stream starting at SOI. return Arrays.copyOfRange(data, offset, data.length); } } // isImageJPEG if (isImagePNG) { // Look for PNG signature. boolean isSignatureFound = false; int upperBound = data.length - 7; for (int j = offset; j < upperBound; j++) { // PNG decimal signature {137 80 78 71 13 10 26 10}. if (-119 == data[j] && 80 == data[j + 1] && 78 == data[j + 2] && 71 == data[j + 3] && 13 == data[j + 4] && 10 == data[j + 5] && 26 == data[j + 6] && 10 == data[j + 7]) // increment by 1 { // PNG signature found. isSignatureFound = true; offset = j; break; // PNG image } } if (isSignatureFound) { // Save PNG data stream starting at signature. return Arrays.copyOfRange(data, offset, data.length); } } // isImagePNG return null; } private byte[] unsynchronizeBuffer(byte[] data) { byte[] udata = new byte[data.length]; int udatalen = 0; for (int i = 0; i < data.length; i++) { if (((data[i] & 0xFF) == 0xFF && data[i + 1] == 0x00 && data[i + 2] == 0x00) || ((data[i] & 0xFF) == 0xFF && data[i + 1] == 0x00 && (data[i + 2] & 0xE0) == 0xE0)) { udata[udatalen] = data[i]; udatalen++; udata[udatalen] = data[i + 2]; udatalen++; i += 2; } else { udata[udatalen] = data[i]; udatalen++; } } return Arrays.copyOf(udata, udatalen); } }