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