Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add support for MP4 UUID and MOV XMP boxes #451

Closed
wants to merge 5 commits into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 5 additions & 1 deletion Source/com/drew/imaging/mp4/Mp4Reader.java
Original file line number Diff line number Diff line change
Expand Up @@ -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;
}
Expand Down
6 changes: 5 additions & 1 deletion Source/com/drew/metadata/mov/QuickTimeAtomHandler.java
Original file line number Diff line number Diff line change
Expand Up @@ -57,7 +57,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
Expand Down Expand Up @@ -90,6 +91,9 @@ 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)) {
XmpAtom xmpAtom = new XmpAtom(reader, atom);
xmpAtom.addMetadata(directory);
}
} else {
if (atom.type.equals(QuickTimeContainerTypes.ATOM_COMPRESSED_MOVIE)) {
Expand Down
2 changes: 2 additions & 0 deletions Source/com/drew/metadata/mov/QuickTimeAtomTypes.java
Original file line number Diff line number Diff line change
Expand Up @@ -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<String> _atomList = new ArrayList<String>();

Expand All @@ -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);
}
}
4 changes: 4 additions & 0 deletions Source/com/drew/metadata/mov/QuickTimeDirectory.java
Original file line number Diff line number Diff line change
Expand Up @@ -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<Integer, String> _tagNameMap = new HashMap<Integer, String>();

Expand All @@ -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()
Expand Down
23 changes: 23 additions & 0 deletions Source/com/drew/metadata/mov/atoms/XmpAtom.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
package com.drew.metadata.mov.atoms;

import com.drew.lang.SequentialReader;
import com.drew.metadata.mov.QuickTimeDirectory;

import java.io.IOException;

public class XmpAtom extends Atom
{
private String xmp;

public XmpAtom(SequentialReader reader, Atom atom) throws IOException
{
super(atom);

xmp = reader.getString(reader.available());
}

public void addMetadata(QuickTimeDirectory directory)
{
directory.setString(QuickTimeDirectory.TAG_ADOBE_XMP, xmp);
}
}
7 changes: 6 additions & 1 deletion Source/com/drew/metadata/mp4/Mp4BoxHandler.java
Original file line number Diff line number Diff line change
Expand Up @@ -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;

Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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)) {
Expand Down
2 changes: 2 additions & 0 deletions Source/com/drew/metadata/mp4/Mp4BoxTypes.java
Original file line number Diff line number Diff line change
Expand Up @@ -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<String> _boxList = new ArrayList<String>();

Expand All @@ -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);
}
}
5 changes: 2 additions & 3 deletions Source/com/drew/metadata/mp4/boxes/Box.java
Original file line number Diff line number Diff line change
Expand Up @@ -32,19 +32,18 @@ public class Box
public long size;
public String type;
public String usertype;
public boolean isLargeSize;

public Box(SequentialReader reader) throws IOException
{
this.size = reader.getUInt32();
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)
Expand Down
1 change: 0 additions & 1 deletion Source/com/drew/metadata/mp4/boxes/MediaHeaderBox.java
Original file line number Diff line number Diff line change
Expand Up @@ -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;

Expand Down
40 changes: 40 additions & 0 deletions Source/com/drew/metadata/mp4/boxes/UuidBox.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
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);
}

public String getUuid(byte[] bytes) {
ByteBuffer bb = ByteBuffer.wrap(bytes);
UUID uuid = new UUID(bb.getLong(), bb.getLong());

return uuid.toString();
}
}
11 changes: 11 additions & 0 deletions Source/com/drew/metadata/mp4/media/Mp4UuidBoxDescriptor.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
package com.drew.metadata.mp4.media;

import com.drew.metadata.TagDescriptor;

public class Mp4UuidBoxDescriptor extends TagDescriptor<Mp4UuidBoxDirectory> {

public Mp4UuidBoxDescriptor(Mp4UuidBoxDirectory directory)
{
super(directory);
}
}
39 changes: 39 additions & 0 deletions Source/com/drew/metadata/mp4/media/Mp4UuidBoxDirectory.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
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<Integer, String> _tagNameMap = new HashMap<Integer, String>();

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<Integer, String> getTagNameMap()
{
return _tagNameMap;
}
}
50 changes: 50 additions & 0 deletions Source/com/drew/metadata/mp4/media/Mp4UuidBoxHandler.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
package com.drew.metadata.mp4.media;

import com.drew.imaging.mp4.Mp4Handler;
import com.drew.lang.SequentialByteArrayReader;
import com.drew.lang.SequentialReader;
import com.drew.metadata.Metadata;
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 java.io.IOException;

public class Mp4UuidBoxHandler<T extends Mp4UuidBoxDirectory> extends Mp4Handler<Mp4UuidBoxDirectory> {

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) {
SequentialReader reader = new SequentialByteArrayReader(payload);
UuidBox userBox = new UuidBox(reader, box);
userBox.addMetadata(directory);
}

return this;
}
}
Binary file added Tests/Data/uuid540.mp4
Binary file not shown.
Binary file added Tests/Data/xmp480qt.mov
Binary file not shown.
58 changes: 58 additions & 0 deletions Tests/com/drew/metadata/mov/XmpAtomTest.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
package com.drew.metadata.mov;

import com.adobe.internal.xmp.XMPConst;
import com.adobe.internal.xmp.XMPException;
import com.adobe.internal.xmp.XMPMeta;
import com.adobe.internal.xmp.XMPMetaFactory;
import com.adobe.internal.xmp.options.ParseOptions;
import com.adobe.internal.xmp.properties.XMPProperty;
import com.drew.imaging.quicktime.QuickTimeReader;
import com.drew.metadata.Directory;
import com.drew.metadata.Metadata;
import org.junit.Test;

import java.io.FileInputStream;
import java.io.IOException;
import java.util.HashSet;
import java.util.Set;

import static org.junit.Assert.assertEquals;

public class XmpAtomTest {

@Test
public void extractXmpTest() throws IOException, XMPException
{
Metadata metadata = new Metadata();
QuickTimeReader.extract(new FileInputStream("Tests/Data/xmp480qt.mov"), new QuickTimeAtomHandler(metadata));

String title = "";
String description = "";
Set<String> keywords = new HashSet<String>();
for (Directory d : metadata.getDirectories()) {
QuickTimeDirectory quickTimeDirectory = (QuickTimeDirectory) d;
if (quickTimeDirectory.getName().equals("QuickTime")) {
String xmp = quickTimeDirectory.getString(0x3000);
XMPMeta xmpMeta = XMPMetaFactory.parseFromString(xmp, new ParseOptions().setOmitNormalization(true));
XMPProperty xmpTitle = xmpMeta.getProperty(XMPConst.NS_PHOTOSHOP, "Headline");
XMPProperty xmpDescription = xmpMeta.getArrayItem(XMPConst.NS_DC, "description", 1);
title = xmpTitle != null ? xmpTitle.getValue() : "";
description = xmpDescription != null ? xmpDescription.getValue() : "";

int k = 1;
while (true) {
XMPProperty keyword = xmpMeta.getArrayItem(XMPConst.NS_DC, "subject", k++);
if (keyword != null) {
keywords.add(keyword.getValue());
} else {
break;
}
}
}

assertEquals("Baltic sea timelapse title", title);
assertEquals("Baltic sea timelapse description", description);
assertEquals(3, keywords.size());
}
}
}
Loading