Skip to content

TIKA-4855: Render EMF/WMF through POI and emit the OLE2 SummaryInformation thumbnail - #3095

Open
dschmidt wants to merge 21 commits into
apache:mainfrom
dschmidt:metafile-rendering
Open

TIKA-4855: Render EMF/WMF through POI and emit the OLE2 SummaryInformation thumbnail#3095
dschmidt wants to merge 21 commits into
apache:mainfrom
dschmidt:metafile-rendering

Conversation

@dschmidt

Copy link
Copy Markdown
Contributor

Office documents carry their preview image as a vector metafile: the OOXML docProps/thumbnail.emf of Word (an EMF wrapping a WMF) and thumbnail.wmf of Excel, and the SummaryInformation thumbnail (PIDSI_THUMBNAIL, CF_METAFILEPICT, a WMF) of the OLE2 formats. Neither is usable as a preview outside Windows, and outside the JVM there is no maintained EMF/WMF rasterizer, while POI's HEMF/HWMF can draw both.

Following the PDF parser's rendering design:

  • POIMetafileRenderer (poi-metafile-renderer) is a Renderer for image/emf and image/wmf: the POI picture drawn onto a white canvas, a PNG of a configurable width. The WMF thumbnails Word writes consist of a setWindowExt and a dibStretchBlt record only, for which POI cannot compute bounds; those are rendered from the record's bitmap.
  • EMFParser and WMFParser implement RenderingParser and, with "emf-parser" / "wmf-parser": {"renderImage": true, "renderWidth": 800} (off by default), emit the rendering as a RENDERING embedded document named after the image. An injected renderer is used when it supports the type, the POI one otherwise. The shared config and emission live in MetafileParserConfig and MetafileRendering.
  • OfficeParser emits the SummaryInformation thumbnail as a THUMBNAIL embedded document (image/wmf, thumbnail.wmf), consistent with the docProps thumbnail of the OOXML parsers. Two PowerPoint tests count one embedded document more because of it.

Verified against tika-server with /unpack: docx, xlsx, doc, xls and ppt files that carry a thumbnail yield it at depth 1 and its PNG rendering at depth 2.

https://issues.apache.org/jira/browse/TIKA-4855

…mation 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.
@dschmidt
dschmidt force-pushed the metafile-rendering branch from 5cafd74 to e966adc Compare August 29, 2026 12:22
…o 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.

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

This PR adds cross-platform rasterization and extraction of Office document preview thumbnails by leveraging Apache POI’s EMF/WMF drawing support, and wires this into Tika’s embedded-document model (THUMBNAIL + optional PNG RENDERING) for both OOXML docProps thumbnails and OLE2 SummaryInformation thumbnails.

Changes:

  • Introduces POIMetafileRenderer to rasterize image/emf and image/wmf into PNG at a configurable width.
  • Extends EMFParser/WMFParser to optionally emit a rendered PNG as an embedded document via a shared MetafileParserConfig + MetafileRendering helper.
  • Updates OfficeParser/SummaryExtractor and tests to emit OLE2 SummaryInformation thumbnails as THUMBNAIL embedded documents, and adjusts integration test expectations.

Reviewed changes

Copilot reviewed 14 out of 14 changed files in this pull request and generated 5 comments.

Show a summary per file
File Description
tika-parsers/.../src/test/resources/configs/tika-config-emf-render.json Adds JSON config used by tests to enable EMF rendering.
tika-parsers/.../WMFParserTest.java Adds unit tests for WMF rendering and thumbnail rendering behavior.
tika-parsers/.../PowerPointParserTest.java Updates expected embedded document counts to include OLE2 thumbnails.
tika-parsers/.../OLE2ThumbnailTest.java New tests verifying OLE2 SummaryInformation thumbnail emission and rendering.
tika-parsers/.../EMFParserTest.java Adds tests for EMF rendering defaults, config-based rendering, and thumbnail rendering constraints.
tika-parsers/.../POIMetafileRenderer.java New renderer implementation using POI HEMF/HWMF to produce PNG renderings.
tika-parsers/.../WMFParser.java Adds rendering support via RenderingParser and parse-context JSON config.
tika-parsers/.../SummaryExtractor.java Extracts WMF thumbnails from OLE2 SummaryInformation via POI HPSF Thumbnail.
tika-parsers/.../OfficeParser.java Emits SummaryInformation thumbnail as a THUMBNAIL embedded document (thumbnail.wmf).
tika-parsers/.../MetafileRendering.java New helper to render metafiles and emit the results as embedded documents.
tika-parsers/.../MetafileParserConfig.java New config bean controlling whether/how metafiles are rendered and filtered by embedded type.
tika-parsers-standard-integration-tests/.../POIContainerExtractionTest.java Adjusts integration test expectations for added thumbnail embedded docs.
CHANGES.txt Adds release note entry for the new thumbnail rendering/extraction behavior.
Suppressed comments (1)

tika-parsers/tika-parsers-standard/tika-parsers-standard-modules/tika-parser-microsoft-module/src/main/java/org/apache/tika/renderer/microsoft/POIMetafileRenderer.java:258

  • setImageFormatName accepts null/blank values, which will later produce invalid content types ("image/null") and likely failures in ImageIO.write. This should validate and normalize the format name.
    public void setImageFormatName(String imageFormatName) {
        this.imageFormatName = imageFormatName;
    }

💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.

Comment on lines 49 to 52
/**
* This parser offers a very rough capability to extract text if there
* is text stored in the WMF files.
*/

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Merged into one.

Comment on lines +70 to +72
if (results == null) {
return;
}

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It is recorded on the metafile's metadata now.

Comment on lines +123 to +128
} catch (SecurityException e) {
throw e;
} catch (Exception e) {
results.add(new RenderResult(RenderResult.STATUS.EXCEPTION, id, null,
renderingMetadata));
}

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Recorded, like PDFBoxRenderer does.

Comment thread CHANGES.txt Outdated
Comment on lines +10 to +12
"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).

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Wrapped.

Comment on lines +68 to +72
* 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

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Merged into one.

@THausherr
THausherr requested a lite review from Copilot August 31, 2026 09:32

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 14 out of 14 changed files in this pull request and generated 3 comments.

Metadata embeddedMetadata = Metadata.newInstance(context);
embeddedMetadata.set(TikaCoreProperties.RESOURCE_NAME_KEY, "thumbnail.wmf");
embeddedMetadata.set(TikaCoreProperties.EMBEDDED_RESOURCE_TYPE,
TikaCoreProperties.EmbeddedResourceType.THUMBNAIL.toString());

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Changed to name().

Comment on lines +73 to +78
for (RenderResult result : results.getResults()) {
if (result.getStatus() != RenderResult.STATUS.SUCCESS) {
EmbeddedDocumentUtil.recordException(
new TikaException("metafile rendering failed"), metadata, context);
continue;
}

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The renderer's recorded warnings are copied to the metafile's metadata now; the generic exception only remains as a fallback when the renderer recorded nothing.

Comment on lines +222 to +234
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;
}

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It is tied in: RenderResult registers the path with its TemporaryResources (deleted on close), RenderResults.add chains the result into its own TemporaryResources, and MetafileRendering closes the RenderResults with try-with-resources. Same lifecycle as PDFBoxRenderer, which also uses Files.createTempFile.

@tballison

Copy link
Copy Markdown
Contributor

From my 🤖 ... I think most are useful. One or two are puntable. Let me know what you think. Thank you for iterating.

  ⎿  tika-parsers/tika-parsers-standard/tika-parsers-standard-modules/tika-parser-microsoft-module/src/main/java/org/apache/tika/renderer/microsoft/POIMetafileRenderer.java
       ● 187 [security]          Rendered height is unbounded (only width is capped at 10000): a hostile metafile's aspect ratio drives an arbitrarily large BufferedImage allocation, and the resulting OutOfMemoryError is an Error that escapes every
                                 catch(Exception) in the render path — in both canvas() (line 187) and the scale() bitmap fallback (line 207).
       ● 152 [correctness]       draw(HwmfPicture) catches bare RuntimeException around picture.getSize() when the intended case is exactly POI's IllegalStateException("invalid wmf file - window records are incomplete.") — unrelated runtime failures
                                 (e.g. an NPE from a POI bug) get silently rerouted to the first-bitmap fallback.
     tika-parsers/tika-parsers-standard/tika-parsers-standard-modules/tika-parser-microsoft-module/src/main/java/org/apache/tika/parser/microsoft/MetafileRendering.java
       ●  61 [correctness]       renderWidth (and imageFormatName) is silently dead in every default configuration: @TikaComponent on POIMetafileRenderer defaults to spi=true, so DefaultParser's always-injected SPI CompositeRenderer claims image/emf|wmf
                                 and takes precedence over defaultRenderer(config) — the PR's own tests request widths 200/300/400, render at 800, and pass because no test asserts output width.
       ●  67 [correctness]       Injected renderers are handed TikaInputStream.get(new byte[0]) with the parsed picture only as an open container — a POIMetafileRenderer-private convention — so any third-party Renderer that follows the Renderer contract
                                 and reads the stream renders 0 bytes on every file.
       ●  90 [correctness]       Rewriting the renderer-assigned RENDERING type to THUMBNAIL hides thumbnail renderings from type-based filters and yields two THUMBNAIL-typed embedded docs for one file.
       ● 145 [simplification]    renderingName() re-implements basename extraction that tika-core's FilenameUtils.getName() already provides, missing its ':' handling and '.'/'..' sanitization.
       ●  68 [efficiency]        The full rasterization (vector draw + PNG encode + temp-file write) runs before extractor.shouldParseEmbedded is consulted, so an extractor that filters out RENDERING docs still pays the entire render cost per image.
     tika-parsers/tika-parsers-standard/tika-parsers-standard-modules/tika-parser-microsoft-module/src/main/java/org/apache/tika/parser/microsoft/OfficeParser.java
       ● 310 [compatibility]     OLE2 SummaryInformation thumbnail emission is default-on with no opt-out: handleThumbnail runs unconditionally (MetafileParserConfig.renderImage gates only rendering; OfficeParserConfig has no thumbnail switch), adding an
                                 extra thumbnail.wmf embedded document to every OLE2 file with a stored thumbnail.
     tika-parsers/tika-parsers-standard/tika-parsers-standard-modules/tika-parser-microsoft-module/src/main/java/org/apache/tika/parser/microsoft/MetafileParserConfig.java
       ●  86 [config-validation] renderOnlyEmbeddedResourceTypes is an unvalidated case-sensitive Set<String> matched against EmbeddedResourceType.name(): a config typo silently disables rendering with no error.
     tika-parsers/tika-parsers-standard/tika-parsers-standard-modules/tika-parser-microsoft-module/src/main/java/org/apache/tika/parser/microsoft/WMFParser.java
       ●  71 [simplification]    EMFParser and WMFParser duplicate ~35 lines of identical wiring (fields, three constructors, getConfig differing only in the "emf-parser"/"wmf-parser" key, setRenderer plus a caller-less getRenderer), and
                                 POIMetafileRenderer's two draw() overloads and canvas()/scale() duplicate the canvas/height-computation logic.

…, bounded rendering height, renderer reads the metafile itself, gate before rendering, validated resource types, thumbnail switch, shared parser base
@dschmidt

dschmidt commented Sep 1, 2026

Copy link
Copy Markdown
Contributor Author

Thanks, that list was worth it. All nine are in, one I would like to keep and explain.

renderWidth was dead, confirmed before fixing: requesting 200 or 400 rendered at 800, because the SPI-injected renderer wins over the config-built one. The renderer now reads the metafile parser config from the ParseContext, the way PDFBoxRenderer reads dpi and imageType from PDFParserConfig, and a parameterized test asserts the width in the PNG header. That gap is why no test caught it.

Unbounded height: capped at 10000 like the width, with an IOException instead of an OutOfMemoryError; both paths share one height helper now.

Bare RuntimeException in draw(HwmfPicture): narrowed to IllegalStateException, which is what POI throws for Word's bitmap-in-WMF thumbnails ("invalid wmf file - window records are incomplete."), verified against testControlCharacters.doc.

Injected renderers got an empty stream: the parser spools when it is going to render, so the renderer is handed the metafile itself; the parsed picture stays attached as the open container for the fast path.

Rendering before shouldParseEmbedded: gated now, with provisional metadata (name, image/png, resource type) before the raster work.

renderingName: FilenameUtils.getName.

renderOnlyEmbeddedResourceTypes: validated against EmbeddedResourceType, a typo throws instead of silently disabling rendering.

OLE2 thumbnail: OfficeParserConfig.extractThumbnail, default true to match the OOXML parsers, which emit docProps/thumbnail unconditionally.

Duplicate wiring: AbstractMetafileParser holds the config and renderer plumbing for both parsers.

The one I kept: the rendering of a THUMBNAIL is typed THUMBNAIL rather than RENDERING. It is deliberate and the reason this PR chain exists: a client should find the preview picture of any file the same way, and for an Office document the stored thumbnail is an EMF/WMF whose only displayable form is that rendering. The two THUMBNAIL entries are the vector original and its raster rendering, so the client rule is "the first raster THUMBNAIL". If you would rather keep the type strictly about provenance, I will change it back and pair the rendering with its parent by embedded path instead.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants