All Downloads are FREE. Search and download functionalities are using the official Maven repository.

net.algart.matrices.tiff.awt.JPEG Maven / Gradle / Ivy

There is a newer version: 1.3.7
Show newest version
/*
 * The MIT License (MIT)
 *
 * Copyright (c) 2023-2024 Daniel Alievsky, AlgART Laboratory (http://algart.net)
 *
 * Permission is hereby granted, free of charge, to any person obtaining a copy
 * of this software and associated documentation files (the "Software"), to deal
 * in the Software without restriction, including without limitation the rights
 * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
 * copies of the Software, and to permit persons to whom the Software is
 * furnished to do so, subject to the following conditions:
 *
 * The above copyright notice and this permission notice shall be included in all
 * copies or substantial portions of the Software.
 *
 * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
 * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
 * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
 * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
 * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
 * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
 * SOFTWARE.
 */

package net.algart.matrices.tiff.awt;

import net.algart.matrices.tiff.TiffException;
import net.algart.matrices.tiff.tags.TagPhotometricInterpretation;
import org.scijava.util.Bytes;
import org.w3c.dom.NamedNodeMap;
import org.w3c.dom.Node;
import org.w3c.dom.NodeList;

import javax.imageio.*;
import javax.imageio.metadata.IIOMetadata;
import javax.imageio.stream.ImageInputStream;
import javax.imageio.stream.ImageOutputStream;
import java.awt.image.*;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.util.Iterator;
import java.util.Objects;

public class JPEG {
    private static final boolean USE_LEGACY_DECODE_Y_CB_CR = false;
    // - Should be false for correct behaviour and better performance; necessary for debugging needs only.

    public record ImageInformation(BufferedImage bufferedImage, IIOMetadata metadata) {
    }

    private JPEG() {
    }

    /**
     * Analog of ImageIO.read. Actually can read any formats, not only JPEG.
     * Also reads metadata (but not thumbnails).
     */
    public static ImageInformation readJPEG(InputStream in) throws IOException {
        ImageInputStream stream = ImageIO.createImageInputStream(in);
        if (stream == null) {
            throw new IIOException("Cannot decompress JPEG tile");
        }
        ImageReader reader = JPEG.getImageReaderOrNull(stream);
        if (reader == null) {
            return null;
        }
        ImageReadParam param = reader.getDefaultReadParam();
        reader.setInput(stream, true, true);
        try {
            IIOMetadata imageMetadata = reader.getImageMetadata(0);
            BufferedImage image = reader.read(0, param);
            return new ImageInformation(image, imageMetadata);
        } finally {
            reader.dispose();
        }
    }

    /**
     * Analog of ImageIO.write for JPEG.
     */
    public static void writeJPEG(
            BufferedImage image,
            OutputStream out,
            TagPhotometricInterpretation colorSpace,
            double quality) throws IOException {
        Objects.requireNonNull(image, "Null image");
        Objects.requireNonNull(out, "Null output stream");
        Objects.requireNonNull(colorSpace, "Null color space");
        if (colorSpace != TagPhotometricInterpretation.Y_CB_CR && colorSpace != TagPhotometricInterpretation.RGB) {
            throw new IllegalArgumentException("Unsupported color space: " + colorSpace);
        }
        final boolean enforceRGB = colorSpace == TagPhotometricInterpretation.RGB;

        final ImageOutputStream ios = ImageIO.createImageOutputStream(out);
        final ImageWriter jpegWriter = getJPEGWriter();
        jpegWriter.setOutput(ios);

        final ImageWriteParam writeParam = jpegWriter.getDefaultWriteParam();
        writeParam.setCompressionMode(ImageWriteParam.MODE_EXPLICIT);
        writeParam.setCompressionType("JPEG");
        writeParam.setCompressionQuality((float) quality);
        final ImageTypeSpecifier imageTypeSpecifier = new ImageTypeSpecifier(image);
        if (enforceRGB) {
            writeParam.setDestinationType(imageTypeSpecifier);
            // - Important! It informs getDefaultImageMetadata to add Adobe and SOF markers,
            // that is detected by JPEGImageWriter and leads to correct outCsType = JPEG.JCS_RGB
        }
        final IIOMetadata metadata = jpegWriter.getDefaultImageMetadata(
                enforceRGB ? null : imageTypeSpecifier,
                writeParam);
        // - Important! imageType = null necessary for RGB, in other case setDestinationType will be ignored!

        final IIOImage iioImage = new IIOImage(image, null, metadata);
        // - metadata necessary (with necessary markers)
        try {
            jpegWriter.write(null, iioImage, writeParam);
        } finally {
            jpegWriter.dispose();
        }
    }

    public static String tryToFindColorSpace(IIOMetadata metadata) {
        Node tree = metadata.getAsTree("javax_imageio_1.0");
        NodeList rootNodes = tree.getChildNodes();
        for (int k = 0, n = rootNodes.getLength(); k < n; k++) {
            Node rootChild = rootNodes.item(k);
            String childName = rootChild.getNodeName();
            if ("Chroma".equalsIgnoreCase(childName)) {
                NodeList nodes = rootChild.getChildNodes();
                for (int i = 0, m = nodes.getLength(); i < m; i++) {
                    Node subChild = nodes.item(i);
                    String subChildName = subChild.getNodeName();
                    if ("ColorSpaceType".equalsIgnoreCase(subChildName)) {
                        NamedNodeMap attributes = subChild.getAttributes();
                        Node name = attributes.getNamedItem("name");
                        return name.getNodeValue();
                    }
                }
            }
        }
        return null;
    }

    public static boolean completeDecodingYCbCrNecessary(
            ImageInformation imageInformation,
            TagPhotometricInterpretation declaredColorSpace,
            int[] declaredSubsampling) {
        Objects.requireNonNull(imageInformation, "Null image information");
        Objects.requireNonNull(declaredColorSpace, "Null color space");
        Objects.requireNonNull(declaredSubsampling, "Null declared subsampling");
        final String colorSpace = tryToFindColorSpace(imageInformation.metadata);
        return "RGB".equalsIgnoreCase(colorSpace)
                && declaredColorSpace == TagPhotometricInterpretation.Y_CB_CR
                && declaredSubsampling.length >= 2
                && declaredSubsampling[0] == 1 && declaredSubsampling[1] == 1
                && imageInformation.bufferedImage.getRaster().getNumBands() == 3;
        // Rare case: YCbCr is encoded with non-standard sub-sampling (more exactly, without sub-sampling),
        // and the JPEG is incorrectly detected as RGB; so, there is no sense to optimize this.
    }

    public static void completeDecodingYCbCr(
            byte[][] data,
            ImageInformation imageInformation,
            TagPhotometricInterpretation declaredColorSpace,
            int[] declaredSubsampling)
            throws TiffException {
        Objects.requireNonNull(data, "Null data");
        Objects.requireNonNull(imageInformation, "Null image information");
        Objects.requireNonNull(declaredColorSpace, "Null color space");
        Objects.requireNonNull(declaredSubsampling, "Null declared subsampling");
        final long bandLength = (long) imageInformation.bufferedImage.getWidth()
                * (long) imageInformation.bufferedImage.getHeight();
        if (USE_LEGACY_DECODE_Y_CB_CR) {
            decodeYCbCrLegacy(data, bandLength);
            return;
        }

        if (data[0].length != bandLength) {
            // - should not occur
            throw new TiffException("Cannot correct unpacked JPEG: number of bytes per sample in JPEG " +
                    "must be 1, but actually we have " +
                    (double) data[0].length / (double) bandLength + " bytes/sample");
        }
        for (int i = 0; i < data[0].length; i++) {
            int y = data[0][i] & 0xFF;
            int cb = data[1][i] & 0xFF;
            int cr = data[2][i] & 0xFF;

            cb -= 128;
            cr -= 128;

            double red = (y + 1.402 * cr);
            double green = (y - 0.34414 * cb - 0.71414 * cr);
            double blue = (y + 1.772 * cb);

            data[0][i] = (byte) toUnsignedByte(red);
            data[1][i] = (byte) toUnsignedByte(green);
            data[2][i] = (byte) toUnsignedByte(blue);
        }
    }


    private static void decodeYCbCrLegacy(byte[][] buf, long bandLength) {
        final boolean littleEndian = false;
        // - not important for 8-bit values
        final int nBytes = (int) (buf[0].length / bandLength);
        final int mask = (int) (Math.pow(2, nBytes * 8) - 1);
        for (int i = 0; i < buf[0].length; i += nBytes) {
            final int y = Bytes.toInt(buf[0], i, nBytes, littleEndian);
            int cb = Bytes.toInt(buf[1], i, nBytes, littleEndian);
            int cr = Bytes.toInt(buf[2], i, nBytes, littleEndian);

            cb = Math.max(0, cb - 128);
            cr = Math.max(0, cr - 128);

            final int red = (int) (y + 1.402 * cr) & mask;
            final int green = (int) (y - 0.34414 * cb - 0.71414 * cr) & mask;
            final int blue = (int) (y + 1.772 * cb) & mask;

            Bytes.unpack(red, buf[0], i, nBytes, littleEndian);
            Bytes.unpack(green, buf[1], i, nBytes, littleEndian);
            Bytes.unpack(blue, buf[2], i, nBytes, littleEndian);
        }
    }

    public static ImageReader getImageReaderOrNull(Object inputStream) {
        Iterator readers = ImageIO.getImageReaders(inputStream);
        return findAWTCodec(readers);
    }

    public static ImageWriter getJPEGWriter() throws IIOException {
        Iterator writers = ImageIO.getImageWritersByFormatName("jpeg");
        ImageWriter result = findAWTCodec(writers);
        if (result == null) {
            throw new IIOException("Cannot write JPEG: no necessary registered plugin");
        }
        return result;
    }

    private static  T findAWTCodec(Iterator iterator) {
        if (!iterator.hasNext()) {
            return null;
        }
        T first = iterator.next();
        if (isProbableAWTClass(first)) {
            return first;
            // - This is maximally typical behaviour, in particularly, used in ImageIO.read/write.
            // But it can be not the desirable behaviour, when we have some additional plugins
            // like TwelveMonkeys ImageIO, which are registered as the first plugin INSTEAD of built-in
            // Java AWT plugin, because, for example, TwelveMonkeys does not guarantee the identical behaviour;
            // in this case, we should try to find the original AWT plugin.
        }
        while (iterator.hasNext()) {
            T other = iterator.next();
            if (isProbableAWTClass(other)) {
                return other;
            }
        }
        return first;
    }

    private static boolean isProbableAWTClass(Object o) {
        return o != null && o.getClass().getName().startsWith("com.sun.");
    }

    private static int toUnsignedByte(double v) {
        return v < 0.0 ? 0 : v > 255.0 ? 255 : (int) Math.round(v);
    }
}




© 2015 - 2024 Weber Informatics LLC | Privacy Policy