diff --git a/imageio/imageio-tiff/src/main/java/com/twelvemonkeys/imageio/plugins/tiff/LZWDecoder.java b/imageio/imageio-tiff/src/main/java/com/twelvemonkeys/imageio/plugins/tiff/LZWDecoder.java
index c8152b91..0d76ab1f 100644
--- a/imageio/imageio-tiff/src/main/java/com/twelvemonkeys/imageio/plugins/tiff/LZWDecoder.java
+++ b/imageio/imageio-tiff/src/main/java/com/twelvemonkeys/imageio/plugins/tiff/LZWDecoder.java
@@ -35,7 +35,8 @@ import java.io.IOException;
import java.io.InputStream;
/**
- * LZWDecoder
+ * Implements Lempel-Ziv & Welch (LZW) decompression.
+ * Inspired by libTiff's LZW decompression.
*
* @author Harald Kuhr
* @author last modified by $Author: haraldk$
@@ -67,8 +68,9 @@ abstract class LZWDecoder implements Decoder {
protected LZWDecoder(final boolean compatibilityMode) {
this.compatibilityMode = compatibilityMode;
- table = new String[compatibilityMode ? 4096 + 1024 : 4096]; // libTiff adds another 1024 "for compatibility"...
+ table = new String[compatibilityMode ? 4096 + 1024 : 4096]; // libTiff adds 1024 "for compatibility"...
+ // First 258 entries of table is always fixed
for (int i = 0; i < 256; i++) {
table[i] = new String((byte) i);
}
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 39389c72..996f6a3d 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
@@ -35,6 +35,7 @@ import com.twelvemonkeys.imageio.metadata.CompoundDirectory;
import com.twelvemonkeys.imageio.metadata.Directory;
import com.twelvemonkeys.imageio.metadata.Entry;
import com.twelvemonkeys.imageio.metadata.exif.EXIFReader;
+import com.twelvemonkeys.imageio.metadata.exif.Rational;
import com.twelvemonkeys.imageio.metadata.exif.TIFF;
import com.twelvemonkeys.imageio.stream.ByteArrayImageInputStream;
import com.twelvemonkeys.imageio.stream.SubImageInputStream;
@@ -129,6 +130,9 @@ public class TIFFImageReader extends ImageReaderBase {
private final static boolean DEBUG = "true".equalsIgnoreCase(System.getProperty("com.twelvemonkeys.imageio.plugins.tiff.debug"));
+ // NOTE: DO NOT MODIFY OR EXPOSE!
+ static final double[] CCIR_601_1_COEFFICIENTS = new double[] {299.0 / 1000.0, 587.0 / 1000.0, 114.0 / 1000.0};
+
private CompoundDirectory IFDs;
private Directory currentIFD;
@@ -217,7 +221,7 @@ public class TIFFImageReader extends ImageReaderBase {
getSampleFormat(); // We don't support anything but SAMPLEFORMAT_UINT at the moment, just sanity checking input
int planarConfiguration = getValueAsIntWithDefault(TIFF.TAG_PLANAR_CONFIGURATION, TIFFExtension.PLANARCONFIG_PLANAR);
int interpretation = getValueAsInt(TIFF.TAG_PHOTOMETRIC_INTERPRETATION, "PhotometricInterpretation");
- int samplesPerPixel = getValueAsIntWithDefault(TIFF.TAG_SAMPLES_PER_PIXELS, 1);
+ int samplesPerPixel = getValueAsIntWithDefault(TIFF.TAG_SAMPLES_PER_PIXEL, 1);
int bitsPerSample = getBitsPerSample();
int dataType = bitsPerSample <= 8 ? DataBuffer.TYPE_BYTE : bitsPerSample <= 16 ? DataBuffer.TYPE_USHORT : DataBuffer.TYPE_INT;
@@ -254,6 +258,7 @@ public class TIFFImageReader extends ImageReaderBase {
case TIFFExtension.PHOTOMETRIC_YCBCR:
// JPEG reader will handle YCbCr to RGB for us, we'll have to do it ourselves if not JPEG...
+ // TODO: Sanity check that we have SamplesPerPixel == 3, BitsPerSample == [8,8,8] and Compression == 1 (none), 5 (LZW), or 6 (JPEG)
// TODO: Handle YCbCrSubsampling (up-scaler stream, or read data as-is + up-sample (sub-)raster after read? Apply smoothing?)
case TIFFBaseline.PHOTOMETRIC_RGB:
// RGB
@@ -427,6 +432,10 @@ public class TIFFImageReader extends ImageReaderBase {
public BufferedImage read(int imageIndex, ImageReadParam param) throws IOException {
readIFD(imageIndex);
+ System.err.println("currentIFD.getEntryById(TIFF.TAG_REFERENCE_BLACK_WHITE): " + currentIFD.getEntryById(TIFF.TAG_REFERENCE_BLACK_WHITE));
+ System.err.println("currentIFD.getEntryById(TIFF.TAG_TRANSFER_FUNCTION): " + currentIFD.getEntryById(TIFF.TAG_TRANSFER_FUNCTION));
+ System.err.println("currentIFD.getEntryById(TIFF.TAG_TRANSFER_RANGE): " + currentIFD.getEntryById(TIFF.TAG_TRANSFER_RANGE));
+
int width = getWidth(imageIndex);
int height = getHeight(imageIndex);
@@ -496,8 +505,53 @@ public class TIFFImageReader extends ImageReaderBase {
case TIFFExtension.COMPRESSION_ZLIB:
// 'Adobe-style' Deflate
- // TODO: Read only tiles that lies within region
+ int[] yCbCrSubsampling = null;
+ int yCbCrPos = 1;
+ double[] yCbCrCoefficients = null;
+ if (interpretation == TIFFExtension.PHOTOMETRIC_YCBCR) {
+ // getRawImageType does the lookup/conversion for these
+ if (raster.getNumBands() != 3) {
+ throw new IIOException("TIFF PhotometricInterpreatation YCbCr requires SamplesPerPixel == 3: " + raster.getNumBands());
+ }
+ if (raster.getTransferType() != DataBuffer.TYPE_BYTE) {
+ throw new IIOException("TIFF PhotometricInterpreatation YCbCr requires BitsPerSample == [8,8,8]");
+ }
+ yCbCrPos = getValueAsIntWithDefault(TIFF.TAG_YCBCR_POSITIONING, 1);
+
+ Entry subSampling = currentIFD.getEntryById(TIFF.TAG_YCBCR_SUB_SAMPLING);
+
+ if (subSampling != null) {
+ try {
+ yCbCrSubsampling = (int[]) subSampling.getValue();
+ }
+ catch (ClassCastException e) {
+ throw new IIOException("Unknown TIFF YCbCrSubSampling value type: " + subSampling.getTypeName(), e);
+ }
+
+ if (yCbCrSubsampling.length != 2 ||
+ yCbCrSubsampling[0] != 1 && yCbCrSubsampling[0] != 2 && yCbCrSubsampling[0] != 4 ||
+ yCbCrSubsampling[1] != 1 && yCbCrSubsampling[1] != 2 && yCbCrSubsampling[1] != 4 ||
+ yCbCrSubsampling[0] < yCbCrSubsampling[1]) {
+ throw new IIOException("Bad TIFF YCbCrSubSampling value: " + Arrays.toString(yCbCrSubsampling));
+ }
+ }
+ else {
+ yCbCrSubsampling = new int[] {2, 2};
+ }
+
+ Entry coefficients = currentIFD.getEntryById(TIFF.TAG_YCBCR_COEFFICIENTS);
+ if (coefficients != null) {
+ Rational[] value = (Rational[]) coefficients.getValue();
+ yCbCrCoefficients = new double[] {value[0].doubleValue(), value[1].doubleValue(), value[2].doubleValue()};
+ }
+ else {
+ // Default to y CCIR Recommendation 601-1 values
+ yCbCrCoefficients = CCIR_601_1_COEFFICIENTS;
+ }
+ }
+
+ // TODO: Read only tiles that lies within region
// General uncompressed/compressed reading
for (int y = 0; y < tilesDown; y++) {
int col = 0;
@@ -510,7 +564,8 @@ public class TIFFImageReader extends ImageReaderBase {
imageInput.seek(stripTileOffsets[i]);
DataInput input;
- if (compression == TIFFBaseline.COMPRESSION_NONE) {
+ if (compression == TIFFBaseline.COMPRESSION_NONE && interpretation != TIFFExtension.PHOTOMETRIC_YCBCR) {
+ // No need for transformation, fast forward
input = imageInput;
}
else {
@@ -518,11 +573,16 @@ public class TIFFImageReader extends ImageReaderBase {
? IIOUtil.createStreamAdapter(imageInput, stripTileByteCounts[i])
: IIOUtil.createStreamAdapter(imageInput);
+ adapter = createDecoderInputStream(compression, adapter);
+
+ if (interpretation == TIFFExtension.PHOTOMETRIC_YCBCR) {
+ adapter = new YCbCrUpsamplerStream(adapter, yCbCrSubsampling, colsInTile, yCbCrCoefficients);
+ }
+
// According to the spec, short/long/etc should follow order of containing stream
input = imageInput.getByteOrder() == ByteOrder.BIG_ENDIAN
- ? new DataInputStream(createDecoderInputStream(compression, adapter))
- : new LittleEndianDataInputStream(createDecoderInputStream(compression, adapter));
-
+ ? new DataInputStream(adapter)
+ : new LittleEndianDataInputStream(adapter);
}
// Read a full strip/tile
@@ -566,6 +626,9 @@ public class TIFFImageReader extends ImageReaderBase {
// Might have something to do with subsampling?
// How do we pass the chroma-subsampling parameter from the TIFF structure to the JPEG reader?
+ // TODO: Consider splicing the TAG_JPEG_TABLES into the streams for each tile, for a more
+ // compatible approach..?
+
jpegReader.setInput(new ByteArrayImageInputStream(tablesValue));
// NOTE: This initializes the tables AND MORE secret internal settings for the reader (as if by magic).
@@ -690,6 +753,7 @@ public class TIFFImageReader extends ImageReaderBase {
switch (rowRaster.getTransferType()) {
case DataBuffer.TYPE_BYTE:
byte[] rowData = ((DataBufferByte) rowRaster.getDataBuffer()).getData();
+
for (int j = 0; j < rowsInStrip; j++) {
int row = startRow + j;
@@ -699,17 +763,6 @@ public class TIFFImageReader extends ImageReaderBase {
input.readFully(rowData);
-// for (int k = 0; k < rowData.length; k++) {
-// try {
-// rowData[k] = input.readByte();
-// }
-// catch (IOException e) {
-// Arrays.fill(rowData, k, rowData.length, (byte) -1);
-// System.err.printf("Unexpected EOF or bad data at [%d, %d]\n", col + k, row);
-// break;
-// }
-// }
-
unPredict(predictor, colsInStrip, 1, numBands, rowData);
normalizeBlack(interpretation, rowData);
@@ -725,6 +778,7 @@ public class TIFFImageReader extends ImageReaderBase {
break;
case DataBuffer.TYPE_USHORT:
short [] rowDataShort = ((DataBufferUShort) rowRaster.getDataBuffer()).getData();
+
for (int j = 0; j < rowsInStrip; j++) {
int row = startRow + j;
@@ -751,6 +805,7 @@ public class TIFFImageReader extends ImageReaderBase {
break;
case DataBuffer.TYPE_INT:
int [] rowDataInt = ((DataBufferInt) rowRaster.getDataBuffer()).getData();
+
for (int j = 0; j < rowsInStrip; j++) {
int row = startRow + j;
@@ -861,6 +916,8 @@ public class TIFFImageReader extends ImageReaderBase {
private InputStream createDecoderInputStream(final int compression, final InputStream stream) throws IOException {
switch (compression) {
+ case TIFFBaseline.COMPRESSION_NONE:
+ return stream;
case TIFFBaseline.COMPRESSION_PACKBITS:
return new DecoderStream(stream, new PackBitsDecoder(), 1024);
case TIFFExtension.COMPRESSION_LZW:
@@ -894,7 +951,7 @@ public class TIFFImageReader extends ImageReaderBase {
short[] shorts = (short[]) entry.getValue();
value = new long[shorts.length];
- for (int i = 0, stripOffsetsValueLength = value.length; i < stripOffsetsValueLength; i++) {
+ for (int i = 0, length = value.length; i < length; i++) {
value[i] = shorts[i];
}
}
@@ -902,7 +959,7 @@ public class TIFFImageReader extends ImageReaderBase {
int[] ints = (int[]) entry.getValue();
value = new long[ints.length];
- for (int i = 0, stripOffsetsValueLength = value.length; i < stripOffsetsValueLength; i++) {
+ for (int i = 0, length = value.length; i < length; i++) {
value[i] = ints[i];
}
}
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
new file mode 100644
index 00000000..2a6c1051
--- /dev/null
+++ b/imageio/imageio-tiff/src/main/java/com/twelvemonkeys/imageio/plugins/tiff/YCbCrUpsamplerStream.java
@@ -0,0 +1,336 @@
+/*
+ * Copyright (c) 2013, 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 "TwelveMonkeys" 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 OWNER 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 java.awt.image.DataBufferByte;
+import java.awt.image.Raster;
+import java.io.EOFException;
+import java.io.FilterInputStream;
+import java.io.IOException;
+import java.io.InputStream;
+import java.util.Arrays;
+
+/**
+ * YCbCrUpsamplerStream
+ *
+ * @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 YCbCrUpsamplerStream extends FilterInputStream {
+ static final boolean DEBUG = false;
+
+ private final int horizChromaSub;
+ private final int vertChromaSub;
+ private final double[] coefficients;
+
+ private final int units;
+ private final int unitSize;
+ private final byte[] decodedRows;
+ int decodedLength;
+ int decodedPos;
+
+ private final byte[] buffer;
+ int bufferLength;
+ int bufferPos;
+
+ public YCbCrUpsamplerStream(InputStream stream, int[] chromaSub, int cols, double[] coefficients) {
+ super(stream);
+
+ this.horizChromaSub = chromaSub[0];
+ this.vertChromaSub = chromaSub[1];
+ this.coefficients = Arrays.equals(TIFFImageReader.CCIR_601_1_COEFFICIENTS, coefficients) ? null : coefficients;
+
+ // In TIFF, subsampled streams are stored in "units" of horiz * vert pixels.
+ // For a 4:2 subsampled stream like this:
+ //
+ // Y0 Y1 Y2 Y3 Cb0 Cr0 Y8 Y9 Y10 Y11 Cb1 Cr1
+ // Y4 Y5 Y6 Y7 Y12Y13Y14 Y15
+ //
+ // In the stream, the order is: Y0,Y1,Y2..Y7,Cb0,Cr0, Y8...Y15,Cb1,Cr1, Y16...
+
+ units = cols / horizChromaSub;
+ unitSize = horizChromaSub * vertChromaSub + 2;
+ decodedRows = new byte[cols * vertChromaSub * 3];
+ buffer = new byte[unitSize * units];
+ }
+
+ @Override
+ public int read() throws IOException {
+ if (decodedLength < 0) {
+ return -1;
+ }
+
+ if (decodedPos >= decodedLength) {
+ fetch();
+
+ if (decodedLength < 0) {
+ return -1;
+ }
+ }
+
+ return decodedRows[decodedPos++];
+ }
+
+ 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 = super.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;
+
+ int rowOff = horizChromaSub * units;
+
+ for (int u = 0; u < units; u++) {
+ if (bufferPos >= bufferLength) {
+ throw new EOFException("Unexpected end of stream");
+ }
+
+ // Decode one unit
+ byte cb = buffer[bufferPos + unitSize - 2];
+ byte cr = buffer[bufferPos + unitSize - 1];
+
+ for (int y = 0; y < vertChromaSub; y++) {
+ for (int x = 0; x < horizChromaSub; x++) {
+ int pixelOff = 3 * (rowOff * y + horizChromaSub * u + x);
+
+ decodedRows[pixelOff] = buffer[bufferPos++];
+ decodedRows[pixelOff + 1] = cb;
+ decodedRows[pixelOff + 2] = cr;
+
+ // Convert to RGB
+ if (coefficients == null) {
+ YCbCrConverter.convertYCbCr2RGB(decodedRows, decodedRows, pixelOff);
+ }
+ else {
+ convertYCbCr2RGB(decodedRows, decodedRows, coefficients, pixelOff);
+ }
+ }
+ }
+
+ bufferPos+= 2;
+ }
+
+ bufferPos = bufferLength;
+ decodedPos = 0;
+ }
+
+ @Override
+ public final int read(byte[] b) throws IOException {
+ return read(b, 0, b.length);
+ }
+
+ @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");
+ }
+
+ private void convertYCbCr2RGB(final byte[] yCbCr, final byte[] rgb, final double[] coefficients, final int offset) {
+ // TODO: FixMe: This is bogus...
+ double y = yCbCr[offset ] & 0xff;
+ double cb = yCbCr[offset + 1] & 0xff;
+ double cr = yCbCr[offset + 2] & 0xff;
+
+ double lumaRed = coefficients[0];
+ double lumaGreen = coefficients[1];
+ double lumaBlue = coefficients[2];
+
+ rgb[offset ] = clamp((int) Math.round(cr * (2 - 2 * lumaRed) + y));
+ rgb[offset + 2] = clamp((int) Math.round(cb * (2 - 2 * lumaBlue) + y) - 128);
+ rgb[offset + 1] = clamp((int) Math.round((y - lumaRed * (rgb[offset] & 0xff) - lumaBlue * (rgb[offset + 2] & 0xff)) / lumaGreen));
+ }
+
+ private static byte clamp(int val) {
+ return (byte) Math.max(0, Math.min(255, val));
+ }
+
+ // TODO: This code is copied from JPEG package, make it "more" public: com.tm.imageio.color package?
+ /**
+ * Static inner class for lazy-loading of conversion tables.
+ */
+ static final class YCbCrConverter {
+ /** Define tables for YCC->RGB color space conversion. */
+ private final static int SCALEBITS = 16;
+ private final static int MAXJSAMPLE = 255;
+ private final static int CENTERJSAMPLE = 128;
+ private final static int ONE_HALF = 1 << (SCALEBITS - 1);
+
+ private final static int[] Cr_R_LUT = new int[MAXJSAMPLE + 1];
+ private final static int[] Cb_B_LUT = new int[MAXJSAMPLE + 1];
+ private final static int[] Cr_G_LUT = new int[MAXJSAMPLE + 1];
+ private final static int[] Cb_G_LUT = new int[MAXJSAMPLE + 1];
+
+ /**
+ * Initializes tables for YCC->RGB color space conversion.
+ */
+ private static void buildYCCtoRGBtable() {
+ if (DEBUG) {
+ System.err.println("Building YCC conversion table");
+ }
+
+ for (int i = 0, x = -CENTERJSAMPLE; i <= MAXJSAMPLE; i++, x++) {
+ // i is the actual input pixel value, in the range 0..MAXJSAMPLE
+ // The Cb or Cr value we are thinking of is x = i - CENTERJSAMPLE
+ // Cr=>R value is nearest int to 1.40200 * x
+ Cr_R_LUT[i] = (int) ((1.40200 * (1 << SCALEBITS) + 0.5) * x + ONE_HALF) >> SCALEBITS;
+ // Cb=>B value is nearest int to 1.77200 * x
+ Cb_B_LUT[i] = (int) ((1.77200 * (1 << SCALEBITS) + 0.5) * x + ONE_HALF) >> SCALEBITS;
+ // Cr=>G value is scaled-up -0.71414 * x
+ Cr_G_LUT[i] = -(int) (0.71414 * (1 << SCALEBITS) + 0.5) * x;
+ // Cb=>G value is scaled-up -0.34414 * x
+ // We also add in ONE_HALF so that need not do it in inner loop
+ Cb_G_LUT[i] = -(int) ((0.34414) * (1 << SCALEBITS) + 0.5) * x + ONE_HALF;
+ }
+ }
+
+ static {
+ buildYCCtoRGBtable();
+ }
+
+ static void convertYCbCr2RGB(final Raster raster) {
+ final int height = raster.getHeight();
+ final int width = raster.getWidth();
+ final byte[] data = ((DataBufferByte) raster.getDataBuffer()).getData();
+
+ for (int y = 0; y < height; y++) {
+ for (int x = 0; x < width; x++) {
+ convertYCbCr2RGB(data, data, (x + y * width) * 3);
+ }
+ }
+ }
+
+ static void convertYCbCr2RGB(final byte[] yCbCr, final byte[] rgb, final int offset) {
+ int y = yCbCr[offset ] & 0xff;
+ int cr = yCbCr[offset + 2] & 0xff;
+ int cb = yCbCr[offset + 1] & 0xff;
+
+ rgb[offset ] = clamp(y + Cr_R_LUT[cr]);
+ rgb[offset + 1] = clamp(y + (Cb_G_LUT[cb] + Cr_G_LUT[cr] >> SCALEBITS));
+ rgb[offset + 2] = clamp(y + Cb_B_LUT[cb]);
+ }
+
+ static void convertYCCK2CMYK(final Raster raster) {
+ final int height = raster.getHeight();
+ final int width = raster.getWidth();
+ final byte[] data = ((DataBufferByte) raster.getDataBuffer()).getData();
+
+ for (int y = 0; y < height; y++) {
+ for (int x = 0; x < width; x++) {
+ convertYCCK2CMYK(data, data, (x + y * width) * 4);
+ }
+ }
+ }
+
+ private static void convertYCCK2CMYK(byte[] ycck, byte[] cmyk, int offset) {
+ // Inverted
+ int y = 255 - ycck[offset ] & 0xff;
+ int cb = 255 - ycck[offset + 1] & 0xff;
+ int cr = 255 - ycck[offset + 2] & 0xff;
+ int k = 255 - ycck[offset + 3] & 0xff;
+
+ int cmykC = MAXJSAMPLE - (y + Cr_R_LUT[cr]);
+ int cmykM = MAXJSAMPLE - (y + (Cb_G_LUT[cb] + Cr_G_LUT[cr] >> SCALEBITS));
+ int cmykY = MAXJSAMPLE - (y + Cb_B_LUT[cb]);
+
+ cmyk[offset ] = clamp(cmykC);
+ cmyk[offset + 1] = clamp(cmykM);
+ cmyk[offset + 2] = clamp(cmykY);
+ cmyk[offset + 3] = (byte) k; // K passes through unchanged
+ }
+
+// 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/YCbCrUpsamplerStreamTest.java b/imageio/imageio-tiff/src/test/java/com/twelvemonkeys/imageio/plugins/tiff/YCbCrUpsamplerStreamTest.java
new file mode 100644
index 00000000..6a031c93
--- /dev/null
+++ b/imageio/imageio-tiff/src/test/java/com/twelvemonkeys/imageio/plugins/tiff/YCbCrUpsamplerStreamTest.java
@@ -0,0 +1,51 @@
+/*
+ * Copyright (c) 2013, 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 "TwelveMonkeys" 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 OWNER 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.io.InputStreamAbstractTestCase;
+import org.junit.Ignore;
+
+import java.io.ByteArrayInputStream;
+import java.io.InputStream;
+
+/**
+ * YCbCrUpsamplerStreamTest
+ *
+ * @author Harald Kuhr
+ * @author last modified by $Author: haraldk$
+ * @version $Id: YCbCrUpsamplerStreamTest.java,v 1.0 31.01.13 14:35 haraldk Exp$
+ */
+@Ignore
+public class YCbCrUpsamplerStreamTest extends InputStreamAbstractTestCase {
+ // TODO: Implement
+ @Override
+ protected InputStream makeInputStream(byte[] pBytes) {
+ return new YCbCrUpsamplerStream(new ByteArrayInputStream(pBytes), new int[] {2, 2}, pBytes.length / 4, null);
+ }
+}