You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@tika.apache.org by ta...@apache.org on 2026/07/09 20:56:58 UTC
(tika) branch TIKA-2861-follow-on created (now ca3c551616)
This is an automated email from the ASF dual-hosted git repository.
tballison pushed a change to branch TIKA-2861-follow-on
in repository https://gitbox.apache.org/repos/asf/tika.git
at ca3c551616 follow on tweaks to the recent TIKA-2861 fix
This branch includes the following new commits:
new ca3c551616 follow on tweaks to the recent TIKA-2861 fix
The 1 revisions listed above as "new" are entirely new to this
repository and will be described in separate emails. The revisions
listed as "add" were already present in the repository and have only
been added to this reference.
(tika) 01/01: follow on tweaks to the recent TIKA-2861 fix
Posted by ta...@apache.org.
This is an automated email from the ASF dual-hosted git repository.
tballison pushed a commit to branch TIKA-2861-follow-on
in repository https://gitbox.apache.org/repos/asf/tika.git
commit ca3c551616ae93d49401534eb083c21717e59492
Author: tallison <ta...@apache.org>
AuthorDate: Thu Jul 9 16:56:48 2026 -0400
follow on tweaks to the recent TIKA-2861 fix
---
.../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);
+ }
+}