RawTiffDetector.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.detect.image;

import java.io.IOException;
import java.io.InputStream;
import java.nio.charset.StandardCharsets;
import java.util.ArrayDeque;
import java.util.Deque;
import java.util.HashSet;
import java.util.Locale;
import java.util.Set;

import org.apache.tika.annotation.TikaComponent;
import org.apache.tika.detect.Detector;
import org.apache.tika.io.TikaInputStream;
import org.apache.tika.metadata.Metadata;
import org.apache.tika.mime.MediaType;
import org.apache.tika.parser.ParseContext;

/**
 * Tells the TIFF-based raw camera formats (Nikon NEF/NRW, Pentax PEF/PTX,
 * Sony ARW/SRF/SR2, Samsung SRW, Adobe DNG) from a plain TIFF by content,
 * so they are recognized without a file name (TIKA-4861). They share the
 * TIFF magic; what differs is the image directory:
 * <ol>
 *   <li>a {@code DNGVersion} tag (0xC612) makes a DNG, whoever wrote it;</li>
 *   <li>a vendor-specific {@code Compression} value (0x0103) in any IFD
 *       names the format: 34713 Nikon, 32767 Sony, 65535 Pentax,
 *       32770 and 32772 Samsung (this is how ExifTool identifies them);</li>
 *   <li>otherwise sensor data in any IFD marks a raw, and the {@code Make}
 *       tag (0x010F) picks the vendor: an image with
 *       {@code PhotometricInterpretation} 32803 (CFA) or 34892 (LinearRaw),
 *       or a full-resolution image (NewSubfileType 0) of one 9 to 16 bit
 *       sample without any PhotometricInterpretation, which a regular TIFF
 *       must have (Samsung NX1). This catches the uncompressed variants. A
 *       plain TIFF from the same camera has RGB data and stays
 *       {@code image/tiff}.</li>
 * </ol>
 * The detector reads the stream into memory as far as the directories
 * require, up to {@link #MAX_PREFIX_LENGTH}, and walks the IFD chain and the
 * SubIFDs (0x014A) inside that prefix; raw files keep their directories
 * ahead of the sensor data, though behind the embedded previews (some
 * hundred KB). A plain TIFF with its directory at the start costs a few KB.
 * Anything it cannot decide is left to the other detectors as
 * {@code application/octet-stream}. Canon CR2 has a fixed signature and is
 * matched by the mime magic instead.
 */
@TikaComponent
public class RawTiffDetector implements Detector {

    private static final long serialVersionUID = 1L;

    /**
     * How far into the stream the directories may lie. The IFD tables of a
     * raw file follow its embedded previews: 265 KB for a Nikon Z 6, 458 KB
     * for a Samsung NX1.
     */
    static final int MAX_PREFIX_LENGTH = 1024 * 1024;

    /**
     * The stream is read in chunks of this size, as the directories require.
     */
    private static final int CHUNK_LENGTH = 64 * 1024;

    private static final int TAG_NEW_SUBFILE_TYPE = 0x00FE;
    private static final int TAG_BITS_PER_SAMPLE = 0x0102;
    private static final int TAG_MAKE = 0x010F;
    private static final int TAG_COMPRESSION = 0x0103;
    private static final int TAG_PHOTOMETRIC_INTERPRETATION = 0x0106;
    private static final int TAG_SUB_IFDS = 0x014A;
    private static final int TAG_DNG_VERSION = 0xC612;

    private static final int COMPRESSION_NIKON = 34713;
    private static final int COMPRESSION_SONY = 32767;
    private static final int COMPRESSION_PENTAX = 65535;
    private static final int COMPRESSION_SAMSUNG = 32770;
    private static final int COMPRESSION_SAMSUNG_2 = 32772;

    private static final int PHOTOMETRIC_CFA = 32803;
    private static final int PHOTOMETRIC_LINEAR_RAW = 34892;

    private static final int TYPE_ASCII = 2;
    private static final int TYPE_SHORT = 3;
    private static final int TYPE_LONG = 4;
    private static final int TYPE_IFD = 13;
    private static final int TYPE_LONG8 = 16;
    private static final int TYPE_IFD8 = 18;

    private static final int MAX_IFDS = 32;
    private static final int MAX_ENTRIES_PER_IFD = 1024;
    private static final int MAX_MAKE_LENGTH = 256;

    static final MediaType NIKON = MediaType.image("x-raw-nikon");
    static final MediaType PENTAX = MediaType.image("x-raw-pentax");
    static final MediaType SONY = MediaType.image("x-raw-sony");
    static final MediaType SAMSUNG = MediaType.image("x-raw-samsung");
    static final MediaType ADOBE = MediaType.image("x-raw-adobe");

    @Override
    public MediaType detect(TikaInputStream tis, Metadata metadata, ParseContext parseContext)
            throws IOException {
        if (tis == null) {
            return MediaType.OCTET_STREAM;
        }
        //one more than is read: a BufferedInputStream drops the mark once the
        //read limit is reached, and reset() in finally must always succeed
        tis.mark(MAX_PREFIX_LENGTH + 1);
        try {
            byte[] header = new byte[16];
            if (tis.readNBytes(header, 0, 16) < 16 || !isTiff(header)) {
                return MediaType.OCTET_STREAM;
            }
            return detect(new Prefix(header, 16, tis, MAX_PREFIX_LENGTH));
        } finally {
            tis.reset();
        }
    }

    /**
     * The start of the file, read further from the stream as the
     * directories require, up to a limit.
     */
    static final class Prefix {
        private byte[] buf;
        private int length;
        private final InputStream in;
        private final int limit;

        /**
         * @param buf    the bytes read so far
         * @param length how many of them are valid
         * @param in     where to read more from, or null if this is all
         * @param limit  how far to read at most
         */
        Prefix(byte[] buf, int length, InputStream in, int limit) {
            this.buf = buf;
            this.length = length;
            this.in = in;
            this.limit = limit;
        }

        /**
         * Makes the bytes up to {@code end} (exclusive) available if the
         * file has them and they are within the limit.
         *
         * @return whether {@code buf[0..end)} is valid now
         */
        boolean ensure(long end) throws IOException {
            //callers add to a 64-bit offset taken from the file, so end can wrap
            //negative; reject that before the length comparison lets it through
            if (end < 0 || end > limit) {
                return false;
            }
            if (end <= length) {
                return true;
            }
            if (in == null) {
                return false;
            }
            int wanted = (int) Math.min(limit, ((end + CHUNK_LENGTH - 1) / CHUNK_LENGTH) * CHUNK_LENGTH);
            if (buf.length < wanted) {
                byte[] bigger = new byte[wanted];
                System.arraycopy(buf, 0, bigger, 0, length);
                buf = bigger;
            }
            while (length < wanted) {
                int n = in.read(buf, length, wanted - length);
                if (n < 0) {
                    break;
                }
                length += n;
            }
            return end <= length;
        }
    }

    private static boolean isTiff(byte[] h) {
        boolean bigEndian = h[0] == 'M' && h[1] == 'M';
        boolean littleEndian = h[0] == 'I' && h[1] == 'I';
        if (!bigEndian && !littleEndian) {
            return false;
        }
        int magic = getUInt16(h, 2, bigEndian);
        return magic == 42 || magic == 43;
    }

    /**
     * @param buf    the start of the file
     * @param length how many bytes of it are valid
     * @return the raw type, or {@link MediaType#OCTET_STREAM} if the prefix
     * does not show one
     */
    static MediaType detect(byte[] buf, int length) {
        try {
            return detect(new Prefix(buf, length, null, length));
        } catch (IOException e) {
            //no stream, nothing to read
            throw new IllegalStateException(e);
        }
    }

    private static MediaType detect(Prefix prefix) throws IOException {
        if (!prefix.ensure(8) || !isTiff(prefix.buf)) {
            return MediaType.OCTET_STREAM;
        }
        byte[] buf = prefix.buf;
        int length = prefix.length;
        boolean bigEndian = buf[0] == 'M';
        boolean bigTiff = getUInt16(buf, 2, bigEndian) == 43;
        int countSize = bigTiff ? 8 : 2;
        int entrySize = bigTiff ? 20 : 12;
        int offsetSize = bigTiff ? 8 : 4;
        int inlineSize = bigTiff ? 8 : 4;
        long firstIfd;
        if (bigTiff) {
            if (!prefix.ensure(16) || getUInt16(buf, 4, bigEndian) != 8) {
                return MediaType.OCTET_STREAM;
            }
            firstIfd = getUInt64(buf, 8, bigEndian);
        } else {
            firstIfd = getUInt32(buf, 4, bigEndian);
        }

        boolean dng = false;
        MediaType byCompression = null;
        boolean rawImage = false;
        String make = null;

        Set<Long> visited = new HashSet<>();
        Deque<Long> toVisit = new ArrayDeque<>();
        toVisit.add(firstIfd);
        while (!toVisit.isEmpty() && visited.size() < MAX_IFDS) {
            long ifdOffset = toVisit.poll();
            if (ifdOffset <= 0 || !visited.add(ifdOffset) || !prefix.ensure(ifdOffset + countSize)) {
                continue;
            }
            buf = prefix.buf;
            int ifd = (int) ifdOffset;
            long numEntries = bigTiff ? getUInt64(buf, ifd, bigEndian) : getUInt16(buf, ifd, bigEndian);
            if (numEntries < 0 || numEntries > MAX_ENTRIES_PER_IFD
                    || !prefix.ensure(ifd + countSize + numEntries * entrySize + offsetSize)) {
                continue;
            }
            buf = prefix.buf;
            length = prefix.length;
            int entries = ifd + countSize;
            long subfileType = -1;
            boolean photometricPresent = false;
            long[] bitsPerSample = new long[0];
            for (int i = 0; i < numEntries; i++) {
                int e = entries + i * entrySize;
                int tag = getUInt16(buf, e, bigEndian);
                int type = getUInt16(buf, e + 2, bigEndian);
                long count = bigTiff ? getUInt64(buf, e + 4, bigEndian) : getUInt32(buf, e + 4, bigEndian);
                int valueField = e + 4 + inlineSize;
                switch (tag) {
                    case TAG_DNG_VERSION:
                        dng = true;
                        break;
                    case TAG_SUB_IFDS:
                        //only MAX_IFDS can ever be visited: do not read more pointers
                        for (long sub : longValues(buf, length, valueField, bigEndian, bigTiff, type,
                                Math.min(count, MAX_IFDS))) {
                            if (toVisit.size() < MAX_IFDS) {
                                toVisit.add(sub);
                            }
                        }
                        break;
                    case TAG_COMPRESSION:
                        if (byCompression == null && count == 1) {
                            long[] v = longValues(buf, length, valueField, bigEndian, bigTiff, type, count);
                            byCompression = v.length == 1 ? vendorByCompression(v[0]) : null;
                        }
                        break;
                    case TAG_PHOTOMETRIC_INTERPRETATION:
                        photometricPresent = true;
                        if (count == 1) {
                            long[] v = longValues(buf, length, valueField, bigEndian, bigTiff, type, count);
                            if (v.length == 1
                                    && (v[0] == PHOTOMETRIC_CFA || v[0] == PHOTOMETRIC_LINEAR_RAW)) {
                                rawImage = true;
                            }
                        }
                        break;
                    case TAG_NEW_SUBFILE_TYPE:
                        if (count == 1) {
                            long[] v = longValues(buf, length, valueField, bigEndian, bigTiff, type, count);
                            subfileType = v.length == 1 ? v[0] : -1;
                        }
                        break;
                    case TAG_BITS_PER_SAMPLE:
                        if (count <= 4) {
                            bitsPerSample = longValues(buf, length, valueField, bigEndian, bigTiff, type, count);
                        }
                        break;
                    case TAG_MAKE:
                        if (make == null && type == TYPE_ASCII) {
                            make = asciiValue(buf, length, valueField, bigEndian, bigTiff, count);
                        }
                        break;
                    default:
                        break;
                }
            }
            //sensor data without a PhotometricInterpretation: a full-resolution
            //image of one deep sample, which no regular TIFF describes that way
            if (!photometricPresent && subfileType == 0 && bitsPerSample.length == 1
                    && bitsPerSample[0] >= 9 && bitsPerSample[0] <= 16) {
                rawImage = true;
            }
            int follower = (int) (entries + numEntries * entrySize);
            long next = bigTiff ? getUInt64(buf, follower, bigEndian) : getUInt32(buf, follower, bigEndian);
            if (toVisit.size() < MAX_IFDS) {
                toVisit.add(next);
            }
        }

        if (dng) {
            return ADOBE;
        }
        if (byCompression != null) {
            return byCompression;
        }
        if (rawImage && make != null) {
            MediaType byMake = vendorByMake(make);
            if (byMake != null) {
                return byMake;
            }
        }
        return MediaType.OCTET_STREAM;
    }

    private static MediaType vendorByCompression(long compression) {
        if (compression == COMPRESSION_NIKON) {
            return NIKON;
        } else if (compression == COMPRESSION_SONY) {
            return SONY;
        } else if (compression == COMPRESSION_PENTAX) {
            return PENTAX;
        } else if (compression == COMPRESSION_SAMSUNG || compression == COMPRESSION_SAMSUNG_2) {
            return SAMSUNG;
        }
        return null;
    }

    private static MediaType vendorByMake(String make) {
        String m = make.trim().toUpperCase(Locale.ROOT);
        if (m.startsWith("NIKON")) {
            return NIKON;
        } else if (m.startsWith("PENTAX") || m.startsWith("RICOH")) {
            return PENTAX;
        } else if (m.startsWith("SONY")) {
            return SONY;
        } else if (m.startsWith("SAMSUNG")) {
            return SAMSUNG;
        }
        return null;
    }

    /**
     * The values of a SHORT, LONG, IFD (and for BigTIFF LONG8, IFD8) entry,
     * inline or at their offset; empty if the entry is another type or
     * points outside the prefix.
     */
    private static long[] longValues(byte[] buf, int length, int valueField, boolean bigEndian,
                                     boolean bigTiff, int type, long count) {
        int typeSize;
        if (type == TYPE_SHORT) {
            typeSize = 2;
        } else if (type == TYPE_LONG || type == TYPE_IFD) {
            typeSize = 4;
        } else if (bigTiff && (type == TYPE_LONG8 || type == TYPE_IFD8)) {
            typeSize = 8;
        } else {
            return new long[0];
        }
        if (count < 1 || count > MAX_ENTRIES_PER_IFD) {
            return new long[0];
        }
        int n = (int) count;
        long totalBytes = (long) typeSize * n;
        int off;
        if (totalBytes <= (bigTiff ? 8 : 4)) {
            off = valueField;
        } else {
            long valueOffset = bigTiff
                    ? getUInt64(buf, valueField, bigEndian) : getUInt32(buf, valueField, bigEndian);
            if (valueOffset < 0 || valueOffset > length - totalBytes) {
                return new long[0];
            }
            off = (int) valueOffset;
        }
        long[] values = new long[n];
        for (int i = 0; i < n; i++) {
            int p = off + i * typeSize;
            if (typeSize == 2) {
                values[i] = getUInt16(buf, p, bigEndian);
            } else if (typeSize == 4) {
                values[i] = getUInt32(buf, p, bigEndian);
            } else {
                values[i] = getUInt64(buf, p, bigEndian);
            }
        }
        return values;
    }

    private static String asciiValue(byte[] buf, int length, int valueField, boolean bigEndian,
                                     boolean bigTiff, long count) {
        if (count < 1 || count > MAX_MAKE_LENGTH) {
            return null;
        }
        int n = (int) count;
        int off;
        if (n <= (bigTiff ? 8 : 4)) {
            off = valueField;
        } else {
            long valueOffset = bigTiff
                    ? getUInt64(buf, valueField, bigEndian) : getUInt32(buf, valueField, bigEndian);
            if (valueOffset < 0 || valueOffset > length - n) {
                return null;
            }
            off = (int) valueOffset;
        }
        int end = off;
        while (end < off + n && buf[end] != 0) {
            end++;
        }
        return new String(buf, off, end - off, StandardCharsets.US_ASCII);
    }

    private static int getUInt16(byte[] b, int off, boolean bigEndian) {
        int a = b[off] & 0xFF;
        int c = b[off + 1] & 0xFF;
        return bigEndian ? (a << 8) | c : (c << 8) | a;
    }

    private static long getUInt32(byte[] b, int off, boolean bigEndian) {
        long high = getUInt16(b, off, bigEndian);
        long low = getUInt16(b, off + 2, bigEndian);
        return bigEndian ? (high << 16) | low : (low << 16) | high;
    }

    private static long getUInt64(byte[] b, int off, boolean bigEndian) {
        long high = getUInt32(b, off, bigEndian);
        long low = getUInt32(b, off + 4, bigEndian);
        return bigEndian ? (high << 32) | low : (low << 32) | high;
    }
}