From 1378d58cde4c49b746ec4ff6dac150a7cba45eb2 Mon Sep 17 00:00:00 2001 From: Dominik Schmidt Date: Sun, 23 Aug 2026 14:54:13 +0000 Subject: [PATCH 1/7] TIKA-4831: Add content-based detection of the GeoGebra zip formats Mark application/vnd.geogebra.file and .tool as sub-classes of application/zip so the filename hint survives magic detection, and add the (IANA-registered) application/vnd.geogebra.slides (*.ggs) and application/vnd.geogebra.pinboard (*.ggp) types. Add a GeoGebraDetector to the zip container detectors that recognizes the formats without a filename by their well-known entries: geogebra.xml (worksheet), structure.json plus _slideN/geogebra.xml (Notes/Slides) and geogebra_macro.xml (tool), in ZipFile and streaming mode. The detection tests parse nameless streams so they exercise the container detector rather than the globs. Keep ZipParser.ZIP_SPECIALIZATIONS in sync with the new registry entries. --- .../org/apache/tika/mime/tika-mimetypes.xml | 14 +++ .../org/apache/tika/TikaDetectionTest.java | 2 + .../org/apache/tika/parser/pkg/ZipParser.java | 2 + .../tika/detect/zip/GeoGebraDetector.java | 111 ++++++++++++++++++ ...pache.tika.detect.zip.ZipContainerDetector | 1 + .../detect/zip/GeoGebraDetectionTest.java | 66 +++++++++++ .../resources/test-documents/testGeoGebra.ggb | Bin 0 -> 2010 bytes .../test-documents/testGeoGebraSlides.ggs | Bin 0 -> 2466 bytes .../test-documents/testGeoGebraTool.ggt | Bin 0 -> 517 bytes 9 files changed, 196 insertions(+) create mode 100644 tika-parsers/tika-parsers-standard/tika-parsers-standard-modules/tika-parser-zip-commons/src/main/java/org/apache/tika/detect/zip/GeoGebraDetector.java create mode 100644 tika-parsers/tika-parsers-standard/tika-parsers-standard-modules/tika-parser-zip-commons/src/test/java/org/apache/tika/detect/zip/GeoGebraDetectionTest.java create mode 100644 tika-parsers/tika-parsers-standard/tika-parsers-standard-modules/tika-parser-zip-commons/src/test/resources/test-documents/testGeoGebra.ggb create mode 100644 tika-parsers/tika-parsers-standard/tika-parsers-standard-modules/tika-parser-zip-commons/src/test/resources/test-documents/testGeoGebraSlides.ggs create mode 100644 tika-parsers/tika-parsers-standard/tika-parsers-standard-modules/tika-parser-zip-commons/src/test/resources/test-documents/testGeoGebraTool.ggt diff --git a/tika-core/src/main/resources/org/apache/tika/mime/tika-mimetypes.xml b/tika-core/src/main/resources/org/apache/tika/mime/tika-mimetypes.xml index 608edcbd72b..7750d5ae902 100644 --- a/tika-core/src/main/resources/org/apache/tika/mime/tika-mimetypes.xml +++ b/tika-core/src/main/resources/org/apache/tika/mime/tika-mimetypes.xml @@ -1591,10 +1591,24 @@ + <_comment>GeoGebra Worksheet + + + + <_comment>GeoGebra Pinboard + + + + + <_comment>GeoGebra Notes/Slides + + + <_comment>GeoGebra Tool + diff --git a/tika-core/src/test/java/org/apache/tika/TikaDetectionTest.java b/tika-core/src/test/java/org/apache/tika/TikaDetectionTest.java index f52482c8d78..8dead957bf5 100644 --- a/tika-core/src/test/java/org/apache/tika/TikaDetectionTest.java +++ b/tika-core/src/test/java/org/apache/tika/TikaDetectionTest.java @@ -239,6 +239,8 @@ public void testHttpServerFileExtensions() { assertEquals("application/vnd.fuzzysheet", tika.detect("x.fzs")); assertEquals("application/vnd.genomatix.tuxedo", tika.detect("x.txd")); assertEquals("application/vnd.geogebra.file", tika.detect("x.ggb")); + assertEquals("application/vnd.geogebra.pinboard", tika.detect("x.ggp")); + assertEquals("application/vnd.geogebra.slides", tika.detect("x.ggs")); assertEquals("application/vnd.geogebra.tool", tika.detect("x.ggt")); assertEquals("application/vnd.geometry-explorer", tika.detect("x.gex")); assertEquals("application/vnd.geometry-explorer", tika.detect("x.gre")); diff --git a/tika-parsers/tika-parsers-standard/tika-parsers-standard-modules/tika-parser-pkg-module/src/main/java/org/apache/tika/parser/pkg/ZipParser.java b/tika-parsers/tika-parsers-standard/tika-parsers-standard-modules/tika-parser-pkg-module/src/main/java/org/apache/tika/parser/pkg/ZipParser.java index c99264fdc07..70f6a727794 100644 --- a/tika-parsers/tika-parsers-standard/tika-parsers-standard-modules/tika-parser-pkg-module/src/main/java/org/apache/tika/parser/pkg/ZipParser.java +++ b/tika-parsers/tika-parsers-standard/tika-parsers-standard-modules/tika-parser-pkg-module/src/main/java/org/apache/tika/parser/pkg/ZipParser.java @@ -127,6 +127,8 @@ private static Set loadZipSpecializations() { "application/vnd.apple.keynote", "application/vnd.apple.numbers", "application/vnd.apple.pages", "application/vnd.apple.unknown.13", "application/vnd.etsi.asic-e+zip", "application/vnd.etsi.asic-s+zip", + "application/vnd.geogebra.file", "application/vnd.geogebra.slides", + "application/vnd.geogebra.tool", "application/vnd.google-earth.kmz", "application/vnd.mindjet.mindmanager", "application/vnd.ms-excel.addin.macroenabled.12", "application/vnd.ms-excel.sheet.binary.macroenabled.12", diff --git a/tika-parsers/tika-parsers-standard/tika-parsers-standard-modules/tika-parser-zip-commons/src/main/java/org/apache/tika/detect/zip/GeoGebraDetector.java b/tika-parsers/tika-parsers-standard/tika-parsers-standard-modules/tika-parser-zip-commons/src/main/java/org/apache/tika/detect/zip/GeoGebraDetector.java new file mode 100644 index 00000000000..e9fe1e0ff90 --- /dev/null +++ b/tika-parsers/tika-parsers-standard/tika-parsers-standard-modules/tika-parser-zip-commons/src/main/java/org/apache/tika/detect/zip/GeoGebraDetector.java @@ -0,0 +1,111 @@ +/* + * 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.detect.zip; + +import java.io.IOException; +import java.io.InputStream; +import java.util.Enumeration; +import java.util.regex.Pattern; + +import org.apache.commons.compress.archivers.zip.ZipArchiveEntry; +import org.apache.commons.compress.archivers.zip.ZipFile; + +import org.apache.tika.io.TikaInputStream; +import org.apache.tika.mime.MediaType; + +/** + * Detects the zip-based GeoGebra formats by their well-known entry names: + *
    + *
  • {@code geogebra.xml} at the root → a worksheet (*.ggb)
  • + *
  • {@code structure.json} at the root plus at least one + * {@code _slide<N>/geogebra.xml} → GeoGebra Notes/Slides (*.ggs)
  • + *
  • {@code geogebra_macro.xml} at the root → a tool (*.ggt)
  • + *
+ * A worksheet with macros contains both {@code geogebra.xml} and + * {@code geogebra_macro.xml}, so the worksheet check takes precedence over + * the tool check, and the decision is only made once all entry names have + * been seen. + */ +public class GeoGebraDetector implements ZipContainerDetector { + + private static final MediaType GGB = MediaType.application("vnd.geogebra.file"); + private static final MediaType GGS = MediaType.application("vnd.geogebra.slides"); + private static final MediaType GGT = MediaType.application("vnd.geogebra.tool"); + + private static final Pattern SLIDE_XML_PATTERN = + Pattern.compile("^_slide\\d+/geogebra\\.xml$"); + + @Override + public MediaType detect(ZipFile zip, TikaInputStream tis) throws IOException { + Names names = new Names(); + Enumeration entries = zip.getEntries(); + while (entries.hasMoreElements()) { + names.update(entries.nextElement().getName()); + } + return names.decide(); + } + + @Override + public MediaType streamingDetectUpdate(ZipArchiveEntry zae, InputStream zis, + StreamingDetectContext detectContext) { + Names names = detectContext.get(Names.class); + if (names == null) { + names = new Names(); + detectContext.set(Names.class, names); + } + names.update(zae.getName()); + return null; + } + + @Override + public MediaType streamingDetectFinal(StreamingDetectContext detectContext) { + Names names = detectContext.get(Names.class); + return names == null ? null : names.decide(); + } + + private static class Names { + private boolean geogebraXml; + private boolean macroXml; + private boolean structureJson; + private boolean slideXml; + + void update(String name) { + if ("geogebra.xml".equals(name)) { + geogebraXml = true; + } else if ("geogebra_macro.xml".equals(name)) { + macroXml = true; + } else if ("structure.json".equals(name)) { + structureJson = true; + } else if (SLIDE_XML_PATTERN.matcher(name).matches()) { + slideXml = true; + } + } + + MediaType decide() { + if (structureJson && slideXml) { + return GGS; + } + if (geogebraXml) { + return GGB; + } + if (macroXml) { + return GGT; + } + return null; + } + } +} diff --git a/tika-parsers/tika-parsers-standard/tika-parsers-standard-modules/tika-parser-zip-commons/src/main/resources/META-INF/services/org.apache.tika.detect.zip.ZipContainerDetector b/tika-parsers/tika-parsers-standard/tika-parsers-standard-modules/tika-parser-zip-commons/src/main/resources/META-INF/services/org.apache.tika.detect.zip.ZipContainerDetector index eb2bfe27742..cc0c6706d95 100644 --- a/tika-parsers/tika-parsers-standard/tika-parsers-standard-modules/tika-parser-zip-commons/src/main/resources/META-INF/services/org.apache.tika.detect.zip.ZipContainerDetector +++ b/tika-parsers/tika-parsers-standard/tika-parsers-standard-modules/tika-parser-zip-commons/src/main/resources/META-INF/services/org.apache.tika.detect.zip.ZipContainerDetector @@ -14,6 +14,7 @@ # limitations under the License. #These are a special type of detector, and we're currently relying on this #manual file. +org.apache.tika.detect.zip.GeoGebraDetector org.apache.tika.detect.zip.IPADetector org.apache.tika.detect.zip.JarDetector org.apache.tika.detect.zip.KMZDetector diff --git a/tika-parsers/tika-parsers-standard/tika-parsers-standard-modules/tika-parser-zip-commons/src/test/java/org/apache/tika/detect/zip/GeoGebraDetectionTest.java b/tika-parsers/tika-parsers-standard/tika-parsers-standard-modules/tika-parser-zip-commons/src/test/java/org/apache/tika/detect/zip/GeoGebraDetectionTest.java new file mode 100644 index 00000000000..d7140aa815b --- /dev/null +++ b/tika-parsers/tika-parsers-standard/tika-parsers-standard-modules/tika-parser-zip-commons/src/test/java/org/apache/tika/detect/zip/GeoGebraDetectionTest.java @@ -0,0 +1,66 @@ +/* + * 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.detect.zip; + +import static org.junit.jupiter.api.Assertions.assertEquals; + +import java.util.List; + +import org.junit.jupiter.api.Test; + +import org.apache.tika.TikaTest; +import org.apache.tika.io.TikaInputStream; +import org.apache.tika.metadata.HttpHeaders; +import org.apache.tika.metadata.Metadata; +import org.apache.tika.parser.ParseContext; + +/** + * Test case for detecting the zip-based GeoGebra formats by their contents. + * The streams are parsed without a resource name, so detection must rely on + * the zip entry names, not the *.ggb/*.ggs/*.ggt globs. + */ +public class GeoGebraDetectionTest extends TikaTest { + + private List getRecursiveMetadataWithoutName(String fileName) throws Exception { + try (TikaInputStream tis = TikaInputStream.get( + getClass().getResourceAsStream("/test-documents/" + fileName), new Metadata())) { + return getRecursiveMetadata(tis, AUTO_DETECT_PARSER, new Metadata(), + new ParseContext(), true); + } + } + + @Test + public void testGGBDetection() throws Exception { + List metadataList = getRecursiveMetadataWithoutName("testGeoGebra.ggb"); + assertEquals("application/vnd.geogebra.file", + metadataList.get(0).get(HttpHeaders.CONTENT_TYPE)); + } + + @Test + public void testGGSDetection() throws Exception { + List metadataList = getRecursiveMetadataWithoutName("testGeoGebraSlides.ggs"); + assertEquals("application/vnd.geogebra.slides", + metadataList.get(0).get(HttpHeaders.CONTENT_TYPE)); + } + + @Test + public void testGGTDetection() throws Exception { + List metadataList = getRecursiveMetadataWithoutName("testGeoGebraTool.ggt"); + assertEquals("application/vnd.geogebra.tool", + metadataList.get(0).get(HttpHeaders.CONTENT_TYPE)); + } +} diff --git a/tika-parsers/tika-parsers-standard/tika-parsers-standard-modules/tika-parser-zip-commons/src/test/resources/test-documents/testGeoGebra.ggb b/tika-parsers/tika-parsers-standard/tika-parsers-standard-modules/tika-parser-zip-commons/src/test/resources/test-documents/testGeoGebra.ggb new file mode 100644 index 0000000000000000000000000000000000000000..087532316a94ac1f7a9a5d58b540cbc2a2079246 GIT binary patch literal 2010 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 literal 0 HcmV?d00001 diff --git a/tika-parsers/tika-parsers-standard/tika-parsers-standard-modules/tika-parser-zip-commons/src/test/resources/test-documents/testGeoGebraTool.ggt b/tika-parsers/tika-parsers-standard/tika-parsers-standard-modules/tika-parser-zip-commons/src/test/resources/test-documents/testGeoGebraTool.ggt new file mode 100644 index 0000000000000000000000000000000000000000..6ed4e8fef6a2a793d412b7d414e7a9466ea1f1f6 GIT binary patch literal 517 zcmWIWW@Zs#U|`^2aPk$8U22d$zn+nS;VTmZgAhZTqfwQ{P|BcKP;f@ll-z**kx} zTO3?`HH_C;K!JDJ64ea}2IAcfoHq<#uKnzv5z3`;wOeM&QFndA@Dme?Zxq@225(-H zUGv6LP}qK1-}l`bWop09_YQ_F;>cVGnQvxSFIk~w?F|fze|A&QGZGe@L_3p+@uDh9w zz4m5)xZM2PZ-uQJV@iO-wSrwo1KK8}^5l&OwX$XndS=DDPb*mHZCHHA)8y$l0vY~Z3jX_V(euF1 zA8!8Rzwl2uBiM2Fe|A~B##>xj|M?!(`z@UFdd;b{%HZ8s^8>sYnM9azM;I{9z@Pyb WiP)kqz?+o~B*O@V4nTT6SRDX`q1Y+_ literal 0 HcmV?d00001 From 34997b98bd36696a365997e68c3c7fd1d2795cb9 Mon Sep 17 00:00:00 2001 From: Dominik Schmidt Date: Sun, 23 Aug 2026 14:54:13 +0000 Subject: [PATCH 2/7] TIKA-4831: Add a GeoGebra parser for worksheets, Notes/Slides and tools Parse geogebra.xml and, when present, geogebra_macro.xml with the pooled, hardened SAX path: construction title/author/date and the app name/version/format/id become metadata, and the user-visible text (string-literal expressions, rich-text content runs, captions, macro names and help texts) is emitted as XHTML paragraphs. Notes/Slides files emit one div per slide in structure.json order and set xmpTPg:NPages. The representative rendering - geogebra_thumbnail.png at the root, or the first slide's thumbnail - is emitted as an embedded document marked embeddedResourceType=THUMBNAIL so unpack sidecars identify the preview image; other slides' thumbnails are redundant renderings and are skipped. Any other embedded file (e.g. inserted pictures) is emitted as an embedded document under its full zip entry name. --- CHANGES.txt | 8 + .../tika/metadata/metadata-key-fields.json | 6 + .../apache/tika/metadata/metadata-keys.json | 6 + .../tika-parser-miscoffice-module/pom.xml | 5 + .../tika/parser/geogebra/GeoGebraParser.java | 334 ++++++++++++++++++ .../parser/geogebra/GeoGebraXMLHandler.java | 155 ++++++++ .../parser/geogebra/GeoGebraParserTest.java | 115 ++++++ .../resources/test-documents/testGeoGebra.ggb | Bin 0 -> 2010 bytes .../test-documents/testGeoGebraSlides.ggs | Bin 0 -> 2466 bytes .../test-documents/testGeoGebraTool.ggt | Bin 0 -> 517 bytes 10 files changed, 629 insertions(+) create mode 100644 tika-parsers/tika-parsers-standard/tika-parsers-standard-modules/tika-parser-miscoffice-module/src/main/java/org/apache/tika/parser/geogebra/GeoGebraParser.java create mode 100644 tika-parsers/tika-parsers-standard/tika-parsers-standard-modules/tika-parser-miscoffice-module/src/main/java/org/apache/tika/parser/geogebra/GeoGebraXMLHandler.java create mode 100644 tika-parsers/tika-parsers-standard/tika-parsers-standard-modules/tika-parser-miscoffice-module/src/test/java/org/apache/tika/parser/geogebra/GeoGebraParserTest.java create mode 100644 tika-parsers/tika-parsers-standard/tika-parsers-standard-modules/tika-parser-miscoffice-module/src/test/resources/test-documents/testGeoGebra.ggb create mode 100644 tika-parsers/tika-parsers-standard/tika-parsers-standard-modules/tika-parser-miscoffice-module/src/test/resources/test-documents/testGeoGebraSlides.ggs create mode 100644 tika-parsers/tika-parsers-standard/tika-parsers-standard-modules/tika-parser-miscoffice-module/src/test/resources/test-documents/testGeoGebraTool.ggt diff --git a/CHANGES.txt b/CHANGES.txt index eea5ad329bc..909a4eaf80b 100644 --- a/CHANGES.txt +++ b/CHANGES.txt @@ -50,6 +50,14 @@ Release 4.1.0 - unreleased guards) and now honors shouldParseEmbedded for embedded file data (TIKA-4814). + * Content-based detection and a parser for the GeoGebra formats. *.ggb and + *.ggt are now sub-classes of application/zip and a zip container detector + identifies worksheets, Notes/Slides (*.ggs, new mime type, along with the + JSON-based *.ggp pinboard) and tools without a filename. GeoGebraParser + extracts the construction metadata and user-visible text and emits the + representative geogebra_thumbnail.png as a thumbnail embedded document + (TIKA-4831). + * RawTiffParser extracts the camera-generated JPEG previews embedded in TIFF-based raw images (Nikon NEF/NRW, Sony ARW/SRF/SR2, Pentax PEF/PTX, Adobe DNG and Canon CR2, including BigTIFF DNG containers) as thumbnail diff --git a/tika-metadata-schema/src/main/resources/org/apache/tika/metadata/metadata-key-fields.json b/tika-metadata-schema/src/main/resources/org/apache/tika/metadata/metadata-key-fields.json index 3392c0e3be2..4fdcd08c5b7 100644 --- a/tika-metadata-schema/src/main/resources/org/apache/tika/metadata/metadata-key-fields.json +++ b/tika-metadata-schema/src/main/resources/org/apache/tika/metadata/metadata-key-fields.json @@ -731,6 +731,12 @@ {"class":"org.apache.tika.parser.gdal.GDALParser","field":"SIZE","key":"gdal:size"}, {"class":"org.apache.tika.parser.gdal.GDALParser","field":"UPPER_LEFT","key":"gdal:upper-left"}, {"class":"org.apache.tika.parser.gdal.GDALParser","field":"UPPER_RIGHT","key":"gdal:upper-right"}, + {"class":"org.apache.tika.parser.geogebra.GeoGebraParser","field":"APP_NAME","key":"geogebra:appName"}, + {"class":"org.apache.tika.parser.geogebra.GeoGebraParser","field":"APP_VERSION","key":"geogebra:appVersion"}, + {"class":"org.apache.tika.parser.geogebra.GeoGebraParser","field":"DATE","key":"geogebra:date"}, + {"class":"org.apache.tika.parser.geogebra.GeoGebraParser","field":"FORMAT_VERSION","key":"geogebra:formatVersion"}, + {"class":"org.apache.tika.parser.geogebra.GeoGebraParser","field":"ID","key":"geogebra:id"}, + {"class":"org.apache.tika.parser.geogebra.GeoGebraParser","field":"TOOL_NAME","key":"geogebra:toolName"}, {"class":"org.apache.tika.parser.hdf.HDFParser","field":"FILE_TYPE_DESCRIPTION","key":"hdf:file-type-description"}, {"class":"org.apache.tika.parser.iwork.KeynoteContentHandler","field":"PRESENTATION_HEIGHT","key":"keynote:slides-height"}, {"class":"org.apache.tika.parser.iwork.KeynoteContentHandler","field":"PRESENTATION_WIDTH","key":"keynote:slides-width"}, diff --git a/tika-metadata-schema/src/main/resources/org/apache/tika/metadata/metadata-keys.json b/tika-metadata-schema/src/main/resources/org/apache/tika/metadata/metadata-keys.json index 817b0026559..2f3112b2c54 100644 --- a/tika-metadata-schema/src/main/resources/org/apache/tika/metadata/metadata-keys.json +++ b/tika-metadata-schema/src/main/resources/org/apache/tika/metadata/metadata-keys.json @@ -191,6 +191,12 @@ {"key":"geo:lat","namespace":"geo","valueType":"REAL","cardinality":"SIMPLE","module":"tika-core"}, {"key":"geo:long","namespace":"geo","valueType":"REAL","cardinality":"SIMPLE","module":"tika-core"}, {"key":"geo:timestamp","namespace":"geo","valueType":"DATE","cardinality":"SIMPLE","module":"tika-core"}, + {"key":"geogebra:appName","namespace":"geogebra","valueType":"TEXT","cardinality":"SIMPLE","module":"tika-parser-miscoffice-module"}, + {"key":"geogebra:appVersion","namespace":"geogebra","valueType":"TEXT","cardinality":"SIMPLE","module":"tika-parser-miscoffice-module"}, + {"key":"geogebra:date","namespace":"geogebra","valueType":"TEXT","cardinality":"SIMPLE","module":"tika-parser-miscoffice-module"}, + {"key":"geogebra:formatVersion","namespace":"geogebra","valueType":"TEXT","cardinality":"SIMPLE","module":"tika-parser-miscoffice-module"}, + {"key":"geogebra:id","namespace":"geogebra","valueType":"TEXT","cardinality":"SIMPLE","module":"tika-parser-miscoffice-module"}, + {"key":"geogebra:toolName","namespace":"geogebra","valueType":"TEXT","cardinality":"BAG","module":"tika-parser-miscoffice-module"}, {"key":"geotopic:latitude","namespace":"geotopic","valueType":"TEXT","cardinality":"SIMPLE","module":""}, {"key":"geotopic:longitude","namespace":"geotopic","valueType":"TEXT","cardinality":"SIMPLE","module":""}, {"key":"geotopic:name","namespace":"geotopic","valueType":"TEXT","cardinality":"SIMPLE","module":""}, diff --git a/tika-parsers/tika-parsers-standard/tika-parsers-standard-modules/tika-parser-miscoffice-module/pom.xml b/tika-parsers/tika-parsers-standard/tika-parsers-standard-modules/tika-parser-miscoffice-module/pom.xml index deb3f8fcbd3..d69b2a536e0 100644 --- a/tika-parsers/tika-parsers-standard/tika-parsers-standard-modules/tika-parser-miscoffice-module/pom.xml +++ b/tika-parsers/tika-parsers-standard/tika-parsers-standard-modules/tika-parser-miscoffice-module/pom.xml @@ -89,6 +89,11 @@ tika-parser-xmp-commons ${project.version} + + + com.fasterxml.jackson.core + jackson-databind + diff --git a/tika-parsers/tika-parsers-standard/tika-parsers-standard-modules/tika-parser-miscoffice-module/src/main/java/org/apache/tika/parser/geogebra/GeoGebraParser.java b/tika-parsers/tika-parsers-standard/tika-parsers-standard-modules/tika-parser-miscoffice-module/src/main/java/org/apache/tika/parser/geogebra/GeoGebraParser.java new file mode 100644 index 00000000000..e15c9f821d4 --- /dev/null +++ b/tika-parsers/tika-parsers-standard/tika-parsers-standard-modules/tika-parser-miscoffice-module/src/main/java/org/apache/tika/parser/geogebra/GeoGebraParser.java @@ -0,0 +1,334 @@ +/* + * 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.geogebra; + +import java.io.IOException; +import java.io.InputStream; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collections; +import java.util.Enumeration; +import java.util.HashSet; +import java.util.LinkedHashSet; +import java.util.List; +import java.util.Set; +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +import com.fasterxml.jackson.databind.JsonNode; +import com.fasterxml.jackson.databind.ObjectMapper; +import org.apache.commons.compress.archivers.zip.ZipArchiveEntry; +import org.apache.commons.compress.archivers.zip.ZipFile; +import org.xml.sax.ContentHandler; +import org.xml.sax.SAXException; + +import org.apache.tika.annotation.TikaComponent; +import org.apache.tika.exception.TikaException; +import org.apache.tika.extractor.EmbeddedDocumentExtractor; +import org.apache.tika.extractor.EmbeddedDocumentUtil; +import org.apache.tika.io.TikaInputStream; +import org.apache.tika.metadata.HttpHeaders; +import org.apache.tika.metadata.Metadata; +import org.apache.tika.metadata.PagedText; +import org.apache.tika.metadata.Property; +import org.apache.tika.metadata.TikaCoreProperties; +import org.apache.tika.mime.MediaType; +import org.apache.tika.parser.ParseContext; +import org.apache.tika.parser.Parser; +import org.apache.tika.sax.EmbeddedContentHandler; +import org.apache.tika.sax.XHTMLContentHandler; +import org.apache.tika.utils.XMLReaderUtils; +import org.apache.tika.zip.utils.ZipFileHelper; + +/** + * Parser for the zip-based GeoGebra formats: worksheets (*.ggb), Notes/Slides + * (*.ggs) and tools (*.ggt). + *

+ * The construction metadata (title, author, date) and the application + * name/version are read from {@code geogebra.xml} (or, for a tool, from + * {@code geogebra_macro.xml}), and the user-visible text (text objects, ink + * notes, captions) is emitted as XHTML paragraphs. For Notes/Slides, the slide + * order is taken from {@code structure.json} and each slide becomes a + * {@code

}. + *

+ * The representative rendering of the document — {@code geogebra_thumbnail.png} + * at the root of a worksheet or tool, or the first slide's thumbnail of a + * Notes/Slides file — is emitted as an embedded document marked with + * {@link TikaCoreProperties.EmbeddedResourceType#THUMBNAIL}, so that clients + * (e.g. the unpacker's sidecar metadata) can pick it as the preview image. + * Thumbnails of the remaining slides are renderings of content that is already + * extracted, so they are skipped. Any other embedded file (e.g. inserted + * pictures) is emitted as an embedded document. + */ +@TikaComponent +public class GeoGebraParser implements Parser { + + /** + * Serial version UID + */ + private static final long serialVersionUID = 2114923339149498692L; + + public static final String GEOGEBRA_PREFIX = "geogebra:"; + + /** + * The GeoGebra application flavor the file was written with, + * e.g. "classic", "notes", "graphing". + */ + public static final Property APP_NAME = + Property.internalText(GEOGEBRA_PREFIX + "appName"); + + /** + * The GeoGebra application version the file was written with. + */ + public static final Property APP_VERSION = + Property.internalText(GEOGEBRA_PREFIX + "appVersion"); + + /** + * The GeoGebra XML format version. + */ + public static final Property FORMAT_VERSION = + Property.internalText(GEOGEBRA_PREFIX + "formatVersion"); + + /** + * The unique id GeoGebra assigns to the document. + */ + public static final Property ID = Property.internalText(GEOGEBRA_PREFIX + "id"); + + /** + * The free-form date string of the construction. This is user-entered + * text, not necessarily a parseable date. + */ + public static final Property DATE = Property.internalText(GEOGEBRA_PREFIX + "date"); + + /** + * The tool names of the macros in a tool file (or in a worksheet with + * embedded macros). + */ + public static final Property TOOL_NAME = + Property.internalTextBag(GEOGEBRA_PREFIX + "toolName"); + + private static final Set SUPPORTED_TYPES = Collections.unmodifiableSet( + new HashSet<>(Arrays.asList(MediaType.application("vnd.geogebra.file"), + MediaType.application("vnd.geogebra.slides"), + MediaType.application("vnd.geogebra.tool")))); + + private static final String GEOGEBRA_XML = "geogebra.xml"; + private static final String MACRO_XML = "geogebra_macro.xml"; + private static final String STRUCTURE_JSON = "structure.json"; + private static final String THUMBNAIL_PNG = "geogebra_thumbnail.png"; + + /** + * Housekeeping entries every ggb-like container may carry; everything + * else is user content and worth emitting as an embedded document. + */ + private static final Set KNOWN_ENTRY_NAMES = Collections.unmodifiableSet( + new HashSet<>(Arrays.asList(GEOGEBRA_XML, MACRO_XML, THUMBNAIL_PNG, + "geogebra_defaults2d.xml", "geogebra_defaults3d.xml", + "geogebra_javascript.js"))); + + private static final Pattern SLIDE_XML_PATTERN = + Pattern.compile("^(_slide\\d+)/" + Pattern.quote(GEOGEBRA_XML) + "$"); + + private static final ObjectMapper OBJECT_MAPPER = new ObjectMapper(); + + @Override + public Set getSupportedTypes(ParseContext context) { + return SUPPORTED_TYPES; + } + + @Override + public void parse(TikaInputStream tis, ContentHandler handler, Metadata metadata, + ParseContext context) throws IOException, SAXException, TikaException { + EmbeddedDocumentExtractor embeddedDocumentExtractor = + EmbeddedDocumentUtil.getEmbeddedDocumentExtractor(context); + + ZipFile zipFile; + Object container = tis.getOpenContainer(); + if (container instanceof ZipFile) { + zipFile = (ZipFile) container; + } else { + zipFile = ZipFileHelper.open(tis, null); + tis.setOpenContainer(zipFile); + } + + XHTMLContentHandler xhtml = new XHTMLContentHandler(handler, metadata, context); + xhtml.startDocument(); + List slideIds = getSlideIds(zipFile); + if (!slideIds.isEmpty()) { + parseSlides(zipFile, slideIds, xhtml, metadata, context, embeddedDocumentExtractor); + } else { + parseWorksheet(zipFile, xhtml, metadata, context, embeddedDocumentExtractor); + } + xhtml.endDocument(); + } + + /** + * Returns the ordered slide directory names of a Notes/Slides file, or an + * empty list if this is not a Notes/Slides file. The order comes from + * {@code structure.json}; slides present in the zip but missing from + * {@code structure.json} are appended in numeric order. + */ + private List getSlideIds(ZipFile zipFile) { + Set inZip = new LinkedHashSet<>(); + List numericallySorted = new ArrayList<>(); + Enumeration entries = zipFile.getEntries(); + while (entries.hasMoreElements()) { + Matcher m = SLIDE_XML_PATTERN.matcher(entries.nextElement().getName()); + if (m.matches()) { + numericallySorted.add(m.group(1)); + } + } + numericallySorted.sort((a, b) -> Integer.compare( + Integer.parseInt(a.substring("_slide".length())), + Integer.parseInt(b.substring("_slide".length())))); + + ZipArchiveEntry structure = zipFile.getEntry(STRUCTURE_JSON); + if (structure == null || numericallySorted.isEmpty()) { + return Collections.emptyList(); + } + Set knownSlideIds = new HashSet<>(numericallySorted); + try (InputStream is = zipFile.getInputStream(structure)) { + JsonNode root = OBJECT_MAPPER.readTree(is); + for (JsonNode chapter : root.path("chapters")) { + for (JsonNode page : chapter.path("pages")) { + for (JsonNode element : page.path("elements")) { + String id = element.path("id").asText(""); + if (knownSlideIds.contains(id)) { + inZip.add(id); + } + } + } + } + } catch (IOException e) { + //fall through to the numeric order + } + for (String id : numericallySorted) { + inZip.add(id); + } + return new ArrayList<>(inZip); + } + + private void parseWorksheet(ZipFile zipFile, XHTMLContentHandler xhtml, Metadata metadata, + ParseContext context, EmbeddedDocumentExtractor embeddedDocumentExtractor) + throws IOException, SAXException, TikaException { + //a worksheet with macros carries both geogebra.xml and geogebra_macro.xml + for (String xmlName : new String[]{GEOGEBRA_XML, MACRO_XML}) { + ZipArchiveEntry contentXml = zipFile.getEntry(xmlName); + if (contentXml != null) { + parseGeoGebraXml(zipFile, contentXml, xhtml, metadata, context); + } + } + handleThumbnail(zipFile, zipFile.getEntry(THUMBNAIL_PNG), xhtml, context, + embeddedDocumentExtractor); + handleOtherEntries(zipFile, xhtml, context, embeddedDocumentExtractor); + } + + private void parseSlides(ZipFile zipFile, List slideIds, XHTMLContentHandler xhtml, + Metadata metadata, ParseContext context, + EmbeddedDocumentExtractor embeddedDocumentExtractor) + throws IOException, SAXException, TikaException { + metadata.set(PagedText.N_PAGES, slideIds.size()); + boolean first = true; + for (String slideId : slideIds) { + xhtml.startElement("div", "class", "slide"); + ZipArchiveEntry contentXml = zipFile.getEntry(slideId + "/" + GEOGEBRA_XML); + if (contentXml != null) { + //document-level metadata comes from the first slide + parseGeoGebraXml(zipFile, contentXml, xhtml, first ? metadata : null, context); + } + xhtml.endElement("div"); + if (first) { + handleThumbnail(zipFile, zipFile.getEntry(slideId + "/" + THUMBNAIL_PNG), xhtml, + context, embeddedDocumentExtractor); + } + first = false; + } + handleOtherEntries(zipFile, xhtml, context, embeddedDocumentExtractor); + } + + private void parseGeoGebraXml(ZipFile zipFile, ZipArchiveEntry entry, + XHTMLContentHandler xhtml, Metadata metadata, + ParseContext context) + throws IOException, SAXException, TikaException { + try (InputStream is = zipFile.getInputStream(entry)) { + XMLReaderUtils.parseSAX(is, + new EmbeddedContentHandler(new GeoGebraXMLHandler(xhtml, metadata)), context); + } + } + + /** + * Emits the representative thumbnail as an embedded document marked + * {@link TikaCoreProperties.EmbeddedResourceType#THUMBNAIL}. + */ + private void handleThumbnail(ZipFile zipFile, ZipArchiveEntry entry, XHTMLContentHandler xhtml, + ParseContext context, EmbeddedDocumentExtractor embeddedDocumentExtractor) + throws IOException, SAXException { + if (entry == null) { + return; + } + Metadata embeddedMetadata = Metadata.newInstance(context); + embeddedMetadata.set(TikaCoreProperties.RESOURCE_NAME_KEY, entry.getName()); + embeddedMetadata.set(TikaCoreProperties.INTERNAL_PATH, entry.getName()); + embeddedMetadata.set(TikaCoreProperties.EMBEDDED_RESOURCE_TYPE, + TikaCoreProperties.EmbeddedResourceType.THUMBNAIL.toString()); + embeddedMetadata.set(HttpHeaders.CONTENT_TYPE, "image/png"); + if (embeddedDocumentExtractor.shouldParseEmbedded(embeddedMetadata, context)) { + try (TikaInputStream tisZip = TikaInputStream.get(zipFile.getInputStream(entry))) { + embeddedDocumentExtractor.parseEmbedded(tisZip, new EmbeddedContentHandler(xhtml), + embeddedMetadata, context, false); + } + } + } + + /** + * Emits everything that is not GeoGebra housekeeping (e.g. inserted + * pictures) as an embedded document. Housekeeping entries are matched by + * their basename so the rule covers slide subdirectories, too. + */ + private void handleOtherEntries(ZipFile zipFile, XHTMLContentHandler xhtml, + ParseContext context, + EmbeddedDocumentExtractor embeddedDocumentExtractor) + throws IOException, SAXException { + Enumeration entries = zipFile.getEntries(); + while (entries.hasMoreElements()) { + ZipArchiveEntry entry = entries.nextElement(); + if (entry.isDirectory()) { + continue; + } + String name = entry.getName(); + if (STRUCTURE_JSON.equals(name)) { + continue; + } + String basename = name.substring(name.lastIndexOf('/') + 1); + if (KNOWN_ENTRY_NAMES.contains(basename)) { + continue; + } + Metadata embeddedMetadata = Metadata.newInstance(context); + embeddedMetadata.set(TikaCoreProperties.RESOURCE_NAME_KEY, name); + embeddedMetadata.set(TikaCoreProperties.INTERNAL_PATH, name); + embeddedMetadata.set(TikaCoreProperties.EMBEDDED_RESOURCE_TYPE, + TikaCoreProperties.EmbeddedResourceType.INLINE.toString()); + if (embeddedDocumentExtractor.shouldParseEmbedded(embeddedMetadata, context)) { + try (TikaInputStream tisZip = + TikaInputStream.get(zipFile.getInputStream(entry))) { + embeddedDocumentExtractor.parseEmbedded(tisZip, new EmbeddedContentHandler(xhtml), + embeddedMetadata, context, false); + } + } + } + } +} diff --git a/tika-parsers/tika-parsers-standard/tika-parsers-standard-modules/tika-parser-miscoffice-module/src/main/java/org/apache/tika/parser/geogebra/GeoGebraXMLHandler.java b/tika-parsers/tika-parsers-standard/tika-parsers-standard-modules/tika-parser-miscoffice-module/src/main/java/org/apache/tika/parser/geogebra/GeoGebraXMLHandler.java new file mode 100644 index 00000000000..c2574b29c41 --- /dev/null +++ b/tika-parsers/tika-parsers-standard/tika-parsers-standard-modules/tika-parser-miscoffice-module/src/main/java/org/apache/tika/parser/geogebra/GeoGebraXMLHandler.java @@ -0,0 +1,155 @@ +/* + * 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.geogebra; + +import java.io.IOException; + +import com.fasterxml.jackson.databind.JsonNode; +import com.fasterxml.jackson.databind.ObjectMapper; +import org.xml.sax.Attributes; +import org.xml.sax.SAXException; +import org.xml.sax.helpers.DefaultHandler; + +import org.apache.tika.metadata.Metadata; +import org.apache.tika.metadata.Property; +import org.apache.tika.metadata.TikaCoreProperties; +import org.apache.tika.sax.XHTMLContentHandler; + +/** + * SAX handler for {@code geogebra.xml} and {@code geogebra_macro.xml}. + *

+ * Extracts the document metadata from the {@code <geogebra>} root and + * the {@code <construction>} element (when a {@link Metadata} object is + * given), and emits the user-visible text as XHTML paragraphs: string-literal + * {@code <expression>}s of text objects, the rich text of + * {@code <content>} elements (ink notes, inline text), element + * {@code <caption>}s and macro names/help texts. + */ +class GeoGebraXMLHandler extends DefaultHandler { + + private static final ObjectMapper OBJECT_MAPPER = new ObjectMapper(); + + private final XHTMLContentHandler xhtml; + private final Metadata metadata; + private int depth = 0; + + /** + * @param xhtml the handler paragraphs are written to + * @param metadata the metadata to fill from the root and construction + * elements, or {@code null} to extract text only + */ + GeoGebraXMLHandler(XHTMLContentHandler xhtml, Metadata metadata) { + this.xhtml = xhtml; + this.metadata = metadata; + } + + @Override + public void startElement(String uri, String localName, String qName, Attributes attributes) + throws SAXException { + if (depth == 0 && "geogebra".equals(localName)) { + if (metadata != null) { + setIfNotBlank(GeoGebraParser.APP_NAME, attributes.getValue("app")); + setIfNotBlank(GeoGebraParser.APP_VERSION, attributes.getValue("version")); + setIfNotBlank(GeoGebraParser.FORMAT_VERSION, attributes.getValue("format")); + setIfNotBlank(GeoGebraParser.ID, attributes.getValue("id")); + } + } else if ("construction".equals(localName)) { + if (metadata != null) { + setIfNotBlank(TikaCoreProperties.TITLE, attributes.getValue("title")); + setIfNotBlank(TikaCoreProperties.CREATOR, attributes.getValue("author")); + setIfNotBlank(GeoGebraParser.DATE, attributes.getValue("date")); + } + } else if ("expression".equals(localName)) { + handleExpression(attributes.getValue("exp")); + } else if ("content".equals(localName)) { + handleContent(attributes.getValue("val")); + } else if ("caption".equals(localName)) { + paragraph(attributes.getValue("val")); + } else if ("macro".equals(localName)) { + String toolName = attributes.getValue("toolName"); + if (isBlank(toolName)) { + toolName = attributes.getValue("cmdName"); + } + if (metadata != null && !isBlank(toolName)) { + metadata.add(GeoGebraParser.TOOL_NAME, toolName); + } + paragraph(toolName); + paragraph(attributes.getValue("toolHelp")); + } + depth++; + } + + @Override + public void endElement(String uri, String localName, String qName) { + depth--; + } + + /** + * Emits a text object's expression, which is a GeoGebra string literal + * like {@code "some text"}. Computational expressions (anything not + * quoted) are geometry, not text, and are skipped. + */ + private void handleExpression(String exp) throws SAXException { + if (exp == null || exp.length() < 2 || exp.charAt(0) != '"' || + exp.charAt(exp.length() - 1) != '"') { + return; + } + paragraph(exp.substring(1, exp.length() - 1).replace("\\\"", "\"")); + } + + /** + * Emits the text runs of a rich-text {@code content} value, a JSON array + * of text runs like {@code [{"text":"Hello\n"}]}. All {@code text} fields + * are collected recursively (tables and mind maps nest them), joined, and + * emitted one paragraph per line. + */ + private void handleContent(String val) throws SAXException { + if (val == null || val.isEmpty()) { + return; + } + JsonNode root; + try { + root = OBJECT_MAPPER.readTree(val); + } catch (IOException e) { + //not JSON; ignore + return; + } + StringBuilder sb = new StringBuilder(); + for (String text : root.findValuesAsText("text")) { + sb.append(text); + } + for (String line : sb.toString().split("\n")) { + paragraph(line); + } + } + + private void paragraph(String text) throws SAXException { + if (!isBlank(text)) { + xhtml.element("p", text.trim()); + } + } + + private void setIfNotBlank(Property property, String value) { + if (!isBlank(value)) { + metadata.set(property, value.trim()); + } + } + + private static boolean isBlank(String s) { + return s == null || s.trim().isEmpty(); + } +} diff --git a/tika-parsers/tika-parsers-standard/tika-parsers-standard-modules/tika-parser-miscoffice-module/src/test/java/org/apache/tika/parser/geogebra/GeoGebraParserTest.java b/tika-parsers/tika-parsers-standard/tika-parsers-standard-modules/tika-parser-miscoffice-module/src/test/java/org/apache/tika/parser/geogebra/GeoGebraParserTest.java new file mode 100644 index 00000000000..feb806ebe1d --- /dev/null +++ b/tika-parsers/tika-parsers-standard/tika-parsers-standard-modules/tika-parser-miscoffice-module/src/test/java/org/apache/tika/parser/geogebra/GeoGebraParserTest.java @@ -0,0 +1,115 @@ +/* + * 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.geogebra; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNull; +import static org.junit.jupiter.api.Assertions.assertTrue; + +import java.util.List; + +import org.junit.jupiter.api.Test; + +import org.apache.tika.TikaTest; +import org.apache.tika.metadata.HttpHeaders; +import org.apache.tika.metadata.Metadata; +import org.apache.tika.metadata.PagedText; +import org.apache.tika.metadata.TikaCoreProperties; + +public class GeoGebraParserTest extends TikaTest { + + @Test + public void testGGB() throws Exception { + List metadataList = getRecursiveMetadata("testGeoGebra.ggb"); + Metadata metadata = metadataList.get(0); + assertEquals("application/vnd.geogebra.file", metadata.get(HttpHeaders.CONTENT_TYPE)); + assertEquals("Pythagorean theorem", metadata.get(TikaCoreProperties.TITLE)); + assertEquals("Ada Lovelace", metadata.get(TikaCoreProperties.CREATOR)); + assertEquals("15 January 2026", metadata.get(GeoGebraParser.DATE)); + assertEquals("classic", metadata.get(GeoGebraParser.APP_NAME)); + assertEquals("5.0.815.0", metadata.get(GeoGebraParser.APP_VERSION)); + assertEquals("5.0", metadata.get(GeoGebraParser.FORMAT_VERSION)); + assertEquals("0c34397e-e3e1-4d1c-9cb6-fe6e54b1e88f", metadata.get(GeoGebraParser.ID)); + + String content = metadata.get(TikaCoreProperties.TIKA_CONTENT); + assertContains("In a right triangle a² + b² = c²", content); + assertContains("Theorem statement", content); + assertContains("Drag the vertices to explore.", content); + + //the embedded macro is parsed alongside geogebra.xml + assertEquals("Midpoint tool", metadata.get(GeoGebraParser.TOOL_NAME)); + assertContains("Select two points to construct their midpoint", content); + + //the thumbnail is emitted as an embedded document marked THUMBNAIL + assertEquals(2, metadataList.size()); + Metadata thumbnail = metadataList.get(1); + assertEquals("image/png", thumbnail.get(HttpHeaders.CONTENT_TYPE)); + assertEquals("geogebra_thumbnail.png", thumbnail.get(TikaCoreProperties.RESOURCE_NAME_KEY)); + assertEquals(TikaCoreProperties.EmbeddedResourceType.THUMBNAIL.toString(), + thumbnail.get(TikaCoreProperties.EMBEDDED_RESOURCE_TYPE)); + } + + @Test + public void testGGS() throws Exception { + List metadataList = getRecursiveMetadata("testGeoGebraSlides.ggs"); + Metadata metadata = metadataList.get(0); + assertEquals("application/vnd.geogebra.slides", metadata.get(HttpHeaders.CONTENT_TYPE)); + assertEquals("notes", metadata.get(GeoGebraParser.APP_NAME)); + assertEquals(2, (int) metadata.getInt(PagedText.N_PAGES)); + + //structure.json orders _slide1 before _slide0 + String content = metadata.get(TikaCoreProperties.TIKA_CONTENT); + assertContains("First slide text", content); + assertContains("Second slide text", content); + assertTrue(content.indexOf("First slide text") < content.indexOf("Second slide text"), + "slide order should follow structure.json"); + + //only the first slide's thumbnail is emitted, marked THUMBNAIL + assertEquals(3, metadataList.size()); + Metadata thumbnail = metadataList.get(1); + assertEquals("image/png", thumbnail.get(HttpHeaders.CONTENT_TYPE)); + assertEquals("_slide1/geogebra_thumbnail.png", + thumbnail.get(TikaCoreProperties.INTERNAL_PATH)); + assertEquals(TikaCoreProperties.EmbeddedResourceType.THUMBNAIL.toString(), + thumbnail.get(TikaCoreProperties.EMBEDDED_RESOURCE_TYPE)); + + //the inserted picture is emitted under its full zip entry name + Metadata picture = metadataList.get(2); + assertEquals("_slide0/8c6976e5b541/photo.png", + picture.get(TikaCoreProperties.RESOURCE_NAME_KEY)); + assertEquals("_slide0/8c6976e5b541/photo.png", + picture.get(TikaCoreProperties.INTERNAL_PATH)); + assertEquals(TikaCoreProperties.EmbeddedResourceType.INLINE.toString(), + picture.get(TikaCoreProperties.EMBEDDED_RESOURCE_TYPE)); + } + + @Test + public void testGGT() throws Exception { + List metadataList = getRecursiveMetadata("testGeoGebraTool.ggt"); + Metadata metadata = metadataList.get(0); + assertEquals("application/vnd.geogebra.tool", metadata.get(HttpHeaders.CONTENT_TYPE)); + assertEquals("Midpoint tool", metadata.get(GeoGebraParser.TOOL_NAME)); + + String content = metadata.get(TikaCoreProperties.TIKA_CONTENT); + assertContains("Midpoint tool", content); + assertContains("Select two points to construct their midpoint", content); + + //no thumbnail in this tool file + assertEquals(1, metadataList.size()); + assertNull(metadata.get(TikaCoreProperties.TITLE)); + } +} diff --git a/tika-parsers/tika-parsers-standard/tika-parsers-standard-modules/tika-parser-miscoffice-module/src/test/resources/test-documents/testGeoGebra.ggb b/tika-parsers/tika-parsers-standard/tika-parsers-standard-modules/tika-parser-miscoffice-module/src/test/resources/test-documents/testGeoGebra.ggb new file mode 100644 index 0000000000000000000000000000000000000000..087532316a94ac1f7a9a5d58b540cbc2a2079246 GIT binary patch literal 2010 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 literal 0 HcmV?d00001 diff --git a/tika-parsers/tika-parsers-standard/tika-parsers-standard-modules/tika-parser-miscoffice-module/src/test/resources/test-documents/testGeoGebraTool.ggt b/tika-parsers/tika-parsers-standard/tika-parsers-standard-modules/tika-parser-miscoffice-module/src/test/resources/test-documents/testGeoGebraTool.ggt new file mode 100644 index 0000000000000000000000000000000000000000..6ed4e8fef6a2a793d412b7d414e7a9466ea1f1f6 GIT binary patch literal 517 zcmWIWW@Zs#U|`^2aPk$8U22d$zn+nS;VTmZgAhZTqfwQ{P|BcKP;f@ll-z**kx} zTO3?`HH_C;K!JDJ64ea}2IAcfoHq<#uKnzv5z3`;wOeM&QFndA@Dme?Zxq@225(-H zUGv6LP}qK1-}l`bWop09_YQ_F;>cVGnQvxSFIk~w?F|fze|A&QGZGe@L_3p+@uDh9w zz4m5)xZM2PZ-uQJV@iO-wSrwo1KK8}^5l&OwX$XndS=DDPb*mHZCHHA)8y$l0vY~Z3jX_V(euF1 zA8!8Rzwl2uBiM2Fe|A~B##>xj|M?!(`z@UFdd;b{%HZ8s^8>sYnM9azM;I{9z@Pyb WiP)kqz?+o~B*O@V4nTT6SRDX`q1Y+_ literal 0 HcmV?d00001 From 9d22e1a18605de7fa8ddc68d8275ee68fc94c703 Mon Sep 17 00:00:00 2001 From: Dominik Schmidt Date: Mon, 24 Aug 2026 20:56:46 +0200 Subject: [PATCH 3/7] TIKA-4831 - sort slide ids without parsing them as int A crafted _slide id with more digits than an int holds threw an uncaught NumberFormatException out of the sort comparator; compare the digit strings by length then lexicographically instead. The regression test builds a two-slide container with an oversized id. --- .../tika/parser/geogebra/GeoGebraParser.java | 12 +++++-- .../parser/geogebra/GeoGebraParserTest.java | 35 +++++++++++++++++++ 2 files changed, 44 insertions(+), 3 deletions(-) diff --git a/tika-parsers/tika-parsers-standard/tika-parsers-standard-modules/tika-parser-miscoffice-module/src/main/java/org/apache/tika/parser/geogebra/GeoGebraParser.java b/tika-parsers/tika-parsers-standard/tika-parsers-standard-modules/tika-parser-miscoffice-module/src/main/java/org/apache/tika/parser/geogebra/GeoGebraParser.java index e15c9f821d4..4537088878c 100644 --- a/tika-parsers/tika-parsers-standard/tika-parsers-standard-modules/tika-parser-miscoffice-module/src/main/java/org/apache/tika/parser/geogebra/GeoGebraParser.java +++ b/tika-parsers/tika-parsers-standard/tika-parsers-standard-modules/tika-parser-miscoffice-module/src/main/java/org/apache/tika/parser/geogebra/GeoGebraParser.java @@ -192,9 +192,15 @@ private List getSlideIds(ZipFile zipFile) { numericallySorted.add(m.group(1)); } } - numericallySorted.sort((a, b) -> Integer.compare( - Integer.parseInt(a.substring("_slide".length())), - Integer.parseInt(b.substring("_slide".length())))); + //compare the digit suffixes numerically without parsing them: a shorter + //digit string is the smaller number, equal lengths compare lexicographically. + //Parsing would throw for a crafted id with more digits than an int holds. + numericallySorted.sort((a, b) -> { + String da = a.substring("_slide".length()); + String db = b.substring("_slide".length()); + return da.length() != db.length() + ? Integer.compare(da.length(), db.length()) : da.compareTo(db); + }); ZipArchiveEntry structure = zipFile.getEntry(STRUCTURE_JSON); if (structure == null || numericallySorted.isEmpty()) { diff --git a/tika-parsers/tika-parsers-standard/tika-parsers-standard-modules/tika-parser-miscoffice-module/src/test/java/org/apache/tika/parser/geogebra/GeoGebraParserTest.java b/tika-parsers/tika-parsers-standard/tika-parsers-standard-modules/tika-parser-miscoffice-module/src/test/java/org/apache/tika/parser/geogebra/GeoGebraParserTest.java index feb806ebe1d..9e4fa267183 100644 --- a/tika-parsers/tika-parsers-standard/tika-parsers-standard-modules/tika-parser-miscoffice-module/src/test/java/org/apache/tika/parser/geogebra/GeoGebraParserTest.java +++ b/tika-parsers/tika-parsers-standard/tika-parsers-standard-modules/tika-parser-miscoffice-module/src/test/java/org/apache/tika/parser/geogebra/GeoGebraParserTest.java @@ -20,15 +20,21 @@ import static org.junit.jupiter.api.Assertions.assertNull; import static org.junit.jupiter.api.Assertions.assertTrue; +import java.io.ByteArrayOutputStream; +import java.nio.charset.StandardCharsets; import java.util.List; +import java.util.zip.ZipEntry; +import java.util.zip.ZipOutputStream; import org.junit.jupiter.api.Test; import org.apache.tika.TikaTest; +import org.apache.tika.io.TikaInputStream; import org.apache.tika.metadata.HttpHeaders; import org.apache.tika.metadata.Metadata; import org.apache.tika.metadata.PagedText; import org.apache.tika.metadata.TikaCoreProperties; +import org.apache.tika.parser.ParseContext; public class GeoGebraParserTest extends TikaTest { @@ -112,4 +118,33 @@ public void testGGT() throws Exception { assertEquals(1, metadataList.size()); assertNull(metadata.get(TikaCoreProperties.TITLE)); } + + /** + * A crafted slide id can carry more digits than an int holds; sorting the + * slide ids must not throw a NumberFormatException out of parse(). + */ + @Test + public void testSlideNumberLargerThanIntParses() throws Exception { + ByteArrayOutputStream bos = new ByteArrayOutputStream(); + try (ZipOutputStream zos = new ZipOutputStream(bos)) { + zos.putNextEntry(new ZipEntry("structure.json")); + //two slides so that sorting actually compares the ids + zos.write(("{\"chapters\":[{\"pages\":[{\"elements\":" + + "[{\"id\":\"_slide0\"},{\"id\":\"_slide99999999999\"}]}]}]}") + .getBytes(StandardCharsets.UTF_8)); + zos.closeEntry(); + zos.putNextEntry(new ZipEntry("_slide0/geogebra.xml")); + zos.write("".getBytes(StandardCharsets.UTF_8)); + zos.closeEntry(); + zos.putNextEntry(new ZipEntry("_slide99999999999/geogebra.xml")); + zos.write("".getBytes(StandardCharsets.UTF_8)); + zos.closeEntry(); + } + try (TikaInputStream tis = TikaInputStream.get(bos.toByteArray())) { + List metadataList = + getRecursiveMetadata(tis, new Metadata(), new ParseContext(), false); + assertEquals("application/vnd.geogebra.slides", + metadataList.get(0).get(HttpHeaders.CONTENT_TYPE)); + } + } } From f6ba68fdfd832236fb79b581a8058e6462335876 Mon Sep 17 00:00:00 2001 From: Dominik Schmidt Date: Thu, 27 Aug 2026 09:32:17 +0200 Subject: [PATCH 4/7] TIKA-4831 - address review: kebab-case keys, robust parts, shared conventions Metadata keys Tika coined are kebab-cased (geogebra:app-name, app-version, format-version); toolName, id and date are verbatim attribute names and stay. The component is named geogebra-parser explicitly. Parser: document metadata comes from the primary XML only (the macro XML of a worksheet contributes tool names, its nested construction no longer overrides title/author), text expressions yield every string literal (dynamic texts like "Area = " + a), structure.json only orders the slides that exist as _slideN/geogebra.xml (missing or corrupt structure.json falls back to numeric order, leading zeros aside), a root geogebra.xml next to slides is parsed too, the thumbnail falls back to the first slide that has one, geogebra_javascript.js is emitted as MACRO, pictures as INLINE with their slide page, other files as ATTACHMENT, housekeeping names only match at the root and in slide directories, unreadable entries and malformed XML are recorded and skipped instead of aborting the parse, structure.json reads are bounded and content JSON is shape-checked before parsing. Detector: getEntry lookups instead of a central directory walk (entries are only enumerated when structure.json exists), registered last in the SPI file so it does not outrank the existing zip detectors. Streaming detection is exercised by TestContainerAwareDetector with the .ggb fixture. CHANGES notes the upgrade behaviour. --- CHANGES.txt | 10 +- .../tika/metadata/metadata-key-fields.json | 6 +- .../apache/tika/metadata/metadata-keys.json | 6 +- .../detect/TestContainerAwareDetector.java | 5 + .../resources/test-documents/testGeoGebra.ggb | Bin 0 -> 2010 bytes .../tika/parser/geogebra/GeoGebraParser.java | 342 +++++++++++------- .../parser/geogebra/GeoGebraXMLHandler.java | 82 +++-- .../parser/geogebra/GeoGebraParserTest.java | 242 +++++++++++-- .../tika/detect/zip/GeoGebraDetector.java | 29 +- ...pache.tika.detect.zip.ZipContainerDetector | 5 +- 10 files changed, 518 insertions(+), 209 deletions(-) create mode 100644 tika-parsers/tika-parsers-standard/tika-parsers-standard-integration-tests/src/test/resources/test-documents/testGeoGebra.ggb diff --git a/CHANGES.txt b/CHANGES.txt index 909a4eaf80b..d053fc91131 100644 --- a/CHANGES.txt +++ b/CHANGES.txt @@ -50,12 +50,10 @@ Release 4.1.0 - unreleased guards) and now honors shouldParseEmbedded for embedded file data (TIKA-4814). - * Content-based detection and a parser for the GeoGebra formats. *.ggb and - *.ggt are now sub-classes of application/zip and a zip container detector - identifies worksheets, Notes/Slides (*.ggs, new mime type, along with the - JSON-based *.ggp pinboard) and tools without a filename. GeoGebraParser - extracts the construction metadata and user-visible text and emits the - representative geogebra_thumbnail.png as a thumbnail embedded document + * New GeoGebraParser for *.ggb/*.ggs/*.ggt: geogebra:* metadata, text and + the thumbnail as a THUMBNAIL embedded document, with content-based + detection. Previously typed application/zip with every entry as an + attachment. *.ggs and *.ggp are new mime types; *.ggp is glob-only (TIKA-4831). * RawTiffParser extracts the camera-generated JPEG previews embedded in diff --git a/tika-metadata-schema/src/main/resources/org/apache/tika/metadata/metadata-key-fields.json b/tika-metadata-schema/src/main/resources/org/apache/tika/metadata/metadata-key-fields.json index 4fdcd08c5b7..5b2ee7e0da3 100644 --- a/tika-metadata-schema/src/main/resources/org/apache/tika/metadata/metadata-key-fields.json +++ b/tika-metadata-schema/src/main/resources/org/apache/tika/metadata/metadata-key-fields.json @@ -731,10 +731,10 @@ {"class":"org.apache.tika.parser.gdal.GDALParser","field":"SIZE","key":"gdal:size"}, {"class":"org.apache.tika.parser.gdal.GDALParser","field":"UPPER_LEFT","key":"gdal:upper-left"}, {"class":"org.apache.tika.parser.gdal.GDALParser","field":"UPPER_RIGHT","key":"gdal:upper-right"}, - {"class":"org.apache.tika.parser.geogebra.GeoGebraParser","field":"APP_NAME","key":"geogebra:appName"}, - {"class":"org.apache.tika.parser.geogebra.GeoGebraParser","field":"APP_VERSION","key":"geogebra:appVersion"}, + {"class":"org.apache.tika.parser.geogebra.GeoGebraParser","field":"APP_NAME","key":"geogebra:app-name"}, + {"class":"org.apache.tika.parser.geogebra.GeoGebraParser","field":"APP_VERSION","key":"geogebra:app-version"}, {"class":"org.apache.tika.parser.geogebra.GeoGebraParser","field":"DATE","key":"geogebra:date"}, - {"class":"org.apache.tika.parser.geogebra.GeoGebraParser","field":"FORMAT_VERSION","key":"geogebra:formatVersion"}, + {"class":"org.apache.tika.parser.geogebra.GeoGebraParser","field":"FORMAT_VERSION","key":"geogebra:format-version"}, {"class":"org.apache.tika.parser.geogebra.GeoGebraParser","field":"ID","key":"geogebra:id"}, {"class":"org.apache.tika.parser.geogebra.GeoGebraParser","field":"TOOL_NAME","key":"geogebra:toolName"}, {"class":"org.apache.tika.parser.hdf.HDFParser","field":"FILE_TYPE_DESCRIPTION","key":"hdf:file-type-description"}, diff --git a/tika-metadata-schema/src/main/resources/org/apache/tika/metadata/metadata-keys.json b/tika-metadata-schema/src/main/resources/org/apache/tika/metadata/metadata-keys.json index 2f3112b2c54..9ff9bcae9c6 100644 --- a/tika-metadata-schema/src/main/resources/org/apache/tika/metadata/metadata-keys.json +++ b/tika-metadata-schema/src/main/resources/org/apache/tika/metadata/metadata-keys.json @@ -191,10 +191,10 @@ {"key":"geo:lat","namespace":"geo","valueType":"REAL","cardinality":"SIMPLE","module":"tika-core"}, {"key":"geo:long","namespace":"geo","valueType":"REAL","cardinality":"SIMPLE","module":"tika-core"}, {"key":"geo:timestamp","namespace":"geo","valueType":"DATE","cardinality":"SIMPLE","module":"tika-core"}, - {"key":"geogebra:appName","namespace":"geogebra","valueType":"TEXT","cardinality":"SIMPLE","module":"tika-parser-miscoffice-module"}, - {"key":"geogebra:appVersion","namespace":"geogebra","valueType":"TEXT","cardinality":"SIMPLE","module":"tika-parser-miscoffice-module"}, + {"key":"geogebra:app-name","namespace":"geogebra","valueType":"TEXT","cardinality":"SIMPLE","module":"tika-parser-miscoffice-module"}, + {"key":"geogebra:app-version","namespace":"geogebra","valueType":"TEXT","cardinality":"SIMPLE","module":"tika-parser-miscoffice-module"}, {"key":"geogebra:date","namespace":"geogebra","valueType":"TEXT","cardinality":"SIMPLE","module":"tika-parser-miscoffice-module"}, - {"key":"geogebra:formatVersion","namespace":"geogebra","valueType":"TEXT","cardinality":"SIMPLE","module":"tika-parser-miscoffice-module"}, + {"key":"geogebra:format-version","namespace":"geogebra","valueType":"TEXT","cardinality":"SIMPLE","module":"tika-parser-miscoffice-module"}, {"key":"geogebra:id","namespace":"geogebra","valueType":"TEXT","cardinality":"SIMPLE","module":"tika-parser-miscoffice-module"}, {"key":"geogebra:toolName","namespace":"geogebra","valueType":"TEXT","cardinality":"BAG","module":"tika-parser-miscoffice-module"}, {"key":"geotopic:latitude","namespace":"geotopic","valueType":"TEXT","cardinality":"SIMPLE","module":""}, diff --git a/tika-parsers/tika-parsers-standard/tika-parsers-standard-integration-tests/src/test/java/org/apache/tika/detect/TestContainerAwareDetector.java b/tika-parsers/tika-parsers-standard/tika-parsers-standard-integration-tests/src/test/java/org/apache/tika/detect/TestContainerAwareDetector.java index 013fceab4d7..2ce165de63c 100644 --- a/tika-parsers/tika-parsers-standard/tika-parsers-standard-integration-tests/src/test/java/org/apache/tika/detect/TestContainerAwareDetector.java +++ b/tika-parsers/tika-parsers-standard/tika-parsers-standard-integration-tests/src/test/java/org/apache/tika/detect/TestContainerAwareDetector.java @@ -454,6 +454,11 @@ public void testDetectKMZ() throws Exception { assertTypeByData("testKMZ.kmz", "application/vnd.google-earth.kmz"); } + @Test + public void testDetectGeoGebra() throws Exception { + assertTypeByData("testGeoGebra.ggb", "application/vnd.geogebra.file"); + } + @Test public void testDetectIPA() throws Exception { assertTypeByNameAndData("testIPA.ipa", "application/x-itunes-ipa"); diff --git a/tika-parsers/tika-parsers-standard/tika-parsers-standard-integration-tests/src/test/resources/test-documents/testGeoGebra.ggb b/tika-parsers/tika-parsers-standard/tika-parsers-standard-integration-tests/src/test/resources/test-documents/testGeoGebra.ggb new file mode 100644 index 0000000000000000000000000000000000000000..087532316a94ac1f7a9a5d58b540cbc2a2079246 GIT binary patch literal 2010 zcmWIWW@Zs#U|`^2aPk$8<(hj-)dk4&1Y$8DPEXBGPfaRHj4#P3%}vTn%*@d%$V-3C z^P%|p(@Bq#5)vMK{ZhcmbV*0y%t02Ft7^qrOqz`T;>Ra+S)OE#te<(OvQNZFMs(7> zh)S&*@qoUAm#>~rog3^V%n*QLuj`q6u~I<$B+%{6N-Rq(PALYPPfHzyF}69L9_$o%J z8ke7^`pnOYtg;${p^m|J@n`189b$Pl!B;Hwn7V)E&N(@WoIe+xN!WTT$SA6C=@;*7 zO>ZUIdGi;fZl7f=e#-P*X}G5m&*3i|Ijelz^w$9MCSyZPHUo^#Q7J>!vs?Z>#7_&*!uT2_5=U<=&Img<$x?Ik5p+8h+47d}Ph zUiRv#%3(DuC6PfUYbOirZ=2^G$hqtum&BgueCMavMmH%wTxHnQCS&*K{%aQTbz4`O zRli=K`uKfDQ_Ov?mMJ#dZvqnWm!u7uJ(c(xd$wBHmK&XgdCqKr?Im4{rQ*w z4*grB8nw+Q%A_<(@j?&!$N9zjnJSWR~|{J7vm(ra#UrkNXBBZtK|@_ul%0 zqT?j@q&4;pfeOlotvjmjMsG^~6k*lyPWO)C^*bwGiC(c_DL?a|#6sr43#kJ3^ISVJ z3=M9jUp!%R%G_&5#@YpD1s~Flcmfs(>{l|qEtxPimm%QCmxXWCx0_tDJoNpA?x6>D zZk@vCw2QWv^R6~ka}H*%$o$y(W!=SP3(Q58w^y+XxzCx@WvQR&SC_gfJRrtn=gN&S|#_XC zMG_0^I*gS{+BfLWKiR+J$o-}d`tuhjd(^FKIPZ2o_`kg1r3H>sDjxFO4crHISo>e; zHi%>q72^+m@I%Dr=z@cN)&%n{3SHL7yo0wdb56W&+C;4XIF%W1w ze>Yz7nOyeXg#yzuoXrJ#F%bH>}I= zyS8DzmfG974cj7_gZ+N_?T$Mz}YPQR_XN!;OJjmYp^WEa$;;Ui2 z&H@U&%a*8aNH7rZZs5FO_;T%M|BO&BjjP=B-5>Rf>T-565>9Ih4YIvUV6AuU()e;(bFwL c=Q$RHPDCmW@MZ;8B@7I#K * The construction metadata (title, author, date) and the application * name/version are read from {@code geogebra.xml} (or, for a tool, from - * {@code geogebra_macro.xml}), and the user-visible text (text objects, ink - * notes, captions) is emitted as XHTML paragraphs. For Notes/Slides, the slide - * order is taken from {@code structure.json} and each slide becomes a - * {@code

}. + * {@code geogebra_macro.xml}), and the user-visible text (text objects, inline + * text, captions, tool names and help) is emitted as XHTML paragraphs. For + * Notes/Slides, each {@code _slideN/geogebra.xml} becomes a + * {@code
}, in the order given by {@code structure.json}. *

- * The representative rendering of the document — {@code geogebra_thumbnail.png} - * at the root of a worksheet or tool, or the first slide's thumbnail of a - * Notes/Slides file — is emitted as an embedded document marked with + * The representative rendering of the document, {@code geogebra_thumbnail.png} + * at the root of a worksheet or tool, or the first available slide thumbnail + * of a Notes/Slides file, is emitted as an embedded document marked with * {@link TikaCoreProperties.EmbeddedResourceType#THUMBNAIL}, so that clients * (e.g. the unpacker's sidecar metadata) can pick it as the preview image. * Thumbnails of the remaining slides are renderings of content that is already - * extracted, so they are skipped. Any other embedded file (e.g. inserted - * pictures) is emitted as an embedded document. + * extracted, so they are skipped. The document script + * {@code geogebra_javascript.js} is emitted as a + * {@link TikaCoreProperties.EmbeddedResourceType#MACRO}, and any other + * embedded file (e.g. inserted pictures) as an embedded document. + *

+ * A part that cannot be read (an unsupported zip entry, malformed XML) is + * recorded in the metadata and skipped; the remaining parts are still parsed. */ -@TikaComponent +@TikaComponent(name = "geogebra-parser") public class GeoGebraParser implements Parser { /** @@ -89,19 +99,19 @@ public class GeoGebraParser implements Parser { * e.g. "classic", "notes", "graphing". */ public static final Property APP_NAME = - Property.internalText(GEOGEBRA_PREFIX + "appName"); + Property.internalText(GEOGEBRA_PREFIX + "app-name"); /** * The GeoGebra application version the file was written with. */ public static final Property APP_VERSION = - Property.internalText(GEOGEBRA_PREFIX + "appVersion"); + Property.internalText(GEOGEBRA_PREFIX + "app-version"); /** * The GeoGebra XML format version. */ public static final Property FORMAT_VERSION = - Property.internalText(GEOGEBRA_PREFIX + "formatVersion"); + Property.internalText(GEOGEBRA_PREFIX + "format-version"); /** * The unique id GeoGebra assigns to the document. @@ -116,7 +126,8 @@ public class GeoGebraParser implements Parser { /** * The tool names of the macros in a tool file (or in a worksheet with - * embedded macros). + * embedded macros). The name is the {@code toolName} attribute of the + * macro element. */ public static final Property TOOL_NAME = Property.internalTextBag(GEOGEBRA_PREFIX + "toolName"); @@ -130,20 +141,29 @@ public class GeoGebraParser implements Parser { private static final String MACRO_XML = "geogebra_macro.xml"; private static final String STRUCTURE_JSON = "structure.json"; private static final String THUMBNAIL_PNG = "geogebra_thumbnail.png"; + private static final String JAVASCRIPT_JS = "geogebra_javascript.js"; /** - * Housekeeping entries every ggb-like container may carry; everything - * else is user content and worth emitting as an embedded document. + * Housekeeping entries at the root or in a slide directory that carry no + * user content of their own. The XML files are parsed for text and the + * thumbnails handled separately. */ - private static final Set KNOWN_ENTRY_NAMES = Collections.unmodifiableSet( + private static final Set HOUSEKEEPING_NAMES = Collections.unmodifiableSet( new HashSet<>(Arrays.asList(GEOGEBRA_XML, MACRO_XML, THUMBNAIL_PNG, - "geogebra_defaults2d.xml", "geogebra_defaults3d.xml", - "geogebra_javascript.js"))); + "geogebra_defaults2d.xml", "geogebra_defaults3d.xml"))); + + private static final String SLIDE_DIR_PREFIX = "_slide"; private static final Pattern SLIDE_XML_PATTERN = - Pattern.compile("^(_slide\\d+)/" + Pattern.quote(GEOGEBRA_XML) + "$"); + Pattern.compile("^(" + SLIDE_DIR_PREFIX + "\\d+)/" + Pattern.quote(GEOGEBRA_XML) + "$"); + + /** + * structure.json only lists chapters, pages and element ids; a real one is + * a few kilobytes. + */ + private static final long MAX_STRUCTURE_JSON_LENGTH = 1024 * 1024; - private static final ObjectMapper OBJECT_MAPPER = new ObjectMapper(); + static final ObjectMapper OBJECT_MAPPER = new ObjectMapper(); @Override public Set getSupportedTypes(ParseContext context) { @@ -168,22 +188,52 @@ public void parse(TikaInputStream tis, ContentHandler handler, Metadata metadata XHTMLContentHandler xhtml = new XHTMLContentHandler(handler, metadata, context); xhtml.startDocument(); List slideIds = getSlideIds(zipFile); + ZipArchiveEntry rootXml = zipFile.getEntry(GEOGEBRA_XML); + ZipArchiveEntry macroXml = zipFile.getEntry(MACRO_XML); + //document metadata comes from the first XML parsed: a worksheet's + //geogebra.xml, a tool's geogebra_macro.xml, or the first slide + boolean documentMetadataPending = true; + if (rootXml != null) { + documentMetadataPending = false; + parseGeoGebraXml(zipFile, rootXml, xhtml, metadata, true, context); + } + if (macroXml != null) { + //a worksheet with macros carries both XMLs; the macro one only + //contributes the tool names then, not the document metadata + parseGeoGebraXml(zipFile, macroXml, xhtml, metadata, documentMetadataPending, context); + documentMetadataPending = false; + } + Map pageNumbers = new HashMap<>(); if (!slideIds.isEmpty()) { - parseSlides(zipFile, slideIds, xhtml, metadata, context, embeddedDocumentExtractor); - } else { - parseWorksheet(zipFile, xhtml, metadata, context, embeddedDocumentExtractor); + metadata.set(PagedText.N_PAGES, slideIds.size()); + int page = 1; + for (String slideId : slideIds) { + pageNumbers.put(slideId, page++); + xhtml.startElement("div", "class", "slide"); + try { + ZipArchiveEntry slideXml = zipFile.getEntry(slideId + "/" + GEOGEBRA_XML); + parseGeoGebraXml(zipFile, slideXml, xhtml, metadata, documentMetadataPending, + context); + documentMetadataPending = false; + } finally { + xhtml.endElement("div"); + } + } } + handleThumbnail(zipFile, slideIds, xhtml, metadata, context, embeddedDocumentExtractor); + handleOtherEntries(zipFile, pageNumbers, xhtml, metadata, context, + embeddedDocumentExtractor); xhtml.endDocument(); } /** * Returns the ordered slide directory names of a Notes/Slides file, or an - * empty list if this is not a Notes/Slides file. The order comes from - * {@code structure.json}; slides present in the zip but missing from - * {@code structure.json} are appended in numeric order. + * empty list if there are no slides. The slides are the + * {@code _slideN/geogebra.xml} entries; {@code structure.json} only + * supplies their order, slides it does not list (or all of them, if it is + * missing or unreadable) follow in numeric order. */ private List getSlideIds(ZipFile zipFile) { - Set inZip = new LinkedHashSet<>(); List numericallySorted = new ArrayList<>(); Enumeration entries = zipFile.getEntries(); while (entries.hasMoreElements()) { @@ -192,120 +242,117 @@ private List getSlideIds(ZipFile zipFile) { numericallySorted.add(m.group(1)); } } - //compare the digit suffixes numerically without parsing them: a shorter - //digit string is the smaller number, equal lengths compare lexicographically. - //Parsing would throw for a crafted id with more digits than an int holds. - numericallySorted.sort((a, b) -> { - String da = a.substring("_slide".length()); - String db = b.substring("_slide".length()); - return da.length() != db.length() - ? Integer.compare(da.length(), db.length()) : da.compareTo(db); - }); - - ZipArchiveEntry structure = zipFile.getEntry(STRUCTURE_JSON); - if (structure == null || numericallySorted.isEmpty()) { + if (numericallySorted.isEmpty()) { return Collections.emptyList(); } - Set knownSlideIds = new HashSet<>(numericallySorted); - try (InputStream is = zipFile.getInputStream(structure)) { - JsonNode root = OBJECT_MAPPER.readTree(is); - for (JsonNode chapter : root.path("chapters")) { - for (JsonNode page : chapter.path("pages")) { - for (JsonNode element : page.path("elements")) { - String id = element.path("id").asText(""); - if (knownSlideIds.contains(id)) { - inZip.add(id); + numericallySorted.sort(GeoGebraParser::compareSlideIds); + + Set ordered = new LinkedHashSet<>(); + ZipArchiveEntry structure = zipFile.getEntry(STRUCTURE_JSON); + if (structure != null && zipFile.canReadEntryData(structure)) { + Set knownSlideIds = new HashSet<>(numericallySorted); + try (InputStream is = new BoundedInputStream(MAX_STRUCTURE_JSON_LENGTH, + zipFile.getInputStream(structure))) { + JsonNode root = OBJECT_MAPPER.readTree(is); + for (JsonNode chapter : root.path("chapters")) { + for (JsonNode page : chapter.path("pages")) { + for (JsonNode element : page.path("elements")) { + String id = element.path("id").asText(""); + if (knownSlideIds.contains(id)) { + ordered.add(id); + } } } } + } catch (IOException e) { + //fall through to the numeric order } - } catch (IOException e) { - //fall through to the numeric order - } - for (String id : numericallySorted) { - inZip.add(id); } - return new ArrayList<>(inZip); + ordered.addAll(numericallySorted); + return new ArrayList<>(ordered); } - private void parseWorksheet(ZipFile zipFile, XHTMLContentHandler xhtml, Metadata metadata, - ParseContext context, EmbeddedDocumentExtractor embeddedDocumentExtractor) - throws IOException, SAXException, TikaException { - //a worksheet with macros carries both geogebra.xml and geogebra_macro.xml - for (String xmlName : new String[]{GEOGEBRA_XML, MACRO_XML}) { - ZipArchiveEntry contentXml = zipFile.getEntry(xmlName); - if (contentXml != null) { - parseGeoGebraXml(zipFile, contentXml, xhtml, metadata, context); - } + /** + * Compares the digit suffixes of two slide ids numerically without + * parsing them (a crafted id may carry more digits than a long holds): + * leading zeros aside, a shorter digit string is the smaller number and + * equal lengths compare lexicographically. + */ + private static int compareSlideIds(String a, String b) { + String da = stripLeadingZeros(a.substring(SLIDE_DIR_PREFIX.length())); + String db = stripLeadingZeros(b.substring(SLIDE_DIR_PREFIX.length())); + if (da.length() != db.length()) { + return Integer.compare(da.length(), db.length()); } - handleThumbnail(zipFile, zipFile.getEntry(THUMBNAIL_PNG), xhtml, context, - embeddedDocumentExtractor); - handleOtherEntries(zipFile, xhtml, context, embeddedDocumentExtractor); + int byValue = da.compareTo(db); + return byValue != 0 ? byValue : a.compareTo(b); } - private void parseSlides(ZipFile zipFile, List slideIds, XHTMLContentHandler xhtml, - Metadata metadata, ParseContext context, - EmbeddedDocumentExtractor embeddedDocumentExtractor) - throws IOException, SAXException, TikaException { - metadata.set(PagedText.N_PAGES, slideIds.size()); - boolean first = true; - for (String slideId : slideIds) { - xhtml.startElement("div", "class", "slide"); - ZipArchiveEntry contentXml = zipFile.getEntry(slideId + "/" + GEOGEBRA_XML); - if (contentXml != null) { - //document-level metadata comes from the first slide - parseGeoGebraXml(zipFile, contentXml, xhtml, first ? metadata : null, context); - } - xhtml.endElement("div"); - if (first) { - handleThumbnail(zipFile, zipFile.getEntry(slideId + "/" + THUMBNAIL_PNG), xhtml, - context, embeddedDocumentExtractor); - } - first = false; + private static String stripLeadingZeros(String digits) { + int i = 0; + while (i < digits.length() - 1 && digits.charAt(i) == '0') { + i++; } - handleOtherEntries(zipFile, xhtml, context, embeddedDocumentExtractor); + return digits.substring(i); } + /** + * Parses one GeoGebra XML for its text and, if {@code documentMetadata} + * is set, the document metadata. A part that cannot be read or is not + * well-formed is recorded in the metadata and skipped. + */ private void parseGeoGebraXml(ZipFile zipFile, ZipArchiveEntry entry, XHTMLContentHandler xhtml, Metadata metadata, - ParseContext context) - throws IOException, SAXException, TikaException { + boolean documentMetadata, ParseContext context) + throws SAXException { + if (entry == null) { + return; + } + if (!zipFile.canReadEntryData(entry)) { + EmbeddedDocumentUtil.recordEmbeddedStreamException( + new IOException("Unsupported zip entry: " + entry.getName()), metadata); + return; + } try (InputStream is = zipFile.getInputStream(entry)) { - XMLReaderUtils.parseSAX(is, - new EmbeddedContentHandler(new GeoGebraXMLHandler(xhtml, metadata)), context); + XMLReaderUtils.parseSAX(is, new EmbeddedContentHandler( + new GeoGebraXMLHandler(xhtml, metadata, documentMetadata)), context); + } catch (SAXException e) { + if (WriteLimitReachedException.isWriteLimitReached(e)) { + throw e; + } + EmbeddedDocumentUtil.recordEmbeddedStreamException(e, metadata); + } catch (IOException | TikaException e) { + EmbeddedDocumentUtil.recordEmbeddedStreamException(e, metadata); } } /** - * Emits the representative thumbnail as an embedded document marked - * {@link TikaCoreProperties.EmbeddedResourceType#THUMBNAIL}. + * Emits the representative thumbnail: the root one of a worksheet or + * tool, or the first slide thumbnail (in slide order) of a Notes/Slides + * file. */ - private void handleThumbnail(ZipFile zipFile, ZipArchiveEntry entry, XHTMLContentHandler xhtml, - ParseContext context, EmbeddedDocumentExtractor embeddedDocumentExtractor) + private void handleThumbnail(ZipFile zipFile, List slideIds, XHTMLContentHandler xhtml, + Metadata metadata, ParseContext context, + EmbeddedDocumentExtractor embeddedDocumentExtractor) throws IOException, SAXException { - if (entry == null) { - return; + ZipArchiveEntry entry = zipFile.getEntry(THUMBNAIL_PNG); + for (int i = 0; entry == null && i < slideIds.size(); i++) { + entry = zipFile.getEntry(slideIds.get(i) + "/" + THUMBNAIL_PNG); } - Metadata embeddedMetadata = Metadata.newInstance(context); - embeddedMetadata.set(TikaCoreProperties.RESOURCE_NAME_KEY, entry.getName()); - embeddedMetadata.set(TikaCoreProperties.INTERNAL_PATH, entry.getName()); - embeddedMetadata.set(TikaCoreProperties.EMBEDDED_RESOURCE_TYPE, - TikaCoreProperties.EmbeddedResourceType.THUMBNAIL.toString()); - embeddedMetadata.set(HttpHeaders.CONTENT_TYPE, "image/png"); - if (embeddedDocumentExtractor.shouldParseEmbedded(embeddedMetadata, context)) { - try (TikaInputStream tisZip = TikaInputStream.get(zipFile.getInputStream(entry))) { - embeddedDocumentExtractor.parseEmbedded(tisZip, new EmbeddedContentHandler(xhtml), - embeddedMetadata, context, false); - } + if (entry != null) { + handleEmbedded(zipFile, entry, TikaCoreProperties.EmbeddedResourceType.THUMBNAIL, + null, xhtml, metadata, context, embeddedDocumentExtractor); } } /** - * Emits everything that is not GeoGebra housekeeping (e.g. inserted - * pictures) as an embedded document. Housekeeping entries are matched by - * their basename so the rule covers slide subdirectories, too. + * Emits everything that is not GeoGebra housekeeping: the document script + * as a macro, and inserted pictures and other files as embedded documents. + * Housekeeping is matched at the root and in the slide directories only, + * so a file of the same name elsewhere is still emitted. */ - private void handleOtherEntries(ZipFile zipFile, XHTMLContentHandler xhtml, + private void handleOtherEntries(ZipFile zipFile, Map pageNumbers, + XHTMLContentHandler xhtml, Metadata metadata, ParseContext context, EmbeddedDocumentExtractor embeddedDocumentExtractor) throws IOException, SAXException { @@ -316,25 +363,68 @@ private void handleOtherEntries(ZipFile zipFile, XHTMLContentHandler xhtml, continue; } String name = entry.getName(); - if (STRUCTURE_JSON.equals(name)) { - continue; + String dir = ""; + String basename = name; + int slash = name.indexOf('/'); + if (slash >= 0) { + dir = name.substring(0, slash); + basename = name.substring(slash + 1); } - String basename = name.substring(name.lastIndexOf('/') + 1); - if (KNOWN_ENTRY_NAMES.contains(basename)) { + boolean knownDir = dir.isEmpty() || pageNumbers.containsKey(dir); + if (knownDir && (HOUSEKEEPING_NAMES.contains(basename) + || (dir.isEmpty() && STRUCTURE_JSON.equals(basename)))) { continue; } - Metadata embeddedMetadata = Metadata.newInstance(context); - embeddedMetadata.set(TikaCoreProperties.RESOURCE_NAME_KEY, name); - embeddedMetadata.set(TikaCoreProperties.INTERNAL_PATH, name); - embeddedMetadata.set(TikaCoreProperties.EMBEDDED_RESOURCE_TYPE, - TikaCoreProperties.EmbeddedResourceType.INLINE.toString()); - if (embeddedDocumentExtractor.shouldParseEmbedded(embeddedMetadata, context)) { - try (TikaInputStream tisZip = - TikaInputStream.get(zipFile.getInputStream(entry))) { - embeddedDocumentExtractor.parseEmbedded(tisZip, new EmbeddedContentHandler(xhtml), - embeddedMetadata, context, false); + TikaCoreProperties.EmbeddedResourceType type = null; + if (knownDir && JAVASCRIPT_JS.equals(basename)) { + type = TikaCoreProperties.EmbeddedResourceType.MACRO; + } + handleEmbedded(zipFile, entry, type, pageNumbers.get(dir), xhtml, metadata, context, + embeddedDocumentExtractor); + } + } + + /** + * Emits one zip entry as an embedded document. Without a given resource + * type, pictures are marked {@link TikaCoreProperties.EmbeddedResourceType#INLINE} + * and other files {@link TikaCoreProperties.EmbeddedResourceType#ATTACHMENT}. + * An entry in a slide directory is tagged with the slide's page number. + */ + private void handleEmbedded(ZipFile zipFile, ZipArchiveEntry entry, + TikaCoreProperties.EmbeddedResourceType type, Integer page, + XHTMLContentHandler xhtml, Metadata parentMetadata, + ParseContext context, + EmbeddedDocumentExtractor embeddedDocumentExtractor) + throws IOException, SAXException { + if (!zipFile.canReadEntryData(entry)) { + EmbeddedDocumentUtil.recordEmbeddedStreamException( + new IOException("Unsupported zip entry: " + entry.getName()), parentMetadata); + return; + } + Metadata embeddedMetadata = Metadata.newInstance(context); + embeddedMetadata.set(TikaCoreProperties.RESOURCE_NAME_KEY, entry.getName()); + embeddedMetadata.set(TikaCoreProperties.INTERNAL_PATH, entry.getName()); + if (page != null) { + PageAnchoring.applyPageMetadata(embeddedMetadata, Collections.singleton(page)); + } + try (TikaInputStream tisZip = TikaInputStream.get(zipFile.getInputStream(entry))) { + if (type == null) { + MediaType mediaType = EmbeddedDocumentUtil.getDetector(context) + .detect(tisZip, embeddedMetadata, context); + if (mediaType != null) { + embeddedMetadata.set(HttpHeaders.CONTENT_TYPE, mediaType.toString()); } + type = mediaType != null && "image".equals(mediaType.getType()) + ? TikaCoreProperties.EmbeddedResourceType.INLINE + : TikaCoreProperties.EmbeddedResourceType.ATTACHMENT; } + embeddedMetadata.set(TikaCoreProperties.EMBEDDED_RESOURCE_TYPE, type.toString()); + if (embeddedDocumentExtractor.shouldParseEmbedded(embeddedMetadata, context)) { + embeddedDocumentExtractor.parseEmbedded(tisZip, new EmbeddedContentHandler(xhtml), + embeddedMetadata, context, false); + } + } catch (IOException e) { + EmbeddedDocumentUtil.recordEmbeddedStreamException(e, parentMetadata); } } } diff --git a/tika-parsers/tika-parsers-standard/tika-parsers-standard-modules/tika-parser-miscoffice-module/src/main/java/org/apache/tika/parser/geogebra/GeoGebraXMLHandler.java b/tika-parsers/tika-parsers-standard/tika-parsers-standard-modules/tika-parser-miscoffice-module/src/main/java/org/apache/tika/parser/geogebra/GeoGebraXMLHandler.java index c2574b29c41..dc282d62db2 100644 --- a/tika-parsers/tika-parsers-standard/tika-parsers-standard-modules/tika-parser-miscoffice-module/src/main/java/org/apache/tika/parser/geogebra/GeoGebraXMLHandler.java +++ b/tika-parsers/tika-parsers-standard/tika-parsers-standard-modules/tika-parser-miscoffice-module/src/main/java/org/apache/tika/parser/geogebra/GeoGebraXMLHandler.java @@ -17,9 +17,10 @@ package org.apache.tika.parser.geogebra; import java.io.IOException; +import java.util.regex.Matcher; +import java.util.regex.Pattern; import com.fasterxml.jackson.databind.JsonNode; -import com.fasterxml.jackson.databind.ObjectMapper; import org.xml.sax.Attributes; import org.xml.sax.SAXException; import org.xml.sax.helpers.DefaultHandler; @@ -28,47 +29,57 @@ import org.apache.tika.metadata.Property; import org.apache.tika.metadata.TikaCoreProperties; import org.apache.tika.sax.XHTMLContentHandler; +import org.apache.tika.utils.StringUtils; /** * SAX handler for {@code geogebra.xml} and {@code geogebra_macro.xml}. *

- * Extracts the document metadata from the {@code <geogebra>} root and - * the {@code <construction>} element (when a {@link Metadata} object is - * given), and emits the user-visible text as XHTML paragraphs: string-literal - * {@code <expression>}s of text objects, the rich text of - * {@code <content>} elements (ink notes, inline text), element - * {@code <caption>}s and macro names/help texts. + * Extracts the document metadata from the {@code } root and its + * {@code } child (when asked to), and emits the user-visible + * text as XHTML paragraphs: the string literals of text object + * {@code }s, the text runs of {@code } elements (inline + * text, tables, mind maps), element {@code }s and macro names and + * help texts. */ class GeoGebraXMLHandler extends DefaultHandler { - private static final ObjectMapper OBJECT_MAPPER = new ObjectMapper(); + /** + * A GeoGebra string literal. GeoGebra writes strings between plain + * double quotes without any escaping, so a literal never contains one. + */ + private static final Pattern STRING_LITERAL = Pattern.compile("\"([^\"]*)\""); private final XHTMLContentHandler xhtml; private final Metadata metadata; + private final boolean documentMetadata; private int depth = 0; /** - * @param xhtml the handler paragraphs are written to - * @param metadata the metadata to fill from the root and construction - * elements, or {@code null} to extract text only + * @param xhtml the handler paragraphs are written to + * @param metadata the metadata tool names are added to + * @param documentMetadata whether to also fill the document metadata from + * the root and construction elements */ - GeoGebraXMLHandler(XHTMLContentHandler xhtml, Metadata metadata) { + GeoGebraXMLHandler(XHTMLContentHandler xhtml, Metadata metadata, boolean documentMetadata) { this.xhtml = xhtml; this.metadata = metadata; + this.documentMetadata = documentMetadata; } @Override public void startElement(String uri, String localName, String qName, Attributes attributes) throws SAXException { if (depth == 0 && "geogebra".equals(localName)) { - if (metadata != null) { + if (documentMetadata) { setIfNotBlank(GeoGebraParser.APP_NAME, attributes.getValue("app")); setIfNotBlank(GeoGebraParser.APP_VERSION, attributes.getValue("version")); setIfNotBlank(GeoGebraParser.FORMAT_VERSION, attributes.getValue("format")); setIfNotBlank(GeoGebraParser.ID, attributes.getValue("id")); } - } else if ("construction".equals(localName)) { - if (metadata != null) { + } else if (depth == 1 && "construction".equals(localName)) { + //only the document's own construction; a macro's construction is + //nested one level deeper inside its element + if (documentMetadata) { setIfNotBlank(TikaCoreProperties.TITLE, attributes.getValue("title")); setIfNotBlank(TikaCoreProperties.CREATOR, attributes.getValue("author")); setIfNotBlank(GeoGebraParser.DATE, attributes.getValue("date")); @@ -81,11 +92,11 @@ public void startElement(String uri, String localName, String qName, Attributes paragraph(attributes.getValue("val")); } else if ("macro".equals(localName)) { String toolName = attributes.getValue("toolName"); - if (isBlank(toolName)) { + if (StringUtils.isBlank(toolName)) { toolName = attributes.getValue("cmdName"); } - if (metadata != null && !isBlank(toolName)) { - metadata.add(GeoGebraParser.TOOL_NAME, toolName); + if (!StringUtils.isBlank(toolName)) { + metadata.add(GeoGebraParser.TOOL_NAME, toolName.trim()); } paragraph(toolName); paragraph(attributes.getValue("toolHelp")); @@ -99,16 +110,21 @@ public void endElement(String uri, String localName, String qName) { } /** - * Emits a text object's expression, which is a GeoGebra string literal - * like {@code "some text"}. Computational expressions (anything not - * quoted) are geometry, not text, and are skipped. + * Emits the string literals of an expression. A text object's expression + * is either a single literal like {@code "some text"} or, for a dynamic + * text, literals combined with values like {@code "Area = " + a}; the + * literals are the user's text, everything else is geometry and skipped. */ private void handleExpression(String exp) throws SAXException { - if (exp == null || exp.length() < 2 || exp.charAt(0) != '"' || - exp.charAt(exp.length() - 1) != '"') { + if (exp == null || exp.indexOf('"') < 0) { return; } - paragraph(exp.substring(1, exp.length() - 1).replace("\\\"", "\"")); + StringBuilder sb = new StringBuilder(); + Matcher m = STRING_LITERAL.matcher(exp); + while (m.find()) { + sb.append(m.group(1)); + } + paragraph(sb.toString()); } /** @@ -118,14 +134,18 @@ private void handleExpression(String exp) throws SAXException { * emitted one paragraph per line. */ private void handleContent(String val) throws SAXException { - if (val == null || val.isEmpty()) { + if (val == null) { + return; + } + String trimmed = val.trim(); + if (trimmed.isEmpty() || (trimmed.charAt(0) != '[' && trimmed.charAt(0) != '{')) { + //not a JSON document; a plain string carries no text runs return; } JsonNode root; try { - root = OBJECT_MAPPER.readTree(val); + root = GeoGebraParser.OBJECT_MAPPER.readTree(trimmed); } catch (IOException e) { - //not JSON; ignore return; } StringBuilder sb = new StringBuilder(); @@ -138,18 +158,14 @@ private void handleContent(String val) throws SAXException { } private void paragraph(String text) throws SAXException { - if (!isBlank(text)) { + if (!StringUtils.isBlank(text)) { xhtml.element("p", text.trim()); } } private void setIfNotBlank(Property property, String value) { - if (!isBlank(value)) { + if (!StringUtils.isBlank(value)) { metadata.set(property, value.trim()); } } - - private static boolean isBlank(String s) { - return s == null || s.trim().isEmpty(); - } } diff --git a/tika-parsers/tika-parsers-standard/tika-parsers-standard-modules/tika-parser-miscoffice-module/src/test/java/org/apache/tika/parser/geogebra/GeoGebraParserTest.java b/tika-parsers/tika-parsers-standard/tika-parsers-standard-modules/tika-parser-miscoffice-module/src/test/java/org/apache/tika/parser/geogebra/GeoGebraParserTest.java index 9e4fa267183..25881e2cccd 100644 --- a/tika-parsers/tika-parsers-standard/tika-parsers-standard-modules/tika-parser-miscoffice-module/src/test/java/org/apache/tika/parser/geogebra/GeoGebraParserTest.java +++ b/tika-parsers/tika-parsers-standard/tika-parsers-standard-modules/tika-parser-miscoffice-module/src/test/java/org/apache/tika/parser/geogebra/GeoGebraParserTest.java @@ -17,12 +17,15 @@ package org.apache.tika.parser.geogebra; import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNotNull; import static org.junit.jupiter.api.Assertions.assertNull; import static org.junit.jupiter.api.Assertions.assertTrue; import java.io.ByteArrayOutputStream; import java.nio.charset.StandardCharsets; +import java.util.LinkedHashMap; import java.util.List; +import java.util.Map; import java.util.zip.ZipEntry; import java.util.zip.ZipOutputStream; @@ -34,10 +37,16 @@ import org.apache.tika.metadata.Metadata; import org.apache.tika.metadata.PagedText; import org.apache.tika.metadata.TikaCoreProperties; +import org.apache.tika.metadata.TikaPagedText; import org.apache.tika.parser.ParseContext; +import org.apache.tika.parser.Parser; public class GeoGebraParserTest extends TikaTest { + private static final String XML_HEAD = "\n"; + + private static final String PNG = "\u0089PNG"; + @Test public void testGGB() throws Exception { List metadataList = getRecursiveMetadata("testGeoGebra.ggb"); @@ -60,13 +69,15 @@ public void testGGB() throws Exception { assertEquals("Midpoint tool", metadata.get(GeoGebraParser.TOOL_NAME)); assertContains("Select two points to construct their midpoint", content); - //the thumbnail is emitted as an embedded document marked THUMBNAIL - assertEquals(2, metadataList.size()); - Metadata thumbnail = metadataList.get(1); + assertEquals(3, metadataList.size()); + Metadata thumbnail = byName(metadataList, "geogebra_thumbnail.png"); assertEquals("image/png", thumbnail.get(HttpHeaders.CONTENT_TYPE)); - assertEquals("geogebra_thumbnail.png", thumbnail.get(TikaCoreProperties.RESOURCE_NAME_KEY)); assertEquals(TikaCoreProperties.EmbeddedResourceType.THUMBNAIL.toString(), thumbnail.get(TikaCoreProperties.EMBEDDED_RESOURCE_TYPE)); + //the document script is user code + Metadata script = byName(metadataList, "geogebra_javascript.js"); + assertEquals(TikaCoreProperties.EmbeddedResourceType.MACRO.toString(), + script.get(TikaCoreProperties.EMBEDDED_RESOURCE_TYPE)); } @Test @@ -83,24 +94,29 @@ public void testGGS() throws Exception { assertContains("Second slide text", content); assertTrue(content.indexOf("First slide text") < content.indexOf("Second slide text"), "slide order should follow structure.json"); + assertContains("

", content); //only the first slide's thumbnail is emitted, marked THUMBNAIL - assertEquals(3, metadataList.size()); - Metadata thumbnail = metadataList.get(1); + assertEquals(5, metadataList.size()); + Metadata thumbnail = byName(metadataList, "_slide1/geogebra_thumbnail.png"); assertEquals("image/png", thumbnail.get(HttpHeaders.CONTENT_TYPE)); - assertEquals("_slide1/geogebra_thumbnail.png", - thumbnail.get(TikaCoreProperties.INTERNAL_PATH)); assertEquals(TikaCoreProperties.EmbeddedResourceType.THUMBNAIL.toString(), thumbnail.get(TikaCoreProperties.EMBEDDED_RESOURCE_TYPE)); + assertNull(byName(metadataList, "_slide0/geogebra_thumbnail.png")); - //the inserted picture is emitted under its full zip entry name - Metadata picture = metadataList.get(2); - assertEquals("_slide0/8c6976e5b541/photo.png", - picture.get(TikaCoreProperties.RESOURCE_NAME_KEY)); + //the inserted picture is emitted under its full zip entry name, as an + //inline image anchored to its slide (the second page) + Metadata picture = byName(metadataList, "_slide0/8c6976e5b541/photo.png"); assertEquals("_slide0/8c6976e5b541/photo.png", picture.get(TikaCoreProperties.INTERNAL_PATH)); + assertEquals("image/png", picture.get(HttpHeaders.CONTENT_TYPE)); assertEquals(TikaCoreProperties.EmbeddedResourceType.INLINE.toString(), picture.get(TikaCoreProperties.EMBEDDED_RESOURCE_TYPE)); + assertEquals("2", picture.get(TikaPagedText.PAGE_NUMBERS)); + + Metadata script = byName(metadataList, "_slide0/geogebra_javascript.js"); + assertEquals(TikaCoreProperties.EmbeddedResourceType.MACRO.toString(), + script.get(TikaCoreProperties.EMBEDDED_RESOURCE_TYPE)); } @Test @@ -109,42 +125,210 @@ public void testGGT() throws Exception { Metadata metadata = metadataList.get(0); assertEquals("application/vnd.geogebra.tool", metadata.get(HttpHeaders.CONTENT_TYPE)); assertEquals("Midpoint tool", metadata.get(GeoGebraParser.TOOL_NAME)); + assertEquals("classic", metadata.get(GeoGebraParser.APP_NAME)); String content = metadata.get(TikaCoreProperties.TIKA_CONTENT); assertContains("Midpoint tool", content); assertContains("Select two points to construct their midpoint", content); - //no thumbnail in this tool file + //no thumbnail in this tool file; the macro's own construction carries + //no document metadata assertEquals(1, metadataList.size()); assertNull(metadata.get(TikaCoreProperties.TITLE)); } + @Test + public void testMacroDoesNotOverrideWorksheetMetadata() throws Exception { + Map entries = new LinkedHashMap<>(); + entries.put("geogebra.xml", geogebra("classic", "5.0.1.0", "doc-id", + "" + + "")); + entries.put("geogebra_macro.xml", geogebra("other", "9.9.9.9", "macro-id", + "" + + "")); + Metadata metadata = parse(entries).get(0); + assertEquals("Worksheet", metadata.get(TikaCoreProperties.TITLE)); + assertEquals("Ada", metadata.get(TikaCoreProperties.CREATOR)); + assertEquals("today", metadata.get(GeoGebraParser.DATE)); + assertEquals("classic", metadata.get(GeoGebraParser.APP_NAME)); + assertEquals("5.0.1.0", metadata.get(GeoGebraParser.APP_VERSION)); + assertEquals("doc-id", metadata.get(GeoGebraParser.ID)); + assertEquals("Midpoint", metadata.get(GeoGebraParser.TOOL_NAME)); + String content = metadata.get(TikaCoreProperties.TIKA_CONTENT); + assertContains("Body", content); + assertContains("Two points", content); + } + + @Test + public void testTextExpressions() throws Exception { + Map entries = new LinkedHashMap<>(); + entries.put("geogebra.xml", geogebra("classic", "5.0.1.0", "id", "" + + "" + //a dynamic text: literals combined with a value + + "" + + "" + //geometry, not text + + "" + + "" + + "")); + String content = parse(entries).get(0).get(TikaCoreProperties.TIKA_CONTENT); + assertContains("

plain text

", content); + assertContains("

Area =

", content); + assertContains("

ab

", content); + assertNotContained("x^2", content); + } + + @Test + public void testSlidesWithoutStructureJson() throws Exception { + Map entries = new LinkedHashMap<>(); + entries.put("_slide10/geogebra.xml", slide("Tenth")); + entries.put("_slide007/geogebra.xml", slide("Seventh")); + entries.put("_slide2/geogebra.xml", slide("Second")); + Metadata metadata = parse(entries, new GeoGebraParser()).get(0); + assertEquals(3, (int) metadata.getInt(PagedText.N_PAGES)); + //numeric order, leading zeros aside + String content = metadata.get(TikaCoreProperties.TIKA_CONTENT); + assertTrue(content.indexOf("Second") < content.indexOf("Seventh"), content); + assertTrue(content.indexOf("Seventh") < content.indexOf("Tenth"), content); + } + + @Test + public void testStructureJsonSuppliesOrderOnly() throws Exception { + Map entries = new LinkedHashMap<>(); + //a structure.json that is not JSON at all, and one slide it would not list + entries.put("structure.json", "not json"); + entries.put("_slide0/geogebra.xml", slide("Zero")); + entries.put("_slide1/geogebra.xml", slide("One")); + Metadata metadata = parse(entries).get(0); + assertEquals(2, (int) metadata.getInt(PagedText.N_PAGES)); + String content = metadata.get(TikaCoreProperties.TIKA_CONTENT); + assertContains("Zero", content); + assertContains("One", content); + } + + @Test + public void testMalformedSlideDoesNotAbortTheRest() throws Exception { + Map entries = new LinkedHashMap<>(); + entries.put("structure.json", "{\"chapters\":[{\"pages\":[{\"elements\":" + + "[{\"id\":\"_slide0\"},{\"id\":\"_slide1\"}]}]}]}"); + entries.put("_slide0/geogebra.xml", XML_HEAD + "" + + ""); + entries.put("_slide1/geogebra.xml", slide("Fine")); + entries.put("_slide1/geogebra_thumbnail.png", PNG); + List metadataList = parse(entries); + Metadata metadata = metadataList.get(0); + String content = metadata.get(TikaCoreProperties.TIKA_CONTENT); + assertContains("Fine", content); + //the slide div was closed and the failure recorded + assertContains("
", content); + assertNotNull(metadata.get(TikaCoreProperties.TIKA_META_EXCEPTION_EMBEDDED_STREAM)); + //the thumbnail that follows is still emitted + assertNotNull(byName(metadataList, "_slide1/geogebra_thumbnail.png")); + } + + @Test + public void testThumbnailFallsBackToNextSlide() throws Exception { + Map entries = new LinkedHashMap<>(); + entries.put("_slide0/geogebra.xml", slide("Zero")); + entries.put("_slide1/geogebra.xml", slide("One")); + entries.put("_slide1/geogebra_thumbnail.png", PNG); + entries.put("_slide2/geogebra.xml", slide("Two")); + entries.put("_slide2/geogebra_thumbnail.png", PNG); + List metadataList = parse(entries, new GeoGebraParser()); + assertEquals(2, metadataList.size()); + Metadata thumbnail = byName(metadataList, "_slide1/geogebra_thumbnail.png"); + assertEquals(TikaCoreProperties.EmbeddedResourceType.THUMBNAIL.toString(), + thumbnail.get(TikaCoreProperties.EMBEDDED_RESOURCE_TYPE)); + } + + @Test + public void testRootWorksheetAlongsideSlides() throws Exception { + Map entries = new LinkedHashMap<>(); + entries.put("geogebra.xml", geogebra("notes", "5.2.0.0", "root-id", + "")); + entries.put("_slide0/geogebra.xml", slide("Slide text")); + Metadata metadata = parse(entries).get(0); + assertEquals("Root", metadata.get(TikaCoreProperties.TITLE)); + String content = metadata.get(TikaCoreProperties.TIKA_CONTENT); + assertContains("Root text", content); + assertContains("Slide text", content); + } + + @Test + public void testHousekeepingNamesOnlyMatchAtKnownPlaces() throws Exception { + Map entries = new LinkedHashMap<>(); + entries.put("geogebra.xml", geogebra("classic", "5.0.1.0", "id", "")); + //a script hidden in a subdirectory is not the document script + entries.put("dir/geogebra_javascript.js", "alert(1)"); + entries.put("dir/geogebra.xml", ""); + List metadataList = parse(entries); + assertEquals(3, metadataList.size()); + Metadata script = byName(metadataList, "dir/geogebra_javascript.js"); + assertEquals(TikaCoreProperties.EmbeddedResourceType.ATTACHMENT.toString(), + script.get(TikaCoreProperties.EMBEDDED_RESOURCE_TYPE)); + assertNotNull(byName(metadataList, "dir/geogebra.xml")); + } + /** * A crafted slide id can carry more digits than an int holds; sorting the * slide ids must not throw a NumberFormatException out of parse(). */ @Test public void testSlideNumberLargerThanIntParses() throws Exception { + Map entries = new LinkedHashMap<>(); + entries.put("structure.json", "{\"chapters\":[{\"pages\":[{\"elements\":" + + "[{\"id\":\"_slide0\"},{\"id\":\"_slide99999999999\"}]}]}]}"); + //two slides so that sorting actually compares the ids + entries.put("_slide0/geogebra.xml", ""); + entries.put("_slide99999999999/geogebra.xml", ""); + assertEquals("application/vnd.geogebra.slides", + parse(entries).get(0).get(HttpHeaders.CONTENT_TYPE)); + } + + private static String geogebra(String app, String version, String id, String body) { + return XML_HEAD + "" + body + ""; + } + + private static String slide(String text) { + return geogebra("notes", "5.2.0.0", "slide-" + text, "" + + ""); + } + + private List parse(Map entries) throws Exception { + return parse(entries, null); + } + + /** + * Parses an in-memory zip, through detection or, for a container that + * detection would not attribute to GeoGebra (a slides file without + * structure.json), with the parser directly. + */ + private List parse(Map entries, Parser parser) throws Exception { ByteArrayOutputStream bos = new ByteArrayOutputStream(); try (ZipOutputStream zos = new ZipOutputStream(bos)) { - zos.putNextEntry(new ZipEntry("structure.json")); - //two slides so that sorting actually compares the ids - zos.write(("{\"chapters\":[{\"pages\":[{\"elements\":" - + "[{\"id\":\"_slide0\"},{\"id\":\"_slide99999999999\"}]}]}]}") - .getBytes(StandardCharsets.UTF_8)); - zos.closeEntry(); - zos.putNextEntry(new ZipEntry("_slide0/geogebra.xml")); - zos.write("".getBytes(StandardCharsets.UTF_8)); - zos.closeEntry(); - zos.putNextEntry(new ZipEntry("_slide99999999999/geogebra.xml")); - zos.write("".getBytes(StandardCharsets.UTF_8)); - zos.closeEntry(); + for (Map.Entry e : entries.entrySet()) { + zos.putNextEntry(new ZipEntry(e.getKey())); + zos.write(e.getValue().getBytes(StandardCharsets.ISO_8859_1)); + zos.closeEntry(); + } } try (TikaInputStream tis = TikaInputStream.get(bos.toByteArray())) { - List metadataList = - getRecursiveMetadata(tis, new Metadata(), new ParseContext(), false); - assertEquals("application/vnd.geogebra.slides", - metadataList.get(0).get(HttpHeaders.CONTENT_TYPE)); + if (parser == null) { + return getRecursiveMetadata(tis, new Metadata(), new ParseContext(), false); + } + return getRecursiveMetadata(tis, parser, new Metadata(), new ParseContext(), false); + } + } + + private static Metadata byName(List metadataList, String name) { + for (Metadata m : metadataList) { + if (name.equals(m.get(TikaCoreProperties.RESOURCE_NAME_KEY))) { + return m; + } } + return null; } } diff --git a/tika-parsers/tika-parsers-standard/tika-parsers-standard-modules/tika-parser-zip-commons/src/main/java/org/apache/tika/detect/zip/GeoGebraDetector.java b/tika-parsers/tika-parsers-standard/tika-parsers-standard-modules/tika-parser-zip-commons/src/main/java/org/apache/tika/detect/zip/GeoGebraDetector.java index e9fe1e0ff90..cba9559cf4e 100644 --- a/tika-parsers/tika-parsers-standard/tika-parsers-standard-modules/tika-parser-zip-commons/src/main/java/org/apache/tika/detect/zip/GeoGebraDetector.java +++ b/tika-parsers/tika-parsers-standard/tika-parsers-standard-modules/tika-parser-zip-commons/src/main/java/org/apache/tika/detect/zip/GeoGebraDetector.java @@ -46,15 +46,26 @@ public class GeoGebraDetector implements ZipContainerDetector { private static final MediaType GGS = MediaType.application("vnd.geogebra.slides"); private static final MediaType GGT = MediaType.application("vnd.geogebra.tool"); + private static final String GEOGEBRA_XML = "geogebra.xml"; + private static final String MACRO_XML = "geogebra_macro.xml"; + private static final String STRUCTURE_JSON = "structure.json"; + private static final Pattern SLIDE_XML_PATTERN = Pattern.compile("^_slide\\d+/geogebra\\.xml$"); @Override public MediaType detect(ZipFile zip, TikaInputStream tis) throws IOException { + //this runs for every zip Tika sees: look the root names up and only + //walk the entries for a slide when there is a structure.json Names names = new Names(); - Enumeration entries = zip.getEntries(); - while (entries.hasMoreElements()) { - names.update(entries.nextElement().getName()); + names.geogebraXml = zip.getEntry(GEOGEBRA_XML) != null; + names.macroXml = zip.getEntry(MACRO_XML) != null; + names.structureJson = zip.getEntry(STRUCTURE_JSON) != null; + if (names.structureJson) { + Enumeration entries = zip.getEntries(); + while (!names.slideXml && entries.hasMoreElements()) { + names.slideXml = isSlideXml(entries.nextElement().getName()); + } } return names.decide(); } @@ -77,6 +88,10 @@ public MediaType streamingDetectFinal(StreamingDetectContext detectContext) { return names == null ? null : names.decide(); } + private static boolean isSlideXml(String name) { + return SLIDE_XML_PATTERN.matcher(name).matches(); + } + private static class Names { private boolean geogebraXml; private boolean macroXml; @@ -84,13 +99,13 @@ private static class Names { private boolean slideXml; void update(String name) { - if ("geogebra.xml".equals(name)) { + if (GEOGEBRA_XML.equals(name)) { geogebraXml = true; - } else if ("geogebra_macro.xml".equals(name)) { + } else if (MACRO_XML.equals(name)) { macroXml = true; - } else if ("structure.json".equals(name)) { + } else if (STRUCTURE_JSON.equals(name)) { structureJson = true; - } else if (SLIDE_XML_PATTERN.matcher(name).matches()) { + } else if (isSlideXml(name)) { slideXml = true; } } diff --git a/tika-parsers/tika-parsers-standard/tika-parsers-standard-modules/tika-parser-zip-commons/src/main/resources/META-INF/services/org.apache.tika.detect.zip.ZipContainerDetector b/tika-parsers/tika-parsers-standard/tika-parsers-standard-modules/tika-parser-zip-commons/src/main/resources/META-INF/services/org.apache.tika.detect.zip.ZipContainerDetector index cc0c6706d95..3d59fd80c01 100644 --- a/tika-parsers/tika-parsers-standard/tika-parsers-standard-modules/tika-parser-zip-commons/src/main/resources/META-INF/services/org.apache.tika.detect.zip.ZipContainerDetector +++ b/tika-parsers/tika-parsers-standard/tika-parsers-standard-modules/tika-parser-zip-commons/src/main/resources/META-INF/services/org.apache.tika.detect.zip.ZipContainerDetector @@ -14,10 +14,11 @@ # limitations under the License. #These are a special type of detector, and we're currently relying on this #manual file. -org.apache.tika.detect.zip.GeoGebraDetector org.apache.tika.detect.zip.IPADetector org.apache.tika.detect.zip.JarDetector org.apache.tika.detect.zip.KMZDetector org.apache.tika.detect.zip.OpenDocumentDetector org.apache.tika.detect.zip.StarOfficeDetector -org.apache.tika.detect.zip.FrictionlessPackageDetector \ No newline at end of file +org.apache.tika.detect.zip.FrictionlessPackageDetector + +org.apache.tika.detect.zip.GeoGebraDetector From 83692abc1c058e8601623aaae991059c1e07ef33 Mon Sep 17 00:00:00 2001 From: Dominik Schmidt Date: Thu, 27 Aug 2026 12:00:31 +0200 Subject: [PATCH 5/7] TIKA-4831 - rewind after detecting embedded entries, split text runs on any line ending Spool and reset the embedded stream around detection like OpenDocumentParser does; split content text runs on CRLF and CR as well; write test zip entries as UTF-8 text or raw bytes. --- .../tika/parser/geogebra/GeoGebraParser.java | 3 ++ .../parser/geogebra/GeoGebraXMLHandler.java | 2 +- .../parser/geogebra/GeoGebraParserTest.java | 38 +++++++++++++------ 3 files changed, 30 insertions(+), 13 deletions(-) diff --git a/tika-parsers/tika-parsers-standard/tika-parsers-standard-modules/tika-parser-miscoffice-module/src/main/java/org/apache/tika/parser/geogebra/GeoGebraParser.java b/tika-parsers/tika-parsers-standard/tika-parsers-standard-modules/tika-parser-miscoffice-module/src/main/java/org/apache/tika/parser/geogebra/GeoGebraParser.java index 70f31b2d2f8..087ef04cd82 100644 --- a/tika-parsers/tika-parsers-standard/tika-parsers-standard-modules/tika-parser-miscoffice-module/src/main/java/org/apache/tika/parser/geogebra/GeoGebraParser.java +++ b/tika-parsers/tika-parsers-standard/tika-parsers-standard-modules/tika-parser-miscoffice-module/src/main/java/org/apache/tika/parser/geogebra/GeoGebraParser.java @@ -409,8 +409,11 @@ private void handleEmbedded(ZipFile zipFile, ZipArchiveEntry entry, } try (TikaInputStream tisZip = TikaInputStream.get(zipFile.getInputStream(entry))) { if (type == null) { + //spool so the stream can be rewound after detection + tisZip.getFile(); MediaType mediaType = EmbeddedDocumentUtil.getDetector(context) .detect(tisZip, embeddedMetadata, context); + tisZip.reset(); if (mediaType != null) { embeddedMetadata.set(HttpHeaders.CONTENT_TYPE, mediaType.toString()); } diff --git a/tika-parsers/tika-parsers-standard/tika-parsers-standard-modules/tika-parser-miscoffice-module/src/main/java/org/apache/tika/parser/geogebra/GeoGebraXMLHandler.java b/tika-parsers/tika-parsers-standard/tika-parsers-standard-modules/tika-parser-miscoffice-module/src/main/java/org/apache/tika/parser/geogebra/GeoGebraXMLHandler.java index dc282d62db2..f2295b9d791 100644 --- a/tika-parsers/tika-parsers-standard/tika-parsers-standard-modules/tika-parser-miscoffice-module/src/main/java/org/apache/tika/parser/geogebra/GeoGebraXMLHandler.java +++ b/tika-parsers/tika-parsers-standard/tika-parsers-standard-modules/tika-parser-miscoffice-module/src/main/java/org/apache/tika/parser/geogebra/GeoGebraXMLHandler.java @@ -152,7 +152,7 @@ private void handleContent(String val) throws SAXException { for (String text : root.findValuesAsText("text")) { sb.append(text); } - for (String line : sb.toString().split("\n")) { + for (String line : sb.toString().split("\r\n|[\r\n]")) { paragraph(line); } } diff --git a/tika-parsers/tika-parsers-standard/tika-parsers-standard-modules/tika-parser-miscoffice-module/src/test/java/org/apache/tika/parser/geogebra/GeoGebraParserTest.java b/tika-parsers/tika-parsers-standard/tika-parsers-standard-modules/tika-parser-miscoffice-module/src/test/java/org/apache/tika/parser/geogebra/GeoGebraParserTest.java index 25881e2cccd..cd8efe7fa6d 100644 --- a/tika-parsers/tika-parsers-standard/tika-parsers-standard-modules/tika-parser-miscoffice-module/src/test/java/org/apache/tika/parser/geogebra/GeoGebraParserTest.java +++ b/tika-parsers/tika-parsers-standard/tika-parsers-standard-modules/tika-parser-miscoffice-module/src/test/java/org/apache/tika/parser/geogebra/GeoGebraParserTest.java @@ -23,6 +23,7 @@ import java.io.ByteArrayOutputStream; import java.nio.charset.StandardCharsets; +import java.util.Collections; import java.util.LinkedHashMap; import java.util.List; import java.util.Map; @@ -45,7 +46,7 @@ public class GeoGebraParserTest extends TikaTest { private static final String XML_HEAD = "\n"; - private static final String PNG = "\u0089PNG"; + private static final byte[] PNG = {(byte) 0x89, 'P', 'N', 'G', '\r', '\n', 0x1a, '\n'}; @Test public void testGGB() throws Exception { @@ -202,8 +203,9 @@ public void testStructureJsonSuppliesOrderOnly() throws Exception { Metadata metadata = parse(entries).get(0); assertEquals(2, (int) metadata.getInt(PagedText.N_PAGES)); String content = metadata.get(TikaCoreProperties.TIKA_CONTENT); - assertContains("Zero", content); - assertContains("One", content); + //the CRLF line ending of the text run does not leak into the paragraph + assertContains("

Zero

", content); + assertContains("

One

", content); } @Test @@ -214,8 +216,8 @@ public void testMalformedSlideDoesNotAbortTheRest() throws Exception { entries.put("_slide0/geogebra.xml", XML_HEAD + "" + ""); entries.put("_slide1/geogebra.xml", slide("Fine")); - entries.put("_slide1/geogebra_thumbnail.png", PNG); - List metadataList = parse(entries); + List metadataList = parse(entries, + Collections.singletonMap("_slide1/geogebra_thumbnail.png", PNG), null); Metadata metadata = metadataList.get(0); String content = metadata.get(TikaCoreProperties.TIKA_CONTENT); assertContains("Fine", content); @@ -231,10 +233,11 @@ public void testThumbnailFallsBackToNextSlide() throws Exception { Map entries = new LinkedHashMap<>(); entries.put("_slide0/geogebra.xml", slide("Zero")); entries.put("_slide1/geogebra.xml", slide("One")); - entries.put("_slide1/geogebra_thumbnail.png", PNG); entries.put("_slide2/geogebra.xml", slide("Two")); - entries.put("_slide2/geogebra_thumbnail.png", PNG); - List metadataList = parse(entries, new GeoGebraParser()); + Map thumbnails = new LinkedHashMap<>(); + thumbnails.put("_slide1/geogebra_thumbnail.png", PNG); + thumbnails.put("_slide2/geogebra_thumbnail.png", PNG); + List metadataList = parse(entries, thumbnails, new GeoGebraParser()); assertEquals(2, metadataList.size()); Metadata thumbnail = byName(metadataList, "_slide1/geogebra_thumbnail.png"); assertEquals(TikaCoreProperties.EmbeddedResourceType.THUMBNAIL.toString(), @@ -294,24 +297,35 @@ private static String geogebra(String app, String version, String id, String bod private static String slide(String text) { return geogebra("notes", "5.2.0.0", "slide-" + text, "" + ""); + + text + "\\r\\n"}]\"/>"); } private List parse(Map entries) throws Exception { return parse(entries, null); } + private List parse(Map entries, Parser parser) throws Exception { + return parse(entries, Collections.emptyMap(), parser); + } + /** * Parses an in-memory zip, through detection or, for a container that * detection would not attribute to GeoGebra (a slides file without * structure.json), with the parser directly. */ - private List parse(Map entries, Parser parser) throws Exception { + private List parse(Map textEntries, + Map binaryEntries, Parser parser) + throws Exception { ByteArrayOutputStream bos = new ByteArrayOutputStream(); try (ZipOutputStream zos = new ZipOutputStream(bos)) { - for (Map.Entry e : entries.entrySet()) { + for (Map.Entry e : textEntries.entrySet()) { + zos.putNextEntry(new ZipEntry(e.getKey())); + zos.write(e.getValue().getBytes(StandardCharsets.UTF_8)); + zos.closeEntry(); + } + for (Map.Entry e : binaryEntries.entrySet()) { zos.putNextEntry(new ZipEntry(e.getKey())); - zos.write(e.getValue().getBytes(StandardCharsets.ISO_8859_1)); + zos.write(e.getValue()); zos.closeEntry(); } } From dcba2d7d5e1739fc1158577451e6c9e64172d7e7 Mon Sep 17 00:00:00 2001 From: Dominik Schmidt Date: Thu, 27 Aug 2026 12:15:58 +0200 Subject: [PATCH 6/7] TIKA-4831 - real GeoGebra fixtures testGeoGebra_classic.ggb was written by GeoGebra Classic 5.4 (three text objects, one of them dynamic), testGeoGebra_notes.ggs by GeoGebra Notes 5.4 (two pages, thumbnails, an inserted picture). Both get parser tests and replace the synthetic worksheet in TestContainerAwareDetector. --- .../detect/TestContainerAwareDetector.java | 3 +- .../resources/test-documents/testGeoGebra.ggb | Bin 2010 -> 0 bytes .../test-documents/testGeoGebra_classic.ggb | Bin 0 -> 4761 bytes .../test-documents/testGeoGebra_notes.ggs | Bin 0 -> 45929 bytes .../parser/geogebra/GeoGebraParserTest.java | 61 ++++++++++++++++++ .../test-documents/testGeoGebra_classic.ggb | Bin 0 -> 4761 bytes .../test-documents/testGeoGebra_notes.ggs | Bin 0 -> 45929 bytes 7 files changed, 63 insertions(+), 1 deletion(-) delete mode 100644 tika-parsers/tika-parsers-standard/tika-parsers-standard-integration-tests/src/test/resources/test-documents/testGeoGebra.ggb create mode 100644 tika-parsers/tika-parsers-standard/tika-parsers-standard-integration-tests/src/test/resources/test-documents/testGeoGebra_classic.ggb create mode 100644 tika-parsers/tika-parsers-standard/tika-parsers-standard-integration-tests/src/test/resources/test-documents/testGeoGebra_notes.ggs create mode 100644 tika-parsers/tika-parsers-standard/tika-parsers-standard-modules/tika-parser-miscoffice-module/src/test/resources/test-documents/testGeoGebra_classic.ggb create mode 100644 tika-parsers/tika-parsers-standard/tika-parsers-standard-modules/tika-parser-miscoffice-module/src/test/resources/test-documents/testGeoGebra_notes.ggs diff --git a/tika-parsers/tika-parsers-standard/tika-parsers-standard-integration-tests/src/test/java/org/apache/tika/detect/TestContainerAwareDetector.java b/tika-parsers/tika-parsers-standard/tika-parsers-standard-integration-tests/src/test/java/org/apache/tika/detect/TestContainerAwareDetector.java index 2ce165de63c..aa663e5bbfc 100644 --- a/tika-parsers/tika-parsers-standard/tika-parsers-standard-integration-tests/src/test/java/org/apache/tika/detect/TestContainerAwareDetector.java +++ b/tika-parsers/tika-parsers-standard/tika-parsers-standard-integration-tests/src/test/java/org/apache/tika/detect/TestContainerAwareDetector.java @@ -456,7 +456,8 @@ public void testDetectKMZ() throws Exception { @Test public void testDetectGeoGebra() throws Exception { - assertTypeByData("testGeoGebra.ggb", "application/vnd.geogebra.file"); + assertTypeByData("testGeoGebra_classic.ggb", "application/vnd.geogebra.file"); + assertTypeByData("testGeoGebra_notes.ggs", "application/vnd.geogebra.slides"); } @Test diff --git a/tika-parsers/tika-parsers-standard/tika-parsers-standard-integration-tests/src/test/resources/test-documents/testGeoGebra.ggb b/tika-parsers/tika-parsers-standard/tika-parsers-standard-integration-tests/src/test/resources/test-documents/testGeoGebra.ggb deleted file mode 100644 index 087532316a94ac1f7a9a5d58b540cbc2a2079246..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 2010 zcmWIWW@Zs#U|`^2aPk$8<(hj-)dk4&1Y$8DPEXBGPfaRHj4#P3%}vTn%*@d%$V-3C z^P%|p(@Bq#5)vMK{ZhcmbV*0y%t02Ft7^qrOqz`T;>Ra+S)OE#te<(OvQNZFMs(7> zh)S&*@qoUAm#>~rog3^V%n*QLuj`q6u~I<$B+%{6N-Rq(PALYPPfHzyF}69L9_$o%J z8ke7^`pnOYtg;${p^m|J@n`189b$Pl!B;Hwn7V)E&N(@WoIe+xN!WTT$SA6C=@;*7 zO>ZUIdGi;fZl7f=e#-P*X}G5m&*3i|Ijelz^w$9MCSyZPHUo^#Q7J>!vs?Z>#7_&*!uT2_5=U<=&Img<$x?Ik5p+8h+47d}Ph zUiRv#%3(DuC6PfUYbOirZ=2^G$hqtum&BgueCMavMmH%wTxHnQCS&*K{%aQTbz4`O zRli=K`uKfDQ_Ov?mMJ#dZvqnWm!u7uJ(c(xd$wBHmK&XgdCqKr?Im4{rQ*w z4*grB8nw+Q%A_<(@j?&!$N9zjnJSWR~|{J7vm(ra#UrkNXBBZtK|@_ul%0 zqT?j@q&4;pfeOlotvjmjMsG^~6k*lyPWO)C^*bwGiC(c_DL?a|#6sr43#kJ3^ISVJ z3=M9jUp!%R%G_&5#@YpD1s~Flcmfs(>{l|qEtxPimm%QCmxXWCx0_tDJoNpA?x6>D zZk@vCw2QWv^R6~ka}H*%$o$y(W!=SP3(Q58w^y+XxzCx@WvQR&SC_gfJRrtn=gN&S|#_XC zMG_0^I*gS{+BfLWKiR+J$o-}d`tuhjd(^FKIPZ2o_`kg1r3H>sDjxFO4crHISo>e; zHi%>q72^+m@I%Dr=z@cN)&%n{3SHL7yo0wdb56W&+C;4XIF%W1w ze>Yz7nOyeXg#yzuoXrJ#F%bH>}I= zyS8DzmfG974cj7_gZ+N_?T$Mz}YPQR_XN!;OJjmYp^WEa$;;Ui2 z&H@U&%a*8aNH7rZZs5FO_;T%M|BO&BjjP=B-5>Rf>T-565>9Ih4YIvUV6AuU()e;(bFwL c=Q$RHPDCmW@MZ;8B@7I#K07xx^E2#5Q7*10Du7RO)1|Ry%E$9#smO_m;nIN->)`SE;d%? z?q(*IR@P>oj!+MNOKu-0$Nfxcj~_zB{o_dWwaGGZ4975_38I|0 ztMtixhe5QH?VD%hi{U=>SVU7uDh5f$!KuRsPH|<42Z22P&5)w^I5Z5^D2AvbrFhmv z*_5(lW!k{&%e9xx49wlcViw72H8`4ocz{u%dX0id*p-jnUEhbZn(M1j(6gB7Z!^$m z(%W!vlamVjs91(Fx>LRyAU25uacB?9mC5DSFV@ai)CU+tirgsSDf?fk>euz_=^`>q z;)e-Qq&6BgT)7`}Jyk_lJllS)m{jeDsj+^q`lTbPc4qqDtjuWKuvw zB<^H1MBoCK1@|E8PvN(b*{y`8?)fm2F_hd072j=?oU3{W9cPz;mBth@M5M~j%s?m|7(4sM5B*lrCh&BU1Y81(5&0jtDayyRf$qht!e4K3n zC_-HZ0NeZ&fXJ1-)jr-sWZ2iu7Xq=rF&Ruf+m_zE7J zfKX5;r+p^LwFlTm^6;T&Vv2o!O#O@h#f&p!i5KAALw^^7?#o8eJ%L+PI&=Jp38`>l ztDLt}JbL{Kr&eh}UYZTZ*Q-x*%%|u^i>_^CDhiI3Q*{GoPw{M@SR`N$5g6%@gGl#Tm=fTj z?2UHWAVd!3bY3lWkk>BYhCA0PZvNgg+3KcH)Kk;YJ`}%>p_{srJ2t`f(4p2yv9YYU zCx`xM(R4EBYX%qKxMJUVo|n(8R_kRhHpbyM;Jxtmq1ng=fe6?4GWL1S9e4tv;VOj- z!zOM?g8hXzLDF&_Yw}!Y(M?=?>@`F@vZLQ+LwvQ)ionP)2)P&7;+3x0lbvZtO3*FZr0=V!N%UJe#>lo ztW+5;4pkfwYW$gH*1-j`qa@43ANccB@UeQlp78nf`r-Qs^^<%Bq@MiYl995u<@-wQnzlw&#i*KG1E}OAlk*77{@u zGgzDPFFF&00(=#Z&uf}+$4j17w%!UuT8`2>+wUjt9X($|_+Oy=ej?V$1+240lI!-P zWZCpfRzZ)|8hm^)Y~DI5DB2eQd2CnG#|CYc$1^=zj8XtbD_DNadf?27Is!j5@yAk&=xU`NY++a(pe_zwg^7Jt2wI z;;c4_wF;ioEN>Pj5_l8L=thxBSxLAt11haY7KRUhojdnK{;^JQ+ewMn?5gQ>qvp`H z5=V(*FPYcby}(Dy0la(cbDP`vG~t(n!3uOdDTeg}YOJXqCu#L_g85dC@g zETY%%PVA)GrhK3kkM6=1K)aUr#y-2^x{7SE=!akLNGV@r?HxF=V< zaAW7sy$KY&U53n_&flKT5}bdIl;K4$->~wQ;MpHCI5QN`z-G>yCJ=LV3J02O%3a9v znPjvJWj75@T~#M&it+~aT{UjoB8ffSd3i*7u$JQfI4~rItUA{;JT@q;g zW7i6X`{jE80RZ$T|6|ua|L?BVA9Y?7A(U{*3teryu3=)qDF7>6e8#PlCvDqHP3{vi ziqTPC_;O+&7$u>tNX4v~S$7peNhcLJq3AC1d_%bK$u?s4rol{%SI=JeL_kFZsJm?l z_Z)0=K0itC2s-}(#rMOvQTUuHRO!kz-k1l%cqZklHo;guKr-m{Y%#gw z)3;nKe@U`LtbK(G)x1ENekO10IOFcnfc|mmV@jlLx9XtH78)>(Ioc+=7mslgN*Mw! z6v#KpBvH=fo+95CHV1Tjp(kKfo9{wVudXLkZ5@wLIUNYWq-VINsXdlt88wf2_qMY2 zdqzz+u=C`!Ikdhh3JY{DDd^>q=;N77Xi`u<3^Ygbkt$=ivuf3>*KXwSrb~^PdL_T9 zlHN~ZapGKL33>uVHx&D{EOFk3hUULX)5>0j91u5}+GBcUGzR*KK`u=r@*+XDc}aasO@=P;JOWEi2493ix7+!1&uICzg)5$MDQ0DCW4E zSoOB93fZSmx6^+0wcb>TNJWgq9*)TOS}`DcfyARMP0)KCuO3;Yl!rYR5Vz9&I49%7k1DG#YX(pO2OFca|KZ$CEwR8NvDx;Hs&d}MTrykOR8AP?6%e8A-AuUVDn;LPhw5vRG zU*Ry5hJs)H<)6Pja9?XPvyl@R9D@z`?qOiNploX&E>S|RC=uvTgM%d#vW@#ONSNpG zIvtV#E7T)gahZ9}UD{R7RaIljx8k!=ZP)IpQ~xSmGtI`Ho3uik5kc4FWT400(2>ha z9QD*7Ut$=G_x!*+R?f8uU#i$NCiFO{sn;nNbo}N^;bTz%H%l_KRzMDd9TI6OPf9Aj5n0M0{Jpzl zrFjxjJ8Gye8UFXt{V(wtX;cw(IXz~3HE>O>ELa)>R(ssJqc*tjYT+GsyNy9srpJXN zdXZLO%Oo{1jO9h0YgEg#A!7<3ax%So^*3o*w zd{_Cjbt6!_wRvPVHHmS@Z2m*^qgk>ek}$@f=7sRg$(onH+IW85Zy=Y2@f2J%;$D9n zIJb4Y3VnJpv0W2G+r|7%{*N5 zXOdB;F08Po(@cv0lK0MUzpLLNYKhMloBf0WZ-bCbMnP*GYYIW=nHb|=SM{^oYKapq z5iJPAJKVMq(Q>Z_v%gsNMi&_Ap#gN$Gk$hlE zfj2W?u!ZL1VG`2nvY4^zx^)t%M7fkH*-pdt@U7ntqdR2-I}6~@Ha-^XVU-=*y~;8v zPMpOk(UaC8=W1**Zz+|@0wap_0$jtYVBnh8b+ zed7l8tie}$rEebHNzyI*ufNW6r>DFmcP_8;xOP++jtUyH9&FFcx-MfJgu@42esNKS z@3*JobNzZMA;Z()ov0a~?)+T(AWkTHzvagEMJCO|HrY@Mr5&D*ea4)!NKWkvyau`E z`T%dM!Wn^jm2(u2LNv2pSuDCoFC-Q5oWYZ3M8MC_s^<3#7LZLn1WGZp!Z(@jDT~21 zeg-7E-GmTnJg&|Wn9_`ZAqCN%R78=q9GTQpuM);~;G<6g01Al=Lxx;KIY8c&TC>{g z=q=)XDPpnd1-X#(ewdZnCu^U=R9~bdSt^nS%H)Ub;L@X=Mm>mKo1Jjz`@O>Bbh%@Q z{qPMU(?>|cSF$BFAuZyR34c|1y2Nusv8LV*MQJv}1HG?*Lyx`!8Vbe=6`2YP`Z}T$ z6XD!01_il?J;?A0XASEz`umHR!O)|Jph%_UMhCXFU25BFU=*x0)p)Yn_0>M6n^*e#XC>Te!oyT7aHMPJm7B#3$r6r3 zcs_aT4z*lg2oaR}y)8Sh=*9hxq}BZjp!nRKk~OW7(=w)ZYVg*q5HZ4svj-Rlu^iObrmQoqICF+g3vHW43V%Jpr*F8+S%GD z9kfBK7Z7XT|0zye*guVe>phpp>fZigVz*t*^WiY&wu4s`Z=I)hC8L#-_jvfWhZ#Ra zl4hQvXorfFAOsl#;Nr%L62%M%_(8a+fnMm@NLH7LKY9T@4JiZvE>cOHTcj z!lgbuVJ!<$Ch3$Nhc0lWE-_+zH{9w1|vYoDVvs?bL_m{ObY>zuf$me zi}E|`IPNDqfD4i&iSVRK(~sz|CG~eEd1&oTct^c??}E`g-;&6r01vCmeR!J2*Tq`! z4))ZWB;N*fU>=?l+0A^hx11*#>bU(}HX>U=(}|s%a04s`T{>Ly{Ta=XhnTQhC_)1s<3q!$cLu!oDV@Z==91)@G&ELU_cG;g8ek!Ng z3k|EU33z51THXpyWBHy0DJ}X0%5s4bzqaf> z{Yx(%^s#o+#f46jg%qZvuGU7bXMPd+=QMuoFH!81q@vpYlsD!zo_C9*0YO722K?{x z<+mRE?fj1_=HHNir;5J_j=y`I{|EVR#`rhn-^%kBg!s|lApa1ce>47VI)5?D{>J#z Ydo&=JSbt72eqRt80O0>S5CH)H0WB)kHUIzs literal 0 HcmV?d00001 diff --git a/tika-parsers/tika-parsers-standard/tika-parsers-standard-integration-tests/src/test/resources/test-documents/testGeoGebra_notes.ggs b/tika-parsers/tika-parsers-standard/tika-parsers-standard-integration-tests/src/test/resources/test-documents/testGeoGebra_notes.ggs new file mode 100644 index 0000000000000000000000000000000000000000..f83ae6e42a539cc48ebec79acb18c3d500febf79 GIT binary patch literal 45929 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 a right triangle a² + b² = c²

", content); + assertContains("

Hypotenuse c =

", content); + assertContains("

Drag the vertices to explore.

", content); + //the point and segment expressions are geometry, not text + assertNotContained("(0, 0)", content); + assertNotContained("Segment", content); + + assertEquals(2, metadataList.size()); + Metadata script = byName(metadataList, "geogebra_javascript.js"); + assertEquals(TikaCoreProperties.EmbeddedResourceType.MACRO.toString(), + script.get(TikaCoreProperties.EMBEDDED_RESOURCE_TYPE)); + } + + /** + * A two page Notes file written by GeoGebra Notes 5.4 itself, with a + * thumbnail per page and a picture inserted on the second page. + */ + @Test + public void testNotes() throws Exception { + List metadataList = getRecursiveMetadata("testGeoGebra_notes.ggs"); + Metadata metadata = metadataList.get(0); + assertEquals("application/vnd.geogebra.slides", metadata.get(HttpHeaders.CONTENT_TYPE)); + assertEquals("notes", metadata.get(GeoGebraParser.APP_NAME)); + assertEquals("5.4.929.3", metadata.get(GeoGebraParser.APP_VERSION)); + assertEquals(2, (int) metadata.getInt(PagedText.N_PAGES)); + String content = metadata.get(TikaCoreProperties.TIKA_CONTENT); + assertContains("

First page text

", content); + assertContains("

Second page text

", content); + assertTrue(content.indexOf("First page") < content.indexOf("Second page"), content); + + //main document, first thumbnail, two scripts and the picture + assertEquals(5, metadataList.size()); + Metadata thumbnail = byName(metadataList, "_slide0/geogebra_thumbnail.png"); + assertEquals(TikaCoreProperties.EmbeddedResourceType.THUMBNAIL.toString(), + thumbnail.get(TikaCoreProperties.EMBEDDED_RESOURCE_TYPE)); + assertEquals("image/png", thumbnail.get(HttpHeaders.CONTENT_TYPE)); + assertNull(byName(metadataList, "_slide1/geogebra_thumbnail.png")); + Metadata picture = byName(metadataList, + "_slide1/29812c8a66471e456649e2d2cfeee1c6/29812c8a66471e456649e2d2cfeee1c6.png"); + assertEquals(TikaCoreProperties.EmbeddedResourceType.INLINE.toString(), + picture.get(TikaCoreProperties.EMBEDDED_RESOURCE_TYPE)); + assertEquals("image/png", picture.get(HttpHeaders.CONTENT_TYPE)); + assertEquals("2", picture.get(TikaPagedText.PAGE_NUMBERS)); + } + @Test public void testGGT() throws Exception { List metadataList = getRecursiveMetadata("testGeoGebraTool.ggt"); diff --git a/tika-parsers/tika-parsers-standard/tika-parsers-standard-modules/tika-parser-miscoffice-module/src/test/resources/test-documents/testGeoGebra_classic.ggb b/tika-parsers/tika-parsers-standard/tika-parsers-standard-modules/tika-parser-miscoffice-module/src/test/resources/test-documents/testGeoGebra_classic.ggb new file mode 100644 index 0000000000000000000000000000000000000000..f50e3e92e572c7aaa6d7a0ebe13623657e87bdbe GIT binary patch literal 4761 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 Ydo&=JSbt72eqRt80O0>S5CH)H0WB)kHUIzs literal 0 HcmV?d00001 diff --git a/tika-parsers/tika-parsers-standard/tika-parsers-standard-modules/tika-parser-miscoffice-module/src/test/resources/test-documents/testGeoGebra_notes.ggs b/tika-parsers/tika-parsers-standard/tika-parsers-standard-modules/tika-parser-miscoffice-module/src/test/resources/test-documents/testGeoGebra_notes.ggs new file mode 100644 index 0000000000000000000000000000000000000000..f83ae6e42a539cc48ebec79acb18c3d500febf79 GIT binary patch literal 45929 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 Date: Thu, 27 Aug 2026 13:56:02 +0200 Subject: [PATCH 7/7] TIKA-4831 - cap the content JSON that is parsed, assert test resources exist --- .../apache/tika/parser/geogebra/GeoGebraXMLHandler.java | 9 ++++++++- .../apache/tika/detect/zip/GeoGebraDetectionTest.java | 7 +++++-- 2 files changed, 13 insertions(+), 3 deletions(-) diff --git a/tika-parsers/tika-parsers-standard/tika-parsers-standard-modules/tika-parser-miscoffice-module/src/main/java/org/apache/tika/parser/geogebra/GeoGebraXMLHandler.java b/tika-parsers/tika-parsers-standard/tika-parsers-standard-modules/tika-parser-miscoffice-module/src/main/java/org/apache/tika/parser/geogebra/GeoGebraXMLHandler.java index f2295b9d791..b3d579fa054 100644 --- a/tika-parsers/tika-parsers-standard/tika-parsers-standard-modules/tika-parser-miscoffice-module/src/main/java/org/apache/tika/parser/geogebra/GeoGebraXMLHandler.java +++ b/tika-parsers/tika-parsers-standard/tika-parsers-standard-modules/tika-parser-miscoffice-module/src/main/java/org/apache/tika/parser/geogebra/GeoGebraXMLHandler.java @@ -49,6 +49,12 @@ class GeoGebraXMLHandler extends DefaultHandler { */ private static final Pattern STRING_LITERAL = Pattern.compile("\"([^\"]*)\""); + /** + * Longest content JSON that is parsed; a real inline text, table or mind + * map is a few kilobytes, anything far beyond that is not worth a tree. + */ + private static final int MAX_CONTENT_LENGTH = 1024 * 1024; + private final XHTMLContentHandler xhtml; private final Metadata metadata; private final boolean documentMetadata; @@ -138,7 +144,8 @@ private void handleContent(String val) throws SAXException { return; } String trimmed = val.trim(); - if (trimmed.isEmpty() || (trimmed.charAt(0) != '[' && trimmed.charAt(0) != '{')) { + if (trimmed.isEmpty() || trimmed.length() > MAX_CONTENT_LENGTH + || (trimmed.charAt(0) != '[' && trimmed.charAt(0) != '{')) { //not a JSON document; a plain string carries no text runs return; } diff --git a/tika-parsers/tika-parsers-standard/tika-parsers-standard-modules/tika-parser-zip-commons/src/test/java/org/apache/tika/detect/zip/GeoGebraDetectionTest.java b/tika-parsers/tika-parsers-standard/tika-parsers-standard-modules/tika-parser-zip-commons/src/test/java/org/apache/tika/detect/zip/GeoGebraDetectionTest.java index d7140aa815b..13dd026680a 100644 --- a/tika-parsers/tika-parsers-standard/tika-parsers-standard-modules/tika-parser-zip-commons/src/test/java/org/apache/tika/detect/zip/GeoGebraDetectionTest.java +++ b/tika-parsers/tika-parsers-standard/tika-parsers-standard-modules/tika-parser-zip-commons/src/test/java/org/apache/tika/detect/zip/GeoGebraDetectionTest.java @@ -17,7 +17,9 @@ package org.apache.tika.detect.zip; import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNotNull; +import java.io.InputStream; import java.util.List; import org.junit.jupiter.api.Test; @@ -36,8 +38,9 @@ public class GeoGebraDetectionTest extends TikaTest { private List getRecursiveMetadataWithoutName(String fileName) throws Exception { - try (TikaInputStream tis = TikaInputStream.get( - getClass().getResourceAsStream("/test-documents/" + fileName), new Metadata())) { + InputStream is = getClass().getResourceAsStream("/test-documents/" + fileName); + assertNotNull(is, "missing test resource " + fileName); + try (TikaInputStream tis = TikaInputStream.get(is, new Metadata())) { return getRecursiveMetadata(tis, AUTO_DETECT_PARSER, new Metadata(), new ParseContext(), true); }