mirror of
https://github.com/haraldk/TwelveMonkeys.git
synced 2025-08-02 19:15:29 -04:00
TMI-TIFF: Implemented YCbCr reading.
This commit is contained in:
parent
8c4f9d3ed6
commit
47fbf473db
@ -35,7 +35,8 @@ import java.io.IOException;
|
|||||||
import java.io.InputStream;
|
import java.io.InputStream;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* LZWDecoder
|
* Implements Lempel-Ziv & Welch (LZW) decompression.
|
||||||
|
* Inspired by libTiff's LZW decompression.
|
||||||
*
|
*
|
||||||
* @author <a href="mailto:harald.kuhr@gmail.com">Harald Kuhr</a>
|
* @author <a href="mailto:harald.kuhr@gmail.com">Harald Kuhr</a>
|
||||||
* @author last modified by $Author: haraldk$
|
* @author last modified by $Author: haraldk$
|
||||||
@ -67,8 +68,9 @@ abstract class LZWDecoder implements Decoder {
|
|||||||
protected LZWDecoder(final boolean compatibilityMode) {
|
protected LZWDecoder(final boolean compatibilityMode) {
|
||||||
this.compatibilityMode = 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++) {
|
for (int i = 0; i < 256; i++) {
|
||||||
table[i] = new String((byte) i);
|
table[i] = new String((byte) i);
|
||||||
}
|
}
|
||||||
|
@ -35,6 +35,7 @@ import com.twelvemonkeys.imageio.metadata.CompoundDirectory;
|
|||||||
import com.twelvemonkeys.imageio.metadata.Directory;
|
import com.twelvemonkeys.imageio.metadata.Directory;
|
||||||
import com.twelvemonkeys.imageio.metadata.Entry;
|
import com.twelvemonkeys.imageio.metadata.Entry;
|
||||||
import com.twelvemonkeys.imageio.metadata.exif.EXIFReader;
|
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.metadata.exif.TIFF;
|
||||||
import com.twelvemonkeys.imageio.stream.ByteArrayImageInputStream;
|
import com.twelvemonkeys.imageio.stream.ByteArrayImageInputStream;
|
||||||
import com.twelvemonkeys.imageio.stream.SubImageInputStream;
|
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"));
|
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 CompoundDirectory IFDs;
|
||||||
private Directory currentIFD;
|
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
|
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 planarConfiguration = getValueAsIntWithDefault(TIFF.TAG_PLANAR_CONFIGURATION, TIFFExtension.PLANARCONFIG_PLANAR);
|
||||||
int interpretation = getValueAsInt(TIFF.TAG_PHOTOMETRIC_INTERPRETATION, "PhotometricInterpretation");
|
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 bitsPerSample = getBitsPerSample();
|
||||||
int dataType = bitsPerSample <= 8 ? DataBuffer.TYPE_BYTE : bitsPerSample <= 16 ? DataBuffer.TYPE_USHORT : DataBuffer.TYPE_INT;
|
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:
|
case TIFFExtension.PHOTOMETRIC_YCBCR:
|
||||||
// JPEG reader will handle YCbCr to RGB for us, we'll have to do it ourselves if not JPEG...
|
// 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?)
|
// TODO: Handle YCbCrSubsampling (up-scaler stream, or read data as-is + up-sample (sub-)raster after read? Apply smoothing?)
|
||||||
case TIFFBaseline.PHOTOMETRIC_RGB:
|
case TIFFBaseline.PHOTOMETRIC_RGB:
|
||||||
// RGB
|
// RGB
|
||||||
@ -427,6 +432,10 @@ public class TIFFImageReader extends ImageReaderBase {
|
|||||||
public BufferedImage read(int imageIndex, ImageReadParam param) throws IOException {
|
public BufferedImage read(int imageIndex, ImageReadParam param) throws IOException {
|
||||||
readIFD(imageIndex);
|
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 width = getWidth(imageIndex);
|
||||||
int height = getHeight(imageIndex);
|
int height = getHeight(imageIndex);
|
||||||
|
|
||||||
@ -496,8 +505,53 @@ public class TIFFImageReader extends ImageReaderBase {
|
|||||||
case TIFFExtension.COMPRESSION_ZLIB:
|
case TIFFExtension.COMPRESSION_ZLIB:
|
||||||
// 'Adobe-style' Deflate
|
// '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
|
// General uncompressed/compressed reading
|
||||||
for (int y = 0; y < tilesDown; y++) {
|
for (int y = 0; y < tilesDown; y++) {
|
||||||
int col = 0;
|
int col = 0;
|
||||||
@ -510,7 +564,8 @@ public class TIFFImageReader extends ImageReaderBase {
|
|||||||
imageInput.seek(stripTileOffsets[i]);
|
imageInput.seek(stripTileOffsets[i]);
|
||||||
|
|
||||||
DataInput input;
|
DataInput input;
|
||||||
if (compression == TIFFBaseline.COMPRESSION_NONE) {
|
if (compression == TIFFBaseline.COMPRESSION_NONE && interpretation != TIFFExtension.PHOTOMETRIC_YCBCR) {
|
||||||
|
// No need for transformation, fast forward
|
||||||
input = imageInput;
|
input = imageInput;
|
||||||
}
|
}
|
||||||
else {
|
else {
|
||||||
@ -518,11 +573,16 @@ public class TIFFImageReader extends ImageReaderBase {
|
|||||||
? IIOUtil.createStreamAdapter(imageInput, stripTileByteCounts[i])
|
? IIOUtil.createStreamAdapter(imageInput, stripTileByteCounts[i])
|
||||||
: IIOUtil.createStreamAdapter(imageInput);
|
: 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
|
// According to the spec, short/long/etc should follow order of containing stream
|
||||||
input = imageInput.getByteOrder() == ByteOrder.BIG_ENDIAN
|
input = imageInput.getByteOrder() == ByteOrder.BIG_ENDIAN
|
||||||
? new DataInputStream(createDecoderInputStream(compression, adapter))
|
? new DataInputStream(adapter)
|
||||||
: new LittleEndianDataInputStream(createDecoderInputStream(compression, adapter));
|
: new LittleEndianDataInputStream(adapter);
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Read a full strip/tile
|
// Read a full strip/tile
|
||||||
@ -566,6 +626,9 @@ public class TIFFImageReader extends ImageReaderBase {
|
|||||||
// Might have something to do with subsampling?
|
// Might have something to do with subsampling?
|
||||||
// How do we pass the chroma-subsampling parameter from the TIFF structure to the JPEG reader?
|
// 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));
|
jpegReader.setInput(new ByteArrayImageInputStream(tablesValue));
|
||||||
|
|
||||||
// NOTE: This initializes the tables AND MORE secret internal settings for the reader (as if by magic).
|
// 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()) {
|
switch (rowRaster.getTransferType()) {
|
||||||
case DataBuffer.TYPE_BYTE:
|
case DataBuffer.TYPE_BYTE:
|
||||||
byte[] rowData = ((DataBufferByte) rowRaster.getDataBuffer()).getData();
|
byte[] rowData = ((DataBufferByte) rowRaster.getDataBuffer()).getData();
|
||||||
|
|
||||||
for (int j = 0; j < rowsInStrip; j++) {
|
for (int j = 0; j < rowsInStrip; j++) {
|
||||||
int row = startRow + j;
|
int row = startRow + j;
|
||||||
|
|
||||||
@ -699,17 +763,6 @@ public class TIFFImageReader extends ImageReaderBase {
|
|||||||
|
|
||||||
input.readFully(rowData);
|
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);
|
unPredict(predictor, colsInStrip, 1, numBands, rowData);
|
||||||
normalizeBlack(interpretation, rowData);
|
normalizeBlack(interpretation, rowData);
|
||||||
|
|
||||||
@ -725,6 +778,7 @@ public class TIFFImageReader extends ImageReaderBase {
|
|||||||
break;
|
break;
|
||||||
case DataBuffer.TYPE_USHORT:
|
case DataBuffer.TYPE_USHORT:
|
||||||
short [] rowDataShort = ((DataBufferUShort) rowRaster.getDataBuffer()).getData();
|
short [] rowDataShort = ((DataBufferUShort) rowRaster.getDataBuffer()).getData();
|
||||||
|
|
||||||
for (int j = 0; j < rowsInStrip; j++) {
|
for (int j = 0; j < rowsInStrip; j++) {
|
||||||
int row = startRow + j;
|
int row = startRow + j;
|
||||||
|
|
||||||
@ -751,6 +805,7 @@ public class TIFFImageReader extends ImageReaderBase {
|
|||||||
break;
|
break;
|
||||||
case DataBuffer.TYPE_INT:
|
case DataBuffer.TYPE_INT:
|
||||||
int [] rowDataInt = ((DataBufferInt) rowRaster.getDataBuffer()).getData();
|
int [] rowDataInt = ((DataBufferInt) rowRaster.getDataBuffer()).getData();
|
||||||
|
|
||||||
for (int j = 0; j < rowsInStrip; j++) {
|
for (int j = 0; j < rowsInStrip; j++) {
|
||||||
int row = startRow + 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 {
|
private InputStream createDecoderInputStream(final int compression, final InputStream stream) throws IOException {
|
||||||
switch (compression) {
|
switch (compression) {
|
||||||
|
case TIFFBaseline.COMPRESSION_NONE:
|
||||||
|
return stream;
|
||||||
case TIFFBaseline.COMPRESSION_PACKBITS:
|
case TIFFBaseline.COMPRESSION_PACKBITS:
|
||||||
return new DecoderStream(stream, new PackBitsDecoder(), 1024);
|
return new DecoderStream(stream, new PackBitsDecoder(), 1024);
|
||||||
case TIFFExtension.COMPRESSION_LZW:
|
case TIFFExtension.COMPRESSION_LZW:
|
||||||
@ -894,7 +951,7 @@ public class TIFFImageReader extends ImageReaderBase {
|
|||||||
short[] shorts = (short[]) entry.getValue();
|
short[] shorts = (short[]) entry.getValue();
|
||||||
value = new long[shorts.length];
|
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];
|
value[i] = shorts[i];
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -902,7 +959,7 @@ public class TIFFImageReader extends ImageReaderBase {
|
|||||||
int[] ints = (int[]) entry.getValue();
|
int[] ints = (int[]) entry.getValue();
|
||||||
value = new long[ints.length];
|
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];
|
value[i] = ints[i];
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -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 <a href="mailto:harald.kuhr@gmail.com">Harald Kuhr</a>
|
||||||
|
* @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));
|
||||||
|
// }
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
@ -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 <a href="mailto:harald.kuhr@gmail.com">Harald Kuhr</a>
|
||||||
|
* @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);
|
||||||
|
}
|
||||||
|
}
|
Loading…
x
Reference in New Issue
Block a user