ImageDecoding.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.imageprocessing;

import java.awt.image.BufferedImage;
import java.awt.image.IndexColorModel;
import java.awt.image.Raster;
import java.io.ByteArrayInputStream;
import java.io.IOException;
import java.util.Iterator;
import javax.imageio.ImageIO;
import javax.imageio.ImageReadParam;
import javax.imageio.ImageReader;
import javax.imageio.stream.ImageInputStream;

import org.apache.tika.parser.enricher.ImageProbe;

/** What the checks read from an image, each computed once per image through the probe. */
final class ImageDecoding {

    /** The decoded copy is at most this many pixels on a side. */
    static final int MAX_SIDE = 1000;

    /** Images with more pixels than this are not decoded: a check cannot judge them. */
    static final long MAX_DECODE_PIXELS = 100_000_000L;

    /**
     * ImageIO buffers a progressive JPEG whole, about 3 bytes of native memory a pixel,
     * whatever the subsampling: a blank one of 587 KB at 10000 x 10000 took 290 MB.
     */
    static final long MAX_PROGRESSIVE_JPEG_PIXELS = 16_000_000L;

    private static final long[] UNKNOWN = {-1, -1};

    private ImageDecoding() {
    }

    /** {width, height} from the image header, or {-1, -1}. */
    static long[] size(ImageProbe image) throws IOException {
        return image.memo("imagecheck:size", () -> {
            // the start of the file holds the dimensions of nearly every format; read the whole
            // image only when it does not (a TIFF whose directory is at the end)
            long[] size = tryReadSize(image.header());
            if (size[0] <= 0 && !image.headerIsComplete()) {
                size = tryReadSize(image.bytes());
            }
            return size;
        });
    }

    /**
     * Gray levels 0..255 of a decoded copy, row by row. An image with transparency has two
     * copies, over white and over black: transparent pixels hide dark text in one and light
     * text in the other. {@code overBlack} is null for an opaque image.
     */
    record Gray(byte[] levels, byte[] overBlack, int width, int height) {
    }

    /**
     * A gray copy of the image of at most {@link #MAX_SIDE} pixels on a side, or null when it
     * cannot be read or is too large to decode.
     */
    static Gray gray(ImageProbe image) throws IOException {
        return image.memo("imagecheck:gray", () -> {
            long[] size = size(image);
            if (size[0] <= 0 || size[1] <= 0) {
                return null;
            }
            long pixels = size[0] * size[1];
            if (pixels > MAX_DECODE_PIXELS || (pixels > MAX_PROGRESSIVE_JPEG_PIXELS
                    && mayBeProgressiveJpeg(image.header()))) {
                return null;
            }
            return decode(image.bytes());
        });
    }

    /** A JPEG whose frame is progressive, or whose frame header is not in {@code bytes}. */
    static boolean mayBeProgressiveJpeg(byte[] bytes) {
        if (bytes.length < 2 || (bytes[0] & 255) != 0xFF || (bytes[1] & 255) != 0xD8) {
            return false;
        }
        int i = 2;
        while (i + 1 < bytes.length) {
            if ((bytes[i] & 255) != 0xFF) {
                return true;
            }
            int marker = bytes[i + 1] & 255;
            if (marker == 0xFF) {
                i++;
                continue;
            }
            if (marker == 0xC2 || marker == 0xC6 || marker == 0xCA || marker == 0xCE) {
                return true;
            }
            if (marker >= 0xC0 && marker <= 0xCF && marker != 0xC4 && marker != 0xC8
                    && marker != 0xCC) {
                return false;
            }
            if (marker == 0x01 || (marker >= 0xD0 && marker <= 0xD7)) {
                i += 2;
                continue;
            }
            if (marker == 0xDA || marker == 0xD9 || i + 3 >= bytes.length) {
                return true;
            }
            int length = ((bytes[i + 2] & 255) << 8) | (bytes[i + 3] & 255);
            if (length < 2) {
                return true;
            }
            i += 2 + length;
        }
        return true;
    }

    private static long[] tryReadSize(byte[] bytes) throws IOException {
        try {
            return readSize(bytes);
        } catch (IOException | RuntimeException e) {
            return UNKNOWN;
        }
    }

    private static long[] readSize(byte[] bytes) throws IOException {
        if (bytes == null) {
            return UNKNOWN;
        }
        try (ImageInputStream iis = ImageIO.createImageInputStream(new ByteArrayInputStream(bytes))) {
            ImageReader reader = reader(iis);
            if (reader == null) {
                return UNKNOWN;
            }
            try {
                reader.setInput(iis, true, true);
                return new long[] {reader.getWidth(0), reader.getHeight(0)};
            } finally {
                reader.dispose();
            }
        }
    }

    private static Gray decode(byte[] bytes) throws IOException {
        try (ImageInputStream iis = ImageIO.createImageInputStream(new ByteArrayInputStream(bytes))) {
            ImageReader reader = reader(iis);
            if (reader == null) {
                return null;
            }
            try {
                reader.setInput(iis, true, true);
                int step = (Math.max(reader.getWidth(0), reader.getHeight(0)) + MAX_SIDE - 1)
                        / MAX_SIDE;
                ImageReadParam param = reader.getDefaultReadParam();
                param.setSourceSubsampling(Math.max(1, step), Math.max(1, step), 0, 0);
                return toGray(reader.read(0, param));
            } finally {
                reader.dispose();
            }
        }
    }

    private static ImageReader reader(ImageInputStream iis) {
        if (iis == null) {
            return null;
        }
        Iterator<ImageReader> readers = ImageIO.getImageReaders(iis);
        return readers.hasNext() ? readers.next() : null;
    }

    static Gray toGray(BufferedImage image) {
        int width = image.getWidth();
        int height = image.getHeight();
        boolean alpha = image.getColorModel().hasAlpha();
        byte[] levels = new byte[width * height];
        byte[] overBlack = alpha ? new byte[width * height] : null;
        if (image.getColorModel() instanceof IndexColorModel) {
            for (int y = 0; y < height; y++) {
                for (int x = 0; x < width; x++) {
                    int argb = image.getRGB(x, y);
                    set(levels, overBlack, y * width + x, luma((argb >> 16) & 255,
                            (argb >> 8) & 255, argb & 255), (argb >>> 24), false);
                }
            }
            return new Gray(levels, overBlack, width, height);
        }
        Raster raster = image.getRaster();
        int bands = raster.getNumBands();
        int colorBands = alpha ? bands - 1 : bands;
        int colorShift = Math.max(0, raster.getSampleModel().getSampleSize(0) - 8);
        int alphaShift = alpha
                ? Math.max(0, raster.getSampleModel().getSampleSize(bands - 1) - 8) : 0;
        boolean premultiplied = image.isAlphaPremultiplied();
        int[] sample = new int[bands];
        for (int y = 0; y < height; y++) {
            for (int x = 0; x < width; x++) {
                raster.getPixel(x, y, sample);
                int gray = colorBands >= 3 ? luma(sample[0], sample[1], sample[2]) : sample[0];
                set(levels, overBlack, y * width + x, gray >> colorShift,
                        alpha ? sample[bands - 1] >> alphaShift : 255, premultiplied);
            }
        }
        return new Gray(levels, overBlack, width, height);
    }

    private static int luma(int r, int g, int b) {
        return (r * 299 + g * 587 + b * 114) / 1000;
    }

    private static void set(byte[] levels, byte[] overBlack, int i, int gray, int alpha,
                            boolean premultiplied) {
        int black = premultiplied ? gray : gray * alpha / 255;
        if (overBlack == null) {
            levels[i] = (byte) gray;
            return;
        }
        levels[i] = (byte) Math.min(255, black + 255 - alpha);
        overBlack[i] = (byte) black;
    }
}