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
+ * "&#169;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);
+    }
+}

Reply via email to