From e966adccc89342d8023335fc0997f151e81b793f Mon Sep 17 00:00:00 2001 From: Dominik Schmidt Date: Sat, 29 Aug 2026 14:17:23 +0200 Subject: [PATCH 1/8] TIKA-4855 - render EMF/WMF through POI and emit the OLE2 SummaryInformation thumbnail POIMetafileRenderer (poi-metafile-renderer) draws EMF and WMF images to a PNG of a configurable width; Word's bitmap-in-WMF thumbnails, which POI has no bounds for, are rendered from the bitmap. EMFParser and WMFParser are RenderingParsers and emit the rendering as a RENDERING embedded document with "emf-parser" / "wmf-parser": {"renderImage": true}, off by default, the way the PDF parser emits page renderings. OfficeParser emits the SummaryInformation thumbnail of the OLE2 formats (a WMF) as a THUMBNAIL embedded document, as the OOXML parsers do with the docProps thumbnail. --- CHANGES.txt | 10 + .../tika/parser/microsoft/EMFParser.java | 55 +++- .../microsoft/MetafileParserConfig.java | 62 +++++ .../parser/microsoft/MetafileRendering.java | 128 +++++++++ .../tika/parser/microsoft/OfficeParser.java | 29 +- .../parser/microsoft/SummaryExtractor.java | 31 +++ .../tika/parser/microsoft/WMFParser.java | 52 +++- .../microsoft/POIMetafileRenderer.java | 259 ++++++++++++++++++ .../tika/parser/microsoft/EMFParserTest.java | 91 ++++++ .../parser/microsoft/OLE2ThumbnailTest.java | 114 ++++++++ .../microsoft/PowerPointParserTest.java | 6 +- .../tika/parser/microsoft/WMFParserTest.java | 53 ++++ .../configs/tika-config-emf-render.json | 11 + 13 files changed, 896 insertions(+), 5 deletions(-) create mode 100644 tika-parsers/tika-parsers-standard/tika-parsers-standard-modules/tika-parser-microsoft-module/src/main/java/org/apache/tika/parser/microsoft/MetafileParserConfig.java create mode 100644 tika-parsers/tika-parsers-standard/tika-parsers-standard-modules/tika-parser-microsoft-module/src/main/java/org/apache/tika/parser/microsoft/MetafileRendering.java create mode 100644 tika-parsers/tika-parsers-standard/tika-parsers-standard-modules/tika-parser-microsoft-module/src/main/java/org/apache/tika/renderer/microsoft/POIMetafileRenderer.java create mode 100644 tika-parsers/tika-parsers-standard/tika-parsers-standard-modules/tika-parser-microsoft-module/src/test/java/org/apache/tika/parser/microsoft/OLE2ThumbnailTest.java create mode 100644 tika-parsers/tika-parsers-standard/tika-parsers-standard-modules/tika-parser-microsoft-module/src/test/resources/configs/tika-config-emf-render.json diff --git a/CHANGES.txt b/CHANGES.txt index df8c73d3f1e..7c99e715703 100644 --- a/CHANGES.txt +++ b/CHANGES.txt @@ -1,5 +1,15 @@ Release 4.1.0 - unreleased + * Raster previews for the vector thumbnails of Office documents: the new + poi-metafile-renderer draws EMF and WMF images through POI (a PNG of + a configurable width; Word's bitmap-in-WMF thumbnails from the bitmap + directly), EMFParser and WMFParser are RenderingParsers that emit the + rendering as a RENDERING embedded document with "emf-parser" / + "wmf-parser": {"renderImage": true, "renderWidth": 800}, off by + default, and OfficeParser emits the SummaryInformation thumbnail of the + OLE2 formats (a WMF) as a THUMBNAIL embedded document, as the OOXML + parsers do with the docProps thumbnail (TIKA-XXXX). + * RawTiffParser marks only the largest embedded JPEG preview as the THUMBNAIL embedded document; the smaller previews of the same image are INLINE images named image-N.jpg. Previously every preview was a diff --git a/tika-parsers/tika-parsers-standard/tika-parsers-standard-modules/tika-parser-microsoft-module/src/main/java/org/apache/tika/parser/microsoft/EMFParser.java b/tika-parsers/tika-parsers-standard/tika-parsers-standard-modules/tika-parser-microsoft-module/src/main/java/org/apache/tika/parser/microsoft/EMFParser.java index ba7bc8efc65..bb9fe6ae85f 100644 --- a/tika-parsers/tika-parsers-standard/tika-parsers-standard-modules/tika-parser-microsoft-module/src/main/java/org/apache/tika/parser/microsoft/EMFParser.java +++ b/tika-parsers/tika-parsers-standard/tika-parsers-standard-modules/tika-parser-microsoft-module/src/main/java/org/apache/tika/parser/microsoft/EMFParser.java @@ -35,6 +35,9 @@ import org.xml.sax.SAXException; import org.apache.tika.annotation.TikaComponent; +import org.apache.tika.config.ConfigDeserializer; +import org.apache.tika.config.JsonConfig; +import org.apache.tika.config.ParseContextConfig; import org.apache.tika.exception.TikaException; import org.apache.tika.extractor.EmbeddedDocumentExtractor; import org.apache.tika.extractor.EmbeddedDocumentUtil; @@ -42,9 +45,12 @@ import org.apache.tika.metadata.HttpHeaders; import org.apache.tika.metadata.Metadata; import org.apache.tika.metadata.Property; +import org.apache.tika.metadata.TikaCoreProperties; import org.apache.tika.mime.MediaType; import org.apache.tika.parser.ParseContext; import org.apache.tika.parser.Parser; +import org.apache.tika.parser.RenderingParser; +import org.apache.tika.renderer.Renderer; import org.apache.tika.sax.EmbeddedContentHandler; import org.apache.tika.sax.XHTMLContentHandler; @@ -62,12 +68,39 @@ * We'd have to do something like what PDFBox or XPS do to sort the * runs and then put the cow back together from the hamburger...lol... */ +/** + * Extracts the text of an EMF image and its embedded WMF and multi-format + * pictures. With {@link MetafileParserConfig#setRenderImage(boolean)} + * ("emf-parser": {"renderImage": true}) the image is also rendered through the + * configured {@link Renderer}, the + * {@link org.apache.tika.renderer.microsoft.POIMetafileRenderer} by + * default, and emitted as a + * {@link TikaCoreProperties.EmbeddedResourceType#RENDERING} embedded document, + * the way the PDF parser emits page renderings, so a client can obtain a + * raster preview of a vector thumbnail such as the docProps thumbnail of a + * Word document. + */ @TikaComponent -public class EMFParser implements Parser { +public class EMFParser implements Parser, RenderingParser { public static Property EMF_ICON_ONLY = Property.internalBoolean("emf:icon-only"); public static Property EMF_ICON_STRING = Property.internalText("emf:icon-string"); + private final MetafileParserConfig defaultConfig; + private Renderer renderer; + + public EMFParser() { + this(new MetafileParserConfig()); + } + + public EMFParser(MetafileParserConfig config) { + this.defaultConfig = config; + } + + public EMFParser(JsonConfig jsonConfig) { + this(ConfigDeserializer.buildConfig(jsonConfig, MetafileParserConfig.class)); + } + private static String ICON_ONLY = "IconOnly"; private static final MediaType MEDIA_TYPE = MediaType.image("emf"); @@ -139,6 +172,11 @@ public void parse(TikaInputStream tis, ContentHandler handler, Metadata metadata xhtml.characters(buffer.toString()); xhtml.endElement("p"); } + MetafileParserConfig config = getConfig(context); + if (config.isRenderImage()) { + MetafileRendering.render(renderer, config, MEDIA_TYPE, ex, xhtml, metadata, + context); + } } catch (RecordFormatException e) { //POI's hemfparser can throw these for "parse // exceptions" @@ -149,6 +187,21 @@ public void parse(TikaInputStream tis, ContentHandler handler, Metadata metadata xhtml.endDocument(); } + private MetafileParserConfig getConfig(ParseContext context) + throws TikaException, IOException { + return ParseContextConfig.getConfig(context, "emf-parser", + MetafileParserConfig.class, defaultConfig); + } + + @Override + public void setRenderer(Renderer renderer) { + this.renderer = renderer; + } + + public Renderer getRenderer() { + return renderer; + } + private void handleExtTextOut(HemfText.EmfExtTextOutA record, ParseState parseState, StringBuilder buffer, XHTMLContentHandler xhtml, double fudgeFactorX, Charset charset) throws IOException, SAXException { diff --git a/tika-parsers/tika-parsers-standard/tika-parsers-standard-modules/tika-parser-microsoft-module/src/main/java/org/apache/tika/parser/microsoft/MetafileParserConfig.java b/tika-parsers/tika-parsers-standard/tika-parsers-standard-modules/tika-parser-microsoft-module/src/main/java/org/apache/tika/parser/microsoft/MetafileParserConfig.java new file mode 100644 index 00000000000..358faef8901 --- /dev/null +++ b/tika-parsers/tika-parsers-standard/tika-parsers-standard-modules/tika-parser-microsoft-module/src/main/java/org/apache/tika/parser/microsoft/MetafileParserConfig.java @@ -0,0 +1,62 @@ +/* + * 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 java.io.Serializable; + +import org.apache.tika.renderer.microsoft.POIMetafileRenderer; + +/** + * Configuration of the {@link EMFParser} ("emf-parser") and the + * {@link WMFParser} ("wmf-parser"). + */ +public class MetafileParserConfig implements Serializable { + + private static final long serialVersionUID = -6371049153052164071L; + + private boolean renderImage = false; + private int renderWidth = 800; + + /** + * Whether to render the image and emit the rendering as a RENDERING + * embedded document. Off by default. + */ + public boolean isRenderImage() { + return renderImage; + } + + public void setRenderImage(boolean renderImage) { + this.renderImage = renderImage; + } + + /** + * Width of the rendering in pixels when the default + * {@link POIMetafileRenderer} is used; the height follows the image's + * aspect ratio. Default 800. + */ + public int getRenderWidth() { + return renderWidth; + } + + public void setRenderWidth(int renderWidth) { + if (renderWidth < 1 || renderWidth > 10000) { + throw new IllegalArgumentException( + "renderWidth must be between 1 and 10000, got: " + renderWidth); + } + this.renderWidth = renderWidth; + } +} diff --git a/tika-parsers/tika-parsers-standard/tika-parsers-standard-modules/tika-parser-microsoft-module/src/main/java/org/apache/tika/parser/microsoft/MetafileRendering.java b/tika-parsers/tika-parsers-standard/tika-parsers-standard-modules/tika-parser-microsoft-module/src/main/java/org/apache/tika/parser/microsoft/MetafileRendering.java new file mode 100644 index 00000000000..8e4e0df4bcc --- /dev/null +++ b/tika-parsers/tika-parsers-standard/tika-parsers-standard-modules/tika-parser-microsoft-module/src/main/java/org/apache/tika/parser/microsoft/MetafileRendering.java @@ -0,0 +1,128 @@ +/* + * 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 java.io.IOException; + +import org.xml.sax.SAXException; + +import org.apache.tika.exception.TikaException; +import org.apache.tika.extractor.EmbeddedDocumentExtractor; +import org.apache.tika.extractor.EmbeddedDocumentUtil; +import org.apache.tika.io.TikaInputStream; +import org.apache.tika.metadata.HttpHeaders; +import org.apache.tika.metadata.Metadata; +import org.apache.tika.metadata.TikaCoreProperties; +import org.apache.tika.mime.MediaType; +import org.apache.tika.parser.ParseContext; +import org.apache.tika.renderer.RenderResult; +import org.apache.tika.renderer.RenderResults; +import org.apache.tika.renderer.Renderer; +import org.apache.tika.renderer.microsoft.POIMetafileRenderer; +import org.apache.tika.sax.EmbeddedContentHandler; +import org.apache.tika.sax.XHTMLContentHandler; + +/** + * Shared by {@link EMFParser} and {@link WMFParser}: renders a parsed + * metafile through a {@link Renderer} and emits the result as a + * {@link TikaCoreProperties.EmbeddedResourceType#RENDERING} embedded + * document, the way the PDF parser emits page renderings. + */ +final class MetafileRendering { + + private MetafileRendering() { + } + + /** + * @param injected the renderer set on the parser, or null + * @param picture the parsed {@code HemfPicture} or {@code HwmfPicture} + */ + static void render(Renderer injected, MetafileParserConfig config, MediaType type, + Object picture, XHTMLContentHandler xhtml, Metadata metadata, + ParseContext context) throws IOException, SAXException { + //like the PDF parser: the injected renderer if it handles the type, + //the default one otherwise + Renderer renderer = injected != null && injected.getSupportedTypes(context).contains(type) + ? injected : defaultRenderer(config); + Metadata renderMetadata = Metadata.newInstance(context); + renderMetadata.set(TikaCoreProperties.TYPE, type.toString()); + EmbeddedDocumentExtractor extractor = + EmbeddedDocumentUtil.getEmbeddedDocumentExtractor(context); + try (TikaInputStream pictureStream = TikaInputStream.get(new byte[0]); + RenderResults results = render(renderer, pictureStream, picture, renderMetadata, + context)) { + if (results == null) { + return; + } + for (RenderResult result : results.getResults()) { + if (result.getStatus() != RenderResult.STATUS.SUCCESS) { + EmbeddedDocumentUtil.recordException( + new TikaException("metafile rendering failed"), metadata, context); + continue; + } + Metadata renderingMetadata = result.getMetadata(); + renderingMetadata.set(TikaCoreProperties.RESOURCE_NAME_KEY, + renderingName(metadata, renderingMetadata)); + if (extractor.shouldParseEmbedded(renderingMetadata, context)) { + try (TikaInputStream tis = result.getInputStream()) { + extractor.parseEmbedded(tis, new EmbeddedContentHandler(xhtml), + renderingMetadata, context, false); + } + } + } + } + } + + private static RenderResults render(Renderer renderer, TikaInputStream pictureStream, + Object picture, Metadata renderMetadata, + ParseContext context) throws IOException { + //hand the parsed picture over instead of re-reading the stream + pictureStream.setOpenContainer(picture); + try { + return renderer.render(pictureStream, renderMetadata, context); + } catch (SecurityException e) { + throw e; + } catch (Exception e) { + EmbeddedDocumentUtil.recordException(e, renderMetadata, context); + return null; + } + } + + private static Renderer defaultRenderer(MetafileParserConfig config) { + POIMetafileRenderer renderer = new POIMetafileRenderer(); + renderer.setWidth(config.getRenderWidth()); + return renderer; + } + + /** + * The rendering is named after the image, with the rendering's format + * as its extension. + */ + private static String renderingName(Metadata metadata, Metadata renderingMetadata) { + String contentType = renderingMetadata.get(HttpHeaders.CONTENT_TYPE); + String extension = contentType != null && contentType.startsWith("image/") + ? contentType.substring("image/".length()) : "png"; + String name = metadata.get(TikaCoreProperties.RESOURCE_NAME_KEY); + if (name == null || name.isEmpty()) { + return "rendering." + extension; + } + int slash = Math.max(name.lastIndexOf('/'), name.lastIndexOf('\\')); + String base = name.substring(slash + 1); + int dot = base.lastIndexOf('.'); + return (dot > 0 ? base.substring(0, dot) : base) + "." + extension; + } +} diff --git a/tika-parsers/tika-parsers-standard/tika-parsers-standard-modules/tika-parser-microsoft-module/src/main/java/org/apache/tika/parser/microsoft/OfficeParser.java b/tika-parsers/tika-parsers-standard/tika-parsers-standard-modules/tika-parser-microsoft-module/src/main/java/org/apache/tika/parser/microsoft/OfficeParser.java index a88bf609a46..9433eb5d2be 100644 --- a/tika-parsers/tika-parsers-standard/tika-parsers-standard-modules/tika-parser-microsoft-module/src/main/java/org/apache/tika/parser/microsoft/OfficeParser.java +++ b/tika-parsers/tika-parsers-standard/tika-parsers-standard-modules/tika-parser-microsoft-module/src/main/java/org/apache/tika/parser/microsoft/OfficeParser.java @@ -214,7 +214,8 @@ protected void parse(DirectoryNode root, ParseContext context, Metadata metadata throws IOException, SAXException, TikaException { // Parse summary entries first, to make metadata available early - new SummaryExtractor(metadata).parseSummaries(root); + SummaryExtractor summaryExtractor = new SummaryExtractor(metadata); + summaryExtractor.parseSummaries(root); // Parse remaining document entries POIFSDocumentType type = POIFSDocumentType.detectType(root); @@ -306,6 +307,32 @@ protected void parse(DirectoryNode root, ParseContext context, Metadata metadata // is extracted, which happened above break; } + handleThumbnail(summaryExtractor.getThumbnailWmf(), xhtml, context); + } + + /** + * Emits the document thumbnail from the SummaryInformation, a WMF, as a + * {@link TikaCoreProperties.EmbeddedResourceType#THUMBNAIL} embedded + * document, as the OOXML parsers do with the docProps thumbnail. + */ + private void handleThumbnail(byte[] wmf, XHTMLContentHandler xhtml, ParseContext context) + throws IOException, SAXException { + if (wmf == null) { + return; + } + Metadata embeddedMetadata = Metadata.newInstance(context); + embeddedMetadata.set(TikaCoreProperties.RESOURCE_NAME_KEY, "thumbnail.wmf"); + embeddedMetadata.set(TikaCoreProperties.EMBEDDED_RESOURCE_TYPE, + TikaCoreProperties.EmbeddedResourceType.THUMBNAIL.toString()); + embeddedMetadata.set(HttpHeaders.CONTENT_TYPE, "image/wmf"); + EmbeddedDocumentExtractor extractor = + EmbeddedDocumentUtil.getEmbeddedDocumentExtractor(context); + if (extractor.shouldParseEmbedded(embeddedMetadata, context)) { + try (TikaInputStream tis = TikaInputStream.get(wmf)) { + extractor.parseEmbedded(tis, new EmbeddedContentHandler(xhtml), embeddedMetadata, + context, false); + } + } } private void setType(Metadata metadata, MediaType type) { diff --git a/tika-parsers/tika-parsers-standard/tika-parsers-standard-modules/tika-parser-microsoft-module/src/main/java/org/apache/tika/parser/microsoft/SummaryExtractor.java b/tika-parsers/tika-parsers-standard/tika-parsers-standard-modules/tika-parser-microsoft-module/src/main/java/org/apache/tika/parser/microsoft/SummaryExtractor.java index 238974eec84..eeeabdc7e7d 100644 --- a/tika-parsers/tika-parsers-standard/tika-parsers-standard-modules/tika-parser-microsoft-module/src/main/java/org/apache/tika/parser/microsoft/SummaryExtractor.java +++ b/tika-parsers/tika-parsers-standard/tika-parsers-standard-modules/tika-parser-microsoft-module/src/main/java/org/apache/tika/parser/microsoft/SummaryExtractor.java @@ -28,6 +28,7 @@ import org.apache.poi.hpsf.NoPropertySetStreamException; import org.apache.poi.hpsf.PropertySet; import org.apache.poi.hpsf.SummaryInformation; +import org.apache.poi.hpsf.Thumbnail; import org.apache.poi.hpsf.UnexpectedPropertySetTypeException; import org.apache.poi.poifs.filesystem.DirectoryNode; import org.apache.poi.poifs.filesystem.DocumentEntry; @@ -61,6 +62,8 @@ public class SummaryExtractor { private final Metadata metadata; + private byte[] thumbnailWmf; + public SummaryExtractor(Metadata metadata) { this.metadata = metadata; } @@ -131,7 +134,16 @@ private void parseSummaryEntryIfExists(DirectoryNode root, String entryName) } } + /** + * The document thumbnail stored in the SummaryInformation as a WMF, or + * null if there is none or it is not a metafile picture. + */ + public byte[] getThumbnailWmf() { + return thumbnailWmf; + } + private void parse(SummaryInformation summary) { + thumbnailWmf = extractThumbnailWmf(summary); set(TikaCoreProperties.TITLE, summary.getTitle()); addMulti(metadata, TikaCoreProperties.CREATOR, summary.getAuthor()); //make sure these are retrievable specifically @@ -158,6 +170,25 @@ private void parse(SummaryInformation summary) { } } + /** + * PIDSI_THUMBNAIL is a clipboard object; Office stores the thumbnail as + * CF_METAFILEPICT, a WMF. Anything else, or a malformed one, yields null. + */ + private static byte[] extractThumbnailWmf(SummaryInformation summary) { + byte[] raw = summary.getThumbnail(); + if (raw == null || raw.length == 0) { + return null; + } + try { + byte[] wmf = new Thumbnail(raw).getThumbnailAsWMF(); + return wmf != null && wmf.length > 0 ? wmf : null; + } catch (SecurityException e) { + throw e; + } catch (Exception e) { + return null; + } + } + private void parse(DocumentSummaryInformation summary) { set(OfficeOpenXMLExtended.COMPANY, summary.getCompany()); addMulti(metadata, OfficeOpenXMLExtended.MANAGER, summary.getManager()); diff --git a/tika-parsers/tika-parsers-standard/tika-parsers-standard-modules/tika-parser-microsoft-module/src/main/java/org/apache/tika/parser/microsoft/WMFParser.java b/tika-parsers/tika-parsers-standard/tika-parsers-standard-modules/tika-parser-microsoft-module/src/main/java/org/apache/tika/parser/microsoft/WMFParser.java index 41b9ecd2c25..2a4a3083edd 100644 --- a/tika-parsers/tika-parsers-standard/tika-parsers-standard-modules/tika-parser-microsoft-module/src/main/java/org/apache/tika/parser/microsoft/WMFParser.java +++ b/tika-parsers/tika-parsers-standard/tika-parsers-standard-modules/tika-parser-microsoft-module/src/main/java/org/apache/tika/parser/microsoft/WMFParser.java @@ -32,25 +32,56 @@ import org.xml.sax.SAXException; import org.apache.tika.annotation.TikaComponent; +import org.apache.tika.config.ConfigDeserializer; +import org.apache.tika.config.JsonConfig; +import org.apache.tika.config.ParseContextConfig; import org.apache.tika.exception.TikaException; import org.apache.tika.io.TikaInputStream; import org.apache.tika.metadata.Metadata; +import org.apache.tika.metadata.TikaCoreProperties; import org.apache.tika.mime.MediaType; import org.apache.tika.parser.ParseContext; import org.apache.tika.parser.Parser; +import org.apache.tika.parser.RenderingParser; +import org.apache.tika.renderer.Renderer; import org.apache.tika.sax.XHTMLContentHandler; /** * This parser offers a very rough capability to extract text if there * is text stored in the WMF files. */ +/** + * Extracts the text of a WMF image. With + * {@link MetafileParserConfig#setRenderImage(boolean)} + * ("wmf-parser": {"renderImage": true}) the image is also rendered through the + * configured {@link Renderer}, the + * {@link org.apache.tika.renderer.microsoft.POIMetafileRenderer} by + * default, and emitted as a + * {@link TikaCoreProperties.EmbeddedResourceType#RENDERING} embedded document, + * the way the PDF parser emits page renderings. + */ @TikaComponent -public class WMFParser implements Parser { +public class WMFParser implements Parser, RenderingParser { private static final MediaType MEDIA_TYPE = MediaType.image("wmf"); private static final Set SUPPORTED_TYPES = Collections.singleton(MEDIA_TYPE); + private final MetafileParserConfig defaultConfig; + private Renderer renderer; + + public WMFParser() { + this(new MetafileParserConfig()); + } + + public WMFParser(MetafileParserConfig config) { + this.defaultConfig = config; + } + + public WMFParser(JsonConfig jsonConfig) { + this(ConfigDeserializer.buildConfig(jsonConfig, MetafileParserConfig.class)); + } + @Override public Set getSupportedTypes(ParseContext context) { return SUPPORTED_TYPES; @@ -95,6 +126,11 @@ public void parse(TikaInputStream tis, ContentHandler handler, Metadata metadata xhtml.endElement("p"); } } + MetafileParserConfig config = getConfig(context); + if (config.isRenderImage()) { + MetafileRendering.render(renderer, config, MEDIA_TYPE, picture, xhtml, metadata, + context); + } } catch (RecordFormatException e) { //POI's hwmfparser can \ throw these for "parse // exceptions" throw new TikaException(e.getMessage(), e); @@ -108,4 +144,18 @@ public void parse(TikaInputStream tis, ContentHandler handler, Metadata metadata xhtml.endDocument(); } + private MetafileParserConfig getConfig(ParseContext context) + throws TikaException, IOException { + return ParseContextConfig.getConfig(context, "wmf-parser", + MetafileParserConfig.class, defaultConfig); + } + + @Override + public void setRenderer(Renderer renderer) { + this.renderer = renderer; + } + + public Renderer getRenderer() { + return renderer; + } } 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..bc7e30499a0 --- /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,259 @@ +/* + * 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.exception.TikaException; +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.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; + + 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(); + 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) : draw((HwmfPicture) picture); + 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) { + results.add(new RenderResult(RenderResult.STATUS.EXCEPTION, id, null, + renderingMetadata)); + } + return results; + } + + private BufferedImage draw(HemfPicture picture) throws IOException { + Dimension2D size = picture.getSize(); + BufferedImage image = canvas(size); + 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) throws IOException { + Dimension2D size; + try { + size = picture.getSize(); + } catch (RuntimeException e) { + //no usable window records: a bitmap wrapped in a metafile + BufferedImage bitmap = firstBitmap(picture); + if (bitmap == null) { + throw new IOException("WMF without bounds and without a bitmap", e); + } + return scale(bitmap); + } + BufferedImage image = canvas(size); + 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; + } + + private BufferedImage canvas(Dimension2D size) throws IOException { + if (size == null || size.getWidth() <= 0 || size.getHeight() <= 0) { + throw new IOException("metafile without a usable size: " + size); + } + int height = (int) Math.max(1, Math.round(size.getHeight() * width / size.getWidth())); + BufferedImage image = new BufferedImage(width, height, 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, width, height); + } finally { + graphics.dispose(); + } + return image; + } + + private BufferedImage scale(BufferedImage bitmap) { + int height = (int) Math.max(1, + Math.round((double) bitmap.getHeight() * width / bitmap.getWidth())); + 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..0ed01572321 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,21 @@ 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.util.List; import org.junit.jupiter.api.Test; import org.apache.tika.TikaTest; +import org.apache.tika.config.loader.TikaLoader; 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; public class EMFParserTest extends TikaTest { @@ -54,6 +60,91 @@ 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"); + } + + /** + * 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"); + 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"); + assertEquals("2", rendering.get(TikaCoreProperties.EMBEDDED_DEPTH)); + assertEquals("/thumbnail.emf/thumbnail.png", + rendering.get(TikaCoreProperties.EMBEDDED_RESOURCE_PATH)); + } + + 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. + */ + private static void assertRendering(Metadata rendering, String name) { + assertEquals("image/png", rendering.get(HttpHeaders.CONTENT_TYPE)); + assertEquals(TikaCoreProperties.EmbeddedResourceType.RENDERING.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..67cd55a6582 --- /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,114 @@ +/* + * 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.assertTrue; + +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 = byType(metadataList, TikaCoreProperties.EmbeddedResourceType.THUMBNAIL); + 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); + Metadata rendering = byType(metadataList, TikaCoreProperties.EmbeddedResourceType.RENDERING); + assertNotNull(rendering); + assertEquals("image/png", rendering.get(HttpHeaders.CONTENT_TYPE)); + 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 = byType(metadataList, TikaCoreProperties.EmbeddedResourceType.THUMBNAIL); + assertNotNull(thumbnail); + assertEquals("image/wmf", thumbnail.get(HttpHeaders.CONTENT_TYPE)); + Metadata rendering = byType(metadataList, TikaCoreProperties.EmbeddedResourceType.RENDERING); + assertNotNull(rendering); + assertEquals("image/png", rendering.get(HttpHeaders.CONTENT_TYPE)); + 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)); + } + + private static Metadata byType(List metadataList, + TikaCoreProperties.EmbeddedResourceType type) { + for (Metadata m : metadataList) { + if (type.name().equals(m.get(TikaCoreProperties.EMBEDDED_RESOURCE_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..94bb6e9ca13 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,55 @@ 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. + */ + @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) { + String type = m.get(TikaCoreProperties.EMBEDDED_RESOURCE_TYPE); + if (TikaCoreProperties.EmbeddedResourceType.THUMBNAIL.name().equals(type)) { + thumbnail = m; + } else if (TikaCoreProperties.EmbeddedResourceType.RENDERING.name().equals(type)) { + 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 + } + } + ] +} From 37a0322e0ef387020b408d2e79476d07fce6b239 Mon Sep 17 00:00:00 2001 From: Dominik Schmidt Date: Sat, 29 Aug 2026 16:46:19 +0200 Subject: [PATCH 2/8] TIKA-4855 - the embedded-file integration test sees the ppt's thumbnail --- .../microsoft/POIContainerExtractionTest.java | 14 +++++++++----- 1 file changed, 9 insertions(+), 5 deletions(-) diff --git a/tika-parsers/tika-parsers-standard/tika-parsers-standard-integration-tests/src/test/java/org/apache/tika/parser/microsoft/POIContainerExtractionTest.java b/tika-parsers/tika-parsers-standard/tika-parsers-standard-integration-tests/src/test/java/org/apache/tika/parser/microsoft/POIContainerExtractionTest.java index cae0d633101..38624d0f196 100644 --- a/tika-parsers/tika-parsers-standard/tika-parsers-standard-integration-tests/src/test/java/org/apache/tika/parser/microsoft/POIContainerExtractionTest.java +++ b/tika-parsers/tika-parsers-standard/tika-parsers-standard-integration-tests/src/test/java/org/apache/tika/parser/microsoft/POIContainerExtractionTest.java @@ -160,10 +160,10 @@ public void testEmbeddedOfficeFiles() throws Exception { assertEquals(TYPE_PNG, handler.mediaTypes.get(15)); // PNG inside .xls - // PowerPoint with excel and word + // PowerPoint with excel and word; its SummaryInformation thumbnail comes last handler = process("testPPT_embeded.ppt", extractor, false); - assertEquals(7, handler.filenames.size()); - assertEquals(7, handler.mediaTypes.size()); + assertEquals(8, handler.filenames.size()); + assertEquals(8, handler.mediaTypes.size()); // Embedded objects get OLE IDs, slide images now get generated names assertEquals("1", handler.filenames.get(0)); @@ -171,6 +171,7 @@ public void testEmbeddedOfficeFiles() throws Exception { for (int i = 2; i < 7; i++) { assertNotNull(handler.filenames.get(i)); } + assertEquals("thumbnail.wmf", handler.filenames.get(7)); // But we do know their types assertEquals(TYPE_XLS, handler.mediaTypes.get(0)); // Embedded office doc assertEquals(TYPE_DOC, handler.mediaTypes.get(1)); // Embedded office doc @@ -179,11 +180,12 @@ public void testEmbeddedOfficeFiles() throws Exception { assertEquals(TYPE_PNG, handler.mediaTypes.get(4)); // Embedded image assertEquals(TYPE_PNG, handler.mediaTypes.get(5)); // Embedded image assertEquals(TYPE_PNG, handler.mediaTypes.get(6)); // Embedded image + assertEquals(TYPE_WMF, handler.mediaTypes.get(7)); // Document thumbnail // Run again on PowerPoint but with recursion handler = process("testPPT_embeded.ppt", extractor, true); - assertEquals(11, handler.filenames.size()); - assertEquals(11, handler.mediaTypes.size()); + assertEquals(12, handler.filenames.size()); + assertEquals(12, handler.mediaTypes.size()); assertEquals("1", handler.filenames.get(0)); assertEquals(null, handler.filenames.get(1)); @@ -195,6 +197,7 @@ public void testEmbeddedOfficeFiles() throws Exception { for (int i = 6; i < 11; i++) { assertNotNull(handler.filenames.get(i)); } + assertEquals("thumbnail.wmf", handler.filenames.get(11)); assertEquals(TYPE_XLS, handler.mediaTypes.get(0)); // Embedded office doc assertEquals(TYPE_PNG, handler.mediaTypes.get(1)); // PNG inside .xls @@ -207,6 +210,7 @@ public void testEmbeddedOfficeFiles() throws Exception { assertEquals(TYPE_PNG, handler.mediaTypes.get(8)); // Embedded image assertEquals(TYPE_PNG, handler.mediaTypes.get(9)); // Embedded image assertEquals(TYPE_PNG, handler.mediaTypes.get(10)); // Embedded image + assertEquals(TYPE_WMF, handler.mediaTypes.get(11)); // Document thumbnail // Word, with a non-office file (PDF) From 1428b304487da1b294d53803816701e3cb2ca704 Mon Sep 17 00:00:00 2001 From: Dominik Schmidt Date: Sat, 29 Aug 2026 17:47:44 +0200 Subject: [PATCH 3/8] TIKA-4855 - renderOnlyEmbeddedResourceTypes restricts the rendering to e.g. thumbnails With "renderOnlyEmbeddedResourceTypes": ["THUMBNAIL"] the parsers render the document's thumbnail but not the metafiles of embedded objects or pictures; empty (the default) renders every image. --- CHANGES.txt | 3 +- .../tika/parser/microsoft/EMFParser.java | 2 +- .../microsoft/MetafileParserConfig.java | 34 +++++++++++++++++++ .../tika/parser/microsoft/WMFParser.java | 2 +- .../tika/parser/microsoft/EMFParserTest.java | 17 ++++++++++ 5 files changed, 55 insertions(+), 3 deletions(-) diff --git a/CHANGES.txt b/CHANGES.txt index 7c99e715703..c3c4479153e 100644 --- a/CHANGES.txt +++ b/CHANGES.txt @@ -6,7 +6,8 @@ Release 4.1.0 - unreleased directly), EMFParser and WMFParser are RenderingParsers that emit the rendering as a RENDERING embedded document with "emf-parser" / "wmf-parser": {"renderImage": true, "renderWidth": 800}, off by - default, and OfficeParser emits the SummaryInformation thumbnail of the + default and restrictable to e.g. THUMBNAIL embedded documents with + "renderOnlyEmbeddedResourceTypes", and OfficeParser emits the SummaryInformation thumbnail of the OLE2 formats (a WMF) as a THUMBNAIL embedded document, as the OOXML parsers do with the docProps thumbnail (TIKA-XXXX). diff --git a/tika-parsers/tika-parsers-standard/tika-parsers-standard-modules/tika-parser-microsoft-module/src/main/java/org/apache/tika/parser/microsoft/EMFParser.java b/tika-parsers/tika-parsers-standard/tika-parsers-standard-modules/tika-parser-microsoft-module/src/main/java/org/apache/tika/parser/microsoft/EMFParser.java index bb9fe6ae85f..970b9ed34b0 100644 --- a/tika-parsers/tika-parsers-standard/tika-parsers-standard-modules/tika-parser-microsoft-module/src/main/java/org/apache/tika/parser/microsoft/EMFParser.java +++ b/tika-parsers/tika-parsers-standard/tika-parsers-standard-modules/tika-parser-microsoft-module/src/main/java/org/apache/tika/parser/microsoft/EMFParser.java @@ -173,7 +173,7 @@ public void parse(TikaInputStream tis, ContentHandler handler, Metadata metadata xhtml.endElement("p"); } MetafileParserConfig config = getConfig(context); - if (config.isRenderImage()) { + if (config.shouldRender(metadata)) { MetafileRendering.render(renderer, config, MEDIA_TYPE, ex, xhtml, metadata, context); } diff --git a/tika-parsers/tika-parsers-standard/tika-parsers-standard-modules/tika-parser-microsoft-module/src/main/java/org/apache/tika/parser/microsoft/MetafileParserConfig.java b/tika-parsers/tika-parsers-standard/tika-parsers-standard-modules/tika-parser-microsoft-module/src/main/java/org/apache/tika/parser/microsoft/MetafileParserConfig.java index 358faef8901..6a0f14c5102 100644 --- a/tika-parsers/tika-parsers-standard/tika-parsers-standard-modules/tika-parser-microsoft-module/src/main/java/org/apache/tika/parser/microsoft/MetafileParserConfig.java +++ b/tika-parsers/tika-parsers-standard/tika-parsers-standard-modules/tika-parser-microsoft-module/src/main/java/org/apache/tika/parser/microsoft/MetafileParserConfig.java @@ -17,7 +17,11 @@ package org.apache.tika.parser.microsoft; import java.io.Serializable; +import java.util.HashSet; +import java.util.Set; +import org.apache.tika.metadata.Metadata; +import org.apache.tika.metadata.TikaCoreProperties; import org.apache.tika.renderer.microsoft.POIMetafileRenderer; /** @@ -30,6 +34,7 @@ public class MetafileParserConfig implements Serializable { private boolean renderImage = false; private int renderWidth = 800; + private Set renderOnlyEmbeddedResourceTypes = new HashSet<>(); /** * Whether to render the image and emit the rendering as a RENDERING @@ -52,6 +57,35 @@ public int getRenderWidth() { return renderWidth; } + /** + * Restricts the rendering to images that are embedded documents of one + * of these {@code tk:embedded-resource-type}s, e.g. {@code ["THUMBNAIL"]} + * to render the thumbnail of an Office document but not the pictures of + * its embedded objects. Empty (the default) renders every image. + */ + public Set getRenderOnlyEmbeddedResourceTypes() { + return renderOnlyEmbeddedResourceTypes; + } + + public void setRenderOnlyEmbeddedResourceTypes(Set renderOnlyEmbeddedResourceTypes) { + this.renderOnlyEmbeddedResourceTypes = renderOnlyEmbeddedResourceTypes == null + ? new HashSet<>() : new HashSet<>(renderOnlyEmbeddedResourceTypes); + } + + /** + * Whether an image with this metadata is to be rendered. + */ + public boolean shouldRender(Metadata metadata) { + if (!renderImage) { + return false; + } + if (renderOnlyEmbeddedResourceTypes.isEmpty()) { + return true; + } + String type = metadata.get(TikaCoreProperties.EMBEDDED_RESOURCE_TYPE); + return type != null && renderOnlyEmbeddedResourceTypes.contains(type); + } + public void setRenderWidth(int renderWidth) { if (renderWidth < 1 || renderWidth > 10000) { throw new IllegalArgumentException( diff --git a/tika-parsers/tika-parsers-standard/tika-parsers-standard-modules/tika-parser-microsoft-module/src/main/java/org/apache/tika/parser/microsoft/WMFParser.java b/tika-parsers/tika-parsers-standard/tika-parsers-standard-modules/tika-parser-microsoft-module/src/main/java/org/apache/tika/parser/microsoft/WMFParser.java index 2a4a3083edd..cb486726ad5 100644 --- a/tika-parsers/tika-parsers-standard/tika-parsers-standard-modules/tika-parser-microsoft-module/src/main/java/org/apache/tika/parser/microsoft/WMFParser.java +++ b/tika-parsers/tika-parsers-standard/tika-parsers-standard-modules/tika-parser-microsoft-module/src/main/java/org/apache/tika/parser/microsoft/WMFParser.java @@ -127,7 +127,7 @@ public void parse(TikaInputStream tis, ContentHandler handler, Metadata metadata } } MetafileParserConfig config = getConfig(context); - if (config.isRenderImage()) { + if (config.shouldRender(metadata)) { MetafileRendering.render(renderer, config, MEDIA_TYPE, picture, xhtml, metadata, context); } 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 0ed01572321..81eb51bbfe9 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 @@ -122,6 +122,23 @@ public void testDocxThumbnailRendering() throws Exception { 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"); + + //a bare EMF is the document itself, not a THUMBNAIL: no rendering + metadataList = getRecursiveMetadata("testEMF.emf", context); + assertEquals(1, metadataList.size()); + } + private static Metadata byName(List metadataList, String name) { for (Metadata m : metadataList) { if (name.equals(m.get(TikaCoreProperties.RESOURCE_NAME_KEY))) { From c7121381ec33fa2269aaefa31d74a776c1cb2a31 Mon Sep 17 00:00:00 2001 From: Dominik Schmidt Date: Sat, 29 Aug 2026 22:01:43 +0200 Subject: [PATCH 4/8] TIKA-4855 - the rendering of a THUMBNAIL is a THUMBNAIL --- .../parser/microsoft/MetafileRendering.java | 13 ++++++++--- .../tika/parser/microsoft/EMFParserTest.java | 19 +++++++++------ .../parser/microsoft/OLE2ThumbnailTest.java | 23 +++++++++++-------- .../tika/parser/microsoft/WMFParserTest.java | 11 +++++---- 4 files changed, 43 insertions(+), 23 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/MetafileRendering.java b/tika-parsers/tika-parsers-standard/tika-parsers-standard-modules/tika-parser-microsoft-module/src/main/java/org/apache/tika/parser/microsoft/MetafileRendering.java index 8e4e0df4bcc..f2a8689f26b 100644 --- a/tika-parsers/tika-parsers-standard/tika-parsers-standard-modules/tika-parser-microsoft-module/src/main/java/org/apache/tika/parser/microsoft/MetafileRendering.java +++ b/tika-parsers/tika-parsers-standard/tika-parsers-standard-modules/tika-parser-microsoft-module/src/main/java/org/apache/tika/parser/microsoft/MetafileRendering.java @@ -38,9 +38,11 @@ /** * Shared by {@link EMFParser} and {@link WMFParser}: renders a parsed - * metafile through a {@link Renderer} and emits the result as a - * {@link TikaCoreProperties.EmbeddedResourceType#RENDERING} embedded - * document, the way the PDF parser emits page renderings. + * metafile through a {@link Renderer} and emits the result as an embedded + * document, the way the PDF parser emits page renderings. The rendering of + * a {@link TikaCoreProperties.EmbeddedResourceType#THUMBNAIL} is itself a + * THUMBNAIL (it is the same picture in a form a client can display); any + * other rendering is a {@link TikaCoreProperties.EmbeddedResourceType#RENDERING}. */ final class MetafileRendering { @@ -77,6 +79,11 @@ static void render(Renderer injected, MetafileParserConfig config, MediaType typ Metadata renderingMetadata = result.getMetadata(); renderingMetadata.set(TikaCoreProperties.RESOURCE_NAME_KEY, renderingName(metadata, renderingMetadata)); + if (TikaCoreProperties.EmbeddedResourceType.THUMBNAIL.name() + .equals(metadata.get(TikaCoreProperties.EMBEDDED_RESOURCE_TYPE))) { + renderingMetadata.set(TikaCoreProperties.EMBEDDED_RESOURCE_TYPE, + TikaCoreProperties.EmbeddedResourceType.THUMBNAIL.name()); + } if (extractor.shouldParseEmbedded(renderingMetadata, context)) { try (TikaInputStream tis = result.getInputStream()) { extractor.parseEmbedded(tis, new EmbeddedContentHandler(xhtml), 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 81eb51bbfe9..5ae04db79f3 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 @@ -81,7 +81,8 @@ public void testRenderingFromConfig() throws Exception { List metadataList = getRecursiveMetadata("testEMF.emf", parser, metadata, new ParseContext(), false); assertEquals(2, metadataList.size()); - assertRendering(metadataList.get(1), "testEMF.png"); + assertRendering(metadataList.get(1), "testEMF.png", + TikaCoreProperties.EmbeddedResourceType.RENDERING); } /** @@ -94,7 +95,8 @@ public void testRenderingFromParseContext() throws Exception { context.setJsonConfig("emf-parser", "{\"renderImage\": true}"); List metadataList = getRecursiveMetadata("testEMF.emf", context); assertEquals(2, metadataList.size()); - assertRendering(metadataList.get(1), "testEMF.png"); + assertRendering(metadataList.get(1), "testEMF.png", + TikaCoreProperties.EmbeddedResourceType.RENDERING); assertEquals("1", metadataList.get(1).get(TikaCoreProperties.EMBEDDED_DEPTH)); } @@ -116,7 +118,8 @@ public void testDocxThumbnailRendering() throws Exception { thumbnail.get(TikaCoreProperties.EMBEDDED_RESOURCE_TYPE)); assertEquals("1", thumbnail.get(TikaCoreProperties.EMBEDDED_DEPTH)); Metadata rendering = byName(metadataList, "thumbnail.png"); - assertRendering(rendering, "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)); @@ -132,7 +135,8 @@ public void testRenderOnlyThumbnails() throws Exception { context.setJsonConfig("emf-parser", "{\"renderImage\": true, \"renderOnlyEmbeddedResourceTypes\": [\"THUMBNAIL\"]}"); List metadataList = getRecursiveMetadata("testDOCX_Thumbnail.docx", context); - assertRendering(byName(metadataList, "thumbnail.png"), "thumbnail.png"); + 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); @@ -151,11 +155,12 @@ private static Metadata byName(List metadataList, String 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) { + private static void assertRendering(Metadata rendering, String name, + TikaCoreProperties.EmbeddedResourceType type) { assertEquals("image/png", rendering.get(HttpHeaders.CONTENT_TYPE)); - assertEquals(TikaCoreProperties.EmbeddedResourceType.RENDERING.name(), - rendering.get(TikaCoreProperties.EMBEDDED_RESOURCE_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); 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 index 67cd55a6582..5bdd0f02ec5 100644 --- 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 @@ -41,7 +41,8 @@ public class OLE2ThumbnailTest extends TikaTest { @Test public void testPptThumbnail() throws Exception { List metadataList = getRecursiveMetadata("testPPT_various.ppt"); - Metadata thumbnail = byType(metadataList, TikaCoreProperties.EmbeddedResourceType.THUMBNAIL); + 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)); @@ -55,9 +56,10 @@ public void testPptThumbnailRendering() throws Exception { ParseContext context = new ParseContext(); context.setJsonConfig("wmf-parser", "{\"renderImage\": true, \"renderWidth\": 400}"); List metadataList = getRecursiveMetadata("testPPT_various.ppt", context); - Metadata rendering = byType(metadataList, TikaCoreProperties.EmbeddedResourceType.RENDERING); + //the rendering of the thumbnail is a THUMBNAIL as well + Metadata rendering = byTypeAndContentType(metadataList, + TikaCoreProperties.EmbeddedResourceType.THUMBNAIL, "image/png"); assertNotNull(rendering); - assertEquals("image/png", rendering.get(HttpHeaders.CONTENT_TYPE)); 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); @@ -73,12 +75,13 @@ public void testDocThumbnailRendering() throws Exception { ParseContext context = new ParseContext(); context.setJsonConfig("wmf-parser", "{\"renderImage\": true}"); List metadataList = getRecursiveMetadata("testControlCharacters.doc", context); - Metadata thumbnail = byType(metadataList, TikaCoreProperties.EmbeddedResourceType.THUMBNAIL); + Metadata thumbnail = byTypeAndContentType(metadataList, + TikaCoreProperties.EmbeddedResourceType.THUMBNAIL, "image/wmf"); assertNotNull(thumbnail); assertEquals("image/wmf", thumbnail.get(HttpHeaders.CONTENT_TYPE)); - Metadata rendering = byType(metadataList, TikaCoreProperties.EmbeddedResourceType.RENDERING); + Metadata rendering = byTypeAndContentType(metadataList, + TikaCoreProperties.EmbeddedResourceType.THUMBNAIL, "image/png"); assertNotNull(rendering); - assertEquals("image/png", rendering.get(HttpHeaders.CONTENT_TYPE)); assertNull(thumbnail.get(TikaCoreProperties.TIKA_META_EXCEPTION_EMBEDDED_STREAM)); } @@ -91,10 +94,12 @@ public void testUnusableThumbnailIsSkipped() throws Exception { assertEquals(0, count(metadataList, TikaCoreProperties.EmbeddedResourceType.THUMBNAIL)); } - private static Metadata byType(List metadataList, - TikaCoreProperties.EmbeddedResourceType type) { + 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))) { + if (type.name().equals(m.get(TikaCoreProperties.EMBEDDED_RESOURCE_TYPE)) + && contentType.equals(m.get(HttpHeaders.CONTENT_TYPE))) { return m; } } 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 94bb6e9ca13..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 @@ -67,7 +67,7 @@ public void testRendering() throws Exception { /** * The docProps thumbnail of this workbook is a WMF; with rendering on its - * PNG rendering follows it, one level deeper. + * PNG rendering follows it, one level deeper, as a THUMBNAIL as well. */ @Test public void testXlsxThumbnailRendering() throws Exception { @@ -77,10 +77,13 @@ public void testXlsxThumbnailRendering() throws Exception { Metadata thumbnail = null; Metadata rendering = null; for (Metadata m : metadataList) { - String type = m.get(TikaCoreProperties.EMBEDDED_RESOURCE_TYPE); - if (TikaCoreProperties.EmbeddedResourceType.THUMBNAIL.name().equals(type)) { + 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 if (TikaCoreProperties.EmbeddedResourceType.RENDERING.name().equals(type)) { + } else { rendering = m; } } From f4c68e09a7f47cbaa5634e053b5f66c17433c639 Mon Sep 17 00:00:00 2001 From: Dominik Schmidt Date: Mon, 31 Aug 2026 08:55:12 +0200 Subject: [PATCH 5/8] TIKA-4855 - address review: merged javadocs, rendering failures recorded on the metafile's metadata and in the renderer's result, CHANGES wrapped --- CHANGES.txt | 7 ++++--- .../org/apache/tika/parser/microsoft/EMFParser.java | 8 +++----- .../tika/parser/microsoft/MetafileRendering.java | 12 ++++++++---- .../org/apache/tika/parser/microsoft/WMFParser.java | 6 ++---- .../tika/renderer/microsoft/POIMetafileRenderer.java | 3 +++ 5 files changed, 20 insertions(+), 16 deletions(-) diff --git a/CHANGES.txt b/CHANGES.txt index 1ab6790f70e..b62a0c27693 100644 --- a/CHANGES.txt +++ b/CHANGES.txt @@ -7,9 +7,10 @@ Release 4.1.0 - unreleased rendering as a RENDERING embedded document with "emf-parser" / "wmf-parser": {"renderImage": true, "renderWidth": 800}, off by default and restrictable to e.g. THUMBNAIL embedded documents with - "renderOnlyEmbeddedResourceTypes", and OfficeParser emits the SummaryInformation thumbnail of the - OLE2 formats (a WMF) as a THUMBNAIL embedded document, as the OOXML - parsers do with the docProps thumbnail (TIKA-4855). + "renderOnlyEmbeddedResourceTypes", and OfficeParser emits the + SummaryInformation thumbnail of the OLE2 formats (a WMF) as a THUMBNAIL + embedded document, as the OOXML parsers do with the docProps thumbnail + (TIKA-4855). * Raw camera formats are detected by content: RawTiffDetector tells Nikon NEF/NRW, Pentax PEF/PTX, Sony ARW/SRF/SR2, Samsung SRW and Adobe diff --git a/tika-parsers/tika-parsers-standard/tika-parsers-standard-modules/tika-parser-microsoft-module/src/main/java/org/apache/tika/parser/microsoft/EMFParser.java b/tika-parsers/tika-parsers-standard/tika-parsers-standard-modules/tika-parser-microsoft-module/src/main/java/org/apache/tika/parser/microsoft/EMFParser.java index 970b9ed34b0..a0faf5361d6 100644 --- a/tika-parsers/tika-parsers-standard/tika-parsers-standard-modules/tika-parser-microsoft-module/src/main/java/org/apache/tika/parser/microsoft/EMFParser.java +++ b/tika-parsers/tika-parsers-standard/tika-parsers-standard-modules/tika-parser-microsoft-module/src/main/java/org/apache/tika/parser/microsoft/EMFParser.java @@ -67,11 +67,9 @@ * We're also relying on storage order for text order, which isn't great. * We'd have to do something like what PDFBox or XPS do to sort the * runs and then put the cow back together from the hamburger...lol... - */ -/** - * Extracts the text of an EMF image and its embedded WMF and multi-format - * pictures. With {@link MetafileParserConfig#setRenderImage(boolean)} - * ("emf-parser": {"renderImage": true}) the image is also rendered through the + *

+ * With {@link MetafileParserConfig#setRenderImage(boolean)} + * ("emf-parser": {"renderImage": true}) the image is rendered through the * configured {@link Renderer}, the * {@link org.apache.tika.renderer.microsoft.POIMetafileRenderer} by * default, and emitted as a diff --git a/tika-parsers/tika-parsers-standard/tika-parsers-standard-modules/tika-parser-microsoft-module/src/main/java/org/apache/tika/parser/microsoft/MetafileRendering.java b/tika-parsers/tika-parsers-standard/tika-parsers-standard-modules/tika-parser-microsoft-module/src/main/java/org/apache/tika/parser/microsoft/MetafileRendering.java index f2a8689f26b..d35143ccae5 100644 --- a/tika-parsers/tika-parsers-standard/tika-parsers-standard-modules/tika-parser-microsoft-module/src/main/java/org/apache/tika/parser/microsoft/MetafileRendering.java +++ b/tika-parsers/tika-parsers-standard/tika-parsers-standard-modules/tika-parser-microsoft-module/src/main/java/org/apache/tika/parser/microsoft/MetafileRendering.java @@ -65,7 +65,7 @@ static void render(Renderer injected, MetafileParserConfig config, MediaType typ EmbeddedDocumentExtractor extractor = EmbeddedDocumentUtil.getEmbeddedDocumentExtractor(context); try (TikaInputStream pictureStream = TikaInputStream.get(new byte[0]); - RenderResults results = render(renderer, pictureStream, picture, renderMetadata, + RenderResults results = render(renderer, pictureStream, picture, metadata, context)) { if (results == null) { return; @@ -94,17 +94,21 @@ static void render(Renderer injected, MetafileParserConfig config, MediaType typ } } + /** + * @param metadata the metafile's metadata; a rendering failure is + * recorded there, so it is not silently swallowed + */ private static RenderResults render(Renderer renderer, TikaInputStream pictureStream, - Object picture, Metadata renderMetadata, + Object picture, Metadata metadata, ParseContext context) throws IOException { //hand the parsed picture over instead of re-reading the stream pictureStream.setOpenContainer(picture); try { - return renderer.render(pictureStream, renderMetadata, context); + return renderer.render(pictureStream, metadata, context); } catch (SecurityException e) { throw e; } catch (Exception e) { - EmbeddedDocumentUtil.recordException(e, renderMetadata, context); + EmbeddedDocumentUtil.recordException(e, metadata, context); return null; } } diff --git a/tika-parsers/tika-parsers-standard/tika-parsers-standard-modules/tika-parser-microsoft-module/src/main/java/org/apache/tika/parser/microsoft/WMFParser.java b/tika-parsers/tika-parsers-standard/tika-parsers-standard-modules/tika-parser-microsoft-module/src/main/java/org/apache/tika/parser/microsoft/WMFParser.java index cb486726ad5..ca51b59e2c9 100644 --- a/tika-parsers/tika-parsers-standard/tika-parsers-standard-modules/tika-parser-microsoft-module/src/main/java/org/apache/tika/parser/microsoft/WMFParser.java +++ b/tika-parsers/tika-parsers-standard/tika-parsers-standard-modules/tika-parser-microsoft-module/src/main/java/org/apache/tika/parser/microsoft/WMFParser.java @@ -49,10 +49,8 @@ /** * This parser offers a very rough capability to extract text if there * is text stored in the WMF files. - */ -/** - * Extracts the text of a WMF image. With - * {@link MetafileParserConfig#setRenderImage(boolean)} + *

+ * With {@link MetafileParserConfig#setRenderImage(boolean)} * ("wmf-parser": {"renderImage": true}) the image is also rendered through the * configured {@link Renderer}, the * {@link org.apache.tika.renderer.microsoft.POIMetafileRenderer} by 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 index bc7e30499a0..d578cdb80ea 100644 --- 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 @@ -39,6 +39,7 @@ import org.apache.tika.annotation.TikaComponent; 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; @@ -123,6 +124,8 @@ public RenderResults render(TikaInputStream tis, Metadata metadata, ParseContext } 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)); } From 17079219bfedf5e31e31ba78a00925dd85daea03 Mon Sep 17 00:00:00 2001 From: Dominik Schmidt Date: Mon, 31 Aug 2026 09:00:32 +0200 Subject: [PATCH 6/8] TIKA-4855 - keep the render metadata for the composite renderer's routing; record failures on the metafile's metadata --- .../parser/microsoft/MetafileRendering.java | 19 +++++++++++-------- 1 file changed, 11 insertions(+), 8 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/MetafileRendering.java b/tika-parsers/tika-parsers-standard/tika-parsers-standard-modules/tika-parser-microsoft-module/src/main/java/org/apache/tika/parser/microsoft/MetafileRendering.java index d35143ccae5..1f49eb05b5e 100644 --- a/tika-parsers/tika-parsers-standard/tika-parsers-standard-modules/tika-parser-microsoft-module/src/main/java/org/apache/tika/parser/microsoft/MetafileRendering.java +++ b/tika-parsers/tika-parsers-standard/tika-parsers-standard-modules/tika-parser-microsoft-module/src/main/java/org/apache/tika/parser/microsoft/MetafileRendering.java @@ -65,8 +65,8 @@ static void render(Renderer injected, MetafileParserConfig config, MediaType typ EmbeddedDocumentExtractor extractor = EmbeddedDocumentUtil.getEmbeddedDocumentExtractor(context); try (TikaInputStream pictureStream = TikaInputStream.get(new byte[0]); - RenderResults results = render(renderer, pictureStream, picture, metadata, - context)) { + RenderResults results = render(renderer, pictureStream, picture, renderMetadata, + metadata, context)) { if (results == null) { return; } @@ -95,20 +95,23 @@ static void render(Renderer injected, MetafileParserConfig config, MediaType typ } /** - * @param metadata the metafile's metadata; a rendering failure is - * recorded there, so it is not silently swallowed + * @param renderMetadata what the renderer is told about the picture (its + * type, so a composite renderer can route it) + * @param parentMetadata the metafile's metadata; a rendering failure is + * recorded there, so it is not silently swallowed */ private static RenderResults render(Renderer renderer, TikaInputStream pictureStream, - Object picture, Metadata metadata, - ParseContext context) throws IOException { + Object picture, Metadata renderMetadata, + Metadata parentMetadata, ParseContext context) + throws IOException { //hand the parsed picture over instead of re-reading the stream pictureStream.setOpenContainer(picture); try { - return renderer.render(pictureStream, metadata, context); + return renderer.render(pictureStream, renderMetadata, context); } catch (SecurityException e) { throw e; } catch (Exception e) { - EmbeddedDocumentUtil.recordException(e, metadata, context); + EmbeddedDocumentUtil.recordException(e, parentMetadata, context); return null; } } From db2bc977b1419f250d2f37734434c8b811cbc028 Mon Sep 17 00:00:00 2001 From: Dominik Schmidt Date: Mon, 31 Aug 2026 11:54:05 +0200 Subject: [PATCH 7/8] TIKA-4855 - name() for the resource type; carry the renderer's diagnostics to the metafile on failure --- .../tika/parser/microsoft/MetafileRendering.java | 12 ++++++++++-- .../apache/tika/parser/microsoft/OfficeParser.java | 2 +- 2 files changed, 11 insertions(+), 3 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/MetafileRendering.java b/tika-parsers/tika-parsers-standard/tika-parsers-standard-modules/tika-parser-microsoft-module/src/main/java/org/apache/tika/parser/microsoft/MetafileRendering.java index 1f49eb05b5e..9035739ae1f 100644 --- a/tika-parsers/tika-parsers-standard/tika-parsers-standard-modules/tika-parser-microsoft-module/src/main/java/org/apache/tika/parser/microsoft/MetafileRendering.java +++ b/tika-parsers/tika-parsers-standard/tika-parsers-standard-modules/tika-parser-microsoft-module/src/main/java/org/apache/tika/parser/microsoft/MetafileRendering.java @@ -72,8 +72,16 @@ static void render(Renderer injected, MetafileParserConfig config, MediaType typ } for (RenderResult result : results.getResults()) { if (result.getStatus() != RenderResult.STATUS.SUCCESS) { - EmbeddedDocumentUtil.recordException( - new TikaException("metafile rendering failed"), metadata, context); + //carry the renderer's diagnostics over to the metafile + String[] warnings = result.getMetadata() + .getValues(TikaCoreProperties.TIKA_META_EXCEPTION_WARNING); + if (warnings.length == 0) { + EmbeddedDocumentUtil.recordException( + new TikaException("metafile rendering failed"), metadata, context); + } + for (String warning : warnings) { + metadata.add(TikaCoreProperties.TIKA_META_EXCEPTION_WARNING, warning); + } continue; } Metadata renderingMetadata = result.getMetadata(); diff --git a/tika-parsers/tika-parsers-standard/tika-parsers-standard-modules/tika-parser-microsoft-module/src/main/java/org/apache/tika/parser/microsoft/OfficeParser.java b/tika-parsers/tika-parsers-standard/tika-parsers-standard-modules/tika-parser-microsoft-module/src/main/java/org/apache/tika/parser/microsoft/OfficeParser.java index 9433eb5d2be..8ef289a0180 100644 --- a/tika-parsers/tika-parsers-standard/tika-parsers-standard-modules/tika-parser-microsoft-module/src/main/java/org/apache/tika/parser/microsoft/OfficeParser.java +++ b/tika-parsers/tika-parsers-standard/tika-parsers-standard-modules/tika-parser-microsoft-module/src/main/java/org/apache/tika/parser/microsoft/OfficeParser.java @@ -323,7 +323,7 @@ private void handleThumbnail(byte[] wmf, XHTMLContentHandler xhtml, ParseContext Metadata embeddedMetadata = Metadata.newInstance(context); embeddedMetadata.set(TikaCoreProperties.RESOURCE_NAME_KEY, "thumbnail.wmf"); embeddedMetadata.set(TikaCoreProperties.EMBEDDED_RESOURCE_TYPE, - TikaCoreProperties.EmbeddedResourceType.THUMBNAIL.toString()); + TikaCoreProperties.EmbeddedResourceType.THUMBNAIL.name()); embeddedMetadata.set(HttpHeaders.CONTENT_TYPE, "image/wmf"); EmbeddedDocumentExtractor extractor = EmbeddedDocumentUtil.getEmbeddedDocumentExtractor(context); From bed5c22de4bb09337f6ab03bd44d6c66e906406f Mon Sep 17 00:00:00 2001 From: Dominik Schmidt Date: Tue, 1 Sep 2026 13:46:45 +0200 Subject: [PATCH 8/8] TIKA-4855 - address review: renderWidth reaches the injected renderer, bounded rendering height, renderer reads the metafile itself, gate before rendering, validated resource types, thumbnail switch, shared parser base --- CHANGES.txt | 3 +- .../microsoft/AbstractMetafileParser.java | 74 +++++++++++++++++ .../tika/parser/microsoft/EMFParser.java | 37 +++------ .../microsoft/MetafileParserConfig.java | 12 ++- .../parser/microsoft/MetafileRendering.java | 73 ++++++++++++----- .../tika/parser/microsoft/OfficeParser.java | 8 +- .../parser/microsoft/OfficeParserConfig.java | 15 ++++ .../tika/parser/microsoft/WMFParser.java | 36 +++------ .../microsoft/POIMetafileRenderer.java | 79 +++++++++++++++---- .../tika/parser/microsoft/EMFParserTest.java | 48 +++++++++++ .../parser/microsoft/OLE2ThumbnailTest.java | 27 +++++++ 11 files changed, 321 insertions(+), 91 deletions(-) create mode 100644 tika-parsers/tika-parsers-standard/tika-parsers-standard-modules/tika-parser-microsoft-module/src/main/java/org/apache/tika/parser/microsoft/AbstractMetafileParser.java diff --git a/CHANGES.txt b/CHANGES.txt index d4c4dfeb234..370a98d95f5 100644 --- a/CHANGES.txt +++ b/CHANGES.txt @@ -9,7 +9,8 @@ Release 4.1.0 - unreleased default and restrictable to e.g. THUMBNAIL embedded documents with "renderOnlyEmbeddedResourceTypes", and OfficeParser emits the SummaryInformation thumbnail of the OLE2 formats (a WMF) as a THUMBNAIL - embedded document, as the OOXML parsers do with the docProps thumbnail + embedded document, as the OOXML parsers do with the docProps thumbnail, + switchable with "office-parser": {"extractThumbnail": false} (TIKA-4855). * Audio cover art is emitted as a THUMBNAIL embedded document, like the diff --git a/tika-parsers/tika-parsers-standard/tika-parsers-standard-modules/tika-parser-microsoft-module/src/main/java/org/apache/tika/parser/microsoft/AbstractMetafileParser.java b/tika-parsers/tika-parsers-standard/tika-parsers-standard-modules/tika-parser-microsoft-module/src/main/java/org/apache/tika/parser/microsoft/AbstractMetafileParser.java new file mode 100644 index 00000000000..2c73b16a075 --- /dev/null +++ b/tika-parsers/tika-parsers-standard/tika-parsers-standard-modules/tika-parser-microsoft-module/src/main/java/org/apache/tika/parser/microsoft/AbstractMetafileParser.java @@ -0,0 +1,74 @@ +/* + * 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 java.io.IOException; + +import org.apache.tika.config.ParseContextConfig; +import org.apache.tika.exception.TikaException; +import org.apache.tika.io.TikaInputStream; +import org.apache.tika.metadata.Metadata; +import org.apache.tika.parser.ParseContext; +import org.apache.tika.parser.Parser; +import org.apache.tika.parser.RenderingParser; +import org.apache.tika.renderer.Renderer; + +/** + * What {@link EMFParser} and {@link WMFParser} share: the configuration of + * the metafile parsers and the renderer they may be handed. + */ +abstract class AbstractMetafileParser implements Parser, RenderingParser { + + private final MetafileParserConfig defaultConfig; + private Renderer renderer; + + AbstractMetafileParser(MetafileParserConfig defaultConfig) { + this.defaultConfig = defaultConfig; + } + + /** + * The component name this parser reads its configuration from, + * {@code emf-parser} or {@code wmf-parser}. + */ + abstract String componentName(); + + MetafileParserConfig getConfig(ParseContext context) throws TikaException, IOException { + return ParseContextConfig.getConfig(context, componentName(), MetafileParserConfig.class, + defaultConfig); + } + + /** + * Spools the stream when the image is going to be rendered, so the + * renderer can read the metafile itself rather than only the parsed + * picture. + */ + static void prepareForRendering(TikaInputStream tis, MetafileParserConfig config, + Metadata metadata) throws IOException { + if (config.shouldRender(metadata)) { + tis.getFile(); + } + } + + Renderer getRenderer() { + return renderer; + } + + @Override + public void setRenderer(Renderer renderer) { + this.renderer = renderer; + } +} diff --git a/tika-parsers/tika-parsers-standard/tika-parsers-standard-modules/tika-parser-microsoft-module/src/main/java/org/apache/tika/parser/microsoft/EMFParser.java b/tika-parsers/tika-parsers-standard/tika-parsers-standard-modules/tika-parser-microsoft-module/src/main/java/org/apache/tika/parser/microsoft/EMFParser.java index a0faf5361d6..dc6c57c6c23 100644 --- a/tika-parsers/tika-parsers-standard/tika-parsers-standard-modules/tika-parser-microsoft-module/src/main/java/org/apache/tika/parser/microsoft/EMFParser.java +++ b/tika-parsers/tika-parsers-standard/tika-parsers-standard-modules/tika-parser-microsoft-module/src/main/java/org/apache/tika/parser/microsoft/EMFParser.java @@ -37,7 +37,6 @@ import org.apache.tika.annotation.TikaComponent; import org.apache.tika.config.ConfigDeserializer; import org.apache.tika.config.JsonConfig; -import org.apache.tika.config.ParseContextConfig; import org.apache.tika.exception.TikaException; import org.apache.tika.extractor.EmbeddedDocumentExtractor; import org.apache.tika.extractor.EmbeddedDocumentUtil; @@ -48,8 +47,6 @@ import org.apache.tika.metadata.TikaCoreProperties; import org.apache.tika.mime.MediaType; import org.apache.tika.parser.ParseContext; -import org.apache.tika.parser.Parser; -import org.apache.tika.parser.RenderingParser; import org.apache.tika.renderer.Renderer; import org.apache.tika.sax.EmbeddedContentHandler; import org.apache.tika.sax.XHTMLContentHandler; @@ -79,26 +76,28 @@ * Word document. */ @TikaComponent -public class EMFParser implements Parser, RenderingParser { +public class EMFParser extends AbstractMetafileParser { public static Property EMF_ICON_ONLY = Property.internalBoolean("emf:icon-only"); public static Property EMF_ICON_STRING = Property.internalText("emf:icon-string"); - private final MetafileParserConfig defaultConfig; - private Renderer renderer; - public EMFParser() { this(new MetafileParserConfig()); } public EMFParser(MetafileParserConfig config) { - this.defaultConfig = config; + super(config); } public EMFParser(JsonConfig jsonConfig) { this(ConfigDeserializer.buildConfig(jsonConfig, MetafileParserConfig.class)); } + @Override + String componentName() { + return "emf-parser"; + } + private static String ICON_ONLY = "IconOnly"; private static final MediaType MEDIA_TYPE = MediaType.image("emf"); @@ -133,6 +132,8 @@ public void parse(TikaInputStream tis, ContentHandler handler, Metadata metadata XHTMLContentHandler xhtml = new XHTMLContentHandler(handler, metadata, context); xhtml.startDocument(); try { + MetafileParserConfig config = getConfig(context); + prepareForRendering(tis, config, metadata); HemfPicture ex = new HemfPicture(tis); ParseState parseState = new ParseState(); long fudgeFactorX = 10;//derive this from the font or frame/bounds information @@ -170,10 +171,9 @@ public void parse(TikaInputStream tis, ContentHandler handler, Metadata metadata xhtml.characters(buffer.toString()); xhtml.endElement("p"); } - MetafileParserConfig config = getConfig(context); if (config.shouldRender(metadata)) { - MetafileRendering.render(renderer, config, MEDIA_TYPE, ex, xhtml, metadata, - context); + MetafileRendering.render(getRenderer(), config, MEDIA_TYPE, tis, ex, xhtml, + metadata, context); } } catch (RecordFormatException e) { //POI's hemfparser can throw these for "parse @@ -185,21 +185,6 @@ public void parse(TikaInputStream tis, ContentHandler handler, Metadata metadata xhtml.endDocument(); } - private MetafileParserConfig getConfig(ParseContext context) - throws TikaException, IOException { - return ParseContextConfig.getConfig(context, "emf-parser", - MetafileParserConfig.class, defaultConfig); - } - - @Override - public void setRenderer(Renderer renderer) { - this.renderer = renderer; - } - - public Renderer getRenderer() { - return renderer; - } - private void handleExtTextOut(HemfText.EmfExtTextOutA record, ParseState parseState, StringBuilder buffer, XHTMLContentHandler xhtml, double fudgeFactorX, Charset charset) throws IOException, SAXException { diff --git a/tika-parsers/tika-parsers-standard/tika-parsers-standard-modules/tika-parser-microsoft-module/src/main/java/org/apache/tika/parser/microsoft/MetafileParserConfig.java b/tika-parsers/tika-parsers-standard/tika-parsers-standard-modules/tika-parser-microsoft-module/src/main/java/org/apache/tika/parser/microsoft/MetafileParserConfig.java index 6a0f14c5102..2db698f672a 100644 --- a/tika-parsers/tika-parsers-standard/tika-parsers-standard-modules/tika-parser-microsoft-module/src/main/java/org/apache/tika/parser/microsoft/MetafileParserConfig.java +++ b/tika-parsers/tika-parsers-standard/tika-parsers-standard-modules/tika-parser-microsoft-module/src/main/java/org/apache/tika/parser/microsoft/MetafileParserConfig.java @@ -68,8 +68,16 @@ public Set getRenderOnlyEmbeddedResourceTypes() { } public void setRenderOnlyEmbeddedResourceTypes(Set renderOnlyEmbeddedResourceTypes) { - this.renderOnlyEmbeddedResourceTypes = renderOnlyEmbeddedResourceTypes == null - ? new HashSet<>() : new HashSet<>(renderOnlyEmbeddedResourceTypes); + if (renderOnlyEmbeddedResourceTypes == null) { + this.renderOnlyEmbeddedResourceTypes = new HashSet<>(); + return; + } + Set types = new HashSet<>(); + for (String type : renderOnlyEmbeddedResourceTypes) { + //a typo would silently disable rendering + types.add(TikaCoreProperties.EmbeddedResourceType.valueOf(type).name()); + } + this.renderOnlyEmbeddedResourceTypes = types; } /** diff --git a/tika-parsers/tika-parsers-standard/tika-parsers-standard-modules/tika-parser-microsoft-module/src/main/java/org/apache/tika/parser/microsoft/MetafileRendering.java b/tika-parsers/tika-parsers-standard/tika-parsers-standard-modules/tika-parser-microsoft-module/src/main/java/org/apache/tika/parser/microsoft/MetafileRendering.java index 9035739ae1f..d4efd0a12cd 100644 --- a/tika-parsers/tika-parsers-standard/tika-parsers-standard-modules/tika-parser-microsoft-module/src/main/java/org/apache/tika/parser/microsoft/MetafileRendering.java +++ b/tika-parsers/tika-parsers-standard/tika-parsers-standard-modules/tika-parser-microsoft-module/src/main/java/org/apache/tika/parser/microsoft/MetafileRendering.java @@ -23,6 +23,7 @@ import org.apache.tika.exception.TikaException; import org.apache.tika.extractor.EmbeddedDocumentExtractor; import org.apache.tika.extractor.EmbeddedDocumentUtil; +import org.apache.tika.io.FilenameUtils; import org.apache.tika.io.TikaInputStream; import org.apache.tika.metadata.HttpHeaders; import org.apache.tika.metadata.Metadata; @@ -54,8 +55,8 @@ private MetafileRendering() { * @param picture the parsed {@code HemfPicture} or {@code HwmfPicture} */ static void render(Renderer injected, MetafileParserConfig config, MediaType type, - Object picture, XHTMLContentHandler xhtml, Metadata metadata, - ParseContext context) throws IOException, SAXException { + TikaInputStream source, Object picture, XHTMLContentHandler xhtml, + Metadata metadata, ParseContext context) throws IOException, SAXException { //like the PDF parser: the injected renderer if it handles the type, //the default one otherwise Renderer renderer = injected != null && injected.getSupportedTypes(context).contains(type) @@ -64,7 +65,17 @@ static void render(Renderer injected, MetafileParserConfig config, MediaType typ renderMetadata.set(TikaCoreProperties.TYPE, type.toString()); EmbeddedDocumentExtractor extractor = EmbeddedDocumentUtil.getEmbeddedDocumentExtractor(context); - try (TikaInputStream pictureStream = TikaInputStream.get(new byte[0]); + //rendering is the expensive part: ask before paying for it. The name + //assumes the renderer's default format; the result's own metadata + //replaces it below. + Metadata gate = Metadata.newInstance(context); + gate.set(TikaCoreProperties.RESOURCE_NAME_KEY, renderingName(metadata, "png")); + gate.set(HttpHeaders.CONTENT_TYPE, "image/png"); + gate.set(TikaCoreProperties.EMBEDDED_RESOURCE_TYPE, renderingType(metadata).name()); + if (!extractor.shouldParseEmbedded(gate, context)) { + return; + } + try (TikaInputStream pictureStream = pictureStream(source); RenderResults results = render(renderer, pictureStream, picture, renderMetadata, metadata, context)) { if (results == null) { @@ -86,12 +97,9 @@ static void render(Renderer injected, MetafileParserConfig config, MediaType typ } Metadata renderingMetadata = result.getMetadata(); renderingMetadata.set(TikaCoreProperties.RESOURCE_NAME_KEY, - renderingName(metadata, renderingMetadata)); - if (TikaCoreProperties.EmbeddedResourceType.THUMBNAIL.name() - .equals(metadata.get(TikaCoreProperties.EMBEDDED_RESOURCE_TYPE))) { - renderingMetadata.set(TikaCoreProperties.EMBEDDED_RESOURCE_TYPE, - TikaCoreProperties.EmbeddedResourceType.THUMBNAIL.name()); - } + renderingName(metadata, extension(renderingMetadata))); + renderingMetadata.set(TikaCoreProperties.EMBEDDED_RESOURCE_TYPE, + renderingType(metadata).name()); if (extractor.shouldParseEmbedded(renderingMetadata, context)) { try (TikaInputStream tis = result.getInputStream()) { extractor.parseEmbedded(tis, new EmbeddedContentHandler(xhtml), @@ -102,6 +110,36 @@ static void render(Renderer injected, MetafileParserConfig config, MediaType typ } } + /** + * The stream the renderer reads the picture from: the metafile itself + * where the parser spooled it, so a renderer that does not know the + * open-container shortcut still sees the bytes. + */ + private static TikaInputStream pictureStream(TikaInputStream source) throws IOException { + if (source != null && source.hasFile()) { + return TikaInputStream.get(source.getPath()); + } + return TikaInputStream.get(new byte[0]); + } + + /** + * The rendering of a THUMBNAIL is itself a THUMBNAIL: it is the same + * picture in a form a client can display. Any other rendering is a + * RENDERING. + */ + private static TikaCoreProperties.EmbeddedResourceType renderingType(Metadata metadata) { + return TikaCoreProperties.EmbeddedResourceType.THUMBNAIL.name() + .equals(metadata.get(TikaCoreProperties.EMBEDDED_RESOURCE_TYPE)) + ? TikaCoreProperties.EmbeddedResourceType.THUMBNAIL + : TikaCoreProperties.EmbeddedResourceType.RENDERING; + } + + private static String extension(Metadata renderingMetadata) { + String contentType = renderingMetadata.get(HttpHeaders.CONTENT_TYPE); + return contentType != null && contentType.startsWith("image/") + ? contentType.substring("image/".length()) : "png"; + } + /** * @param renderMetadata what the renderer is told about the picture (its * type, so a composite renderer can route it) @@ -131,20 +169,15 @@ private static Renderer defaultRenderer(MetafileParserConfig config) { } /** - * The rendering is named after the image, with the rendering's format - * as its extension. + * The rendering is named after the image, with the rendering's format as + * its extension. */ - private static String renderingName(Metadata metadata, Metadata renderingMetadata) { - String contentType = renderingMetadata.get(HttpHeaders.CONTENT_TYPE); - String extension = contentType != null && contentType.startsWith("image/") - ? contentType.substring("image/".length()) : "png"; - String name = metadata.get(TikaCoreProperties.RESOURCE_NAME_KEY); + private static String renderingName(Metadata metadata, String extension) { + String name = FilenameUtils.getName(metadata.get(TikaCoreProperties.RESOURCE_NAME_KEY)); if (name == null || name.isEmpty()) { return "rendering." + extension; } - int slash = Math.max(name.lastIndexOf('/'), name.lastIndexOf('\\')); - String base = name.substring(slash + 1); - int dot = base.lastIndexOf('.'); - return (dot > 0 ? base.substring(0, dot) : base) + "." + extension; + int dot = name.lastIndexOf('.'); + return (dot > 0 ? name.substring(0, dot) : name) + "." + extension; } } diff --git a/tika-parsers/tika-parsers-standard/tika-parsers-standard-modules/tika-parser-microsoft-module/src/main/java/org/apache/tika/parser/microsoft/OfficeParser.java b/tika-parsers/tika-parsers-standard/tika-parsers-standard-modules/tika-parser-microsoft-module/src/main/java/org/apache/tika/parser/microsoft/OfficeParser.java index 8ef289a0180..423a51f86be 100644 --- a/tika-parsers/tika-parsers-standard/tika-parsers-standard-modules/tika-parser-microsoft-module/src/main/java/org/apache/tika/parser/microsoft/OfficeParser.java +++ b/tika-parsers/tika-parsers-standard/tika-parsers-standard-modules/tika-parser-microsoft-module/src/main/java/org/apache/tika/parser/microsoft/OfficeParser.java @@ -307,13 +307,17 @@ protected void parse(DirectoryNode root, ParseContext context, Metadata metadata // is extracted, which happened above break; } - handleThumbnail(summaryExtractor.getThumbnailWmf(), xhtml, context); + OfficeParserConfig config = context.get(OfficeParserConfig.class); + if (config == null || config.isExtractThumbnail()) { + handleThumbnail(summaryExtractor.getThumbnailWmf(), xhtml, context); + } } /** * Emits the document thumbnail from the SummaryInformation, a WMF, as a * {@link TikaCoreProperties.EmbeddedResourceType#THUMBNAIL} embedded - * document, as the OOXML parsers do with the docProps thumbnail. + * document, as the OOXML parsers do with the docProps thumbnail. Switched + * off with {@code "office-parser": {"extractThumbnail": false}}. */ private void handleThumbnail(byte[] wmf, XHTMLContentHandler xhtml, ParseContext context) throws IOException, SAXException { diff --git a/tika-parsers/tika-parsers-standard/tika-parsers-standard-modules/tika-parser-microsoft-module/src/main/java/org/apache/tika/parser/microsoft/OfficeParserConfig.java b/tika-parsers/tika-parsers-standard/tika-parsers-standard-modules/tika-parser-microsoft-module/src/main/java/org/apache/tika/parser/microsoft/OfficeParserConfig.java index bb2ff73af57..5b174caaaa4 100644 --- a/tika-parsers/tika-parsers-standard/tika-parsers-standard-modules/tika-parser-microsoft-module/src/main/java/org/apache/tika/parser/microsoft/OfficeParserConfig.java +++ b/tika-parsers/tika-parsers-standard/tika-parsers-standard-modules/tika-parser-microsoft-module/src/main/java/org/apache/tika/parser/microsoft/OfficeParserConfig.java @@ -36,6 +36,8 @@ public class OfficeParserConfig implements Serializable { private boolean writeSelectHeadersInBody = false; + private boolean extractThumbnail = true; + /** * Maximum bytes per embedded object/pict when extracting from RTF within * MSG files. Since embedded data is streamed to disk (not held in memory), @@ -302,4 +304,17 @@ public int getRtfEmbeddedMaxBytesInKb() { public void setRtfEmbeddedMaxBytesInKb(int rtfEmbeddedMaxBytesInKb) { this.rtfEmbeddedMaxBytesInKb = rtfEmbeddedMaxBytesInKb; } + + /** + * Whether to emit the document thumbnail an OLE2 file stores in its + * SummaryInformation (a WMF) as a THUMBNAIL embedded document, the way + * the OOXML parsers emit the docProps thumbnail. On by default. + */ + public boolean isExtractThumbnail() { + return extractThumbnail; + } + + public void setExtractThumbnail(boolean extractThumbnail) { + this.extractThumbnail = extractThumbnail; + } } diff --git a/tika-parsers/tika-parsers-standard/tika-parsers-standard-modules/tika-parser-microsoft-module/src/main/java/org/apache/tika/parser/microsoft/WMFParser.java b/tika-parsers/tika-parsers-standard/tika-parsers-standard-modules/tika-parser-microsoft-module/src/main/java/org/apache/tika/parser/microsoft/WMFParser.java index ca51b59e2c9..9bbf578d11b 100644 --- a/tika-parsers/tika-parsers-standard/tika-parsers-standard-modules/tika-parser-microsoft-module/src/main/java/org/apache/tika/parser/microsoft/WMFParser.java +++ b/tika-parsers/tika-parsers-standard/tika-parsers-standard-modules/tika-parser-microsoft-module/src/main/java/org/apache/tika/parser/microsoft/WMFParser.java @@ -34,15 +34,12 @@ import org.apache.tika.annotation.TikaComponent; import org.apache.tika.config.ConfigDeserializer; import org.apache.tika.config.JsonConfig; -import org.apache.tika.config.ParseContextConfig; import org.apache.tika.exception.TikaException; import org.apache.tika.io.TikaInputStream; import org.apache.tika.metadata.Metadata; import org.apache.tika.metadata.TikaCoreProperties; import org.apache.tika.mime.MediaType; import org.apache.tika.parser.ParseContext; -import org.apache.tika.parser.Parser; -import org.apache.tika.parser.RenderingParser; import org.apache.tika.renderer.Renderer; import org.apache.tika.sax.XHTMLContentHandler; @@ -59,27 +56,29 @@ * the way the PDF parser emits page renderings. */ @TikaComponent -public class WMFParser implements Parser, RenderingParser { +public class WMFParser extends AbstractMetafileParser { private static final MediaType MEDIA_TYPE = MediaType.image("wmf"); private static final Set SUPPORTED_TYPES = Collections.singleton(MEDIA_TYPE); - private final MetafileParserConfig defaultConfig; - private Renderer renderer; - public WMFParser() { this(new MetafileParserConfig()); } public WMFParser(MetafileParserConfig config) { - this.defaultConfig = config; + super(config); } public WMFParser(JsonConfig jsonConfig) { this(ConfigDeserializer.buildConfig(jsonConfig, MetafileParserConfig.class)); } + @Override + String componentName() { + return "wmf-parser"; + } + @Override public Set getSupportedTypes(ParseContext context) { return SUPPORTED_TYPES; @@ -92,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); @@ -124,10 +125,9 @@ public void parse(TikaInputStream tis, ContentHandler handler, Metadata metadata xhtml.endElement("p"); } } - MetafileParserConfig config = getConfig(context); if (config.shouldRender(metadata)) { - MetafileRendering.render(renderer, config, MEDIA_TYPE, picture, xhtml, metadata, - context); + MetafileRendering.render(getRenderer(), config, MEDIA_TYPE, tis, picture, xhtml, + metadata, context); } } catch (RecordFormatException e) { //POI's hwmfparser can \ throw these for "parse // exceptions" @@ -142,18 +142,4 @@ public void parse(TikaInputStream tis, ContentHandler handler, Metadata metadata xhtml.endDocument(); } - private MetafileParserConfig getConfig(ParseContext context) - throws TikaException, IOException { - return ParseContextConfig.getConfig(context, "wmf-parser", - MetafileParserConfig.class, defaultConfig); - } - - @Override - public void setRenderer(Renderer renderer) { - this.renderer = renderer; - } - - public Renderer getRenderer() { - return renderer; - } } 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 index d578cdb80ea..c2fde311e25 100644 --- 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 @@ -38,6 +38,7 @@ 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; @@ -48,6 +49,7 @@ 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; @@ -79,6 +81,14 @@ public class POIMetafileRenderer implements Renderer { 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"; @@ -107,6 +117,7 @@ public RenderResults render(TikaInputStream tis, Metadata metadata, ParseContext 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()); @@ -114,7 +125,7 @@ public RenderResults render(TikaInputStream tis, Metadata metadata, ParseContext try { long start = System.currentTimeMillis(); BufferedImage image = picture instanceof HemfPicture - ? draw((HemfPicture) picture) : draw((HwmfPicture) picture); + ? 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); @@ -132,9 +143,9 @@ public RenderResults render(TikaInputStream tis, Metadata metadata, ParseContext return results; } - private BufferedImage draw(HemfPicture picture) throws IOException { + private BufferedImage draw(HemfPicture picture, int width) throws IOException { Dimension2D size = picture.getSize(); - BufferedImage image = canvas(size); + BufferedImage image = canvas(size, width); Graphics2D graphics = image.createGraphics(); try { picture.draw(graphics, new Rectangle2D.Double(0, 0, image.getWidth(), @@ -145,19 +156,20 @@ private BufferedImage draw(HemfPicture picture) throws IOException { return image; } - private BufferedImage draw(HwmfPicture picture) throws IOException { + private BufferedImage draw(HwmfPicture picture, int width) throws IOException { Dimension2D size; try { size = picture.getSize(); - } catch (RuntimeException e) { - //no usable window records: a bitmap wrapped in a metafile + } 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); + return scale(bitmap, width); } - BufferedImage image = canvas(size); + BufferedImage image = canvas(size, width); Graphics2D graphics = image.createGraphics(); try { picture.draw(graphics, new Rectangle2D.Double(0, 0, image.getWidth(), @@ -180,12 +192,34 @@ private static BufferedImage firstBitmap(HwmfPicture picture) { return null; } - private BufferedImage canvas(Dimension2D size) throws IOException { + /** + * 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); } - int height = (int) Math.max(1, Math.round(size.getHeight() * width / size.getWidth())); - BufferedImage image = new BufferedImage(width, height, BufferedImage.TYPE_INT_RGB); + 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, @@ -195,16 +229,31 @@ private BufferedImage canvas(Dimension2D size) throws IOException { graphics.setRenderingHint(RenderingHints.KEY_RENDERING, RenderingHints.VALUE_RENDER_QUALITY); graphics.setColor(Color.WHITE); - graphics.fillRect(0, 0, width, height); + graphics.fillRect(0, 0, image.getWidth(), image.getHeight()); } finally { graphics.dispose(); } return image; } - private BufferedImage scale(BufferedImage bitmap) { - int height = (int) Math.max(1, - Math.round((double) bitmap.getHeight() * width / bitmap.getWidth())); + /** + * 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 { 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 5ae04db79f3..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 @@ -20,18 +20,28 @@ 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 { @@ -143,6 +153,44 @@ public void testRenderOnlyThumbnails() throws Exception { 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))) { 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 index 5bdd0f02ec5..c91ff74dcc2 100644 --- 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 @@ -19,8 +19,10 @@ 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; @@ -94,6 +96,31 @@ public void testUnusableThumbnailIsSkipped() throws Exception { 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) {