diff --git a/Source/com/drew/imaging/FileType.java b/Source/com/drew/imaging/FileType.java index 9914ac50c..7b4a2c627 100644 --- a/Source/com/drew/imaging/FileType.java +++ b/Source/com/drew/imaging/FileType.java @@ -50,6 +50,7 @@ public enum FileType Avif("AVIF", "AV1 Image File Format", "image/avif", "avif"), Eps("EPS", "Encapsulated PostScript", "application/postscript", "eps", "epsf", "epsi"), Mp3("MP3", "MPEG Audio Layer III", "audio/mpeg", "mp3"), + Mkv("MKV", "Matroska Video Container", "video/x-matroska", "mkv", "webm"), /** Sony camera raw. */ Arw("ARW", "Sony Camera Raw", null, "arw"), diff --git a/Source/com/drew/imaging/FileTypeDetector.java b/Source/com/drew/imaging/FileTypeDetector.java index fda22575e..f324a48fe 100644 --- a/Source/com/drew/imaging/FileTypeDetector.java +++ b/Source/com/drew/imaging/FileTypeDetector.java @@ -103,6 +103,7 @@ public class FileTypeDetector _root.addPath(FileType.Swf, "ZWS".getBytes()); _root.addPath(FileType.Vob, new byte[]{0x00, 0x00, 0x01, (byte)0xBA}); _root.addPath(FileType.Zip, "PK".getBytes()); + _root.addPath(FileType.Mkv, new byte[]{0x1A, 0x45, (byte) 0xDF, (byte) 0xA3}); int bytesNeeded = _root.getMaxDepth(); for (TypeChecker fixedChecker : _fixedCheckers) { diff --git a/Source/com/drew/imaging/ImageMetadataReader.java b/Source/com/drew/imaging/ImageMetadataReader.java index 474300950..075de71bd 100644 --- a/Source/com/drew/imaging/ImageMetadataReader.java +++ b/Source/com/drew/imaging/ImageMetadataReader.java @@ -27,6 +27,7 @@ import com.drew.imaging.heif.HeifMetadataReader; import com.drew.imaging.ico.IcoMetadataReader; import com.drew.imaging.jpeg.JpegMetadataReader; +import com.drew.imaging.mkv.MkvMetadataReader; import com.drew.imaging.mp3.Mp3MetadataReader; import com.drew.imaging.mp4.Mp4MetadataReader; import com.drew.imaging.quicktime.QuickTimeMetadataReader; @@ -182,6 +183,8 @@ public static Metadata readMetadata(@NotNull final InputStream inputStream, fina case Heif: case Avif: return HeifMetadataReader.readMetadata(inputStream); + case Mkv: + return MkvMetadataReader.readMetadata(inputStream); case Unknown: throw new ImageProcessingException("File format could not be determined"); default: diff --git a/Source/com/drew/imaging/mkv/MkvMetadataReader.java b/Source/com/drew/imaging/mkv/MkvMetadataReader.java new file mode 100644 index 000000000..5d1b4623b --- /dev/null +++ b/Source/com/drew/imaging/mkv/MkvMetadataReader.java @@ -0,0 +1,20 @@ +package com.drew.imaging.mkv; + +import com.drew.lang.StreamReader; +import com.drew.lang.annotations.NotNull; +import com.drew.metadata.Metadata; +import com.drew.metadata.mkv.MkvReader; + +import java.io.IOException; +import java.io.InputStream; + +public class MkvMetadataReader +{ + @NotNull + public static Metadata readMetadata(@NotNull InputStream inputStream) throws IOException + { + Metadata metadata = new Metadata(); + new MkvReader().extract(new StreamReader(inputStream), metadata); + return metadata; + } +} diff --git a/Source/com/drew/imaging/mkv/package-info.java b/Source/com/drew/imaging/mkv/package-info.java new file mode 100644 index 000000000..4cd58342d --- /dev/null +++ b/Source/com/drew/imaging/mkv/package-info.java @@ -0,0 +1 @@ +package com.drew.imaging.mkv; diff --git a/Source/com/drew/metadata/mkv/AudioDirectory.java b/Source/com/drew/metadata/mkv/AudioDirectory.java new file mode 100644 index 000000000..8bb220034 --- /dev/null +++ b/Source/com/drew/metadata/mkv/AudioDirectory.java @@ -0,0 +1,45 @@ +package com.drew.metadata.mkv; + +import com.drew.metadata.Directory; +import com.drew.metadata.TagDescriptor; + +import java.util.HashMap; + +import static com.drew.metadata.mkv.ElementIDs.*; + +public class AudioDirectory extends Directory +{ + private static final HashMap _tagNameMap = new HashMap<>(); + + static + { + _tagNameMap.put(TRACK_NUMBER, "Track number"); + _tagNameMap.put(TRACK_UID, "Track UID"); + _tagNameMap.put(TRACK_TYPE, "Track type"); + _tagNameMap.put(TAG_LACING, "Tag lacing"); + _tagNameMap.put(CODEC_ID, "Codec ID"); + _tagNameMap.put(LANGUAGE, "Language"); + _tagNameMap.put(LANGUAGE_BCP47, "Language BCP47"); + _tagNameMap.put(DEFAULT_DURATION, "Default duration"); + _tagNameMap.put(CHANNELS, "Channels"); + _tagNameMap.put(SAMPLING_FREQUENCY, "Sampling frequency"); + _tagNameMap.put(BIT_DEPTH, "Bit depth"); + } + + public AudioDirectory() + { + this.setDescriptor(new TagDescriptor(this)); + } + + @Override + public String getName() + { + return "Audio"; + } + + @Override + protected HashMap getTagNameMap() + { + return _tagNameMap; + } +} diff --git a/Source/com/drew/metadata/mkv/DataParser.java b/Source/com/drew/metadata/mkv/DataParser.java new file mode 100644 index 000000000..dba9ad45f --- /dev/null +++ b/Source/com/drew/metadata/mkv/DataParser.java @@ -0,0 +1,82 @@ +package com.drew.metadata.mkv; + +import com.drew.lang.SequentialReader; + +import java.io.IOException; + +public class DataParser +{ + private static final long[] VSINT_SUBTR = {0x3F, 0x1FFF, 0x0FFFFF, 0x07FFFFFF, 0x03FFFFFFFFL, 0x01FFFFFFFFFFL, 0x00FFFFFFFFFFFFL, 0x007FFFFFFFFFFFFFL}; + + static long doDecodeInteger(final SequentialReader reader, boolean signed) throws IOException + { + byte firstByte = reader.getBytes(1)[0]; + int position = 7; + for (; position >= 0; position--) + { + if ((firstByte & (1 << position)) != 0) + { + break; + } + } + int length = 7 - position; + byte[] values = reader.getBytes(length); + long result = (firstByte & ((1L << position) - 1)) << (length * 8); + for (int i = 1; i <= length; i++) + { + result |= ((long) (values[i - 1] & 0xFF) << ((length - i) * 8)); + } + return signed ? result - VSINT_SUBTR[length] : result; + } + + static long decodeInteger(final SequentialReader reader) throws IOException + { + return doDecodeInteger(reader, false); + } + + static long decodeSignedInteger(final SequentialReader reader) throws IOException + { + return doDecodeInteger(reader, true); + } + + static int getElementId(final SequentialReader reader) throws IOException + { + byte firstByte = reader.getBytes(1)[0]; + int position = 7; + for (; position >= 0; position--) + { + if ((firstByte & (1 << position)) != 0) + { + break; + } + } + int length = 7 - position; + byte[] values = reader.getBytes(length); + int result = ((int) (firstByte & 0xFF)) << (length * 8); + for (int i = 1; i <= length; i++) + { + result |= (((int) values[i - 1] & 0xFF) << ((length - i) * 8)); + } + return result; + } + + static long getLong(final SequentialReader reader, long size) throws IOException + { + long result = 0L; + for (long i = size - 1; i >= 0; i--) + { + result |= (long) (reader.getByte() & 0xFF) << (i * 8); + } + return result; + } + + static byte[] getByteArray(final SequentialReader reader, long size) throws IOException + { + return reader.getBytes((int) size); + } + + static String getString(final SequentialReader reader, long size) throws IOException + { + return reader.getString((int) size); + } +} diff --git a/Source/com/drew/metadata/mkv/EbmlDirectory.java b/Source/com/drew/metadata/mkv/EbmlDirectory.java new file mode 100644 index 000000000..9e2b6428c --- /dev/null +++ b/Source/com/drew/metadata/mkv/EbmlDirectory.java @@ -0,0 +1,42 @@ +package com.drew.metadata.mkv; + +import com.drew.metadata.Directory; +import com.drew.metadata.TagDescriptor; + +import java.util.HashMap; + +import static com.drew.metadata.mkv.ElementIDs.*; + +public class EbmlDirectory extends Directory +{ + + private static final HashMap _tagNameMap = new HashMap<>(); + + static + { + _tagNameMap.put(EBML_VERSION, "Version"); + _tagNameMap.put(EBML_READ_VERSION, "Read version"); + _tagNameMap.put(EBML_MAX_ID_LENGTH, "Maximum ID length"); + _tagNameMap.put(EBML_MAX_SIZE_LENGTH, "Maximum size length"); + _tagNameMap.put(DOCTYPE, "Doctype"); + _tagNameMap.put(DOCTYPE_VERSION, "Doctype version"); + _tagNameMap.put(DOCTYPE_READ_VERSION, "Doctype read version"); + } + + public EbmlDirectory() + { + this.setDescriptor(new TagDescriptor(this)); + } + + @Override + public String getName() + { + return "EBML"; + } + + @Override + protected HashMap getTagNameMap() + { + return _tagNameMap; + } +} diff --git a/Source/com/drew/metadata/mkv/EbmlElement.java b/Source/com/drew/metadata/mkv/EbmlElement.java new file mode 100644 index 000000000..c6faa7c08 --- /dev/null +++ b/Source/com/drew/metadata/mkv/EbmlElement.java @@ -0,0 +1,50 @@ +package com.drew.metadata.mkv; + +public class EbmlElement +{ + private final String _name; + private final Type _type; + private final DirectoryType _directory; + + public EbmlElement(String _name, Type type) + { + this(_name, type, DirectoryType.UNKNOWN); + } + + public EbmlElement(String name, Type type, DirectoryType directory) + { + _name = name; + _type = type; + _directory = directory; + } + + public String toString() + { + return _name; + } + + public String get_name() + { + return _name; + } + + public Type get_type() + { + return _type; + } + + public DirectoryType getDirectory() + { + return _directory; + } + + public enum Type + { + MASTER, STRING, INTEGER, SIGNED_INTEGER, UTF8, BINARY, VOID, UNKNOWN, FLOAT + } + + public enum DirectoryType + { + EBML, SEGMENT, VIDEO, AUDIO, UNKNOWN + } +} diff --git a/Source/com/drew/metadata/mkv/ElementIDs.java b/Source/com/drew/metadata/mkv/ElementIDs.java new file mode 100644 index 000000000..ffdc983f8 --- /dev/null +++ b/Source/com/drew/metadata/mkv/ElementIDs.java @@ -0,0 +1,68 @@ +package com.drew.metadata.mkv; + +public class ElementIDs +{ + + static final int EBML_HEADER_ELEMENT = 0x1A45DFA3; + static final int EBML_VERSION = 0x4286; + static final int EBML_READ_VERSION = 0x42F7; + static final int EBML_MAX_ID_LENGTH = 0x42F2; + static final int EBML_MAX_SIZE_LENGTH = 0x42F3; + static final int DOCTYPE = 0x4282; + static final int DOCTYPE_VERSION = 0x4287; + static final int DOCTYPE_READ_VERSION = 0x4285; + static final int SEGMENT = 0x18538067; + static final int SEGMENT_INFO = 0x1549A966; + static final int SEEK_HEAD = 0x114D9B74; + static final int SEEK = 0x4DBB; + static final int SEEK_ID = 0x53AB; + static final int SEEK_POSITION = 0x53AC; + static final int MUXING_APP = 0x4D80; + static final int WRITING_APP = 0x5741; + static final int CODEC_ID = 0x86; + static final int VOID_ELEMENT = 0xEC; + static final int TIMESTAMP_SCALE = 0x2AD7B1; + static final int DURATION = 0x4489; + static final int CLUSTER = 0x1F43B675; + static final int SEGMENT_UUID = 0x73A4; + static final int TRACKS = 0x1654AE6B; + static final int TRACK_ENTRY = 0xAE; + static final int TRACK_NUMBER = 0xD7; + static final int TRACK_UID = 0x73C5; + static final int TRACK_TYPE = 0x83; + static final int TAG_LACING = 0x9C; + static final int AUDIO = 0xE1; + static final int CHANNELS = 0x9F; + static final int SAMPLING_FREQUENCY = 0xB5; + static final int BIT_DEPTH = 0x6264; + static final int CODEC_PRIVATE = 0x63A2; + static final int CUES = 0x1C53BB6B; + static final int LANGUAGE = 0x22B59C; + static final int LANGUAGE_BCP47 = 0x22B59D; + static final int DEFAULT_DURATION = 0x23E383; + static final int VIDEO = 0xE0; + static final int DISPLAY_WIDTH = 0x54B0; + static final int DISPLAY_HEIGHT = 0x54BA; + static final int DISPLAY_UNIT = 0x54B2; + static final int PIXEL_WIDTH = 0xB0; + static final int PIXEL_HEIGHT = 0xBA; + static final int FLAG_INTERLACED = 0x9A; + static final int COLOR = 0x55B0; + static final int TRANSFER_CHARACTERISTICS = 0x55BA; + static final int MATRIX_COEFFICIENTS = 0x55B1; + static final int PRIMARIES = 0x55BB; + static final int RANGE = 0x55B9; + static final int CHROMA_SITING_HORZ = 0x55B7; + static final int CHROMA_SITING_VERT = 0x55B8; + static final int CODEC_DELAY = 0x56AA; + static final int SEEK_PRE_ROLL = 0x56BB; + static final int TAGS = 0x1254C367; + static final int TAG = 0x7373; + static final int TARGETS = 0x63C0; + static final int SIMPLE_TAG = 0x67C8; + static final int TAG_NAME = 0x45A3; + static final int TAG_LANGUAGE = 0x447A; + static final int TAG_STRING = 0x4487; + static final int TAG_LANGUAGE_BCP47 = 0x447B; + static final int TAG_TRACK_UID = 0x63C5; +} diff --git a/Source/com/drew/metadata/mkv/MkvReader.java b/Source/com/drew/metadata/mkv/MkvReader.java new file mode 100644 index 000000000..69aaca2b5 --- /dev/null +++ b/Source/com/drew/metadata/mkv/MkvReader.java @@ -0,0 +1,276 @@ +package com.drew.metadata.mkv; + +import com.drew.lang.SequentialReader; +import com.drew.lang.StreamReader; +import com.drew.metadata.Directory; +import com.drew.metadata.Metadata; + +import java.io.ByteArrayInputStream; +import java.io.IOException; +import java.nio.charset.StandardCharsets; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +import static com.drew.metadata.mkv.DataParser.decodeInteger; +import static com.drew.metadata.mkv.EbmlElement.DirectoryType.EBML; +import static com.drew.metadata.mkv.EbmlElement.Type.*; + +public class MkvReader +{ + + private static final Map ELEMENTS = new HashMap<>(); + + static + { + // VOID is legit element type in Matroska spec, we're abusing it here to skip parts we don't need + ELEMENTS.put(ElementIDs.EBML_HEADER_ELEMENT, new EbmlElement("EBML_HEADER", MASTER)); + ELEMENTS.put(ElementIDs.EBML_VERSION, new EbmlElement("EBML_VERSION", INTEGER, EBML)); + ELEMENTS.put(ElementIDs.EBML_READ_VERSION, new EbmlElement("EBML_READ_VERSION", INTEGER, EBML)); + ELEMENTS.put(ElementIDs.EBML_MAX_ID_LENGTH, new EbmlElement("EBML_MAX_ID_LENGTH", INTEGER, EBML)); + ELEMENTS.put(ElementIDs.EBML_MAX_SIZE_LENGTH, new EbmlElement("EBML_MAX_SIZE_LENGTH", INTEGER, EBML)); + ELEMENTS.put(ElementIDs.DOCTYPE, new EbmlElement("DOCTYPE", STRING, EBML)); + ELEMENTS.put(ElementIDs.DOCTYPE_VERSION, new EbmlElement("DOCTYPE_VERSION", INTEGER, EBML)); + ELEMENTS.put(ElementIDs.DOCTYPE_READ_VERSION, new EbmlElement("DOCTYPE_READ_VERSION", INTEGER, EBML)); + ELEMENTS.put(ElementIDs.SEGMENT, new EbmlElement("SEGMENT", MASTER)); + ELEMENTS.put(ElementIDs.SEGMENT_INFO, new EbmlElement("SEGMENT_INFO", MASTER)); + ELEMENTS.put(ElementIDs.SEEK_HEAD, new EbmlElement("SEEK_HEAD", VOID)); + ELEMENTS.put(ElementIDs.SEEK, new EbmlElement("SEEK", MASTER)); + ELEMENTS.put(ElementIDs.SEEK_ID, new EbmlElement("SEEK_ID", BINARY)); + ELEMENTS.put(ElementIDs.SEEK_POSITION, new EbmlElement("SEEK_POSITION", INTEGER)); + ELEMENTS.put(ElementIDs.MUXING_APP, new EbmlElement("MUXING_APP", UTF8)); + ELEMENTS.put(ElementIDs.WRITING_APP, new EbmlElement("WRITING_APP", UTF8)); + ELEMENTS.put(ElementIDs.CODEC_ID, new EbmlElement("CODEC_ID", STRING)); + ELEMENTS.put(ElementIDs.VOID_ELEMENT, new EbmlElement("VOID", VOID)); + ELEMENTS.put(ElementIDs.TIMESTAMP_SCALE, new EbmlElement("TIMESTAMP_SCALE", INTEGER)); + ELEMENTS.put(ElementIDs.DURATION, new EbmlElement("DURATION", FLOAT)); + ELEMENTS.put(ElementIDs.CLUSTER, new EbmlElement("CLUSTER", VOID)); + ELEMENTS.put(ElementIDs.SEGMENT_UUID, new EbmlElement("SEGMENT_UUID", BINARY)); + ELEMENTS.put(ElementIDs.TRACKS, new EbmlElement("TRACKS", MASTER)); + ELEMENTS.put(ElementIDs.TRACK_ENTRY, new EbmlElement("TRACK_ENTRY", MASTER)); + ELEMENTS.put(ElementIDs.TRACK_NUMBER, new EbmlElement("TRACK_NUMBER", INTEGER)); + ELEMENTS.put(ElementIDs.TRACK_UID, new EbmlElement("TRACK_UID", INTEGER)); + ELEMENTS.put(ElementIDs.TRACK_TYPE, new EbmlElement("TRACK_TYPE", INTEGER)); + ELEMENTS.put(ElementIDs.TAG_LACING, new EbmlElement("TAG_LACING", INTEGER)); + ELEMENTS.put(ElementIDs.AUDIO, new EbmlElement("AUDIO", MASTER)); + ELEMENTS.put(ElementIDs.CHANNELS, new EbmlElement("CHANNELS", INTEGER)); + ELEMENTS.put(ElementIDs.SAMPLING_FREQUENCY, new EbmlElement("SAMPLING_FREQUENCY", FLOAT)); + ELEMENTS.put(ElementIDs.BIT_DEPTH, new EbmlElement("BIT_DEPTH", INTEGER)); + ELEMENTS.put(ElementIDs.CODEC_PRIVATE, new EbmlElement("CODEC_PRIVATE", VOID)); + ELEMENTS.put(ElementIDs.CUES, new EbmlElement("CUES", VOID)); + ELEMENTS.put(ElementIDs.LANGUAGE, new EbmlElement("LANGUAGE", STRING)); + ELEMENTS.put(ElementIDs.LANGUAGE_BCP47, new EbmlElement("LANGUAGE_BCP47", STRING)); + ELEMENTS.put(ElementIDs.DEFAULT_DURATION, new EbmlElement("DEFAULT_DURATION", INTEGER)); + ELEMENTS.put(ElementIDs.VIDEO, new EbmlElement("VIDEO", MASTER)); + ELEMENTS.put(ElementIDs.DISPLAY_WIDTH, new EbmlElement("DISPLAY_WIDTH", INTEGER)); + ELEMENTS.put(ElementIDs.DISPLAY_HEIGHT, new EbmlElement("DISPLAY_HEIGHT", INTEGER)); + ELEMENTS.put(ElementIDs.DISPLAY_UNIT, new EbmlElement("DISPLAY_UNIT", INTEGER)); + ELEMENTS.put(ElementIDs.PIXEL_WIDTH, new EbmlElement("PIXEL_WIDTH", INTEGER)); + ELEMENTS.put(ElementIDs.PIXEL_HEIGHT, new EbmlElement("PIXEL_HEIGHT", INTEGER)); + ELEMENTS.put(ElementIDs.FLAG_INTERLACED, new EbmlElement("FLAG_INTERLACED", INTEGER)); + ELEMENTS.put(ElementIDs.COLOR, new EbmlElement("COLOR", MASTER)); + ELEMENTS.put(ElementIDs.TRANSFER_CHARACTERISTICS, new EbmlElement("TRANSFER_CHARACTERISTICS", INTEGER)); + ELEMENTS.put(ElementIDs.MATRIX_COEFFICIENTS, new EbmlElement("MATRIX_COEFFICIENTS", INTEGER)); + ELEMENTS.put(ElementIDs.PRIMARIES, new EbmlElement("PRIMARIES", INTEGER)); + ELEMENTS.put(ElementIDs.RANGE, new EbmlElement("RANGE", INTEGER)); + ELEMENTS.put(ElementIDs.CHROMA_SITING_HORZ, new EbmlElement("CHROMA_SITING_HORZ", INTEGER)); + ELEMENTS.put(ElementIDs.CHROMA_SITING_VERT, new EbmlElement("CHROMA_SITING_VERT", INTEGER)); + ELEMENTS.put(ElementIDs.CODEC_DELAY, new EbmlElement("CODEC_DELAY", INTEGER)); + ELEMENTS.put(ElementIDs.SEEK_PRE_ROLL, new EbmlElement("SEEK_PRE_ROLL", INTEGER)); + ELEMENTS.put(ElementIDs.TAGS, new EbmlElement("TAGS", MASTER)); + ELEMENTS.put(ElementIDs.TAG, new EbmlElement("TAG", MASTER)); + ELEMENTS.put(ElementIDs.TARGETS, new EbmlElement("TARGETS", MASTER)); + ELEMENTS.put(ElementIDs.SIMPLE_TAG, new EbmlElement("SIMPLE_TAG", MASTER)); + ELEMENTS.put(ElementIDs.TAG_NAME, new EbmlElement("TAG_NAME", UTF8)); + ELEMENTS.put(ElementIDs.TAG_LANGUAGE, new EbmlElement("TAG_LANGUAGE", STRING)); + ELEMENTS.put(ElementIDs.TAG_STRING, new EbmlElement("TAG_STRING", UTF8)); + ELEMENTS.put(ElementIDs.TAG_LANGUAGE_BCP47, new EbmlElement("TAG_LANGUAGE_BCP47", STRING)); + ELEMENTS.put(ElementIDs.TAG_TRACK_UID, new EbmlElement("TAG_TRACK_UID", INTEGER)); + } + + public void extract(final SequentialReader reader, Metadata metadata) throws IOException + { + reader.setMotorolaByteOrder(true); + Map data = new HashMap<>(); + while (reader.available() > 0) + { + extractSubContext(reader, data); + } + getMetadata(data, metadata); + } + + + private void getMetadata(Map data, Metadata metadata) throws IOException + { + createDirectories(data, metadata); + for (Map.Entry entry : data.entrySet()) + { + if (entry.getValue() instanceof List) + { + for (Object member : (List) entry.getValue()) + { + doCreateDirectory(member, metadata); + } + } else + { + doCreateDirectory(entry.getValue(), metadata); + } + } + } + + @SuppressWarnings("unchecked") + private void doCreateDirectory(Object data, Metadata metadata) throws IOException + { + if (data instanceof Map) + { + createDirectories((Map) data, metadata); + } + } + + @SuppressWarnings("unchecked") + private void createDirectories(Map data, Metadata metadata) + { + Directory dir = null; + if (data.containsKey(ElementIDs.TRACK_TYPE)) + { + switch (((Long) data.get(ElementIDs.TRACK_TYPE)).intValue()) + { + case 1: + dir = new VideoDirectory(); + mapToDirectory(dir, (Map) data.get(ElementIDs.VIDEO)); + break; + case 2: + dir = new AudioDirectory(); + mapToDirectory(dir, (Map) data.get(ElementIDs.AUDIO)); + break; + } + if (dir != null) + { + mapToDirectory(dir, data); + metadata.addDirectory(dir); + } + } + if (data.containsKey(ElementIDs.SEGMENT_INFO)) + { + dir = new SegmentInfoDirectory(); + createDirectories((Map) data.get(ElementIDs.SEGMENT_INFO), metadata); + mapToDirectory(dir, (Map) data.get(ElementIDs.SEGMENT_INFO)); + metadata.addDirectory(dir); + } + if (data.containsKey(ElementIDs.EBML_HEADER_ELEMENT)) + { + dir = new EbmlDirectory(); + mapToDirectory(dir, (Map) data.get(ElementIDs.EBML_HEADER_ELEMENT)); + metadata.addDirectory(dir); + } + if (data.containsKey(ElementIDs.TRACKS)) + { + createDirectories((Map) data.get(ElementIDs.TRACKS), metadata); + } + if (data.containsKey(ElementIDs.TRACK_ENTRY)) + { + if (data.get(ElementIDs.TRACK_ENTRY) instanceof List) + { + for (Object entry : (List) data.get(ElementIDs.TRACK_ENTRY)) + { + if (entry instanceof Map) + { + createDirectories((Map) entry, metadata); + } + } + } + } + + } + + private void mapToDirectory(Directory directory, Map data) + { + for (Map.Entry values : data.entrySet()) + { + put(directory, values.getKey(), values.getValue()); + } + } + + private void put(Directory directory, int id, Object value) + { + EbmlElement element = ELEMENTS.get(id); + switch (element.get_type()) + { + case INTEGER: + case SIGNED_INTEGER: + directory.setLong(id, (long) value); + break; + case FLOAT: + directory.setDouble(id, (double) value); + break; + case STRING: + case UTF8: + directory.setString(id, (String) value); + break; + case BINARY: + directory.setByteArray(id, (byte[]) value); + break; + } + } + + private void extractSubContext(final SequentialReader reader, Map data) throws IOException + { + reader.setMotorolaByteOrder(true); + int eid = DataParser.getElementId(reader); + long size = decodeInteger(reader); + Object value = null; + EbmlElement element = ELEMENTS.get(eid); + + if (element == null) + { + element = new EbmlElement(String.format("0x%02X [ unknown ]", eid), UNKNOWN); + } + switch (element.get_type()) + { + case STRING: + value = DataParser.getString(reader, size); + break; + case INTEGER: + value = DataParser.getLong(reader, size); + if (eid == ElementIDs.DISPLAY_WIDTH) + { + System.out.println("VALUE " + value); + } + break; + case BINARY: + value = DataParser.getByteArray(reader, size); + break; + case MASTER: + StreamReader sc = new StreamReader(new ByteArrayInputStream(reader.getBytes((int) size))); + Map subData = new HashMap<>(); + while (sc.available() > 0) extractSubContext(sc, subData); + value = subData; + break; + case UTF8: + value = new String(reader.getBytes((int) size), StandardCharsets.UTF_8); + break; + case VOID: + reader.skip(size); + break; + case FLOAT: + value = size == 4 ? reader.getFloat32() : reader.getDouble64(); + break; + case UNKNOWN: + reader.skip(size); + return; + } + if (ELEMENTS.containsKey(eid) && value != null) + { + if (data.containsKey(eid)) + { + Object previous = data.get(eid); + List list = new ArrayList<>(); + list.add(previous); + list.add(value); + value = list; + } + data.put(eid, value); + } + } +} diff --git a/Source/com/drew/metadata/mkv/SegmentInfoDirectory.java b/Source/com/drew/metadata/mkv/SegmentInfoDirectory.java new file mode 100644 index 000000000..7834c7a1c --- /dev/null +++ b/Source/com/drew/metadata/mkv/SegmentInfoDirectory.java @@ -0,0 +1,38 @@ +package com.drew.metadata.mkv; + +import com.drew.metadata.Directory; +import com.drew.metadata.TagDescriptor; + +import java.util.HashMap; + +import static com.drew.metadata.mkv.ElementIDs.*; + +public class SegmentInfoDirectory extends Directory +{ + private static final HashMap _tagNameMap = new HashMap<>(); + + static + { + _tagNameMap.put(TIMESTAMP_SCALE, "Timestamp scale"); + _tagNameMap.put(MUXING_APP, "Muxing app"); + _tagNameMap.put(WRITING_APP, "Writing app"); + _tagNameMap.put(DURATION, "Duration"); + } + + public SegmentInfoDirectory() + { + this.setDescriptor(new TagDescriptor<>(this)); + } + + @Override + public String getName() + { + return "Segment"; + } + + @Override + protected HashMap getTagNameMap() + { + return _tagNameMap; + } +} diff --git a/Source/com/drew/metadata/mkv/VideoDirectory.java b/Source/com/drew/metadata/mkv/VideoDirectory.java new file mode 100644 index 000000000..a8ea68e26 --- /dev/null +++ b/Source/com/drew/metadata/mkv/VideoDirectory.java @@ -0,0 +1,54 @@ +package com.drew.metadata.mkv; + +import com.drew.metadata.Directory; +import com.drew.metadata.TagDescriptor; + +import java.util.HashMap; + +import static com.drew.metadata.mkv.ElementIDs.*; + +public class VideoDirectory extends Directory +{ + private static final HashMap _tagNameMap = new HashMap<>(); + + static + { + _tagNameMap.put(TRACK_NUMBER, "Track number"); + _tagNameMap.put(TRACK_UID, "Track UID"); + _tagNameMap.put(TRACK_TYPE, "Track type"); + _tagNameMap.put(TAG_LACING, "Tag lacing"); + _tagNameMap.put(CODEC_ID, "Codec ID"); + _tagNameMap.put(LANGUAGE, "Language"); + _tagNameMap.put(LANGUAGE_BCP47, "Language BCP47"); + _tagNameMap.put(DEFAULT_DURATION, "Default duration"); + _tagNameMap.put(PIXEL_HEIGHT, "Pixel height"); + _tagNameMap.put(PIXEL_WIDTH, "Pixel width"); + _tagNameMap.put(FLAG_INTERLACED, "Interalced"); + _tagNameMap.put(CHROMA_SITING_HORZ, "Chroma siting horizontal"); + _tagNameMap.put(CHROMA_SITING_VERT, "Chroma siting vertical"); + _tagNameMap.put(TRANSFER_CHARACTERISTICS, "Color transfer characteristics"); + _tagNameMap.put(MATRIX_COEFFICIENTS, "Color matrix coefficients"); + _tagNameMap.put(PRIMARIES, "Color primaries"); + _tagNameMap.put(RANGE, "Color range"); + _tagNameMap.put(DISPLAY_WIDTH, "Display width"); + _tagNameMap.put(DISPLAY_HEIGHT, "Display height"); + _tagNameMap.put(DISPLAY_UNIT, "Display unit"); + } + + public VideoDirectory() + { + this.setDescriptor(new TagDescriptor<>(this)); + } + + @Override + public String getName() + { + return "Video"; + } + + @Override + protected HashMap getTagNameMap() + { + return _tagNameMap; + } +} diff --git a/Source/com/drew/metadata/mkv/package-info.java b/Source/com/drew/metadata/mkv/package-info.java new file mode 100644 index 000000000..dfb50d61d --- /dev/null +++ b/Source/com/drew/metadata/mkv/package-info.java @@ -0,0 +1 @@ +package com.drew.metadata.mkv;