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