Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
21 commits
Select commit Hold shift + click to select a range
e966adc
TIKA-4855 - render EMF/WMF through POI and emit the OLE2 SummaryInfor…
dschmidt Aug 29, 2026
37a0322
TIKA-4855 - the embedded-file integration test sees the ppt's thumbnail
dschmidt Aug 29, 2026
1428b30
TIKA-4855 - renderOnlyEmbeddedResourceTypes restricts the rendering t…
dschmidt Aug 29, 2026
c712138
TIKA-4855 - the rendering of a THUMBNAIL is a THUMBNAIL
dschmidt Aug 29, 2026
6b14289
Merge branch 'main' into metafile-rendering
dschmidt Aug 29, 2026
201ba73
Merge branch 'main' into metafile-rendering
dschmidt Aug 30, 2026
6f6ee6f
Merge branch 'main' into metafile-rendering
THausherr Aug 30, 2026
f4c68e0
TIKA-4855 - address review: merged javadocs, rendering failures recor…
dschmidt Aug 31, 2026
48ecb99
Merge remote-tracking branch 'fork/metafile-rendering' into metafile-…
dschmidt Aug 31, 2026
1707921
TIKA-4855 - keep the render metadata for the composite renderer's rou…
dschmidt Aug 31, 2026
f041611
Merge branch 'main' into metafile-rendering
THausherr Aug 31, 2026
db2bc97
TIKA-4855 - name() for the resource type; carry the renderer's diagno…
dschmidt Aug 31, 2026
a6497e9
Merge remote-tracking branch 'fork/metafile-rendering' into metafile-…
dschmidt Aug 31, 2026
6ebaf2e
Merge branch 'main' into metafile-rendering
dschmidt Aug 31, 2026
c668e0a
Merge branch 'main' into metafile-rendering
dschmidt Aug 31, 2026
527ddca
Merge branch 'main' into metafile-rendering
dschmidt Aug 31, 2026
85e1492
Merge branch 'main' into metafile-rendering
dschmidt Aug 31, 2026
3e14628
Merge branch 'main' into metafile-rendering
dschmidt Sep 1, 2026
4d77a40
Merge branch 'main' into metafile-rendering
tballison Sep 1, 2026
bed5c22
TIKA-4855 - address review: renderWidth reaches the injected renderer…
dschmidt Sep 1, 2026
97a4d2e
Merge branch 'main' into metafile-rendering
dschmidt Sep 1, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
13 changes: 13 additions & 0 deletions CHANGES.txt
Original file line number Diff line number Diff line change
@@ -1,5 +1,18 @@
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 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,
switchable with "office-parser": {"extractThumbnail": false}
(TIKA-4855).

* Add "exception-reporting" parse-context config to redact and bound
exception text in metadata, tika-server error bodies and pipes/grpc
messages; FileSystemEmitter writes atomically (TIKA-4848).
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -160,17 +160,18 @@ 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));
assertEquals("2", handler.filenames.get(1));
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
Expand All @@ -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));
Expand All @@ -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
Expand All @@ -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)
Expand Down
Original file line number Diff line number Diff line change
@@ -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;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -35,16 +35,19 @@
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.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.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.renderer.Renderer;
import org.apache.tika.sax.EmbeddedContentHandler;
import org.apache.tika.sax.XHTMLContentHandler;

Expand All @@ -61,13 +64,40 @@
* 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...
* <p/>
* 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
* {@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 extends AbstractMetafileParser {

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

public EMFParser() {
this(new MetafileParserConfig());
}

public EMFParser(MetafileParserConfig 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");
Expand Down Expand Up @@ -102,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
Expand Down Expand Up @@ -139,6 +171,10 @@ public void parse(TikaInputStream tis, ContentHandler handler, Metadata metadata
xhtml.characters(buffer.toString());
xhtml.endElement("p");
}
if (config.shouldRender(metadata)) {
MetafileRendering.render(getRenderer(), config, MEDIA_TYPE, tis, ex, xhtml,
metadata, context);
}

} catch (RecordFormatException e) { //POI's hemfparser can throw these for "parse
// exceptions"
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,104 @@
/*
* 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 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;

/**
* 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;
private Set<String> renderOnlyEmbeddedResourceTypes = new HashSet<>();

/**
* 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;
}

/**
* 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<String> getRenderOnlyEmbeddedResourceTypes() {
return renderOnlyEmbeddedResourceTypes;
}

public void setRenderOnlyEmbeddedResourceTypes(Set<String> renderOnlyEmbeddedResourceTypes) {
if (renderOnlyEmbeddedResourceTypes == null) {
this.renderOnlyEmbeddedResourceTypes = new HashSet<>();
return;
}
Set<String> types = new HashSet<>();
for (String type : renderOnlyEmbeddedResourceTypes) {
//a typo would silently disable rendering
types.add(TikaCoreProperties.EmbeddedResourceType.valueOf(type).name());
}
this.renderOnlyEmbeddedResourceTypes = types;
}

/**
* 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(
"renderWidth must be between 1 and 10000, got: " + renderWidth);
}
this.renderWidth = renderWidth;
}
}
Loading
Loading