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 <[email protected]> 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); + } +}
