Skip to content

Commit 660fef4

Browse files
committed
[TIKA-2861] Handle numeric QuickTime metadata value types
The item-list parser only emitted UTF-8 text values (well-known type 1) and silently dropped the numeric types. Real iPhone Live Photo videos carry several of those, e.g. com.apple.quicktime.live-photo.vitality-score (float32), live-photo.auto (uint8) and camera.focal_length.35mm_equivalent (int32). Decode the QTFF well-known types 21 (signed int BE), 22 (unsigned int BE), 23 (float32) and 24 (float64) as well. Integers may be 1 to 8 bytes wide. Other value types (images, binary plists) are still skipped.
1 parent 4b06331 commit 660fef4

3 files changed

Lines changed: 53 additions & 9 deletions

File tree

tika-parsers/tika-parsers-standard/tika-parsers-standard-modules/tika-parser-audiovideo-module/src/main/java/org/apache/tika/parser/mp4/TikaMp4BoxHandler.java

Lines changed: 45 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -17,8 +17,11 @@
1717
package org.apache.tika.parser.mp4;
1818

1919
import java.io.IOException;
20+
import java.math.BigInteger;
21+
import java.nio.ByteBuffer;
2022
import java.nio.charset.StandardCharsets;
2123
import java.util.ArrayList;
24+
import java.util.Arrays;
2225
import java.util.List;
2326
import java.util.regex.Matcher;
2427
import java.util.regex.Pattern;
@@ -37,8 +40,12 @@
3740

3841
public class TikaMp4BoxHandler extends Mp4BoxHandler {
3942

40-
//metadata item value type for UTF-8 text (QTFF "well-known" type 1)
43+
//QTFF "well-known" metadata item value types
4144
private static final int QT_TEXT_TYPE = 1;
45+
private static final int QT_INT_BE_TYPE = 21;
46+
private static final int QT_UINT_BE_TYPE = 22;
47+
private static final int QT_FLOAT32_TYPE = 23;
48+
private static final int QT_FLOAT64_TYPE = 24;
4249

4350
//QuickTime stores location as an ISO 6709 string (e.g. +32.4720-084.9952+073.827/)
4451
private static final String QT_LOCATION_ISO6709 = "com.apple.quicktime.location.ISO6709";
@@ -133,7 +140,8 @@ private void processQuickTimeKeys(@Nullable byte[] payload) {
133140
/**
134141
* Parses the QuickTime metadata 'ilst' box, whose entries are keyed by the
135142
* 1-based index into the preceding 'keys' box. Each entry holds a 'data' box
136-
* with the value. Only UTF-8 text values are emitted, under their key name.
143+
* with the value. UTF-8 text and the numeric "well-known" value types are emitted
144+
* under their key name; other types (e.g. images, binary plists) are skipped.
137145
*/
138146
private void processQuickTimeItemList(@Nullable byte[] payload) {
139147
if (payload == null) {
@@ -156,14 +164,14 @@ private void processQuickTimeItemList(@Nullable byte[] payload) {
156164
if (isData && dataSize >= 16 && data + dataSize <= entryEnd) {
157165
int valueType = (int) readUInt32(payload, data + 8);
158166
int valueLength = (int) dataSize - 16;
159-
if (valueType == QT_TEXT_TYPE
160-
&& index >= 1 && index <= quickTimeMetadataKeys.size()) {
167+
if (index >= 1 && index <= quickTimeMetadataKeys.size()) {
161168
String key = quickTimeMetadataKeys.get(index - 1);
162-
String value =
163-
new String(payload, data + 16, valueLength, StandardCharsets.UTF_8);
164-
tikaMetadata.add(key, value);
165-
if (key.equals(QT_LOCATION_ISO6709)) {
166-
addLocation(value);
169+
String value = decodeValue(payload, data + 16, valueLength, valueType);
170+
if (value != null) {
171+
tikaMetadata.add(key, value);
172+
if (key.equals(QT_LOCATION_ISO6709)) {
173+
addLocation(value);
174+
}
167175
}
168176
}
169177
}
@@ -188,6 +196,34 @@ private void addLocation(String iso6709) {
188196
}
189197
}
190198

199+
/**
200+
* Decodes a metadata item value of one of the QTFF "well-known" types to a string,
201+
* or returns null for types that are not handled (e.g. images or binary plists).
202+
* Integers may be 1 to 8 bytes wide (e.g. the live-photo.auto flag is a single byte).
203+
*/
204+
@Nullable
205+
private static String decodeValue(byte[] b, int off, int len, int valueType) {
206+
switch (valueType) {
207+
case QT_TEXT_TYPE:
208+
return new String(b, off, len, StandardCharsets.UTF_8);
209+
case QT_INT_BE_TYPE:
210+
case QT_UINT_BE_TYPE:
211+
if (len < 1 || len > 8) {
212+
return null;
213+
}
214+
byte[] intBytes = Arrays.copyOfRange(b, off, off + len);
215+
return valueType == QT_INT_BE_TYPE
216+
? new BigInteger(intBytes).toString()
217+
: new BigInteger(1, intBytes).toString();
218+
case QT_FLOAT32_TYPE:
219+
return len == 4 ? String.valueOf(ByteBuffer.wrap(b, off, len).getFloat()) : null;
220+
case QT_FLOAT64_TYPE:
221+
return len == 8 ? String.valueOf(ByteBuffer.wrap(b, off, len).getDouble()) : null;
222+
default:
223+
return null;
224+
}
225+
}
226+
191227
private static long readUInt32(byte[] b, int off) {
192228
return ((b[off] & 0xFFL) << 24) | ((b[off + 1] & 0xFFL) << 16)
193229
| ((b[off + 2] & 0xFFL) << 8) | (b[off + 3] & 0xFFL);

tika-parsers/tika-parsers-standard/tika-parsers-standard-modules/tika-parser-audiovideo-module/src/test/java/org/apache/tika/parser/mp4/MP4ParserTest.java

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -299,6 +299,14 @@ public void testQuickTimeMetadataKeys() throws Exception {
299299
assertEquals(12.3456, Double.parseDouble(metadata.get(TikaCoreProperties.LATITUDE)), 0.00001);
300300
assertEquals(-98.7654, Double.parseDouble(metadata.get(TikaCoreProperties.LONGITUDE)), 0.00001);
301301
assertEquals(10.5, Double.parseDouble(metadata.get(TikaCoreProperties.ALTITUDE)), 0.00001);
302+
303+
//numeric well-known value types (uint8, float32, int32, float64)
304+
assertEquals("1", metadata.get("com.apple.quicktime.live-photo.auto"));
305+
assertEquals("0.75", metadata.get("com.apple.quicktime.live-photo.vitality-score"));
306+
assertEquals("-13",
307+
metadata.get("com.apple.quicktime.camera.focal_length.35mm_equivalent"));
308+
assertEquals("1.5",
309+
metadata.get("com.apple.quicktime.full-frame-rate-playback-intent"));
302310
}
303311

304312
@Test

0 commit comments

Comments
 (0)