From 47fbf473dbdebceefe23ca1e6dbe84c7a04cd2b7 Mon Sep 17 00:00:00 2001 From: Harald Kuhr Date: Thu, 31 Jan 2013 15:38:45 +0100 Subject: [PATCH] TMI-TIFF: Implemented YCbCr reading. --- .../imageio/plugins/tiff/LZWDecoder.java | 6 +- .../imageio/plugins/tiff/TIFFImageReader.java | 95 ++++- .../plugins/tiff/YCbCrUpsamplerStream.java | 336 ++++++++++++++++++ .../tiff/YCbCrUpsamplerStreamTest.java | 51 +++ 4 files changed, 467 insertions(+), 21 deletions(-) create mode 100644 imageio/imageio-tiff/src/main/java/com/twelvemonkeys/imageio/plugins/tiff/YCbCrUpsamplerStream.java create mode 100644 imageio/imageio-tiff/src/test/java/com/twelvemonkeys/imageio/plugins/tiff/YCbCrUpsamplerStreamTest.java 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); + } +}