This is an automated email from the ASF dual-hosted git repository.

tballison pushed a commit to branch 3x-port-sax-ooxml
in repository https://gitbox.apache.org/repos/asf/tika.git

commit c11e0377784e3eaaece27e0d9634635dbd3570d1
Author: tallison <[email protected]>
AuthorDate: Wed Jul 8 11:36:36 2026 -0400

    First steps in porting 4.x sax ooxml -> 3.x - checkpoint
---
 .../microsoft/ooxml/OOXMLExtractorFactory.java     |  17 +
 .../microsoft/ooxml/TikaSheetContentsHandler.java  |  36 ++
 .../microsoft/ooxml/TikaSheetXMLHandler.java       | 398 ++++++++++++++++++++
 .../microsoft/ooxml/TikaXSSFBCommentsTable.java    | 137 +++++++
 .../ooxml/TikaXSSFBSharedStringsTable.java         | 159 ++++++++
 .../microsoft/ooxml/VSDXExtractorDecorator.java    | 177 +++++++++
 .../ooxml/XSSFBExcelExtractorDecorator.java        | 106 +++---
 .../parser/microsoft/ooxml/XSSFCommentsShim.java   | 187 +++++++++
 .../ooxml/XSSFExcelExtractorDecorator.java         | 418 ++++++++++++++-------
 .../microsoft/ooxml/XSSFSharedStringsShim.java     | 156 ++++++++
 .../parser/microsoft/ooxml/XSSFStylesShim.java     | 146 +++++++
 .../parser/microsoft/ooxml/VSDXParserTest.java     |  41 ++
 12 files changed, 1800 insertions(+), 178 deletions(-)

diff --git 
a/tika-parsers/tika-parsers-standard/tika-parsers-standard-modules/tika-parser-microsoft-module/src/main/java/org/apache/tika/parser/microsoft/ooxml/OOXMLExtractorFactory.java
 
b/tika-parsers/tika-parsers-standard/tika-parsers-standard-modules/tika-parser-microsoft-module/src/main/java/org/apache/tika/parser/microsoft/ooxml/OOXMLExtractorFactory.java
index 7e1e3c0fc6..4ddccbe158 100644
--- 
a/tika-parsers/tika-parsers-standard/tika-parsers-standard-modules/tika-parser-microsoft-module/src/main/java/org/apache/tika/parser/microsoft/ooxml/OOXMLExtractorFactory.java
+++ 
b/tika-parsers/tika-parsers-standard/tika-parsers-standard-modules/tika-parser-microsoft-module/src/main/java/org/apache/tika/parser/microsoft/ooxml/OOXMLExtractorFactory.java
@@ -21,6 +21,7 @@ import java.io.IOException;
 import java.io.InputStream;
 import java.nio.file.Files;
 import java.util.Locale;
+import java.util.Set;
 
 import org.apache.poi.extractor.ExtractorFactory;
 import org.apache.poi.ooxml.POIXMLDocument;
@@ -182,6 +183,10 @@ public class OOXMLExtractorFactory {
                         
XSLFEventBasedPowerPointExtractor.class.getCanonicalName());
             } else if (poiExtractor instanceof XPSTextExtractor) {
                 extractor = new XPSExtractorDecorator(context, poiExtractor);
+            } else if (isVisioType(type)) {
+                //SAX-based .vsdx extractor; the XDGF-based poiExtractor is 
used only for its
+                //OPCPackage (VSDXExtractorDecorator reads the package 
directly)
+                extractor = new VSDXExtractorDecorator(context, poiExtractor);
             } else if (document == null) {
                 throw new TikaException(
                         "Expecting UserModel based POI OOXML extractor with a 
document, but none" +
@@ -295,5 +300,17 @@ public class OOXMLExtractorFactory {
         return null;
     }
 
+    private static final Set<String> VISIO_SUBTYPES = Set.of(
+            "vnd.ms-visio.drawing",
+            "vnd.ms-visio.drawing.macroenabled.12",
+            "vnd.ms-visio.stencil",
+            "vnd.ms-visio.stencil.macroenabled.12",
+            "vnd.ms-visio.template",
+            "vnd.ms-visio.template.macroenabled.12"
+    );
+
+    private static boolean isVisioType(MediaType type) {
+        return type != null && VISIO_SUBTYPES.contains(type.getSubtype());
+    }
 
 }
diff --git 
a/tika-parsers/tika-parsers-standard/tika-parsers-standard-modules/tika-parser-microsoft-module/src/main/java/org/apache/tika/parser/microsoft/ooxml/TikaSheetContentsHandler.java
 
b/tika-parsers/tika-parsers-standard/tika-parsers-standard-modules/tika-parser-microsoft-module/src/main/java/org/apache/tika/parser/microsoft/ooxml/TikaSheetContentsHandler.java
new file mode 100644
index 0000000000..44173ec322
--- /dev/null
+++ 
b/tika-parsers/tika-parsers-standard/tika-parsers-standard-modules/tika-parser-microsoft-module/src/main/java/org/apache/tika/parser/microsoft/ooxml/TikaSheetContentsHandler.java
@@ -0,0 +1,36 @@
+/*
+ * 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.microsoft.ooxml;
+
+/**
+ * Sheet contents handler that uses {@link XSSFCommentsShim.CommentData}
+ * instead of POI's XMLBeans-dependent {@code XSSFComment}.
+ */
+interface TikaSheetContentsHandler {
+
+    void startRow(int rowNum);
+
+    void endRow(int rowNum);
+
+    void cell(String cellRef, String formattedValue, 
XSSFCommentsShim.CommentData comment);
+
+    default void headerFooter(String text, boolean isHeader, String tagName) {
+    }
+
+    default void endSheet() {
+    }
+}
diff --git 
a/tika-parsers/tika-parsers-standard/tika-parsers-standard-modules/tika-parser-microsoft-module/src/main/java/org/apache/tika/parser/microsoft/ooxml/TikaSheetXMLHandler.java
 
b/tika-parsers/tika-parsers-standard/tika-parsers-standard-modules/tika-parser-microsoft-module/src/main/java/org/apache/tika/parser/microsoft/ooxml/TikaSheetXMLHandler.java
new file mode 100644
index 0000000000..e95506be5d
--- /dev/null
+++ 
b/tika-parsers/tika-parsers-standard/tika-parsers-standard-modules/tika-parser-microsoft-module/src/main/java/org/apache/tika/parser/microsoft/ooxml/TikaSheetXMLHandler.java
@@ -0,0 +1,398 @@
+/*
+ * 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.microsoft.ooxml;
+
+import java.util.Iterator;
+import java.util.LinkedList;
+import java.util.Queue;
+
+import org.apache.poi.ss.usermodel.BuiltinFormats;
+import org.apache.poi.ss.usermodel.DataFormatter;
+import org.apache.poi.ss.util.CellAddress;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+import org.xml.sax.Attributes;
+import org.xml.sax.SAXException;
+import org.xml.sax.helpers.DefaultHandler;
+
+/**
+ * Sheet XML handler for XLSX event-based parsing that uses {@link 
XSSFStylesShim}
+ * and {@link XSSFCommentsShim} instead of POI's XMLBeans-dependent
+ * {@code StylesTable} and {@code CommentsTable}.
+ * <p>
+ * Adapted from Apache POI's {@code XSSFSheetXMLHandler} (Apache 2.0 license).
+ */
+class TikaSheetXMLHandler extends DefaultHandler {
+
+    private static final Logger LOG = 
LoggerFactory.getLogger(TikaSheetXMLHandler.class);
+
+    private static final String NS_SPREADSHEETML =
+            "http://schemas.openxmlformats.org/spreadsheetml/2006/main";;
+
+    enum XssfDataType {
+        BOOLEAN,
+        ERROR,
+        FORMULA,
+        INLINE_STRING,
+        SST_STRING,
+        NUMBER,
+    }
+
+    private final XSSFStylesShim stylesShim;
+    private final XSSFCommentsShim commentsShim;
+    private final XSSFSharedStringsShim sharedStringsShim;
+    private final TikaSheetContentsHandler output;
+    private final DataFormatter formatter;
+    private final boolean formulasNotResults;
+
+    private boolean vIsOpen;
+    private boolean fIsOpen;
+    private boolean isIsOpen;
+    private boolean hfIsOpen;
+
+    private XssfDataType nextDataType;
+    private short formatIndex;
+    private String formatString;
+
+    private int rowNum;
+    private int nextRowNum;
+    private String cellRef;
+
+    private final StringBuilder value = new StringBuilder(64);
+    private final StringBuilder formula = new StringBuilder(64);
+    private final StringBuilder headerFooter = new StringBuilder(64);
+
+    private Queue<CellAddress> commentCellRefs;
+
+    TikaSheetXMLHandler(XSSFStylesShim stylesShim,
+                         XSSFCommentsShim commentsShim,
+                         XSSFSharedStringsShim sharedStringsShim,
+                         TikaSheetContentsHandler sheetContentsHandler,
+                         DataFormatter dataFormatter,
+                         boolean formulasNotResults) {
+        this.stylesShim = stylesShim;
+        this.commentsShim = commentsShim;
+        this.sharedStringsShim = sharedStringsShim;
+        this.output = sheetContentsHandler;
+        this.formatter = dataFormatter;
+        this.formulasNotResults = formulasNotResults;
+        this.nextDataType = XssfDataType.NUMBER;
+        initComments(commentsShim);
+    }
+
+    TikaSheetXMLHandler(XSSFStylesShim stylesShim,
+                         XSSFSharedStringsShim sharedStringsShim,
+                         TikaSheetContentsHandler sheetContentsHandler,
+                         DataFormatter dataFormatter,
+                         boolean formulasNotResults) {
+        this(stylesShim, null, sharedStringsShim, sheetContentsHandler, 
dataFormatter,
+                formulasNotResults);
+    }
+
+    private void initComments(XSSFCommentsShim commentsShim) {
+        if (commentsShim != null) {
+            commentCellRefs = new LinkedList<>();
+            for (Iterator<CellAddress> iter = commentsShim.getCellAddresses();
+                 iter.hasNext(); ) {
+                commentCellRefs.add(iter.next());
+            }
+        }
+    }
+
+    private boolean isTextTag(String name) {
+        if ("v".equals(name)) {
+            return true;
+        }
+        if ("inlineStr".equals(name)) {
+            return true;
+        }
+        return "t".equals(name) && isIsOpen;
+    }
+
+    @Override
+    public void startElement(String uri, String localName, String qName,
+                             Attributes attributes) throws SAXException {
+        if (uri != null && !uri.equals(NS_SPREADSHEETML)) {
+            return;
+        }
+
+        if (isTextTag(localName)) {
+            vIsOpen = true;
+            if (!isIsOpen) {
+                value.setLength(0);
+            }
+        } else if ("is".equals(localName)) {
+            isIsOpen = true;
+        } else if ("f".equals(localName)) {
+            formula.setLength(0);
+            if (this.nextDataType == XssfDataType.NUMBER) {
+                this.nextDataType = XssfDataType.FORMULA;
+            }
+            String type = attributes.getValue("t");
+            if (type != null && type.equals("shared")) {
+                String ref = attributes.getValue("ref");
+                if (ref != null) {
+                    fIsOpen = true;
+                }
+                // shared-formula reference without a `ref` attribute is not 
yet supported
+            } else {
+                fIsOpen = true;
+            }
+        } else if ("oddHeader".equals(localName) || 
"evenHeader".equals(localName) ||
+                "firstHeader".equals(localName) || 
"firstFooter".equals(localName) ||
+                "oddFooter".equals(localName) || 
"evenFooter".equals(localName)) {
+            hfIsOpen = true;
+            headerFooter.setLength(0);
+        } else if ("row".equals(localName)) {
+            String rowNumStr = attributes.getValue("r");
+            if (rowNumStr != null) {
+                rowNum = Integer.parseInt(rowNumStr.trim()) - 1;
+            } else {
+                rowNum = nextRowNum;
+            }
+            output.startRow(rowNum);
+        } else if ("c".equals(localName)) {
+            // Cell element — resolve style to format index/string
+            this.formula.setLength(0);
+            this.nextDataType = XssfDataType.NUMBER;
+            this.formatIndex = -1;
+            this.formatString = null;
+            cellRef = attributes.getValue("r");
+            String cellType = attributes.getValue("t");
+            String cellStyleStr = attributes.getValue("s");
+
+            if ("b".equals(cellType)) {
+                nextDataType = XssfDataType.BOOLEAN;
+            } else if ("e".equals(cellType)) {
+                nextDataType = XssfDataType.ERROR;
+            } else if ("inlineStr".equals(cellType)) {
+                nextDataType = XssfDataType.INLINE_STRING;
+            } else if ("s".equals(cellType)) {
+                nextDataType = XssfDataType.SST_STRING;
+            } else if ("str".equals(cellType)) {
+                nextDataType = XssfDataType.FORMULA;
+            } else {
+                // Number — resolve format via our styles shim
+                if (stylesShim != null) {
+                    int styleIndex;
+                    if (cellStyleStr != null) {
+                        styleIndex = Integer.parseInt(cellStyleStr.trim());
+                    } else if (stylesShim.getNumCellStyles() > 0) {
+                        styleIndex = 0;
+                    } else {
+                        styleIndex = -1;
+                    }
+                    if (styleIndex >= 0) {
+                        this.formatIndex = 
stylesShim.getFormatIndex(styleIndex);
+                        this.formatString = 
stylesShim.getFormatString(styleIndex);
+                        if (this.formatString == null) {
+                            this.formatString =
+                                    
BuiltinFormats.getBuiltinFormat(this.formatIndex);
+                        }
+                    }
+                }
+            }
+        }
+    }
+
+    @Override
+    public void endElement(String uri, String localName, String qName)
+            throws SAXException {
+        if (uri != null && !uri.equals(NS_SPREADSHEETML)) {
+            return;
+        }
+
+        if (isTextTag(localName)) {
+            vIsOpen = false;
+            if (!isIsOpen) {
+                outputCell();
+                value.setLength(0);
+            }
+        } else if ("f".equals(localName)) {
+            fIsOpen = false;
+        } else if ("is".equals(localName)) {
+            isIsOpen = false;
+            outputCell();
+            value.setLength(0);
+        } else if ("row".equals(localName)) {
+            checkForEmptyCellComments(EmptyCellCommentsCheckType.END_OF_ROW);
+            output.endRow(rowNum);
+            nextRowNum = rowNum + 1;
+        } else if ("sheetData".equals(localName)) {
+            
checkForEmptyCellComments(EmptyCellCommentsCheckType.END_OF_SHEET_DATA);
+            output.endSheet();
+        } else if ("oddHeader".equals(localName) || 
"evenHeader".equals(localName) ||
+                "firstHeader".equals(localName)) {
+            hfIsOpen = false;
+            output.headerFooter(headerFooter.toString(), true, localName);
+        } else if ("oddFooter".equals(localName) || 
"evenFooter".equals(localName) ||
+                "firstFooter".equals(localName)) {
+            hfIsOpen = false;
+            output.headerFooter(headerFooter.toString(), false, localName);
+        }
+    }
+
+    @Override
+    public void characters(char[] ch, int start, int length) throws 
SAXException {
+        if (vIsOpen) {
+            value.append(ch, start, length);
+        }
+        if (fIsOpen) {
+            formula.append(ch, start, length);
+        }
+        if (hfIsOpen) {
+            headerFooter.append(ch, start, length);
+        }
+    }
+
+    private void outputCell() {
+        String thisStr = null;
+
+        if (formulasNotResults && formula.length() > 0) {
+            thisStr = formula.toString();
+        } else {
+            switch (nextDataType) {
+                case BOOLEAN:
+                    char first = value.charAt(0);
+                    thisStr = first == '0' ? "FALSE" : "TRUE";
+                    break;
+                case ERROR:
+                    thisStr = "ERROR:" + value;
+                    break;
+                case FORMULA:
+                    if (formulasNotResults) {
+                        thisStr = formula.toString();
+                    } else {
+                        String fv = value.toString();
+                        if (this.formatString != null) {
+                            try {
+                                double d = Double.parseDouble(fv.trim());
+                                thisStr = formatter.formatRawCellContents(
+                                        d, this.formatIndex, 
this.formatString);
+                            } catch (Exception e) {
+                                thisStr = fv;
+                            }
+                        } else {
+                            thisStr = fv;
+                        }
+                    }
+                    break;
+                case INLINE_STRING:
+                    thisStr = value.toString();
+                    break;
+                case SST_STRING:
+                    String sstIndex = value.toString().trim();
+                    if (!sstIndex.isEmpty() && sharedStringsShim != null) {
+                        try {
+                            int idx = Integer.parseInt(sstIndex);
+                            thisStr = sharedStringsShim.getItemAt(idx);
+                        } catch (NumberFormatException ex) {
+                            LOG.error("Failed to parse SST index '{}'", 
sstIndex, ex);
+                        }
+                    }
+                    break;
+                case NUMBER:
+                    String n = value.toString();
+                    if (this.formatString != null && !n.isEmpty()) {
+                        try {
+                            thisStr = formatter.formatRawCellContents(
+                                    Double.parseDouble(n.trim()),
+                                    this.formatIndex, this.formatString);
+                        } catch (Exception e) {
+                            thisStr = n;
+                        }
+                    } else {
+                        thisStr = n;
+                    }
+                    break;
+                default:
+                    thisStr = "(TODO: Unexpected type: " + nextDataType + ")";
+                    break;
+            }
+        }
+
+        checkForEmptyCellComments(EmptyCellCommentsCheckType.CELL);
+        XSSFCommentsShim.CommentData comment = commentsShim != null ?
+                commentsShim.findCellComment(new CellAddress(cellRef)) : null;
+        output.cell(cellRef, thisStr, comment);
+    }
+
+    private void checkForEmptyCellComments(EmptyCellCommentsCheckType type) {
+        if (commentCellRefs != null && !commentCellRefs.isEmpty()) {
+            if (type == EmptyCellCommentsCheckType.END_OF_SHEET_DATA) {
+                while (!commentCellRefs.isEmpty()) {
+                    outputEmptyCellComment(commentCellRefs.remove());
+                }
+                return;
+            }
+
+            if (this.cellRef == null) {
+                if (type == EmptyCellCommentsCheckType.END_OF_ROW) {
+                    while (!commentCellRefs.isEmpty()) {
+                        if (commentCellRefs.peek().getRow() == rowNum) {
+                            outputEmptyCellComment(commentCellRefs.remove());
+                        } else {
+                            return;
+                        }
+                    }
+                    return;
+                } else {
+                    throw new IllegalStateException(
+                            "Cell ref should be null only if there are only 
empty " +
+                                    "cells in the row; rowNum: " + rowNum);
+                }
+            }
+
+            CellAddress nextCommentCellRef;
+            do {
+                CellAddress cellAddr = new CellAddress(this.cellRef);
+                CellAddress peekCellRef = commentCellRefs.peek();
+                if (type == EmptyCellCommentsCheckType.CELL &&
+                        cellAddr.equals(peekCellRef)) {
+                    commentCellRefs.remove();
+                    return;
+                } else {
+                    int comparison = peekCellRef.compareTo(cellAddr);
+                    if (comparison > 0 &&
+                            type == EmptyCellCommentsCheckType.END_OF_ROW &&
+                            peekCellRef.getRow() <= rowNum) {
+                        nextCommentCellRef = commentCellRefs.remove();
+                        outputEmptyCellComment(nextCommentCellRef);
+                    } else if (comparison < 0 &&
+                            type == EmptyCellCommentsCheckType.CELL &&
+                            peekCellRef.getRow() <= rowNum) {
+                        nextCommentCellRef = commentCellRefs.remove();
+                        outputEmptyCellComment(nextCommentCellRef);
+                    } else {
+                        nextCommentCellRef = null;
+                    }
+                }
+            } while (nextCommentCellRef != null && !commentCellRefs.isEmpty());
+        }
+    }
+
+    private void outputEmptyCellComment(CellAddress cellRef) {
+        XSSFCommentsShim.CommentData comment = 
commentsShim.findCellComment(cellRef);
+        output.cell(cellRef.formatAsString(), null, comment);
+    }
+
+    private enum EmptyCellCommentsCheckType {
+        CELL,
+        END_OF_ROW,
+        END_OF_SHEET_DATA
+    }
+}
diff --git 
a/tika-parsers/tika-parsers-standard/tika-parsers-standard-modules/tika-parser-microsoft-module/src/main/java/org/apache/tika/parser/microsoft/ooxml/TikaXSSFBCommentsTable.java
 
b/tika-parsers/tika-parsers-standard/tika-parsers-standard-modules/tika-parser-microsoft-module/src/main/java/org/apache/tika/parser/microsoft/ooxml/TikaXSSFBCommentsTable.java
new file mode 100644
index 0000000000..79a968a0ba
--- /dev/null
+++ 
b/tika-parsers/tika-parsers-standard/tika-parsers-standard-modules/tika-parser-microsoft-module/src/main/java/org/apache/tika/parser/microsoft/ooxml/TikaXSSFBCommentsTable.java
@@ -0,0 +1,137 @@
+/*
+ * 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.microsoft.ooxml;
+
+import java.io.IOException;
+import java.io.InputStream;
+import java.util.ArrayList;
+import java.util.List;
+import java.util.Map;
+import java.util.TreeMap;
+
+import org.apache.poi.ss.util.CellAddress;
+import org.apache.poi.util.LittleEndian;
+import org.apache.poi.util.LittleEndianConsts;
+import org.apache.poi.xssf.binary.XSSFBParser;
+import org.apache.poi.xssf.binary.XSSFBRecordType;
+import org.apache.poi.xssf.binary.XSSFBUtils;
+import org.xml.sax.SAXException;
+
+import org.apache.tika.sax.XHTMLContentHandler;
+
+/**
+ * Replacement for POI's {@code XSSFBCommentsTable} that does not depend on
+ * {@code XSSFBComment}/{@code XSSFBRichTextString}/{@code XSSFRichTextString}
+ * (which pull in poi-ooxml-lite / xmlbeans via {@code CTRst}).
+ * <p>
+ * Stores comments as plain author + text strings.
+ */
+class TikaXSSFBCommentsTable extends XSSFBParser {
+
+    private final Map<CellAddress, CommentEntry> comments = new TreeMap<>();
+    private final List<String> authors = new ArrayList<>();
+
+    private int authorId = -1;
+    private int cellRow = -1;
+    private int cellCol = -1;
+    private String commentText;
+    private final StringBuilder buffer = new StringBuilder();
+
+    TikaXSSFBCommentsTable(InputStream is) throws IOException {
+        super(is);
+        parse();
+    }
+
+    @Override
+    public void handleRecord(int id, byte[] data) {
+        XSSFBRecordType recordType = XSSFBRecordType.lookup(id);
+        switch (recordType) {
+            case BrtBeginComment:
+                authorId = (int) LittleEndian.getUInt(data, 0);
+                // cell range: firstRow at offset 4, firstCol at offset 12
+                cellRow = (int) LittleEndian.getUInt(data, 
LittleEndianConsts.INT_SIZE);
+                cellCol = (int) LittleEndian.getUInt(data,
+                        LittleEndianConsts.INT_SIZE + 2 * 
LittleEndianConsts.INT_SIZE);
+                break;
+            case BrtCommentText:
+                buffer.setLength(0);
+                XSSFBUtils.readXLWideString(data, 1, buffer);
+                commentText = buffer.toString();
+                break;
+            case BrtEndComment:
+                CellAddress addr = new CellAddress(cellRow, cellCol);
+                String author = (authorId >= 0 && authorId < authors.size())
+                        ? authors.get(authorId) : "";
+                comments.put(addr, new CommentEntry(author, commentText));
+                authorId = -1;
+                cellRow = -1;
+                cellCol = -1;
+                commentText = null;
+                break;
+            case BrtCommentAuthor:
+                buffer.setLength(0);
+                XSSFBUtils.readXLWideString(data, 0, buffer);
+                authors.add(buffer.toString());
+                break;
+            default:
+                break;
+        }
+    }
+
+    CommentEntry get(CellAddress cellAddress) {
+        return cellAddress == null ? null : comments.get(cellAddress);
+    }
+
+    boolean hasComments() {
+        return !comments.isEmpty();
+    }
+
+    /**
+     * Emits all comments as cell content. Called after sheet processing
+     * since we bypass POI's built-in comment handling.
+     */
+    void emitAllComments(XHTMLContentHandler xhtml) throws SAXException {
+        for (Map.Entry<CellAddress, CommentEntry> entry : comments.entrySet()) 
{
+            CommentEntry comment = entry.getValue();
+            xhtml.startElement("p", "class", "cell-comment");
+            String author = comment.getAuthor();
+            if (author != null && !author.isEmpty()) {
+                xhtml.characters(author + ": ");
+            }
+            xhtml.characters(comment.getText());
+            xhtml.endElement("p");
+        }
+    }
+
+    static class CommentEntry {
+        private final String author;
+        private final String text;
+
+        CommentEntry(String author, String text) {
+            this.author = author;
+            this.text = text;
+        }
+
+        String getAuthor() {
+            return author;
+        }
+
+        String getText() {
+            return text;
+        }
+    }
+}
diff --git 
a/tika-parsers/tika-parsers-standard/tika-parsers-standard-modules/tika-parser-microsoft-module/src/main/java/org/apache/tika/parser/microsoft/ooxml/TikaXSSFBSharedStringsTable.java
 
b/tika-parsers/tika-parsers-standard/tika-parsers-standard-modules/tika-parser-microsoft-module/src/main/java/org/apache/tika/parser/microsoft/ooxml/TikaXSSFBSharedStringsTable.java
new file mode 100644
index 0000000000..6e1ec853d5
--- /dev/null
+++ 
b/tika-parsers/tika-parsers-standard/tika-parsers-standard-modules/tika-parser-microsoft-module/src/main/java/org/apache/tika/parser/microsoft/ooxml/TikaXSSFBSharedStringsTable.java
@@ -0,0 +1,159 @@
+/*
+ * 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.microsoft.ooxml;
+
+import java.io.IOException;
+import java.io.InputStream;
+import java.util.ArrayList;
+import java.util.List;
+
+import org.apache.poi.openxml4j.opc.OPCPackage;
+import org.apache.poi.openxml4j.opc.PackagePart;
+import org.apache.poi.ss.usermodel.Font;
+import org.apache.poi.ss.usermodel.RichTextString;
+import org.apache.poi.util.LittleEndian;
+import org.apache.poi.xssf.binary.XSSFBParser;
+import org.apache.poi.xssf.binary.XSSFBRecordType;
+import org.apache.poi.xssf.binary.XSSFBUtils;
+import org.apache.poi.xssf.model.SharedStrings;
+
+/**
+ * Replacement for POI's {@code XSSFBSharedStringsTable} that does not depend 
on
+ * {@code XSSFRichTextString} (which pulls in poi-ooxml-lite / xmlbeans via 
{@code CTRst}).
+ * <p>
+ * The binary parsing logic is identical to POI's implementation; only
+ * {@link #getItemAt(int)} is changed to return a lightweight {@link 
RichTextString}
+ * wrapper instead of {@code XSSFRichTextString}.
+ */
+class TikaXSSFBSharedStringsTable implements SharedStrings {
+
+    private static final String SHARED_STRINGS_BINARY_CT =
+            "application/vnd.ms-excel.sharedStrings";
+
+    private int count;
+    private int uniqueCount;
+    private final List<String> strings = new ArrayList<>();
+
+    TikaXSSFBSharedStringsTable(OPCPackage pkg) throws IOException {
+        ArrayList<PackagePart> parts =
+                pkg.getPartsByContentType(SHARED_STRINGS_BINARY_CT);
+        if (!parts.isEmpty()) {
+            PackagePart sstPart = parts.get(0);
+            try (InputStream stream = sstPart.getInputStream()) {
+                readFrom(stream);
+            }
+        }
+    }
+
+    private void readFrom(InputStream inputStream) throws IOException {
+        new SSTBinaryReader(inputStream).parse();
+    }
+
+    @Override
+    public RichTextString getItemAt(int idx) {
+        return new PlainRichTextString(strings.get(idx));
+    }
+
+    @Override
+    public int getCount() {
+        return count;
+    }
+
+    @Override
+    public int getUniqueCount() {
+        return uniqueCount;
+    }
+
+    private class SSTBinaryReader extends XSSFBParser {
+
+        SSTBinaryReader(InputStream is) {
+            super(is);
+        }
+
+        @Override
+        public void handleRecord(int recordType, byte[] data) {
+            XSSFBRecordType type = XSSFBRecordType.lookup(recordType);
+            switch (type) {
+                case BrtSstItem:
+                    // Inline XSSFBRichStr.build() logic — that class is 
package-private in POI
+                    StringBuilder sb = new StringBuilder();
+                    XSSFBUtils.readXLWideString(data, 1, sb);
+                    strings.add(sb.toString());
+                    break;
+                case BrtBeginSst:
+                    count = (int) LittleEndian.getUInt(data, 0);
+                    uniqueCount = (int) LittleEndian.getUInt(data, 4);
+                    break;
+                default:
+                    break;
+            }
+        }
+    }
+
+    /**
+     * Minimal {@link RichTextString} that just wraps a plain string,
+     * avoiding the xmlbeans dependency in {@code XSSFRichTextString}.
+     */
+    private static class PlainRichTextString implements RichTextString {
+
+        private final String text;
+
+        PlainRichTextString(String text) {
+            this.text = text;
+        }
+
+        @Override
+        public String getString() {
+            return text;
+        }
+
+        @Override
+        public int length() {
+            return text == null ? 0 : text.length();
+        }
+
+        @Override
+        public int numFormattingRuns() {
+            return 0;
+        }
+
+        @Override
+        public int getIndexOfFormattingRun(int index) {
+            return 0;
+        }
+
+        @Override
+        public void applyFont(int startIndex, int endIndex, short fontIndex) {
+        }
+
+        @Override
+        public void applyFont(int startIndex, int endIndex, Font font) {
+        }
+
+        @Override
+        public void applyFont(Font font) {
+        }
+
+        @Override
+        public void clearFormatting() {
+        }
+
+        @Override
+        public void applyFont(short fontIndex) {
+        }
+    }
+}
diff --git 
a/tika-parsers/tika-parsers-standard/tika-parsers-standard-modules/tika-parser-microsoft-module/src/main/java/org/apache/tika/parser/microsoft/ooxml/VSDXExtractorDecorator.java
 
b/tika-parsers/tika-parsers-standard/tika-parsers-standard-modules/tika-parser-microsoft-module/src/main/java/org/apache/tika/parser/microsoft/ooxml/VSDXExtractorDecorator.java
new file mode 100644
index 0000000000..197ccf6eda
--- /dev/null
+++ 
b/tika-parsers/tika-parsers-standard/tika-parsers-standard-modules/tika-parser-microsoft-module/src/main/java/org/apache/tika/parser/microsoft/ooxml/VSDXExtractorDecorator.java
@@ -0,0 +1,177 @@
+/*
+ * 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.microsoft.ooxml;
+
+import java.io.IOException;
+import java.io.InputStream;
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.List;
+
+import org.apache.poi.ooxml.extractor.POIXMLTextExtractor;
+import org.apache.poi.openxml4j.exceptions.InvalidFormatException;
+import org.apache.poi.openxml4j.opc.OPCPackage;
+import org.apache.poi.openxml4j.opc.PackagePart;
+import org.apache.poi.openxml4j.opc.PackageRelationship;
+import org.apache.poi.openxml4j.opc.PackageRelationshipCollection;
+import org.xml.sax.Attributes;
+import org.xml.sax.SAXException;
+import org.xml.sax.helpers.DefaultHandler;
+
+import org.apache.tika.exception.TikaException;
+import org.apache.tika.parser.ParseContext;
+import org.apache.tika.sax.XHTMLContentHandler;
+import org.apache.tika.utils.XMLReaderUtils;
+
+/**
+ * SAX-based extractor for Visio OOXML (.vsdx) files.
+ * Extracts text from {@code <Text>} elements inside shapes on each page.
+ */
+public class VSDXExtractorDecorator extends AbstractOOXMLExtractor {
+
+    private static final String VISIO_DOCUMENT_REL =
+            "http://schemas.microsoft.com/visio/2010/relationships/document";;
+    private static final String VISIO_PAGES_REL =
+            "http://schemas.microsoft.com/visio/2010/relationships/pages";;
+    private static final String VISIO_PAGE_REL =
+            "http://schemas.microsoft.com/visio/2010/relationships/page";;
+
+    private final ParseContext context;
+
+    public VSDXExtractorDecorator(ParseContext context, POIXMLTextExtractor 
extractor) {
+        //keep the 3x extractor-based ctor (factory + AbstractOOXMLExtractor 
unchanged);
+        //the body reads from the base opcPackage, derived from the extractor
+        super(context, extractor);
+        this.context = context;
+    }
+
+    @Override
+    protected void buildXHTML(XHTMLContentHandler xhtml)
+            throws SAXException, IOException {
+        try {
+            List<PackagePart> pageParts = getPageParts();
+            for (PackagePart pagePart : pageParts) {
+                xhtml.startElement("div", "class", "page");
+                try (InputStream is = pagePart.getInputStream()) {
+                    XMLReaderUtils.parseSAX(is, new VisioPageHandler(xhtml), 
context);
+                } catch (TikaException e) {
+                    throw new SAXException(e);
+                }
+                xhtml.endElement("div");
+            }
+        } catch (InvalidFormatException e) {
+            throw new SAXException("Error reading VSDX pages", e);
+        }
+    }
+
+    private List<PackagePart> getPageParts() throws InvalidFormatException {
+        // Root -> visio/document.xml
+        PackagePart documentPart = getRelatedPart(opcPackage, 
VISIO_DOCUMENT_REL);
+        if (documentPart == null) {
+            return Collections.emptyList();
+        }
+
+        // document.xml -> pages/pages.xml
+        PackagePart pagesPart = getRelatedPart(documentPart, VISIO_PAGES_REL);
+        if (pagesPart == null) {
+            return Collections.emptyList();
+        }
+
+        // pages.xml -> page1.xml, page2.xml, ...
+        List<PackagePart> pageParts = new ArrayList<>();
+        PackageRelationshipCollection pageRels =
+                pagesPart.getRelationshipsByType(VISIO_PAGE_REL);
+        for (PackageRelationship rel : pageRels) {
+            PackagePart pagePart = pagesPart.getRelatedPart(rel);
+            if (pagePart != null) {
+                pageParts.add(pagePart);
+            }
+        }
+        return pageParts;
+    }
+
+    private PackagePart getRelatedPart(OPCPackage pkg, String relType)
+            throws InvalidFormatException {
+        PackageRelationshipCollection rels = 
pkg.getRelationshipsByType(relType);
+        if (rels.isEmpty()) {
+            return null;
+        }
+        return pkg.getPart(rels.getRelationship(0));
+    }
+
+    private PackagePart getRelatedPart(PackagePart part, String relType)
+            throws InvalidFormatException {
+        PackageRelationshipCollection rels = 
part.getRelationshipsByType(relType);
+        if (rels.isEmpty()) {
+            return null;
+        }
+        return part.getRelatedPart(rels.getRelationship(0));
+    }
+
+    @Override
+    protected List<PackagePart> getMainDocumentParts() {
+        return Collections.emptyList();
+    }
+
+    /**
+     * SAX handler for Visio page XML. Extracts text from {@code <Text>}
+     * elements inside {@code <Shape>} elements.
+     */
+    private static class VisioPageHandler extends DefaultHandler {
+
+        private static final String VISIO_NS =
+                "http://schemas.microsoft.com/office/visio/2012/main";;
+
+        private final XHTMLContentHandler xhtml;
+        private boolean inText;
+        private final StringBuilder textBuffer = new StringBuilder();
+
+        VisioPageHandler(XHTMLContentHandler xhtml) {
+            this.xhtml = xhtml;
+        }
+
+        @Override
+        public void startElement(String uri, String localName, String qName,
+                                 Attributes atts) {
+            if ("Text".equals(localName) && VISIO_NS.equals(uri)) {
+                inText = true;
+                textBuffer.setLength(0);
+            }
+        }
+
+        @Override
+        public void endElement(String uri, String localName, String qName)
+                throws SAXException {
+            if ("Text".equals(localName) && VISIO_NS.equals(uri)) {
+                inText = false;
+                String text = textBuffer.toString().trim();
+                if (!text.isEmpty()) {
+                    xhtml.startElement("p");
+                    xhtml.characters(text);
+                    xhtml.endElement("p");
+                }
+            }
+        }
+
+        @Override
+        public void characters(char[] ch, int start, int length) {
+            if (inText) {
+                textBuffer.append(ch, start, length);
+            }
+        }
+    }
+}
diff --git 
a/tika-parsers/tika-parsers-standard/tika-parsers-standard-modules/tika-parser-microsoft-module/src/main/java/org/apache/tika/parser/microsoft/ooxml/XSSFBExcelExtractorDecorator.java
 
b/tika-parsers/tika-parsers-standard/tika-parsers-standard-modules/tika-parser-microsoft-module/src/main/java/org/apache/tika/parser/microsoft/ooxml/XSSFBExcelExtractorDecorator.java
index 77000b9a9a..f9e87301d8 100644
--- 
a/tika-parsers/tika-parsers-standard/tika-parsers-standard-modules/tika-parser-microsoft-module/src/main/java/org/apache/tika/parser/microsoft/ooxml/XSSFBExcelExtractorDecorator.java
+++ 
b/tika-parsers/tika-parsers-standard/tika-parsers-standard-modules/tika-parser-microsoft-module/src/main/java/org/apache/tika/parser/microsoft/ooxml/XSSFBExcelExtractorDecorator.java
@@ -18,27 +18,27 @@ package org.apache.tika.parser.microsoft.ooxml;
 
 import java.io.IOException;
 import java.io.InputStream;
-import java.util.List;
 import java.util.Locale;
 
 import org.apache.poi.ooxml.extractor.POIXMLTextExtractor;
+import org.apache.poi.openxml4j.exceptions.InvalidFormatException;
 import org.apache.poi.openxml4j.exceptions.OpenXML4JException;
 import org.apache.poi.openxml4j.opc.OPCPackage;
 import org.apache.poi.openxml4j.opc.PackagePart;
-import org.apache.poi.xssf.binary.XSSFBCommentsTable;
-import org.apache.poi.xssf.binary.XSSFBSharedStringsTable;
+import org.apache.poi.openxml4j.opc.PackagePartName;
+import org.apache.poi.openxml4j.opc.PackageRelationship;
+import org.apache.poi.openxml4j.opc.PackageRelationshipCollection;
+import org.apache.poi.openxml4j.opc.PackagingURIHelper;
 import org.apache.poi.xssf.binary.XSSFBSheetHandler;
 import org.apache.poi.xssf.binary.XSSFBStylesTable;
 import org.apache.poi.xssf.eventusermodel.XSSFBReader;
-import 
org.apache.poi.xssf.eventusermodel.XSSFSheetXMLHandler.SheetContentsHandler;
-import org.apache.poi.xssf.extractor.XSSFBEventBasedExcelExtractor;
-import org.apache.poi.xssf.usermodel.XSSFShape;
 import org.apache.xmlbeans.XmlException;
 import org.xml.sax.ContentHandler;
 import org.xml.sax.SAXException;
 
 import org.apache.tika.exception.TikaException;
 import org.apache.tika.metadata.Metadata;
+import org.apache.tika.metadata.Office;
 import org.apache.tika.metadata.TikaCoreProperties;
 import org.apache.tika.parser.ParseContext;
 import org.apache.tika.sax.XHTMLContentHandler;
@@ -50,17 +50,9 @@ public class XSSFBExcelExtractorDecorator extends 
XSSFExcelExtractorDecorator {
         super(context, extractor, locale);
     }
 
-    @Override
-    protected void configureExtractor(POIXMLTextExtractor extractor, Locale 
locale) {
-        //need to override this because setFormulasNotResults is not yet 
available
-        //for xlsb
-        
//((XSSFBEventBasedExcelExtractor)extractor).setFormulasNotResults(false);
-        ((XSSFBEventBasedExcelExtractor) extractor).setLocale(locale);
-    }
-
     @Override
     public void getXHTML(ContentHandler handler, Metadata metadata, 
ParseContext context)
-            throws SAXException, XmlException, IOException, TikaException {
+            throws SAXException, IOException, TikaException, XmlException {
 
         this.metadata = metadata;
         this.parseContext = context;
@@ -69,15 +61,12 @@ public class XSSFBExcelExtractorDecorator extends 
XSSFExcelExtractorDecorator {
         super.getXHTML(handler, metadata, context);
     }
 
-    /**
-     * @see 
org.apache.poi.xssf.extractor.XSSFBEventBasedExcelExtractor#getText()
-     */
     @Override
     protected void buildXHTML(XHTMLContentHandler xhtml)
-            throws SAXException, XmlException, IOException {
-        OPCPackage container = extractor.getPackage();
+            throws SAXException, IOException {
+        OPCPackage container = opcPackage;
 
-        XSSFBSharedStringsTable strings;
+        TikaXSSFBSharedStringsTable strings;
         XSSFBReader.SheetIterator iter;
         XSSFBReader xssfReader;
         XSSFBStylesTable styles;
@@ -89,9 +78,9 @@ public class XSSFBExcelExtractorDecorator extends 
XSSFExcelExtractorDecorator {
             }
             styles = xssfReader.getXSSFBStylesTable();
             iter = (XSSFBReader.SheetIterator) xssfReader.getSheetsData();
-            strings = new XSSFBSharedStringsTable(container);
+            strings = new TikaXSSFBSharedStringsTable(container);
         } catch (OpenXML4JException e) {
-            throw new XmlException(e);
+            throw new IOException(e);
         }
 
         while (iter.hasNext()) {
@@ -101,44 +90,71 @@ public class XSSFBExcelExtractorDecorator extends 
XSSFExcelExtractorDecorator {
             sheetParts.add(sheetPart);
 
             SheetTextAsHTML sheetExtractor = new SheetTextAsHTML(config, 
xhtml);
-            XSSFBCommentsTable comments = iter.getXSSFBSheetComments();
 
-            // Start, and output the sheet name
+            // Parse comments with our own binary parser that avoids xmlbeans
+            TikaXSSFBCommentsTable tikaComments = 
parseBinaryComments(sheetPart);
+            if (tikaComments != null && tikaComments.hasComments()) {
+                metadata.set(Office.HAS_COMMENTS, true);
+            }
+
             xhtml.startElement("div");
             xhtml.element("h1", iter.getSheetName());
 
-            // Extract the main sheet contents
             xhtml.startElement("table");
             xhtml.startElement("tbody");
 
-            processSheet(sheetExtractor, comments, styles, strings, stream);
+            // Pass null for POI's comments table to avoid xmlbeans dependency.
+            // Comments are emitted separately after sheet processing.
+            XSSFBSheetHandler xssfbSheetHandler =
+                    new XSSFBSheetHandler(stream, styles, null, strings,
+                            sheetExtractor, formatter, false);
+            xssfbSheetHandler.parse();
 
             xhtml.endElement("tbody");
             xhtml.endElement("table");
 
-            // Output any headers and footers
-            // (Need to process the sheet to get them, so we can't
-            //  do the headers before the contents)
+            // Emit comments after the table (since we bypass POI's inline
+            // comment handling to avoid xmlbeans dependency)
+            if (tikaComments != null) {
+                tikaComments.emitAllComments(xhtml);
+            }
+
             for (String header : sheetExtractor.headers) {
                 extractHeaderFooter(header, xhtml);
             }
             for (String footer : sheetExtractor.footers) {
                 extractHeaderFooter(footer, xhtml);
             }
-            List<XSSFShape> shapes = iter.getShapes();
-
-            processShapes(shapes, xhtml);
-
-            //for now dump sheet hyperlinks at bottom of page
-            //consider a double-pass of the inputstream to reunite hyperlinks 
with cells/textboxes
-            //step 1: extract hyperlink info from bottom of page
-            //step 2: process as we do now, but with cached hyperlink 
relationship info
+            processDrawings(sheetPart, xhtml);
             extractHyperLinks(sheetPart, xhtml);
-            // All done with this sheet
             xhtml.endElement("div");
         }
     }
 
+    private static final String RELATION_COMMENTS =
+            
"http://schemas.openxmlformats.org/officeDocument/2006/relationships/comments";;
+
+    private TikaXSSFBCommentsTable parseBinaryComments(PackagePart sheetPart) {
+        try {
+            PackageRelationshipCollection rels =
+                    sheetPart.getRelationshipsByType(RELATION_COMMENTS);
+            if (rels.isEmpty()) {
+                return null;
+            }
+            PackageRelationship rel = rels.getRelationship(0);
+            PackagePartName partName =
+                    PackagingURIHelper.createPartName(rel.getTargetURI());
+            PackagePart commentsPart = rel.getPackage().getPart(partName);
+            if (commentsPart == null) {
+                return null;
+            }
+            try (InputStream is = commentsPart.getInputStream()) {
+                return new TikaXSSFBCommentsTable(is);
+            }
+        } catch (InvalidFormatException | IOException e) {
+            return null;
+        }
+    }
 
     @Override
     protected void extractHeaderFooter(String hf, XHTMLContentHandler xhtml) 
throws SAXException {
@@ -146,16 +162,4 @@ public class XSSFBExcelExtractorDecorator extends 
XSSFExcelExtractorDecorator {
             xhtml.element("p", hf);
         }
     }
-
-
-    private void processSheet(SheetContentsHandler sheetContentsExtractor,
-                              XSSFBCommentsTable comments, XSSFBStylesTable 
styles,
-                              XSSFBSharedStringsTable strings, InputStream 
sheetInputStream)
-            throws IOException, SAXException {
-
-        XSSFBSheetHandler xssfbSheetHandler =
-                new XSSFBSheetHandler(sheetInputStream, styles, comments, 
strings,
-                        sheetContentsExtractor, formatter, false);
-        xssfbSheetHandler.parse();
-    }
 }
diff --git 
a/tika-parsers/tika-parsers-standard/tika-parsers-standard-modules/tika-parser-microsoft-module/src/main/java/org/apache/tika/parser/microsoft/ooxml/XSSFCommentsShim.java
 
b/tika-parsers/tika-parsers-standard/tika-parsers-standard-modules/tika-parser-microsoft-module/src/main/java/org/apache/tika/parser/microsoft/ooxml/XSSFCommentsShim.java
new file mode 100644
index 0000000000..f3293a0d3c
--- /dev/null
+++ 
b/tika-parsers/tika-parsers-standard/tika-parsers-standard-modules/tika-parser-microsoft-module/src/main/java/org/apache/tika/parser/microsoft/ooxml/XSSFCommentsShim.java
@@ -0,0 +1,187 @@
+/*
+ * 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.microsoft.ooxml;
+
+import java.io.IOException;
+import java.io.InputStream;
+import java.util.ArrayList;
+import java.util.Iterator;
+import java.util.LinkedHashMap;
+import java.util.List;
+import java.util.Map;
+
+import org.apache.poi.ss.util.CellAddress;
+import org.xml.sax.Attributes;
+import org.xml.sax.SAXException;
+import org.xml.sax.helpers.DefaultHandler;
+
+import org.apache.tika.exception.TikaException;
+import org.apache.tika.parser.ParseContext;
+import org.apache.tika.utils.XMLReaderUtils;
+
+/**
+ * SAX-based shim that parses {@code xl/commentsN.xml} without XMLBeans.
+ * Replaces POI's {@code CommentsTable} (which depends on poi-ooxml-lite)
+ * for Tika's text extraction needs.
+ *
+ * <p>Only extracts what Tika needs: cell reference → (author, text) 
mapping.</p>
+ */
+class XSSFCommentsShim {
+
+    private final Map<CellAddress, CommentData> commentsByCell;
+
+    /**
+     * Simple holder for comment data needed by Tika.
+     */
+    static class CommentData {
+        private final String author;
+        private final String text;
+
+        CommentData(String author, String text) {
+            this.author = author;
+            this.text = text;
+        }
+
+        public String getAuthor() {
+            return author;
+        }
+
+        public String getText() {
+            return text;
+        }
+    }
+
+    /**
+     * Parse a comments XML stream.
+     *
+     * @param is           the {@code xl/commentsN.xml} stream (may be null)
+     * @param parseContext parse context for SAX parser configuration
+     */
+    XSSFCommentsShim(InputStream is, ParseContext parseContext)
+            throws IOException, TikaException, SAXException {
+        commentsByCell = new LinkedHashMap<>();
+        if (is != null) {
+            CommentsHandler handler = new CommentsHandler();
+            XMLReaderUtils.parseSAX(is, handler, parseContext);
+        }
+    }
+
+    /**
+     * @return the number of comments parsed
+     */
+    int getNumberOfComments() {
+        return commentsByCell.size();
+    }
+
+    /**
+     * Find comment data for a given cell address.
+     *
+     * @return CommentData or null if no comment at that cell
+     */
+    CommentData findCellComment(CellAddress cellAddress) {
+        return commentsByCell.get(cellAddress);
+    }
+
+    /**
+     * @return iterator over all cell addresses that have comments, in 
document order
+     */
+    Iterator<CellAddress> getCellAddresses() {
+        return commentsByCell.keySet().iterator();
+    }
+
+    /**
+     * SAX handler for comments XML.  Structure:
+     * <pre>
+     * &lt;comments&gt;
+     *   &lt;authors&gt;
+     *     &lt;author&gt;Name&lt;/author&gt;
+     *   &lt;/authors&gt;
+     *   &lt;commentList&gt;
+     *     &lt;comment ref="A1" authorId="0"&gt;
+     *       &lt;text&gt;
+     *         &lt;r&gt;&lt;t&gt;Comment text&lt;/t&gt;&lt;/r&gt;
+     *         or plain &lt;t&gt;Comment text&lt;/t&gt;
+     *       &lt;/text&gt;
+     *     &lt;/comment&gt;
+     *   &lt;/commentList&gt;
+     * &lt;/comments&gt;
+     * </pre>
+     */
+    private class CommentsHandler extends DefaultHandler {
+
+        private final List<String> authors = new ArrayList<>();
+        private final StringBuilder textBuffer = new StringBuilder();
+
+        private boolean inAuthor;
+        private boolean inT;
+        private boolean inText;
+
+        private String currentRef;
+        private int currentAuthorId;
+        private final StringBuilder commentText = new StringBuilder();
+
+        @Override
+        public void startElement(String uri, String localName, String qName,
+                                 Attributes atts) {
+            if ("author".equals(localName)) {
+                inAuthor = true;
+                textBuffer.setLength(0);
+            } else if ("comment".equals(localName)) {
+                currentRef = atts.getValue("ref");
+                String authorIdStr = atts.getValue("authorId");
+                currentAuthorId = authorIdStr != null ? 
Integer.parseInt(authorIdStr) : -1;
+                commentText.setLength(0);
+            } else if ("text".equals(localName)) {
+                inText = true;
+            } else if ("t".equals(localName) && inText) {
+                inT = true;
+                textBuffer.setLength(0);
+            }
+        }
+
+        @Override
+        public void endElement(String uri, String localName, String qName) {
+            if ("author".equals(localName)) {
+                inAuthor = false;
+                authors.add(textBuffer.toString());
+            } else if ("t".equals(localName) && inT) {
+                inT = false;
+                if (commentText.length() > 0) {
+                    commentText.append(' ');
+                }
+                commentText.append(textBuffer);
+            } else if ("text".equals(localName)) {
+                inText = false;
+            } else if ("comment".equals(localName)) {
+                if (currentRef != null) {
+                    String author = (currentAuthorId >= 0 && currentAuthorId < 
authors.size())
+                            ? authors.get(currentAuthorId) : "";
+                    commentsByCell.put(new CellAddress(currentRef),
+                            new CommentData(author, commentText.toString()));
+                }
+                currentRef = null;
+            }
+        }
+
+        @Override
+        public void characters(char[] ch, int start, int length) {
+            if (inAuthor || inT) {
+                textBuffer.append(ch, start, length);
+            }
+        }
+    }
+}
diff --git 
a/tika-parsers/tika-parsers-standard/tika-parsers-standard-modules/tika-parser-microsoft-module/src/main/java/org/apache/tika/parser/microsoft/ooxml/XSSFExcelExtractorDecorator.java
 
b/tika-parsers/tika-parsers-standard/tika-parsers-standard-modules/tika-parser-microsoft-module/src/main/java/org/apache/tika/parser/microsoft/ooxml/XSSFExcelExtractorDecorator.java
index fe63fe156d..194d206a0e 100644
--- 
a/tika-parsers/tika-parsers-standard/tika-parsers-standard-modules/tika-parser-microsoft-module/src/main/java/org/apache/tika/parser/microsoft/ooxml/XSSFExcelExtractorDecorator.java
+++ 
b/tika-parsers/tika-parsers-standard/tika-parsers-standard-modules/tika-parser-microsoft-module/src/main/java/org/apache/tika/parser/microsoft/ooxml/XSSFExcelExtractorDecorator.java
@@ -21,11 +21,9 @@ import java.io.IOException;
 import java.io.InputStream;
 import java.util.ArrayList;
 import java.util.HashMap;
-import java.util.HashSet;
 import java.util.List;
 import java.util.Locale;
 import java.util.Map;
-import java.util.Set;
 
 import org.apache.poi.hssf.extractor.ExcelExtractor;
 import org.apache.poi.ooxml.extractor.POIXMLTextExtractor;
@@ -42,24 +40,11 @@ import org.apache.poi.openxml4j.opc.TargetMode;
 import org.apache.poi.ss.usermodel.DataFormatter;
 import org.apache.poi.ss.usermodel.HeaderFooter;
 import org.apache.poi.ss.util.CellReference;
-import org.apache.poi.xssf.eventusermodel.ReadOnlySharedStringsTable;
 import org.apache.poi.xssf.eventusermodel.XSSFReader;
-import org.apache.poi.xssf.eventusermodel.XSSFSheetXMLHandler;
 import 
org.apache.poi.xssf.eventusermodel.XSSFSheetXMLHandler.SheetContentsHandler;
-import org.apache.poi.xssf.extractor.XSSFEventBasedExcelExtractor;
-import org.apache.poi.xssf.model.Comments;
-import org.apache.poi.xssf.model.StylesTable;
 import org.apache.poi.xssf.usermodel.XSSFComment;
-import org.apache.poi.xssf.usermodel.XSSFDrawing;
-import org.apache.poi.xssf.usermodel.XSSFRelation;
-import org.apache.poi.xssf.usermodel.XSSFShape;
-import org.apache.poi.xssf.usermodel.XSSFSimpleShape;
 import org.apache.poi.xssf.usermodel.helpers.HeaderFooterHelper;
 import org.apache.xmlbeans.XmlException;
-import org.openxmlformats.schemas.drawingml.x2006.main.CTHyperlink;
-import org.openxmlformats.schemas.drawingml.x2006.main.CTNonVisualDrawingProps;
-import org.openxmlformats.schemas.drawingml.x2006.spreadsheetDrawing.CTShape;
-import 
org.openxmlformats.schemas.drawingml.x2006.spreadsheetDrawing.CTShapeNonVisual;
 import org.xml.sax.Attributes;
 import org.xml.sax.ContentHandler;
 import org.xml.sax.Locator;
@@ -68,6 +53,7 @@ import org.xml.sax.helpers.DefaultHandler;
 
 import org.apache.tika.exception.RuntimeSAXException;
 import org.apache.tika.exception.TikaException;
+import org.apache.tika.exception.WriteLimitReachedException;
 import org.apache.tika.metadata.Metadata;
 import org.apache.tika.metadata.Office;
 import org.apache.tika.metadata.TikaCoreProperties;
@@ -75,6 +61,7 @@ import org.apache.tika.parser.ParseContext;
 import org.apache.tika.parser.microsoft.OfficeParserConfig;
 import org.apache.tika.parser.microsoft.TikaExcelDataFormatter;
 import org.apache.tika.sax.XHTMLContentHandler;
+import org.apache.tika.utils.ExceptionUtils;
 import org.apache.tika.utils.StringUtils;
 import org.apache.tika.utils.XMLReaderUtils;
 
@@ -92,6 +79,20 @@ public class XSSFExcelExtractorDecorator extends 
AbstractOOXMLExtractor {
     // Power Query stores data in customData parts
     private static final String POWER_QUERY_CONTENT_TYPE =
             "application/vnd.ms-excel.customDataProperties+xml";
+    private static final String RELATION_DRAWING =
+            
"http://schemas.openxmlformats.org/officeDocument/2006/relationships/drawing";;
+    private static final String RELATION_CHART =
+            
"http://schemas.openxmlformats.org/officeDocument/2006/relationships/chart";;
+    private static final String RELATION_HYPERLINK =
+            
"http://schemas.openxmlformats.org/officeDocument/2006/relationships/hyperlink";;
+    private static final String NS_DRAWING_ML =
+            "http://schemas.openxmlformats.org/drawingml/2006/main";;
+    private static final String NS_RELATIONSHIPS =
+            
"http://schemas.openxmlformats.org/officeDocument/2006/relationships";;
+    private static final String RELATION_VML_DRAWING =
+            
"http://schemas.openxmlformats.org/officeDocument/2006/relationships/vmlDrawing";;
+    private static final String RELATION_COMMENTS =
+            
"http://schemas.openxmlformats.org/officeDocument/2006/relationships/comments";;
 
     /**
      * Allows access to headers/footers from raw xml strings
@@ -105,11 +106,11 @@ public class XSSFExcelExtractorDecorator extends 
AbstractOOXMLExtractor {
 
     public XSSFExcelExtractorDecorator(ParseContext context, 
POIXMLTextExtractor extractor,
                                        Locale locale) {
+        //keep the 3x extractor-based ctor so the factory and 
AbstractOOXMLExtractor are
+        //unchanged; the base derives opcPackage from the extractor (used by 
the body below)
         super(context, extractor);
 
         this.parseContext = context;
-        this.extractor = (XSSFEventBasedExcelExtractor) extractor;
-        configureExtractor(this.extractor, locale);
 
         if (locale == null) {
             formatter = new TikaExcelDataFormatter();
@@ -123,19 +124,14 @@ public class XSSFExcelExtractorDecorator extends 
AbstractOOXMLExtractor {
         }
     }
 
-    protected void configureExtractor(POIXMLTextExtractor extractor, Locale 
locale) {
-        ((XSSFEventBasedExcelExtractor) extractor)
-                .setIncludeTextBoxes(config.isIncludeShapeBasedContent());
-        ((XSSFEventBasedExcelExtractor) 
extractor).setFormulasNotResults(false);
-        ((XSSFEventBasedExcelExtractor) extractor).setLocale(locale);
-        //given that we load our own shared strings table, setting:
-        
//((XSSFEventBasedExcelExtractor)extractor).setConcatenatePhoneticRuns();
-        //does no good here.
+    @Override
+    public MetadataExtractor getMetadataExtractor() {
+        return new SAXBasedMetadataExtractor(opcPackage, parseContext);
     }
 
     @Override
     public void getXHTML(ContentHandler handler, Metadata metadata, 
ParseContext context)
-            throws SAXException, XmlException, IOException, TikaException {
+            throws SAXException, IOException, TikaException, XmlException {
 
         this.metadata = metadata;
         this.parseContext = context;
@@ -149,34 +145,66 @@ public class XSSFExcelExtractorDecorator extends 
AbstractOOXMLExtractor {
      */
     @Override
     protected void buildXHTML(XHTMLContentHandler xhtml)
-            throws SAXException, XmlException, IOException {
-        OPCPackage container = extractor.getPackage();
+            throws SAXException, IOException {
+        OPCPackage container = opcPackage;
 
-        ReadOnlySharedStringsTable strings;
+        XSSFSharedStringsShim stringsShim = null;
         XSSFReader.SheetIterator iter;
         XSSFReader xssfReader;
-        StylesTable styles;
+        XSSFStylesShim stylesShim = null;
         try {
             xssfReader = new XSSFReader(container);
-            styles = xssfReader.getStylesTable();
-
             iter = (XSSFReader.SheetIterator) xssfReader.getSheetsData();
-            strings = new ReadOnlySharedStringsTable(container, 
config.isConcatenatePhoneticRuns());
-        } catch (OpenXML4JException e) {
-            throw new XmlException(e);
+        } catch (OpenXML4JException | RuntimeException e) {
+            throw new IOException(e);
         }
-
-        while (iter.hasNext()) {
+        // Styles and shared strings are optional — if either part is missing 
or
+        // unreadable, log to metadata and continue with degraded extraction.
+        try {
+            stylesShim = new XSSFStylesShim(xssfReader.getStylesData(), 
parseContext);
+        } catch (Exception e) {
+            metadata.add(TikaCoreProperties.TIKA_META_EXCEPTION_WARNING,
+                    ExceptionUtils.getStackTrace(e));
+        }
+        try {
+            stringsShim = new 
XSSFSharedStringsShim(xssfReader.getSharedStringsData(),
+                    config.isConcatenatePhoneticRuns(), parseContext);
+        } catch (Exception e) {
+            metadata.add(TikaCoreProperties.TIKA_META_EXCEPTION_WARNING,
+                    ExceptionUtils.getStackTrace(e));
+        }
+        while (true) {
+            try {
+                if (!iter.hasNext()) {
+                    break;
+                }
+            } catch (RuntimeException e) {
+                metadata.add(TikaCoreProperties.TIKA_META_EXCEPTION_WARNING,
+                        ExceptionUtils.getStackTrace(e));
+                break;
+            }
             SheetTextAsHTML sheetExtractor = new SheetTextAsHTML(config, 
xhtml);
             PackagePart sheetPart = null;
-            try (InputStream stream = iter.next()) {
+            InputStream nextStream;
+            try {
+                nextStream = iter.next();
+            } catch (RuntimeException e) {
+                // POI can throw POIXMLException for missing sheet parts (e.g.,
+                // truncated workbook references a sheet that isn't in the 
zip).
+                // Break rather than continue — POI's iterator state may not 
have
+                // advanced, which would cause an infinite loop.
+                metadata.add(TikaCoreProperties.TIKA_META_EXCEPTION_WARNING,
+                        ExceptionUtils.getStackTrace(e));
+                break;
+            }
+            try (InputStream stream = nextStream) {
                 sheetPart = iter.getSheetPart();
 
                 addDrawingHyperLinks(sheetPart);
                 sheetParts.add(sheetPart);
 
-                Comments comments = iter.getSheetComments();
-                if (comments != null && comments.getNumberOfComments() > 0) {
+                XSSFCommentsShim commentsShim = parseSheetComments(sheetPart);
+                if (commentsShim != null && commentsShim.getNumberOfComments() 
> 0) {
                     metadata.set(Office.HAS_COMMENTS, true);
                 }
 
@@ -188,7 +216,26 @@ public class XSSFExcelExtractorDecorator extends 
AbstractOOXMLExtractor {
                 xhtml.startElement("table");
                 xhtml.startElement("tbody");
 
-                processSheet(sheetExtractor, comments, styles, strings, 
stream);
+                try {
+                    processSheet(sheetExtractor, commentsShim, stylesShim, 
stringsShim, stream);
+                } catch (SAXException e) {
+                    // Truncated/malformed sheet XML — keep prior sheets and
+                    // record the failure as a warning.
+                    WriteLimitReachedException.throwIfWriteLimitReached(e);
+                    
metadata.add(TikaCoreProperties.TIKA_META_EXCEPTION_WARNING,
+                            ExceptionUtils.getStackTrace(e));
+                    // Balance any <tr>/<td> left open by the partial parse so
+                    // the </tbody></table></div> emitted below land in the
+                    // right place.
+                    sheetExtractor.closeAnyPending();
+                } catch (IOException e) {
+                    // Truncated stream — same risk: partial <tr>/<td> still
+                    // open. Close them so the surrounding </tbody></table>
+                    // stays balanced, record the failure, and keep going.
+                    
metadata.add(TikaCoreProperties.TIKA_META_EXCEPTION_WARNING,
+                            ExceptionUtils.getStackTrace(e));
+                    sheetExtractor.closeAnyPending();
+                }
                 try {
                     getThreadedComments(container, sheetPart, xhtml);
                 } catch (InvalidFormatException | TikaException | IOException 
e) {
@@ -210,8 +257,7 @@ public class XSSFExcelExtractorDecorator extends 
AbstractOOXMLExtractor {
 
             // Do text held in shapes, if required
             if (config.isIncludeShapeBasedContent()) {
-                List<XSSFShape> shapes = iter.getShapes();
-                processShapes(shapes, xhtml);
+                processDrawings(sheetPart, xhtml);
             }
 
             //for now dump sheet hyperlinks at bottom of page
@@ -670,7 +716,7 @@ public class XSSFExcelExtractorDecorator extends 
AbstractOOXMLExtractor {
     protected void addDrawingHyperLinks(PackagePart sheetPart) {
         try {
             for (PackageRelationship rel : sheetPart
-                    
.getRelationshipsByType(XSSFRelation.DRAWINGS.getRelation())) {
+                    .getRelationshipsByType(RELATION_DRAWING)) {
                 if (rel.getTargetMode() == TargetMode.INTERNAL) {
                     PackagePartName relName = 
PackagingURIHelper.createPartName(rel.getTargetURI());
                     PackagePart part = rel.getPackage().getPart(relName);
@@ -679,7 +725,7 @@ public class XSSFExcelExtractorDecorator extends 
AbstractOOXMLExtractor {
                         continue;
                     }
                     for (PackageRelationship drawRel : part
-                            
.getRelationshipsByType(XSSFRelation.SHEET_HYPERLINKS.getRelation())) {
+                            .getRelationshipsByType(RELATION_HYPERLINK)) {
                         drawingHyperlinks.put(drawRel.getId(), 
drawRel.getTargetURI().toString());
                     }
                 }
@@ -696,8 +742,13 @@ public class XSSFExcelExtractorDecorator extends 
AbstractOOXMLExtractor {
     protected void extractHyperLinks(PackagePart sheetPart, 
XHTMLContentHandler xhtml)
             throws SAXException {
         try {
+            boolean first = true;
             for (PackageRelationship rel : sheetPart
-                    
.getRelationshipsByType(XSSFRelation.SHEET_HYPERLINKS.getRelation())) {
+                    .getRelationshipsByType(RELATION_HYPERLINK)) {
+                if (!first) {
+                    xhtml.characters(" ");
+                }
+                first = false;
                 xhtml.startElement("a", "href", rel.getTargetURI().toString());
                 xhtml.characters(rel.getTargetURI().toString());
                 xhtml.endElement("a");
@@ -714,101 +765,125 @@ public class XSSFExcelExtractorDecorator extends 
AbstractOOXMLExtractor {
         }
     }
 
-    protected void processShapes(List<XSSFShape> shapes, XHTMLContentHandler 
xhtml)
+    protected void processDrawings(PackagePart sheetPart, XHTMLContentHandler 
xhtml)
             throws SAXException {
-        if (shapes == null) {
-            return;
-        }
-        //We don't currently have an obvious way to get drawings
-        //directly from sheetIter. Therefore, we grab the shapes and process 
those.
-        //To get the diagrams and charts, we need to get the parent drawing 
for each
-        //shape, and we need to make sure that we only process each parent 
shape once!
-        //SEE TIKA-2703 TODO: add unit test
-        Set<String> seenParentDrawings = new HashSet<>();
-        for (XSSFShape shape : shapes) {
-            if (shape instanceof XSSFSimpleShape) {
-                String sText = ((XSSFSimpleShape) shape).getText();
-                if (sText != null && sText.length() > 0) {
-                    xhtml.element("p", sText);
+        try {
+            for (PackageRelationship rel : sheetPart
+                    .getRelationshipsByType(RELATION_DRAWING)) {
+                if (rel.getTargetMode() != TargetMode.INTERNAL) {
+                    continue;
                 }
-                extractHyperLinksFromShape(((XSSFSimpleShape) 
shape).getCTShape(), xhtml);
-            }
-
-            XSSFDrawing parentDrawing = shape.getDrawing();
-            if (parentDrawing != null) {
-                if (!seenParentDrawings
-                        
.contains(parentDrawing.getPackagePart().getPartName().toString())) {
-                    //dump diagram data
-                    
handleGeneralTextContainingPart(AbstractOOXMLExtractor.RELATION_DIAGRAM_DATA,
-                            "diagram-data", parentDrawing.getPackagePart(), 
metadata,
-                            new OOXMLWordAndPowerPointTextHandler(
-                                    new OOXMLTikaBodyPartHandler(xhtml),
-                                    new HashMap<>()//empty
-                            ));
-                    //dump chart data
-                    
handleGeneralTextContainingPart(XSSFRelation.CHART.getRelation(), "chart",
-                            parentDrawing.getPackagePart(), metadata,
-                            new OOXMLWordAndPowerPointTextHandler(
-                                    new OOXMLTikaBodyPartHandler(xhtml),
-                                    new HashMap<>()//empty
-                            ));
+                PackagePartName relName =
+                        PackagingURIHelper.createPartName(rel.getTargetURI());
+                PackagePart drawingPart = rel.getPackage().getPart(relName);
+                if (drawingPart == null) {
+                    continue;
+                }
+                // SAX-parse drawing XML for shape text and hyperlinks
+                try (InputStream is = drawingPart.getInputStream()) {
+                    XMLReaderUtils.parseSAX(is,
+                            new DrawingShapeHandler(xhtml, drawingHyperlinks),
+                            parseContext);
+                } catch (IOException | TikaException e) {
+                    //swallow
                 }
-                
seenParentDrawings.add(parentDrawing.getPackagePart().getPartName().toString());
+                // Process diagram and chart data through drawing part 
relationships
+                handleGeneralTextContainingPart(
+                        AbstractOOXMLExtractor.RELATION_DIAGRAM_DATA,
+                        "diagram-data", drawingPart, metadata,
+                        new OOXMLWordAndPowerPointTextHandler(
+                                new OOXMLTikaBodyPartHandler(xhtml),
+                                new HashMap<>()));
+                handleGeneralTextContainingPart(RELATION_CHART, "chart",
+                        drawingPart, metadata,
+                        new OOXMLWordAndPowerPointTextHandler(
+                                new OOXMLTikaBodyPartHandler(xhtml),
+                                new HashMap<>()));
             }
+        } catch (InvalidFormatException e) {
+            //swallow
         }
     }
 
-    private void extractHyperLinksFromShape(CTShape ctShape, 
XHTMLContentHandler xhtml)
-            throws SAXException {
-
-        if (ctShape == null) {
-            return;
-        }
+    /**
+     * SAX handler for drawing XML that extracts shape text and hyperlinks
+     * without requiring XMLBeans or the POI usermodel (XSSFShape, etc.).
+     */
+    private static class DrawingShapeHandler extends DefaultHandler {
 
-        CTShapeNonVisual nvSpPR = ctShape.getNvSpPr();
-        if (nvSpPR == null) {
-            return;
-        }
+        private final XHTMLContentHandler xhtml;
+        private final Map<String, String> hyperlinks;
 
-        CTNonVisualDrawingProps cNvPr = nvSpPR.getCNvPr();
-        if (cNvPr == null) {
-            return;
-        }
+        private boolean inTxBody;
+        private boolean inT;
+        private final StringBuilder textBuffer = new StringBuilder();
+        private final StringBuilder shapeText = new StringBuilder();
 
-        CTHyperlink ctHyperlink = cNvPr.getHlinkClick();
-        if (ctHyperlink == null) {
-            return;
+        DrawingShapeHandler(XHTMLContentHandler xhtml, Map<String, String> 
hyperlinks) {
+            this.xhtml = xhtml;
+            this.hyperlinks = hyperlinks;
         }
 
-        String url = drawingHyperlinks.get(ctHyperlink.getId());
-        if (url != null) {
-            xhtml.startElement("a", "href", url);
-            xhtml.characters(url);
-            xhtml.endElement("a");
+        @Override
+        public void startElement(String uri, String localName, String qName,
+                                 Attributes atts) throws SAXException {
+            if ("txBody".equals(localName)) {
+                inTxBody = true;
+                shapeText.setLength(0);
+            } else if ("t".equals(localName) && inTxBody) {
+                inT = true;
+                textBuffer.setLength(0);
+            } else if ("hlinkClick".equals(localName) || 
"hlinkHover".equals(localName)) {
+                String rId = atts.getValue(NS_RELATIONSHIPS, "id");
+                if (rId == null) {
+                    // try non-namespace-aware fallback
+                    rId = atts.getValue("r:id");
+                }
+                if (rId != null) {
+                    String url = hyperlinks.get(rId);
+                    if (url != null) {
+                        xhtml.startElement("a", "href", url);
+                        xhtml.characters(url);
+                        xhtml.endElement("a");
+                    }
+                }
+            }
         }
 
-        CTHyperlink ctHoverHyperlink = cNvPr.getHlinkHover();
-        if (ctHoverHyperlink == null) {
-            return;
+        @Override
+        public void endElement(String uri, String localName, String qName)
+                throws SAXException {
+            if ("t".equals(localName) && inT) {
+                inT = false;
+                shapeText.append(textBuffer);
+            } else if ("p".equals(localName) && inTxBody &&
+                    shapeText.length() > 0) {
+                shapeText.append('\n');
+            } else if ("txBody".equals(localName)) {
+                inTxBody = false;
+                String text = shapeText.toString().trim();
+                if (!text.isEmpty()) {
+                    xhtml.element("p", text);
+                }
+            }
         }
 
-        url = drawingHyperlinks.get(ctHoverHyperlink.getId());
-        if (url != null) {
-            xhtml.startElement("a", "href", url);
-            xhtml.characters(url);
-            xhtml.endElement("a");
+        @Override
+        public void characters(char[] ch, int start, int length) {
+            if (inT) {
+                textBuffer.append(ch, start, length);
+            }
         }
-
     }
 
-    public void processSheet(SheetContentsHandler sheetContentsHandler, 
Comments comments,
-                             StylesTable styles, ReadOnlySharedStringsTable 
strings,
+    public void processSheet(TikaSheetContentsHandler sheetContentsHandler,
+                             XSSFCommentsShim commentsShim,
+                             XSSFStylesShim stylesShim, XSSFSharedStringsShim 
stringsShim,
                              InputStream sheetInputStream) throws IOException, 
SAXException {
         try {
-
             XSSFSheetInterestingPartsCapturer handler = new 
XSSFSheetInterestingPartsCapturer(
-                    new XSSFSheetXMLHandler(styles, comments, strings, 
sheetContentsHandler,
-                            formatter, false));
+                    new TikaSheetXMLHandler(stylesShim, commentsShim, 
stringsShim,
+                            sheetContentsHandler, formatter, false));
             XMLReaderUtils.parseSAX(sheetInputStream, handler, parseContext);
             sheetInputStream.close();
 
@@ -826,6 +901,33 @@ public class XSSFExcelExtractorDecorator extends 
AbstractOOXMLExtractor {
         }
     }
 
+    /**
+     * Parse the comments XML for a sheet part via SAX, avoiding XMLBeans.
+     */
+    private XSSFCommentsShim parseSheetComments(PackagePart sheetPart) {
+        try {
+            PackageRelationshipCollection rels =
+                    sheetPart.getRelationshipsByType(RELATION_COMMENTS);
+            if (rels.isEmpty()) {
+                return null;
+            }
+            PackageRelationship rel = rels.getRelationship(0);
+            PackagePartName partName =
+                    PackagingURIHelper.createPartName(rel.getTargetURI());
+            PackagePart commentsPart = rel.getPackage().getPart(partName);
+            if (commentsPart == null) {
+                return null;
+            }
+            try (InputStream is = commentsPart.getInputStream()) {
+                return new XSSFCommentsShim(is, parseContext);
+            }
+        } catch (InvalidFormatException | IOException | TikaException | 
SAXException e) {
+            //swallow — comments are not critical
+            return null;
+        }
+    }
+
+
     /**
      * In Excel files, sheets have things embedded in them,
      * and sheet drawings which have the images
@@ -833,26 +935,33 @@ public class XSSFExcelExtractorDecorator extends 
AbstractOOXMLExtractor {
     @Override
     protected List<PackagePart> getMainDocumentParts() throws TikaException {
         List<PackagePart> parts = new ArrayList<>();
+        // The sheet order in sheetParts mirrors the workbook's sheet
+        // ordering (populated in buildXHTML), so the index here is the
+        // 1-based sheet number.
+        int sheetNumber = 0;
         for (PackagePart part : sheetParts) {
+            sheetNumber++;
             // Add the sheet
             parts.add(part);
 
             // If it has drawings, return those too
             try {
                 for (PackageRelationship rel : part
-                        
.getRelationshipsByType(XSSFRelation.DRAWINGS.getRelation())) {
+                        .getRelationshipsByType(RELATION_DRAWING)) {
                     if (rel.getTargetMode() == TargetMode.INTERNAL) {
                         PackagePartName relName =
                                 
PackagingURIHelper.createPartName(rel.getTargetURI());
-                        parts.add(rel.getPackage().getPart(relName));
+                        PackagePart drawingPart = 
rel.getPackage().getPart(relName);
+                        parts.add(drawingPart);
                     }
                 }
                 for (PackageRelationship rel : part
-                        
.getRelationshipsByType(XSSFRelation.VML_DRAWINGS.getRelation())) {
+                        .getRelationshipsByType(RELATION_VML_DRAWING)) {
                     if (rel.getTargetMode() == TargetMode.INTERNAL) {
                         PackagePartName relName =
                                 
PackagingURIHelper.createPartName(rel.getTargetURI());
-                        parts.add(rel.getPackage().getPart(relName));
+                        PackagePart vmlPart = 
rel.getPackage().getPart(relName);
+                        parts.add(vmlPart);
                     }
                 }
             } catch (InvalidFormatException e) {
@@ -862,7 +971,7 @@ public class XSSFExcelExtractorDecorator extends 
AbstractOOXMLExtractor {
 
         //add main document so that macros can be extracted
         //by AbstractOOXMLExtractor
-        parts.addAll(extractor.getPackage()
+        parts.addAll(opcPackage
                 
.getPartsByRelationshipType(PackageRelationshipTypes.CORE_DOCUMENT));
 
         return parts;
@@ -871,7 +980,8 @@ public class XSSFExcelExtractorDecorator extends 
AbstractOOXMLExtractor {
     /**
      * Turns formatted sheet events into HTML
      */
-    protected static class SheetTextAsHTML implements SheetContentsHandler {
+    protected static class SheetTextAsHTML
+            implements TikaSheetContentsHandler, SheetContentsHandler {
         private final boolean includeHeadersFooters;
         private final boolean includeMissingRows;
         protected List<String> headers;
@@ -879,6 +989,12 @@ public class XSSFExcelExtractorDecorator extends 
AbstractOOXMLExtractor {
         private XHTMLContentHandler xhtml;
         private int lastSeenRow = -1;
         private int lastSeenCol = -1;
+        // Track open <tr>/<td> so the outer catch can emit balanced closes
+        // when processSheet throws part-way through a row (e.g., a malformed
+        // sheet XML). Without this, the outer code would emit </tbody></table>
+        // while <tr> (or <td>) was still on the stack, producing malformed 
XHTML.
+        private boolean rowOpen;
+        private boolean cellOpen;
 
         protected SheetTextAsHTML(OfficeParserConfig config, 
XHTMLContentHandler xhtml) {
             this.includeHeadersFooters = config.isIncludeHeadersAndFooters();
@@ -894,14 +1010,19 @@ public class XSSFExcelExtractorDecorator extends 
AbstractOOXMLExtractor {
                 if (includeMissingRows && rowNum > (lastSeenRow + 1)) {
                     for (int rn = lastSeenRow + 1; rn < rowNum; rn++) {
                         xhtml.startElement("tr");
+                        rowOpen = true;
                         xhtml.startElement("td");
+                        cellOpen = true;
                         xhtml.endElement("td");
+                        cellOpen = false;
                         xhtml.endElement("tr");
+                        rowOpen = false;
                     }
                 }
 
                 // Start the new row
                 xhtml.startElement("tr");
+                rowOpen = true;
                 lastSeenCol = -1;
             } catch (SAXException e) {
                 //swallow
@@ -913,24 +1034,45 @@ public class XSSFExcelExtractorDecorator extends 
AbstractOOXMLExtractor {
         public void endRow(int rowNum) {
             try {
                 xhtml.endElement("tr");
+                rowOpen = false;
             } catch (SAXException e) {
                 throw new RuntimeSAXException(e);
             }
         }
 
-        public void cell(String cellRef, String formattedValue, XSSFComment 
comment) {
+        /**
+         * Closes any pending {@code <tr>} or {@code <td>} that was opened
+         * before a {@link SAXException} interrupted sheet processing. Safe to
+         * call when nothing is open.
+         */
+        void closeAnyPending() throws SAXException {
+            if (cellOpen) {
+                xhtml.endElement("td");
+                cellOpen = false;
+            }
+            if (rowOpen) {
+                xhtml.endElement("tr");
+                rowOpen = false;
+            }
+        }
+
+        public void cell(String cellRef, String formattedValue,
+                          XSSFCommentsShim.CommentData comment) {
             try {
                 // Handle any missing cells
                 int colNum =
                         (cellRef == null) ? lastSeenCol + 1 : (new 
CellReference(cellRef)).getCol();
                 for (int cn = lastSeenCol + 1; cn < colNum; cn++) {
                     xhtml.startElement("td");
+                    cellOpen = true;
                     xhtml.endElement("td");
+                    cellOpen = false;
                 }
                 lastSeenCol = colNum;
 
                 // Start this cell
                 xhtml.startElement("td");
+                cellOpen = true;
 
                 // Main cell contents
                 if (formattedValue != null) {
@@ -943,15 +1085,31 @@ public class XSSFExcelExtractorDecorator extends 
AbstractOOXMLExtractor {
                     xhtml.endElement("br");
                     xhtml.characters(comment.getAuthor());
                     xhtml.characters(": ");
-                    xhtml.characters(comment.getString().getString());
+                    xhtml.characters(comment.getText());
                 }
 
                 xhtml.endElement("td");
+                cellOpen = false;
             } catch (SAXException e) {
                 throw new RuntimeSAXException(e);
             }
         }
 
+        /**
+         * Bridge for POI's {@link SheetContentsHandler} interface, used by the
+         * XLSB (binary) path via {@link 
org.apache.poi.xssf.binary.XSSFBSheetHandler}.
+         */
+        public void cell(String cellRef, String formattedValue, XSSFComment 
comment) {
+            XSSFCommentsShim.CommentData commentData = null;
+            if (comment != null) {
+                String text = comment.getString() != null ?
+                        comment.getString().getString() : "";
+                commentData = new XSSFCommentsShim.CommentData(
+                        comment.getAuthor(), text);
+            }
+            cell(cellRef, formattedValue, commentData);
+        }
+
         public void headerFooter(String text, boolean isHeader, String 
tagName) {
             if (!includeHeadersFooters) {
                 return;
@@ -962,6 +1120,11 @@ public class XSSFExcelExtractorDecorator extends 
AbstractOOXMLExtractor {
                 footers.add(text);
             }
         }
+
+        @Override
+        public void endSheet() {
+            // no-op — satisfies both TikaSheetContentsHandler and 
SheetContentsHandler
+        }
     }
 
     protected static class HeaderFooterFromString implements HeaderFooter {
@@ -1143,4 +1306,5 @@ public class XSSFExcelExtractorDecorator extends 
AbstractOOXMLExtractor {
             }
         }
     }
+
 }
diff --git 
a/tika-parsers/tika-parsers-standard/tika-parsers-standard-modules/tika-parser-microsoft-module/src/main/java/org/apache/tika/parser/microsoft/ooxml/XSSFSharedStringsShim.java
 
b/tika-parsers/tika-parsers-standard/tika-parsers-standard-modules/tika-parser-microsoft-module/src/main/java/org/apache/tika/parser/microsoft/ooxml/XSSFSharedStringsShim.java
new file mode 100644
index 0000000000..8556d0fbb3
--- /dev/null
+++ 
b/tika-parsers/tika-parsers-standard/tika-parsers-standard-modules/tika-parser-microsoft-module/src/main/java/org/apache/tika/parser/microsoft/ooxml/XSSFSharedStringsShim.java
@@ -0,0 +1,156 @@
+/*
+ * 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.microsoft.ooxml;
+
+import java.io.IOException;
+import java.io.InputStream;
+import java.util.ArrayList;
+import java.util.List;
+
+import org.xml.sax.Attributes;
+import org.xml.sax.SAXException;
+import org.xml.sax.helpers.DefaultHandler;
+
+import org.apache.tika.exception.TikaException;
+import org.apache.tika.parser.ParseContext;
+import org.apache.tika.utils.XMLReaderUtils;
+
+/**
+ * SAX-based shim that replaces POI's {@code ReadOnlySharedStringsTable}
+ * for XLSX event-based parsing.
+ * <p>
+ * Parses {@code xl/sharedStrings.xml} and stores each shared string entry
+ * as a plain {@code String}, avoiding the XMLBeans dependency that
+ * {@code XSSFRichTextString} requires. Rich text runs within a single
+ * {@code <si>} are concatenated into a single string.
+ */
+class XSSFSharedStringsShim {
+
+    private final List<String> strings;
+    private final boolean includePhoneticRuns;
+
+    XSSFSharedStringsShim(InputStream sharedStringsData,
+                           boolean includePhoneticRuns,
+                           ParseContext parseContext)
+            throws IOException, SAXException, TikaException {
+        this.includePhoneticRuns = includePhoneticRuns;
+        SharedStringsHandler handler = new SharedStringsHandler();
+        if (sharedStringsData != null) {
+            try {
+                XMLReaderUtils.parseSAX(sharedStringsData, handler, 
parseContext);
+            } finally {
+                sharedStringsData.close();
+            }
+        }
+        this.strings = handler.strings;
+    }
+
+    String getItemAt(int idx) {
+        return strings.get(idx);
+    }
+
+    int getCount() {
+        return strings.size();
+    }
+
+    private class SharedStringsHandler extends DefaultHandler {
+
+        private static final String NS =
+                "http://schemas.openxmlformats.org/spreadsheetml/2006/main";;
+
+        final List<String> strings = new ArrayList<>();
+        private StringBuilder characters;
+        private boolean tIsOpen;
+        private boolean inRPh;
+
+        @Override
+        public void startElement(String uri, String localName, String qName,
+                                 Attributes attributes) {
+            if (uri != null && !NS.equals(uri)) {
+                return;
+            }
+            switch (localName) {
+                case "sst":
+                    String uniqueCount = attributes.getValue("uniqueCount");
+                    if (uniqueCount != null) {
+                        try {
+                            int hint = (int) Long.parseLong(uniqueCount);
+                            // guard against corrupt files with absurd counts
+                            ((ArrayList<String>) strings).ensureCapacity(
+                                    Math.min(hint, 100_000));
+                        } catch (NumberFormatException e) {
+                            // ignore
+                        }
+                    }
+                    characters = new StringBuilder(64);
+                    break;
+                case "si":
+                    if (characters != null) {
+                        characters.setLength(0);
+                    }
+                    break;
+                case "t":
+                    tIsOpen = true;
+                    break;
+                case "rPh":
+                    inRPh = true;
+                    if (includePhoneticRuns && characters != null &&
+                            characters.length() > 0) {
+                        characters.append(" ");
+                    }
+                    break;
+                default:
+                    break;
+            }
+        }
+
+        @Override
+        public void endElement(String uri, String localName, String qName) {
+            if (uri != null && !NS.equals(uri)) {
+                return;
+            }
+            switch (localName) {
+                case "si":
+                    if (characters != null) {
+                        strings.add(characters.toString());
+                    }
+                    break;
+                case "t":
+                    tIsOpen = false;
+                    break;
+                case "rPh":
+                    inRPh = false;
+                    break;
+                default:
+                    break;
+            }
+        }
+
+        @Override
+        public void characters(char[] ch, int start, int length) {
+            if (tIsOpen && characters != null) {
+                if (inRPh) {
+                    if (includePhoneticRuns) {
+                        characters.append(ch, start, length);
+                    }
+                } else {
+                    characters.append(ch, start, length);
+                }
+            }
+        }
+    }
+}
diff --git 
a/tika-parsers/tika-parsers-standard/tika-parsers-standard-modules/tika-parser-microsoft-module/src/main/java/org/apache/tika/parser/microsoft/ooxml/XSSFStylesShim.java
 
b/tika-parsers/tika-parsers-standard/tika-parsers-standard-modules/tika-parser-microsoft-module/src/main/java/org/apache/tika/parser/microsoft/ooxml/XSSFStylesShim.java
new file mode 100644
index 0000000000..ca99c7512e
--- /dev/null
+++ 
b/tika-parsers/tika-parsers-standard/tika-parsers-standard-modules/tika-parser-microsoft-module/src/main/java/org/apache/tika/parser/microsoft/ooxml/XSSFStylesShim.java
@@ -0,0 +1,146 @@
+/*
+ * 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.microsoft.ooxml;
+
+import java.io.IOException;
+import java.io.InputStream;
+import java.util.ArrayList;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+
+import org.apache.poi.ss.usermodel.BuiltinFormats;
+import org.xml.sax.Attributes;
+import org.xml.sax.SAXException;
+import org.xml.sax.helpers.DefaultHandler;
+
+import org.apache.tika.exception.TikaException;
+import org.apache.tika.parser.ParseContext;
+import org.apache.tika.utils.XMLReaderUtils;
+
+/**
+ * SAX-based shim that replaces POI's {@code StylesTable} for XLSX event-based 
parsing.
+ * <p>
+ * Parses {@code xl/styles.xml} and extracts only the information needed for 
text
+ * extraction: the number format resolution chain (cellXfs index to format 
string).
+ * This avoids the XMLBeans dependency that {@code StylesTable} requires.
+ */
+class XSSFStylesShim {
+
+    private final Map<Short, String> numberFormats = new HashMap<>();
+    private final List<Short> cellXfFormatIds = new ArrayList<>();
+
+    XSSFStylesShim(InputStream stylesData, ParseContext parseContext)
+            throws IOException, SAXException, TikaException {
+        if (stylesData != null) {
+            try {
+                XMLReaderUtils.parseSAX(stylesData, new StylesHandler(), 
parseContext);
+            } finally {
+                stylesData.close();
+            }
+        }
+    }
+
+    int getNumCellStyles() {
+        return cellXfFormatIds.size();
+    }
+
+    short getFormatIndex(int styleIndex) {
+        if (styleIndex < 0 || styleIndex >= cellXfFormatIds.size()) {
+            return -1;
+        }
+        return cellXfFormatIds.get(styleIndex);
+    }
+
+    String getFormatString(int styleIndex) {
+        short fmtId = getFormatIndex(styleIndex);
+        if (fmtId == -1) {
+            return null;
+        }
+        String fmt = numberFormats.get(fmtId);
+        if (fmt == null) {
+            fmt = BuiltinFormats.getBuiltinFormat(fmtId);
+        }
+        return fmt;
+    }
+
+    private class StylesHandler extends DefaultHandler {
+
+        private static final String NS =
+                "http://schemas.openxmlformats.org/spreadsheetml/2006/main";;
+
+        private boolean inCellXfs;
+        private boolean inNumFmts;
+
+        @Override
+        public void startElement(String uri, String localName, String qName,
+                                 Attributes attributes) {
+            if (!NS.equals(uri)) {
+                return;
+            }
+            switch (localName) {
+                case "numFmts":
+                    inNumFmts = true;
+                    break;
+                case "numFmt":
+                    if (inNumFmts) {
+                        String idStr = attributes.getValue("numFmtId");
+                        String code = attributes.getValue("formatCode");
+                        if (idStr != null && code != null) {
+                            try {
+                                numberFormats.put(Short.parseShort(idStr), 
code);
+                            } catch (NumberFormatException e) {
+                                // skip malformed
+                            }
+                        }
+                    }
+                    break;
+                case "cellXfs":
+                    inCellXfs = true;
+                    break;
+                case "xf":
+                    if (inCellXfs) {
+                        String numFmtIdStr = attributes.getValue("numFmtId");
+                        short numFmtId = 0;
+                        if (numFmtIdStr != null) {
+                            try {
+                                numFmtId = Short.parseShort(numFmtIdStr);
+                            } catch (NumberFormatException e) {
+                                // default to 0 (General)
+                            }
+                        }
+                        cellXfFormatIds.add(numFmtId);
+                    }
+                    break;
+                default:
+                    break;
+            }
+        }
+
+        @Override
+        public void endElement(String uri, String localName, String qName) {
+            if (!NS.equals(uri)) {
+                return;
+            }
+            if ("numFmts".equals(localName)) {
+                inNumFmts = false;
+            } else if ("cellXfs".equals(localName)) {
+                inCellXfs = false;
+            }
+        }
+    }
+}
diff --git 
a/tika-parsers/tika-parsers-standard/tika-parsers-standard-modules/tika-parser-microsoft-module/src/test/java/org/apache/tika/parser/microsoft/ooxml/VSDXParserTest.java
 
b/tika-parsers/tika-parsers-standard/tika-parsers-standard-modules/tika-parser-microsoft-module/src/test/java/org/apache/tika/parser/microsoft/ooxml/VSDXParserTest.java
new file mode 100644
index 0000000000..c4a832bd96
--- /dev/null
+++ 
b/tika-parsers/tika-parsers-standard/tika-parsers-standard-modules/tika-parser-microsoft-module/src/test/java/org/apache/tika/parser/microsoft/ooxml/VSDXParserTest.java
@@ -0,0 +1,41 @@
+/*
+ * 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.microsoft.ooxml;
+
+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.Metadata;
+import org.apache.tika.metadata.TikaCoreProperties;
+
+public class VSDXParserTest extends TikaTest {
+
+    @Test
+    public void testBasicTextExtraction() throws Exception {
+        List<Metadata> metadataList = getRecursiveMetadata("testVISIO.vsdx");
+        String content = 
metadataList.get(0).get(TikaCoreProperties.TIKA_CONTENT);
+        assertEquals("application/vnd.ms-visio.drawing",
+                metadataList.get(0).get(Metadata.CONTENT_TYPE));
+        assertContains("test", content);
+        assertContains("This is a test.", content);
+        assertContains("Nothing fancy.", content);
+    }
+}

Reply via email to