getSupportedTypes(ParseContext context) {
return SUPPORTED_TYPES;
@@ -63,6 +91,8 @@ public void parse(TikaInputStream tis, ContentHandler handler, Metadata metadata
xhtml.startDocument();
tis.setCloseShield();
try {
+ MetafileParserConfig config = getConfig(context);
+ prepareForRendering(tis, config, metadata);
HwmfPicture picture = null;
try {
picture = new HwmfPicture(tis);
@@ -95,6 +125,10 @@ public void parse(TikaInputStream tis, ContentHandler handler, Metadata metadata
xhtml.endElement("p");
}
}
+ if (config.shouldRender(metadata)) {
+ MetafileRendering.render(getRenderer(), config, MEDIA_TYPE, tis, picture, xhtml,
+ metadata, context);
+ }
} catch (RecordFormatException e) { //POI's hwmfparser can \ throw these for "parse
// exceptions"
throw new TikaException(e.getMessage(), e);
diff --git a/tika-parsers/tika-parsers-standard/tika-parsers-standard-modules/tika-parser-microsoft-module/src/main/java/org/apache/tika/renderer/microsoft/POIMetafileRenderer.java b/tika-parsers/tika-parsers-standard/tika-parsers-standard-modules/tika-parser-microsoft-module/src/main/java/org/apache/tika/renderer/microsoft/POIMetafileRenderer.java
new file mode 100644
index 00000000000..c2fde311e25
--- /dev/null
+++ b/tika-parsers/tika-parsers-standard/tika-parsers-standard-modules/tika-parser-microsoft-module/src/main/java/org/apache/tika/renderer/microsoft/POIMetafileRenderer.java
@@ -0,0 +1,311 @@
+/*
+ * 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.renderer.microsoft;
+
+import java.awt.Color;
+import java.awt.Graphics2D;
+import java.awt.RenderingHints;
+import java.awt.geom.Dimension2D;
+import java.awt.geom.Rectangle2D;
+import java.awt.image.BufferedImage;
+import java.io.IOException;
+import java.io.OutputStream;
+import java.nio.file.Files;
+import java.nio.file.Path;
+import java.util.Arrays;
+import java.util.Collections;
+import java.util.HashSet;
+import java.util.Set;
+import javax.imageio.ImageIO;
+
+import org.apache.poi.hemf.usermodel.HemfPicture;
+import org.apache.poi.hwmf.record.HwmfFill;
+import org.apache.poi.hwmf.record.HwmfRecord;
+import org.apache.poi.hwmf.usermodel.HwmfPicture;
+
+import org.apache.tika.annotation.TikaComponent;
+import org.apache.tika.config.ParseContextConfig;
+import org.apache.tika.exception.TikaException;
+import org.apache.tika.extractor.EmbeddedDocumentUtil;
+import org.apache.tika.io.TemporaryResources;
+import org.apache.tika.io.TikaInputStream;
+import org.apache.tika.metadata.HttpHeaders;
+import org.apache.tika.metadata.Metadata;
+import org.apache.tika.metadata.Rendering;
+import org.apache.tika.metadata.TikaCoreProperties;
+import org.apache.tika.mime.MediaType;
+import org.apache.tika.parser.ParseContext;
+import org.apache.tika.parser.microsoft.MetafileParserConfig;
+import org.apache.tika.renderer.RenderRequest;
+import org.apache.tika.renderer.RenderResult;
+import org.apache.tika.renderer.RenderResults;
+import org.apache.tika.renderer.Renderer;
+import org.apache.tika.renderer.RenderingTracker;
+
+/**
+ * Renders EMF and WMF images to a raster image through POI's HEMF and HWMF,
+ * the way {@code PDFBoxRenderer} renders PDF pages. The rendering has the
+ * configured width, its height follows the image's aspect ratio, and it is
+ * drawn on a white canvas. Metafiles have no pages, so the render requests
+ * are ignored and a single result is returned.
+ *
+ * The WMF thumbnails that Word stores in the SummaryInformation of a .doc
+ * consist of a window extent and a single {@code dibStretchBlt} record, for
+ * which POI cannot compute bounds; those are rendered from the record's
+ * bitmap directly.
+ */
+@TikaComponent(name = "poi-metafile-renderer")
+public class POIMetafileRenderer implements Renderer {
+
+ public static final String RENDERED_BY = "poi-metafile-renderer";
+
+ public static final MediaType EMF = MediaType.image("emf");
+ public static final MediaType WMF = MediaType.image("wmf");
+
+ private static final Set SUPPORTED_TYPES =
+ Collections.unmodifiableSet(new HashSet<>(Arrays.asList(EMF, WMF)));
+
+ private static final int MAX_WIDTH = 10000;
+
+ /**
+ * A metafile declares its own aspect ratio, so a hostile one could ask
+ * for an arbitrarily tall canvas at any width. The renderer refuses
+ * beyond this height; an OutOfMemoryError would escape every catch in
+ * the parse.
+ */
+ private static final int MAX_HEIGHT = 10000;
+
+ private int width = 800;
+ private String imageFormatName = "png";
+
+ @Override
+ public Set getSupportedTypes(ParseContext context) {
+ return SUPPORTED_TYPES;
+ }
+
+ /**
+ * Renders the metafile in the stream, or the {@link HemfPicture} or
+ * {@link HwmfPicture} set as the stream's open container. The metadata's
+ * {@link TikaCoreProperties#TYPE} tells EMF from WMF when a stream is
+ * parsed; it defaults to EMF.
+ */
+ @Override
+ public RenderResults render(TikaInputStream tis, Metadata metadata, ParseContext parseContext,
+ RenderRequest... requests) throws IOException, TikaException {
+ Object picture = tis.getOpenContainer();
+ if (!(picture instanceof HemfPicture) && !(picture instanceof HwmfPicture)) {
+ picture = WMF.toString().equals(metadata.get(TikaCoreProperties.TYPE))
+ ? new HwmfPicture(tis) : new HemfPicture(tis);
+ }
+ RenderingTracker tracker = parseContext.get(RenderingTracker.class);
+ if (tracker == null) {
+ tracker = new RenderingTracker();
+ parseContext.set(RenderingTracker.class, tracker);
+ }
+ int id = tracker.getNextId();
+ int width = width(parseContext, metadata);
+ Metadata renderingMetadata = Metadata.newInstance(parseContext);
+ renderingMetadata.set(TikaCoreProperties.EMBEDDED_RESOURCE_TYPE,
+ TikaCoreProperties.EmbeddedResourceType.RENDERING.name());
+ RenderResults results = new RenderResults(new TemporaryResources());
+ try {
+ long start = System.currentTimeMillis();
+ BufferedImage image = picture instanceof HemfPicture
+ ? draw((HemfPicture) picture, width) : draw((HwmfPicture) picture, width);
+ Path tmpFile = write(image, id);
+ renderingMetadata.set(Rendering.RENDERED_MS, System.currentTimeMillis() - start);
+ renderingMetadata.add(Rendering.RENDERED_BY, RENDERED_BY);
+ renderingMetadata.set(HttpHeaders.CONTENT_TYPE, "image/" + imageFormatName);
+ results.add(new RenderResult(RenderResult.STATUS.SUCCESS, id, tmpFile,
+ renderingMetadata));
+ } catch (SecurityException e) {
+ throw e;
+ } catch (Exception e) {
+ //record the cause, as PDFBoxRenderer does, so the failure is diagnosable
+ EmbeddedDocumentUtil.recordException(e, renderingMetadata, parseContext);
+ results.add(new RenderResult(RenderResult.STATUS.EXCEPTION, id, null,
+ renderingMetadata));
+ }
+ return results;
+ }
+
+ private BufferedImage draw(HemfPicture picture, int width) throws IOException {
+ Dimension2D size = picture.getSize();
+ BufferedImage image = canvas(size, width);
+ Graphics2D graphics = image.createGraphics();
+ try {
+ picture.draw(graphics, new Rectangle2D.Double(0, 0, image.getWidth(),
+ image.getHeight()));
+ } finally {
+ graphics.dispose();
+ }
+ return image;
+ }
+
+ private BufferedImage draw(HwmfPicture picture, int width) throws IOException {
+ Dimension2D size;
+ try {
+ size = picture.getSize();
+ } catch (IllegalStateException e) {
+ //POI throws this for "window records are incomplete": a bitmap
+ //wrapped in a metafile, as Word's .doc thumbnails are
+ BufferedImage bitmap = firstBitmap(picture);
+ if (bitmap == null) {
+ throw new IOException("WMF without bounds and without a bitmap", e);
+ }
+ return scale(bitmap, width);
+ }
+ BufferedImage image = canvas(size, width);
+ Graphics2D graphics = image.createGraphics();
+ try {
+ picture.draw(graphics, new Rectangle2D.Double(0, 0, image.getWidth(),
+ image.getHeight()));
+ } finally {
+ graphics.dispose();
+ }
+ return image;
+ }
+
+ private static BufferedImage firstBitmap(HwmfPicture picture) {
+ for (HwmfRecord record : picture.getRecords()) {
+ if (record instanceof HwmfFill.HwmfImageRecord) {
+ BufferedImage image = ((HwmfFill.HwmfImageRecord) record).getImage();
+ if (image != null) {
+ return image;
+ }
+ }
+ }
+ return null;
+ }
+
+ /**
+ * The width of the rendering: the {@code renderWidth} of the metafile
+ * parser's configuration where the parse has one, else this renderer's
+ * own {@link #setWidth(int)}. The parser configuration wins so that a
+ * request configuring {@code "emf-parser": {"renderWidth": N}} reaches
+ * the renderer the parser was handed, which need not be this instance.
+ */
+ private int width(ParseContext parseContext, Metadata metadata)
+ throws IOException, TikaException {
+ MetafileParserConfig config = parseContext.get(MetafileParserConfig.class);
+ if (config == null) {
+ String component = WMF.toString().equals(metadata.get(TikaCoreProperties.TYPE))
+ ? "wmf-parser" : "emf-parser";
+ if (parseContext.getJsonConfig(component) == null) {
+ return width;
+ }
+ config = ParseContextConfig.getConfig(parseContext, component,
+ MetafileParserConfig.class, new MetafileParserConfig());
+ }
+ return config.getRenderWidth();
+ }
+
+ private BufferedImage canvas(Dimension2D size, int width) throws IOException {
+ if (size == null || size.getWidth() <= 0 || size.getHeight() <= 0) {
+ throw new IOException("metafile without a usable size: " + size);
+ }
+ BufferedImage image = new BufferedImage(width, height(size.getWidth(), size.getHeight(),
+ width), BufferedImage.TYPE_INT_RGB);
+ Graphics2D graphics = image.createGraphics();
+ try {
+ graphics.setRenderingHint(RenderingHints.KEY_ANTIALIASING,
+ RenderingHints.VALUE_ANTIALIAS_ON);
+ graphics.setRenderingHint(RenderingHints.KEY_TEXT_ANTIALIASING,
+ RenderingHints.VALUE_TEXT_ANTIALIAS_ON);
+ graphics.setRenderingHint(RenderingHints.KEY_RENDERING,
+ RenderingHints.VALUE_RENDER_QUALITY);
+ graphics.setColor(Color.WHITE);
+ graphics.fillRect(0, 0, image.getWidth(), image.getHeight());
+ } finally {
+ graphics.dispose();
+ }
+ return image;
+ }
+
+ /**
+ * The height that keeps the aspect ratio at the rendering's width.
+ *
+ * @throws IOException if the ratio asks for an image taller than
+ * {@link #MAX_HEIGHT}
+ */
+ private static int height(double sourceWidth, double sourceHeight, int width)
+ throws IOException {
+ long height = Math.max(1, Math.round(sourceHeight * width / sourceWidth));
+ if (height > MAX_HEIGHT) {
+ throw new IOException("metafile aspect ratio asks for a " + height
+ + " pixel high rendering at width " + width + ", the maximum is " + MAX_HEIGHT);
+ }
+ return (int) height;
+ }
+
+ private BufferedImage scale(BufferedImage bitmap, int width) throws IOException {
+ int height = height(bitmap.getWidth(), bitmap.getHeight(), width);
+ BufferedImage image = new BufferedImage(width, height, BufferedImage.TYPE_INT_RGB);
+ Graphics2D graphics = image.createGraphics();
+ try {
+ graphics.setRenderingHint(RenderingHints.KEY_INTERPOLATION,
+ RenderingHints.VALUE_INTERPOLATION_BILINEAR);
+ graphics.setColor(Color.WHITE);
+ graphics.fillRect(0, 0, width, height);
+ graphics.drawImage(bitmap, 0, 0, width, height, null);
+ } finally {
+ graphics.dispose();
+ }
+ return image;
+ }
+
+ private Path write(BufferedImage image, int id) throws IOException {
+ Path tmpFile = Files.createTempFile("tika-metafile-rendering-",
+ "-" + id + "." + imageFormatName);
+ try (OutputStream os = Files.newOutputStream(tmpFile)) {
+ if (!ImageIO.write(image, imageFormatName, os)) {
+ throw new IOException("no ImageIO writer for " + imageFormatName);
+ }
+ } catch (IOException | RuntimeException e) {
+ Files.deleteIfExists(tmpFile);
+ throw e;
+ }
+ return tmpFile;
+ }
+
+ public int getWidth() {
+ return width;
+ }
+
+ /**
+ * @param width the rendering's width in pixels, 1 to 10000; the height
+ * follows the image's aspect ratio. Default 800.
+ */
+ public void setWidth(int width) {
+ if (width < 1 || width > MAX_WIDTH) {
+ throw new IllegalArgumentException(
+ "width must be between 1 and " + MAX_WIDTH + ", got: " + width);
+ }
+ this.width = width;
+ }
+
+ public String getImageFormatName() {
+ return imageFormatName;
+ }
+
+ /**
+ * @param imageFormatName an ImageIO format name, "png" (default) or "jpeg"
+ */
+ public void setImageFormatName(String imageFormatName) {
+ this.imageFormatName = imageFormatName;
+ }
+}
diff --git a/tika-parsers/tika-parsers-standard/tika-parsers-standard-modules/tika-parser-microsoft-module/src/test/java/org/apache/tika/parser/microsoft/EMFParserTest.java b/tika-parsers/tika-parsers-standard/tika-parsers-standard-modules/tika-parser-microsoft-module/src/test/java/org/apache/tika/parser/microsoft/EMFParserTest.java
index e296c5ef2df..60bb779a214 100644
--- a/tika-parsers/tika-parsers-standard/tika-parsers-standard-modules/tika-parser-microsoft-module/src/test/java/org/apache/tika/parser/microsoft/EMFParserTest.java
+++ b/tika-parsers/tika-parsers-standard/tika-parsers-standard-modules/tika-parser-microsoft-module/src/test/java/org/apache/tika/parser/microsoft/EMFParserTest.java
@@ -17,15 +17,31 @@
package org.apache.tika.parser.microsoft;
import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertNull;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+import java.io.IOException;
+import java.io.InputStream;
+import java.nio.ByteBuffer;
+import java.util.ArrayList;
import java.util.List;
import org.junit.jupiter.api.Test;
+import org.junit.jupiter.params.ParameterizedTest;
+import org.junit.jupiter.params.provider.ValueSource;
+import org.xml.sax.ContentHandler;
import org.apache.tika.TikaTest;
+import org.apache.tika.config.loader.TikaLoader;
+import org.apache.tika.extractor.EmbeddedDocumentExtractor;
+import org.apache.tika.io.TikaInputStream;
import org.apache.tika.metadata.HttpHeaders;
import org.apache.tika.metadata.Metadata;
+import org.apache.tika.metadata.Rendering;
import org.apache.tika.metadata.TikaCoreProperties;
+import org.apache.tika.parser.ParseContext;
+import org.apache.tika.parser.Parser;
+import org.apache.tika.sax.BodyContentHandler;
public class EMFParserTest extends TikaTest {
@@ -54,6 +70,151 @@ public void testIconOnly() throws Exception {
metadataList.get(0).get(TikaCoreProperties.TIKA_CONTENT));
}
+ /**
+ * Rendering is off by default: an EMF yields no embedded document of
+ * its own.
+ */
+ @Test
+ public void testNoRenderingByDefault() throws Exception {
+ List metadataList = getRecursiveMetadata("testEMF.emf");
+ assertEquals(1, metadataList.size());
+ }
+
+ @Test
+ public void testRenderingFromConfig() throws Exception {
+ Parser parser = TikaLoader
+ .load(getConfigPath(EMFParserTest.class, "tika-config-emf-render.json"))
+ .loadParsers();
+ Metadata metadata = new Metadata();
+ metadata.set(TikaCoreProperties.RESOURCE_NAME_KEY, "testEMF.emf");
+ metadata.set(HttpHeaders.CONTENT_TYPE, "image/emf");
+ List metadataList =
+ getRecursiveMetadata("testEMF.emf", parser, metadata, new ParseContext(), false);
+ assertEquals(2, metadataList.size());
+ assertRendering(metadataList.get(1), "testEMF.png",
+ TikaCoreProperties.EmbeddedResourceType.RENDERING);
+ }
+
+ /**
+ * The per-request form: the parser config is supplied through the
+ * ParseContext, as tika-server does for a multipart config part.
+ */
+ @Test
+ public void testRenderingFromParseContext() throws Exception {
+ ParseContext context = new ParseContext();
+ context.setJsonConfig("emf-parser", "{\"renderImage\": true}");
+ List metadataList = getRecursiveMetadata("testEMF.emf", context);
+ assertEquals(2, metadataList.size());
+ assertRendering(metadataList.get(1), "testEMF.png",
+ TikaCoreProperties.EmbeddedResourceType.RENDERING);
+ assertEquals("1", metadataList.get(1).get(TikaCoreProperties.EMBEDDED_DEPTH));
+ }
+
+ /**
+ * The docProps thumbnail of a Word document is an EMF; with rendering on
+ * its PNG rendering follows it, one level deeper.
+ */
+ @Test
+ public void testDocxThumbnailRendering() throws Exception {
+ ParseContext context = new ParseContext();
+ context.setJsonConfig("emf-parser", "{\"renderImage\": true, \"renderWidth\": 200}");
+ List metadataList = getRecursiveMetadata("testDOCX_Thumbnail.docx", context);
+ //the document, its thumbnail, the WMF picture inside the thumbnail's
+ //EMF and the thumbnail's rendering
+ assertEquals(4, metadataList.size());
+ Metadata thumbnail = byName(metadataList, "thumbnail.emf");
+ assertEquals("image/emf", thumbnail.get(HttpHeaders.CONTENT_TYPE));
+ assertEquals(TikaCoreProperties.EmbeddedResourceType.THUMBNAIL.name(),
+ thumbnail.get(TikaCoreProperties.EMBEDDED_RESOURCE_TYPE));
+ assertEquals("1", thumbnail.get(TikaCoreProperties.EMBEDDED_DEPTH));
+ Metadata rendering = byName(metadataList, "thumbnail.png");
+ assertRendering(rendering, "thumbnail.png",
+ TikaCoreProperties.EmbeddedResourceType.THUMBNAIL);
+ assertEquals("2", rendering.get(TikaCoreProperties.EMBEDDED_DEPTH));
+ assertEquals("/thumbnail.emf/thumbnail.png",
+ rendering.get(TikaCoreProperties.EMBEDDED_RESOURCE_PATH));
+ }
+
+ /**
+ * Restricted to THUMBNAIL embedded documents, the parser renders the
+ * document's thumbnail but not a picture that is merely embedded.
+ */
+ @Test
+ public void testRenderOnlyThumbnails() throws Exception {
+ ParseContext context = new ParseContext();
+ context.setJsonConfig("emf-parser",
+ "{\"renderImage\": true, \"renderOnlyEmbeddedResourceTypes\": [\"THUMBNAIL\"]}");
+ List metadataList = getRecursiveMetadata("testDOCX_Thumbnail.docx", context);
+ assertRendering(byName(metadataList, "thumbnail.png"), "thumbnail.png",
+ TikaCoreProperties.EmbeddedResourceType.THUMBNAIL);
+
+ //a bare EMF is the document itself, not a THUMBNAIL: no rendering
+ metadataList = getRecursiveMetadata("testEMF.emf", context);
+ assertEquals(1, metadataList.size());
+ }
+
+ /**
+ * The configured width reaches the renderer the parser was handed, which
+ * is the SPI-injected one in a default setup, not the parser's own
+ * instance.
+ */
+ @ParameterizedTest
+ @ValueSource(ints = {200, 400})
+ public void testRenderWidth(int width) throws Exception {
+ ParseContext context = new ParseContext();
+ context.setJsonConfig("emf-parser",
+ "{\"renderImage\": true, \"renderWidth\": " + width + "}");
+ List renderings = new ArrayList<>();
+ context.set(EmbeddedDocumentExtractor.class, collector(renderings));
+ try (InputStream is = getResourceAsStream("/test-documents/testEMF.emf")) {
+ AUTO_DETECT_PARSER.parse(TikaInputStream.get(is), new BodyContentHandler(-1),
+ new Metadata(), context);
+ }
+ assertEquals(1, renderings.size());
+ //the PNG header carries the width at offset 16
+ assertEquals(width, ByteBuffer.wrap(renderings.get(0), 16, 4).getInt());
+ }
+
+ private static EmbeddedDocumentExtractor collector(List renderings) {
+ return new EmbeddedDocumentExtractor() {
+ @Override
+ public boolean shouldParseEmbedded(Metadata metadata, ParseContext parseContext) {
+ return true;
+ }
+
+ @Override
+ public void parseEmbedded(TikaInputStream stream, ContentHandler handler,
+ Metadata metadata, ParseContext parseContext,
+ boolean outputHtml) throws IOException {
+ renderings.add(stream.readAllBytes());
+ }
+ };
+ }
+
+ private static Metadata byName(List metadataList, String name) {
+ for (Metadata m : metadataList) {
+ if (name.equals(m.get(TikaCoreProperties.RESOURCE_NAME_KEY))) {
+ return m;
+ }
+ }
+ throw new AssertionError("no embedded document named " + name);
+ }
+
+ /**
+ * There is no image parser on this module's test classpath, so the PNG
+ * is checked by its type, name and size rather than its dimensions.
+ * The rendering of a THUMBNAIL is a THUMBNAIL, any other a RENDERING.
+ */
+ private static void assertRendering(Metadata rendering, String name,
+ TikaCoreProperties.EmbeddedResourceType type) {
+ assertEquals("image/png", rendering.get(HttpHeaders.CONTENT_TYPE));
+ assertEquals(type.name(), rendering.get(TikaCoreProperties.EMBEDDED_RESOURCE_TYPE));
+ assertEquals(name, rendering.get(TikaCoreProperties.RESOURCE_NAME_KEY));
+ assertEquals("poi-metafile-renderer", rendering.get(Rendering.RENDERED_BY));
+ assertTrue(Long.parseLong(rendering.get(HttpHeaders.CONTENT_LENGTH)) > 100);
+ assertNull(rendering.get(TikaCoreProperties.TIKA_META_EXCEPTION_EMBEDDED_STREAM));
+ }
+
@Test
public void testMissingCoords() throws Exception {
//TIKA-4432
diff --git a/tika-parsers/tika-parsers-standard/tika-parsers-standard-modules/tika-parser-microsoft-module/src/test/java/org/apache/tika/parser/microsoft/OLE2ThumbnailTest.java b/tika-parsers/tika-parsers-standard/tika-parsers-standard-modules/tika-parser-microsoft-module/src/test/java/org/apache/tika/parser/microsoft/OLE2ThumbnailTest.java
new file mode 100644
index 00000000000..c91ff74dcc2
--- /dev/null
+++ b/tika-parsers/tika-parsers-standard/tika-parsers-standard-modules/tika-parser-microsoft-module/src/test/java/org/apache/tika/parser/microsoft/OLE2ThumbnailTest.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;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertNotNull;
+import static org.junit.jupiter.api.Assertions.assertNull;
+import static org.junit.jupiter.api.Assertions.assertThrows;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+
+import java.util.Collections;
+import java.util.List;
+
+import org.junit.jupiter.api.Test;
+
+import org.apache.tika.TikaTest;
+import org.apache.tika.metadata.HttpHeaders;
+import org.apache.tika.metadata.Metadata;
+import org.apache.tika.metadata.TikaCoreProperties;
+import org.apache.tika.parser.ParseContext;
+
+/**
+ * The thumbnail that Office stores in the SummaryInformation of the OLE2
+ * formats (PIDSI_THUMBNAIL, a WMF) is emitted as a THUMBNAIL embedded
+ * document, as the docProps thumbnail of the OOXML formats is.
+ */
+public class OLE2ThumbnailTest extends TikaTest {
+
+ @Test
+ public void testPptThumbnail() throws Exception {
+ List metadataList = getRecursiveMetadata("testPPT_various.ppt");
+ Metadata thumbnail = byTypeAndContentType(metadataList,
+ TikaCoreProperties.EmbeddedResourceType.THUMBNAIL, "image/wmf");
+ assertNotNull(thumbnail);
+ assertEquals("image/wmf", thumbnail.get(HttpHeaders.CONTENT_TYPE));
+ assertEquals("thumbnail.wmf", thumbnail.get(TikaCoreProperties.RESOURCE_NAME_KEY));
+ assertEquals("1", thumbnail.get(TikaCoreProperties.EMBEDDED_DEPTH));
+ //exactly one document thumbnail
+ assertEquals(1, count(metadataList, TikaCoreProperties.EmbeddedResourceType.THUMBNAIL));
+ }
+
+ @Test
+ public void testPptThumbnailRendering() throws Exception {
+ ParseContext context = new ParseContext();
+ context.setJsonConfig("wmf-parser", "{\"renderImage\": true, \"renderWidth\": 400}");
+ List metadataList = getRecursiveMetadata("testPPT_various.ppt", context);
+ //the rendering of the thumbnail is a THUMBNAIL as well
+ Metadata rendering = byTypeAndContentType(metadataList,
+ TikaCoreProperties.EmbeddedResourceType.THUMBNAIL, "image/png");
+ assertNotNull(rendering);
+ assertEquals("thumbnail.png", rendering.get(TikaCoreProperties.RESOURCE_NAME_KEY));
+ assertEquals("2", rendering.get(TikaCoreProperties.EMBEDDED_DEPTH));
+ assertTrue(Long.parseLong(rendering.get(HttpHeaders.CONTENT_LENGTH)) > 100);
+ }
+
+ /**
+ * Word wraps its thumbnail bitmap in a WMF with a window extent and a
+ * single dibStretchBlt record, which has no bounds POI can compute; the
+ * renderer falls back to the bitmap.
+ */
+ @Test
+ public void testDocThumbnailRendering() throws Exception {
+ ParseContext context = new ParseContext();
+ context.setJsonConfig("wmf-parser", "{\"renderImage\": true}");
+ List metadataList = getRecursiveMetadata("testControlCharacters.doc", context);
+ Metadata thumbnail = byTypeAndContentType(metadataList,
+ TikaCoreProperties.EmbeddedResourceType.THUMBNAIL, "image/wmf");
+ assertNotNull(thumbnail);
+ assertEquals("image/wmf", thumbnail.get(HttpHeaders.CONTENT_TYPE));
+ Metadata rendering = byTypeAndContentType(metadataList,
+ TikaCoreProperties.EmbeddedResourceType.THUMBNAIL, "image/png");
+ assertNotNull(rendering);
+ assertNull(thumbnail.get(TikaCoreProperties.TIKA_META_EXCEPTION_EMBEDDED_STREAM));
+ }
+
+ /**
+ * A thumbnail that is not a metafile picture is left alone.
+ */
+ @Test
+ public void testUnusableThumbnailIsSkipped() throws Exception {
+ List metadataList = getRecursiveMetadata("testEXCEL_embeddedPDF_mac.xls");
+ assertEquals(0, count(metadataList, TikaCoreProperties.EmbeddedResourceType.THUMBNAIL));
+ }
+
+ /**
+ * The thumbnail can be switched off for callers that do not want the
+ * extra embedded document.
+ */
+ @Test
+ public void testThumbnailCanBeSwitchedOff() throws Exception {
+ OfficeParserConfig config = new OfficeParserConfig();
+ config.setExtractThumbnail(false);
+ ParseContext context = new ParseContext();
+ context.set(OfficeParserConfig.class, config);
+ List metadataList = getRecursiveMetadata("testPPT_various.ppt", context);
+ assertEquals(0, count(metadataList, TikaCoreProperties.EmbeddedResourceType.THUMBNAIL));
+ }
+
+ /**
+ * A misspelt resource type would silently disable rendering.
+ */
+ @Test
+ public void testRenderOnlyTypesAreValidated() {
+ MetafileParserConfig config = new MetafileParserConfig();
+ assertThrows(IllegalArgumentException.class,
+ () -> config.setRenderOnlyEmbeddedResourceTypes(
+ Collections.singleton("THUMBNAILS")));
+ }
+
+ private static Metadata byTypeAndContentType(List metadataList,
+ TikaCoreProperties.EmbeddedResourceType type,
+ String contentType) {
+ for (Metadata m : metadataList) {
+ if (type.name().equals(m.get(TikaCoreProperties.EMBEDDED_RESOURCE_TYPE))
+ && contentType.equals(m.get(HttpHeaders.CONTENT_TYPE))) {
+ return m;
+ }
+ }
+ return null;
+ }
+
+ private static int count(List metadataList,
+ TikaCoreProperties.EmbeddedResourceType type) {
+ int n = 0;
+ for (Metadata m : metadataList) {
+ if (type.name().equals(m.get(TikaCoreProperties.EMBEDDED_RESOURCE_TYPE))) {
+ n++;
+ }
+ }
+ return n;
+ }
+}
diff --git a/tika-parsers/tika-parsers-standard/tika-parsers-standard-modules/tika-parser-microsoft-module/src/test/java/org/apache/tika/parser/microsoft/PowerPointParserTest.java b/tika-parsers/tika-parsers-standard/tika-parsers-standard-modules/tika-parser-microsoft-module/src/test/java/org/apache/tika/parser/microsoft/PowerPointParserTest.java
index d2ec1f4718a..87e6c4b2053 100644
--- a/tika-parsers/tika-parsers-standard/tika-parsers-standard-modules/tika-parser-microsoft-module/src/test/java/org/apache/tika/parser/microsoft/PowerPointParserTest.java
+++ b/tika-parsers/tika-parsers-standard/tika-parsers-standard-modules/tika-parser-microsoft-module/src/test/java/org/apache/tika/parser/microsoft/PowerPointParserTest.java
@@ -331,7 +331,8 @@ public void testEncrypted() throws Exception {
@Test
public void testGroups() throws Exception {
List metadataList = getRecursiveMetadata("testPPT_groups.ppt");
- assertEquals(3, metadataList.size());
+ //two pictures and the SummaryInformation thumbnail
+ assertEquals(4, metadataList.size());
String content = metadataList.get(0).get(TikaCoreProperties.TIKA_CONTENT);
//this tests that we're ignoring text shapes at depth=0
//i.e. POI has already included them in the slide's getTextParagraphs()
@@ -379,7 +380,8 @@ public void testHyperlinksInTextBoxes() throws Exception {
@Test
public void testEmbeddedXLSInOLEObject() throws Exception {
List metadataList = getRecursiveMetadata("testPPT_oleWorkbook.ppt");
- assertEquals(3, metadataList.size());
+ //the workbook, its picture and the SummaryInformation thumbnail
+ assertEquals(4, metadataList.size());
Metadata xlsx = metadataList.get(1);
assertContains("Sheet1
", xlsx.get(TikaCoreProperties.TIKA_CONTENT));
assertContains("1 | ", xlsx.get(TikaCoreProperties.TIKA_CONTENT));
diff --git a/tika-parsers/tika-parsers-standard/tika-parsers-standard-modules/tika-parser-microsoft-module/src/test/java/org/apache/tika/parser/microsoft/WMFParserTest.java b/tika-parsers/tika-parsers-standard/tika-parsers-standard-modules/tika-parser-microsoft-module/src/test/java/org/apache/tika/parser/microsoft/WMFParserTest.java
index 3317c0bae03..5aca34707cb 100644
--- a/tika-parsers/tika-parsers-standard/tika-parsers-standard-modules/tika-parser-microsoft-module/src/test/java/org/apache/tika/parser/microsoft/WMFParserTest.java
+++ b/tika-parsers/tika-parsers-standard/tika-parsers-standard-modules/tika-parser-microsoft-module/src/test/java/org/apache/tika/parser/microsoft/WMFParserTest.java
@@ -17,6 +17,8 @@
package org.apache.tika.parser.microsoft;
import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertNotNull;
+import static org.junit.jupiter.api.Assertions.assertTrue;
import java.util.List;
@@ -25,7 +27,9 @@
import org.apache.tika.TikaTest;
import org.apache.tika.metadata.HttpHeaders;
import org.apache.tika.metadata.Metadata;
+import org.apache.tika.metadata.Rendering;
import org.apache.tika.metadata.TikaCoreProperties;
+import org.apache.tika.parser.ParseContext;
public class WMFParserTest extends TikaTest {
@@ -40,6 +44,58 @@ public void testTextExtractionShiftJISencoding() throws Exception {
testTextExtraction("testWMF_charset.wmf", 0, "普林斯");
}
+ /**
+ * Rendering is off by default; with "wmf-parser": {"renderImage": true}
+ * the rendering follows the image as a RENDERING embedded document.
+ */
+ @Test
+ public void testRendering() throws Exception {
+ assertEquals(1, getRecursiveMetadata("testWMF.wmf").size());
+
+ ParseContext context = new ParseContext();
+ context.setJsonConfig("wmf-parser", "{\"renderImage\": true, \"renderWidth\": 300}");
+ List metadataList = getRecursiveMetadata("testWMF.wmf", context);
+ assertEquals(2, metadataList.size());
+ Metadata rendering = metadataList.get(1);
+ assertEquals("image/png", rendering.get(HttpHeaders.CONTENT_TYPE));
+ assertEquals(TikaCoreProperties.EmbeddedResourceType.RENDERING.name(),
+ rendering.get(TikaCoreProperties.EMBEDDED_RESOURCE_TYPE));
+ assertEquals("testWMF.png", rendering.get(TikaCoreProperties.RESOURCE_NAME_KEY));
+ assertEquals("poi-metafile-renderer", rendering.get(Rendering.RENDERED_BY));
+ assertTrue(Long.parseLong(rendering.get(HttpHeaders.CONTENT_LENGTH)) > 100);
+ }
+
+ /**
+ * The docProps thumbnail of this workbook is a WMF; with rendering on its
+ * PNG rendering follows it, one level deeper, as a THUMBNAIL as well.
+ */
+ @Test
+ public void testXlsxThumbnailRendering() throws Exception {
+ ParseContext context = new ParseContext();
+ context.setJsonConfig("wmf-parser", "{\"renderImage\": true}");
+ List metadataList = getRecursiveMetadata("testXLSX_Thumbnail.xlsx", context);
+ Metadata thumbnail = null;
+ Metadata rendering = null;
+ for (Metadata m : metadataList) {
+ if (!TikaCoreProperties.EmbeddedResourceType.THUMBNAIL.name()
+ .equals(m.get(TikaCoreProperties.EMBEDDED_RESOURCE_TYPE))) {
+ continue;
+ }
+ if ("image/wmf".equals(m.get(HttpHeaders.CONTENT_TYPE))) {
+ thumbnail = m;
+ } else {
+ rendering = m;
+ }
+ }
+ assertNotNull(thumbnail);
+ assertEquals("image/wmf", thumbnail.get(HttpHeaders.CONTENT_TYPE));
+ assertEquals("1", thumbnail.get(TikaCoreProperties.EMBEDDED_DEPTH));
+ assertNotNull(rendering);
+ assertEquals("image/png", rendering.get(HttpHeaders.CONTENT_TYPE));
+ assertEquals("2", rendering.get(TikaCoreProperties.EMBEDDED_DEPTH));
+ assertEquals("thumbnail.png", rendering.get(TikaCoreProperties.RESOURCE_NAME_KEY));
+ }
+
private void testTextExtraction(String fileName, int metaDataItemIndex, String expectedText)
throws Exception {
List metadataList = getRecursiveMetadata(fileName);
diff --git a/tika-parsers/tika-parsers-standard/tika-parsers-standard-modules/tika-parser-microsoft-module/src/test/resources/configs/tika-config-emf-render.json b/tika-parsers/tika-parsers-standard/tika-parsers-standard-modules/tika-parser-microsoft-module/src/test/resources/configs/tika-config-emf-render.json
new file mode 100644
index 00000000000..a5f99414689
--- /dev/null
+++ b/tika-parsers/tika-parsers-standard/tika-parsers-standard-modules/tika-parser-microsoft-module/src/test/resources/configs/tika-config-emf-render.json
@@ -0,0 +1,11 @@
+{
+ "parsers": [
+ "default-parser",
+ {
+ "emf-parser": {
+ "renderImage": true,
+ "renderWidth": 400
+ }
+ }
+ ]
+}