diff --git a/imageio/imageio-metadata/src/main/java/com/twelvemonkeys/imageio/metadata/tiff/TIFFEntry.java b/imageio/imageio-metadata/src/main/java/com/twelvemonkeys/imageio/metadata/tiff/TIFFEntry.java index e54ddfb4..fe9fe794 100644 --- a/imageio/imageio-metadata/src/main/java/com/twelvemonkeys/imageio/metadata/tiff/TIFFEntry.java +++ b/imageio/imageio-metadata/src/main/java/com/twelvemonkeys/imageio/metadata/tiff/TIFFEntry.java @@ -173,10 +173,14 @@ public final class TIFFEntry extends AbstractEntry { return "TileByteCounts"; case TIFF.TAG_COPYRIGHT: return "Copyright"; + case TIFF.TAG_YCBCR_COEFFICIENTS: + return "YCbCrCoefficients"; case TIFF.TAG_YCBCR_SUB_SAMPLING: return "YCbCrSubSampling"; case TIFF.TAG_YCBCR_POSITIONING: return "YCbCrPositioning"; + case TIFF.TAG_REFERENCE_BLACK_WHITE: + return "ReferenceBlackWhite"; case TIFF.TAG_COLOR_MAP: return "ColorMap"; case TIFF.TAG_INK_SET: diff --git a/imageio/imageio-tiff/src/main/java/com/twelvemonkeys/imageio/plugins/tiff/TIFFImageReader.java b/imageio/imageio-tiff/src/main/java/com/twelvemonkeys/imageio/plugins/tiff/TIFFImageReader.java index d9785a8c..3a41a3a9 100644 --- a/imageio/imageio-tiff/src/main/java/com/twelvemonkeys/imageio/plugins/tiff/TIFFImageReader.java +++ b/imageio/imageio-tiff/src/main/java/com/twelvemonkeys/imageio/plugins/tiff/TIFFImageReader.java @@ -1092,19 +1092,14 @@ public final class TIFFImageReader extends ImageReaderBase { : createStreamAdapter(imageInput); adapter = createFillOrderStream(fillOrder, adapter); - adapter = createDecompressorStream(compression, stripTileWidth, numBands, adapter); - adapter = createUnpredictorStream(predictor, stripTileWidth, numBands, bitsPerSample, adapter, imageInput.getByteOrder()); - if (interpretation == TIFFExtension.PHOTOMETRIC_YCBCR && rowRaster.getTransferType() == DataBuffer.TYPE_BYTE) { - adapter = new YCbCrUpsamplerStream(adapter, yCbCrSubsampling, yCbCrPos, colsInTile); - } - else if (interpretation == TIFFExtension.PHOTOMETRIC_YCBCR && rowRaster.getTransferType() == DataBuffer.TYPE_USHORT) { - adapter = new YCbCr16UpsamplerStream(adapter, yCbCrSubsampling, yCbCrPos, colsInTile, imageInput.getByteOrder()); - } - else if (interpretation == TIFFExtension.PHOTOMETRIC_YCBCR) { - // Handled in getRawImageType - throw new AssertionError(); - } + // For subsampled planar, the compressed data will not be full width + int compressedStripTileWidth = planarConfiguration == TIFFExtension.PLANARCONFIG_PLANAR && b > 0 && yCbCrSubsampling != null + ? ((stripTileWidth + yCbCrSubsampling[0] - 1) / yCbCrSubsampling[0]) + : stripTileWidth; + adapter = createDecompressorStream(compression, compressedStripTileWidth, numBands, adapter); + adapter = createUnpredictorStream(predictor, compressedStripTileWidth, numBands, bitsPerSample, adapter, imageInput.getByteOrder()); + adapter = createYCbCrUpsamplerStream(interpretation, planarConfiguration, b, rowRaster.getTransferType(), yCbCrSubsampling, yCbCrPos, colsInTile, adapter, imageInput.getByteOrder()); if (needsBitPadding) { // We'll pad "odd" bitsPerSample streams to the smallest data type (byte/short/int) larger than the input @@ -1129,6 +1124,11 @@ public final class TIFFImageReader extends ImageReaderBase { readStripTileData(clippedRow, srcRegion, xSub, ySub, b, numBands, interpretation, destRaster, col, srcRow, colsInTile, rowsInTile, input); } + // Need to do color normalization after reading all bands for planar + if (planarConfiguration == TIFFExtension.PLANARCONFIG_PLANAR) { + normalizeColorPlanar(interpretation, destRaster); + } + col += colsInTile; if (abortRequested()) { @@ -1567,7 +1567,7 @@ public final class TIFFImageReader extends ImageReaderBase { break; - // Known, but unsupported compression types + // Known, but unsupported compression types case TIFFCustom.COMPRESSION_NEXT: case TIFFCustom.COMPRESSION_CCITTRLEW: case TIFFCustom.COMPRESSION_THUNDERSCAN: @@ -1582,8 +1582,8 @@ public final class TIFFImageReader extends ImageReaderBase { case TIFFCustom.COMPRESSION_SGILOG: case TIFFCustom.COMPRESSION_SGILOG24: case TIFFCustom.COMPRESSION_JPEG2000: // Doable with JPEG2000 plugin? - throw new IIOException("Unsupported TIFF Compression value: " + compression); + default: throw new IIOException("Unknown TIFF Compression value: " + compression); } @@ -1595,6 +1595,28 @@ public final class TIFFImageReader extends ImageReaderBase { return destination; } + private InputStream createYCbCrUpsamplerStream(int photometricInterpretation, int planarConfiguration, int plane, int transferType, + int[] yCbCrSubsampling, int yCbCrPos, int colsInTile, InputStream stream, ByteOrder byteOrder) { + if (photometricInterpretation == TIFFExtension.PHOTOMETRIC_YCBCR) { + if (planarConfiguration == TIFFExtension.PLANARCONFIG_PLANAR && transferType == DataBuffer.TYPE_BYTE) { + // For planar YCbCr, only the chroma planes are subsampled + return plane > 0 && (yCbCrSubsampling[0] != 1 || yCbCrSubsampling[1] != 1) + ? new YCbCrPlanarUpsamplerStream(stream, yCbCrSubsampling, yCbCrPos, colsInTile) : stream; + } + else if (transferType == DataBuffer.TYPE_BYTE) { + return new YCbCrUpsamplerStream(stream, yCbCrSubsampling, yCbCrPos, colsInTile); + } + else if (transferType == DataBuffer.TYPE_USHORT) { + return new YCbCr16UpsamplerStream(stream, yCbCrSubsampling, yCbCrPos, colsInTile, byteOrder); + } + + // Handled in getRawImageType + throw new AssertionError(); + } + + return stream; + } + private boolean containsZero(long[] byteCounts) { for (long byteCount : byteCounts) { if (byteCount <= 0) { @@ -1919,12 +1941,8 @@ public final class TIFFImageReader extends ImageReaderBase { } } -// if (banded) { -// // TODO: Normalize colors for tile (need to know tile region and sample model) -// // Unfortunately, this will disable acceleration... -// } - break; + case DataBuffer.TYPE_USHORT: case DataBuffer.TYPE_SHORT: /*for (int band = 0; band < bands; band++)*/ { @@ -1962,6 +1980,7 @@ public final class TIFFImageReader extends ImageReaderBase { } break; + case DataBuffer.TYPE_INT: /*for (int band = 0; band < bands; band++)*/ { int[] rowDataInt = ((DataBufferInt) dataBuffer).getData(band); @@ -2101,6 +2120,102 @@ public final class TIFFImageReader extends ImageReaderBase { } } + private void normalizeColorPlanar(int photometricInterpretation, WritableRaster raster) throws IIOException { + // TODO: Other transfer types? + if (raster.getTransferType() != DataBuffer.TYPE_BYTE) { + return; + } + + byte[] pixel = null; + + switch (photometricInterpretation) { + case TIFFExtension.PHOTOMETRIC_YCBCR: + + // Default: CCIR Recommendation 601-1: 299/1000, 587/1000 and 114/1000 + double[] coefficients = getValueAsDoubleArray(TIFF.TAG_YCBCR_COEFFICIENTS, "YCbCrCoefficients", false, 3); + + // "Default" [0, 255, 128, 255, 128, 255] for YCbCr (real default is [0, 255, 0, 255, 0, 255] for RGB) + double[] referenceBW = getValueAsDoubleArray(TIFF.TAG_REFERENCE_BLACK_WHITE, "ReferenceBlackWhite", false, 6); + + if ((coefficients == null || Arrays.equals(coefficients, CCIR_601_1_COEFFICIENTS)) + && (referenceBW == null || Arrays.equals(referenceBW, REFERENCE_BLACK_WHITE_YCC_DEFAULT))) { + + // Fast, default conversion + for (int y = 0; y < raster.getHeight(); y++) { + for (int x = 0; x < raster.getWidth(); x++) { + pixel = (byte[]) raster.getDataElements(x, y, pixel); + YCbCrConverter.convertJPEGYCbCr2RGB(pixel, pixel, 0); + raster.setDataElements(x, y, pixel); + } + } + } + else { + // If one of the values are null, we'll need the other here... + if (coefficients == null) { + coefficients = CCIR_601_1_COEFFICIENTS; + } + + if (referenceBW != null && Arrays.equals(referenceBW, REFERENCE_BLACK_WHITE_YCC_DEFAULT)) { + referenceBW = null; + } + + for (int y = 0; y < raster.getHeight(); y++) { + for (int x = 0; x < raster.getWidth(); x++) { + pixel = (byte[]) raster.getDataElements(x, y, pixel); + YCbCrConverter.convertYCbCr2RGB(pixel, pixel, coefficients, referenceBW, 0); + raster.setDataElements(x, y, pixel); + } + } + } + + break; + + case TIFFExtension.PHOTOMETRIC_CIELAB: + case TIFFExtension.PHOTOMETRIC_ICCLAB: + case TIFFExtension.PHOTOMETRIC_ITULAB: + // TODO: White point may be encoded in separate tag + CIELabColorConverter converter = new CIELabColorConverter( + photometricInterpretation == TIFFExtension.PHOTOMETRIC_CIELAB + ? Illuminant.D65 + : Illuminant.D50 + ); + + float[] temp = new float[3]; + + for (int y = 0; y < raster.getHeight(); y++) { + for (int x = 0; x < raster.getWidth(); x++) { + pixel = (byte[]) raster.getDataElements(x, y, pixel); + + float LStar = (pixel[0] & 0xff) * 100f / 255.0f; + float aStar; + float bStar; + + if (photometricInterpretation == TIFFExtension.PHOTOMETRIC_CIELAB) { + // -128...127 + aStar = pixel[1]; + bStar = pixel[2]; + } + else { + // Assumes same data for ICC and ITU (unsigned) + // 0...255 + aStar = (pixel[1] & 0xff) - 128; + bStar = (pixel[2] & 0xff) - 128; + } + + converter.toRGB(LStar, aStar, bStar, temp); + + pixel[0] = (byte) temp[0]; + pixel[1] = (byte) temp[1]; + pixel[2] = (byte) temp[2]; + + raster.setDataElements(x, y, pixel); + } + } + + break; + } + } + private void normalizeColor(int photometricInterpretation, byte[] data) throws IOException { switch (photometricInterpretation) { case TIFFBaseline.PHOTOMETRIC_WHITE_IS_ZERO: @@ -2705,8 +2820,14 @@ public final class TIFFImageReader extends ImageReaderBase { try { long start = System.currentTimeMillis(); - int width = reader.getWidth(imageNo); - int height = reader.getHeight(imageNo); +// int width = reader.getWidth(imageNo); +// int height = reader.getHeight(imageNo); + if (param.canSetSourceRenderSize()) { + int thumbSize = 512; + float aspectRatio = reader.getAspectRatio(imageNo); + param.setSourceRenderSize(aspectRatio > 1f ? new Dimension(thumbSize, (int) Math.ceil(thumbSize / aspectRatio)) + : new Dimension((int) Math.ceil(thumbSize * aspectRatio), thumbSize)); + } // param.setSourceRegion(new Rectangle(width / 4, height / 4, width / 2, height / 2)); // param.setSourceRegion(new Rectangle(100, 300, 400, 400)); // param.setSourceRegion(new Rectangle(95, 105, 100, 100)); diff --git a/imageio/imageio-tiff/src/main/java/com/twelvemonkeys/imageio/plugins/tiff/YCbCrPlanarUpsamplerStream.java b/imageio/imageio-tiff/src/main/java/com/twelvemonkeys/imageio/plugins/tiff/YCbCrPlanarUpsamplerStream.java new file mode 100644 index 00000000..e32a3c53 --- /dev/null +++ b/imageio/imageio-tiff/src/main/java/com/twelvemonkeys/imageio/plugins/tiff/YCbCrPlanarUpsamplerStream.java @@ -0,0 +1,198 @@ +/* + * Copyright (c) 2022, Harald Kuhr + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * + * * Redistributions of source code must retain the above copyright notice, this + * list of conditions and the following disclaimer. + * + * * 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. + * + * * Neither the name of the copyright holder nor the names of its + * contributors may be used to endorse or promote products derived from + * this software without specific prior written permission. + * + * 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 HOLDER 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. + */ + +package com.twelvemonkeys.imageio.plugins.tiff; + +import com.twelvemonkeys.lang.Validate; + +import java.io.EOFException; +import java.io.FilterInputStream; +import java.io.IOException; +import java.io.InputStream; + +/** + * Input stream that provides on-the-fly upsampling of TIFF subsampled YCbCr samples. + * + * @author Harald Kuhr + * @author last modified by $Author: haraldk$ + * @version $Id: YCbCrUpsamplerStream.java,v 1.0 31.01.13 09:25 haraldk Exp$ + */ +final class YCbCrPlanarUpsamplerStream extends FilterInputStream { + + private final int horizChromaSub; + private final int vertChromaSub; + private final int yCbCrPos; + private final int columns; + + private final int units; + private final byte[] decodedRows; + int decodedLength; + int decodedPos; + + private final byte[] buffer; + int bufferLength; + int bufferPos; + + public YCbCrPlanarUpsamplerStream(final InputStream stream, final int[] chromaSub, final int yCbCrPos, final int columns) { + super(Validate.notNull(stream, "stream")); + + Validate.notNull(chromaSub, "chromaSub"); + Validate.isTrue(chromaSub.length == 2, "chromaSub.length != 2"); + + this.horizChromaSub = chromaSub[0]; + this.vertChromaSub = chromaSub[1]; + this.yCbCrPos = yCbCrPos; + this.columns = columns; + + units = (columns + horizChromaSub - 1) / horizChromaSub; // If columns % horizChromasSub != 0... + // ...each coded row will be padded to fill unit + decodedRows = new byte[columns * vertChromaSub]; + buffer = new byte[units]; + } + + private void fetch() throws IOException { + if (bufferPos >= bufferLength) { + int pos = 0; + int read; + + // This *SHOULD* read an entire row of units into the buffer, otherwise decodeRows will throw EOFException + while (pos < buffer.length && (read = in.read(buffer, pos, buffer.length - pos)) > 0) { + pos += read; + } + + bufferLength = pos; + bufferPos = 0; + } + + if (bufferLength > 0) { + decodeRows(); + } + else { + decodedLength = -1; + } + } + + private void decodeRows() throws EOFException { + decodedLength = decodedRows.length; + + for (int u = 0; u < units; u++) { + if (u >= bufferLength) { + throw new EOFException("Unexpected end of stream"); + } + + // Decode one unit + byte c = buffer[u]; + + for (int y = 0; y < vertChromaSub; y++) { + for (int x = 0; x < horizChromaSub; x++) { + // Skip padding at end of row + int column = horizChromaSub * u + x; + if (column >= columns) { + break; + } + + int pixelOff = column + columns * y; + decodedRows[pixelOff] = c; + } + } + } + + bufferPos = bufferLength; + decodedPos = 0; + } + + @Override + public int read() throws IOException { + if (decodedLength < 0) { + return -1; + } + + if (decodedPos >= decodedLength) { + fetch(); + + if (decodedLength < 0) { + return -1; + } + } + + return decodedRows[decodedPos++] & 0xff; + } + + @Override + public int read(byte[] b, int off, int len) throws IOException { + if (decodedLength < 0) { + return -1; + } + + if (decodedPos >= decodedLength) { + fetch(); + + if (decodedLength < 0) { + return -1; + } + } + + int read = Math.min(decodedLength - decodedPos, len); + System.arraycopy(decodedRows, decodedPos, b, off, read); + decodedPos += read; + + return read; + } + + @Override + public long skip(long n) throws IOException { + if (decodedLength < 0) { + return -1; + } + + if (decodedPos >= decodedLength) { + fetch(); + + if (decodedLength < 0) { + return -1; + } + } + + int skipped = (int) Math.min(decodedLength - decodedPos, n); + decodedPos += skipped; + + return skipped; + } + + @Override + public boolean markSupported() { + return false; + } + + @Override + public synchronized void reset() throws IOException { + throw new IOException("mark/reset not supported"); + } +} diff --git a/imageio/imageio-tiff/src/main/java/com/twelvemonkeys/imageio/plugins/tiff/YCbCrUpsamplerStream.java b/imageio/imageio-tiff/src/main/java/com/twelvemonkeys/imageio/plugins/tiff/YCbCrUpsamplerStream.java index f4c07fba..989cf5c8 100644 --- a/imageio/imageio-tiff/src/main/java/com/twelvemonkeys/imageio/plugins/tiff/YCbCrUpsamplerStream.java +++ b/imageio/imageio-tiff/src/main/java/com/twelvemonkeys/imageio/plugins/tiff/YCbCrUpsamplerStream.java @@ -213,8 +213,4 @@ final class YCbCrUpsamplerStream extends FilterInputStream { public synchronized void reset() throws IOException { throw new IOException("mark/reset not supported"); } - - private static byte clamp(int val) { - return (byte) Math.max(0, Math.min(255, val)); - } } diff --git a/imageio/imageio-tiff/src/test/java/com/twelvemonkeys/imageio/plugins/tiff/TIFFImageReaderTest.java b/imageio/imageio-tiff/src/test/java/com/twelvemonkeys/imageio/plugins/tiff/TIFFImageReaderTest.java index 642954c4..e452cfe8 100644 --- a/imageio/imageio-tiff/src/test/java/com/twelvemonkeys/imageio/plugins/tiff/TIFFImageReaderTest.java +++ b/imageio/imageio-tiff/src/test/java/com/twelvemonkeys/imageio/plugins/tiff/TIFFImageReaderTest.java @@ -173,7 +173,19 @@ public class TIFFImageReaderTest extends ImageReaderAbstractTest