Skip to content

Commit 1e3d8f8

Browse files
dschmidtTHausherrtballison
authored
TIKA-4855: Render EMF/WMF through POI and emit the OLE2 SummaryInformation thumbnail (#3095)
* 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. * TIKA-4855 - the embedded-file integration test sees the ppt's thumbnail * 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. * TIKA-4855 - the rendering of a THUMBNAIL is a THUMBNAIL * TIKA-4855 - address review: merged javadocs, rendering failures recorded on the metafile's metadata and in the renderer's result, CHANGES wrapped * TIKA-4855 - keep the render metadata for the composite renderer's routing; record failures on the metafile's metadata * TIKA-4855 - name() for the resource type; carry the renderer's diagnostics to the metafile on failure * 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 --------- Co-authored-by: Tilman Hausherr <tilman@snafu.de> Co-authored-by: Tim Allison <tallison@apache.org>
1 parent ea3cc54 commit 1e3d8f8

16 files changed

Lines changed: 1224 additions & 12 deletions

File tree

CHANGES.txt

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,18 @@
11
Release 4.1.0 - unreleased
22

3+
* Raster previews for the vector thumbnails of Office documents: the new
4+
poi-metafile-renderer draws EMF and WMF images through POI (a PNG of
5+
a configurable width; Word's bitmap-in-WMF thumbnails from the bitmap
6+
directly), EMFParser and WMFParser are RenderingParsers that emit the
7+
rendering as a RENDERING embedded document with "emf-parser" /
8+
"wmf-parser": {"renderImage": true, "renderWidth": 800}, off by
9+
default and restrictable to e.g. THUMBNAIL embedded documents with
10+
"renderOnlyEmbeddedResourceTypes", and OfficeParser emits the
11+
SummaryInformation thumbnail of the OLE2 formats (a WMF) as a THUMBNAIL
12+
embedded document, as the OOXML parsers do with the docProps thumbnail,
13+
switchable with "office-parser": {"extractThumbnail": false}
14+
(TIKA-4855).
15+
316
* Add "exception-reporting" parse-context config to redact and bound
417
exception text in metadata, tika-server error bodies and pipes/grpc
518
messages; FileSystemEmitter writes atomically (TIKA-4848).

tika-parsers/tika-parsers-standard/tika-parsers-standard-integration-tests/src/test/java/org/apache/tika/parser/microsoft/POIContainerExtractionTest.java

Lines changed: 9 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -160,17 +160,18 @@ public void testEmbeddedOfficeFiles() throws Exception {
160160
assertEquals(TYPE_PNG, handler.mediaTypes.get(15)); // PNG inside .xls
161161

162162

163-
// PowerPoint with excel and word
163+
// PowerPoint with excel and word; its SummaryInformation thumbnail comes last
164164
handler = process("testPPT_embeded.ppt", extractor, false);
165-
assertEquals(7, handler.filenames.size());
166-
assertEquals(7, handler.mediaTypes.size());
165+
assertEquals(8, handler.filenames.size());
166+
assertEquals(8, handler.mediaTypes.size());
167167

168168
// Embedded objects get OLE IDs, slide images now get generated names
169169
assertEquals("1", handler.filenames.get(0));
170170
assertEquals("2", handler.filenames.get(1));
171171
for (int i = 2; i < 7; i++) {
172172
assertNotNull(handler.filenames.get(i));
173173
}
174+
assertEquals("thumbnail.wmf", handler.filenames.get(7));
174175
// But we do know their types
175176
assertEquals(TYPE_XLS, handler.mediaTypes.get(0)); // Embedded office doc
176177
assertEquals(TYPE_DOC, handler.mediaTypes.get(1)); // Embedded office doc
@@ -179,11 +180,12 @@ public void testEmbeddedOfficeFiles() throws Exception {
179180
assertEquals(TYPE_PNG, handler.mediaTypes.get(4)); // Embedded image
180181
assertEquals(TYPE_PNG, handler.mediaTypes.get(5)); // Embedded image
181182
assertEquals(TYPE_PNG, handler.mediaTypes.get(6)); // Embedded image
183+
assertEquals(TYPE_WMF, handler.mediaTypes.get(7)); // Document thumbnail
182184

183185
// Run again on PowerPoint but with recursion
184186
handler = process("testPPT_embeded.ppt", extractor, true);
185-
assertEquals(11, handler.filenames.size());
186-
assertEquals(11, handler.mediaTypes.size());
187+
assertEquals(12, handler.filenames.size());
188+
assertEquals(12, handler.mediaTypes.size());
187189

188190
assertEquals("1", handler.filenames.get(0));
189191
assertEquals(null, handler.filenames.get(1));
@@ -195,6 +197,7 @@ public void testEmbeddedOfficeFiles() throws Exception {
195197
for (int i = 6; i < 11; i++) {
196198
assertNotNull(handler.filenames.get(i));
197199
}
200+
assertEquals("thumbnail.wmf", handler.filenames.get(11));
198201

199202
assertEquals(TYPE_XLS, handler.mediaTypes.get(0)); // Embedded office doc
200203
assertEquals(TYPE_PNG, handler.mediaTypes.get(1)); // PNG inside .xls
@@ -207,6 +210,7 @@ public void testEmbeddedOfficeFiles() throws Exception {
207210
assertEquals(TYPE_PNG, handler.mediaTypes.get(8)); // Embedded image
208211
assertEquals(TYPE_PNG, handler.mediaTypes.get(9)); // Embedded image
209212
assertEquals(TYPE_PNG, handler.mediaTypes.get(10)); // Embedded image
213+
assertEquals(TYPE_WMF, handler.mediaTypes.get(11)); // Document thumbnail
210214

211215

212216
// Word, with a non-office file (PDF)
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,74 @@
1+
/*
2+
* Licensed to the Apache Software Foundation (ASF) under one or more
3+
* contributor license agreements. See the NOTICE file distributed with
4+
* this work for additional information regarding copyright ownership.
5+
* The ASF licenses this file to You under the Apache License, Version 2.0
6+
* (the "License"); you may not use this file except in compliance with
7+
* the License. You may obtain a copy of the License at
8+
*
9+
* http://www.apache.org/licenses/LICENSE-2.0
10+
*
11+
* Unless required by applicable law or agreed to in writing, software
12+
* distributed under the License is distributed on an "AS IS" BASIS,
13+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14+
* See the License for the specific language governing permissions and
15+
* limitations under the License.
16+
*/
17+
package org.apache.tika.parser.microsoft;
18+
19+
import java.io.IOException;
20+
21+
import org.apache.tika.config.ParseContextConfig;
22+
import org.apache.tika.exception.TikaException;
23+
import org.apache.tika.io.TikaInputStream;
24+
import org.apache.tika.metadata.Metadata;
25+
import org.apache.tika.parser.ParseContext;
26+
import org.apache.tika.parser.Parser;
27+
import org.apache.tika.parser.RenderingParser;
28+
import org.apache.tika.renderer.Renderer;
29+
30+
/**
31+
* What {@link EMFParser} and {@link WMFParser} share: the configuration of
32+
* the metafile parsers and the renderer they may be handed.
33+
*/
34+
abstract class AbstractMetafileParser implements Parser, RenderingParser {
35+
36+
private final MetafileParserConfig defaultConfig;
37+
private Renderer renderer;
38+
39+
AbstractMetafileParser(MetafileParserConfig defaultConfig) {
40+
this.defaultConfig = defaultConfig;
41+
}
42+
43+
/**
44+
* The component name this parser reads its configuration from,
45+
* {@code emf-parser} or {@code wmf-parser}.
46+
*/
47+
abstract String componentName();
48+
49+
MetafileParserConfig getConfig(ParseContext context) throws TikaException, IOException {
50+
return ParseContextConfig.getConfig(context, componentName(), MetafileParserConfig.class,
51+
defaultConfig);
52+
}
53+
54+
/**
55+
* Spools the stream when the image is going to be rendered, so the
56+
* renderer can read the metafile itself rather than only the parsed
57+
* picture.
58+
*/
59+
static void prepareForRendering(TikaInputStream tis, MetafileParserConfig config,
60+
Metadata metadata) throws IOException {
61+
if (config.shouldRender(metadata)) {
62+
tis.getFile();
63+
}
64+
}
65+
66+
Renderer getRenderer() {
67+
return renderer;
68+
}
69+
70+
@Override
71+
public void setRenderer(Renderer renderer) {
72+
this.renderer = renderer;
73+
}
74+
}

tika-parsers/tika-parsers-standard/tika-parsers-standard-modules/tika-parser-microsoft-module/src/main/java/org/apache/tika/parser/microsoft/EMFParser.java

Lines changed: 38 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -35,16 +35,19 @@
3535
import org.xml.sax.SAXException;
3636

3737
import org.apache.tika.annotation.TikaComponent;
38+
import org.apache.tika.config.ConfigDeserializer;
39+
import org.apache.tika.config.JsonConfig;
3840
import org.apache.tika.exception.TikaException;
3941
import org.apache.tika.extractor.EmbeddedDocumentExtractor;
4042
import org.apache.tika.extractor.EmbeddedDocumentUtil;
4143
import org.apache.tika.io.TikaInputStream;
4244
import org.apache.tika.metadata.HttpHeaders;
4345
import org.apache.tika.metadata.Metadata;
4446
import org.apache.tika.metadata.Property;
47+
import org.apache.tika.metadata.TikaCoreProperties;
4548
import org.apache.tika.mime.MediaType;
4649
import org.apache.tika.parser.ParseContext;
47-
import org.apache.tika.parser.Parser;
50+
import org.apache.tika.renderer.Renderer;
4851
import org.apache.tika.sax.EmbeddedContentHandler;
4952
import org.apache.tika.sax.XHTMLContentHandler;
5053

@@ -61,13 +64,40 @@
6164
* We're also relying on storage order for text order, which isn't great.
6265
* We'd have to do something like what PDFBox or XPS do to sort the
6366
* runs and then put the cow back together from the hamburger...lol...
67+
* <p/>
68+
* With {@link MetafileParserConfig#setRenderImage(boolean)}
69+
* ("emf-parser": {"renderImage": true}) the image is rendered through the
70+
* configured {@link Renderer}, the
71+
* {@link org.apache.tika.renderer.microsoft.POIMetafileRenderer} by
72+
* default, and emitted as a
73+
* {@link TikaCoreProperties.EmbeddedResourceType#RENDERING} embedded document,
74+
* the way the PDF parser emits page renderings, so a client can obtain a
75+
* raster preview of a vector thumbnail such as the docProps thumbnail of a
76+
* Word document.
6477
*/
6578
@TikaComponent
66-
public class EMFParser implements Parser {
79+
public class EMFParser extends AbstractMetafileParser {
6780

6881
public static Property EMF_ICON_ONLY = Property.internalBoolean("emf:icon-only");
6982
public static Property EMF_ICON_STRING = Property.internalText("emf:icon-string");
7083

84+
public EMFParser() {
85+
this(new MetafileParserConfig());
86+
}
87+
88+
public EMFParser(MetafileParserConfig config) {
89+
super(config);
90+
}
91+
92+
public EMFParser(JsonConfig jsonConfig) {
93+
this(ConfigDeserializer.buildConfig(jsonConfig, MetafileParserConfig.class));
94+
}
95+
96+
@Override
97+
String componentName() {
98+
return "emf-parser";
99+
}
100+
71101
private static String ICON_ONLY = "IconOnly";
72102

73103
private static final MediaType MEDIA_TYPE = MediaType.image("emf");
@@ -102,6 +132,8 @@ public void parse(TikaInputStream tis, ContentHandler handler, Metadata metadata
102132
XHTMLContentHandler xhtml = new XHTMLContentHandler(handler, metadata, context);
103133
xhtml.startDocument();
104134
try {
135+
MetafileParserConfig config = getConfig(context);
136+
prepareForRendering(tis, config, metadata);
105137
HemfPicture ex = new HemfPicture(tis);
106138
ParseState parseState = new ParseState();
107139
long fudgeFactorX = 10;//derive this from the font or frame/bounds information
@@ -139,6 +171,10 @@ public void parse(TikaInputStream tis, ContentHandler handler, Metadata metadata
139171
xhtml.characters(buffer.toString());
140172
xhtml.endElement("p");
141173
}
174+
if (config.shouldRender(metadata)) {
175+
MetafileRendering.render(getRenderer(), config, MEDIA_TYPE, tis, ex, xhtml,
176+
metadata, context);
177+
}
142178

143179
} catch (RecordFormatException e) { //POI's hemfparser can throw these for "parse
144180
// exceptions"
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,104 @@
1+
/*
2+
* Licensed to the Apache Software Foundation (ASF) under one or more
3+
* contributor license agreements. See the NOTICE file distributed with
4+
* this work for additional information regarding copyright ownership.
5+
* The ASF licenses this file to You under the Apache License, Version 2.0
6+
* (the "License"); you may not use this file except in compliance with
7+
* the License. You may obtain a copy of the License at
8+
*
9+
* http://www.apache.org/licenses/LICENSE-2.0
10+
*
11+
* Unless required by applicable law or agreed to in writing, software
12+
* distributed under the License is distributed on an "AS IS" BASIS,
13+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14+
* See the License for the specific language governing permissions and
15+
* limitations under the License.
16+
*/
17+
package org.apache.tika.parser.microsoft;
18+
19+
import java.io.Serializable;
20+
import java.util.HashSet;
21+
import java.util.Set;
22+
23+
import org.apache.tika.metadata.Metadata;
24+
import org.apache.tika.metadata.TikaCoreProperties;
25+
import org.apache.tika.renderer.microsoft.POIMetafileRenderer;
26+
27+
/**
28+
* Configuration of the {@link EMFParser} ("emf-parser") and the
29+
* {@link WMFParser} ("wmf-parser").
30+
*/
31+
public class MetafileParserConfig implements Serializable {
32+
33+
private static final long serialVersionUID = -6371049153052164071L;
34+
35+
private boolean renderImage = false;
36+
private int renderWidth = 800;
37+
private Set<String> renderOnlyEmbeddedResourceTypes = new HashSet<>();
38+
39+
/**
40+
* Whether to render the image and emit the rendering as a RENDERING
41+
* embedded document. Off by default.
42+
*/
43+
public boolean isRenderImage() {
44+
return renderImage;
45+
}
46+
47+
public void setRenderImage(boolean renderImage) {
48+
this.renderImage = renderImage;
49+
}
50+
51+
/**
52+
* Width of the rendering in pixels when the default
53+
* {@link POIMetafileRenderer} is used; the height follows the image's
54+
* aspect ratio. Default 800.
55+
*/
56+
public int getRenderWidth() {
57+
return renderWidth;
58+
}
59+
60+
/**
61+
* Restricts the rendering to images that are embedded documents of one
62+
* of these {@code tk:embedded-resource-type}s, e.g. {@code ["THUMBNAIL"]}
63+
* to render the thumbnail of an Office document but not the pictures of
64+
* its embedded objects. Empty (the default) renders every image.
65+
*/
66+
public Set<String> getRenderOnlyEmbeddedResourceTypes() {
67+
return renderOnlyEmbeddedResourceTypes;
68+
}
69+
70+
public void setRenderOnlyEmbeddedResourceTypes(Set<String> renderOnlyEmbeddedResourceTypes) {
71+
if (renderOnlyEmbeddedResourceTypes == null) {
72+
this.renderOnlyEmbeddedResourceTypes = new HashSet<>();
73+
return;
74+
}
75+
Set<String> types = new HashSet<>();
76+
for (String type : renderOnlyEmbeddedResourceTypes) {
77+
//a typo would silently disable rendering
78+
types.add(TikaCoreProperties.EmbeddedResourceType.valueOf(type).name());
79+
}
80+
this.renderOnlyEmbeddedResourceTypes = types;
81+
}
82+
83+
/**
84+
* Whether an image with this metadata is to be rendered.
85+
*/
86+
public boolean shouldRender(Metadata metadata) {
87+
if (!renderImage) {
88+
return false;
89+
}
90+
if (renderOnlyEmbeddedResourceTypes.isEmpty()) {
91+
return true;
92+
}
93+
String type = metadata.get(TikaCoreProperties.EMBEDDED_RESOURCE_TYPE);
94+
return type != null && renderOnlyEmbeddedResourceTypes.contains(type);
95+
}
96+
97+
public void setRenderWidth(int renderWidth) {
98+
if (renderWidth < 1 || renderWidth > 10000) {
99+
throw new IllegalArgumentException(
100+
"renderWidth must be between 1 and 10000, got: " + renderWidth);
101+
}
102+
this.renderWidth = renderWidth;
103+
}
104+
}

0 commit comments

Comments
 (0)