[ 
https://issues.apache.org/jira/browse/TIKA-4831?page=com.atlassian.jira.plugin.system.issuetabpanels:comment-tabpanel&focusedCommentId=18107069#comment-18107069
 ] 

ASF GitHub Bot commented on TIKA-4831:
--------------------------------------

Copilot commented on code in PR #3044:
URL: https://github.com/apache/tika/pull/3044#discussion_r3838152113


##########
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,332 @@
+/*
+ * 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;
+
+/**
+ * Parser for the zip-based GeoGebra formats: worksheets (*.ggb), Notes/Slides
+ * (*.ggs) and tools (*.ggt).
+ * <p>
+ * 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 <div class="slide">}.
+ * <p>
+ * 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<MediaType> 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<String> 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<MediaType> 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 = ZipFile.builder().setFile(tis.getFile()).get();
+            tis.setOpenContainer(zipFile);
+        }
+
+        XHTMLContentHandler xhtml = new XHTMLContentHandler(handler, metadata, 
context);
+        xhtml.startDocument();
+        List<String> 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<String> getSlideIds(ZipFile zipFile) {
+        Set<String> inZip = new LinkedHashSet<>();
+        List<String> numericallySorted = new ArrayList<>();
+        Enumeration<ZipArchiveEntry> 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();
+        }
+        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 (numericallySorted.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 {
+        ZipArchiveEntry contentXml = zipFile.getEntry(GEOGEBRA_XML);
+        if (contentXml == null) {
+            contentXml = zipFile.getEntry(MACRO_XML);
+        }
+        if (contentXml != null) {
+            parseGeoGebraXml(zipFile, contentXml, xhtml, metadata, context);
+        }

Review Comment:
   In worksheets that include macros, GeoGebra containers can contain both 
geogebra.xml and geogebra_macro.xml (as noted in the detector’s javadoc). 
parseWorksheet currently parses only one of them, so macro names/help text and 
GeoGebraParser.TOOL_NAME metadata may be missed when both entries exist. 
Consider parsing geogebra.xml (for document metadata) and then also parsing 
geogebra_macro.xml (for macro text/metadata) when present.



##########
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,332 @@
+/*
+ * 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;
+
+/**
+ * Parser for the zip-based GeoGebra formats: worksheets (*.ggb), Notes/Slides
+ * (*.ggs) and tools (*.ggt).
+ * <p>
+ * 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 <div class="slide">}.
+ * <p>
+ * 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<MediaType> 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<String> 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<MediaType> 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 = ZipFile.builder().setFile(tis.getFile()).get();
+            tis.setOpenContainer(zipFile);
+        }
+
+        XHTMLContentHandler xhtml = new XHTMLContentHandler(handler, metadata, 
context);
+        xhtml.startDocument();
+        List<String> 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<String> getSlideIds(ZipFile zipFile) {
+        Set<String> inZip = new LinkedHashSet<>();
+        List<String> numericallySorted = new ArrayList<>();
+        Enumeration<ZipArchiveEntry> 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();
+        }
+        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 (numericallySorted.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 {
+        ZipArchiveEntry contentXml = zipFile.getEntry(GEOGEBRA_XML);
+        if (contentXml == null) {
+            contentXml = zipFile.getEntry(MACRO_XML);
+        }
+        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<String> 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<ZipArchiveEntry> 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, 
basename);
+            embeddedMetadata.set(TikaCoreProperties.INTERNAL_PATH, name);

Review Comment:
   handleOtherEntries sets RESOURCE_NAME_KEY to the basename only. If a 
GeoGebra container has multiple embedded files with the same basename in 
different directories (e.g., per-slide subdirectories), this can cause 
ambiguous/duplicated embedded resource names for consumers. Setting 
RESOURCE_NAME_KEY to the full zip entry name keeps it unique and matches how 
thumbnails are labeled in this parser.



##########
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,54 @@
+/*
+ * 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.metadata.HttpHeaders;
+import org.apache.tika.metadata.Metadata;
+
+/**
+ * Test case for detecting the zip-based GeoGebra formats by their contents.
+ */
+public class GeoGebraDetectionTest extends TikaTest {
+
+    @Test
+    public void testGGBDetection() throws Exception {
+        List<Metadata> metadataList = getRecursiveMetadata("testGeoGebra.ggb");
+        assertEquals("application/vnd.geogebra.file",
+                metadataList.get(0).get(HttpHeaders.CONTENT_TYPE));
+    }
+
+    @Test
+    public void testGGSDetection() throws Exception {
+        List<Metadata> metadataList = 
getRecursiveMetadata("testGeoGebraSlides.ggs");
+        assertEquals("application/vnd.geogebra.slides",
+                metadataList.get(0).get(HttpHeaders.CONTENT_TYPE));
+    }
+
+    @Test
+    public void testGGTDetection() throws Exception {
+        List<Metadata> metadataList = 
getRecursiveMetadata("testGeoGebraTool.ggt");
+        assertEquals("application/vnd.geogebra.tool",
+                metadataList.get(0).get(HttpHeaders.CONTENT_TYPE));
+    }

Review Comment:
   These tests currently use getRecursiveMetadata(String), which sets 
RESOURCE_NAME_KEY from the filename (TikaTest.getResourceAsStream), so 
detection can succeed purely via the *.ggb/*.ggs/*.ggt extension. That doesn’t 
exercise the content-based GeoGebraDetector behavior described in the class 
javadoc. Consider constructing the TikaInputStream with empty Metadata (no 
resource name) so detection must rely on zip entry names.





> Add content-based detection and a parser for GeoGebra files (ggb, ggs, ggt)
> ---------------------------------------------------------------------------
>
>                 Key: TIKA-4831
>                 URL: https://issues.apache.org/jira/browse/TIKA-4831
>             Project: Tika
>          Issue Type: New Feature
>            Reporter: Dominik Schmidt
>            Priority: Major
>
> GeoGebra files are currently only recognized by file extension. The mime
> registry has glob-only entries for {{application/vnd.geogebra.file}} 
> ({{*.ggb}})
> and {{application/vnd.geogebra.tool}} ({{*.ggt}}), but both formats are zip
> containers and the entries are not declared as sub-classes of
> {{application/zip}}. As a result, as soon as content is available, magic
> detection returns {{application/zip}} and the filename hint is discarded in
> {{MimeTypes.applyHint()}} - even when the resource name is known. Content-only
> detection (no filename) has no way to identify the formats at all, and the
> newer GeoGebra formats {{*.ggs}} (Notes/Slides) and {{*.ggp}} (Pinboard) are
> missing from the registry entirely.
> There is also no parser for any of the GeoGebra formats: files fall through to
> the generic {{PackageParser}}, which extracts the zip entries but produces no
> document metadata and no usable text (the {{geogebra.xml}} construction is
> emitted as raw XML through the XML parser).
> Proposed improvement:
> * mime registry: declare {{application/vnd.geogebra.file}} and
>   {{application/vnd.geogebra.tool}} as {{sub-class-of application/zip}}; add
>   {{application/vnd.geogebra.slides}} ({{*.ggs}}, zip-based) and
>   {{application/vnd.geogebra.pinboard}} ({{*.ggp}}, JSON-based)
> * a {{ZipContainerDetector}} that identifies the formats without a filename by
>   their well-known entries: {{geogebra.xml}} (worksheet), {{structure.json}}
>   plus {{_slideN/geogebra.xml}} (Notes/Slides), {{geogebra_macro.xml}} (tool);
>   a worksheet with macros contains both {{geogebra.xml}} and
>   {{geogebra_macro.xml}}, so the decision must be made after all entry names
>   have been seen
> * a {{GeoGebraParser}} for ggb/ggs/ggt that extracts the construction metadata
>   (title, author, date) and application name/version, emits the user-visible
>   text (text objects, rich-text notes, captions, macro names/help) as XHTML,
>   and emits the embedded {{geogebra_thumbnail.png}} (root, or the first 
> slide's
>   for Notes/Slides) as an embedded document marked
>   {{embeddedResourceType=THUMBNAIL}}, following the existing convention in the
>   OOXML, ODF and iWork parsers, so downstream consumers of {{/unpack/all}}
>   sidecars can identify the preview image
> Use case: file sync/share servers (e.g. OpenCloud) use Tika for content
> extraction and for serving embedded preview images; with the THUMBNAIL marker
> they can select the representative preview of a GeoGebra file the same way as
> for Office documents.
> Pull request to follow.



--
This message was sent by Atlassian Jira
(v8.20.10#820010)

Reply via email to