This is an automated email from the ASF dual-hosted git repository.
tballison pushed a commit to branch main
in repository https://gitbox.apache.org/repos/asf/tika.git
The following commit(s) were added to refs/heads/main by this push:
new 278fec0fe5 follow on tweaks to the recent TIKA-2861 fix (#2938)
278fec0fe5 is described below
commit 278fec0fe52236f04e35de84b4c58105b3b07bc9
Author: Tim Allison <[email protected]>
AuthorDate: Fri Jul 10 06:38:13 2026 -0400
follow on tweaks to the recent TIKA-2861 fix (#2938)
---
.../apache/tika/parser/mp4/TikaMp4BoxHandler.java | 17 ++--
.../org/apache/tika/parser/mp4/boxes/ISO6709.java | 105 +++++++++++++++++++++
.../tika/parser/mp4/boxes/TikaUserDataBox.java | 19 ++--
.../apache/tika/parser/mp4/boxes/ISO6709Test.java | 91 ++++++++++++++++++
4 files changed, 209 insertions(+), 23 deletions(-)
diff --git
a/tika-parsers/tika-parsers-standard/tika-parsers-standard-modules/tika-parser-audiovideo-module/src/main/java/org/apache/tika/parser/mp4/TikaMp4BoxHandler.java
b/tika-parsers/tika-parsers-standard/tika-parsers-standard-modules/tika-parser-audiovideo-module/src/main/java/org/apache/tika/parser/mp4/TikaMp4BoxHandler.java
index 00f85b1037..9f65e48e32 100644
---
a/tika-parsers/tika-parsers-standard/tika-parsers-standard-modules/tika-parser-audiovideo-module/src/main/java/org/apache/tika/parser/mp4/TikaMp4BoxHandler.java
+++
b/tika-parsers/tika-parsers-standard/tika-parsers-standard-modules/tika-parser-audiovideo-module/src/main/java/org/apache/tika/parser/mp4/TikaMp4BoxHandler.java
@@ -23,8 +23,6 @@ import java.nio.charset.StandardCharsets;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
-import java.util.regex.Matcher;
-import java.util.regex.Pattern;
import com.drew.imaging.mp4.Mp4Handler;
import com.drew.lang.annotations.NotNull;
@@ -35,6 +33,7 @@ import com.drew.metadata.mp4.Mp4Context;
import org.xml.sax.SAXException;
import org.apache.tika.metadata.TikaCoreProperties;
+import org.apache.tika.parser.mp4.boxes.ISO6709;
import org.apache.tika.parser.mp4.boxes.TikaUserDataBox;
import org.apache.tika.sax.XHTMLContentHandler;
@@ -49,8 +48,6 @@ public class TikaMp4BoxHandler extends Mp4BoxHandler {
//QuickTime stores location as an ISO 6709 string (e.g.
+32.4720-084.9952+073.827/)
private static final String QT_LOCATION_ISO6709 =
"com.apple.quicktime.location.ISO6709";
- private static final Pattern ISO6709_PATTERN =
-
Pattern.compile("([+-]\\d+(?:\\.\\d+)?)([+-]\\d+(?:\\.\\d+)?)([+-]\\d+(?:\\.\\d+)?)?");
org.apache.tika.metadata.Metadata tikaMetadata;
final XHTMLContentHandler xhtml;
@@ -186,12 +183,12 @@ public class TikaMp4BoxHandler extends Mp4BoxHandler {
* the raw value, so QuickTime location matches the {@code geo:*} output
of the udta path.
*/
private void addLocation(String iso6709) {
- Matcher matcher = ISO6709_PATTERN.matcher(iso6709);
- if (matcher.find()) {
- tikaMetadata.set(TikaCoreProperties.LATITUDE,
Double.parseDouble(matcher.group(1)));
- tikaMetadata.set(TikaCoreProperties.LONGITUDE,
Double.parseDouble(matcher.group(2)));
- if (matcher.group(3) != null) {
- tikaMetadata.set(TikaCoreProperties.ALTITUDE,
Double.parseDouble(matcher.group(3)));
+ ISO6709.Location location = ISO6709.parse(iso6709);
+ if (location != null) {
+ tikaMetadata.set(TikaCoreProperties.LATITUDE, location.latitude);
+ tikaMetadata.set(TikaCoreProperties.LONGITUDE, location.longitude);
+ if (location.altitude != null) {
+ tikaMetadata.set(TikaCoreProperties.ALTITUDE,
location.altitude);
}
}
}
diff --git
a/tika-parsers/tika-parsers-standard/tika-parsers-standard-modules/tika-parser-audiovideo-module/src/main/java/org/apache/tika/parser/mp4/boxes/ISO6709.java
b/tika-parsers/tika-parsers-standard/tika-parsers-standard-modules/tika-parser-audiovideo-module/src/main/java/org/apache/tika/parser/mp4/boxes/ISO6709.java
new file mode 100644
index 0000000000..14bac89052
--- /dev/null
+++
b/tika-parsers/tika-parsers-standard/tika-parsers-standard-modules/tika-parser-audiovideo-module/src/main/java/org/apache/tika/parser/mp4/boxes/ISO6709.java
@@ -0,0 +1,105 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You 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.
+ */
+package org.apache.tika.parser.mp4.boxes;
+
+import java.util.regex.Matcher;
+import java.util.regex.Pattern;
+
+/**
+ * Parses ISO 6709 location strings as written in QuickTime/MP4 metadata: the
udta
+ * "©xyz" box and the {@code com.apple.quicktime.location.ISO6709} value.
Returns the
+ * latitude, longitude and optional altitude.
+ * <p>
+ * Both the decimal-degree form (e.g. {@code +12.3456-098.7654+010.500/}) and
the ISO 6709
+ * sexagesimal compact forms ({@code ±DDMM.MMMM} / {@code ±DDMMSS.SSSS} for
latitude and the
+ * {@code ±DDD...} equivalents for longitude) are handled. The form is chosen
by the
+ * integer-digit count of each angle, which is unambiguous for valid
coordinates: a decimal
+ * latitude ({@code |lat| <= 90}) has at most 2 integer digits and a decimal
longitude
+ * ({@code |lon| <= 180}) at most 3, so neither can collide with the 4/6
(latitude) or 5/7
+ * (longitude) integer-digit counts of the minute/second forms. Any other
digit count falls
+ * back to a lenient decimal parse.
+ */
+public final class ISO6709 {
+
+ //latitude, longitude and an optional altitude, each a signed number; any
trailing
+ //CRS designator and the "/" terminator are ignored
+ private static final Pattern PATTERN = Pattern.compile(
+
"([+-]\\d+(?:\\.\\d+)?)([+-]\\d+(?:\\.\\d+)?)([+-]\\d+(?:\\.\\d+)?)?");
+
+ private ISO6709() {
+ }
+
+ public static final class Location {
+ public final double latitude;
+ public final double longitude;
+ //null when the string carries no altitude component
+ public final Double altitude;
+
+ Location(double latitude, double longitude, Double altitude) {
+ this.latitude = latitude;
+ this.longitude = longitude;
+ this.altitude = altitude;
+ }
+ }
+
+ /**
+ * @param s an ISO 6709 location string, or null
+ * @return the parsed location, or null if {@code s} is null or contains
no location
+ */
+ public static Location parse(String s) {
+ if (s == null) {
+ return null;
+ }
+ Matcher matcher = PATTERN.matcher(s);
+ if (!matcher.find()) {
+ return null;
+ }
+ double latitude = decodeAngle(matcher.group(1), 2); //latitude: 2
degree digits
+ double longitude = decodeAngle(matcher.group(2), 3); //longitude: 3
degree digits
+ Double altitude = matcher.group(3) == null ? null :
Double.parseDouble(matcher.group(3));
+ return new Location(latitude, longitude, altitude);
+ }
+
+ /**
+ * Decodes one signed angle. {@code degreeDigits} is the number of integer
digits the
+ * degrees field occupies in the ISO 6709 compact forms: 2 for latitude, 3
for longitude.
+ */
+ private static double decodeAngle(String token, int degreeDigits) {
+ char sign = token.charAt(0);
+ int dot = token.indexOf('.');
+ String intPart = dot < 0 ? token.substring(1) : token.substring(1,
dot);
+ String frac = dot < 0 ? "" : token.substring(dot); //includes the '.'
+ int digits = intPart.length();
+ double value;
+ if (digits == degreeDigits + 2) {
+ //±DDMM.MMMM (or ±DDDMM.MMMM): degrees + minutes/60
+ double degrees = Integer.parseInt(intPart.substring(0,
degreeDigits));
+ double minutes =
Double.parseDouble(intPart.substring(degreeDigits) + frac);
+ value = degrees + minutes / 60.0;
+ } else if (digits == degreeDigits + 4) {
+ //±DDMMSS.SSSS (or ±DDDMMSS.SSSS): degrees + minutes/60 +
seconds/3600
+ double degrees = Integer.parseInt(intPart.substring(0,
degreeDigits));
+ double minutes = Integer.parseInt(intPart.substring(degreeDigits,
degreeDigits + 2));
+ double seconds = Double.parseDouble(intPart.substring(degreeDigits
+ 2) + frac);
+ value = degrees + minutes / 60.0 + seconds / 3600.0;
+ } else {
+ //decimal degrees (the common case) or a non-conformant count:
lenient fallback
+ value = Double.parseDouble(intPart + frac);
+ }
+ return sign == '-' ? -value : value;
+ }
+}
diff --git
a/tika-parsers/tika-parsers-standard/tika-parsers-standard-modules/tika-parser-audiovideo-module/src/main/java/org/apache/tika/parser/mp4/boxes/TikaUserDataBox.java
b/tika-parsers/tika-parsers-standard/tika-parsers-standard-modules/tika-parser-audiovideo-module/src/main/java/org/apache/tika/parser/mp4/boxes/TikaUserDataBox.java
index 4fd264aed3..fecac20f7c 100644
---
a/tika-parsers/tika-parsers-standard/tika-parsers-standard-modules/tika-parser-audiovideo-module/src/main/java/org/apache/tika/parser/mp4/boxes/TikaUserDataBox.java
+++
b/tika-parsers/tika-parsers-standard/tika-parsers-standard-modules/tika-parser-audiovideo-module/src/main/java/org/apache/tika/parser/mp4/boxes/TikaUserDataBox.java
@@ -18,8 +18,6 @@ package org.apache.tika.parser.mp4.boxes;
import java.io.IOException;
import java.nio.charset.StandardCharsets;
-import java.util.regex.Matcher;
-import java.util.regex.Pattern;
import com.drew.lang.SequentialByteArrayReader;
import com.drew.lang.SequentialReader;
@@ -43,8 +41,6 @@ public class TikaUserDataBox {
private static final String MDTA = "mdta";
private static final String HDLR = "hdlr";
private static final String MDIR = "mdir";//apple metadata itunes reader
- private static final Pattern COORDINATE_PATTERN =
-
Pattern.compile("([+-]\\d+\\.\\d+)([+-]\\d+\\.\\d+)([+-]\\d+(?:\\.\\d+)?)?");
@Nullable
private String coordinateString;
@@ -264,16 +260,13 @@ public class TikaUserDataBox {
public void addMetadata(Mp4Directory directory) {
if (this.coordinateString != null) {
- Matcher matcher =
COORDINATE_PATTERN.matcher(this.coordinateString);
- if (matcher.find()) {
- double latitude = Double.parseDouble(matcher.group(1));
- double longitude = Double.parseDouble(matcher.group(2));
- directory.setDouble(8193, latitude);
- directory.setDouble(8194, longitude);
+ ISO6709.Location location = ISO6709.parse(this.coordinateString);
+ if (location != null) {
+ directory.setDouble(8193, location.latitude);
+ directory.setDouble(8194, location.longitude);
//Mp4Directory has no altitude tag, so set geo:alt directly
- if (matcher.group(3) != null) {
- metadata.set(TikaCoreProperties.ALTITUDE,
- Double.parseDouble(matcher.group(3)));
+ if (location.altitude != null) {
+ metadata.set(TikaCoreProperties.ALTITUDE,
location.altitude);
}
}
}
diff --git
a/tika-parsers/tika-parsers-standard/tika-parsers-standard-modules/tika-parser-audiovideo-module/src/test/java/org/apache/tika/parser/mp4/boxes/ISO6709Test.java
b/tika-parsers/tika-parsers-standard/tika-parsers-standard-modules/tika-parser-audiovideo-module/src/test/java/org/apache/tika/parser/mp4/boxes/ISO6709Test.java
new file mode 100644
index 0000000000..03390fdbd4
--- /dev/null
+++
b/tika-parsers/tika-parsers-standard/tika-parsers-standard-modules/tika-parser-audiovideo-module/src/test/java/org/apache/tika/parser/mp4/boxes/ISO6709Test.java
@@ -0,0 +1,91 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You 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.
+ */
+package org.apache.tika.parser.mp4.boxes;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertNull;
+
+import org.junit.jupiter.api.Test;
+
+public class ISO6709Test {
+
+ private static final double DELTA = 0.00001;
+
+ @Test
+ public void testNullAndEmpty() {
+ assertNull(ISO6709.parse(null));
+ assertNull(ISO6709.parse(""));
+ assertNull(ISO6709.parse("not a location"));
+ }
+
+ @Test
+ public void testDecimalDegrees() {
+ //the value carried by the TIKA-2861 test fixtures; must stay unchanged
+ ISO6709.Location loc = ISO6709.parse("+12.3456-098.7654+010.500/");
+ assertEquals(12.3456, loc.latitude, DELTA);
+ assertEquals(-98.7654, loc.longitude, DELTA);
+ assertEquals(10.5, loc.altitude, DELTA);
+ }
+
+ @Test
+ public void testDecimalNoAltitude() {
+ ISO6709.Location loc = ISO6709.parse("+12.34-098.76/");
+ assertEquals(12.34, loc.latitude, DELTA);
+ assertEquals(-98.76, loc.longitude, DELTA);
+ assertNull(loc.altitude);
+ }
+
+ @Test
+ public void testDecimalWithAltitudeAndCrsSuffix() {
+ //Everest-like: large altitude followed by a CRS designator before the
terminator
+ ISO6709.Location loc =
ISO6709.parse("+27.5916+086.5640+8850.000CRSWGS_84/");
+ assertEquals(27.5916, loc.latitude, DELTA);
+ assertEquals(86.5640, loc.longitude, DELTA);
+ assertEquals(8850.0, loc.altitude, DELTA);
+ }
+
+ @Test
+ public void testIntegerDegreesNoDecimalPoint() {
+ ISO6709.Location loc = ISO6709.parse("+12-098/");
+ assertEquals(12.0, loc.latitude, DELTA);
+ assertEquals(-98.0, loc.longitude, DELTA);
+ }
+
+ @Test
+ public void testNegativeSubDegreeWithLeadingZeros() {
+ ISO6709.Location loc = ISO6709.parse("-00.5000-000.5000/");
+ assertEquals(-0.5, loc.latitude, DELTA);
+ assertEquals(-0.5, loc.longitude, DELTA);
+ }
+
+ @Test
+ public void testCompactDegreesMinutes() {
+ //+DDMM.MMMM / -DDDMM.MMMM -> degrees + minutes/60
+ ISO6709.Location loc = ISO6709.parse("+1234.5600-09830.0000/");
+ assertEquals(12 + 34.56 / 60.0, loc.latitude, DELTA);
+ assertEquals(-(98 + 30.0 / 60.0), loc.longitude, DELTA);
+ assertNull(loc.altitude);
+ }
+
+ @Test
+ public void testCompactDegreesMinutesSeconds() {
+ //+DDMMSS.S / -DDDMMSS.S -> degrees + minutes/60 + seconds/3600
+ ISO6709.Location loc = ISO6709.parse("+123456.0-0983456.0/");
+ assertEquals(12 + 34.0 / 60.0 + 56.0 / 3600.0, loc.latitude, DELTA);
+ assertEquals(-(98 + 34.0 / 60.0 + 56.0 / 3600.0), loc.longitude,
DELTA);
+ }
+}