Skip to content

TIKA-4831: Add content-based detection and a parser for GeoGebra files (ggb, ggs, ggt) - #3044

Merged
tballison merged 8 commits into
apache:mainfrom
dschmidt:feat/geogebra
Aug 27, 2026
Merged

TIKA-4831: Add content-based detection and a parser for GeoGebra files (ggb, ggs, ggt)#3044
tballison merged 8 commits into
apache:mainfrom
dschmidt:feat/geogebra

Conversation

@dschmidt

Copy link
Copy Markdown
Contributor

Adds detection and parsing support for the GeoGebra file formats.

Issue: https://issues.apache.org/jira/browse/TIKA-4831

Detection

  • application/vnd.geogebra.file (*.ggb) and application/vnd.geogebra.tool (*.ggt) are now sub-class-of application/zip, so the filename hint survives magic detection instead of being discarded in favor of plain application/zip
  • new mime types: application/vnd.geogebra.slides (*.ggs, zip-based) and application/vnd.geogebra.pinboard (*.ggp, JSON-based)
  • a new GeoGebraDetector (zip container detector, ZipFile and streaming mode) recognizes the formats without a filename by their well-known entries: geogebra.xml (worksheet), structure.json + _slideN/geogebra.xml (Notes/Slides), geogebra_macro.xml (tool). Since a worksheet with macros contains both geogebra.xml and geogebra_macro.xml, the decision is made after all entry names have been seen
  • ZipParser.ZIP_SPECIALIZATIONS is kept in sync with the new registry entries

Parser

A new GeoGebraParser (miscoffice module) for ggb/ggs/ggt:

  • metadata: construction title/author/date become dc:title/dc:creator/geogebra:date, plus geogebra:appName, geogebra:appVersion, geogebra:formatVersion, geogebra:id; Notes/Slides set xmpTPg:NPages
  • content: user-visible text as XHTML paragraphs - string-literal expression elements of text objects, rich-text content runs (JSON), element captions, macro names and help texts; Notes/Slides emit one div per slide in structure.json order
  • the representative rendering (geogebra_thumbnail.png at the root, or the first slide's thumbnail) is emitted as an embedded document marked embeddedResourceType=THUMBNAIL, following the existing convention in the OOXML, ODF and iWork parsers, so /unpack/all 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

XML is parsed through XMLReaderUtils.parseSAX like the other parsers in the module; structure.json and rich-text runs use Jackson (new jackson-databind dependency in the miscoffice module, version managed by the existing BOM import).

Testing

  • new unit tests: GeoGebraDetectionTest (zip-commons) and GeoGebraParserTest (miscoffice) with crafted ggb/ggs/ggt fixtures; the ggs fixture deliberately orders _slide1 before _slide0 in structure.json to pin the ordering behavior
  • full test suites of tika-core, tika-parser-zip-commons, tika-parser-miscoffice-module and tika-parser-pkg-module pass, plus CompositeZipContainerDetectorTest in the integration tests
  • verified end-to-end against a real-world GeoGebra Notes file: detected as vnd.geogebra.slides, text extracted, thumbnail emitted with the THUMBNAIL marker

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Adds first-class GeoGebra support to Apache Tika by introducing content-based detection for GeoGebra zip containers and a dedicated parser that extracts both metadata and user-visible text (plus a representative thumbnail as an embedded resource).

Changes:

  • Added a GeoGebraDetector (zip-commons) to recognize ggb/ggs/ggt by zip entry names (including streaming detection).
  • Added a GeoGebraParser + GeoGebraXMLHandler (miscoffice) to extract GeoGebra metadata/text and emit the thumbnail as an embedded document.
  • Updated mime registry, zip specializations, module dependencies, and tests to cover new media types and parsing behavior.

Reviewed changes

Copilot reviewed 11 out of 17 changed files in this pull request and generated 3 comments.

Show a summary per file
File Description
tika-parsers/tika-parsers-standard/tika-parsers-standard-modules/tika-parser-zip-commons/src/test/java/org/apache/tika/detect/zip/GeoGebraDetectionTest.java Adds detection tests for GeoGebra zip-based formats.
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 Registers GeoGebraDetector via ServiceLoader.
tika-parsers/tika-parsers-standard/tika-parsers-standard-modules/tika-parser-zip-commons/src/main/java/org/apache/tika/detect/zip/GeoGebraDetector.java Implements content-based container detection for GeoGebra zip formats.
tika-parsers/tika-parsers-standard/tika-parsers-standard-modules/tika-parser-pkg-module/src/main/java/org/apache/tika/parser/pkg/ZipParser.java Adds GeoGebra media types to zip specialization set.
tika-parsers/tika-parsers-standard/tika-parsers-standard-modules/tika-parser-miscoffice-module/src/test/java/org/apache/tika/parser/geogebra/GeoGebraParserTest.java Adds parser tests for metadata, slide ordering, and thumbnail embedding behavior.
tika-parsers/tika-parsers-standard/tika-parsers-standard-modules/tika-parser-miscoffice-module/src/main/java/org/apache/tika/parser/geogebra/GeoGebraXMLHandler.java SAX handler for extracting metadata/text from GeoGebra XML + inline rich text JSON.
tika-parsers/tika-parsers-standard/tika-parsers-standard-modules/tika-parser-miscoffice-module/src/main/java/org/apache/tika/parser/geogebra/GeoGebraParser.java New parser for ggb/ggs/ggt, including embedded thumbnail + embedded resources.
tika-parsers/tika-parsers-standard/tika-parsers-standard-modules/tika-parser-miscoffice-module/pom.xml Adds jackson-databind dependency for parsing structure.json and rich-text runs.
tika-core/src/test/java/org/apache/tika/TikaDetectionTest.java Extends extension-based detection coverage for .ggp and .ggs.
tika-core/src/main/resources/org/apache/tika/mime/tika-mimetypes.xml Adds .ggp mime type and marks GeoGebra zip-based types as sub-class-of application/zip.
CHANGES.txt Documents new GeoGebra detection/parsing support (TIKA-4831).

💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 11 out of 17 changed files in this pull request and generated 1 comment.

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 13 out of 19 changed files in this pull request and generated 2 comments.

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.
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.

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 13 out of 19 changed files in this pull request and generated no new comments.

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.

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 13 out of 19 changed files in this pull request and generated no new comments.

Suppressed comments (1)

Previously missed (1) — in code that hasn't changed since the last review.

tika-parsers/tika-parsers-standard/tika-parsers-standard-modules/tika-parser-zip-commons/src/main/java/org/apache/tika/detect/zip/GeoGebraDetector.java:71

  • In streaming detection mode, this detector always returns null from streamingDetectUpdate and waits for streamingDetectFinal to decide. Once both structure.json and a _slideN/geogebra.xml have been seen, the result is unambiguously GeoGebra Slides (GGS) and cannot be overturned by later entries, so returning GGS early would let DefaultZipContainerDetector/StreamingZipContainerDetector stop scanning the rest of a large ZIP.
    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;

@tballison

Copy link
Copy Markdown
Contributor

Let me know what your agent thinks of my agent's input.

 Verdict: still spiking (contract findings). Base 2b70202aae, head 9d22e1a186. Shape: full (6 Opus agents + direction). Direction: proceed — the cheap alternative
  (registry-only + ZipParser) yields zero text because all GeoGebra text lives in XML attributes and Tika's XML parsers emit character data only; miscoffice is the right
  module. CI green; touched modules build and pass locally; hygiene grep clean; schema JSON hand-edits byte-match the generator.

  Contract (maintainer decisions / interface-shaping)

  1. Metadata key casing — geogebra:appName/appVersion/formatVersion are Tika-coined and must be kebab-case per metadata-changes-4x.adoc:162 ("names Tika coined are
     kebab-cased; verbatim names keep spelling"). toolName, id, date are verbatim attributes → fine. Public API freezes at 4.1.0; no 4.x→4.x migration path exists.
  2. Component name — @TikaComponent without name yields geo-gebra-parser (KebabCaseConverter). Set name = "geogebra-parser" now; it's public config surface.
  3. Macro XML clobbers worksheet metadata (GeoGebraParser.java:231-240, handler :63-75) — both XMLs get the same Metadata; the macro's root <geogebra> and its nested
     <construction> overwrite app/version/format and dc:title/dc:creator. The fixture hides it (identical versions, blank macro construction attrs). Reached independently
     by 4 lanes. Fix: metadata only from the primary XML; construction only at depth 1.
  4. handleExpression drops the most common text-object form (GeoGebraXMLHandler.java:106-112) — "Area = "+a is dropped entirely; "a"+"b" emits a"+"b. Extract each quoted
     segment instead of requiring a single literal. The \" unescape is unverified against the format.
  5. Detector at head of the SPI file — first-non-null, no sorting; a planted root geogebra.xml now outranks JAR/APK/ODF/StarOffice detection. Every prior detector was
     appended. Move to end; also swap the full central-directory walk for getEntry() lookups (enumerate only when structure.json exists) — this detector now runs on every
     zip Tika sees.
  6. No degraded-input handling, unlike ZipParser — (a) unguarded getInputStream at :273/:296/:333: one AES/unsupported entry aborts the whole parse (canReadEntryData +
     recordEmbeddedStreamException convention); (b) ZipFileHelper.open failure has no fallback — and since .ggb is now a zip subclass, any bytes named x.ggb route here and
     throw a bare IOException where 4.0.0 gave application/zip; (c) a malformed slide XML aborts remaining slides + thumbnail + pictures and leaves <div class="slide">
     unclosed (EpubParser's per-part catch/drain is the module convention).
  7. .ggs with missing/corrupt structure.json → zero output, no error (:205-208, :319-325) — slides path is gated on structure.json, then the worksheet path skips every
     _slideN/geogebra.xml by basename. structure.json should supply order only.
  8. geogebra_javascript.js is user code, dropped as "housekeeping" — should be emitted as EmbeddedResourceType.MACRO. Related: INLINE on every other entry is wrong for
     non-images (VLM/OCR-encode parsers gate on INLINE); use ATTACHMENT or leave unset. Also basename skip-matching lets dir/geogebra_javascript.js hide anything.
  9. Streaming detection path entirely untested — delete streamingDetectFinal's body and all tests pass. Add a .ggb to
     tika-parsers-standard-integration-tests/test-documents — TestContainerAwareDetector:117 auto-exercises streaming for every zip specialization. More broadly all three
     fixtures are synthetic; one real GeoGebra file would have caught #4.
  10. Upgrade behavior change under-documented — 4.0.0 typed real .ggb as application/zip (glob lost to magic) with every entry as an attachment; now 1 attachment and no
      zip:* metadata. CHANGES should say so, and should drop the false claim that the detector identifies .ggp.

  Edge-case

  - readTree on structure.json unbounded (peers use BoundedInputStream); per-<content> readTree with no shape pre-check builds an exception per non-JSON value, and blank
    output disengages the write limit. Guard on leading [/{; consider streaming jackson-core (cad-module precedent) instead of databind.
  - First slide without a thumbnail → no thumbnail at all (:260-264; others filtered by basename).
  - Slide sort: leading zeros misorder (_slide007 after _slide10); sort runs before the early return.
  - structure.json + _slideN/ + root geogebra.xml → root XML silently never parsed nor emitted.
  - Hard-coded image/png on thumbnail: redundant (glob) and a lie for non-PNG bytes; the test assertion on it is vacuous.
  - Untested: malformed-structure.json fallback, slides-missing-from-JSON append, unquoted-expression skip, cmdName fallback, nested/multi-line content, BAG cardinality,
    <div class="slide">, first-slide-only metadata (fixture values identical).
  - No PageAnchoring on _slideN/ pictures despite setting N_PAGES (OpenDocumentParser in the same module does it).

  Hygiene (one line each)

  Private isBlank duplicates StringUtils.isBlank (used 3× in module) · two static ObjectMappers · inZip/numericallySorted misnamed · handleThumbnail/handleOtherEntries
  duplicate ~10 lines · {@code &lt;geogebra&gt;} renders entities literally · javadoc claims "ink notes" (penstrokes carry no text) and omits macro help · 3-line sort
  comment and test comments that narrate asserts / echo the Copilot exchange · regen.sh should produce the schema JSONs (result matches anyway) · CHANGES ~2× target length
  — proposed: "New GeoGebraParser for *.ggb/*.ggs/*.ggt: geogebra:* metadata, text, thumbnail as THUMBNAIL embedded doc. Previously typed application/zip with every entry
  as an attachment. *.ggs/*.ggp are new mime types; *.ggp is glob-only (TIKA-4831)."

…ventions

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.
@dschmidt

Copy link
Copy Markdown
Contributor Author

Thanks, that was a genuinely useful pass. I've gone through all of it.

The two things worth calling out: the metadata keys are now kebab-cased (geogebra:app-name etc., the verbatim attribute names like toolName stay), and the component is explicitly named geogebra-parser. Both change public surface, so shout if you'd rather have something else before 4.1.0.

On the parser, the real bugs were the macro XML overwriting the worksheet metadata and the text expressions dropping anything that wasn't a single literal. Both fixed; for the latter I checked GeoGebra's source and strings are written between plain quotes with no escaping at all, so the \" handling was simply wrong and is gone. The rest follows the module conventions now: unreadable or malformed parts are recorded and skipped instead of aborting, structure.json only orders the slides that actually exist, the script is a MACRO, pictures are INLINE with their page number, and the detector uses getEntry() lookups and sits at the end of the SPI file.

One thing I left alone: ZipFileHelper.open failing still throws, same as OpenDocumentParser next door. A non-zip named .ggb isn't detected as GeoGebra anyway, so it only bites when the parser is called directly.

Streaming detection is now covered by TestContainerAwareDetector. The fixtures are still synthetic though; I'm trying to get hold of a real GeoGebra file with a suitable licence and will add it when I have one.

CHANGES is rewritten along your suggestion. The smaller items (bounded JSON reads, thumbnail fallback, leading zeros in slide ids, the hygiene list) are all in, each with a test.

@THausherr
THausherr requested a lite review from Copilot August 27, 2026 09:38

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 14 out of 21 changed files in this pull request and generated 4 comments.

…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.
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.
@dschmidt

Copy link
Copy Markdown
Contributor Author

Real fixtures are in now: a worksheet written by GeoGebra Classic 5.4 (three text objects, one of them the dynamic "Hypotenuse c = " + c kind that item 4 was about) and a two-page Notes file with thumbnails and an inserted picture. Both were produced with the GeoGebra web apps, so no licensing strings attached. They replace the synthetic .ggb in TestContainerAwareDetector and get their own parser tests.

@THausherr
THausherr requested a lite review from Copilot August 27, 2026 10:36

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 14 out of 24 changed files in this pull request and generated 5 comments.

@tballison

Copy link
Copy Markdown
Contributor

Will merge on green ci.

@dschmidt

Copy link
Copy Markdown
Contributor Author

Great, thanks!

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants