diff --git a/README.md b/README.md
index fa04caa4..52120686 100644
--- a/README.md
+++ b/README.md
@@ -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
diff --git a/imageio/imageio-core/src/main/java/com/twelvemonkeys/imageio/color/CIELabColorConverter.java b/imageio/imageio-core/src/main/java/com/twelvemonkeys/imageio/color/CIELabColorConverter.java
new file mode 100644
index 00000000..bb900d06
--- /dev/null
+++ b/imageio/imageio-core/src/main/java/com/twelvemonkeys/imageio/color/CIELabColorConverter.java
@@ -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);
+ }
+}
diff --git a/imageio/imageio-core/src/main/java/com/twelvemonkeys/imageio/color/YCbCrConverter.java b/imageio/imageio-core/src/main/java/com/twelvemonkeys/imageio/color/YCbCrConverter.java
new file mode 100644
index 00000000..f21f712c
--- /dev/null
+++ b/imageio/imageio-core/src/main/java/com/twelvemonkeys/imageio/color/YCbCrConverter.java
@@ -0,0 +1,81 @@
+package com.twelvemonkeys.imageio.color;
+
+/**
+ * Fast YCbCr to RGB conversion.
+ *
+ * @author Harald Kuhr
+ * @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));
+ }
+}
diff --git a/imageio/imageio-core/src/test/java/com/twelvemonkeys/imageio/color/CIELabColorConverterTest.java b/imageio/imageio-core/src/test/java/com/twelvemonkeys/imageio/color/CIELabColorConverterTest.java
new file mode 100644
index 00000000..ff3ddbeb
--- /dev/null
+++ b/imageio/imageio-core/src/test/java/com/twelvemonkeys/imageio/color/CIELabColorConverterTest.java
@@ -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 Harald Kuhr
+ * @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);
+ }
+}
diff --git a/imageio/imageio-jpeg/src/main/java/com/twelvemonkeys/imageio/plugins/jpeg/EXIFThumbnailReader.java b/imageio/imageio-jpeg/src/main/java/com/twelvemonkeys/imageio/plugins/jpeg/EXIFThumbnailReader.java
index 704dc1c9..20b5b50b 100644
--- a/imageio/imageio-jpeg/src/main/java/com/twelvemonkeys/imageio/plugins/jpeg/EXIFThumbnailReader.java
+++ b/imageio/imageio-jpeg/src/main/java/com/twelvemonkeys/imageio/plugins/jpeg/EXIFThumbnailReader.java
@@ -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(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");
diff --git a/imageio/imageio-jpeg/src/main/java/com/twelvemonkeys/imageio/plugins/jpeg/JPEGImageReader.java b/imageio/imageio-jpeg/src/main/java/com/twelvemonkeys/imageio/plugins/jpeg/JPEGImageReader.java
index 86c53931..6264cb81 100644
--- a/imageio/imageio-jpeg/src/main/java/com/twelvemonkeys/imageio/plugins/jpeg/JPEGImageReader.java
+++ b/imageio/imageio-jpeg/src/main/java/com/twelvemonkeys/imageio/plugins/jpeg/JPEGImageReader.java
@@ -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 Harald Kuhr
- * @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 {
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 c6dd6e90..470eba56 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
@@ -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 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);
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
index 8f3d591a..8045e632 100644
--- 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
@@ -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 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;
@@ -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));
- }
}
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 82ab8a17..560845d7 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
@@ -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 Harald Kuhr
* @author last modified by $Author: haraldk$
* @version $Id: YCbCrUpsamplerStream.java,v 1.0 31.01.13 09:25 haraldk Exp$
*/
final class YCbCrUpsamplerStream extends FilterInputStream {
- // 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
- }
- }
}
diff --git a/imageio/imageio-tiff/src/test/java/com/twelvemonkeys/imageio/plugins/tiff/TIFFImageReaderTest.java b/imageio/imageio-tiff/src/test/java/com/twelvemonkeys/imageio/plugins/tiff/TIFFImageReaderTest.java
index 85d98d6b..4cf86840 100644
--- a/imageio/imageio-tiff/src/test/java/com/twelvemonkeys/imageio/plugins/tiff/TIFFImageReaderTest.java
+++ b/imageio/imageio-tiff/src/test/java/com/twelvemonkeys/imageio/plugins/tiff/TIFFImageReaderTest.java
@@ -96,6 +96,8 @@ public class TIFFImageReaderTest extends ImageReaderAbstractTest