From 1ff764997b2af8a43f9cd23c2ccc3b2e7d8c2f29 Mon Sep 17 00:00:00 2001 From: Harald Kuhr Date: Mon, 29 Sep 2014 14:50:28 +0200 Subject: [PATCH] TMI-TIFF: 16 bit YCbCr support + minor improvements --- .../tiff/HorizontalDeDifferencingStream.java | 23 +- .../imageio/plugins/tiff/LZWDecoder.java | 73 ++++- .../imageio/plugins/tiff/TIFFImageReader.java | 16 +- .../plugins/tiff/YCbCr16UpsamplerStream.java | 283 ++++++++++++++++++ .../plugins/tiff/YCbCrUpsamplerStream.java | 11 +- .../tiff/YCbCr16UpsamplerStreamTest.java | 150 ++++++++++ .../tiff/YCbCrUpsamplerStreamTest.java | 7 +- 7 files changed, 526 insertions(+), 37 deletions(-) create mode 100644 imageio/imageio-tiff/src/main/java/com/twelvemonkeys/imageio/plugins/tiff/YCbCr16UpsamplerStream.java create mode 100644 imageio/imageio-tiff/src/test/java/com/twelvemonkeys/imageio/plugins/tiff/YCbCr16UpsamplerStreamTest.java diff --git a/imageio/imageio-tiff/src/main/java/com/twelvemonkeys/imageio/plugins/tiff/HorizontalDeDifferencingStream.java b/imageio/imageio-tiff/src/main/java/com/twelvemonkeys/imageio/plugins/tiff/HorizontalDeDifferencingStream.java index 04b86dc0..b4e9ce79 100644 --- a/imageio/imageio-tiff/src/main/java/com/twelvemonkeys/imageio/plugins/tiff/HorizontalDeDifferencingStream.java +++ b/imageio/imageio-tiff/src/main/java/com/twelvemonkeys/imageio/plugins/tiff/HorizontalDeDifferencingStream.java @@ -57,12 +57,12 @@ final class HorizontalDeDifferencingStream extends InputStream { private final ByteBuffer buffer; public HorizontalDeDifferencingStream(final InputStream stream, final int columns, final int samplesPerPixel, final int bitsPerSample, final ByteOrder byteOrder) { - channel = Channels.newChannel(Validate.notNull(stream, "stream")); - this.columns = Validate.isTrue(columns > 0, columns, "width must be greater than 0"); this.samplesPerPixel = Validate.isTrue(bitsPerSample >= 8 || samplesPerPixel == 1, samplesPerPixel, "Unsupported samples per pixel for < 8 bit samples: %s"); this.bitsPerSample = Validate.isTrue(isValidBPS(bitsPerSample), bitsPerSample, "Unsupported bits per sample value: %s"); + channel = Channels.newChannel(Validate.notNull(stream, "stream")); + buffer = ByteBuffer.allocate((columns * samplesPerPixel * bitsPerSample + 7) / 8).order(byteOrder); buffer.flip(); } @@ -113,10 +113,15 @@ final class HorizontalDeDifferencingStream extends InputStream { int sample = 0; byte temp; + // Optimization: + // Access array directly for <= 8 bits per sample, as buffer does extra index bounds check for every + // put/get operation... (Measures to about 100 ms difference for 4000 x 3000 image) + final byte[] array = buffer.array(); + switch (bitsPerSample) { case 1: for (int b = 0; b < (columns + 7) / 8; b++) { - original = buffer.get(b); + original = array[b]; sample += (original >> 7) & 0x1; temp = (byte) ((sample << 7) & 0x80); sample += (original >> 6) & 0x1; @@ -132,13 +137,13 @@ final class HorizontalDeDifferencingStream extends InputStream { sample += (original >> 1) & 0x1; temp |= (byte) ((sample << 1) & 0x02); sample += original & 0x1; - buffer.put(b, (byte) (temp | sample & 0x1)); + array[b] = (byte) (temp | sample & 0x1); } break; case 2: for (int b = 0; b < (columns + 3) / 4; b++) { - original = buffer.get(b); + original = array[b]; sample += (original >> 6) & 0x3; temp = (byte) ((sample << 6) & 0xc0); sample += (original >> 4) & 0x3; @@ -146,17 +151,17 @@ final class HorizontalDeDifferencingStream extends InputStream { sample += (original >> 2) & 0x3; temp |= (byte) ((sample << 2) & 0x0c); sample += original & 0x3; - buffer.put(b, (byte) (temp | sample & 0x3)); + array[b] = (byte) (temp | sample & 0x3); } break; case 4: for (int b = 0; b < (columns + 1) / 2; b++) { - original = buffer.get(b); + original = array[b]; sample += (original >> 4) & 0xf; temp = (byte) ((sample << 4) & 0xf0); sample += original & 0x0f; - buffer.put(b, (byte) (temp | sample & 0xf)); + array[b] = (byte) (temp | sample & 0xf); } break; @@ -164,7 +169,7 @@ final class HorizontalDeDifferencingStream extends InputStream { for (int x = 1; x < columns; x++) { for (int b = 0; b < samplesPerPixel; b++) { int off = x * samplesPerPixel + b; - buffer.put(off, (byte) (buffer.get(off - samplesPerPixel) + buffer.get(off))); + array[off] = (byte) (array[off - samplesPerPixel] + array[off]); } } break; 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 789190d6..1cb62ba3 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 @@ -33,6 +33,7 @@ import com.twelvemonkeys.io.enc.Decoder; import java.io.IOException; import java.io.InputStream; +import java.lang.String; import java.nio.ByteBuffer; /** @@ -58,7 +59,7 @@ abstract class LZWDecoder implements Decoder { private final boolean compatibilityMode; - private final String[] table; + private final LZWString[] table; private int tableLength; int bitsPerCode; private int oldCode = CLEAR_CODE; @@ -73,11 +74,11 @@ abstract class LZWDecoder implements Decoder { protected LZWDecoder(final boolean compatibilityMode) { this.compatibilityMode = compatibilityMode; - table = new String[compatibilityMode ? TABLE_SIZE + 1024 : TABLE_SIZE]; // libTiff adds 1024 "for compatibility"... + table = new LZWString[compatibilityMode ? TABLE_SIZE + 1024 : TABLE_SIZE]; // 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); + table[i] = new LZWString((byte) i); } init(); @@ -112,12 +113,17 @@ abstract class LZWDecoder implements Decoder { table[code].writeTo(buffer); } else { + if (table[oldCode] == null) { + System.err.println("tableLength: " + tableLength); + System.err.println("oldCode: " + oldCode); + } + if (isInTable(code)) { table[code].writeTo(buffer); addStringToTable(table[oldCode].concatenate(table[code].firstChar)); } else { - String outString = table[oldCode].concatenate(table[oldCode].firstChar); + LZWString outString = table[oldCode].concatenate(table[oldCode].firstChar); outString.writeTo(buffer); addStringToTable(outString); @@ -135,7 +141,7 @@ abstract class LZWDecoder implements Decoder { return buffer.position(); } - private void addStringToTable(final String string) throws IOException { + private void addStringToTable(final LZWString string) throws IOException { table[tableLength++] = string; if (tableLength > maxCode) { @@ -146,7 +152,7 @@ abstract class LZWDecoder implements Decoder { bitsPerCode--; } else { - throw new DecodeException(java.lang.String.format("TIFF LZW with more than %d bits per code encountered (table overflow)", MAX_BITS)); + throw new DecodeException(String.format("TIFF LZW with more than %d bits per code encountered (table overflow)", MAX_BITS)); } } @@ -279,26 +285,26 @@ abstract class LZWDecoder implements Decoder { } } - private static final class String { - final String previous; + static final class LZWString { + final LZWString previous; final int length; final byte value; final byte firstChar; // Copied forward for fast access - public String(final byte code) { + public LZWString(final byte code) { this(code, code, 1, null); } - private String(final byte value, final byte firstChar, final int length, final String previous) { + private LZWString(final byte value, final byte firstChar, final int length, final LZWString previous) { this.value = value; this.firstChar = firstChar; this.length = length; this.previous = previous; } - public final String concatenate(final byte firstChar) { - return new String(firstChar, this.firstChar, length + 1, this); + public final LZWString concatenate(final byte firstChar) { + return new LZWString(firstChar, this.firstChar, length + 1, this); } public final void writeTo(final ByteBuffer buffer) { @@ -310,7 +316,7 @@ abstract class LZWDecoder implements Decoder { buffer.put(value); } else { - String e = this; + LZWString e = this; final int offset = buffer.position(); for (int i = length - 1; i >= 0; i--) { @@ -321,6 +327,47 @@ abstract class LZWDecoder implements Decoder { buffer.position(offset + length); } } + + @Override + public String toString() { + StringBuilder builder = new StringBuilder("ZLWString["); + int offset = builder.length(); + LZWString e = this; + for (int i = length - 1; i >= 0; i--) { + builder.insert(offset, String.format("%2x", e.value)); + e = e.previous; + } + builder.append("]"); + return builder.toString(); + } + + @Override + public boolean equals(final Object other) { + if (this == other) { + return true; + } + if (other == null || getClass() != other.getClass()) { + return false; + } + + LZWString string = (LZWString) other; + + return firstChar == string.firstChar && + length == string.length && + value == string.value && +// !(previous != null ? !previous.equals(string.previous) : string.previous != null); + previous == string.previous; + } + + @Override + public int hashCode() { + int result = previous != null ? previous.hashCode() : 0; + result = 31 * result + length; + result = 31 * result + (int) value; + result = 31 * result + (int) firstChar; + return result; + } + } } 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 5e6f4501..a78b9d89 100755 --- 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 @@ -274,7 +274,7 @@ public class TIFFImageReader extends ImageReaderBase { case TIFFExtension.PHOTOMETRIC_YCBCR: // JPEG reader will handle YCbCr to RGB for us, otherwise we'll convert while reading - // TODO: Sanity check that we have SamplesPerPixel == 3, BitsPerSample == [8,8,8] and Compression == 1 (none), 5 (LZW), or 6 (JPEG) + // TODO: Sanity check that we have SamplesPerPixel == 3, BitsPerSample == [8,8,8] (or [16,16,16]) and Compression == 1 (none), 5 (LZW), or 6 (JPEG) case TIFFBaseline.PHOTOMETRIC_RGB: // RGB cs = profile == null ? ColorSpace.getInstance(ColorSpace.CS_sRGB) : ColorSpaces.createColorSpace(profile); @@ -617,9 +617,16 @@ public class TIFFImageReader extends ImageReaderBase { adapter = createDecompressorStream(compression, width, adapter); adapter = createUnpredictorStream(predictor, width, numBands, getBitsPerSample(), adapter, imageInput.getByteOrder()); - if (interpretation == TIFFExtension.PHOTOMETRIC_YCBCR) { + if (interpretation == TIFFExtension.PHOTOMETRIC_YCBCR && rowRaster.getTransferType() == DataBuffer.TYPE_BYTE) { adapter = new YCbCrUpsamplerStream(adapter, yCbCrSubsampling, yCbCrPos, colsInTile, yCbCrCoefficients); } + else if (interpretation == TIFFExtension.PHOTOMETRIC_YCBCR && rowRaster.getTransferType() == DataBuffer.TYPE_USHORT) { + adapter = new YCbCr16UpsamplerStream(adapter, yCbCrSubsampling, yCbCrPos, colsInTile, yCbCrCoefficients, imageInput.getByteOrder()); + } + else if (interpretation == TIFFExtension.PHOTOMETRIC_YCBCR) { + // Handled in getRawImageType + throw new AssertionError(); + } // According to the spec, short/long/etc should follow order of containing stream input = imageInput.getByteOrder() == ByteOrder.BIG_ENDIAN @@ -765,8 +772,9 @@ public class TIFFImageReader extends ImageReaderBase { if (jpegOffset != -1) { // Straight forward case: We're good to go! We'll disregard tiling and any tables tags - - if (currentIFD.getEntryById(TIFF.TAG_OLD_JPEG_Q_TABLES) != null || currentIFD.getEntryById(TIFF.TAG_OLD_JPEG_DC_TABLES) != null || currentIFD.getEntryById(TIFF.TAG_OLD_JPEG_AC_TABLES) != null) { + if (currentIFD.getEntryById(TIFF.TAG_OLD_JPEG_Q_TABLES) != null + || currentIFD.getEntryById(TIFF.TAG_OLD_JPEG_DC_TABLES) != null + || currentIFD.getEntryById(TIFF.TAG_OLD_JPEG_AC_TABLES) != null) { processWarningOccurred("Old-style JPEG compressed TIFF with JFIF stream encountered. Ignoring JPEG tables. Reading as single tile."); } else { diff --git a/imageio/imageio-tiff/src/main/java/com/twelvemonkeys/imageio/plugins/tiff/YCbCr16UpsamplerStream.java b/imageio/imageio-tiff/src/main/java/com/twelvemonkeys/imageio/plugins/tiff/YCbCr16UpsamplerStream.java new file mode 100644 index 00000000..8f3d591a --- /dev/null +++ b/imageio/imageio-tiff/src/main/java/com/twelvemonkeys/imageio/plugins/tiff/YCbCr16UpsamplerStream.java @@ -0,0 +1,283 @@ +/* + * Copyright (c) 2014, 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.lang.Validate; + +import java.io.EOFException; +import java.io.FilterInputStream; +import java.io.IOException; +import java.io.InputStream; +import java.nio.ByteOrder; + +/** + * Input stream that provides on-the-fly conversion and upsampling of TIFF subsampled YCbCr 16 bit samples + * to (raw) RGB 16 bit 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 YCbCr16UpsamplerStream extends FilterInputStream { + // TODO: As we deal with short/16 bit samples, we need to take byte order into account + private final int horizChromaSub; + private final int vertChromaSub; + private final int yCbCrPos; + private final int columns; + private final double[] coefficients; + private final ByteOrder byteOrder; + + private final int units; + private final int unitSize; + private final int padding; + private final byte[] decodedRows; + + int decodedLength; + int decodedPos; + + private final byte[] buffer; + int bufferLength; + int bufferPos; + + public YCbCr16UpsamplerStream(final InputStream stream, final int[] chromaSub, final int yCbCrPos, final int columns, final double[] coefficients, final ByteOrder byteOrder) { + super(Validate.notNull(stream, "stream")); + + Validate.notNull(chromaSub, "chromaSub"); + Validate.isTrue(chromaSub.length == 2, "chromaSub.length != 2"); + Validate.notNull(byteOrder, "byteOrder"); + + this.horizChromaSub = chromaSub[0]; + this.vertChromaSub = chromaSub[1]; + this.yCbCrPos = yCbCrPos; + this.columns = columns; + this.coefficients = coefficients == null ? YCbCrUpsamplerStream.CCIR_601_1_COEFFICIENTS : coefficients; + this.byteOrder = byteOrder; + + // 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... + + unitSize = 2 * (horizChromaSub * vertChromaSub + 2); + units = (columns + horizChromaSub - 1) / horizChromaSub; // If columns % horizChromasSub != 0... + padding = 2 * (units * horizChromaSub - columns); // ...each coded row will be padded to fill unit + + decodedRows = new byte[2 * columns * vertChromaSub * 3]; + buffer = new byte[unitSize * 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 (bufferPos >= bufferLength) { + throw new EOFException("Unexpected end of stream"); + } + + // Decode one unit + byte cb1 = buffer[bufferPos + unitSize - 4]; + byte cb2 = buffer[bufferPos + unitSize - 3]; + byte cr1 = buffer[bufferPos + unitSize - 2]; + byte cr2 = buffer[bufferPos + unitSize - 1]; + + 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) { + bufferPos += padding; + break; + } + + int pixelOff = 2 * 3 * (column + columns * y); + + decodedRows[pixelOff ] = buffer[bufferPos++]; + decodedRows[pixelOff + 1] = buffer[bufferPos++]; + decodedRows[pixelOff + 2] = cb1; + decodedRows[pixelOff + 3] = cb2; + decodedRows[pixelOff + 4] = cr1; + decodedRows[pixelOff + 5] = cr2; + + // Convert to RGB + convertYCbCr2RGB(decodedRows, decodedRows, coefficients, pixelOff); + } + } + + bufferPos += 2 * 2; // Skip CbCr bytes at end of unit + } + + 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"); + } + + private void convertYCbCr2RGB(final byte[] yCbCr, final byte[] rgb, final double[] coefficients, final int offset) { + int y; + int cb; + int cr; + + // Short values, depends on byte order! + if (byteOrder == ByteOrder.BIG_ENDIAN) { + y = ((yCbCr[offset ] & 0xff) << 8) | (yCbCr[offset + 1] & 0xff); + cb = (((yCbCr[offset + 2] & 0xff) << 8) | (yCbCr[offset + 3] & 0xff)) - 32768; + cr = (((yCbCr[offset + 4] & 0xff) << 8) | (yCbCr[offset + 5] & 0xff)) - 32768; + } + else { + y = ((yCbCr[offset + 1] & 0xff) << 8) | (yCbCr[offset ] & 0xff); + cb = (((yCbCr[offset + 3] & 0xff) << 8) | (yCbCr[offset + 2] & 0xff)) - 32768; + cr = (((yCbCr[offset + 5] & 0xff) << 8) | (yCbCr[offset + 4] & 0xff)) - 32768; + } + + double lumaRed = coefficients[0]; + double lumaGreen = coefficients[1]; + double lumaBlue = coefficients[2]; + + int red = (int) Math.round(cr * (2 - 2 * lumaRed) + y); + int blue = (int) Math.round(cb * (2 - 2 * lumaBlue) + y); + int green = (int) Math.round((y - lumaRed * (red) - lumaBlue * (blue)) / lumaGreen); + + short r = clampShort(red); + short g = clampShort(green); + short b = clampShort(blue); + + // Short values, depends on byte order! + if (byteOrder == ByteOrder.BIG_ENDIAN) { + rgb[offset ] = (byte) ((r >>> 8) & 0xff); + rgb[offset + 1] = (byte) (r & 0xff); + rgb[offset + 2] = (byte) ((g >>> 8) & 0xff); + rgb[offset + 3] = (byte) (g & 0xff); + rgb[offset + 4] = (byte) ((b >>> 8) & 0xff); + rgb[offset + 5] = (byte) (b & 0xff); + } + else { + rgb[offset ] = (byte) (r & 0xff); + rgb[offset + 1] = (byte) ((r >>> 8) & 0xff); + rgb[offset + 2] = (byte) (g & 0xff); + rgb[offset + 3] = (byte) ((g >>> 8) & 0xff); + rgb[offset + 4] = (byte) (b & 0xff); + rgb[offset + 5] = (byte) ((b >>> 8) & 0xff); + } + } + + private short clampShort(int val) { + return (short) Math.max(0, Math.min(0xffff, val)); + } +} 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 c0826c4c..82ab8a17 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 @@ -39,7 +39,7 @@ import java.io.InputStream; import java.util.Arrays; /** - * Input stream that provides on-the-fly conversion and upsampling of TIFF susampled YCbCr samples to (raw) RGB samples. + * Input stream that provides on-the-fly conversion and upsampling of TIFF subsampled YCbCr samples to (raw) RGB samples. * * @author Harald Kuhr * @author last modified by $Author: haraldk$ @@ -229,7 +229,7 @@ final class YCbCrUpsamplerStream extends FilterInputStream { private void convertYCbCr2RGB(final byte[] yCbCr, final byte[] rgb, final double[] coefficients, final int offset) { double y = (yCbCr[offset ] & 0xff); - double cb = (yCbCr[offset + 1] & 0xff) - 128; // TODO: The -128 part seems bogus... Consult ReferenceBlackWhite??? But default to these values? + double cb = (yCbCr[offset + 1] & 0xff) - 128; double cr = (yCbCr[offset + 2] & 0xff) - 128; double lumaRed = coefficients[0]; @@ -238,7 +238,7 @@ final class YCbCrUpsamplerStream extends FilterInputStream { int red = (int) Math.round(cr * (2 - 2 * lumaRed) + y); int blue = (int) Math.round(cb * (2 - 2 * lumaBlue) + y); - int green = (int) Math.round((y - lumaRed * (rgb[offset] & 0xff) - lumaBlue * (rgb[offset + 2] & 0xff)) / lumaGreen); + int green = (int) Math.round((y - lumaRed * red - lumaBlue * blue) / lumaGreen); rgb[offset ] = clamp(red); rgb[offset + 2] = clamp(blue); @@ -342,10 +342,5 @@ final class YCbCrUpsamplerStream extends FilterInputStream { 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/YCbCr16UpsamplerStreamTest.java b/imageio/imageio-tiff/src/test/java/com/twelvemonkeys/imageio/plugins/tiff/YCbCr16UpsamplerStreamTest.java new file mode 100644 index 00000000..04b65723 --- /dev/null +++ b/imageio/imageio-tiff/src/test/java/com/twelvemonkeys/imageio/plugins/tiff/YCbCr16UpsamplerStreamTest.java @@ -0,0 +1,150 @@ +/* + * 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.LittleEndianDataInputStream; +import org.junit.Test; + +import java.io.ByteArrayInputStream; +import java.io.DataInputStream; +import java.io.IOException; +import java.io.InputStream; +import java.nio.ByteBuffer; +import java.nio.ByteOrder; + +import static org.junit.Assert.*; + +/** + * YCbCr16UpsamplerStreamTest + * + * @author Harald Kuhr + * @author last modified by $Author: haraldk$ + * @version $Id: YCbCr16UpsamplerStreamTest.java,v 1.0 31.01.13 14:35 haraldk Exp$ + */ +public class YCbCr16UpsamplerStreamTest { + @Test(expected = IllegalArgumentException.class) + public void testCreateNullStream() { + new YCbCr16UpsamplerStream(null, new int[2], 7, 5, null, ByteOrder.LITTLE_ENDIAN); + } + + @Test(expected = IllegalArgumentException.class) + public void testCreateNullChroma() { + new YCbCr16UpsamplerStream(new ByteArrayInputStream(new byte[0]), new int[3], 7, 5, null, ByteOrder.LITTLE_ENDIAN); + } + + @Test(expected = IllegalArgumentException.class) + public void testCreateShortChroma() { + new YCbCr16UpsamplerStream(new ByteArrayInputStream(new byte[0]), new int[1], 7, 5, null, ByteOrder.LITTLE_ENDIAN); + } + + @Test(expected = IllegalArgumentException.class) + public void testCreateNoByteOrder() { + new YCbCr16UpsamplerStream(new ByteArrayInputStream(new byte[0]), new int[] {2, 2}, 7, 5, null, null); + } + + // TODO: The expected values seems bogus... + // But visually, it looks okay for the one and only sample image I've got... + + @Test + public void testUpsample22() throws IOException { + short[] shorts = new short[] { + 1, 2, 3, 4, 5, 6, 7, 8, 42, 96, + 108, 109, 110, 111, 112, 113, 114, 115, 43, 97 + }; + + byte[] bytes = new byte[shorts.length * 2]; + ByteBuffer.wrap(bytes).order(ByteOrder.LITTLE_ENDIAN).asShortBuffer().put(shorts); + + InputStream stream = new YCbCr16UpsamplerStream(new ByteArrayInputStream(bytes), new int[] {2, 2}, TIFFExtension.YCBCR_POSITIONING_CENTERED, 8, null, ByteOrder.LITTLE_ENDIAN); + + short[] expected = new short[] { + 0, -30864, 0, 0, -30863, 0, 0, -30966, 0, 0, -30965, 0, 0, -30870, 0, 0, -30869, 0, 0, -30815, 0, 0, -30761, 0, + 0, -30862, 0, 0, -30861, 0, 0, -30931, 0, 0, -30877, 0, 0, -30868, 0, 0, -30867, 0, 0, -30858, 0, 0, -30858, 0 + }; + short[] upsampled = new short[expected.length]; + + LittleEndianDataInputStream dataInput = new LittleEndianDataInputStream(stream); + for (int i = 0; i < upsampled.length; i++) { + upsampled[i] = dataInput.readShort(); + } + + assertArrayEquals(expected, upsampled); + assertEquals(-1, stream.read()); + } + + @Test + public void testUpsample21() throws IOException { + short[] shorts = new short[] { + 1, 2, 3, 4, 42, 96, 77, + 112, 113, 114, 115, 43, 97, 43 + }; + + byte[] bytes = new byte[shorts.length * 2]; + ByteBuffer.wrap(bytes).order(ByteOrder.BIG_ENDIAN).asShortBuffer().put(shorts); + InputStream stream = new YCbCr16UpsamplerStream(new ByteArrayInputStream(bytes), new int[] {2, 1}, TIFFExtension.YCBCR_POSITIONING_CENTERED, 4, null, ByteOrder.BIG_ENDIAN); + + short[] expected = new short[] { + 0, -30861, 0, 0, -30860, 0, 0, -30923, 0, 0, -30869, 0, 0, -30816, 0, 0, -30815, 0, 0, -30868, 0, 0, -30922, 0 + }; + short[] upsampled = new short[expected.length]; + + DataInputStream dataInput = new DataInputStream(stream); + for (int i = 0; i < upsampled.length; i++) { + upsampled[i] = dataInput.readShort(); + } + + assertArrayEquals(expected, upsampled); + assertEquals(-1, stream.read()); + } + + @Test + public void testUpsample12() throws IOException { + short[] shorts = new short[] { + 1, 2, 3, 4, 42, 96, 77, + 112, 113, 114, 115, 43, 97, 43 + }; + + byte[] bytes = new byte[shorts.length * 2]; + ByteBuffer.wrap(bytes).order(ByteOrder.BIG_ENDIAN).asShortBuffer().put(shorts); + InputStream stream = new YCbCr16UpsamplerStream(new ByteArrayInputStream(bytes), new int[] {1, 2}, TIFFExtension.YCBCR_POSITIONING_CENTERED, 4, null, ByteOrder.BIG_ENDIAN); + + short[] expected = new short[] { + 0, -30861, 0, 0, -30923, 0, 0, -30816, 0, 0, -30761, 0, 0, -30860, 0, 0, -30869, 0, 0, -30815, 0, 0, -30815, 0 + }; + short[] upsampled = new short[expected.length]; + + DataInputStream dataInput = new DataInputStream(stream); + for (int i = 0; i < upsampled.length; i++) { + upsampled[i] = dataInput.readShort(); + } + + assertArrayEquals(expected, upsampled); + assertEquals(-1, stream.read()); + } +} 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 index ebb4a236..a0faaced 100644 --- 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 @@ -33,6 +33,7 @@ import org.junit.Test; import java.io.ByteArrayInputStream; import java.io.DataInputStream; import java.io.IOException; +import java.io.InputStream; import static org.junit.Assert.*; @@ -65,7 +66,7 @@ public class YCbCrUpsamplerStreamTest { 1, 2, 3, 4, 5, 6, 7, 8, 42, 96, 108, 109, 110, 111, 112, 113, 114, 115, 43, 97 }; - YCbCrUpsamplerStream stream = new YCbCrUpsamplerStream(new ByteArrayInputStream(bytes), new int[] {2, 2}, TIFFExtension.YCBCR_POSITIONING_CENTERED, 8, null); + InputStream stream = new YCbCrUpsamplerStream(new ByteArrayInputStream(bytes), new int[] {2, 2}, TIFFExtension.YCBCR_POSITIONING_CENTERED, 8, null); byte[] expected = new byte[] { 0, -126, 0, 0, -125, 0, 0, 27, 0, 0, 28, 0, 92, 124, 85, 93, 125, 86, 0, -78, 0, 0, -24, 0, @@ -85,7 +86,7 @@ public class YCbCrUpsamplerStreamTest { 1, 2, 3, 4, 42, 96, 77, 112, 113, 114, 115, 43, 97, 43 }; - YCbCrUpsamplerStream stream = new YCbCrUpsamplerStream(new ByteArrayInputStream(bytes), new int[] {2, 1}, TIFFExtension.YCBCR_POSITIONING_CENTERED, 4, null); + InputStream stream = new YCbCrUpsamplerStream(new ByteArrayInputStream(bytes), new int[] {2, 1}, TIFFExtension.YCBCR_POSITIONING_CENTERED, 4, null); byte[] expected = new byte[] { 0, -123, 0, 0, -122, 0, 20, 71, 0, 74, 125, 6, 0, -78, 90, 0, -77, 91, 75, 126, 7, 21, 72, 0 @@ -104,7 +105,7 @@ public class YCbCrUpsamplerStreamTest { 1, 2, 3, 4, 42, 96, 77, 112, 113, 114, 115, 43, 97, 43 }; - YCbCrUpsamplerStream stream = new YCbCrUpsamplerStream(new ByteArrayInputStream(bytes), new int[] {1, 2}, TIFFExtension.YCBCR_POSITIONING_CENTERED, 4, null); + InputStream stream = new YCbCrUpsamplerStream(new ByteArrayInputStream(bytes), new int[] {1, 2}, TIFFExtension.YCBCR_POSITIONING_CENTERED, 4, null); byte[] expected = new byte[] { 0, -123, 0, 20, 71, 0, 0, -78, 90, 0, -24, 0, 0, -122, 0, 74, 125, 6, 0, -77, 91, 0, -78, 0