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

net.algart.matrices.tiff.codecs.JPEG2000Codec 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.codecs;

import com.github.jaiimageio.jpeg2000.J2KImageReadParam;
import com.github.jaiimageio.jpeg2000.J2KImageWriteParam;
import com.github.jaiimageio.jpeg2000.impl.J2KImageReader;
import com.github.jaiimageio.jpeg2000.impl.J2KImageWriter;
import net.algart.matrices.tiff.TiffException;
import net.algart.matrices.tiff.awt.AWTImages;
import net.algart.matrices.tiff.awt.UnsignedIntBuffer;
import org.scijava.io.handle.DataHandle;
import org.scijava.io.location.Location;
import org.scijava.util.Bytes;

import javax.imageio.IIOImage;
import javax.imageio.ImageIO;
import javax.imageio.ImageWriteParam;
import javax.imageio.stream.ImageOutputStream;
import javax.imageio.stream.MemoryCacheImageInputStream;
import java.awt.image.*;
import java.io.*;
import java.util.Arrays;
import java.util.Objects;

// This class avoids the bug in SCIFIO: https://github.com/scifio/scifio/issues/495
// and allows to work without SCIFIO context
public class JPEG2000Codec extends AbstractCodec {
    // (It is placed here to avoid autocorrection by IntelliJ IDEA)
    /*
     * #%L
     * SCIFIO library for reading and converting scientific file formats.
     * %%
     * Copyright (C) 2011 - 2023 SCIFIO developers.
     * %%
     * Redistribution and use in source and binary forms, with or without
     * modification, are permitted provided that the following conditions are met:
     *
     * 1. Redistributions of source code must retain the above copyright notice,
     *    this list of conditions and the following disclaimer.
     * 2. Redistributions in binary form must reproduce the above copyright notice,
     *    this list of conditions and the following disclaimer in the documentation
     *    and/or other materials provided with the distribution.
     *
     * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
     * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
     * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
     * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDERS OR CONTRIBUTORS BE
     * LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
     * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
     * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
     * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
     * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
     * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
     * POSSIBILITY OF SUCH DAMAGE.
     * #L%
     */

    /**
     * Options for compressing and decompressing JPEG-2000 data.
     */
    public static class JPEG2000Options extends Options {
        public static final double DEFAULT_NORMAL_QUALITY = 10.0;

        /**
         * The lossless mode affects the default quality and the argument of J2KImageWriteParam.setFilter method
         * (WRITE).
         */
        boolean lossless = true;

        /**
         * Color model to use when constructing an image (WRITE).
         * */
        ColorModel colorModel = null;

        /**
         * The maximum code-block size to use per tile-component as it would be
         * provided to: {@code J2KImageWriteParam#setCodeBlockSize(int[])} (WRITE).
         */
        int[] codeBlockSize = { 64, 64 };

        /**
         * The number of decomposition levels as would be provided to:
         * {@code J2KImageWriteParam#setNumDecompositionLevels(int)} (WRITE). Leaving
         * this value {@code null} signifies that when a JPEG 2000 parameter set is
         * created for the purposes of compression the number of decomposition levels
         * will be left as the default.
         */
        Integer numberOfDecompositionLevels = null;

        /**
         * The resolution level as would be provided to:
         * {@code J2KImageWriteParam#setResolution(int)} (READ). Leaving this value
         * {@code null} signifies that when a JPEG 2000 parameter set is created for
         * the purposes of compression the number of decomposition levels will be left
         * as the default.
         */
        Integer resolution = null;

        public JPEG2000Options() {
            setQuality(Double.MAX_VALUE);
        }

        public boolean isLossless() {
            return lossless;
        }

        public JPEG2000Options setLossless(boolean lossless) {
            this.lossless = lossless;
            return this;
        }

        public ColorModel getColorModel() {
            return colorModel;
        }

        public JPEG2000Options setColorModel(ColorModel colorModel) {
            this.colorModel = colorModel;
            return this;
        }

        public int[] getCodeBlockSize() {
            return codeBlockSize.clone();
        }

        public JPEG2000Options setCodeBlockSize(int[] codeBlockSize) {
            Objects.requireNonNull(codeBlockSize, "Null codeBlockSize");
            // see J2KImageWriteParamJava constructor: it requires non-null j2kParam.getCodeBlockSize
            if (codeBlockSize.length < 2) {
                throw new IllegalArgumentException("Too short codeBlockSize array: int[" + codeBlockSize.length +
                        "] (must contain 2 elements)");
            }
            this.codeBlockSize = codeBlockSize.clone();
            return this;
        }

        public Integer getNumberOfDecompositionLevels() {
            return numberOfDecompositionLevels;
        }

        public JPEG2000Options setNumberOfDecompositionLevels(Integer numberOfDecompositionLevels) {
            this.numberOfDecompositionLevels = numberOfDecompositionLevels;
            return this;
        }

        public Integer getResolution() {
            return resolution;
        }

        public JPEG2000Options setResolution(Integer resolution) {
            this.resolution = resolution;
            return this;
        }

        // Note: this method SHOULD be overridden to provide correct clone() behaviour.
        @Override
        public JPEG2000Options setTo(Options options) {
            return setTo(options, true);
        }

        public JPEG2000Options setTo(Options options, boolean lossless) {
            super.setTo(options);
            if (options instanceof JPEG2000Options o) {
                setLossless(o.lossless);
                setColorModel(o.colorModel);
                setCodeBlockSize(o.codeBlockSize);
                setNumberOfDecompositionLevels(o.numberOfDecompositionLevels);
                setResolution(o.resolution);
            } else {
                setLossless(lossless);
                if (!hasQuality()) {
                    setQuality(lossless ? Double.MAX_VALUE : DEFAULT_NORMAL_QUALITY);
                }
            }
            return this;
        }

        @Override
        public  T toOldStyleOptions(Class oldStyleClass) {
            T result = super.toOldStyleOptions(oldStyleClass);
            setField(oldStyleClass, result, "lossless", lossless);
            setField(oldStyleClass, result, "colorModel", colorModel);
            return result;
        }

        @Override
        public void setToOldStyleOptions(Object oldStyleOptions) {
            super.setToOldStyleOptions(oldStyleOptions);
            lossless = getField(oldStyleOptions, Boolean.class, "lossless");
            colorModel = getField(oldStyleOptions, ColorModel.class, "colorModel");
        }

        @Override
        public String toString() {
            return super.toString() +
                    ", lossless=" + lossless +
                    ", colorModel=" + colorModel +
                    ", codeBlockSize=" + Arrays.toString(codeBlockSize) +
                    ", numDecompositionLevels=" + numberOfDecompositionLevels +
                    ", resolution=" + resolution;
        }
    }

    // Copy of equivalent SCIFIO method, not using jaiIIOService field
    public byte[] compress(final byte[] data, Options options) throws TiffException {
        Objects.requireNonNull(data, "Null data");
        Objects.requireNonNull(options, "Null codec options");
        if (options.floatingPoint) {
            throw new TiffException("JPEG-2000 compression cannot be used for floating-point values");
        }
        if (options.signed) {
            throw new TiffException("JPEG compression for signed samples is not supported " +
                    "(only unsigned samples allowed)");
        }
        if (data.length == 0) {
            return data;
        }

        JPEG2000Options jpeg2000Options = new JPEG2000Options().setTo(options);

        final ByteArrayOutputStream out = new ByteArrayOutputStream();
        BufferedImage img = null;

        int next = 0;

        // NB: Construct BufferedImages manually, rather than using
        // AWTImages.makeImage. The AWTImages.makeImage methods
        // construct
        // images that are not properly handled by the JPEG2000 writer.
        // Specifically, 8-bit multi-channel images are constructed with type
        // DataBuffer.TYPE_INT (so a single int is used to store all of the
        // channels for a specific pixel).

        final int plane = jpeg2000Options.width * jpeg2000Options.height;

        if (jpeg2000Options.bitsPerSample == 8) {
            final byte[][] b = new byte[jpeg2000Options.numberOfChannels][plane];
            if (jpeg2000Options.interleaved) {
                for (int q = 0; q < plane; q++) {
                    for (int c = 0; c < jpeg2000Options.numberOfChannels; c++) {
                        b[c][q] = data[next++];
                    }
                }
            } else {
                for (int c = 0; c < jpeg2000Options.numberOfChannels; c++) {
                    System.arraycopy(data, c * plane, b[c], 0, plane);
                }
            }
            final DataBuffer buffer = new DataBufferByte(b, plane);
            img = AWTImages.constructImage(b.length, DataBuffer.TYPE_BYTE,
                    jpeg2000Options.width, jpeg2000Options.height, false, true, buffer,
                    jpeg2000Options.colorModel);
        } else if (jpeg2000Options.bitsPerSample == 16) {
            final short[][] s = new short[jpeg2000Options.numberOfChannels][plane];
            if (jpeg2000Options.interleaved) {
                for (int q = 0; q < plane; q++) {
                    for (int c = 0; c < jpeg2000Options.numberOfChannels; c++) {
                        s[c][q] = Bytes.toShort(data, next, 2, jpeg2000Options.littleEndian);
                        next += 2;
                    }
                }
            } else {
                for (int c = 0; c < jpeg2000Options.numberOfChannels; c++) {
                    for (int q = 0; q < plane; q++) {
                        s[c][q] = Bytes.toShort(data, next, 2, jpeg2000Options.littleEndian);
                        next += 2;
                    }
                }
            }
            final DataBuffer buffer = new DataBufferUShort(s, plane);
            img = AWTImages.constructImage(s.length, DataBuffer.TYPE_USHORT,
                    jpeg2000Options.width, jpeg2000Options.height, false, true, buffer,
                    jpeg2000Options.colorModel);
        } else if (jpeg2000Options.bitsPerSample == 32) {
            final int[][] s = new int[jpeg2000Options.numberOfChannels][plane];
            if (jpeg2000Options.interleaved) {
                for (int q = 0; q < plane; q++) {
                    for (int c = 0; c < jpeg2000Options.numberOfChannels; c++) {
                        s[c][q] = Bytes.toInt(data, next, 4, jpeg2000Options.littleEndian);
                        next += 4;
                    }
                }
            } else {
                for (int c = 0; c < jpeg2000Options.numberOfChannels; c++) {
                    for (int q = 0; q < plane; q++) {
                        s[c][q] = Bytes.toInt(data, next, 4, jpeg2000Options.littleEndian);
                        next += 4;
                    }
                }
            }

            final DataBuffer buffer = new UnsignedIntBuffer(s, plane);
            img = AWTImages.constructImage(s.length, DataBuffer.TYPE_INT,
                    jpeg2000Options.width, jpeg2000Options.height, false, true, buffer,
                    jpeg2000Options.colorModel);
        }

        try {
            writeImage(out, img, jpeg2000Options);
        } catch (final IOException e) {
            throw new TiffException("Could not compress JPEG-2000 data.", e);
        }

        return out.toByteArray();
    }

    // Almost exact copy of equivalent SCIFIO method
    @Override
    public byte[] decompress(final DataHandle in, Options options) throws IOException {
        Objects.requireNonNull(in, "Null in");
        Objects.requireNonNull(options, "Null codec options");

        byte[] buf = null;
        final long fp = in.offset();
        if (options.maxSizeInBytes == 0) {
            buf = new byte[(int) (in.length() - fp)];
        } else {
            buf = new byte[(int) (options.maxSizeInBytes - fp)];
        }
        in.read(buf);
        return decompress(buf, options);
    }

    // Copy of equivalent SCIFIO method, not using jaiIIOService field
    @Override
    public byte[] decompress(byte[] data, Options options) throws TiffException {
        Objects.requireNonNull(data, "Null data");
        Objects.requireNonNull(options, "Null codec options");
        JPEG2000Options jpeg2000Options = new JPEG2000Options().setTo(options);

        byte[][] single = null;
        WritableRaster b = null;
        int bpp = jpeg2000Options.bitsPerSample / 8;

        try {
            final ByteArrayInputStream bis = new ByteArrayInputStream(data);
            b = (WritableRaster) readRaster(bis, jpeg2000Options);
            // - instead of:
            // b = (WritableRaster) this.jaiIIOService.readRaster(bis,
            //        (JPEG2000CodecOptions) options);
            single = AWTImages.getPixelBytes(b, jpeg2000Options.littleEndian);
            bpp = single[0].length / (b.getWidth() * b.getHeight());

            bis.close();
            b = null;
        } catch (final IOException e) {
            throw new TiffException("Could not decompress JPEG2000 image. Please " +
                    "make sure that jai_imageio.jar is installed.", e);
        }
//        catch (final ServiceException e) {
//            throw new TiffException("Could not decompress JPEG2000 image. Please " +
//                "make sure that jai_imageio.jar is installed.", e);
//        }

        if (single.length == 1) return single[0];
        final byte[] rtn = new byte[single.length * single[0].length];
        if (jpeg2000Options.interleaved) {
            int next = 0;
            for (int i = 0; i < single[0].length / bpp; i++) {
                for (int j = 0; j < single.length; j++) {
                    for (int bb = 0; bb < bpp; bb++) {
                        rtn[next++] = single[j][i * bpp + bb];
                    }
                }
            }
        } else {
            for (int i = 0; i < single.length; i++) {
                System.arraycopy(single[i], 0, rtn, i * single[0].length,
                        single[i].length);
            }
        }
        single = null;

        return rtn;
    }

    private static void writeImage(final OutputStream out, final BufferedImage img,
                                   final JPEG2000Options options) throws IOException {
        final ImageOutputStream ios = ImageIO.createImageOutputStream(out);

        final J2KImageWriter writer = new J2KImageWriter(null);
        writer.setOutput(ios);

        final String filter = options.lossless ? J2KImageWriteParam.FILTER_53
                : J2KImageWriteParam.FILTER_97;

        final IIOImage iioImage = new IIOImage(img, null, null);
        final J2KImageWriteParam param = (J2KImageWriteParam) writer
                .getDefaultWriteParam();
        param.setCompressionMode(ImageWriteParam.MODE_EXPLICIT);
        param.setCompressionType("JPEG2000");
        param.setLossless(options.lossless);
        param.setFilter(filter);
        param.setCodeBlockSize(options.getCodeBlockSize());
        param.setEncodingRate(options.quality());
//      TIFF provides its own tile subsystem
//        if (options.tileWidth > 0 && options.tileHeight > 0) {
//            param.setTiling(options.tileWidth, options.tileHeight,
//                    options.tileGridXOffset, options.tileGridYOffset);
//        }
        if (options.numberOfDecompositionLevels != null) {
            param.setNumDecompositionLevels(options.numberOfDecompositionLevels);
        }
        writer.write(null, iioImage, param);
        ios.close();
    }

    private static Raster readRaster(final InputStream in, final JPEG2000Options options) throws IOException {
        final J2KImageReader reader = new J2KImageReader(null);
        final MemoryCacheImageInputStream mciis = new MemoryCacheImageInputStream(in);
        reader.setInput(mciis, false, true);
        final J2KImageReadParam param = (J2KImageReadParam) reader.getDefaultReadParam();
        if (options.resolution != null) {
            param.setResolution(options.resolution);
        }
        return reader.readRaster(0, param);
    }
}




© 2015 - 2024 Weber Informatics LLC | Privacy Policy