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

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

import net.algart.math.IRectangularArea;
import net.algart.matrices.tiff.*;

import java.util.*;
import java.util.function.Consumer;

/**
 * TIFF tile: container for samples (encoded or decoded) with given {@link TiffTileIndex index}.
 *
 * @author Denial Alievsky
 */
public final class TiffTile {
    private final TiffMap map;
    private final int samplesPerPixel;
    private final int bytesPerSample;
    private final int bytesPerPixel;
    private final boolean littleEndian;
    private final TiffTileIndex index;
    private int sizeX;
    private int sizeY;
    private int sizeInPixels;
    private int sizeInBytes;
    private boolean interleaved = false;
    private boolean encoded = false;
    private byte[] data = null;
    private long storedDataFileOffset = -1;
    private int storedDataLength = 0;
    private int storedNumberOfPixels = 0;
    private Queue unsetArea = null;
    // - null value marks that all is empty;
    // it helps to defer actual subtracting until the moment when we know correct tile sizes

    /**
     * Creates new tile with given index.
     *
     * 

Note: created tile may lie outside its map, it is not prohibited. * This condition is checked not here, but in {@link TiffMap#put(TiffTile)} and other {@link TiffMap} methods. * * @param index tile index. */ public TiffTile(TiffTileIndex index) { this.index = Objects.requireNonNull(index, "Null tile index"); this.map = index.map(); this.samplesPerPixel = map.tileSamplesPerPixel(); this.bytesPerSample = map.bytesPerSample(); this.bytesPerPixel = samplesPerPixel * bytesPerSample; this.littleEndian = map.ifd().isLittleEndian(); assert bytesPerPixel <= TiffMap.MAX_TOTAL_BYTES_PER_PIXEL : samplesPerPixel + "*" + bytesPerPixel + " were not checked in TiffMap!"; assert index.ifd() == map.ifd() : "index retrieved ifd from its tile map!"; setSizes(map.tileSizeX(), map.tileSizeY()); } public TiffMap map() { return map; } public TiffIFD ifd() { return map.ifd(); } public TiffTileIndex index() { return index; } public boolean isPlanarSeparated() { return map.isPlanarSeparated(); } public int samplesPerPixel() { return samplesPerPixel; } public int bytesPerSample() { return bytesPerSample; } public int bytesPerPixel() { return bytesPerPixel; } public boolean isLittleEndian() { return littleEndian; } public TiffSampleType sampleType() { return map.sampleType(); } public Class elementType() { return map.elementType(); } public int getSizeX() { return sizeX; } public TiffTile setSizeX(int sizeX) { return setSizes(sizeX, this.sizeY); } public int getSizeY() { return sizeY; } public TiffTile setSizeY(int sizeY) { return setSizes(this.sizeX, sizeY); } /** * Sets the sizes of this tile. * *

These are purely informational properties, not affecting processing the stored data * and supported for additional convenience of usage this object. * *

There is a guarantee that the total {@link #getSizeInBytes() number of bytes}, * required to store sizeX*sizeY pixels, will be ≤Integer.MAX_VALUE. * * @param sizeX the tile width; must be positive. * @param sizeY the tile height; must be positive. * @return a reference to this object. */ public TiffTile setSizes(int sizeX, int sizeY) { if (sizeX <= 0) { throw new IllegalArgumentException("Zero or negative tile x-size: " + sizeX); } if (sizeY <= 0) { throw new IllegalArgumentException("Zero or negative tile y-size: " + sizeY); } // - zero sizes are disabled to provide correct IRectangularArea processing if ((long) sizeX * (long) sizeY > Integer.MAX_VALUE) { throw new IllegalArgumentException("Very large TIFF tile " + sizeX + "x" + sizeY + " >= 2^31 pixels is not supported"); } final int sizeInPixels = sizeX * sizeY; if ((long) sizeInPixels * (long) bytesPerPixel > Integer.MAX_VALUE) { throw new IllegalArgumentException("Very large TIFF tile " + sizeX + "x" + sizeY + ", " + samplesPerPixel + " channels per " + bytesPerSample + " bytes >= 2^31 bytes is not supported"); } this.sizeX = sizeX; this.sizeY = sizeY; this.sizeInPixels = sizeInPixels; this.sizeInBytes = sizeInPixels * bytesPerPixel; return this; } public TiffTile setEqualSizes(TiffTile other) { Objects.requireNonNull(other, "Null other tile"); return setSizes(other.sizeX, other.sizeY); } public boolean equalSizes(TiffTile other) { return other != null && sizeX == other.sizeX && sizeY == other.sizeY; // - note: there is no sense to check samplesPerPixel, it is not a "size", but property of pixel format } public int getSizeInPixels() { return sizeInPixels; } public int getSizeInBytes() { return sizeInBytes; } public IRectangularArea rectangle() { return rectangleInTile(0, 0, sizeX, sizeY); } public IRectangularArea rectangleInTile(int fromXInTile, int fromYInTile, int sizeXInTile, int sizeYInTile) { if (sizeXInTile <= 0) { throw new IllegalArgumentException("Zero or negative sizeXInTile = " + sizeXInTile); } if (sizeYInTile <= 0) { throw new IllegalArgumentException("Zero or negative sizeYInTile = " + sizeYInTile); } final long minX = (long) index.fromX() + (long) fromXInTile; final long minY = (long) index.fromY() + (long) fromYInTile; final long maxX = minX + (long) sizeXInTile - 1; final long maxY = minY + (long) sizeYInTile - 1; return IRectangularArea.valueOf(minX, minY, maxX, maxY); } public boolean isFullyInsideMap() { return index.fromX() + sizeX <= map.dimX() && index.fromY() + sizeY <= map.dimY(); } /** * Reduces sizes of this tile so that it will completely lie inside map dimensions. * *

This operation can be useful for stripped TIFF image, especially while writing. * But you should not call this for tiled image (when {@link TiffMap#isTiled()} returns true). * For tiled image, TIFF file usually contains full-size encoded tiles even on image boundary; * they should be cropped after decoding by external means. You can disable attempt to reduce * tile in tiled image by passing nonTiledOnly=true. * * @param nonTiledOnly if true, this function will not do anything when the map * is {@link TiffMap#isTiled() tiled}. While using for reading/writing TIFF files, * this argument usually should be true. * @return a reference to this object. * @throws IllegalStateException if this tile is completely outside map dimensions. */ public TiffTile cropToMap(boolean nonTiledOnly) { checkOutsideMap(); if (nonTiledOnly && map.isTiled()) { return this; } else { return setSizes(Math.min(sizeX, map.dimX() - index.fromX()), Math.min(sizeY, map.dimY() - index.fromY())); } } public Collection getUnsetArea() { return unsetArea == null ? List.of(rectangle()) : Collections.unmodifiableCollection(unsetArea); } public TiffTile unsetAll() { unsetArea = null; return this; } public TiffTile removeUnset() { unsetArea = new LinkedList<>(); return this; } public TiffTile reduceUnset(IRectangularArea... newlyFilledArea) { Objects.requireNonNull(newlyFilledArea, "Null newlyFilledArea"); initializeEmptyArea(); IRectangularArea.subtractCollection(unsetArea, newlyFilledArea); return this; } public TiffTile reduceUnsetInTile(int fromXInTile, int fromYInTile, int sizeXInTile, int sizeYInTile) { if (sizeXInTile > 0 && sizeYInTile > 0) { reduceUnset(rectangleInTile(fromXInTile, fromYInTile, sizeXInTile, sizeYInTile)); } return this; } public TiffTile cropUnsetToMap() { checkOutsideMap(); if (!isFullyInsideMap()) { // - little optimization reduceUnset( IRectangularArea.valueOf(0, map.dimY(), Integer.MAX_VALUE, Integer.MAX_VALUE), IRectangularArea.valueOf(map.dimX(), 0, Integer.MAX_VALUE, Integer.MAX_VALUE)); // Integer.MAX_VALUE is enough: we work with 32-bit coordinates // Note that Long.MAX_VALUE is not permitted here, maximal allowed value is Long.MAX_VALUE-1 } return this; } public boolean isCompleted() { return !hasUnset(); } public boolean hasUnset() { return unsetArea == null || !unsetArea.isEmpty(); } /** * Returns true>, if the stored pixel samples (as supposed) are interleaved, like RGBRGB..., * or false if not (RRR...GGG...BBB...). * It doesn't matter in a case of monochrome images and in a case of {@link #isEncoded() encoded} data. * Default value is false. * *

By default, the data are considered to be not interleaved, in other words, {@link #isSeparated() * separated}. Methods, reading and decoding the tile from TIFF, always return separated tile. * Methods, encoding the file for writing to TIFF, may work both with interleaved tiles, * but it should be explicitly declared, like in * {@link TiffWriter#setAutoInterleaveSource(boolean)} method (with false argument).

* *

This is purely informational property, not affecting processing the stored data * by methods of this object and supported for additional convenience of usage this class.

* * @return whether the data in the tile are interleaved. */ public boolean isInterleaved() { return interleaved; } public boolean isSeparated() { return !interleaved; } /** * Sets the flag, are the stored pixel samples are interleaved (like RGBRGB...) or not (RRR...GGG...BBB...). * See {@link #isInterleaved()}. * * @param interleaved whether the data should be considered as interleaved. * @return a reference to this object. */ public TiffTile setInterleaved(boolean interleaved) { this.interleaved = interleaved; return this; } //Note: there is no setEncoded() method, it could violate invariants provided by setDecodedData public boolean isEncoded() { return encoded; } public boolean isEmpty() { return data == null; } public void checkReadyForNewDecodedData(Boolean requiredInterleavedState) { if (encoded) { throw new IllegalStateException("TIFF tile is not ready to store new decoded data, because " + "it is encoded (probably contains encoded data): " + this); } if (requiredInterleavedState != null && requiredInterleavedState != interleaved) { final String status = interleaved ? "interleaved" : "separated"; throw new IllegalStateException("TIFF tile is not ready to store new decoded data, because " + "it is " + status + " (probably contains decoded, but already " + status + " data): " + this); } } public byte[] getData() { checkEmpty(); return data; } public byte[] getEncodedData() { checkEmpty(); if (!isEncoded()) { throw new IllegalStateException("TIFF tile is not encoded: " + this); } return data; } public TiffTile setEncodedData(byte[] data) { return setData(data, true, false); } public TiffTile fillEmpty(Consumer initializer) { if (isEmpty()) { setDecodedData(new byte[sizeInBytes]); if (initializer != null) { initializer.accept(this); } } return this; } public byte[] getDecodedData() { checkEmpty(); if (isEncoded()) { throw new IllegalStateException("TIFF tile data are not decoded and cannot be retrieved: " + this); } return data; } /** * Gets the decoded data with unpacking non-usual precisions: 16/24-bit floating points data * and any 3-byte/sample integer data. The same operations are performed by * {@link TiffReader} automatically * if the {@link TiffReader#isAutoUnpackUnusualPrecisions()} mode is set. * *

This method is necessary rarely: {@link #getDecodedData()} is enough for most needs. * * @return unpacked data. */ public byte[] unpackUnusualDecodedData() { byte[] samples = getDecodedData(); try { samples = TiffTools.unpackUnusualPrecisions( samples, ifd(), samplesPerPixel, sizeX * sizeY, true); } catch (TiffException e) { throw new IllegalStateException("Illegal IFD inside the tile map", e); } return samples; } public TiffTile setDecodedData(byte[] data) { return setData(data, false, true); } public TiffTile setPartiallyDecodedData(byte[] data) { return setData(data, false, false); } public TiffTile free() { this.data = null; this.interleaved = false; // - before possible setting new decoded data, we should restore default status interleaved = false this.encoded = false; // - method checkReadyForNewDecodedData() require that the tile should not be declared as encoded // Note: we should not clear information about stored data file range, because // it will be used even after flushing data to disk (with freeing this tile) return this; } /** * Return the length of the last non-null {@link #getData() data array}, stored in this tile, * or 0 after creating this object. * *

Immediately after reading tile from file, as well as * immediately before/after writing it into file, this method returns the number of encoded bytes, * which are actually stored in the file for this tile. * *

Note: {@link #free()} method does not change this value! So, you can know the stored data size * even after freeing data inside this object. * * @return the length of the last non-null data array, which was stored in this object. */ public int getStoredDataLength() { return storedDataLength; } public boolean isStoredInFile() { return storedDataFileOffset >= 0; } public long getStoredDataFileOffset() { checkStoredFilePosition(); return storedDataFileOffset; } public TiffTile setStoredDataFileOffset(long storedDataFileOffset) { if (storedDataFileOffset < 0) { throw new IllegalArgumentException("Negative storedDataFileOffset = " + storedDataFileOffset); } this.storedDataFileOffset = storedDataFileOffset; return this; } public TiffTile removeStoredDataFileOffset() { storedDataFileOffset = -1; return this; } public TiffTile setStoredDataFileRange(long storedDataFileOffset, int storedDataLength) { if (storedDataFileOffset < 0) { throw new IllegalArgumentException("Negative storedDataFileOffset = " + storedDataFileOffset); } if (storedDataLength < 0) { throw new IllegalArgumentException("Negative storedDataLength = " + storedDataLength); } this.storedDataLength = storedDataLength; this.storedDataFileOffset = storedDataFileOffset; return this; } public TiffTile copyStoredDataFileRange(TiffTile other) { Objects.requireNonNull(other, "Null other tile"); this.storedDataLength = other.storedDataLength; this.storedDataFileOffset = other.storedDataFileOffset; return this; } /** * Returns the number of pixels, actually stored in the {@link #getData() data array} in this tile * in the decoded form, or 0 after creating this object. * *

Note: that this method throws IllegalStateException if the data are * {@link #isEncoded() encoded}, for example, immediately after reading tile from file. * If the tile is {@link #isEmpty() empty} (no data), * the exception is not thrown, though usually there is no sense to call this method in this situation.

* *

If the data are not {@link #isEncoded() encoded}, the following equality is usually true:

* *
{@link #getStoredDataLength()} == {@link #getStoredNumberOfPixels()} * {@link #bytesPerPixel()}
* *

The only possible exception is when you sets the data with help of * {@link #setPartiallyDecodedData(byte[])} (when data are almost decoded, but, maybe, some additional * unpacking is necessary). This condition is always checked inside {@link #setDecodedData(byte[])} * method. You may also check this directly by {@link #checkDataLengthAlignment()} method.

* *

Warning: the stored number of pixels, returned by this method, may differ from the tile * size {@link #getSizeX()} * {@link #getSizeY()}! Usually it occurs after decoding encoded tile, when the * decoding method returns only sequence of pixels and does not return information about the size. * In this situation, the external code sets the tile sizes from a priory information, but the decoded tile * may be actually less; for example, it takes place for the last strip in non-tiled TIFF format. * You can check, does the actual number of stored pixels equal to tile size, via * {@link #checkStoredNumberOfPixels()} method. * * @return the number of pixels in the last non-null data array, which was stored in this object. */ @SuppressWarnings("JavadocDeclaration") public int getStoredNumberOfPixels() { if (isEncoded()) { throw new IllegalStateException("TIFF tile data are not decoded, number of pixels is unknown: " + this); } return storedNumberOfPixels; } public void checkDataLengthAlignment() { if (!encoded && storedNumberOfPixels * bytesPerPixel != data.length) { throw new IllegalStateException("Unaligned length of decoded data " + data.length + ": it must be a multiple of the pixel length " + bytesPerPixel + " = " + samplesPerPixel + " * " + bytesPerSample + " (channels per pixel * bytes per channel sample)"); } } public TiffTile checkStoredNumberOfPixels() throws IllegalStateException { checkEmpty(); int storedNumberOfPixels = getStoredNumberOfPixels(); if (storedNumberOfPixels != sizeX * sizeY) { throw new IllegalStateException("Number of stored pixels " + storedNumberOfPixels + " does not match tile sizes " + sizeX + "x" + sizeY + " = " + (sizeX * sizeY)); } assert storedNumberOfPixels * bytesPerPixel == storedDataLength; return this; } public TiffTile adjustNumberOfPixels(boolean allowDecreasing) { return changeNumberOfPixels(sizeX * sizeY, allowDecreasing); } public TiffTile changeNumberOfPixels(int newNumberOfPixels, boolean allowDecreasing) { if (newNumberOfPixels < 0) { throw new IllegalArgumentException("Negative new number of pixels = " + newNumberOfPixels); } final byte[] data = getDecodedData(); // - performs all necessary state checks int numberOfPixels = storedNumberOfPixels; if (numberOfPixels == newNumberOfPixels) { return this; } if ((long) newNumberOfPixels * (long) bytesPerPixel > Integer.MAX_VALUE) { throw new IllegalArgumentException("Too large requested number of pixels in tile: " + newNumberOfPixels + " pixels * " + samplesPerPixel + " samples/pixel * " + bytesPerSample + " bytes/sample >= 2^31"); } if (newNumberOfPixels < numberOfPixels && !allowDecreasing) { throw new IllegalArgumentException("The new number of pixels " + newNumberOfPixels + " is less than actually stored " + numberOfPixels + "; this is not allowed: data may be lost"); } final int newLength = newNumberOfPixels * bytesPerPixel; byte[] newData; if (interleaved) { newData = Arrays.copyOf(data, newLength); } else { newData = new byte[newLength]; // - zero-filled by Java final int size = numberOfPixels * bytesPerSample; final int newSize = newNumberOfPixels * bytesPerSample; final int sizeToCopy = Math.min(size, newSize); for (int s = 0, disp = 0, newDisp = 0; s < samplesPerPixel; s++, disp += size, newDisp += newSize) { System.arraycopy(data, disp, newData, newDisp, sizeToCopy); } } return setDecodedData(newData); } public TiffTile interleaveSamplesIfNecessary() { checkEmpty(); if (!isInterleaved()) { interleaveSamples(); } return this; } public TiffTile separateSamplesIfNecessary() { checkEmpty(); if (isInterleaved()) { separateSamples(); } return this; } public TiffTile interleaveSamples() { byte[] data = getDecodedData(); if (isInterleaved()) { throw new IllegalStateException("TIFF tile is already interleaved: " + this); } data = TiffTools.toInterleavedSamples(data, samplesPerPixel(), bytesPerSample(), getStoredNumberOfPixels()); setInterleaved(true); setDecodedData(data); return this; } public TiffTile separateSamples() { byte[] data = getDecodedData(); if (!isInterleaved()) { throw new IllegalStateException("TIFF tile is already separated: " + this); } data = TiffTools.toSeparatedSamples(data, samplesPerPixel(), bytesPerSample(), getStoredNumberOfPixels()); setInterleaved(false); setDecodedData(data); return this; } @Override public String toString() { return "TIFF " + (isEmpty() ? "(empty) " : "") + (encoded ? "encoded" : "non-encoded") + (interleaved ? " interleaved" : "") + " tile" + (isEmpty() ? ", " + sizeX + "x" + sizeY : ", actual sizes " + sizeX + "x" + sizeY + " (" + storedNumberOfPixels + " pixels, " + storedDataLength + " bytes)" + (isCompleted() ? ", completed" : ", partial")) + ", index " + index + (isStoredInFile() ? " at file offset " + storedDataFileOffset : ""); } @Override public boolean equals(Object o) { if (this == o) { return true; } if (o == null || getClass() != o.getClass()) { return false; } TiffTile tiffTile = (TiffTile) o; return sizeX == tiffTile.sizeX && sizeY == tiffTile.sizeY && interleaved == tiffTile.interleaved && encoded == tiffTile.encoded && samplesPerPixel == tiffTile.samplesPerPixel && bytesPerSample == tiffTile.bytesPerSample && storedDataFileOffset == tiffTile.storedDataFileOffset && storedDataLength == tiffTile.storedDataLength && Objects.equals(index, tiffTile.index) && Arrays.equals(data, tiffTile.data); // Note: doesn't check "map" to avoid infinite recursion! } @Override public int hashCode() { int result = Objects.hash(index, sizeX, sizeY, interleaved, encoded, storedDataFileOffset, storedDataLength); result = 31 * result + Arrays.hashCode(data); return result; // Note: doesn't check this.map to avoid infinite recursion! } private TiffTile setData(byte[] data, boolean encoded, boolean checkAligned) { Objects.requireNonNull(data, "Null " + (encoded ? "encoded" : "decoded") + " data"); final int storedNumberOfPixels = data.length / bytesPerPixel; if (!encoded && checkAligned && storedNumberOfPixels * bytesPerPixel != data.length) { throw new IllegalArgumentException("Invalid length of decoded data " + data.length + ": it must be a multiple of the pixel length " + bytesPerPixel + " = " + samplesPerPixel + " * " + bytesPerSample + " (channels per pixel * bytes per channel sample)"); } this.data = data; this.storedDataLength = data.length; this.storedNumberOfPixels = storedNumberOfPixels; this.encoded = encoded; if (!encoded) { removeStoredDataFileOffset(); // - data file offset has no sense for decoded data } return this; } private void initializeEmptyArea() { if (unsetArea == null) { unsetArea = new LinkedList<>(); unsetArea.add(rectangle()); } } private void checkEmpty() { if (data == null) { throw new IllegalStateException("TIFF tile is still not filled by any data: " + this); } } private void checkOutsideMap() { if (index.fromX() >= map.dimX() || index.fromY() >= map.dimY()) { throw new IllegalStateException("Tile is fully outside the map dimensions " + map.dimX() + "x" + map.dimY() + ": " + this); } } private void checkStoredFilePosition() { if (storedDataFileOffset < 0) { throw new IllegalStateException("File offset of the TIFF tile is not set yet: " + this); } } }





© 2015 - 2024 Weber Informatics LLC | Privacy Policy