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

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

import net.algart.arrays.*;
import net.algart.matrices.tiff.tags.TagCompression;
import net.algart.matrices.tiff.tags.TagPhotometricInterpretation;
import net.algart.matrices.tiff.tags.TagRational;
import net.algart.matrices.tiff.tags.Tags;
import net.algart.matrices.tiff.tiles.TiffMap;
import net.algart.matrices.tiff.tiles.TiffTile;
import org.scijava.Context;
import org.scijava.io.handle.BytesHandle;
import org.scijava.io.handle.DataHandle;
import org.scijava.io.handle.FileHandle;
import org.scijava.io.location.BytesLocation;
import org.scijava.io.location.FileLocation;
import org.scijava.io.location.Location;
import org.scijava.util.Bytes;

import java.io.FileNotFoundException;
import java.nio.ByteBuffer;
import java.nio.ByteOrder;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.Arrays;
import java.util.Objects;
import java.util.concurrent.atomic.AtomicBoolean;
import java.util.function.Supplier;
import java.util.stream.Collectors;

/**
 * Utility methods for working with TIFF files.
 *
 * @author Daniel Alievsky
 */
public class TiffTools {
    /**
     * The number of bytes in each IFD entry.
     */
    public static final int BYTES_PER_ENTRY = 12;
    /**
     * The number of bytes in each IFD entry of a BigTIFF file.
     */
    public static final int BIG_TIFF_BYTES_PER_ENTRY = 20;
    // TIFF header constants
    public static final int FILE_USUAL_MAGIC_NUMBER = 42;
    public static final int FILE_BIG_TIFF_MAGIC_NUMBER = 43;
    public static final int FILE_PREFIX_LITTLE_ENDIAN = 0x49;
    public static final int FILE_PREFIX_BIG_ENDIAN = 0x4d;

    private static final boolean OPTIMIZE_SEPARATING_WHOLE_BYTES = true;
    // - should be true for good performance; false value can help while debugging
    static final boolean BUILT_IN_TIMING = getBooleanProperty("net.algart.matrices.tiff.timing");

    private TiffTools() {
    }

    public static TiffSampleType javaArrayToSampleType(Object javaArray, boolean signedIntegers) {
        Objects.requireNonNull(javaArray, "Null Java array");
        Class elementType = javaArray.getClass().getComponentType();
        if (elementType == null) {
            throw new IllegalArgumentException("The specified javaArray is not actual an array: " +
                    "it is " + javaArray.getClass());
        }
        return TiffSampleType.valueOf(elementType, signedIntegers);
    }

    public static UpdatablePArray bytesToArray(byte[] bytes, TiffSampleType sampleType, boolean littleEndian) {
        return (UpdatablePArray) SimpleMemoryModel.asUpdatableArray(bytesToJavaArray(bytes, sampleType, littleEndian));
    }

    public static Object bytesToJavaArray(byte[] bytes, TiffSampleType sampleType, boolean littleEndian) {
        Objects.requireNonNull(bytes, "Null bytes");
        Objects.requireNonNull(sampleType, "Null sampleType");
        switch (sampleType) {
            case INT8, UINT8 -> {
                return bytes;
            }
            case INT16, UINT16 -> {
                return bytesToShortArray(bytes, littleEndian);
            }
            case INT32, UINT32 -> {
                return bytesToIntArray(bytes, littleEndian);
            }
            case FLOAT -> {
                return bytesToFloatArray(bytes, littleEndian);
            }
            case DOUBLE -> {
                return bytesToDoubleArray(bytes, littleEndian);
            }
        }
        throw new IllegalArgumentException("Unknown sample type: " + sampleType);
    }

    public static short[] bytesToShortArray(byte[] samples, boolean littleEndian) {
        final short[] shortValues = new short[samples.length / 2];
        final ByteBuffer bb = ByteBuffer.wrap(samples);
        bb.order(littleEndian ? ByteOrder.LITTLE_ENDIAN : ByteOrder.BIG_ENDIAN);
        bb.asShortBuffer().get(shortValues);
        return shortValues;
    }

    public static int[] bytesToIntArray(byte[] samples, boolean littleEndian) {
        final int[] intValues = new int[samples.length / 4];
        final ByteBuffer bb = ByteBuffer.wrap(samples);
        bb.order(littleEndian ? ByteOrder.LITTLE_ENDIAN : ByteOrder.BIG_ENDIAN);
        bb.asIntBuffer().get(intValues);
        return intValues;
    }

    public static long[] bytesToLongArray(byte[] samples, boolean littleEndian) {
        final long[] longValues = new long[samples.length / 8];
        final ByteBuffer bb = ByteBuffer.wrap(samples);
        bb.order(littleEndian ? ByteOrder.LITTLE_ENDIAN : ByteOrder.BIG_ENDIAN);
        bb.asLongBuffer().get(longValues);
        return longValues;
    }

    public static float[] bytesToFloatArray(byte[] samples, boolean littleEndian) {
        final float[] floatValues = new float[samples.length / 4];
        final ByteBuffer bb = ByteBuffer.wrap(samples);
        bb.order(littleEndian ? ByteOrder.LITTLE_ENDIAN : ByteOrder.BIG_ENDIAN);
        bb.asFloatBuffer().get(floatValues);
        return floatValues;
    }

    public static double[] bytesToDoubleArray(byte[] samples, boolean littleEndian) {
        final double[] doubleValues = new double[samples.length / 8];
        final ByteBuffer bb = ByteBuffer.wrap(samples);
        bb.order(littleEndian ? ByteOrder.LITTLE_ENDIAN : ByteOrder.BIG_ENDIAN);
        bb.asDoubleBuffer().get(doubleValues);
        return doubleValues;
    }

    public static byte[] arrayToBytes(PArray array, boolean littleEndian) {
        Objects.requireNonNull(array, "Null array");
        final Object javaArray =
                array instanceof DirectAccessible da && da.hasJavaArray() && da.javaArrayOffset() == 0 ?
                        da.javaArray() :
                        net.algart.arrays.Arrays.toJavaArray(array);
        return javaArrayToBytes(javaArray, littleEndian);
    }

    public static byte[] javaArrayToBytes(Object javaArray, boolean littleEndian) {
        final TiffSampleType sampleType = javaArrayToSampleType(javaArray, false);
        // - note: signed and unsigned values correspond to the same element types,
        // so, the "signedIntegers" argument is not important
        switch (sampleType) {
            case INT8, UINT8 -> {
                return (byte[]) javaArray;
            }
            case INT16, UINT16 -> {
                final short[] shortValues = (short[]) javaArray;
                final byte[] v = new byte[shortValues.length * 2];
                final ByteBuffer bb = ByteBuffer.wrap(v);
                bb.order(littleEndian ? ByteOrder.LITTLE_ENDIAN : ByteOrder.BIG_ENDIAN);
                bb.asShortBuffer().put(shortValues);
                return v;
            }
            case INT32, UINT32 -> {
                final int[] intValues = (int[]) javaArray;
                final byte[] v = new byte[intValues.length * 4];
                final ByteBuffer bb = ByteBuffer.wrap(v);
                bb.order(littleEndian ? ByteOrder.LITTLE_ENDIAN : ByteOrder.BIG_ENDIAN);
                bb.asIntBuffer().put(intValues);
                return v;
            }
            case FLOAT -> {
                final float[] floatValues = (float[]) javaArray;
                final byte[] v = new byte[floatValues.length * 4];
                final ByteBuffer bb = ByteBuffer.wrap(v);
                bb.order(littleEndian ? ByteOrder.LITTLE_ENDIAN : ByteOrder.BIG_ENDIAN);
                bb.asFloatBuffer().put(floatValues);
                return v;
            }
            case DOUBLE -> {
                final double[] doubleValue = (double[]) javaArray;
                final byte[] v = new byte[doubleValue.length * 8];
                final ByteBuffer bb = ByteBuffer.wrap(v);
                bb.order(littleEndian ? ByteOrder.LITTLE_ENDIAN : ByteOrder.BIG_ENDIAN);
                bb.asDoubleBuffer().put(doubleValue);
                return v;
            }
        }
        throw new AssertionError("(should be already checked in arrayToSampleType)");
    }

    public static Matrix asMatrix(
            Object javaArray,
            int sizeX,
            int sizeY,
            int numberOfChannels,
            boolean interleavedSamples) {
        javaArrayToSampleType(javaArray, false);
        // - checks that javaArray is a array of supported primitive types
        if (sizeX < 0 || sizeY < 0) {
            throw new IllegalArgumentException("Negative sizeX = " + sizeX + " or sizeY = " + sizeY);
        }
        if (numberOfChannels <= 0) {
            throw new IllegalArgumentException("Zero or negative numberOfChannels = " + numberOfChannels);
        }
        return interleavedSamples ?
                SimpleMemoryModel.asMatrix(javaArray, numberOfChannels, sizeX, sizeY) :
                SimpleMemoryModel.asMatrix(javaArray, sizeX, sizeY, numberOfChannels);
    }

    public static byte[] toInterleavedSamples(
            byte[] bytes,
            int numberOfChannels,
            int bytesPerSample,
            int numberOfPixels) {
        Objects.requireNonNull(bytes, "Null bytes");
        if (numberOfChannels <= 0) {
            throw new IllegalArgumentException("Zero or negative numberOfChannels = " + numberOfChannels);
        }
        if (bytesPerSample <= 0) {
            throw new IllegalArgumentException("Zero or negative bytesPerSample = " + bytesPerSample);
        }
        if (numberOfPixels < 0) {
            throw new IllegalArgumentException("Negative numberOfPixels = " + numberOfPixels);
        }
        final int size = checkedMulNoException(
                new long[]{numberOfPixels, numberOfChannels, bytesPerSample},
                new String[]{"number of pixels", "bytes per pixel", "bytes per sample"},
                () -> "Invalid sizes: ", () -> "");
        // - exception usually should not occur: this function is typically called after analysing IFD
        if (bytes.length < size) {
            throw new IllegalArgumentException("Too short bytes array: " + bytes.length + " < " + size);
        }
        if (numberOfChannels == 1) {
            return bytes;
        }
        final int bandSize = numberOfPixels * bytesPerSample;
        final byte[] interleavedBytes = new byte[size];
        if (bytesPerSample == 1) {
            Matrix mI = SimpleMemoryModel.asMatrix(interleavedBytes, numberOfChannels, numberOfPixels);
            Matrix mS = SimpleMemoryModel.asMatrix(bytes, numberOfPixels, numberOfChannels);
            Matrices.interleave(null, mI, mS.asLayers());
//            if (numberOfChannels == 3) {
//                quickInterleave3(interleavedBytes, bytes, bandSize);
//            } else {
//                for (int i = 0, disp = 0; i < bandSize; i++) {
//                    for (int bandDisp = i, j = 0; j < numberOfChannels; j++, bandDisp += bandSize) {
                        // note: we must check j, not bandDisp, because "bandDisp += bandSize" can lead to overflow
//                        interleavedBytes[disp++] = bytes[bandDisp];
//                    }
//                }
//            }
        } else {
            for (int i = 0, disp = 0; i < bandSize; i += bytesPerSample) {
                for (int bandDisp = i, j = 0; j < numberOfChannels; j++, bandDisp += bandSize) {
                    for (int k = 0; k < bytesPerSample; k++) {
                        interleavedBytes[disp++] = bytes[bandDisp + k];
                    }
                }
            }
        }
        return interleavedBytes;
    }

    public static byte[] toSeparatedSamples(
            byte[] bytes,
            int numberOfChannels,
            int bytesPerSample,
            int numberOfPixels) {
        Objects.requireNonNull(bytes, "Null bytes");
        if (numberOfChannels <= 0) {
            throw new IllegalArgumentException("Zero or negative numberOfChannels = " + numberOfChannels);
        }
        if (bytesPerSample <= 0) {
            throw new IllegalArgumentException("Zero or negative bytesPerSample = " + bytesPerSample);
        }
        if (numberOfPixels < 0) {
            throw new IllegalArgumentException("Negative numberOfPixels = " + numberOfPixels);
        }
        final int size = checkedMulNoException(
                new long[]{numberOfPixels, numberOfChannels, bytesPerSample},
                new String[]{"number of pixels", "bytes per pixel", "bytes per sample"},
                () -> "Invalid sizes: ", () -> "");
        // - exception usually should not occur: this function is typically called after analysing IFD
        if (bytes.length < size) {
            throw new IllegalArgumentException("Too short bytes array: " + bytes.length + " < " + size);
        }
        if (numberOfChannels == 1) {
            return bytes;
        }
        final int bandSize = numberOfPixels * bytesPerSample;
        final byte[] separatedBytes = new byte[size];
        if (bytesPerSample == 1) {
            Matrix mI = SimpleMemoryModel.asMatrix(bytes, numberOfChannels, numberOfPixels);
            Matrix mS = SimpleMemoryModel.asMatrix(separatedBytes, numberOfPixels, numberOfChannels);
            Matrices.separate(null, mS.asLayers(), mI);
//            if (numberOfChannels == 3) {
//                quickSeparate3(separatedBytes, bytes, bandSize);
//            } else {
//                for (int i = 0, disp = 0; i < bandSize; i++) {
//                    for (int bandDisp = i, j = 0; j < numberOfChannels; j++, bandDisp += bandSize) {
                        // note: we must check j, not bandDisp, because "bandDisp += bandSize" can lead to overflow
//                        separatedBytes[bandDisp] = bytes[disp++];
//                    }
//                }
//            }
        } else {
            for (int i = 0, disp = 0; i < bandSize; i += bytesPerSample) {
                for (int bandDisp = i, j = 0; j < numberOfChannels; j++, bandDisp += bandSize) {
                    for (int k = 0; k < bytesPerSample; k++) {
                        separatedBytes[bandDisp + k] = bytes[disp++];
                    }
                }
            }
        }
        return separatedBytes;
    }

    /**
     * Changes bits order inside the tile if FillOrder=2.
     *
     * @param tile TIFF tile
     * @throws TiffException in a case of error in IFD
     */
    public static void reverseFillOrderIfRequested(TiffTile tile) throws TiffException {
        Objects.requireNonNull(tile, "Null tile");
        if (tile.ifd().isReversedFillOrder()) {
            PackedBitArraysPer8.reverseBitOrder(tile.getData());
        }
    }

    // Analog of the function DefaultTiffService.undifference
    public static void subtractPredictionIfRequested(TiffTile tile) throws TiffException {
        Objects.requireNonNull(tile, "Null tile");
        final byte[] data = tile.getDecodedData();
        final int predictor = tile.ifd().getInt(Tags.PREDICTOR, TiffIFD.PREDICTOR_NONE);
        if (predictor == TiffIFD.PREDICTOR_HORIZONTAL) {
            final boolean little = tile.isLittleEndian();
            final int bytes = tile.bytesPerSample();
            final int len = tile.bytesPerPixel();
            final long xSize = tile.getSizeX();
            final long xSizeInBytes = xSize * len;

            int k = data.length - bytes;
            long xOffset = k % xSizeInBytes;

            if (bytes == 1) {
                for (; k >= 0; k--, xOffset--) {
                    if (xOffset < 0) {
                        xOffset += xSizeInBytes;
                    }
//                    assert (k / len % xSize == 0) == (xOffset < len);
                    if (xOffset >= len) {
                        data[k] -= data[k - len];
                    }
                }
            } else {
                for (; k >= 0; k -= bytes) {
                    if (k / len % xSize == 0) {
                        continue;
                    }
                    int value = Bytes.toInt(data, k, bytes, little);
                    value -= Bytes.toInt(data, k - len, bytes, little);
                    Bytes.unpack(value, data, k, bytes, little);
                }
            }
        } else if (predictor != TiffIFD.PREDICTOR_NONE) {
            throw new TiffException("Unsupported TIFF Predictor tag: " + predictor);
        }
    }

    // Analog of the function DefaultTiffService.difference
    public static void unsubtractPredictionIfRequested(TiffTile tile) throws TiffException {
        Objects.requireNonNull(tile, "Null tile");
        final byte[] data = tile.getDecodedData();
        final int predictor = tile.ifd().getInt(Tags.PREDICTOR, TiffIFD.PREDICTOR_NONE);
        if (predictor == TiffIFD.PREDICTOR_HORIZONTAL) {
            final boolean little = tile.isLittleEndian();
            final int bytes = tile.bytesPerSample();
            final int len = tile.bytesPerPixel();
            final long xSize = tile.getSizeX();
            final long xSizeInBytes = xSize * len;

            int k = 0;
            long xOffset = 0;

            if (bytes == 1) {
                for (; k <= data.length - 1; k++, xOffset++) {
                    if (xOffset == xSizeInBytes) {
                        xOffset = 0;
                    }
//                    assert (k / len % xSize == 0) == (xOffset < len);
                    if (xOffset >= len) {
                        data[k] += data[k - len];
                    }
                }
            } else {
                for (; k <= data.length - bytes; k += bytes) {
                    if (k / len % xSize == 0) {
                        continue;
                    }
                    int value = Bytes.toInt(data, k, bytes, little);
                    value += Bytes.toInt(data, k - len, bytes, little);
                    Bytes.unpack(value, data, k, bytes, little);
                }
            }
        } else if (predictor != TiffIFD.PREDICTOR_NONE) {
            throw new TiffException("Unsupported TIFF Predictor tag: " + predictor);
        }
    }

    public static boolean separateUnpackedSamples(TiffTile tile) throws TiffException {
        Objects.requireNonNull(tile);
        final TiffIFD ifd = tile.ifd();

        AtomicBoolean simpleLossless = new AtomicBoolean();
        if (!isSimpleRearrangingBytesEnough(ifd, simpleLossless)) {
            return false;
        }
        if (!OPTIMIZE_SEPARATING_WHOLE_BYTES && tile.isInterleaved()) {
            // - if tile is not interleaved, we MUST use this method:
            // separateBitsAndInvertValues does not "understand" this situation
            return false;
        }

        // We have equal number N of bits/sample for all samples,
        // N % 8 == 0, N / 8 is 1, 2, 3, 4 or 8;
        // for all other cases isSimpleRearrangingBytesEnough returns false.
        // The only unusual case here is 3 bytes/sample: 24-bit float or 24-bit integer;
        // these cases (as well as 16-bit float) will be processed later in unpackUnusualPrecisions.
        if (tile.getStoredDataLength() > tile.map().tileSizeInBytes() && !simpleLossless.get()) {
            // - Strange situation: JPEG or some extended codec has decoded to large tile.
            // But for "simple" compressions (uncompressed, Deflate) we enable this situation:
            // it helps to create special tests (for example, an image with "fake" too little dimensions).
            // (Note: for LZW compression, in current version, this situation is impossible, because
            // LZWCodec creates result array on the base of options.maxBytes field.)
            // In comparison, further adjustNumberOfPixels throws IllegalArgumentException
            // in the same situation, when its argument is false.
            throw new TiffException("Too large decoded TIFF data: " + tile.getStoredDataLength() +
                    " bytes, its is greater than one " +
                    (tile.map().isTiled() ? "tile" : "strip") + " (" + tile.map().tileSizeInBytes() + " bytes); "
                    + "probably TIFF file is corrupted or format is not properly supported");
        }
        tile.adjustNumberOfPixels(true);
        // - Note: getStoredDataLength() is unpredictable, because it is the result of decompression by a codec;
        // in particular, for JPEG compression last strip in non-tiled TIFF may be shorter or even larger
        // than a full tile.
        // If cropping boundary tiles is enabled, actual height of the last strip is reduced
        // (see readEncodedTile method), so larger data is possible (it is a minor format separately).
        // If cropping boundary tiles is disabled, larger data MAY be considered as a format error,
        // because tile sizes are the FULL sizes of tile in the grid (it is checked above independently).
        // We consider this situation as an error in a case of "complex" codecs like JPEG, JPEG-2000 etc.
        // Also note: it is better to rearrange pixels before separating (if necessary),
        // because rearranging interleaved pixels is little more simple.
        tile.separateSamplesIfNecessary();

        // Note: other 2 functions, unpackUnusualBits and decodeYCbCr, always work with interleaved
        // data and separate data themselves to a tile with correct sizes
        return true;

    }

    // Note: this method may be tested with the images ycbcr-cat.tif and dscf0013.tif
    public static boolean separateYCbCrToRGB(TiffTile tile) throws TiffException {
        Objects.requireNonNull(tile);
        final TiffIFD ifd = tile.ifd();

        if (!ifd.isStandardYCbCrNonJpeg()) {
            return false;
        }
        checkInterleaved(tile);
        final byte[] data = tile.getDecodedData();

        final TiffMap map = tile.map();
        if (map.isPlanarSeparated()) {
            // - there is no simple way to support this exotic situation: Cb/Cr values are saved in other tiles
            throw new UnsupportedTiffFormatException("TIFF YCbCr photometric interpretation is not supported " +
                    "in planar format (separated component planes: TIFF tag PlanarConfiguration=2)");
        }

        final int bits = ifd.tryEqualBitDepth().orElse(-1);
        if (bits != 8) {
            throw new UnsupportedTiffFormatException("Cannot unpack YCbCr TIFF image with " +
                    Arrays.toString(ifd.getBitsPerSample()) +
                    " bits per samples: only [8, 8, 8] variant is supported");
        }

        if (map.tileSamplesPerPixel() != 3) {
            throw new TiffException("TIFF YCbCr photometric interpretation requires 3 channels, but " +
                    "there are " + map.tileSamplesPerPixel() + " channels in SamplesPerPixel TIFF tag");
        }

        final int sizeX = tile.getSizeX();
        final int sizeY = tile.getSizeY();
        final int numberOfPixels = tile.getSizeInPixels();

        final byte[] unpacked = new byte[3 * numberOfPixels];

        // unpack pixels
        // set up YCbCr-specific values
        double lumaRed = 0.299;
        double lumaGreen = 0.587;
        double lumaBlue = 0.114;
        int[] reference = ifd.getIntArray(Tags.REFERENCE_BLACK_WHITE);
        if (reference == null) {
            reference = new int[]{0, 255, 128, 255, 128, 255};
            // - original SCIFIO code used here zero-filled array, this is incorrect
        }
        final int[] subsamplingLog = ifd.getYCbCrSubsamplingLogarithms();
        final TagRational[] coefficients = ifd.getValue(Tags.Y_CB_CR_COEFFICIENTS, TagRational[].class)
                .orElse(new TagRational[0]);
        if (coefficients.length >= 3) {
            lumaRed = coefficients[0].doubleValue();
            lumaGreen = coefficients[1].doubleValue();
            lumaBlue = coefficients[2].doubleValue();
        }
        final double crCoefficient = 2.0 - 2.0 * lumaRed;
        final double cbCoefficient = 2.0 - 2.0 * lumaBlue;
        final double lumaGreenInv = 1.0 / lumaGreen;
        final int subXLog = subsamplingLog[0];
        final int subYLog = subsamplingLog[1];
        final int subXMinus1 = (1 << subXLog) - 1;
        final int blockLog = subXLog + subYLog;
        final int block = 1 << blockLog;
        final int numberOfXBlocks = (sizeX + subXMinus1) >>> subXLog;
        for (int yIndex = 0, i = 0; yIndex < sizeY; yIndex++) {
            final int yBlockIndex = yIndex >>> subYLog;
            final int yAligned = yBlockIndex << subYLog;
            final int lineAligned = yBlockIndex * numberOfXBlocks;
            for (int xIndex = 0; xIndex < sizeX; xIndex++, i++) {
                // unpack non-YCbCr samples
                // unpack YCbCr unpacked; these need special handling, as
                // each of the RGB components depends upon two or more of the YCbCr
                // components
                final int blockIndex = i >>> blockLog; // = i / block
                final int aligned = blockIndex << blockLog;
                final int indexInBlock = i - aligned;
                assert indexInBlock < block;
                final int blockIndexTwice = 2 * blockIndex; // 1 block for Cb + 1 block for Cr
                final int blockStart = aligned + blockIndexTwice;
                final int lumaIndex = blockStart + indexInBlock;
                final int chromaIndex = blockStart + block;
                // Every block contains block Luma (Y) values, 1 Cb value and 1 Cr value, for example (2x2):
                //    YYYYbrYYYYbrYYYYbr...
                //                |"blockStart" position

                if (chromaIndex + 1 >= data.length) {
                    break;
                }

                final int yIndexInBlock = indexInBlock >>> subXLog;
                final int xIndexInBlock = indexInBlock - (yIndexInBlock << subXLog);
                final int resultYIndex = yAligned + yIndexInBlock;
                final int resultXIndex = ((blockIndex - lineAligned) << subXLog) + xIndexInBlock;
                if (TiffReader.THOROUGHLY_TEST_Y_CB_CR_LOOP) {
                    final int subX = 1 << subXLog;
                    final int subY = 1 << subYLog;
                    final int t = i / block;
                    final int pixel = i % block;
                    final long r = (long) subY * (t / numberOfXBlocks) + (pixel / subX);
                    final long c = (long) subX * (t % numberOfXBlocks) + (pixel % subX);
                    // - these formulas were used in original SCIFIO code
                    assert t / numberOfXBlocks == yBlockIndex;
                    assert t % numberOfXBlocks == blockIndex - lineAligned;
                    assert c == resultXIndex;
                    assert r == resultYIndex;
                }
                if (resultXIndex >= sizeX || resultYIndex >= sizeY) {
                    // - for a case when the sizes are not aligned by subX/subY
                    continue;
                }

                final int resultIndex = resultYIndex * sizeX + resultXIndex;
                final int y = (data[lumaIndex] & 0xff) - reference[0];
                final int cb = (data[chromaIndex] & 0xff) - reference[2];
                final int cr = (data[chromaIndex + 1] & 0xff) - reference[4];

                final double red = cr * crCoefficient + y;
                final double blue = cb * cbCoefficient + y;
                final double green = (y - lumaBlue * blue - lumaRed * red) * lumaGreenInv;

                unpacked[resultIndex] = (byte) toUnsignedByte(red);
                unpacked[numberOfPixels + resultIndex] = (byte) toUnsignedByte(green);
                unpacked[2 * numberOfPixels + resultIndex] = (byte) toUnsignedByte(blue);
            }
            while ((i & subXMinus1) != 0) {
                i++;
                // - horizontal alignment
            }
        }

        tile.setDecodedData(unpacked);
        tile.setInterleaved(false);
        return true;
    }

    public static boolean separateBitsAndInvertValues(
            TiffTile tile,
            boolean scaleWhenIncreasingBitDepth,
            boolean correctInvertedBrightness)
            throws TiffException {
        Objects.requireNonNull(tile);
        final TiffIFD ifd = tile.ifd();

        if (OPTIMIZE_SEPARATING_WHOLE_BYTES && isSimpleRearrangingBytesEnough(ifd, null)) {
            return false;
        }
        if (ifd.isStandardYCbCrNonJpeg()) {
            return false;
        }
        checkInterleaved(tile);
        if (!ifd.isStandardCompression() || ifd.isJpeg()) {
            throw new IllegalStateException("Corrupted IFD, probably by direct modifications (" +
                    "non-standard/JPEG compression, though it was already checked)");
            // - was checked in isSimpleRearrangingBytesEnough
        }
        final TagPhotometricInterpretation photometricInterpretation = ifd.getPhotometricInterpretation();
        if (photometricInterpretation.isIndexed() ||
                photometricInterpretation == TagPhotometricInterpretation.TRANSPARENCY_MASK) {
            scaleWhenIncreasingBitDepth = false;
        }
        final boolean invertedBrightness = photometricInterpretation.isInvertedBrightness();
        if (tile.sampleType().isFloatingPoint() && OPTIMIZE_SEPARATING_WHOLE_BYTES) {
            // - TIFF with float/double samples must not require bit unpacking or inverting brightness
            throw new TiffException("Invalid TIFF image: floating-point values, compression \"" +
                    ifd.compressionPrettyName() + "\", photometric interpretation \"" +
                    photometricInterpretation.prettyName() + "\", " +
                    Arrays.toString(ifd.getBitsPerSample()) + " bits per sample");
        }

        final int bytesPerSample = tile.bytesPerSample();
        if (bytesPerSample > 4) {
            throw new IllegalStateException("Corrupted IFD, probably by direct modifications (" +
                    bytesPerSample + " bytes/sample in tile, though it was already checked)");
            // - was checked in isSimpleRearrangingBytesEnough
        }
        // The only non-standard bytesPerSample is 3: 17..24-bit integer (but not all bits/sample are 24);
        // we must complete such samples to 24 bits, and they will be processed later in unpackUnusualPrecisions
        final int[] bitsPerSample = ifd.getBitsPerSample();
        final boolean byteAligned = Arrays.stream(bitsPerSample).noneMatch(bits -> (bits & 7) != 0);
        if (byteAligned && !invertedBrightness && OPTIMIZE_SEPARATING_WHOLE_BYTES) {
            throw new IllegalStateException("Corrupted IFD, probably by a parallel thread " +
                    "(BitsPerSample tag is byte-aligned and inversion is not necessary, " +
                    "though it was already checked)");
            // - was checked in isSimpleRearrangingBytesEnough; other case,
            // when we have DIFFERENT number of bytes, must be checked while creating TiffMap
        }

        final int samplesPerPixel = tile.samplesPerPixel();
        if (samplesPerPixel > bitsPerSample.length)
            throw new IllegalStateException("Corrupted IFD, probably by direct modifications (" +
                    samplesPerPixel + " samples/pixel is greater than the length of BitsPerSample tag; " +
                    "it is possible only for OLD_JPEG, that was already checked)");
        // - but samplesPerPixel can be =1 for planar-separated tiles

        final int sizeX = tile.getSizeX();
        final int sizeY = tile.getSizeY();
        final int resultSamplesLength = tile.getSizeInBytes();
        final int numberOfPixels = tile.getSizeInPixels();
        assert numberOfPixels == sizeX * sizeY;

        final boolean littleEndian = ifd.isLittleEndian();

//        debugPrintBits(tile);
        final byte[] data = tile.getDecodedData();
        final BitsUnpacker bitsUnpacker = BitsUnpacker.getUnpackerHighBitFirst(data);
        final byte[] unpacked = new byte[resultSamplesLength];

        final long[] multipliers = new long[bitsPerSample.length];
        for (int k = 0; k < multipliers.length; k++) {
            multipliers[k] = ((1L << 8 * bytesPerSample) - 1) / ((1L << bitsPerSample[k]) - 1);
            // - note that 2^n-1 is divisible by 2^m-1 when m < n; actually it is less or about 256
        }
        MainLoop:
        for (int yIndex = 0, i = 0; yIndex < sizeY; yIndex++) {
            for (int xIndex = 0; xIndex < sizeX; xIndex++, i++) {
                for (int s = 0; s < samplesPerPixel; s++) {
                    final int bits = bitsPerSample[s];
                    final long maxValue = (1L << bits) - 1;
                    // - we need long type, because maximal number of bits here is 32
                    // (but if not ALL bits/sample are 32 - such cases do not require this method)
                    final int outputIndex = (s * numberOfPixels + i) * bytesPerSample;

                    long value;
                    if (byteAligned) {
                        final int index = (i * samplesPerPixel + s) * bytesPerSample;
                        value = Bytes.toLong(data, index, bytesPerSample, littleEndian);
                        // - It is strange, but it is a fact:
                        //      for byte-aligned pixels (for example, 16 or 24 bits/sample),
                        // the byte order (little-endian or big-endian) is important;
                        //      for unaligned samples (for example, 12, 18 or 26 bits/sample),
                        // the byte order is ignored and actually is always big-endian,
                        // and also the bit order inside bytes is always big-endian
                        // (after applying possible bit inversion by invertFillOrderIfRequested,
                        // but this inversion is performed in the very beginning,
                        // even before decoding LZW/Deflate etc.)
                    } else {
                        if (bitsUnpacker.isEof()) {
                            break MainLoop;
                        }
                        value = bitsUnpacker.getBits(bits) & 0xFFFFFFFFL;
                        // - unsigned 32-bit value
                    }
                    if (correctInvertedBrightness && invertedBrightness) {
                        value = maxValue - value;
                    }
                    if (scaleWhenIncreasingBitDepth) {
                        value *= multipliers[s];
                    }
                    assert outputIndex + bytesPerSample <= unpacked.length;
                    Bytes.unpack(value, unpacked, outputIndex, bytesPerSample, littleEndian);
                }
            }
            bitsUnpacker.skipBitsUntilNextByte();
        }
        tile.setDecodedData(unpacked);
        tile.setInterleaved(false);
        return true;
    }

    // Note: we prefer to pass numberOfChannels directly, not calculate it on the base of IFD,
    // because in some cases (like processing tile) number of channels should be set to 1 for planar IFD,
    // but in other cases (like processing whole image) it is not so.
    // Note: this method CHANGES the number of bytes/sample.
    public static byte[] unpackUnusualPrecisions(
            final byte[] samples,
            final TiffIFD ifd,
            final int numberOfChannels,
            final int numberOfPixels,
            boolean scaleUnsignedInt24) throws TiffException {
        Objects.requireNonNull(samples, "Null samples");
        Objects.requireNonNull(ifd, "Null IFD");
        if (numberOfChannels <= 0) {
            throw new IllegalArgumentException("Zero or negative numberOfChannels = " + numberOfChannels);
        }
        if (numberOfPixels < 0) {
            throw new IllegalArgumentException("Negative numberOfPixels = " + numberOfPixels);
        }
        final int packedBytesPerSample = ifd.equalBytesPerSample();
        final TiffSampleType sampleType = ifd.sampleType();
        final boolean floatingPoint = sampleType.isFloatingPoint();
        // - actually DOUBLE is not used below
        final int bitsPerSample = ifd.tryEqualBitDepth().orElse(-1);
        final boolean float16 = bitsPerSample == 16 && floatingPoint;
        final boolean float24 = bitsPerSample == 24 && floatingPoint;
        final boolean int24 = packedBytesPerSample == 3 && !float24;
        // If the data were correctly loaded into a tile with usage TiffReader.completeDecoding method, then:
        // 1) they are aligned by 8-bit (by unpackBitsAndInvertValues method);
        // 2) number of bytes per sample may be only 1..4 or 8: other cases are rejected by unpackBitsAndInvertValues.
        // So, we only need to check a case 3 bytes (17..24 bits before correction) and special cases float16/float24.
        if (!float16 && !float24 && !int24) {
            return samples;
        }
        // Following code is necessary in a very rare case, and no sense to seriously optimize it
        final boolean littleEndian = ifd.isLittleEndian();

        final int size = checkedMul(new long[]{numberOfPixels, numberOfChannels, 4},
                new String[]{"number of pixels", "number of channels", "4 bytes per float"},
                () -> "Invalid sizes: ", () -> "");
        final int numberOfSamples = numberOfChannels * numberOfPixels;
        if (samples.length < numberOfSamples * packedBytesPerSample) {
            throw new IllegalArgumentException("Too short samples array byte[" + samples.length +
                    "]: it does not contain " + numberOfPixels + " pixels per " + numberOfChannels +
                    " samples, " + packedBytesPerSample + " bytes/sample");
        }
        final byte[] unpacked = new byte[size];
        if (int24) {
            for (int i = 0, disp = 0; i < numberOfSamples; i++, disp += packedBytesPerSample) {
                // - very rare case, no sense to optimize
                final int value = Bytes.toInt(samples, disp, packedBytesPerSample, littleEndian);
                final long newValue = scaleUnsignedInt24 ? (long) value << 8 : value;
                Bytes.unpack(newValue, unpacked, i * 4, 4, littleEndian);
            }
            return unpacked;
        }

//        final int mantissaBits = float16 ? 10 : 16;
//        final int exponentBits = float16 ? 5 : 7;
        for (int i = 0, disp = 0; i < numberOfSamples; i++, disp += packedBytesPerSample) {
            final int packedValue = Bytes.toInt(samples, disp, packedBytesPerSample, littleEndian);
//            final int valueToCompare = unpackFloatBits(packedValue, mantissaBits, exponentBits);
            final int value = float16 ?
                    unpack16BitFloat((short) packedValue) :
                    unpack24BitFloat(packedValue);
//            if (value != valueToCompare) {
//                System.out.printf("%h %f != %h %f%n", value, Float.intBitsToFloat(value),
//                        valueToCompare, Float.intBitsToFloat(valueToCompare));
//            }
            Bytes.unpack(value, unpacked, i * 4, 4, littleEndian);
        }
        return unpacked;
    }

    public static String escapeJsonString(CharSequence string) {
        final StringBuilder result = new StringBuilder();
        escapeJsonString(result, string);
        return result.toString();
    }

    // Clone of the method JsonGeneratorImpl.writeEscapedString
    public static void escapeJsonString(StringBuilder result, CharSequence string) {
        int len = string.length();
        for (int i = 0; i < len; i++) {
            int begin = i;
            int end = i;
            char c = string.charAt(i);
            // find all the characters that need not be escaped
            // unescaped = %x20-21 | %x23-5B | %x5D-10FFFF
            while (c >= 0x20 && c != 0x22 && c != 0x5c) {
                i++;
                end = i;
                if (i < len) {
                    c = string.charAt(i);
                } else {
                    break;
                }
            }
            // Write characters without escaping
            if (begin < end) {
                result.append(string, begin, end);
                if (i == len) {
                    break;
                }
            }

            switch (c) {
                case '"':
                case '\\':
                    result.append('\\');
                    result.append(c);
                    break;
                case '\b':
                    result.append('\\');
                    result.append('b');
                    break;
                case '\f':
                    result.append('\\');
                    result.append('f');
                    break;
                case '\n':
                    result.append('\\');
                    result.append('n');
                    break;
                case '\r':
                    result.append('\\');
                    result.append('r');
                    break;
                case '\t':
                    result.append('\\');
                    result.append('t');
                    break;
                default:
                    String hex = "000" + Integer.toHexString(c);
                    result.append("\\u").append(hex.substring(hex.length() - 4));
            }
        }
    }

    public static DataHandle getExistingFileHandle(Path file) throws FileNotFoundException {
        if (!Files.isRegularFile(file)) {
            throw new FileNotFoundException("File " + file
                    + (Files.exists(file) ? " is not a regular file" : " does not exist"));
        }
        return getFileHandle(file);
    }

    public static void checkRequestedArea(long fromX, long fromY, long sizeX, long sizeY) {
        if (sizeX < 0 || sizeY < 0) {
            throw new IllegalArgumentException("Negative sizeX = " + sizeX + " or sizeY = " + sizeY);
        }
        if (fromX != (int) fromX || fromY != (int) fromY) {
            throw new IllegalArgumentException("Too large absolute values of fromX = " + fromX +
                    " or fromY = " + fromY + " (out of -2^31..2^31-1 ranges)");
        }
        if (sizeX > Integer.MAX_VALUE || sizeY > Integer.MAX_VALUE) {
            throw new IllegalArgumentException("Too large sizeX = " + sizeX + " or sizeY = " + sizeY + " (>2^31-1)");
        }
        if (sizeX >= Integer.MAX_VALUE - fromX || sizeY >= Integer.MAX_VALUE - fromY) {
            // - Note: >= instead of > ! This allows to use "toX = fromX + sizeX" without overflow
            throw new IllegalArgumentException("Requested area [" + fromX + ".." + (fromX + sizeX - 1) +
                    " x " + fromY + ".." + (fromY + sizeY - 1) + " is out of 0..2^31-2 ranges");
        }
    }

    public static void checkRequestedAreaInArray(byte[] array, long sizeX, long sizeY, int bytesPerPixel) {
        Objects.requireNonNull(array, "Null array");
        checkRequestedAreaInArray(array.length, sizeX, sizeY, bytesPerPixel);
    }

    public static void checkRequestedAreaInArray(int arrayLength, long sizeX, long sizeY, int pixelLength) {
        if (arrayLength < 0) {
            throw new IllegalArgumentException("Negative arrayLength = " + arrayLength);
        }
        if (pixelLength <= 0) {
            throw new IllegalArgumentException("Zero or negative pixelLength = " + pixelLength);
        }
        checkRequestedArea(0, 0, sizeX, sizeY);
        if (sizeX * sizeY > arrayLength || sizeX * sizeY * (long) pixelLength > arrayLength) {
            throw new IllegalArgumentException("Requested area " + sizeX + "x" + sizeY +
                    " is too large for array of " + arrayLength + " elements, " + pixelLength + " per pixel");
        }
    }

    public static Context newSCIFIOContext() {
        return SCIFIOBridge.getDefaultScifioContext();
    }

    // Almost exact copy of old TiffParser.unpackBytes method
    /*
    static void unpackBytesLegacy(
            final byte[] samples, final int startIndex,
            final byte[] bytes, final TiffIFD ifd) throws TiffException {
        final boolean planar = ifd.getPlanarConfiguration() == 2;

        final TiffCompression compression = ifd.getCompression();
        TiffPhotometricInterpretation photometricInterpretation = ifd.getPhotometricInterpretation();
        if (compression == TiffCompression.JPEG) {
            photometricInterpretation = TiffPhotometricInterpretation.RGB;
        }

        final int[] bitsPerSample = ifd.getBitsPerSample();
        int nChannels = bitsPerSample.length;

        int sampleCount = (int) (((long) 8 * bytes.length) / bitsPerSample[0]);
        //!! It is a bug! This formula is invalid in the case skipBits!=0
        if (photometricInterpretation == TiffPhotometricInterpretation.Y_CB_CR) sampleCount *= 3;
        if (planar) {
            nChannels = 1;
        } else {
            sampleCount /= nChannels;
        }

//        log.trace("unpacking " + sampleCount + " samples (startIndex=" +
//                startIndex + "; totalBits=" + (nChannels * bitsPerSample[0]) +
//                "; numBytes=" + bytes.length + ")");

        final long imageWidth = ifd.getImageDimX();
        final long imageHeight = ifd.getImageDimY();

        final int bps0 = bitsPerSample[0];
        final int numBytes = ifd.getBytesPerSample()[0];
        final int nSamples = samples.length / (nChannels * numBytes);

        final boolean noDiv8 = bps0 % 8 != 0;
        final boolean bps8 = bps0 == 8;
        final boolean bps16 = bps0 == 16;

        final boolean littleEndian = ifd.isLittleEndian();

        final io.scif.codec.BitBuffer bb = new io.scif.codec.BitBuffer(bytes);

        // Hyper optimisation that takes any 8-bit or 16-bit data, where there
        // is
        // only one channel, the source byte buffer's size is less than or equal
        // to
        // that of the destination buffer and for which no special unpacking is
        // required and performs a simple array copy. Over the course of reading
        // semi-large datasets this can save **billions** of method calls.
        // Wed Aug 5 19:04:59 BST 2009
        // Chris Allan 
        if ((bps8 || bps16) && bytes.length <= samples.length && nChannels == 1 &&
                photometricInterpretation != TiffPhotometricInterpretation.WHITE_IS_ZERO &&
                photometricInterpretation != TiffPhotometricInterpretation.CMYK &&
                photometricInterpretation != TiffPhotometricInterpretation.Y_CB_CR) {
            System.arraycopy(bytes, 0, samples, 0, bytes.length);
            return;
        }

        long maxValue = (long) Math.pow(2, bps0) - 1;
        if (photometricInterpretation == TiffPhotometricInterpretation.CMYK) maxValue = Integer.MAX_VALUE;

        int skipBits = (int) (8 - ((imageWidth * bps0 * nChannels) % 8));
        if (skipBits == 8 || (bytes.length * 8L < bps0 * (nChannels * imageWidth +
                imageHeight))) {
            skipBits = 0;
        }

        // set up YCbCr-specific values
        float lumaRed = PhotoInterp.LUMA_RED;
        float lumaGreen = PhotoInterp.LUMA_GREEN;
        float lumaBlue = PhotoInterp.LUMA_BLUE;
        int[] reference = ifd.getIntArray(IFD.REFERENCE_BLACK_WHITE);
        if (reference == null) {
            reference = new int[]{0, 0, 0, 0, 0, 0};
        }
        final int[] subsampling = ifd.getIntArray(IFD.Y_CB_CR_SUB_SAMPLING);
        final TiffRational[] coefficients = ifd.optValue(
                TiffIFD.Y_CB_CR_COEFFICIENTS, TiffRational[].class).orElse(null);
        if (coefficients != null) {
            lumaRed = coefficients[0].floatValue();
            lumaGreen = coefficients[1].floatValue();
            lumaBlue = coefficients[2].floatValue();
        }
        final int subX = subsampling == null ? 2 : subsampling[0];
        final int subY = subsampling == null ? 2 : subsampling[1];
        final int block = subX * subY;
        final int nTiles = (int) (imageWidth / subX);

        // unpack pixels
        for (int sample = 0; sample < sampleCount; sample++) {
            final int ndx = startIndex + sample;
            if (ndx >= nSamples) break;

            for (int channel = 0; channel < nChannels; channel++) {
                final int index = numBytes * (sample * nChannels + channel);
                final int outputIndex = (channel * nSamples + ndx) * numBytes;

                // unpack non-YCbCr samples
                if (photometricInterpretation != TiffPhotometricInterpretation.Y_CB_CR) {
                    long value = 0;

                    if (noDiv8) {
                        // bits per sample is not a multiple of 8

                        if ((channel == 0 && photometricInterpretation == TiffPhotometricInterpretation.RGB_PALETTE) ||
                                (photometricInterpretation != TiffPhotometricInterpretation.CFA_ARRAY &&
                                        photometricInterpretation != TiffPhotometricInterpretation.RGB_PALETTE)) {
//                            System.out.println((count++) + "/" + nSamples * nChannels);
                            value = bb.getBits(bps0) & 0xffff;
                            if ((ndx % imageWidth) == imageWidth - 1) {
                                bb.skipBits(skipBits);
                            }
                        }
                    } else {
                        value = Bytes.toLong(bytes, index, numBytes, littleEndian);
                    }

                    if (photometricInterpretation == TiffPhotometricInterpretation.WHITE_IS_ZERO ||
                            photometricInterpretation == TiffPhotometricInterpretation.CMYK) {
                        value = maxValue - value;
                    }

                    if (outputIndex + numBytes <= samples.length) {
                        Bytes.unpack(value, samples, outputIndex, numBytes, littleEndian);
                    }
                } else {
                    // unpack YCbCr samples; these need special handling, as
                    // each of
                    // the RGB components depends upon two or more of the YCbCr
                    // components
                    if (channel == nChannels - 1) {
                        final int lumaIndex = sample + (2 * (sample / block));
                        final int chromaIndex = (sample / block) * (block + 2) + block;

                        if (chromaIndex + 1 >= bytes.length) break;

                        final int tile = ndx / block;
                        final int pixel = ndx % block;
                        final long r = (long) subY * (tile / nTiles) + (pixel / subX);
                        final long c = (long) subX * (tile % nTiles) + (pixel % subX);

                        final int idx = (int) (r * imageWidth + c);

                        if (idx < nSamples) {
                            final int y = (bytes[lumaIndex] & 0xff) - reference[0];
                            final int cb = (bytes[chromaIndex] & 0xff) - reference[2];
                            final int cr = (bytes[chromaIndex + 1] & 0xff) - reference[4];

                            final int red = (int) (cr * (2 - 2 * lumaRed) + y);
                            final int blue = (int) (cb * (2 - 2 * lumaBlue) + y);
                            final int green = (int) ((y - lumaBlue * blue - lumaRed * red) /
                                    lumaGreen);

                            samples[idx] = (byte) (red & 0xff);
                            samples[nSamples + idx] = (byte) (green & 0xff);
                            samples[2 * nSamples + idx] = (byte) (blue & 0xff);
                        }
                    }
                }
            }
        }
    }
     */

    static DataHandle getFileHandle(Path file) {
        Objects.requireNonNull(file, "Null file");
        return getFileHandle(new FileLocation(file.toFile()));
    }

    /**
     * Warning: you should never call {@link DataHandle#set(Object)} method of the returned result!
     * It can lead to unpredictable ClassCastException.
     */
    @SuppressWarnings("rawtypes, unchecked")
    static DataHandle getFileHandle(FileLocation fileLocation) {
        Objects.requireNonNull(fileLocation, "Null fileLocation");
        FileHandle fileHandle = new FileHandle(fileLocation);
        fileHandle.setLittleEndian(false);
        // - in current implementation it is an extra operator: BigEndian is default in scijava;
        // but we want to be sure that this behaviour will be the same in all future versions
        return (DataHandle) fileHandle;
    }

    /**
     * Warning: you should never call {@link DataHandle#set(Object)} method of the returned result!
     * It can lead to unpredictable ClassCastException.
     */
    @SuppressWarnings("rawtypes, unchecked")
    static DataHandle getBytesHandle(BytesLocation bytesLocation) {
        Objects.requireNonNull(bytesLocation, "Null bytesLocation");
        BytesHandle bytesHandle = new BytesHandle(bytesLocation);
        return (DataHandle) bytesHandle;
    }

    static int checkedMul(
            long v1, long v2, long v3,
            String n1, String n2, String n3,
            Supplier prefix,
            Supplier postfix) throws TiffException {
        return checkedMul(new long[]{v1, v2, v3}, new String[]{n1, n2, n3}, prefix, postfix);
    }

    static int checkedMul(
            long v1, long v2, long v3, long v4,
            String n1, String n2, String n3, String n4,
            Supplier prefix,
            Supplier postfix) throws TiffException {
        return checkedMul(new long[]{v1, v2, v3, v4}, new String[]{n1, n2, n3, n4}, prefix, postfix);
    }

    static int checkedMulNoException(
            long[] values,
            String[] names,
            Supplier prefix,
            Supplier postfix) {
        try {
            return checkedMul(values, names, prefix, postfix);
        } catch (TiffException e) {
            throw new IllegalArgumentException(e.getMessage());
        }
    }

    static int checkedMul(
            long[] values,
            String[] names,
            Supplier prefix,
            Supplier postfix) throws TiffException {
        Objects.requireNonNull(values);
        Objects.requireNonNull(prefix);
        Objects.requireNonNull(postfix);
        Objects.requireNonNull(names);
        if (values.length == 0) {
            return 1;
        }
        long result = 1L;
        double product = 1.0;
        boolean overflow = false;
        for (int i = 0; i < values.length; i++) {
            long m = values[i];
            if (m < 0) {
                throw new TiffException(prefix.get() + "negative " + names[i] + " = " + m + postfix.get());
            }
            result *= m;
            product *= m;
            if (result > Integer.MAX_VALUE) {
                overflow = true;
                // - we just indicate this, but still calculate the floating-point product
            }
        }
        if (overflow) {
            throw new TiffException(prefix.get() + "too large " + String.join(" * ", names) +
                    " = " + Arrays.stream(values).mapToObj(String::valueOf).collect(
                    Collectors.joining(" * ")) +
                    " = " + product + " >= 2^31" + postfix.get());
        }
        return (int) result;
    }

    static boolean getBooleanProperty(String propertyName) {
        try {
            return Boolean.getBoolean(propertyName);
        } catch (Exception e) {
            return false;
        }
    }

    private static void debugPrintBits(TiffTile tile) throws TiffException {
        if (tile.index().yIndex() != 0) {
            return;
        }
        final byte[] data = tile.getDecodedData();
        final int sizeX = tile.getSizeX();
        final int[] bitsPerSample = tile.ifd().getBitsPerSample();
        final int samplesPerPixel = tile.samplesPerPixel();
        System.out.printf("%nPacked bits %s:%n", Arrays.toString(bitsPerSample));
        for (int i = 0, bit = 0; i < sizeX; i++) {
            System.out.printf("Pixel #%d: ", i);
            for (int s = 0; s < samplesPerPixel; s++) {
                final int bits = bitsPerSample[s];
                int v = 0;
                for (int j = 0; j < bits; j++, bit++) {
                    final int bitIndex = 7 - bit % 8;
                    int b = (data[bit / 8] >> bitIndex) & 1;
                    System.out.print(b);
                    v |= b << (bits - 1 - j);
                }
                System.out.printf(" = %-6d ", v);
            }
            System.out.println();
        }
    }

    /*
    // Unnecessary since AlgART 1.4.0
    private static void quickInterleave3(byte[] interleavedBytes, byte[] bytes, int bandSize) {
        final int bandSize2 = 2 * bandSize;
        for (int i = 0, disp = 0; i < bandSize; i++) {
            interleavedBytes[disp++] = bytes[i];
            interleavedBytes[disp++] = bytes[i + bandSize];
            interleavedBytes[disp++] = bytes[i + bandSize2];
        }
    }

    private static void quickSeparate3(byte[] separatedBytes, byte[] bytes, int bandSize) {
        final int bandSize2 = 2 * bandSize;
        for (int i = 0, disp = 0; i < bandSize; i++) {
            separatedBytes[i] = bytes[disp++];
            separatedBytes[i + bandSize] = bytes[disp++];
            separatedBytes[i + bandSize2] = bytes[disp++];
        }
    }
     */

    private static boolean isSimpleRearrangingBytesEnough(TiffIFD ifd, AtomicBoolean simpleLossless)
            throws TiffException {
        final TagCompression compression = ifd.optCompression().orElse(null);
        final boolean advancedFormat = compression != null && (!compression.isStandard() || compression.isJpeg());
        if (simpleLossless != null) {
            simpleLossless.set(!advancedFormat);
        }
        if (advancedFormat) {
            // JPEG codec and all non-standard codecs like JPEG-2000 should perform all necessary
            // bits unpacking or color space corrections themselves
            return true;
        }
        int bits = ifd.tryEqualBitDepthAlignedByBytes().orElse(-1);
        if (bits == -1) {
            return false;
        }
        if (bits != 8 && bits != 16 && bits != 24 && bits != 32 && bits != 64) {
            // - should not occur: the same check is performed in TiffIFD.sampleType(), called while creating TiffMap
            throw new UnsupportedTiffFormatException("Not supported TIFF format: compression \"" +
                    ifd.compressionPrettyName() + "\", " + bits + " bits per every sample");
        }
        if (ifd.getPhotometricInterpretation() == TagPhotometricInterpretation.Y_CB_CR) {
            // - convertYCbCrToRGB function performs necessary repacking itself
            return false;
        }
        return !ifd.isStandardInvertedCompression();
    }

    // Common prototype, based on SCIFIO code
    private static int unpackFloatBits(int value, int mantissaBits, int exponentBits) {
        final int exponentIncrement = 127 - (pow2(exponentBits - 1) - 1);
        final int power2ExponentBitsMinus1 = pow2(exponentBits) - 1;
        final int packedBitsPerSampleMinus1 = mantissaBits + exponentBits;
        final int sign = value >> packedBitsPerSampleMinus1;
        final int power2MantissaBits = pow2(mantissaBits);
        int exponent = (value >> mantissaBits) & power2ExponentBitsMinus1;
        int mantissa = value & (power2MantissaBits - 1);

        if (exponent == 0) {
            if (mantissa != 0) {
                while ((mantissa & power2MantissaBits) == 0) {
                    mantissa <<= 1;
                    exponent--;
                }
                exponent++;
                mantissa &= power2MantissaBits - 1;
                exponent += exponentIncrement;
            }
        } else if (exponent == power2ExponentBitsMinus1) {
            exponent = 255;
        } else {
            exponent += exponentIncrement;
        }

        mantissa <<= (23 - mantissaBits);

        return (sign << 31) | (exponent << 23) | mantissa;
    }

    private static int unpack24BitFloat(int value) {
        final int mantissaBits = 16;
        final int exponentIncrement = 64;
        final int power2ExponentBitsMinus1 = 127;

        final int sign = value >> 23;
        final int power2MantissaBits = 1 << 16;
        int exponent = (value >> 16) & 127;
        int mantissa = value & 65535;

        if (exponent == 0) {
            if (mantissa != 0) {
                while ((mantissa & power2MantissaBits) == 0) {
                    mantissa <<= 1;
                    exponent--;
                }
                exponent++;
                mantissa &= 65535;
                exponent += exponentIncrement;
            }
        } else if (exponent == power2ExponentBitsMinus1) {
            exponent = 255;
        } else {
            exponent += exponentIncrement;
        }
        mantissa <<= 23 - mantissaBits;

        return (sign << 31) | (exponent << 23) | mantissa;
    }

    // From TwelveMonkey: equivalent code
    private static int unpack16BitFloat(short value) {
        int mantissa = value & 0x03ff;           // 10 bits mantissa
        int exponent = value & 0x7c00;           //  5 bits exponent

        if (exponent == 0x7c00) {               // NaN/Inf
            exponent = 0x3fc00;                 // -> NaN/Inf
        } else if (exponent != 0) {             // Normalized value
            exponent += 0x1c000;                // exp - 15 + 127

            // [ Below is commented addition from TwelveMonkey, that sometimes leads to incorrect results;
            // fixed in https://github.com/haraldk/TwelveMonkeys/issues/865 ]
            // Smooth transition
//            if (mantissa == 0 && exponent > 0x1c400) {
//                return (value & 0x8000) << 16 | exponent << 13 | 0x3ff;
//            }
        } else if (mantissa != 0) {             // && exp == 0 -> subnormal
            exponent = 0x1c400;                 // Make it normal

            do {
                mantissa <<= 1;                 // mantissa * 2
                exponent -= 0x400;              // Decrease exp by 1
            } while ((mantissa & 0x400) == 0);  // while not normal

            mantissa &= 0x3ff;                  // Discard subnormal bit
        }                                       // else +/-0 -> +/-0

        // Combine all parts,  sign << (31 - 15), value << (23 - 10)
        return (value & 0x8000) << 16 | (exponent | mantissa) << 13;
    }

    private static void checkInterleaved(TiffTile tile) throws TiffException {
        if (!tile.isInterleaved()) {
            throw new IllegalArgumentException("Tile data must be interleaved for correct completing " +
                    "to decode " + tile.ifd().compressionPrettyName() + " (separated data are allowed for codecs " +
                    "like JPEG, that must fully decode data themselves, but not for this " +
                    "compression): " + tile);
        }
    }

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

    private static int pow2(int b) {
        return 1 << b;
    }
}




© 2015 - 2024 Weber Informatics LLC | Privacy Policy