#173 Support for PhotometricInterpretation 8/CIELab, 9/ICCLab and 10/ITULab

This commit is contained in:
Harald Kuhr 2015-10-22 21:35:10 +02:00
parent 30a7283b35
commit 8a38b2fde6
13 changed files with 658 additions and 436 deletions

View File

@ -100,7 +100,7 @@ Alternatively, if you have or know of a JPEG-2000 implementation in Java with a
* JPEG
* RAW (RGB)
* Support for "Large Document Format" (PSB)
* Native metadata support
* Native and Standard metadata support
#### TIFF - Aldus/Adobe Tagged Image File Format
@ -111,24 +111,27 @@ Alternatively, if you have or know of a JPEG-2000 implementation in Java with a
* Class R (RGB), all relevant compression types, 8 or 16 bits per sample, unsigned integer
* Read support for the following TIFF extensions:
* Tiling
* Class F (Facsimile), CCITT Modified Huffman RLE, T4 and T6 (type 2, 3 and 4) compressions.
* LZW Compression (type 5)
* "Old-style" JPEG Compression (type 6), as a best effort, as the spec is not well-defined
* JPEG Compression (type 7)
* ZLib (aka Adobe-style Deflate) Compression (type 8)
* Deflate Compression (type 32946)
* Horizontal differencing Predictor (type 2) for LZW, ZLib, Deflate and PackBits compression
* Alpha channel (ExtraSamples type 1/Associated Alpha)
* Alpha channel (ExtraSamples type 1/Associated Alpha and type 2/Unassociated Alpha)
* CMYK data (PhotometricInterpretation type 5/Separated)
* YCbCr data (PhotometricInterpretation type 6/YCbCr) for JPEG
* CIELab data (PhotometricInterpretation type 9, 10 and 11)
* Planar data (PlanarConfiguration type 2/Planar)
* ICC profiles (ICCProfile)
* BitsPerSample values up to 16 for most PhotometricInterpretations
* Multiple images (pages) in one file
* Write support for most "Baseline" TIFF options
* Uncompressed, PackBits, ZLib and Deflate
* Currently missing the CCITT fax encodings
* Additional support for CCITT T4 and and T6 compressions.
* Additional support for LZW and JPEG (type 7) compressions
* Horizontal differencing Predictor (type 2) for LZW, ZLib, Deflate
* Native and Standard metadata support
Legacy formats

View File

@ -0,0 +1,152 @@
package com.twelvemonkeys.imageio.color;
import com.twelvemonkeys.lang.Validate;
/**
* Converts between CIE L*a*b* and sRGB color spaces.
*/
// Code adapted from ImageJ's Color_Space_Converter.java (Public Domain):
// http://rsb.info.nih.gov/ij/plugins/download/Color_Space_Converter.java
public final class CIELabColorConverter {
// TODO: Add tests
// TODO: Create interface in the color package?
// TODO: Make YCbCr/YCCK -> RGB/CMYK implement same interface?
public enum Illuminant {
D50(new float[] {96.4212f, 100.0f, 82.5188f}),
D65(new float[] {95.0429f, 100.0f, 108.8900f});
private final float[] whitePoint;
Illuminant(final float[] wp) {
whitePoint = Validate.isTrue(wp != null && wp.length == 3, wp, "Bad white point definition: %s");
}
public float[] getWhitePoint() {
return whitePoint;
}
}
private final float[] whitePoint;
public CIELabColorConverter(final Illuminant illuminant) {
whitePoint = Validate.notNull(illuminant, "illuminant").getWhitePoint();
}
private float clamp(float x) {
if (x < 0.0f) {
return 0.0f;
}
else if (x > 255.0f) {
return 255.0f;
}
else {
return x;
}
}
public void toRGB(float l, float a, float b, float[] rgbResult) {
XYZtoRGB(LABtoXYZ(l, a, b, rgbResult), rgbResult);
}
/**
* Convert LAB to XYZ.
* @param L
* @param a
* @param b
* @return XYZ values
*/
private float[] LABtoXYZ(float L, float a, float b, float[] xyzResult) {
// Significant speedup: Removing Math.pow
float y = (L + 16.0f) / 116.0f;
float y3 = y * y * y; // Math.pow(y, 3.0);
float x = (a / 500.0f) + y;
float x3 = x * x * x; // Math.pow(x, 3.0);
float z = y - (b / 200.0f);
float z3 = z * z * z; // Math.pow(z, 3.0);
if (y3 > 0.008856f) {
y = y3;
}
else {
y = (y - (16.0f / 116.0f)) / 7.787f;
}
if (x3 > 0.008856f) {
x = x3;
}
else {
x = (x - (16.0f / 116.0f)) / 7.787f;
}
if (z3 > 0.008856f) {
z = z3;
}
else {
z = (z - (16.0f / 116.0f)) / 7.787f;
}
xyzResult[0] = x * whitePoint[0];
xyzResult[1] = y * whitePoint[1];
xyzResult[2] = z * whitePoint[2];
return xyzResult;
}
/**
* Convert XYZ to RGB
* @param xyz
* @return RGB values
*/
private float[] XYZtoRGB(final float[] xyz, final float[] rgbResult) {
return XYZtoRGB(xyz[0], xyz[1], xyz[2], rgbResult);
}
private float[] XYZtoRGB(final float X, final float Y, final float Z, float[] rgbResult) {
float x = X / 100.0f;
float y = Y / 100.0f;
float z = Z / 100.0f;
float r = x * 3.2406f + y * -1.5372f + z * -0.4986f;
float g = x * -0.9689f + y * 1.8758f + z * 0.0415f;
float b = x * 0.0557f + y * -0.2040f + z * 1.0570f;
// assume sRGB
if (r > 0.0031308f) {
r = ((1.055f * (float) pow(r, 1.0 / 2.4)) - 0.055f);
}
else {
r = (r * 12.92f);
}
if (g > 0.0031308f) {
g = ((1.055f * (float) pow(g, 1.0 / 2.4)) - 0.055f);
}
else {
g = (g * 12.92f);
}
if (b > 0.0031308f) {
b = ((1.055f * (float) pow(b, 1.0 / 2.4)) - 0.055f);
}
else {
b = (b * 12.92f);
}
// convert 0..1 into 0..255
rgbResult[0] = clamp(r * 255);
rgbResult[1] = clamp(g * 255);
rgbResult[2] = clamp(b * 255);
return rgbResult;
}
// TODO: Test, to figure out if accuracy is good enough.
// Visual inspection looks good! The author claims 5-12% error, worst case up to 25%...
// http://martin.ankerl.com/2007/10/04/optimized-pow-approximation-for-java-and-c-c/
static double pow(final double a, final double b) {
long tmp = Double.doubleToLongBits(a);
long tmp2 = (long) (b * (tmp - 4606921280493453312L)) + 4606921280493453312L;
return Double.longBitsToDouble(tmp2);
}
}

View File

@ -0,0 +1,81 @@
package com.twelvemonkeys.imageio.color;
/**
* Fast YCbCr to RGB conversion.
*
* @author <a href="mailto:harald.kuhr@gmail.com">Harald Kuhr</a>
* @author Original code by Werner Randelshofer (used by permission).
*/
public 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 (ColorSpaces.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();
}
public static 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;
double cr = (yCbCr[offset + 2] & 0xff) - 128;
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);
rgb[offset] = clamp(red);
rgb[offset + 2] = clamp(blue);
rgb[offset + 1] = clamp(green);
}
public 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]);
}
private static byte clamp(int val) {
return (byte) Math.max(0, Math.min(255, val));
}
}

View File

@ -0,0 +1,68 @@
package com.twelvemonkeys.imageio.color;
import com.twelvemonkeys.imageio.color.CIELabColorConverter.Illuminant;
import org.junit.Test;
import static org.junit.Assert.assertArrayEquals;
/**
* CIELabColorConverterTest.
*
* @author <a href="mailto:harald.kuhr@gmail.com">Harald Kuhr</a>
* @author last modified by $Author: harald.kuhr$
* @version $Id: CIELabColorConverterTest.java,v 1.0 22/10/15 harald.kuhr Exp$
*/
public class CIELabColorConverterTest {
@Test(expected = IllegalArgumentException.class)
public void testNoIllumninant() {
new CIELabColorConverter(null);
}
@Test
public void testD50() {
CIELabColorConverter converter = new CIELabColorConverter(Illuminant.D50);
float[] rgb = new float[3];
converter.toRGB(100, -128, -128, rgb);
assertArrayEquals(new float[] {0, 255, 255}, rgb, 1);
converter.toRGB(100, 0, 0, rgb);
assertArrayEquals(new float[] {255, 252, 220}, rgb, 5);
converter.toRGB(0, 0, 0, rgb);
assertArrayEquals(new float[] {0, 0, 0}, rgb, 1);
converter.toRGB(100, 0, 127, rgb);
assertArrayEquals(new float[] {255, 249, 0}, rgb, 5);
converter.toRGB(50, -128, 127, rgb);
assertArrayEquals(new float[] {0, 152, 0}, rgb, 2);
converter.toRGB(50, 127, -128, rgb);
assertArrayEquals(new float[] {222, 0, 255}, rgb, 2);
}
@Test
public void testD65() {
CIELabColorConverter converter = new CIELabColorConverter(Illuminant.D65);
float[] rgb = new float[3];
converter.toRGB(100, -128, -128, rgb);
assertArrayEquals(new float[] {0, 255, 255}, rgb, 1);
converter.toRGB(100, 0, 0, rgb);
assertArrayEquals(new float[] {255, 252, 255}, rgb, 5);
converter.toRGB(0, 0, 0, rgb);
assertArrayEquals(new float[] {0, 0, 0}, rgb, 1);
converter.toRGB(100, 0, 127, rgb);
assertArrayEquals(new float[] {255, 250, 0}, rgb, 5);
converter.toRGB(50, -128, 127, rgb);
assertArrayEquals(new float[] {0, 152, 0}, rgb, 3);
converter.toRGB(50, 127, -128, rgb);
assertArrayEquals(new float[] {184, 0, 255}, rgb, 5);
}
}

View File

@ -28,6 +28,7 @@
package com.twelvemonkeys.imageio.plugins.jpeg;
import com.twelvemonkeys.imageio.color.YCbCrConverter;
import com.twelvemonkeys.imageio.metadata.Directory;
import com.twelvemonkeys.imageio.metadata.Entry;
import com.twelvemonkeys.imageio.metadata.exif.TIFF;
@ -102,7 +103,7 @@ final class EXIFThumbnailReader extends ThumbnailReader {
thumbnail = readJPEG();
}
cachedThumbnail = pixelsExposed ? null : new SoftReference<BufferedImage>(thumbnail);
cachedThumbnail = pixelsExposed ? null : new SoftReference<>(thumbnail);
return thumbnail;
}
@ -132,14 +133,10 @@ final class EXIFThumbnailReader extends ThumbnailReader {
input = new SequenceInputStream(new ByteArrayInputStream(fakeEmptyExif), input);
try {
MemoryCacheImageInputStream stream = new MemoryCacheImageInputStream(input);
try {
try (MemoryCacheImageInputStream stream = new MemoryCacheImageInputStream(input)) {
return readJPEGThumbnail(reader, stream);
}
finally {
stream.close();
}
}
finally {
input.close();
@ -195,15 +192,15 @@ final class EXIFThumbnailReader extends ThumbnailReader {
break;
case 6:
// YCbCr
for (int i = 0, thumbDataLength = thumbData.length; i < thumbDataLength; i += 3) {
JPEGImageReader.YCbCrConverter.convertYCbCr2RGB(thumbData, thumbData, i);
for (int i = 0; i < thumbSize; i += 3) {
YCbCrConverter.convertYCbCr2RGB(thumbData, thumbData, i);
}
break;
default:
throw new IIOException("Unknown PhotometricInterpretation value for uncompressed EXIF thumbnail (expected 2 or 6): " + interpretation);
}
return ThumbnailReader.readRawThumbnail(thumbData, thumbData.length, 0, w, h);
return ThumbnailReader.readRawThumbnail(thumbData, thumbSize, 0, w, h);
}
throw new IIOException("Missing StripOffsets tag for uncompressed EXIF thumbnail");

View File

@ -30,6 +30,7 @@ package com.twelvemonkeys.imageio.plugins.jpeg;
import com.twelvemonkeys.imageio.ImageReaderBase;
import com.twelvemonkeys.imageio.color.ColorSpaces;
import com.twelvemonkeys.imageio.color.YCbCrConverter;
import com.twelvemonkeys.imageio.metadata.CompoundDirectory;
import com.twelvemonkeys.imageio.metadata.Directory;
import com.twelvemonkeys.imageio.metadata.Entry;
@ -462,14 +463,13 @@ public class JPEGImageReader extends ImageReaderBase {
// Apply source color conversion from implicit color space
if (csType == JPEGColorSpace.YCbCr || csType == JPEGColorSpace.YCbCrA) {
YCbCrConverter.convertYCbCr2RGB(raster);
convertYCbCr2RGB(raster);
}
else if (csType == JPEGColorSpace.YCCK) {
// TODO: Need to rethink this (non-) inversion, see #147
// TODO: Allow param to specify inversion, or possibly the PDF decode array
// flag0 bit 15, blend = 1 see http://graphicdesign.stackexchange.com/questions/12894/cmyk-jpegs-extracted-from-pdf-appear-inverted
boolean invert = true;// || (adobeDCT.flags0 & 0x8000) == 0;
YCbCrConverter.convertYCCK2CMYK(raster, invert);
convertYCCK2CMYK(raster);
}
else if (csType == JPEGColorSpace.CMYK) {
invertCMYK(raster);
@ -1107,130 +1107,32 @@ public class JPEGImageReader extends ImageReaderBase {
}
}
/**
* Static inner class for lazy-loading of conversion tables.
*
* @author <a href="mailto:harald.kuhr@gmail.com">Harald Kuhr</a>
* @author Original code by Werner Randelshofer
*/
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);
public static void convertYCbCr2RGB(final Raster raster) {
final int height = raster.getHeight();
final int width = raster.getWidth();
final byte[] data = ((DataBufferByte) raster.getDataBuffer()).getData();
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;
for (int y = 0; y < height; y++) {
for (int x = 0; x < width; x++) {
YCbCrConverter.convertYCbCr2RGB(data, data, (x + y * width) * 3);
}
}
}
static {
buildYCCtoRGBtable();
}
public static void convertYCCK2CMYK(final Raster raster) {
final int height = raster.getHeight();
final int width = raster.getWidth();
final byte[] data = ((DataBufferByte) raster.getDataBuffer()).getData();
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);
}
for (int y = 0; y < height; y++) {
for (int x = 0; x < width; x++) {
int offset = (x + y * width) * 4;
// YCC -> CMY
YCbCrConverter.convertYCbCr2RGB(data, data, offset);
// Inverse K
data[offset + 3] = (byte) (0xff - data[offset + 3] & 0xff);
}
}
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 boolean invert) {
final int height = raster.getHeight();
final int width = raster.getWidth();
final byte[] data = ((DataBufferByte) raster.getDataBuffer()).getData();
if (invert) {
for (int y = 0; y < height; y++) {
for (int x = 0; x < width; x++) {
convertYCCK2CMYKInverted(data, data, (x + y * width) * 4);
}
}
}
else {
for (int y = 0; y < height; y++) {
for (int x = 0; x < width; x++) {
convertYCCK2CMYK(data, data, (x + y * width) * 4);
}
}
}
}
private static void convertYCCK2CMYKInverted(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 void convertYCCK2CMYK(byte[] ycck, byte[] cmyk, int offset) {
int y = ycck[offset ] & 0xff;
int cb = ycck[offset + 1] & 0xff;
int cr = ycck[offset + 2] & 0xff;
int k = 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));
}
}
private class ProgressDelegator extends ProgressListenerBase implements IIOReadUpdateListener, IIOReadWarningListener {

View File

@ -29,7 +29,10 @@
package com.twelvemonkeys.imageio.plugins.tiff;
import com.twelvemonkeys.imageio.ImageReaderBase;
import com.twelvemonkeys.imageio.color.CIELabColorConverter;
import com.twelvemonkeys.imageio.color.CIELabColorConverter.Illuminant;
import com.twelvemonkeys.imageio.color.ColorSpaces;
import com.twelvemonkeys.imageio.color.YCbCrConverter;
import com.twelvemonkeys.imageio.metadata.CompoundDirectory;
import com.twelvemonkeys.imageio.metadata.Directory;
import com.twelvemonkeys.imageio.metadata.Entry;
@ -49,12 +52,10 @@ import com.twelvemonkeys.io.FastByteArrayOutputStream;
import com.twelvemonkeys.io.LittleEndianDataInputStream;
import com.twelvemonkeys.io.enc.DecoderStream;
import com.twelvemonkeys.io.enc.PackBitsDecoder;
import com.twelvemonkeys.xml.XMLSerializer;
import javax.imageio.*;
import javax.imageio.event.IIOReadWarningListener;
import javax.imageio.metadata.IIOMetadata;
import javax.imageio.metadata.IIOMetadataFormatImpl;
import javax.imageio.plugins.jpeg.JPEGImageReadParam;
import javax.imageio.spi.IIORegistry;
import javax.imageio.spi.ImageReaderSpi;
@ -145,6 +146,9 @@ public class TIFFImageReader extends ImageReaderBase {
final static boolean DEBUG = "true".equalsIgnoreCase(System.getProperty("com.twelvemonkeys.imageio.plugins.tiff.debug"));
// NOTE: DO NOT MODIFY OR EXPOSE THIS ARRAY OUTSIDE PACKAGE!
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;
@ -506,15 +510,29 @@ public class TIFFImageReader extends ImageReaderBase {
default:
throw new IIOException(
String.format("Unsupported TIFF SamplesPerPixels/BitsPerSample combination for Separated TIFF (expected 4/8, 4/16, 5/8 or 5/16): %d/%s", samplesPerPixel, bitsPerSample)
String.format("Unsupported SamplesPerPixels/BitsPerSample combination for Separated TIFF (expected 4/8, 4/16, 5/8 or 5/16): %d/%s", samplesPerPixel, bitsPerSample)
);
}
case TIFFExtension.PHOTOMETRIC_CIELAB:
case TIFFExtension.PHOTOMETRIC_ICCLAB:
case TIFFExtension.PHOTOMETRIC_ITULAB:
// TODO: Would probably be more correct to handle using a CIELabColorSpace for RAW type?
// L*a*b* color. Handled using conversion to sRGB
cs = ColorSpace.getInstance(ColorSpace.CS_sRGB);
switch (planarConfiguration) {
case TIFFBaseline.PLANARCONFIG_CHUNKY:
return ImageTypeSpecifiers.createInterleaved(cs, new int[] {0, 1, 2}, dataType, false, false);
case TIFFExtension.PLANARCONFIG_PLANAR:
// TODO: Reading works fine, but we can't convert the Lab values properly yet. Need to rewrite normalizeColor
//return ImageTypeSpecifiers.createBanded(cs, new int[] {0, 1, 2}, new int[] {0, 0, 0}, dataType, false, false);
default:
throw new IIOException(
String.format("Unsupported PlanarConfiguration for Lab color TIFF (expected 1): %d", planarConfiguration)
);
}
case TIFFBaseline.PHOTOMETRIC_MASK:
// Transparency mask
case TIFFExtension.PHOTOMETRIC_CIELAB:
case TIFFExtension.PHOTOMETRIC_ICCLAB:
case TIFFExtension.PHOTOMETRIC_ITULAB:
// L*a*b* color. Handled using conversion to linear RGB
// TODO: Treat as grey?
case TIFFCustom.PHOTOMETRIC_LOGL:
case TIFFCustom.PHOTOMETRIC_LOGLUV:
// Log
@ -542,6 +560,12 @@ public class TIFFImageReader extends ImageReaderBase {
return 3;
case TIFFExtension.PHOTOMETRIC_SEPARATED:
return getValueAsIntWithDefault(TIFF.TAG_NUMBER_OF_INKS, 4);
case TIFFCustom.PHOTOMETRIC_LOGL:
case TIFFCustom.PHOTOMETRIC_LOGLUV:
case TIFFCustom.PHOTOMETRIC_CFA:
case TIFFCustom.PHOTOMETRIC_LINEAR_RAW:
throw new IIOException("Unsupported TIFF PhotometricInterpretation value: " + photometricInterpretation);
default:
throw new IIOException("Unknown TIFF PhotometricInterpretation value: " + photometricInterpretation);
}
@ -549,6 +573,12 @@ public class TIFFImageReader extends ImageReaderBase {
private int getDataType(int sampleFormat, int bitsPerSample) throws IIOException {
switch (sampleFormat) {
case TIFFExtension.SAMPLEFORMAT_UNDEFINED:
// Spec says:
// A field value of undefined is a statement by the writer that it did not know how
// to interpret the data samples; for example, if it were copying an existing image. A
// reader would typically treat an image with undefined data as if the field were
// not present (i.e. as unsigned integer data).
case TIFFBaseline.SAMPLEFORMAT_UINT:
return bitsPerSample <= 8 ? DataBuffer.TYPE_BYTE : bitsPerSample <= 16 ? DataBuffer.TYPE_USHORT : DataBuffer.TYPE_INT;
case TIFFExtension.SAMPLEFORMAT_INT:
@ -569,15 +599,6 @@ public class TIFFImageReader extends ImageReaderBase {
}
throw new IIOException("Unsupported BitsPerSample for SampleFormat 3/Floating Point (expected 32): " + bitsPerSample);
case TIFFExtension.SAMPLEFORMAT_UNDEFINED:
// Spec says:
// A field value of undefined is a statement by the writer that it did not know how
// to interpret the data samples; for example, if it were copying an existing image. A
// reader would typically treat an image with undefined data as if the field were
// not present (i.e. as unsigned integer data).
// TODO: We should probably issue a warning instead, and assume SAMPLEFORMAT_UINT
throw new IIOException("Unsupported TIFF SampleFormat: 4 (Undefined)");
default:
throw new IIOException("Unknown TIFF SampleFormat (expected 1, 2, 3 or 4): " + sampleFormat);
}
@ -671,7 +692,6 @@ public class TIFFImageReader extends ImageReaderBase {
Set<ImageTypeSpecifier> specs = new LinkedHashSet<>(5);
// TODO: Based on raw type, we can probably convert to most RGB types at least, maybe gray etc
// TODO: Planar to chunky by default
if (rawType.getColorModel().getColorSpace().getType() == ColorSpace.TYPE_RGB) {
if (rawType.getNumBands() == 3 && rawType.getBitsPerBand(0) == 8) {
specs.add(ImageTypeSpecifier.createFromBufferedImageType(BufferedImage.TYPE_3BYTE_BGR));
@ -776,7 +796,7 @@ public class TIFFImageReader extends ImageReaderBase {
int[] yCbCrSubsampling = null;
int yCbCrPos = 1;
double[] yCbCrCoefficients = null;
// double[] yCbCrCoefficients = null;
if (interpretation == TIFFExtension.PHOTOMETRIC_YCBCR) {
// getRawImageType does the lookup/conversion for these
if (rowRaster.getNumBands() != 3) {
@ -815,15 +835,15 @@ public class TIFFImageReader extends ImageReaderBase {
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 = YCbCrUpsamplerStream.CCIR_601_1_COEFFICIENTS;
}
// 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 = YCbCrUpsamplerStream.CCIR_601_1_COEFFICIENTS;
// }
}
// Read data
@ -854,10 +874,10 @@ public class TIFFImageReader extends ImageReaderBase {
adapter = createUnpredictorStream(predictor, stripTileWidth, numBands, getBitsPerSample(), adapter, imageInput.getByteOrder());
if (interpretation == TIFFExtension.PHOTOMETRIC_YCBCR && rowRaster.getTransferType() == DataBuffer.TYPE_BYTE) {
adapter = new YCbCrUpsamplerStream(adapter, yCbCrSubsampling, yCbCrPos, colsInTile, yCbCrCoefficients);
adapter = new YCbCrUpsamplerStream(adapter, yCbCrSubsampling, yCbCrPos, colsInTile);
}
else if (interpretation == TIFFExtension.PHOTOMETRIC_YCBCR && rowRaster.getTransferType() == DataBuffer.TYPE_USHORT) {
adapter = new YCbCr16UpsamplerStream(adapter, yCbCrSubsampling, yCbCrPos, colsInTile, yCbCrCoefficients, imageInput.getByteOrder());
adapter = new YCbCr16UpsamplerStream(adapter, yCbCrSubsampling, yCbCrPos, colsInTile, imageInput.getByteOrder());
}
else if (interpretation == TIFFExtension.PHOTOMETRIC_YCBCR) {
// Handled in getRawImageType
@ -939,13 +959,14 @@ public class TIFFImageReader extends ImageReaderBase {
// Read only tiles that lies within region
if (new Rectangle(col, row, colsInTile, rowsInTile).intersects(srcRegion)) {
imageInput.seek(stripTileOffsets[i]);
ImageInputStream subStream = new SubImageInputStream(imageInput, stripTileByteCounts != null ? (int) stripTileByteCounts[i] : Short.MAX_VALUE);
try {
int length = stripTileByteCounts != null ? (int) stripTileByteCounts[i] : Short.MAX_VALUE;
try (ImageInputStream subStream = new SubImageInputStream(imageInput, length)) {
jpegReader.setInput(subStream);
jpegParam.setSourceRegion(new Rectangle(0, 0, colsInTile, rowsInTile));
if (interpretation == TIFFExtension.PHOTOMETRIC_YCBCR) {
if (interpretation == TIFFExtension.PHOTOMETRIC_YCBCR || interpretation == TIFFBaseline.PHOTOMETRIC_RGB) {
jpegParam.setDestinationOffset(new Point(col - srcRegion.x, row - srcRegion.y));
jpegParam.setDestination(destination);
jpegReader.read(0, jpegParam);
@ -954,12 +975,10 @@ public class TIFFImageReader extends ImageReaderBase {
// Otherwise, it's likely CMYK or some other interpretation we don't need to convert.
// We'll have to use readAsRaster and later apply color space conversion ourselves
Raster raster = jpegReader.readRaster(0, jpegParam);
normalizeColor(interpretation, ((DataBufferByte) raster.getDataBuffer()).getData());
destination.getRaster().setDataElements(col - srcRegion.x, row - srcRegion.y, raster);
}
}
finally {
subStream.close();
}
}
@ -1012,7 +1031,6 @@ public class TIFFImageReader extends ImageReaderBase {
// 517/JPEGLosslessPredictors
// 518/JPEGPointTransforms
ImageInputStream stream;
if (jpegOffset != -1) {
// Straight forward case: We're good to go! We'll disregard tiling and any tables tags
@ -1053,16 +1071,17 @@ public class TIFFImageReader extends ImageReaderBase {
imageInput.seek(realJPEGOffset);
stream = new SubImageInputStream(imageInput, jpegLenght != -1 ? jpegLenght : Integer.MAX_VALUE);
jpegReader.setInput(stream);
// Read data
processImageStarted(imageIndex); // Better yet, would be to delegate read progress here...
try {
int length = jpegLenght != -1 ? jpegLenght : Integer.MAX_VALUE;
try (ImageInputStream stream = new SubImageInputStream(imageInput, length)) {
jpegReader.setInput(stream);
jpegParam.setSourceRegion(new Rectangle(0, 0, width, height));
if (interpretation == TIFFExtension.PHOTOMETRIC_YCBCR) {
if (interpretation == TIFFExtension.PHOTOMETRIC_YCBCR || interpretation == TIFFBaseline.PHOTOMETRIC_RGB) {
jpegParam.setDestination(destination);
jpegReader.read(0, jpegParam);
}
@ -1073,9 +1092,6 @@ public class TIFFImageReader extends ImageReaderBase {
destination.getRaster().setDataElements(0, 0, raster);
}
}
finally {
stream.close();
}
processImageProgress(100f);
@ -1149,26 +1165,31 @@ public class TIFFImageReader extends ImageReaderBase {
// Read only tiles that lies within region
if (new Rectangle(col, row, colsInTile, rowsInTile).intersects(srcRegion)) {
imageInput.seek(stripTileOffsets[i]);
stream = ImageIO.createImageInputStream(new SequenceInputStream(Collections.enumeration(
try (ImageInputStream stream = ImageIO.createImageInputStream(new SequenceInputStream(Collections.enumeration(
Arrays.asList(
createJFIFStream(destRaster, stripTileWidth, stripTileHeight, qTables, dcTables, acTables),
IIOUtil.createStreamAdapter(imageInput, stripTileByteCounts != null ? (int) stripTileByteCounts[i] : Short.MAX_VALUE),
IIOUtil.createStreamAdapter(imageInput, stripTileByteCounts != null
? (int) stripTileByteCounts[i]
: Short.MAX_VALUE),
new ByteArrayInputStream(new byte[] {(byte) 0xff, (byte) 0xd9}) // EOI
)
)));
jpegReader.setInput(stream);
try {
)))) {
jpegReader.setInput(stream);
jpegParam.setSourceRegion(new Rectangle(0, 0, colsInTile, rowsInTile));
jpegParam.setDestinationOffset(new Point(col - srcRegion.x, row - srcRegion.y));
jpegParam.setDestination(destination);
// TODO: This works only if Gray/YCbCr/RGB, not CMYK/LAB/etc...
// In the latter case we will have to use readAsRaster and do color conversion ourselves
jpegReader.read(0, jpegParam);
}
finally {
stream.close();
if (interpretation == TIFFExtension.PHOTOMETRIC_YCBCR || interpretation == TIFFBaseline.PHOTOMETRIC_RGB) {
jpegParam.setDestination(destination);
jpegReader.read(0, jpegParam);
}
else {
// Otherwise, it's likely CMYK or some other interpretation we don't need to convert.
// We'll have to use readAsRaster and later apply color space conversion ourselves
Raster raster = jpegReader.readRaster(0, jpegParam);
destination.getRaster().setDataElements(0, 0, raster);
}
}
}
@ -1348,7 +1369,8 @@ public class TIFFImageReader extends ImageReaderBase {
case DataBuffer.TYPE_BYTE:
for (int band = 0; band < bands; band++) {
byte[] rowDataByte = ((DataBufferByte) dataBuffer).getData(band);
int bank = banded ? ((BandedSampleModel) tileRowRaster.getSampleModel()).getBankIndices()[band] : band;
byte[] rowDataByte = ((DataBufferByte) dataBuffer).getData(bank);
WritableRaster destChannel = banded
? raster.createWritableChild(raster.getMinX(), raster.getMinY(), raster.getWidth(), raster.getHeight(), 0, 0, new int[] {band})
: raster;
@ -1364,7 +1386,9 @@ public class TIFFImageReader extends ImageReaderBase {
input.readFully(rowDataByte);
if (row % ySub == 0 && row >= srcRegion.y) {
normalizeBlack(interpretation, rowDataByte);
if (!banded) {
normalizeColor(interpretation, rowDataByte);
}
// Subsample horizontal
if (xSub != 1) {
@ -1379,6 +1403,11 @@ public class TIFFImageReader extends ImageReaderBase {
}
}
// if (banded) {
// // TODO: Normalize colors for tile (need to know tile region and sample model)
// // Unfortunately, this will disable acceleration...
// }
break;
case DataBuffer.TYPE_USHORT:
case DataBuffer.TYPE_SHORT:
@ -1402,7 +1431,7 @@ public class TIFFImageReader extends ImageReaderBase {
readFully(input, rowDataShort);
if (row >= srcRegion.y) {
normalizeBlack(interpretation, rowDataShort);
normalizeColor(interpretation, rowDataShort);
// Subsample horizontal
if (xSub != 1) {
@ -1439,7 +1468,7 @@ public class TIFFImageReader extends ImageReaderBase {
readFully(input, rowDataInt);
if (row >= srcRegion.y) {
normalizeBlack(interpretation, rowDataInt);
normalizeColor(interpretation, rowDataInt);
// Subsample horizontal
if (xSub != 1) {
@ -1477,6 +1506,7 @@ public class TIFFImageReader extends ImageReaderBase {
if (row >= srcRegion.y) {
// TODO: Allow param to decide tone mapping strategy, like in the HDRImageReader
clamp(rowDataFloat);
normalizeColor(interpretation, rowDataFloat);
// Subsample horizontal
if (xSub != 1) {
@ -1542,33 +1572,203 @@ public class TIFFImageReader extends ImageReaderBase {
}
}
private void normalizeBlack(int photometricInterpretation, short[] data) {
if (photometricInterpretation == TIFFBaseline.PHOTOMETRIC_WHITE_IS_ZERO) {
// Inverse values
for (int i = 0; i < data.length; i++) {
data[i] = (short) (0xffff - data[i] & 0xffff);
}
private void normalizeColor(int photometricInterpretation, byte[] data) {
switch (photometricInterpretation) {
case TIFFBaseline.PHOTOMETRIC_WHITE_IS_ZERO:
// Inverse values
for (int i = 0; i < data.length; i++) {
data[i] ^= -1;
}
break;
case TIFFExtension.PHOTOMETRIC_CIELAB:
case TIFFExtension.PHOTOMETRIC_ICCLAB:
case TIFFExtension.PHOTOMETRIC_ITULAB:
CIELabColorConverter converter = new CIELabColorConverter(
photometricInterpretation != TIFFExtension.PHOTOMETRIC_ITULAB
? Illuminant.D65
: Illuminant.D50
);
float[] temp = new float[3];
for (int i = 0; i < data.length; i += 3) {
// Unsigned scaled form 0...100
float LStar = (data[i] & 0xff) * 100f / 255.0f;
float aStar;
float bStar;
if (photometricInterpretation == TIFFExtension.PHOTOMETRIC_CIELAB) {
// -128...127
aStar = data[i + 1];
bStar = data[i + 2];
}
else {
// Assumes same data for ICC and ITU (unsigned)
// 0...255
aStar = (data[i + 1] & 0xff) - 128;
bStar = (data[i + 2] & 0xff) - 128;
}
converter.toRGB(LStar, aStar, bStar, temp);
data[i ] = (byte) temp[0];
data[i + 1] = (byte) temp[1];
data[i + 2] = (byte) temp[2];
}
break;
case TIFFExtension.PHOTOMETRIC_YCBCR:
Entry coefficients = currentIFD.getEntryById(TIFF.TAG_YCBCR_COEFFICIENTS);
if (coefficients == null) {
for (int i = 0; i < data.length; i += 3) {
YCbCrConverter.convertYCbCr2RGB(data, data, i);
}
}
else {
Rational[] value = (Rational[]) coefficients.getValue();
double[] yCbCrCoefficients = new double[] {value[0].doubleValue(), value[1].doubleValue(), value[2].doubleValue()};
for (int i = 0; i < data.length; i += 3) {
YCbCrConverter.convertYCbCr2RGB(data, data, yCbCrCoefficients, i);
}
}
break;
}
}
private void normalizeBlack(int photometricInterpretation, int[] data) {
if (photometricInterpretation == TIFFBaseline.PHOTOMETRIC_WHITE_IS_ZERO) {
// Inverse values
for (int i = 0; i < data.length; i++) {
data[i] = (0xffffffff - data[i]);
}
private void normalizeColor(int photometricInterpretation, short[] data) {
switch (photometricInterpretation) {
case TIFFBaseline.PHOTOMETRIC_WHITE_IS_ZERO:
// Inverse values
for (int i = 0; i < data.length; i++) {
data[i] ^= -1;
}
break;
case TIFFExtension.PHOTOMETRIC_CIELAB:
case TIFFExtension.PHOTOMETRIC_ICCLAB:
case TIFFExtension.PHOTOMETRIC_ITULAB:
CIELabColorConverter converter = new CIELabColorConverter(
photometricInterpretation != TIFFExtension.PHOTOMETRIC_ITULAB
? Illuminant.D65
: Illuminant.D50
);
float[] temp = new float[3];
float scaleL = photometricInterpretation == TIFFExtension.PHOTOMETRIC_CIELAB ? 65535f : 65280f; // Is for ICC lab, assumes the same for ITU....
for (int i = 0; i < data.length; i += 3) {
// Unsigned scaled form 0...100
float LStar = (data[i] & 0xffff) * 100.0f / scaleL;
float aStar;
float bStar;
if (photometricInterpretation == TIFFExtension.PHOTOMETRIC_CIELAB) {
// -32768...32767
aStar = data[i + 1] / 256f;
bStar = data[i + 2] / 256f;
}
else {
// Assumes same data for ICC and ITU (unsigned)
// 0...65535f
aStar = ((data[i + 1] & 0xffff) - 32768) / 256f;
bStar = ((data[i + 2] & 0xffff) - 32768) / 256f;
}
converter.toRGB(LStar, aStar, bStar, temp);
data[i ] = (short) (temp[0] * 257f);
data[i + 1] = (short) (temp[1] * 257f);
data[i + 2] = (short) (temp[2] * 257f);
}
break;
case TIFFExtension.PHOTOMETRIC_YCBCR:
double[] coefficients;
Entry coefficientsTag = currentIFD.getEntryById(TIFF.TAG_YCBCR_COEFFICIENTS);
if (coefficientsTag != null) {
Rational[] value = (Rational[]) coefficientsTag.getValue();
coefficients = new double[] {value[0].doubleValue(), value[1].doubleValue(), value[2].doubleValue()};
}
else {
coefficients = CCIR_601_1_COEFFICIENTS;
}
for (int i = 0; i < data.length; i += 3) {
convertYCbCr2RGB(data, data, coefficients, i);
}
}
}
private void normalizeBlack(int photometricInterpretation, byte[] data) {
if (photometricInterpretation == TIFFBaseline.PHOTOMETRIC_WHITE_IS_ZERO) {
// Inverse values
for (int i = 0; i < data.length; i++) {
data[i] = (byte) (0xff - data[i] & 0xff);
}
private void normalizeColor(int photometricInterpretation, int[] data) {
switch (photometricInterpretation) {
case TIFFBaseline.PHOTOMETRIC_WHITE_IS_ZERO:
// Inverse values
for (int i = 0; i < data.length; i++) {
data[i] ^= -1;
}
break;
case TIFFExtension.PHOTOMETRIC_CIELAB:
case TIFFExtension.PHOTOMETRIC_ICCLAB:
case TIFFExtension.PHOTOMETRIC_ITULAB:
case TIFFExtension.PHOTOMETRIC_YCBCR:
// Not supported
break;
}
}
private void normalizeColor(int photometricInterpretation, float[] data) {
switch (photometricInterpretation) {
case TIFFBaseline.PHOTOMETRIC_WHITE_IS_ZERO:
case TIFFExtension.PHOTOMETRIC_CIELAB:
case TIFFExtension.PHOTOMETRIC_ICCLAB:
case TIFFExtension.PHOTOMETRIC_ITULAB:
case TIFFExtension.PHOTOMETRIC_YCBCR:
// Not supported
break;
}
}
private void convertYCbCr2RGB(final short[] yCbCr, final short[] rgb, final double[] coefficients, final int offset) {
int y;
int cb;
int cr;
y = (yCbCr[offset + 0] & 0xffff);
cb = (yCbCr[offset + 1] & 0xffff) - 32768;
cr = (yCbCr[offset + 2] & 0xffff) - 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!
rgb[offset] = r;
rgb[offset + 1] = g;
rgb[offset + 2] = b;
}
private short clampShort(int val) {
return (short) Math.max(0, Math.min(0xffff, val));
}
private InputStream createDecompressorStream(final int compression, final int width, final int bands, final InputStream stream) throws IOException {
switch (compression) {
case TIFFBaseline.COMPRESSION_NONE:
@ -1776,16 +1976,16 @@ public class TIFFImageReader extends ImageReaderBase {
BufferedImage image = reader.read(imageNo, param);
System.err.println("Read time: " + (System.currentTimeMillis() - start) + " ms");
IIOMetadata metadata = reader.getImageMetadata(imageNo);
if (metadata != null) {
if (metadata.getNativeMetadataFormatName() != null) {
new XMLSerializer(System.out, "UTF-8").serialize(metadata.getAsTree(metadata.getNativeMetadataFormatName()), false);
}
/*else*/
if (metadata.isStandardMetadataFormatSupported()) {
new XMLSerializer(System.out, "UTF-8").serialize(metadata.getAsTree(IIOMetadataFormatImpl.standardMetadataFormatName), false);
}
}
// IIOMetadata metadata = reader.getImageMetadata(imageNo);
// if (metadata != null) {
// if (metadata.getNativeMetadataFormatName() != null) {
// new XMLSerializer(System.out, "UTF-8").serialize(metadata.getAsTree(metadata.getNativeMetadataFormatName()), false);
// }
// /*else*/
// if (metadata.isStandardMetadataFormatSupported()) {
// new XMLSerializer(System.out, "UTF-8").serialize(metadata.getAsTree(IIOMetadataFormatImpl.standardMetadataFormatName), false);
// }
// }
System.err.println("image: " + image);

View File

@ -37,21 +37,17 @@ 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.
* Input stream that provides on-the-fly upsampling of TIFF subsampled YCbCr 16 bit samples.
*
* @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 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;
@ -65,7 +61,7 @@ final class YCbCr16UpsamplerStream extends FilterInputStream {
int bufferLength;
int bufferPos;
public YCbCr16UpsamplerStream(final InputStream stream, final int[] chromaSub, final int yCbCrPos, final int columns, final double[] coefficients, final ByteOrder byteOrder) {
public YCbCr16UpsamplerStream(final InputStream stream, final int[] chromaSub, final int yCbCrPos, final int columns, final ByteOrder byteOrder) {
super(Validate.notNull(stream, "stream"));
Validate.notNull(chromaSub, "chromaSub");
@ -76,8 +72,6 @@ final class YCbCr16UpsamplerStream extends FilterInputStream {
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:
@ -150,7 +144,7 @@ final class YCbCr16UpsamplerStream extends FilterInputStream {
decodedRows[pixelOff + 5] = cr2;
// Convert to RGB
convertYCbCr2RGB(decodedRows, decodedRows, coefficients, pixelOff);
// convertYCbCr2RGB(decodedRows, decodedRows, coefficients, pixelOff);
}
}
@ -228,56 +222,4 @@ final class YCbCr16UpsamplerStream extends FilterInputStream {
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));
}
}

View File

@ -30,30 +30,24 @@ package com.twelvemonkeys.imageio.plugins.tiff;
import com.twelvemonkeys.lang.Validate;
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;
/**
* Input stream that provides on-the-fly conversion and upsampling of TIFF subsampled YCbCr samples to (raw) RGB samples.
* Input stream that provides on-the-fly upsampling of TIFF subsampled YCbCr samples.
*
* @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 {
// NOTE: DO NOT MODIFY OR EXPOSE THIS ARRAY OUTSIDE PACKAGE!
static final double[] CCIR_601_1_COEFFICIENTS = new double[] {299.0 / 1000.0, 587.0 / 1000.0, 114.0 / 1000.0};
private final int horizChromaSub;
private final int vertChromaSub;
private final int yCbCrPos;
private final int columns;
private final double[] coefficients;
private final int units;
private final int unitSize;
@ -66,7 +60,7 @@ final class YCbCrUpsamplerStream extends FilterInputStream {
int bufferLength;
int bufferPos;
public YCbCrUpsamplerStream(final InputStream stream, final int[] chromaSub, final int yCbCrPos, final int columns, final double[] coefficients) {
public YCbCrUpsamplerStream(final InputStream stream, final int[] chromaSub, final int yCbCrPos, final int columns) {
super(Validate.notNull(stream, "stream"));
Validate.notNull(chromaSub, "chromaSub");
@ -76,7 +70,6 @@ final class YCbCrUpsamplerStream extends FilterInputStream {
this.vertChromaSub = chromaSub[1];
this.yCbCrPos = yCbCrPos;
this.columns = columns;
this.coefficients = Arrays.equals(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:
@ -141,14 +134,6 @@ final class YCbCrUpsamplerStream extends FilterInputStream {
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);
}
}
}
@ -227,120 +212,7 @@ final class YCbCrUpsamplerStream extends FilterInputStream {
throw new IOException("mark/reset not supported");
}
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;
double cr = (yCbCr[offset + 2] & 0xff) - 128;
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);
rgb[offset ] = clamp(red);
rgb[offset + 2] = clamp(blue);
rgb[offset + 1] = clamp(green);
}
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 (TIFFImageReader.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
}
}
}

View File

@ -96,6 +96,8 @@ public class TIFFImageReaderTest extends ImageReaderAbstractTest<TIFFImageReader
new TestData(getClassLoaderResource("/tiff/ccitt/group3_2d_lsb2msb.tif"), new Dimension(6, 4)), // B/W, CCITT T4 2D, LSB
new TestData(getClassLoaderResource("/tiff/ccitt/group4.tif"), new Dimension(6, 4)), // B/W, CCITT T6 1D
new TestData(getClassLoaderResource("/tiff/fivepages-scan-causingerrors.tif"), new Dimension(2480, 3518)), // B/W, CCITT T4
// CIELab
new TestData(getClassLoaderResource("/tiff/ColorCheckerCalculator.tif"), new Dimension(798, 546)), // CIELab 8 bit/sample
// Gray
new TestData(getClassLoaderResource("/tiff/depth/flower-minisblack-02.tif"), new Dimension(73, 43)), // Gray 2 bit/sample
new TestData(getClassLoaderResource("/tiff/depth/flower-minisblack-04.tif"), new Dimension(73, 43)), // Gray 4 bit/sample
@ -120,8 +122,8 @@ public class TIFFImageReaderTest extends ImageReaderAbstractTest<TIFFImageReader
new TestData(getClassLoaderResource("/tiff/depth/flower-separated-contig-16.tif"), new Dimension(73, 43)), // CMYK 16 bit/sample
// Separated (CMYK) Planar (PlanarConfiguration: 2)
new TestData(getClassLoaderResource("/tiff/depth/flower-separated-planar-08.tif"), new Dimension(73, 43)), // CMYK 8 bit/sample
new TestData(getClassLoaderResource("/tiff/depth/flower-separated-planar-16.tif"), new Dimension(73, 43)) // CMYK 16 bit/sample
);
new TestData(getClassLoaderResource("/tiff/depth/flower-separated-planar-16.tif"), new Dimension(73, 43)) // CMYK 16 bit/sample
);
}
@Override

View File

@ -38,7 +38,8 @@ import java.io.InputStream;
import java.nio.ByteBuffer;
import java.nio.ByteOrder;
import static org.junit.Assert.*;
import static org.junit.Assert.assertArrayEquals;
import static org.junit.Assert.assertEquals;
/**
* YCbCr16UpsamplerStreamTest
@ -50,22 +51,22 @@ import static org.junit.Assert.*;
public class YCbCr16UpsamplerStreamTest {
@Test(expected = IllegalArgumentException.class)
public void testCreateNullStream() {
new YCbCr16UpsamplerStream(null, new int[2], 7, 5, null, ByteOrder.LITTLE_ENDIAN);
new YCbCr16UpsamplerStream(null, new int[2], 7, 5, 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);
new YCbCr16UpsamplerStream(new ByteArrayInputStream(new byte[0]), new int[3], 7, 5, 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);
new YCbCr16UpsamplerStream(new ByteArrayInputStream(new byte[0]), new int[1], 7, 5, 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);
new YCbCr16UpsamplerStream(new ByteArrayInputStream(new byte[0]), new int[] {2, 2}, 7, 5, null);
}
// TODO: The expected values seems bogus...
@ -81,11 +82,11 @@ public class YCbCr16UpsamplerStreamTest {
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);
InputStream stream = new YCbCr16UpsamplerStream(new ByteArrayInputStream(bytes), new int[] {2, 2}, TIFFExtension.YCBCR_POSITIONING_CENTERED, 8, 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
1, 5, 6, 2, 5, 6, 7, 108, 109, 8, 108, 109, 110, 114, 115, 111, 114, 115, 43, 0, 0, 97, 0, 0,
3, 5, 6, 4, 5, 6, 42, 108, 109, 96, 108, 109, 112, 114, 115, 113, 114, 115, 0, 0, 0, 0, 0, 0
};
short[] upsampled = new short[expected.length];
@ -107,10 +108,10 @@ public class YCbCr16UpsamplerStreamTest {
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);
InputStream stream = new YCbCr16UpsamplerStream(new ByteArrayInputStream(bytes), new int[] {2, 1}, TIFFExtension.YCBCR_POSITIONING_CENTERED, 4, 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
1, 3, 4, 2, 3, 4, 42, 77, 112, 96, 77, 112, 113, 115, 43, 114, 115, 43, 97, 77, 112, 43, 77, 112
};
short[] upsampled = new short[expected.length];
@ -132,10 +133,10 @@ public class YCbCr16UpsamplerStreamTest {
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);
InputStream stream = new YCbCr16UpsamplerStream(new ByteArrayInputStream(bytes), new int[] {1, 2}, TIFFExtension.YCBCR_POSITIONING_CENTERED, 4, 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
1, 3, 4, 42, 77, 112, 113, 115, 43, 97, 0, 0, 2, 3, 4, 96, 77, 112, 114, 115, 43, 43, 0, 0
};
short[] upsampled = new short[expected.length];

View File

@ -35,7 +35,8 @@ import java.io.DataInputStream;
import java.io.IOException;
import java.io.InputStream;
import static org.junit.Assert.*;
import static org.junit.Assert.assertArrayEquals;
import static org.junit.Assert.assertEquals;
/**
* YCbCrUpsamplerStreamTest
@ -47,17 +48,17 @@ import static org.junit.Assert.*;
public class YCbCrUpsamplerStreamTest {
@Test(expected = IllegalArgumentException.class)
public void testCreateNullStream() {
new YCbCrUpsamplerStream(null, new int[2], 7, 5, null);
new YCbCrUpsamplerStream(null, new int[2], 7, 5);
}
@Test(expected = IllegalArgumentException.class)
public void testCreateNullChroma() {
new YCbCrUpsamplerStream(new ByteArrayInputStream(new byte[0]), new int[3], 7, 5, null);
new YCbCrUpsamplerStream(new ByteArrayInputStream(new byte[0]), new int[3], 7, 5);
}
@Test(expected = IllegalArgumentException.class)
public void testCreateShortChroma() {
new YCbCrUpsamplerStream(new ByteArrayInputStream(new byte[0]), new int[1], 7, 5, null);
new YCbCrUpsamplerStream(new ByteArrayInputStream(new byte[0]), new int[1], 7, 5);
}
@Test
@ -66,16 +67,17 @@ public class YCbCrUpsamplerStreamTest {
1, 2, 3, 4, 5, 6, 7, 8, 42, 96,
108, 109, 110, 111, 112, 113, 114, 115, 43, 97
};
InputStream 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);
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,
0, -124, 0, 0, -123, 0, 15, 62, 7, 69, 116, 61, 94, 126, 87, 95, 127, 88, 0, -121, 0, 0, -121, 0
1, 5, 6, 2, 5, 6, 7, 108, 109, 8, 108, 109, 110, 114, 115, 111, 114, 115, 43, 0, 0, 97, 0, 0,
3, 5, 6, 4, 5, 6, 42, 108, 109, 96, 108, 109, 112, 114, 115, 113, 114, 115, 0, 0, 0, 0, 0, 0
};
byte[] upsampled = new byte[expected.length];
new DataInputStream(stream).readFully(upsampled);
assertArrayEquals(expected, upsampled);
assertEquals(-1, stream.read());
}
@ -86,10 +88,10 @@ public class YCbCrUpsamplerStreamTest {
1, 2, 3, 4, 42, 96, 77,
112, 113, 114, 115, 43, 97, 43
};
InputStream 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);
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
1, 3, 4, 2, 3, 4, 42, 77, 112, 96, 77, 112, 113, 115, 43, 114, 115, 43, 97, 77, 112, 43, 77, 112
};
byte[] upsampled = new byte[expected.length];
@ -105,10 +107,10 @@ public class YCbCrUpsamplerStreamTest {
1, 2, 3, 4, 42, 96, 77,
112, 113, 114, 115, 43, 97, 43
};
InputStream 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);
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
1, 3, 4, 42, 77, 112, 113, 115, 43, 97, 0, 0, 2, 3, 4, 96, 77, 112, 114, 115, 43, 43, 0, 0
};
byte[] upsampled = new byte[expected.length];