MetafileRendering.java

/*
 * 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.FilenameUtils;
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 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 {

    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,
                       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)
                ? injected : defaultRenderer(config);
        Metadata renderMetadata = Metadata.newInstance(context);
        renderMetadata.set(TikaCoreProperties.TYPE, type.toString());
        EmbeddedDocumentExtractor extractor =
                EmbeddedDocumentUtil.getEmbeddedDocumentExtractor(context);
        //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) {
                return;
            }
            for (RenderResult result : results.getResults()) {
                if (result.getStatus() != RenderResult.STATUS.SUCCESS) {
                    //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();
                renderingMetadata.set(TikaCoreProperties.RESOURCE_NAME_KEY,
                        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),
                                renderingMetadata, context, false);
                    }
                }
            }
        }
    }

    /**
     * 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.getPlaceholder();
    }

    /**
     * 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)
     * @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 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, renderMetadata, context);
        } catch (SecurityException e) {
            throw e;
        } catch (Exception e) {
            EmbeddedDocumentUtil.recordException(e, parentMetadata, 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, String extension) {
        String name = FilenameUtils.getName(metadata.get(TikaCoreProperties.RESOURCE_NAME_KEY));
        if (name == null || name.isEmpty()) {
            return "rendering." + extension;
        }
        int dot = name.lastIndexOf('.');
        return (dot > 0 ? name.substring(0, dot) : name) + "." + extension;
    }
}