diff --git a/Source/com/drew/imaging/mp4/Mp4Reader.java b/Source/com/drew/imaging/mp4/Mp4Reader.java index 1abd987a4..844df0944 100644 --- a/Source/com/drew/imaging/mp4/Mp4Reader.java +++ b/Source/com/drew/imaging/mp4/Mp4Reader.java @@ -62,7 +62,11 @@ private static void processBoxes(StreamReader reader, long atomEnd, Mp4Handler h } else if (box.usertype != null) { reader.skip(box.size - 24); } else if (box.size > 1) { - reader.skip(box.size - 8); + if (box.isLargeSize) { + reader.skip(box.size - 16); + } else { + reader.skip(box.size - 8); + } } else if (box.size == -1) { break; } diff --git a/Source/com/drew/metadata/mov/QuickTimeAtomHandler.java b/Source/com/drew/metadata/mov/QuickTimeAtomHandler.java index 0c9af4f6c..cc0a02fc4 100644 --- a/Source/com/drew/metadata/mov/QuickTimeAtomHandler.java +++ b/Source/com/drew/metadata/mov/QuickTimeAtomHandler.java @@ -28,6 +28,7 @@ import com.drew.metadata.Metadata; import com.drew.metadata.mov.atoms.*; import com.drew.metadata.mov.atoms.canon.CanonThumbnailAtom; +import com.drew.metadata.xmp.XmpReader; import java.io.IOException; @@ -57,7 +58,8 @@ public boolean shouldAcceptAtom(@NotNull Atom atom) || atom.type.equals(QuickTimeAtomTypes.ATOM_MOVIE_HEADER) || atom.type.equals(QuickTimeAtomTypes.ATOM_HANDLER) || atom.type.equals(QuickTimeAtomTypes.ATOM_MEDIA_HEADER) - || atom.type.equals(QuickTimeAtomTypes.ATOM_CANON_THUMBNAIL); + || atom.type.equals(QuickTimeAtomTypes.ATOM_CANON_THUMBNAIL) + || atom.type.equals(QuickTimeAtomTypes.ATOM_ADOBE_XMP); } @Override @@ -90,6 +92,8 @@ public QuickTimeHandler processAtom(@NotNull Atom atom, @Nullable byte[] payload } else if (atom.type.equals(QuickTimeAtomTypes.ATOM_CANON_THUMBNAIL)) { CanonThumbnailAtom canonThumbnailAtom = new CanonThumbnailAtom(reader); canonThumbnailAtom.addMetadata(directory); + } else if (atom.type.equals(QuickTimeAtomTypes.ATOM_ADOBE_XMP)) { + new XmpReader().extract(payload, metadata, directory); } } else { if (atom.type.equals(QuickTimeContainerTypes.ATOM_COMPRESSED_MOVIE)) { diff --git a/Source/com/drew/metadata/mov/QuickTimeAtomTypes.java b/Source/com/drew/metadata/mov/QuickTimeAtomTypes.java index 053335542..a02096687 100644 --- a/Source/com/drew/metadata/mov/QuickTimeAtomTypes.java +++ b/Source/com/drew/metadata/mov/QuickTimeAtomTypes.java @@ -40,6 +40,7 @@ public class QuickTimeAtomTypes public static final String ATOM_TIME_TO_SAMPLE = "stts"; public static final String ATOM_MEDIA_HEADER = "mdhd"; public static final String ATOM_CANON_THUMBNAIL = "CNTH"; + public static final String ATOM_ADOBE_XMP = "XMP_"; private static final ArrayList _atomList = new ArrayList(); @@ -57,5 +58,6 @@ public class QuickTimeAtomTypes _atomList.add(ATOM_TIME_TO_SAMPLE); _atomList.add(ATOM_MEDIA_HEADER); _atomList.add(ATOM_CANON_THUMBNAIL); + _atomList.add(ATOM_ADOBE_XMP); } } diff --git a/Source/com/drew/metadata/mov/QuickTimeDirectory.java b/Source/com/drew/metadata/mov/QuickTimeDirectory.java index 68bb91bde..5af040623 100644 --- a/Source/com/drew/metadata/mov/QuickTimeDirectory.java +++ b/Source/com/drew/metadata/mov/QuickTimeDirectory.java @@ -54,6 +54,8 @@ public class QuickTimeDirectory extends Directory { public static final int TAG_CANON_THUMBNAIL_DT = 0x2000; + public static final int TAG_ADOBE_XMP = 0x3000; + @NotNull private static final HashMap _tagNameMap = new HashMap(); @@ -80,6 +82,8 @@ public class QuickTimeDirectory extends Directory { _tagNameMap.put(TAG_MEDIA_TIME_SCALE, "Media Time Scale"); _tagNameMap.put(TAG_CANON_THUMBNAIL_DT, "Canon Thumbnail DateTime"); + + _tagNameMap.put(TAG_ADOBE_XMP, "Adobe Bridge XMP"); } public QuickTimeDirectory() diff --git a/Source/com/drew/metadata/mp4/Mp4BoxHandler.java b/Source/com/drew/metadata/mp4/Mp4BoxHandler.java index 5a2d8a448..af5f3b8db 100644 --- a/Source/com/drew/metadata/mp4/Mp4BoxHandler.java +++ b/Source/com/drew/metadata/mp4/Mp4BoxHandler.java @@ -27,6 +27,7 @@ import com.drew.lang.annotations.Nullable; import com.drew.metadata.Metadata; import com.drew.metadata.mp4.boxes.*; +import com.drew.metadata.mp4.media.Mp4UuidBoxHandler; import java.io.IOException; @@ -56,7 +57,8 @@ public boolean shouldAcceptBox(@NotNull Box box) || box.type.equals(Mp4BoxTypes.BOX_MOVIE_HEADER) || box.type.equals(Mp4BoxTypes.BOX_HANDLER) || box.type.equals(Mp4BoxTypes.BOX_MEDIA_HEADER) - || box.type.equals(Mp4BoxTypes.BOX_TRACK_HEADER); + || box.type.equals(Mp4BoxTypes.BOX_TRACK_HEADER) + || box.type.equals(Mp4BoxTypes.BOX_USER_DEFINED); } @Override @@ -84,6 +86,9 @@ public Mp4Handler processBox(@NotNull Box box, @Nullable byte[] payload, Mp4Cont processMediaHeader(reader, box, context); } else if (box.type.equals(Mp4BoxTypes.BOX_TRACK_HEADER)) { processTrackHeader(reader, box); + } else if (box.type.equals(Mp4BoxTypes.BOX_USER_DEFINED)) { + Mp4UuidBoxHandler userBoxHandler = new Mp4UuidBoxHandler(metadata); + userBoxHandler.processBox(box, payload, context); } } else { if (box.type.equals(Mp4ContainerTypes.BOX_COMPRESSED_MOVIE)) { diff --git a/Source/com/drew/metadata/mp4/Mp4BoxTypes.java b/Source/com/drew/metadata/mp4/Mp4BoxTypes.java index 259c3cf5f..a4f675f3b 100644 --- a/Source/com/drew/metadata/mp4/Mp4BoxTypes.java +++ b/Source/com/drew/metadata/mp4/Mp4BoxTypes.java @@ -38,6 +38,7 @@ public class Mp4BoxTypes public static final String BOX_TIME_TO_SAMPLE = "stts"; public static final String BOX_MEDIA_HEADER = "mdhd"; public static final String BOX_TRACK_HEADER = "tkhd"; + public static final String BOX_USER_DEFINED = "uuid"; private static final ArrayList _boxList = new ArrayList(); @@ -53,5 +54,6 @@ public class Mp4BoxTypes _boxList.add(BOX_TIME_TO_SAMPLE); _boxList.add(BOX_MEDIA_HEADER); _boxList.add(BOX_TRACK_HEADER); + _boxList.add(BOX_USER_DEFINED); } } diff --git a/Source/com/drew/metadata/mp4/boxes/Box.java b/Source/com/drew/metadata/mp4/boxes/Box.java index 2acb8707e..101a9cbd7 100644 --- a/Source/com/drew/metadata/mp4/boxes/Box.java +++ b/Source/com/drew/metadata/mp4/boxes/Box.java @@ -32,6 +32,7 @@ public class Box public long size; public String type; public String usertype; + public boolean isLargeSize; public Box(SequentialReader reader) throws IOException { @@ -39,12 +40,10 @@ public Box(SequentialReader reader) throws IOException this.type = reader.getString(4); if (size == 1) { size = reader.getInt64(); + isLargeSize = true; } else if (size == 0) { size = -1; } - if (type.equals("uuid")) { - usertype = reader.getString(16); - } } public Box(Box box) diff --git a/Source/com/drew/metadata/mp4/boxes/MediaHeaderBox.java b/Source/com/drew/metadata/mp4/boxes/MediaHeaderBox.java index 17d297b21..23e850082 100644 --- a/Source/com/drew/metadata/mp4/boxes/MediaHeaderBox.java +++ b/Source/com/drew/metadata/mp4/boxes/MediaHeaderBox.java @@ -22,7 +22,6 @@ import com.drew.lang.SequentialReader; import com.drew.metadata.mp4.Mp4Context; -import com.drew.metadata.mp4.Mp4HandlerFactory; import java.io.IOException; diff --git a/Source/com/drew/metadata/mp4/boxes/UuidBox.java b/Source/com/drew/metadata/mp4/boxes/UuidBox.java new file mode 100644 index 000000000..9de99f170 --- /dev/null +++ b/Source/com/drew/metadata/mp4/boxes/UuidBox.java @@ -0,0 +1,60 @@ +/* + * Copyright 2002-2019 Drew Noakes and contributors + * + * Licensed 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. + * + * More information about this project is available at: + * + * https://drewnoakes.com/code/exif/ + * https://github.com/drewnoakes/metadata-extractor + */ +package com.drew.metadata.mp4.boxes; + +import com.drew.lang.SequentialReader; +import com.drew.metadata.mp4.Mp4BoxTypes; +import com.drew.metadata.mp4.Mp4Directory; + +import java.io.IOException; +import java.nio.ByteBuffer; +import java.util.UUID; + +import static com.drew.metadata.mp4.media.Mp4UuidBoxDirectory.*; + +public class UuidBox extends Box +{ + private byte[] userData; + + public UuidBox(SequentialReader reader, Box box) throws IOException + { + super(box); + + if (type.equals(Mp4BoxTypes.BOX_USER_DEFINED)) { + usertype = getUuid(reader.getBytes(16)); + } + + userData = reader.getBytes(reader.available()); + } + + public void addMetadata(Mp4Directory directory) + { + directory.setString(TAG_UUID, usertype); + directory.setByteArray(TAG_USER_DATA, userData); + } + + private String getUuid(byte[] bytes) { + ByteBuffer bb = ByteBuffer.wrap(bytes); + UUID uuid = new UUID(bb.getLong(), bb.getLong()); + + return uuid.toString(); + } +} diff --git a/Source/com/drew/metadata/mp4/media/Mp4UuidBoxDescriptor.java b/Source/com/drew/metadata/mp4/media/Mp4UuidBoxDescriptor.java new file mode 100644 index 000000000..e02e2aa76 --- /dev/null +++ b/Source/com/drew/metadata/mp4/media/Mp4UuidBoxDescriptor.java @@ -0,0 +1,31 @@ +/* + * Copyright 2002-2019 Drew Noakes and contributors + * + * Licensed 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. + * + * More information about this project is available at: + * + * https://drewnoakes.com/code/exif/ + * https://github.com/drewnoakes/metadata-extractor + */ +package com.drew.metadata.mp4.media; + +import com.drew.metadata.TagDescriptor; + +public class Mp4UuidBoxDescriptor extends TagDescriptor +{ + public Mp4UuidBoxDescriptor(Mp4UuidBoxDirectory directory) + { + super(directory); + } +} diff --git a/Source/com/drew/metadata/mp4/media/Mp4UuidBoxDirectory.java b/Source/com/drew/metadata/mp4/media/Mp4UuidBoxDirectory.java new file mode 100644 index 000000000..7974323b7 --- /dev/null +++ b/Source/com/drew/metadata/mp4/media/Mp4UuidBoxDirectory.java @@ -0,0 +1,60 @@ +/* + * Copyright 2002-2019 Drew Noakes and contributors + * + * Licensed 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. + * + * More information about this project is available at: + * + * https://drewnoakes.com/code/exif/ + * https://github.com/drewnoakes/metadata-extractor + */ +package com.drew.metadata.mp4.media; + +import com.drew.lang.annotations.NotNull; + +import java.util.HashMap; + +public class Mp4UuidBoxDirectory extends Mp4MediaDirectory +{ + public static final Integer TAG_UUID = 901; + public static final Integer TAG_USER_DATA = 902; + + @NotNull + private static final HashMap _tagNameMap = new HashMap(); + + static + { + Mp4UuidBoxDirectory.addMp4MediaTags(_tagNameMap); + _tagNameMap.put(TAG_UUID, "uuid"); + _tagNameMap.put(TAG_USER_DATA, "data"); + } + + public Mp4UuidBoxDirectory() + { + this.setDescriptor(new Mp4UuidBoxDescriptor(this)); + } + + @NotNull + @Override + public String getName() + { + return "UUID"; + } + + @NotNull + @Override + protected HashMap getTagNameMap() + { + return _tagNameMap; + } +} diff --git a/Source/com/drew/metadata/mp4/media/Mp4UuidBoxHandler.java b/Source/com/drew/metadata/mp4/media/Mp4UuidBoxHandler.java new file mode 100644 index 000000000..36420e466 --- /dev/null +++ b/Source/com/drew/metadata/mp4/media/Mp4UuidBoxHandler.java @@ -0,0 +1,129 @@ +/* + * Copyright 2002-2019 Drew Noakes and contributors + * + * Licensed 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. + * + * More information about this project is available at: + * + * https://drewnoakes.com/code/exif/ + * https://github.com/drewnoakes/metadata-extractor + */ +package com.drew.metadata.mp4.media; + +import com.drew.imaging.mp4.Mp4Handler; +import com.drew.lang.ByteArrayReader; +import com.drew.lang.ByteTrie; +import com.drew.lang.SequentialByteArrayReader; +import com.drew.lang.SequentialReader; +import com.drew.metadata.Metadata; +import com.drew.metadata.exif.ExifReader; +import com.drew.metadata.iptc.IptcReader; +import com.drew.metadata.mp4.Mp4BoxTypes; +import com.drew.metadata.mp4.Mp4Context; +import com.drew.metadata.mp4.boxes.Box; +import com.drew.metadata.mp4.boxes.UuidBox; +import com.drew.metadata.photoshop.PhotoshopReader; +import com.drew.metadata.xmp.XmpReader; + +import java.io.IOException; + +public class Mp4UuidBoxHandler extends Mp4Handler +{ + // http://xhelmboyx.tripod.com/formats/mp4-layout.txt + // http://fileformats.archiveteam.org/wiki/Boxes/atoms_format#UUID_boxes + + private enum UuidType + { + Unknown, + Exif, // 0537cdab-9d0c-4431-a72a-fa561f2a113e + PhotoshopImageResources, // 2c4c0100-8504-40b9-a03e-562148d6dfeb + IptcIim, // 33c7a4d2-b81d-4723-a0ba-f1a3e097ad38 + PiffTrackEncryptionBox, // 8974dbce-7be7-4c51-84f9-7148f9882554 + GeoJp2WorldFileBox, // 96a9f1f1-dc98-402d-a7ae-d68e34451809 + PiffSampleEncryptionBox, // a2394f52-5a9b-4f14-a244-6c427c648df4 + GeoJp2GeoTiffBox, // b14bf8bd-083d-4b43-a5ae-8cd7d5a6ce03 + Xmp, // be7acfcb-97a9-42e8-9c71-999491e3afac + PiffProtectionSystemSpecificHeaderBox, // d08a4f18-10f3-4a82-b6c8-32d8aba183d3 + } + + private final static ByteTrie _uuidLookup; + + static + { + _uuidLookup = new ByteTrie(); + _uuidLookup.setDefaultValue(UuidType.Unknown); + + _uuidLookup.addPath(UuidType.Exif, new byte[] { 0x05, 0x37, (byte)0xcd, (byte)0xab, (byte)0x9d, 0x0c, 0x44, 0x31, (byte)0xa7, 0x2a, (byte)0xfa, 0x56, 0x1f, 0x2a, 0x11, 0x3e }); + _uuidLookup.addPath(UuidType.PhotoshopImageResources, new byte[] { 0x2c, 0x4c, 0x01, 0x00, (byte)0x85, 0x04, 0x40, (byte)0xb9, (byte)0xa0, 0x3e, 0x56, 0x21, 0x48, (byte)0xd6, (byte)0xdf, (byte)0xeb }); + _uuidLookup.addPath(UuidType.IptcIim, new byte[] { 0x33, (byte)0xc7, (byte)0xa4, (byte)0xd2, (byte)0xb8, 0x1d, 0x47, 0x23, (byte)0xa0, (byte)0xba, (byte)0xf1, (byte)0xa3, (byte)0xe0, (byte)0x97, (byte)0xad, 0x38 }); + _uuidLookup.addPath(UuidType.PiffTrackEncryptionBox, new byte[] { (byte)0x89, 0x74, (byte)0xdb, (byte)0xce, 0x7b, (byte)0xe7, 0x4c, 0x51, (byte)0x84, (byte)0xf9, 0x71, 0x48, (byte)0xf9, (byte)0x88, 0x25, 0x54 }); + _uuidLookup.addPath(UuidType.GeoJp2WorldFileBox, new byte[] { (byte)0x96, (byte)0xa9, (byte)0xf1, (byte)0xf1, (byte)0xdc, (byte)0x98, 0x40, 0x2d, (byte)0xa7, (byte)0xae, (byte)0xd6, (byte)0x8e, 0x34, 0x45, 0x18, 0x09 }); + _uuidLookup.addPath(UuidType.PiffSampleEncryptionBox, new byte[] { (byte)0xa2, 0x39, 0x4f, 0x52, 0x5a, (byte)0x9b, 0x4f, 0x14, (byte)0xa2, 0x44, 0x6c, 0x42, 0x7c, 0x64, (byte)0x8d, (byte)0xf4 }); + _uuidLookup.addPath(UuidType.GeoJp2GeoTiffBox, new byte[] { (byte)0xb1, 0x4b, (byte)0xf8, (byte)0xbd, 0x08, 0x3d, 0x4b, 0x43, (byte)0xa5, (byte)0xae, (byte)0x8c, (byte)0xd7, (byte)0xd5, (byte)0xa6, (byte)0xce, 0x03 }); + _uuidLookup.addPath(UuidType.Xmp, new byte[] { (byte)0xbe, 0x7a, (byte)0xcf, (byte)0xcb, (byte)0x97, (byte)0xa9, 0x42, (byte)0xe8, (byte)0x9c, 0x71, (byte)0x99, (byte)0x94, (byte)0x91, (byte)0xe3, (byte)0xaf, (byte)0xac }); + _uuidLookup.addPath(UuidType.PiffProtectionSystemSpecificHeaderBox, new byte[] { (byte)0xd0, (byte)0x8a, 0x4f, 0x18, 0x10, (byte)0xf3, 0x4a, (byte)0x82, (byte)0xb6, (byte)0xc8, 0x32, (byte)0xd8, (byte)0xab, (byte)0xa1, (byte)0x83, (byte)0xd3 }); + } + + public Mp4UuidBoxHandler(Metadata metadata) + { + super(metadata); + } + + @Override + protected Mp4UuidBoxDirectory getDirectory() + { + return new Mp4UuidBoxDirectory(); + } + + @Override + protected boolean shouldAcceptBox(Box box) + { + return box.type.equals(Mp4BoxTypes.BOX_USER_DEFINED); + } + + @Override + protected boolean shouldAcceptContainer(Box box) + { + return false; + } + + @Override + public Mp4Handler processBox(Box box, byte[] payload, Mp4Context context) throws IOException + { + if (payload != null && payload.length >= 16) { + UuidType type = _uuidLookup.find(payload); + + switch (type) { + case Exif: + new ExifReader().extract(new ByteArrayReader(payload, 16), metadata, 0, directory); + break; + case IptcIim: + new IptcReader().extract(new SequentialByteArrayReader(payload, 16), metadata, payload.length - 16, directory); + break; + case PhotoshopImageResources: + new PhotoshopReader().extract(new SequentialByteArrayReader(payload, 16), payload.length - 16, metadata, directory); + break; + case Xmp: + new XmpReader().extract(payload, 16, payload.length - 16, metadata, directory); + break; + default: + SequentialReader reader = new SequentialByteArrayReader(payload); + UuidBox userBox = new UuidBox(reader, box); + userBox.addMetadata(directory); + break; + } + } + + return this; + } +}