From bc6ee96e1a409cfeae97c6cd2805b2ef9c420ac7 Mon Sep 17 00:00:00 2001 From: Dustin Spicuzza Date: Mon, 19 Oct 2015 06:26:57 +0000 Subject: [PATCH] Add Visio OOXML text extractor + tests git-svn-id: https://svn.apache.org/repos/asf/poi/trunk@1709361 13f79535-47bb-0310-9956-ffa450edef68 --- .../org/apache/poi/TestAllFiles.java | 2 +- .../apache/poi/stress/XDGFFileHandler.java | 36 ++----------- .../poi/extractor/ExtractorFactory.java | 9 ++-- .../xdgf/extractor/XDGFVisioExtractor.java | 51 ++++++++++++++++++ .../poi/xdgf/usermodel/XDGFDocument.java | 3 ++ .../poi/xdgf/usermodel/XmlVisioDocument.java | 9 +++- .../usermodel/shape/ShapeTextVisitor.java | 41 ++++++++++++++ .../poi/extractor/TestExtractorFactory.java | 37 ++++++++----- .../extractor/TestXDGFVisioExtractor.java | 39 ++++++++++++++ test-data/diagram/test_text_extraction.vsdx | Bin 0 -> 22343 bytes 10 files changed, 176 insertions(+), 51 deletions(-) create mode 100644 src/ooxml/java/org/apache/poi/xdgf/extractor/XDGFVisioExtractor.java create mode 100644 src/ooxml/java/org/apache/poi/xdgf/usermodel/shape/ShapeTextVisitor.java create mode 100644 src/ooxml/testcases/org/apache/poi/xdgf/extractor/TestXDGFVisioExtractor.java create mode 100644 test-data/diagram/test_text_extraction.vsdx diff --git a/src/integrationtest/org/apache/poi/TestAllFiles.java b/src/integrationtest/org/apache/poi/TestAllFiles.java index d453da27f9..6231065e7f 100644 --- a/src/integrationtest/org/apache/poi/TestAllFiles.java +++ b/src/integrationtest/org/apache/poi/TestAllFiles.java @@ -105,7 +105,7 @@ public class TestAllFiles { // Visio - binary HANDLERS.put(".vsd", new HDGFFileHandler()); - // Visio - ooxml (currently unsupported) + // Visio - ooxml HANDLERS.put(".vsdm", new XDGFFileHandler()); HANDLERS.put(".vsdx", new XDGFFileHandler()); HANDLERS.put(".vssm", new XDGFFileHandler()); diff --git a/src/integrationtest/org/apache/poi/stress/XDGFFileHandler.java b/src/integrationtest/org/apache/poi/stress/XDGFFileHandler.java index 4c4fd6088b..9b7d03f8a5 100644 --- a/src/integrationtest/org/apache/poi/stress/XDGFFileHandler.java +++ b/src/integrationtest/org/apache/poi/stress/XDGFFileHandler.java @@ -16,19 +16,11 @@ ==================================================================== */ package org.apache.poi.stress; -import java.io.File; -import java.io.IOException; import java.io.InputStream; -import java.util.ArrayList; -import java.util.List; -import org.apache.poi.POIXMLDocument; -import org.apache.poi.openxml4j.exceptions.OpenXML4JException; import org.apache.poi.openxml4j.opc.OPCPackage; import org.apache.poi.openxml4j.opc.PackageAccess; -import org.apache.poi.openxml4j.opc.PackagePart; -import org.apache.poi.openxml4j.opc.PackageRelationshipTypes; -import org.apache.poi.util.PackageHelper; +import org.apache.poi.xdgf.usermodel.XmlVisioDocument; import org.junit.Test; public class XDGFFileHandler extends AbstractFileHandler { @@ -37,39 +29,19 @@ public class XDGFFileHandler extends AbstractFileHandler { // ignore password protected files if (POIXMLDocumentHandler.isEncrypted(stream)) return; - TestXDGFXMLDocument doc = new TestXDGFXMLDocument(stream); + XmlVisioDocument doc = new XmlVisioDocument(stream); new POIXMLDocumentHandler().handlePOIXMLDocument(doc); } - - @Override - public void handleExtracting(File file) throws Exception { - // TODO: extraction/actual operations not supported yet - } - + // a test-case to test this locally without executing the full TestAllFiles @Test public void test() throws Exception { OPCPackage pkg = OPCPackage.open("test-data/diagram/test.vsdx", PackageAccess.READ); try { - TestXDGFXMLDocument doc = new TestXDGFXMLDocument(pkg); + XmlVisioDocument doc = new XmlVisioDocument(pkg); new POIXMLDocumentHandler().handlePOIXMLDocument(doc); } finally { pkg.close(); } } - - // TODO: Get rid of this when full visio ooxml support is added - private final static class TestXDGFXMLDocument extends POIXMLDocument { - public TestXDGFXMLDocument(OPCPackage pkg) { - super(pkg, PackageRelationshipTypes.VISIO_CORE_DOCUMENT); - } - - public TestXDGFXMLDocument(InputStream is) throws IOException { - this(PackageHelper.open(is)); - } - - public List getAllEmbedds() throws OpenXML4JException { - return new ArrayList(); - } - } } \ No newline at end of file diff --git a/src/ooxml/java/org/apache/poi/extractor/ExtractorFactory.java b/src/ooxml/java/org/apache/poi/extractor/ExtractorFactory.java index 59ebeb10d0..906c026375 100644 --- a/src/ooxml/java/org/apache/poi/extractor/ExtractorFactory.java +++ b/src/ooxml/java/org/apache/poi/extractor/ExtractorFactory.java @@ -55,6 +55,7 @@ import org.apache.poi.poifs.filesystem.NotOLE2FileException; import org.apache.poi.poifs.filesystem.OPOIFSFileSystem; import org.apache.poi.poifs.filesystem.OfficeXmlFileException; import org.apache.poi.poifs.filesystem.POIFSFileSystem; +import org.apache.poi.xdgf.extractor.XDGFVisioExtractor; import org.apache.poi.xslf.extractor.XSLFPowerPointExtractor; import org.apache.poi.xslf.usermodel.XSLFRelation; import org.apache.poi.xslf.usermodel.XSLFSlideShow; @@ -172,11 +173,9 @@ public class ExtractorFactory { } if (core.size() == 0) { // Could it be a visio one? - PackageRelationshipCollection visio = - pkg.getRelationshipsByType(VISIO_DOCUMENT_REL); - if (visio.size() == 1) { - throw new IllegalArgumentException("Text extraction not supported for Visio OOXML files"); - } + core = pkg.getRelationshipsByType(VISIO_DOCUMENT_REL); + if (core.size() == 1) + return new XDGFVisioExtractor(pkg); } // Should just be a single core document, complain if not diff --git a/src/ooxml/java/org/apache/poi/xdgf/extractor/XDGFVisioExtractor.java b/src/ooxml/java/org/apache/poi/xdgf/extractor/XDGFVisioExtractor.java new file mode 100644 index 0000000000..c49c2121dc --- /dev/null +++ b/src/ooxml/java/org/apache/poi/xdgf/extractor/XDGFVisioExtractor.java @@ -0,0 +1,51 @@ +package org.apache.poi.xdgf.extractor; + +import java.io.IOException; + +import org.apache.poi.POIXMLDocument; +import org.apache.poi.POIXMLTextExtractor; +import org.apache.poi.openxml4j.opc.OPCPackage; +import org.apache.poi.xdgf.usermodel.XDGFPage; +import org.apache.poi.xdgf.usermodel.XmlVisioDocument; +import org.apache.poi.xdgf.usermodel.shape.ShapeTextVisitor; + +/** + * Helper class to extract text from an OOXML Visio File + */ +public class XDGFVisioExtractor extends POIXMLTextExtractor { + + protected final XmlVisioDocument document; + + public XDGFVisioExtractor(XmlVisioDocument document) { + super(document); + this.document = document; + } + + public XDGFVisioExtractor(OPCPackage openPackage) throws IOException { + this(new XmlVisioDocument(openPackage)); + } + + public String getText() { + ShapeTextVisitor visitor = new ShapeTextVisitor(); + + for (XDGFPage page: document.getPages()) { + page.getContent().visitShapes(visitor); + } + + return visitor.getText().toString(); + } + + public static void main(String [] args) throws IOException { + if (args.length < 1) { + System.err.println("Use:"); + System.err.println(" XDGFVisioExtractor "); + System.exit(1); + } + POIXMLTextExtractor extractor = + new XDGFVisioExtractor(POIXMLDocument.openPackage( + args[0] + )); + System.out.println(extractor.getText()); + extractor.close(); + } +} diff --git a/src/ooxml/java/org/apache/poi/xdgf/usermodel/XDGFDocument.java b/src/ooxml/java/org/apache/poi/xdgf/usermodel/XDGFDocument.java index dc42fa192d..ba460cc13c 100644 --- a/src/ooxml/java/org/apache/poi/xdgf/usermodel/XDGFDocument.java +++ b/src/ooxml/java/org/apache/poi/xdgf/usermodel/XDGFDocument.java @@ -29,6 +29,9 @@ import com.microsoft.schemas.office.visio.x2012.main.VisioDocumentType; /** * Represents the root document: /visio/document.xml + * + * You're probably actually looking for {@link XmlVisioDocument}, this + * only contains metadata about the root document in the OOXML package. */ public class XDGFDocument { diff --git a/src/ooxml/java/org/apache/poi/xdgf/usermodel/XmlVisioDocument.java b/src/ooxml/java/org/apache/poi/xdgf/usermodel/XmlVisioDocument.java index e5589c7aaa..8794874040 100644 --- a/src/ooxml/java/org/apache/poi/xdgf/usermodel/XmlVisioDocument.java +++ b/src/ooxml/java/org/apache/poi/xdgf/usermodel/XmlVisioDocument.java @@ -19,6 +19,7 @@ package org.apache.poi.xdgf.usermodel; import java.io.IOException; import java.io.InputStream; +import java.util.ArrayList; import java.util.Collection; import java.util.List; @@ -107,15 +108,21 @@ public class XmlVisioDocument extends POIXMLDocument { _pages.onDocumentRead(); } + /** + * Not currently implemented + */ @Override public List getAllEmbedds() throws OpenXML4JException { - throw new UnsupportedOperationException("Not implemented"); + return new ArrayList(); } // // Useful public API goes here // + /** + * @return pages ordered by page number + */ public Collection getPages() { return _pages.getPageList(); } diff --git a/src/ooxml/java/org/apache/poi/xdgf/usermodel/shape/ShapeTextVisitor.java b/src/ooxml/java/org/apache/poi/xdgf/usermodel/shape/ShapeTextVisitor.java new file mode 100644 index 0000000000..4589bc8ad7 --- /dev/null +++ b/src/ooxml/java/org/apache/poi/xdgf/usermodel/shape/ShapeTextVisitor.java @@ -0,0 +1,41 @@ +package org.apache.poi.xdgf.usermodel.shape; + +import java.awt.geom.AffineTransform; + +import org.apache.poi.xdgf.usermodel.XDGFShape; + +/** + * Only visits text nodes, accumulates text content into a string + * + * The text is returned in arbitrary order, with no regards to + * the location of the text on the page. This may change in the + * future. + */ +public class ShapeTextVisitor extends ShapeVisitor { + + protected StringBuilder text = new StringBuilder(); + + public static class TextAcceptor implements ShapeVisitorAcceptor { + public boolean accept(XDGFShape shape) { + return shape.hasText(); + } + } + + protected ShapeVisitorAcceptor getAcceptor() { + return new TextAcceptor(); + } + + public void visit(XDGFShape shape, AffineTransform globalTransform, + int level) { + text.append(shape.getText().getTextContent().trim()); + text.append('\n'); + } + + /** + * Call this after visitation has completed + */ + public String getText() { + return text.toString(); + } + +} diff --git a/src/ooxml/testcases/org/apache/poi/extractor/TestExtractorFactory.java b/src/ooxml/testcases/org/apache/poi/extractor/TestExtractorFactory.java index 3d2d2a5b15..fba530757a 100644 --- a/src/ooxml/testcases/org/apache/poi/extractor/TestExtractorFactory.java +++ b/src/ooxml/testcases/org/apache/poi/extractor/TestExtractorFactory.java @@ -44,6 +44,7 @@ import org.apache.poi.openxml4j.exceptions.InvalidOperationException; import org.apache.poi.openxml4j.opc.OPCPackage; import org.apache.poi.openxml4j.opc.PackageAccess; import org.apache.poi.poifs.filesystem.POIFSFileSystem; +import org.apache.poi.xdgf.extractor.XDGFVisioExtractor; import org.apache.poi.xslf.extractor.XSLFPowerPointExtractor; import org.apache.poi.xssf.extractor.XSSFEventBasedExcelExtractor; import org.apache.poi.xssf.extractor.XSSFExcelExtractor; @@ -271,12 +272,13 @@ public class TestExtractorFactory { ExtractorFactory.createExtractor(vsd).getText().length() > 50 ); // Visio - vsdx - try { - ExtractorFactory.createExtractor(vsdx); - fail(); - } catch(IllegalArgumentException e) { - // Good - } + assertTrue( + ExtractorFactory.createExtractor(vsdx) + instanceof XDGFVisioExtractor + ); + assertTrue( + ExtractorFactory.createExtractor(vsdx).getText().length() > 20 + ); // Publisher assertTrue( @@ -391,13 +393,15 @@ public class TestExtractorFactory { ExtractorFactory.createExtractor(new FileInputStream(vsd)).getText().length() > 50 ); // Visio - vsdx - try { - ExtractorFactory.createExtractor(new FileInputStream(vsdx)); - fail(); - } catch(IllegalArgumentException e) { - // Good - } + assertTrue( + ExtractorFactory.createExtractor(new FileInputStream(vsdx)) + instanceof XDGFVisioExtractor + ); + assertTrue( + ExtractorFactory.createExtractor(new FileInputStream(vsdx)).getText().length() > 20 + ); + // Publisher assertTrue( ExtractorFactory.createExtractor(new FileInputStream(pub)) @@ -551,6 +555,15 @@ public class TestExtractorFactory { extractor.getText().length() > 120 ); extractor.close(); + + // Visio + assertTrue( + ExtractorFactory.createExtractor(OPCPackage.open(vsdx.toString())) + instanceof XDGFVisioExtractor + ); + assertTrue( + extractor.getText().length() > 20 + ); // Text try { diff --git a/src/ooxml/testcases/org/apache/poi/xdgf/extractor/TestXDGFVisioExtractor.java b/src/ooxml/testcases/org/apache/poi/xdgf/extractor/TestXDGFVisioExtractor.java new file mode 100644 index 0000000000..4c7459518e --- /dev/null +++ b/src/ooxml/testcases/org/apache/poi/xdgf/extractor/TestXDGFVisioExtractor.java @@ -0,0 +1,39 @@ +package org.apache.poi.xdgf.extractor; + +import java.io.IOException; + +import org.apache.poi.POIDataSamples; +import org.apache.poi.openxml4j.opc.OPCPackage; +import org.apache.poi.xdgf.usermodel.XmlVisioDocument; + +import junit.framework.TestCase; + +public class TestXDGFVisioExtractor extends TestCase { + + private POIDataSamples diagrams; + private OPCPackage pkg; + private XmlVisioDocument xml; + + protected void setUp() throws Exception { + diagrams = POIDataSamples.getDiagramInstance(); + + pkg = OPCPackage.open(diagrams.openResourceAsStream("test_text_extraction.vsdx")); + xml = new XmlVisioDocument(pkg); + } + + public void testGetSimpleText() throws IOException { + new XDGFVisioExtractor(xml).close(); + new XDGFVisioExtractor(pkg).close(); + + XDGFVisioExtractor extractor = new XDGFVisioExtractor(xml); + extractor.getText(); + + String text = extractor.getText(); + assertTrue(text.length() > 0); + + assertEquals("Text here\nText there\nText, text, everywhere!\nRouter here\n", + text); + + extractor.close(); + } +} diff --git a/test-data/diagram/test_text_extraction.vsdx b/test-data/diagram/test_text_extraction.vsdx new file mode 100644 index 0000000000000000000000000000000000000000..39b6401772cec6cee989fa888caee23ca2561e52 GIT binary patch literal 22343 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 B%X0t# literal 0 HcmV?d00001 -- 2.39.5