Skip to content

Commit 278fec0

Browse files
authored
follow on tweaks to the recent TIKA-2861 fix (#2938)
1 parent 60a13c5 commit 278fec0

4 files changed

Lines changed: 209 additions & 23 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: 7 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -23,8 +23,6 @@
2323
import java.util.ArrayList;
2424
import java.util.Arrays;
2525
import java.util.List;
26-
import java.util.regex.Matcher;
27-
import java.util.regex.Pattern;
2826

2927
import com.drew.imaging.mp4.Mp4Handler;
3028
import com.drew.lang.annotations.NotNull;
@@ -35,6 +33,7 @@
3533
import org.xml.sax.SAXException;
3634

3735
import org.apache.tika.metadata.TikaCoreProperties;
36+
import org.apache.tika.parser.mp4.boxes.ISO6709;
3837
import org.apache.tika.parser.mp4.boxes.TikaUserDataBox;
3938
import org.apache.tika.sax.XHTMLContentHandler;
4039

@@ -49,8 +48,6 @@ public class TikaMp4BoxHandler extends Mp4BoxHandler {
4948

5049
//QuickTime stores location as an ISO 6709 string (e.g. +32.4720-084.9952+073.827/)
5150
private static final String QT_LOCATION_ISO6709 = "com.apple.quicktime.location.ISO6709";
52-
private static final Pattern ISO6709_PATTERN =
53-
Pattern.compile("([+-]\\d+(?:\\.\\d+)?)([+-]\\d+(?:\\.\\d+)?)([+-]\\d+(?:\\.\\d+)?)?");
5451

5552
org.apache.tika.metadata.Metadata tikaMetadata;
5653
final XHTMLContentHandler xhtml;
@@ -186,12 +183,12 @@ private void processQuickTimeItemList(@Nullable byte[] payload) {
186183
* the raw value, so QuickTime location matches the {@code geo:*} output of the udta path.
187184
*/
188185
private void addLocation(String iso6709) {
189-
Matcher matcher = ISO6709_PATTERN.matcher(iso6709);
190-
if (matcher.find()) {
191-
tikaMetadata.set(TikaCoreProperties.LATITUDE, Double.parseDouble(matcher.group(1)));
192-
tikaMetadata.set(TikaCoreProperties.LONGITUDE, Double.parseDouble(matcher.group(2)));
193-
if (matcher.group(3) != null) {
194-
tikaMetadata.set(TikaCoreProperties.ALTITUDE, Double.parseDouble(matcher.group(3)));
186+
ISO6709.Location location = ISO6709.parse(iso6709);
187+
if (location != null) {
188+
tikaMetadata.set(TikaCoreProperties.LATITUDE, location.latitude);
189+
tikaMetadata.set(TikaCoreProperties.LONGITUDE, location.longitude);
190+
if (location.altitude != null) {
191+
tikaMetadata.set(TikaCoreProperties.ALTITUDE, location.altitude);
195192
}
196193
}
197194
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,105 @@
1+
/*
2+
* Licensed to the Apache Software Foundation (ASF) under one or more
3+
* contributor license agreements. See the NOTICE file distributed with
4+
* this work for additional information regarding copyright ownership.
5+
* The ASF licenses this file to You under the Apache License, Version 2.0
6+
* (the "License"); you may not use this file except in compliance with
7+
* the License. You may obtain a copy of the License at
8+
*
9+
* http://www.apache.org/licenses/LICENSE-2.0
10+
*
11+
* Unless required by applicable law or agreed to in writing, software
12+
* distributed under the License is distributed on an "AS IS" BASIS,
13+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14+
* See the License for the specific language governing permissions and
15+
* limitations under the License.
16+
*/
17+
package org.apache.tika.parser.mp4.boxes;
18+
19+
import java.util.regex.Matcher;
20+
import java.util.regex.Pattern;
21+
22+
/**
23+
* Parses ISO 6709 location strings as written in QuickTime/MP4 metadata: the udta
24+
* "©xyz" box and the {@code com.apple.quicktime.location.ISO6709} value. Returns the
25+
* latitude, longitude and optional altitude.
26+
* <p>
27+
* Both the decimal-degree form (e.g. {@code +12.3456-098.7654+010.500/}) and the ISO 6709
28+
* sexagesimal compact forms ({@code ±DDMM.MMMM} / {@code ±DDMMSS.SSSS} for latitude and the
29+
* {@code ±DDD...} equivalents for longitude) are handled. The form is chosen by the
30+
* integer-digit count of each angle, which is unambiguous for valid coordinates: a decimal
31+
* latitude ({@code |lat| <= 90}) has at most 2 integer digits and a decimal longitude
32+
* ({@code |lon| <= 180}) at most 3, so neither can collide with the 4/6 (latitude) or 5/7
33+
* (longitude) integer-digit counts of the minute/second forms. Any other digit count falls
34+
* back to a lenient decimal parse.
35+
*/
36+
public final class ISO6709 {
37+
38+
//latitude, longitude and an optional altitude, each a signed number; any trailing
39+
//CRS designator and the "/" terminator are ignored
40+
private static final Pattern PATTERN = Pattern.compile(
41+
"([+-]\\d+(?:\\.\\d+)?)([+-]\\d+(?:\\.\\d+)?)([+-]\\d+(?:\\.\\d+)?)?");
42+
43+
private ISO6709() {
44+
}
45+
46+
public static final class Location {
47+
public final double latitude;
48+
public final double longitude;
49+
//null when the string carries no altitude component
50+
public final Double altitude;
51+
52+
Location(double latitude, double longitude, Double altitude) {
53+
this.latitude = latitude;
54+
this.longitude = longitude;
55+
this.altitude = altitude;
56+
}
57+
}
58+
59+
/**
60+
* @param s an ISO 6709 location string, or null
61+
* @return the parsed location, or null if {@code s} is null or contains no location
62+
*/
63+
public static Location parse(String s) {
64+
if (s == null) {
65+
return null;
66+
}
67+
Matcher matcher = PATTERN.matcher(s);
68+
if (!matcher.find()) {
69+
return null;
70+
}
71+
double latitude = decodeAngle(matcher.group(1), 2); //latitude: 2 degree digits
72+
double longitude = decodeAngle(matcher.group(2), 3); //longitude: 3 degree digits
73+
Double altitude = matcher.group(3) == null ? null : Double.parseDouble(matcher.group(3));
74+
return new Location(latitude, longitude, altitude);
75+
}
76+
77+
/**
78+
* Decodes one signed angle. {@code degreeDigits} is the number of integer digits the
79+
* degrees field occupies in the ISO 6709 compact forms: 2 for latitude, 3 for longitude.
80+
*/
81+
private static double decodeAngle(String token, int degreeDigits) {
82+
char sign = token.charAt(0);
83+
int dot = token.indexOf('.');
84+
String intPart = dot < 0 ? token.substring(1) : token.substring(1, dot);
85+
String frac = dot < 0 ? "" : token.substring(dot); //includes the '.'
86+
int digits = intPart.length();
87+
double value;
88+
if (digits == degreeDigits + 2) {
89+
//±DDMM.MMMM (or ±DDDMM.MMMM): degrees + minutes/60
90+
double degrees = Integer.parseInt(intPart.substring(0, degreeDigits));
91+
double minutes = Double.parseDouble(intPart.substring(degreeDigits) + frac);
92+
value = degrees + minutes / 60.0;
93+
} else if (digits == degreeDigits + 4) {
94+
//±DDMMSS.SSSS (or ±DDDMMSS.SSSS): degrees + minutes/60 + seconds/3600
95+
double degrees = Integer.parseInt(intPart.substring(0, degreeDigits));
96+
double minutes = Integer.parseInt(intPart.substring(degreeDigits, degreeDigits + 2));
97+
double seconds = Double.parseDouble(intPart.substring(degreeDigits + 2) + frac);
98+
value = degrees + minutes / 60.0 + seconds / 3600.0;
99+
} else {
100+
//decimal degrees (the common case) or a non-conformant count: lenient fallback
101+
value = Double.parseDouble(intPart + frac);
102+
}
103+
return sign == '-' ? -value : value;
104+
}
105+
}

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

Lines changed: 6 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -18,8 +18,6 @@
1818

1919
import java.io.IOException;
2020
import java.nio.charset.StandardCharsets;
21-
import java.util.regex.Matcher;
22-
import java.util.regex.Pattern;
2321

2422
import com.drew.lang.SequentialByteArrayReader;
2523
import com.drew.lang.SequentialReader;
@@ -43,8 +41,6 @@ public class TikaUserDataBox {
4341
private static final String MDTA = "mdta";
4442
private static final String HDLR = "hdlr";
4543
private static final String MDIR = "mdir";//apple metadata itunes reader
46-
private static final Pattern COORDINATE_PATTERN =
47-
Pattern.compile("([+-]\\d+\\.\\d+)([+-]\\d+\\.\\d+)([+-]\\d+(?:\\.\\d+)?)?");
4844

4945
@Nullable
5046
private String coordinateString;
@@ -264,16 +260,13 @@ private void addMetadata(String key, String value) throws SAXException {
264260

265261
public void addMetadata(Mp4Directory directory) {
266262
if (this.coordinateString != null) {
267-
Matcher matcher = COORDINATE_PATTERN.matcher(this.coordinateString);
268-
if (matcher.find()) {
269-
double latitude = Double.parseDouble(matcher.group(1));
270-
double longitude = Double.parseDouble(matcher.group(2));
271-
directory.setDouble(8193, latitude);
272-
directory.setDouble(8194, longitude);
263+
ISO6709.Location location = ISO6709.parse(this.coordinateString);
264+
if (location != null) {
265+
directory.setDouble(8193, location.latitude);
266+
directory.setDouble(8194, location.longitude);
273267
//Mp4Directory has no altitude tag, so set geo:alt directly
274-
if (matcher.group(3) != null) {
275-
metadata.set(TikaCoreProperties.ALTITUDE,
276-
Double.parseDouble(matcher.group(3)));
268+
if (location.altitude != null) {
269+
metadata.set(TikaCoreProperties.ALTITUDE, location.altitude);
277270
}
278271
}
279272
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,91 @@
1+
/*
2+
* Licensed to the Apache Software Foundation (ASF) under one or more
3+
* contributor license agreements. See the NOTICE file distributed with
4+
* this work for additional information regarding copyright ownership.
5+
* The ASF licenses this file to You under the Apache License, Version 2.0
6+
* (the "License"); you may not use this file except in compliance with
7+
* the License. You may obtain a copy of the License at
8+
*
9+
* http://www.apache.org/licenses/LICENSE-2.0
10+
*
11+
* Unless required by applicable law or agreed to in writing, software
12+
* distributed under the License is distributed on an "AS IS" BASIS,
13+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14+
* See the License for the specific language governing permissions and
15+
* limitations under the License.
16+
*/
17+
package org.apache.tika.parser.mp4.boxes;
18+
19+
import static org.junit.jupiter.api.Assertions.assertEquals;
20+
import static org.junit.jupiter.api.Assertions.assertNull;
21+
22+
import org.junit.jupiter.api.Test;
23+
24+
public class ISO6709Test {
25+
26+
private static final double DELTA = 0.00001;
27+
28+
@Test
29+
public void testNullAndEmpty() {
30+
assertNull(ISO6709.parse(null));
31+
assertNull(ISO6709.parse(""));
32+
assertNull(ISO6709.parse("not a location"));
33+
}
34+
35+
@Test
36+
public void testDecimalDegrees() {
37+
//the value carried by the TIKA-2861 test fixtures; must stay unchanged
38+
ISO6709.Location loc = ISO6709.parse("+12.3456-098.7654+010.500/");
39+
assertEquals(12.3456, loc.latitude, DELTA);
40+
assertEquals(-98.7654, loc.longitude, DELTA);
41+
assertEquals(10.5, loc.altitude, DELTA);
42+
}
43+
44+
@Test
45+
public void testDecimalNoAltitude() {
46+
ISO6709.Location loc = ISO6709.parse("+12.34-098.76/");
47+
assertEquals(12.34, loc.latitude, DELTA);
48+
assertEquals(-98.76, loc.longitude, DELTA);
49+
assertNull(loc.altitude);
50+
}
51+
52+
@Test
53+
public void testDecimalWithAltitudeAndCrsSuffix() {
54+
//Everest-like: large altitude followed by a CRS designator before the terminator
55+
ISO6709.Location loc = ISO6709.parse("+27.5916+086.5640+8850.000CRSWGS_84/");
56+
assertEquals(27.5916, loc.latitude, DELTA);
57+
assertEquals(86.5640, loc.longitude, DELTA);
58+
assertEquals(8850.0, loc.altitude, DELTA);
59+
}
60+
61+
@Test
62+
public void testIntegerDegreesNoDecimalPoint() {
63+
ISO6709.Location loc = ISO6709.parse("+12-098/");
64+
assertEquals(12.0, loc.latitude, DELTA);
65+
assertEquals(-98.0, loc.longitude, DELTA);
66+
}
67+
68+
@Test
69+
public void testNegativeSubDegreeWithLeadingZeros() {
70+
ISO6709.Location loc = ISO6709.parse("-00.5000-000.5000/");
71+
assertEquals(-0.5, loc.latitude, DELTA);
72+
assertEquals(-0.5, loc.longitude, DELTA);
73+
}
74+
75+
@Test
76+
public void testCompactDegreesMinutes() {
77+
//+DDMM.MMMM / -DDDMM.MMMM -> degrees + minutes/60
78+
ISO6709.Location loc = ISO6709.parse("+1234.5600-09830.0000/");
79+
assertEquals(12 + 34.56 / 60.0, loc.latitude, DELTA);
80+
assertEquals(-(98 + 30.0 / 60.0), loc.longitude, DELTA);
81+
assertNull(loc.altitude);
82+
}
83+
84+
@Test
85+
public void testCompactDegreesMinutesSeconds() {
86+
//+DDMMSS.S / -DDDMMSS.S -> degrees + minutes/60 + seconds/3600
87+
ISO6709.Location loc = ISO6709.parse("+123456.0-0983456.0/");
88+
assertEquals(12 + 34.0 / 60.0 + 56.0 / 3600.0, loc.latitude, DELTA);
89+
assertEquals(-(98 + 34.0 / 60.0 + 56.0 / 3600.0), loc.longitude, DELTA);
90+
}
91+
}

0 commit comments

Comments
 (0)