RawTiffParserTest.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.image;

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.Arrays;
import java.util.List;

import org.junit.jupiter.api.Test;
import org.xml.sax.helpers.DefaultHandler;

import org.apache.tika.TikaTest;
import org.apache.tika.config.loader.TikaLoader;
import org.apache.tika.io.TikaInputStream;
import org.apache.tika.metadata.HttpHeaders;
import org.apache.tika.metadata.Metadata;
import org.apache.tika.metadata.TIFF;
import org.apache.tika.metadata.TikaCoreProperties;
import org.apache.tika.parser.ParseContext;
import org.apache.tika.parser.Parser;

public class RawTiffParserTest extends TikaTest {

    private List<Metadata> parseByName(String fileName) throws Exception {
        Metadata metadata = new Metadata();
        metadata.set(TikaCoreProperties.RESOURCE_NAME_KEY, fileName);
        return getRecursiveMetadata(fileName, metadata);
    }

    /**
     * The largest extracted preview: the file's thumbnail, which the parser
     * emits before the smaller ones.
     */
    private void assertThumbnail(Metadata preview, int width, int height) {
        assertPreview(preview, TikaCoreProperties.EmbeddedResourceType.THUMBNAIL,
                "thumbnail-0.jpg", width, height);
    }

    /**
     * A smaller preview, emitted after the thumbnail as an inline image.
     */
    private void assertInlinePreview(Metadata preview, int index, int width, int height) {
        assertPreview(preview, TikaCoreProperties.EmbeddedResourceType.INLINE,
                "image-" + index + ".jpg", width, height);
    }

    private void assertPreview(Metadata preview, TikaCoreProperties.EmbeddedResourceType type,
                               String name, int width, int height) {
        assertEquals("image/jpeg", preview.get(HttpHeaders.CONTENT_TYPE));
        assertEquals(name, preview.get(TikaCoreProperties.RESOURCE_NAME_KEY));
        assertEquals(type.toString(), preview.get(TikaCoreProperties.EMBEDDED_RESOURCE_TYPE));
        assertEquals(Integer.toString(width), preview.get(TIFF.IMAGE_WIDTH));
        assertEquals(Integer.toString(height), preview.get(TIFF.IMAGE_LENGTH));
        //the IFD knows how long the preview is, so the metadata says so
        assertTrue(Long.parseLong(preview.get(HttpHeaders.CONTENT_LENGTH)) > 0,
                preview.get(HttpHeaders.CONTENT_LENGTH));
    }

    @Test
    public void testNEF() throws Exception {
        List<Metadata> metadataList = parseByName("testNEF.nef");
        assertEquals(2, metadataList.size());

        Metadata container = metadataList.get(0);
        assertEquals("image/x-raw-nikon", container.get(HttpHeaders.CONTENT_TYPE));
        assertEquals("NIKON CORPORATION", container.get(TIFF.EQUIPMENT_MAKE));
        assertEquals("NIKON D3000", container.get(TIFF.EQUIPMENT_MODEL));

        assertThumbnail(metadataList.get(1), 64, 48);
    }

    @Test
    public void testDuplicatePreviewOffsetsDeduplicated() throws Exception {
        //IFD0 and IFD1 both point their JPEGInterchangeFormat at the same region;
        //it is extracted once, not per referencing IFD
        List<Metadata> metadataList = parseByName("testNEF_dup.nef");
        assertEquals(2, metadataList.size());
        assertThumbnail(metadataList.get(1), 64, 48);
    }

    @Test
    public void testARW() throws Exception {
        List<Metadata> metadataList = parseByName("testARW.arw");
        assertEquals(3, metadataList.size());

        Metadata container = metadataList.get(0);
        assertEquals("image/x-raw-sony", container.get(HttpHeaders.CONTENT_TYPE));
        assertEquals("SONY", container.get(TIFF.EQUIPMENT_MAKE));
        assertEquals("NEX-6", container.get(TIFF.EQUIPMENT_MODEL));

        //full-size preview from IFD0, then the camera thumbnail from IFD1
        assertThumbnail(metadataList.get(1), 64, 48);
        assertInlinePreview(metadataList.get(2), 0, 32, 24);
    }

    @Test
    public void testPEF() throws Exception {
        List<Metadata> metadataList = parseByName("testPEF.pef");
        assertEquals(3, metadataList.size());

        Metadata container = metadataList.get(0);
        assertEquals("image/x-raw-pentax", container.get(HttpHeaders.CONTENT_TYPE));
        assertEquals("PENTAX K-7", container.get(TIFF.EQUIPMENT_MODEL));

        //the camera thumbnail comes first in the file (IFD1), the full-size
        //preview second (IFD2): the larger one is still emitted first
        assertThumbnail(metadataList.get(1), 64, 48);
        assertInlinePreview(metadataList.get(2), 0, 32, 24);
    }

    @Test
    public void testDNG() throws Exception {
        List<Metadata> metadataList = parseByName("testDNG.dng");
        //the raw strip in SubIFD0 also starts with a JPEG SOI marker
        //(lossless JPEG) and must not be extracted
        assertEquals(2, metadataList.size());

        Metadata container = metadataList.get(0);
        assertEquals("image/x-raw-adobe", container.get(HttpHeaders.CONTENT_TYPE));
        assertEquals("PENTAX K-x", container.get(TIFF.EQUIPMENT_MODEL));

        assertThumbnail(metadataList.get(1), 64, 48);
    }

    @Test
    public void testCR2() throws Exception {
        List<Metadata> metadataList = parseByName("testCR2.cr2");
        //the lossless-JPEG raw strip in the last IFD must not be extracted
        assertEquals(3, metadataList.size());

        Metadata container = metadataList.get(0);
        assertEquals("image/x-canon-cr2", container.get(HttpHeaders.CONTENT_TYPE));
        assertEquals("Canon EOS 7D", container.get(TIFF.EQUIPMENT_MODEL));

        //full-size preview stored as a strip in IFD0, thumbnail from IFD1
        assertThumbnail(metadataList.get(1), 64, 48);
        assertInlinePreview(metadataList.get(2), 0, 32, 24);
    }

    @Test
    public void testParsedByRawTiffParser() throws Exception {
        List<Metadata> metadataList = parseByName("testNEF.nef");
        List<String> parsedBy =
                Arrays.asList(metadataList.get(0).getValues(TikaCoreProperties.TIKA_PARSED_BY));
        assertContains(RawTiffParser.class.getName(), parsedBy);
    }

    @Test
    public void testBigTiffDNG() throws Exception {
        //DNG allows BigTIFF containers since spec version 1.7
        List<Metadata> metadataList = parseByName("testDNG_bigtiff.dng");
        assertEquals(2, metadataList.size());

        Metadata container = metadataList.get(0);
        assertEquals("image/x-raw-adobe", container.get(HttpHeaders.CONTENT_TYPE));
        assertNull(container.get(TikaCoreProperties.TIKA_META_EXCEPTION_WARNING));

        assertThumbnail(metadataList.get(1), 64, 48);
    }

    @Test
    public void testEmptyBigTiffWithoutException() throws Exception {
        //a BigTIFF file without any previews must parse quietly,
        //without recording an exception
        byte[] bigTiff = new byte[]{
                'M', 'M', 0, 0x2B, 0, 8, 0, 0,
                0, 0, 0, 0, 0, 0, 0, 16,  //offset of IFD0
                0, 0, 0, 0, 0, 0, 0, 0,   //IFD0: zero entries
                0, 0, 0, 0, 0, 0, 0, 0    //no next IFD
        };
        Metadata metadata = new Metadata();
        metadata.set(HttpHeaders.CONTENT_TYPE, "image/x-raw-adobe");
        try (TikaInputStream tis = TikaInputStream.get(bigTiff)) {
            new RawTiffParser().parse(tis, new DefaultHandler(), metadata, new ParseContext());
        }
        assertNull(metadata.get(TikaCoreProperties.TIKA_META_EXCEPTION_WARNING));
    }

    @Test
    public void testMaxPreviewLength() throws Exception {
        Parser parser = TikaLoader
                .load(getConfigPath(RawTiffParserTest.class,
                        "tika-config-raw-preview-max-length.json"))
                .loadParsers();
        Metadata metadata = new Metadata();
        metadata.set(TikaCoreProperties.RESOURCE_NAME_KEY, "testNEF.nef");
        metadata.set(HttpHeaders.CONTENT_TYPE, "image/x-raw-nikon");
        List<Metadata> metadataList =
                getRecursiveMetadata("testNEF.nef", parser, metadata, new ParseContext(), false);

        //the preview is larger than the configured limit and is skipped
        assertEquals(1, metadataList.size());
    }

    @Test
    public void testMaxTotalPreviewBytes() throws Exception {
        //testARW has two previews (676 + 644 bytes); a 700-byte total budget
        //admits only the first, bounding aggregate extraction work
        Parser parser = TikaLoader
                .load(getConfigPath(RawTiffParserTest.class,
                        "tika-config-raw-total-preview-budget.json"))
                .loadParsers();
        Metadata metadata = new Metadata();
        metadata.set(TikaCoreProperties.RESOURCE_NAME_KEY, "testARW.arw");
        metadata.set(HttpHeaders.CONTENT_TYPE, "image/x-raw-sony");
        List<Metadata> metadataList =
                getRecursiveMetadata("testARW.arw", parser, metadata, new ParseContext(), false);

        assertEquals(2, metadataList.size());
        assertThumbnail(metadataList.get(1), 64, 48);
    }

    @Test
    public void testExtractPreviewsDisabled() throws Exception {
        Parser parser = TikaLoader
                .load(getConfigPath(RawTiffParserTest.class, "tika-config-raw-previews-off.json"))
                .loadParsers();
        Metadata metadata = new Metadata();
        metadata.set(TikaCoreProperties.RESOURCE_NAME_KEY, "testNEF.nef");
        metadata.set(HttpHeaders.CONTENT_TYPE, "image/x-raw-nikon");
        List<Metadata> metadataList =
                getRecursiveMetadata("testNEF.nef", parser, metadata, new ParseContext(), false);

        assertEquals(1, metadataList.size());
        assertEquals("NIKON CORPORATION", metadataList.get(0).get(TIFF.EQUIPMENT_MAKE));
    }
}