From 8a38b2fde6f9549bca91779e7f1b6f36d330e8ea Mon Sep 17 00:00:00 2001 From: Harald Kuhr Date: Thu, 22 Oct 2015 21:35:10 +0200 Subject: [PATCH] #173 Support for PhotometricInterpretation 8/CIELab, 9/ICCLab and 10/ITULab --- README.md | 9 +- .../imageio/color/CIELabColorConverter.java | 152 +++++++ .../imageio/color/YCbCrConverter.java | 81 ++++ .../color/CIELabColorConverterTest.java | 68 ++++ .../plugins/jpeg/EXIFThumbnailReader.java | 15 +- .../imageio/plugins/jpeg/JPEGImageReader.java | 142 +------ .../imageio/plugins/tiff/TIFFImageReader.java | 376 ++++++++++++++---- .../plugins/tiff/YCbCr16UpsamplerStream.java | 64 +-- .../plugins/tiff/YCbCrUpsamplerStream.java | 132 +----- .../plugins/tiff/TIFFImageReaderTest.java | 6 +- .../tiff/YCbCr16UpsamplerStreamTest.java | 25 +- .../tiff/YCbCrUpsamplerStreamTest.java | 24 +- .../resources/tiff/ColorCheckerCalculator.tif | Bin 0 -> 55346 bytes 13 files changed, 658 insertions(+), 436 deletions(-) create mode 100644 imageio/imageio-core/src/main/java/com/twelvemonkeys/imageio/color/CIELabColorConverter.java create mode 100644 imageio/imageio-core/src/main/java/com/twelvemonkeys/imageio/color/YCbCrConverter.java create mode 100644 imageio/imageio-core/src/test/java/com/twelvemonkeys/imageio/color/CIELabColorConverterTest.java create mode 100755 imageio/imageio-tiff/src/test/resources/tiff/ColorCheckerCalculator.tif 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 ImageReaderAbstractTesttO{IZ03u$yp=_l0~40O{W67X9Pz1M?CZd zjOrhbssDBlJRe4PlETR=5kWBeKaV3}41e4YU4e1@af}5WOa1*g#1;F=5oN}>Q<=LSHT6#x|)091(qP=j(?Z65&j4FEJ20nnTQKx+s9 z?GgZVpl5XY0MLbK)P-o!gWi4P9RT`G02tH&aI*{mLx=_=h!5io08EkrxD^M$ZHN}r z2mtN`0buqJ0CR5uEFhXJ9RRSh0>Ii702@O9Y$4k0)B&(p0>D8Q07ppxoFE#V`2ld@ z1mNy@09>K6cY|nkKLdaV5&%yG0A2tJ5Y66D-rSo4zz5>hXBU9`(EA=h_dS5(eb)ha zxCDUTJOKU>PpB~f0tNsG>;fRD6@cId079TV42Ad%%LgDl3xEhT0FM#@h)MEf1 zhXD{B06@$G0G@aP@YDr>SUUioSpe`H%G)>t0A6SV5U&bAf+7HkG5{or1CT5XKngDa zsT=^Lu>yc*03aO;3Q)deK>TDt?|TW|mkIHa`5u5*bpT{R@6GlGAm;`ExzO`@5FdFP z0KD!4ARoHF0Lsroh|ePEJ#WqfPz3hw}D)dippO0NT=zvm$U z14yV5(E%_9r88~@5e?A6iISPe- zfNwGd_06Pz`xleFt_EL*u0S2lXnD~X{2C4i`~X005xVyK z>I4?Y>F5RZ5zq#QpN?P`NT4`qjQIOziU(+6P=D~BZ>Kb@o!&zVq|BmEQ~BfnQ2JKGLhw>SyL5G^q$P4jJoC|0G?0DjM4v;Dd%O;a zuUa^pSpo}x=o#4YGzbDnJUl#nJS09ol8lgmkc^T9i6o(-e^OV|e&4h$q0|w=A*`DfbeEe?iCPI}_638wet81aq z{L)>NdPr~Hamg0qHnZaexTSpeQUzN9we%`rZMtj}v?#RW@QAA+ZSY}S&BM`skNgjLqoUF(kxPN=Eq6zpjsf)QlmtS(@iSgeGtc)$S+J z#$DQGQj6Rk;9lAZxe{YzyU+SjuKp_S@{J#s_x&nYc%t)X}C zQ&)j2T3t|REc(SERkp}d4C!`&6$S+S-C)h79pFqz^BYnSXjYpjQ`=OjP9|i@E!My!3$gy)478COpu05(eWEWGH zr{}plw9sC33{b~lMb{~52%)CYaY-0)(@8pO-z%`vW+$-TZntrEb@yQugX{rCf)l*fx#iqyxXqIWj01g7otq^9+_YGzEyY_ z`1%;ki2ALn`TeBi%bWS~fa4g<2G^8cE_CwVQpe(FcKOPCg-`b!-(a!{uYO$Q#ydrA z>NfOT!3DneL8(5KU)CkrNF?e~%Idlw?t8f3* z{csrRe6*Q&=)CWx0oAMj>wv|^u=VZaSsw3+qN z+IA#)7s>uc8z`B!Bfv&uCICDSHj*cZTXj<%FnO-t~DBYew$KhdtT=@H8U0$?-@T_ zBozz)xU=6T@&!hgeM16jPDi@q$Do?PZ~G}RPs**r(m11Gx?i)89#7vrnlzH5VH7c* zNxR-tIp(z|m6lZ`~jC^o?w$jhDj{`9T-fVWqffbUQ~{!Q@ulyYkm>jK|0Q z(uW&A@j?0U>eBPQxCfITBg`0t<{M}_REF_2etiCwUwwtXdoH>b#>=thdSP)}`gpK) z>GEFuHLf)cH>ukyR%7CwlV6?Ft`|-pe7f>$h{Kz`$HoFQpYWcXEtzlBFFINtSxHeLyue4?b@oR|obj}@ zf+=&ORc}7ed#DcnOuGtS^F9U}jk~>#`)VXvXT+alpCRrpr+a8NwZnb*!}l}%*(UCK zRZ}OPPgVBT!_x769$x!Iq3(7LpKMZJ!H;x4(w=@*^9u_@`~FUcJh|;2&Nd-+m*0OW zFW8q(`uSCBRs*}f-8V7z2=Rewg=I&-ZZ7<;biVnf#<#qnoRTeE0rD7d(2zz1wZVJi zd(4*wJ`1o?azs4y{qTN`o2RzsFAZ9>~-|iNLt^gQN4M&=;g)Tl9uNl{-Tc@cB^#h zt49TT?GIB|peAd!p5XXu4QG8{ZYw&v&1J91`{6xemkT+c`?7Bjv2V^S?WLW)*fL48 zXYKNWR?_%(J)&~fdx6!`CT!OpznJ1CbaBk^VkJ3U-t3a0 z@$H*Qm-I?U&HX&vD~C%D=w9Snf2;k*B|cu~;gG>_^%w7z66<9t!_a2_r{?X_W5n>y zwSzC~$KbWu7lFfW{6#b0P8n1SOS9B1-Y*AVmi(3rkHG{qB5WtN?hlXh`vmfb(7)eO zSr>KA>q1MFmnZppmii4Gv>v@mYxq`|XL7U8!{Ar43CV-&7q$#CnQ-2AGd-leaM659 z@Z-mwqt{1keQn?K>v}!~7`%i@?BJjACHo=0&U8Gqu!6nrufgzOWLNr!#*Dtk5uZ4p z^2+Xx;*O+p$@JqH9i{Tx3hLw{ed`13>|5Vfc)O1*e6@~2SMu#GlB4TJEq%YdKM@mt ztatvAX?+abOAPkHFV%jY6($?8Ns)Ttmc8hA(V^*Z5z4_22T!NJO5b=`^vTy^DgN2^ zKJBCD>i0`Hh*;w2>@DLL->v`fQFp80y1k{p(s-n6=9hR34moqT&VA@|eNX|4l0KVt zc7G@J&I=`e&wl%d2Gc0+irkH~;lY+fKk1IV&xNT3OGjnyXM=->aQ1wqndrWi&yTO% z7>Ad%SHI9-zh7S=G_fPmP;_|PaZK2~&cJ>Kw|oZ6?<0jd5kW&`Y5K;I`MOK8m~WpT zTO*FkF?hU1Y>T`b-Rrf$Rkc*9^@}_!(9txK+w^L&e*kAuTs4%%Tc+B+C{`*nQ{CDG_gKutrpOGa} zy(9FO&wg`##lkn(ARcgKJwYMz3Fi35k{o>+ zKeEeBFmZSzSRoSS@9EE~G*ZB^C=er3yx!io_-hRR&bh#_z_tf|d^^6p3->Aypr*X= zocHy?EM4F?KWwI6>-DE4cN*a9Of%UASf;RcK+=&Gn<#{eCNi=2jV6B)`0;y7++~Cu zFa2;xgH8OyA3JHs!<9#z(rI6BkF*?AKN>LIW2*Lki1(GXJ5<5*{v67F1E=SnMdNB; zcM)uC-)VZq!F;Va>CjKM&F!eqZ_Do(lrCNT*6=3q7)U&X8c(Z-^udl{HEvT8zw}nx z+C-x*qwZ%XX)9`mw|AW@*ZeHlT@R%-oTrXDk4S#@Z`kXM?sqO%+^yMmw2^QXf04`I z+xKu)L+@sqH&&pU&H^^f`4*b^FB7&#$e! z6}I~}Y7Fntmj3Kl*-lNH($*PRgzqx+AMiXm2C0q)-b|aG)JIe=kAai`;;VSk%eys? zvuwBf7Hifs@honxB%M8QzB+p3arEk8+T<)utc{c_8h*_LT)jbuM>5E*|s1|hsqH*`(Z;u}tx^&03_n4Er;RgjuA8@1~B zLH~gMC~aRkcSml;)IlKH*4yn+L;av>{;+4pcdyXi8K_+nqbJE4Gm2xNQ%hp`!uad^li3MP3-dc@$E&1%L!x1s0H6Z3d~^%s>-TzMW6iY`4>+6NQ)jzqo@L zhyq@~f07{Zmu?~zJ5NP~8z{l-NJD=MF^j4;by}idA)}5|G}o$H@RVYTQL@m!d_GR? z!ZT${?P`k(6TWy=E1k=kV~&@S)va}^7j}K6(V8~8;%rn=*D|$jb!#j|Q#5jP?R3z> zG;DC9rdAgLRpf0rfwHMK`vDOOen)P_v(~a=*15{qNnhgC*qT?}Eoc4O&%1|yO{Oje zlI&E$Mt}rufkzW^%{*#iOj9J8s63>{%I#*|qL)=8Q;!+Hy(8l1hU zbATV|iS?Ts3b2mKowJJ<7G)#}lQ+{6%e=w!-{7X|<1NHZR8WQ{hdVP!Mdjx{ulej* zfBTJr!wm_xxL)frL&qEc$c?u;Dn5^YKl4mZT=480h#P96&{ng%26a}K7^6EZTyIKd z>GR-Z10qd+Bpuf85}v=jMWZ3Y=M{Pjk|^;JPWa03kz2fp8C9)g!;W8*Y@dtZ15Rw<+%)h;bPRD;7&XbW+T(eH!5vjS zXJ_!<6qX3A1*a~n%fOzZJ_{#S84-NX2%egYdz9;R5zZv%KIM)XyfC4jo_o(grh$pyK6yzUYh94P{s1RMN&(UQUSOxGnXdj}Ihf3b?quu<0 zzrVkD6M*Lv)xv}RUSf(d!tTh=a4UIp&G>wFKKcmRmz(%dn<2g+)3YJiO7w;FnLnE; zz75vx3xBj3Msje?{CmRANt=<4*A(s{Qn{X^*zKA_vrjLRu6SW^LjLi~zpiC^z##}+ z$d(y%xg^pR2eSMx{73?w6Yf8l>r{m5XT&lDZk^S$(` z6E$H9+mssGrsIQ^^UQ+#3Zsqw)5gUh%XD&|Yk4LWwWeN8BxxlogQ$Z;enBLWMKA|( z&DkYsJ|Fd-l7TY}NB+d*_pn~7{K9&h?%cBUp^9_pOi!hQPHPGW78WADtiwq2rz}Z{ zQl|*VG!H_m2~GvX)?e-gvwu(44Q`65Et~NtU9}{sQpP4H;eI1`nXUz3)9iGTO%<`V z2)a_dPggDR|9FjG$eyo<9N8w&6U*Hzl-)Ybzt-3#DYn>&kA1%Gr8-3#{F+jb&wjeL zj!f{?$K8w3T}s1B?dhD@JhYu*BkzN%z9-&e(FmJYkWwxhqNhgRjiL#Y9T|YB1Co6Y z5hW5F)#RGuR^}MdjUhGZt2&0{S{F7mXyyFLIO(NXDD#*UI3saxb7hd1o3OMmJ$DZh zQ|)CFW-;GKTt{2#|IxcA=tx$wo=f}rR44*D)dLEL4xmp`1UG?MS@n1)&1CScJrs|y+rJJNGb z53i9nHu+i56f!=dy!J%s9?wjOO-B%ISUO3Hk+X4!$32+rM0wTYH!Ml+A!Nn{zw zvt22zmyON6$31xmf*qB7j^37ZMljwNGc-1TgEr@>Y~N&vX6xKc&hHMHtzmWVHsiPC z0`DuX1hp>GJqm5CinXIPn3q9(VYb1$f$TQ#oc&m>oOA0F6U#(|*}&TO9lUtv`<_n0 zm3WFZp)d3M?THFIYom*24Cg7IeKYW&3)`Zf_kP@=?GJRUjf|w{JbdDu>kq04uY@i6 z3Mq&2?YB+arc39k??l|D{nrB@0)0?6w&lqFGuxpzavnmA&Z}32{tR2d-ZfRle5OX z?;RjVTJV0knAa?{A#6cpN!UFj#Gk9kNiCao<}_5tK*R82QoPf#98$h1F_@E zIV2eZ8X|^M%L@bu-zCcy1tQjJb5sUauc->7!PQ{`(qFF&r@ve{f4ziSR7_iz^b@a} z>KAOzZ_|kO5;XP)nU=eWX&h}TVkp8oVmDkI=^XlR1VSYQq+j3QIfPWzsqcZsmd8mC zQITcm#;=v;JcvHuP_|93TQc;B+3T2(?mmxgE%|-Q#=S5V{kKvdG z<}cITRc(Egc!xVbaxEtwZJ&%UHBGfv5lo3(9Rb%~i#JN3y$EA(G^*~fT`7EK3bME; zOUOL9^o0+ZOoR#ynkF?e$gKuMgU+K0e;6{URoGa;#iR*R4vDxbUy%m4=Z7G{ou`}{ zmnFtiNC;V8ZwTJ&c3x4n~& zUUbszrq!C?R-tHo5YM`zlAmivu%NBS(@8%b6q2+iBUNELXbBlu-EoxW&K`#sC7pHuGH80*biLLYg*{Of4|XJq-QKo~0L z-fGP7yZT!z@1!H$d#|uF+fH#ZT>0|(2Wd;>-PBJEUM?0RR}{kJnP3R+ueQh}FYX@3 zBvP+OZ@;uqY1g2&NJAV|kEM5Xv(e2jy^ErrUCm;|jag;(<#1xH$)LpWk=g)Aoit2DZ>iGB_4HHEeJn3a`!fzE$fs4y@x?D;owlPeX`EnxVg@6qI zbN*P!_Y66xqB+}m*)c>GD%aG*AIAn}4B)07b6A3ilHg(a4Iyq8IG*$XvllcDU;AERm)$cZ%$;ASmfjSxQfySH zl|Jp4=wP4=yJ%;RS0yzd$b`k*Cl`>wFN3ugfBBn^sVSf8r#GAaq?t<1dHi3m#9n!W zmA{vV#(F}+Lb@^n;A%DGxaa}7aU{zSxjv3Wn6^8rJxAQILGH~A!}~I4h9?N+Cj_|X zq=!~$&UW{&qPRGn+I->gYuaMsdA)M{Gman|cSb=1Pn#XOTY+j=&==pkdx-XG34 zc_|-n=G?&oJe8df@O zTroD`*Aoru;p^@p_GGCQp}e}xuza-7egeDZerjE)Y1tHlS%=GxtfEpmS-hF$D{GGr zv+b-*V19xat%jnHIQ(r}rk@ofSNb{(xmr8(<;ccM%3+5TMq z1j($8+^8XD$3(plBaxKBe13)GP1>vIL7O+%AH1^`2@L-_Zaczmj-4m}Pxu)K43Ace7AjbQrSLZdNz=)rLlDG~?P6vIsfe@m)LiSstbff2WvG?ESfZL=#P zw!Ryy23_4>LbF=4YDan`oTD;TP@_b@0K~t}v%VMRO*HFj>HoDkcD|7AOg`IkQqMqh zrx5>9nZOXiww&6_2Y~xKg%MC1a0o;y5Rl6bRv`mi4kfSsj5f36{zCXUV6)I~v@7)x-B(ei2#td_*-=H)$4Z4oUE$`N%!QB)nuQ)6d zAWjv4e?~WlEi+({$NjUDN(YOYTn+Uj4?amzWn4>tDu>q|w+fVr(fKP}g-aZ)L%7Vn zD<$bS0d@V^uFMsJq}vng)4})B4OW==*bBG%1N^BDo6FH%4HqAARJC?T_4l$(O^djh zQG048tox=d(GdryFD3Fuy5$~)8 zFu%cgzEShrR#xmQP9BCO^U2mqhFeO!YK~eC1>L3Ec&gb%+66c~HVf2@F+b4LhfI~} z5>ZsGvW9J?MgDZYv^nUh=%k%`xU6lPxiUkMbn`h|f4mFVPu5|P_80} zoXK0HQMPKpnK5p6dfASLL1d(gt>f}R?OUTsuhYx=4YSjl1W%wA)Dhl0CVwk z@w>u-EeqYGYUkycd+&as58ORUHU7K2A#Z!n#fhM1h>Vi{8CP)kJppM!TxyzFw=m*e z7IB=tDNS0wfYz+b%>j0+O2+ss_y~^5(ATixWR1W~TFs6u^^Crx-UaYbd zQBPzqDFoK3eR{$cLcGGp8lQH~v<2ae*AtY%I)C_F?oCiD!*WBj3Im?}OCy}m+ig1f zXFD#s*bzDLD`p~OWd5c9I>pg6zh@FL!Y|8AJ`r1>Sca1~^}N~J!;%G0+W$ZW!{HXB z@iWaPlmWd~!zZX90g=oSfA|QUC3~XA$8B-?KE&jU&ZTu5M(yYN`SNyphM%qe1xL%M z0I&7ZOT*Y?VCrv-)m%W6Fa(lIYfm#~mwYoV<+c7k~IR&bfFS{{tpV*;x}UI84mkPn*X2U zqIa-f$_1Iu1+zmauPS}fg&_07Qh zNrtb_C}6zzU+o?m8c4MW2G#}~hqh{wu@b7W4yt6$yWYIqtN!}J>oM1ax`pP(JFye1 z)wAqz8uk^PYd;u8D^1SU-a7x|^jU0TzKx+~Yy71DQd^PdaIski{~7f) z?H!M8Rv;9`#?rp`Cx(rsHMgEf`oSqT^EZaAqp(E5BTQ2lP$qtgVQWmzeK8(Dg>(foJ3=UotnZIb!aHaScG9)w}*{w$lf7Ed+(Zct~@ z@aS5?kgfL&g!)qFj#_#6^xcLqY?)Qj%PBA9vPaB$s=q$IwVXTQ;H-V~)i2A+8T~h< z3Gcu7HfDWVrYOXvjuP$}ea-ODT2$j@$<6B7E1CBCFaAo#z;OL1T8=4eOby4hYa2{& zc47s?^x-GAKg`I58Lu%?3Pv`=NTe7!9HV_;bS{jhiP5hyr~(E{Ir07co0STKAYl+B z3`Y9fyBUKFpZIiR5F`wOgt2{KEIt@J7sk4Wv1wu~xfuI3#%ho8Rro*0LnGUyXu`qM zNGj`Jyr|Bf!TR6tRrC*YNB?E!qQedG0@ludnQi%d?r7BvHinx2bMrK>2d0fRzF7h?Ck8hq!Vv6+2D!Iz$Mji zzg9CFn@e_^gGy!E%T@VVH{5jcI#+g*E4-xmizX~~C3yYRvSX*#O78o%YZXmgD;y7( zTKQQ#<78IFz>r25I_u)C*<&T%56!CnXaDazWF&v#k=D7pbw+`ZBEYcZ?;dGzlXb2{ zvUf1rKRwdAuYM553pR28!y|2W{Z0PB?lQyQJklWQx8xw|Z=cLVu68Aws;f|#)2FGE zWph87RULjl(fZ=^_ZtS1Y*!q`68f^^jiGtSd>Mm_?uoagd0zA=ta#i^xmB6~&8pUv zAbM?d2Ispxe5=ldj=Q@(i{i#sVkQJ#0br=vP+Slqqj zZn1B~mh@_TRk3_B9uLF$-_cY|ng54b_okTIj%nv7>qcODI85KJqktLJFylE!lKEp+ z0Y-+yNUs>V9iy>ebTy2YiqX$8$OHzH!JttX91ep(Vz5^X>WsnLF_s35jRj*}`ll78 zFxDlEb?J0%Zj4Du>KnVJ8^C+5HTRRU=B zEsUSszxneH$Z9}0%^-lfd&ORP2L!&22fBVUqUU2ThX^B}o^^>Dh^roElaus3@sl&$*tGvkKe^W@ zescL%akYQpCx^r@N&nZC+xoBBOeqj@h{_B8-A@jyN&Q12VJOSw-~8khV;S3pl~MTH zf7sP~o!cS@A_edL%})-ZzEqa}Y4mSPquusp$qEJZzxR_Xq&e}EL-UUgS(y1rj>ugUi+MT&ZN;B0|%}1nK_XFOsw6gHCzJ6&u|@INW8Lb z=e$4btY5>yKB`o#ylS=wU1DcSYoyNK z`gXBy*~75PW54}hzR`-T0N^it;abt~eFehn*FS4l|Nh3Kh2ruviYGw4~TW}Y!$3KyBQ5dqEDVcJ-QM39~P2kxy zufD`9DqE2xh>O~VLX=3l7GISlJJki%Y*jlN2gn!BAaSgCQu5V$OGwEbho8oap50Cf z#%@zmY}(%hPq?VzIkH5JIe6-DD@a>c&M0q^bH_nnF^#g^c=H$EXvJ^`d1_+H?zAVw z)J#mf!}KbcJ`*$EVMYXutb&omFmfhFimtK{#b`h#7##tly<&884Dx`%RqkkFMg$Dv zhru&3Xe|bd#vtu~|H=x+j)2h-FggN8N5JR^7#-nMN5HJEH1_|>HxaGhc?(*-!nr0h zK`Qe6mb^MDhS{2P+bkF7^-a?NZ^qQ2$#VUCKuC(!dAIJmqd%cL@yB~ja=czf4Y)+1 zt!8(1=oI-FqdP3jnI~uwxX%FcqnPotEpE`GZ}v%vam^3zru~}V`1AE(cv-IX1!5^k!rAdT^sA{ob;>Tj$#Y74d{}; ziMD?Xnwd-FIM$cW{=RMcqdvzvccSdA#Eplz-3P}_qy%yP{{Q^7zQ3$NSLHMXN02E9 zE17BjOPRDPEb&TB*fexUh{bqyGv*1pF{D9Kn~sZsB=T4!MO$mY{5dL zsl2R{3|AKvlL=fD(TB0eLH5PjzV8eT+!TM54|B}0pmN&IV!T74@eUpX->L)fI za?uY8S1*~v-~Kd{a7WS=g4|ZgjoW_hqq*B2Ri5#2w^ycwqMN{XF`})w!>S_fih7}l z1S@m~KlzQFD{5Sulg(kPSb;K+bskQ=CGE-?_9smwI4qRomXB<3Ru(?yA~T8<7vO4n zU(HH}vms z=}InzhgAIz=d}?w-Mhp=4+`=leZ&$t?5BmJDwdz(wWWU_V$poP9l?;muCq{9%N@oh z-bWTqCvdhD4F?G-WdU>r6)RJC?PDDIJ6h;L2c=;34qxIx9gh2;#Go|er2^}-`2>xZ zdUBLbB9U@?J3|$Rx*?-+hREr{8+pv#eM=UL3kv$;wASc~2x=*{xDspKft>n{GKCHa zu>e!SG9D~{1Qz+j56^eLsTdMc4!U+zw}s-Jdn7yc`9@B?T7+_rNLYx$jxtMFANLfl zs_|=CydUV@%;!m8X#3p1{4cI17tP6RIzelO*or1%wppU>LeQaNP0hUNx)70pFKKLjnyd#L{BYE>q`EyOYq9X3Qpf3lhbWG7V@w#U@o*RGA zg*k@rzdovZ*R45q-mz47=^~c_*kIw?QUQ%?b$9`lrf{FuEZ*9Z2*K41TD&q2o$WTX zgZ<1*DHWzG_kyTO=n#Uf-9j5dh1^rqTBR;26d}W5j8eEP&Z8=N>8i0a z!>_yY>ZzE!A6?Z!KYohn$=4R2L%qdwt zRFx@iaIgfwLi52lss3$75{1Fu(*EniJB%87qdTzDaPIgu*khPC_L z`h50WOq9n{*#r@R=-o4q-)}ACD>5eT+A!UobBVbW5-{2CDIVr%W4=A`(`5A${8s9V zj&R2&T-P9N63@6W&s&DVRRwz`H1n>>(!DPq)LdGr@6g%f;l7T?Y>ku~=sLKJHxiua zVVM3w3n74$(8t+~_>g8)=jK2WT)TzrFob<$jt(v>)4A1!W^uQ|zPb07dhzH(F`>Wa zS**r3TO>0KKg5MJK+M4V8d^odJGeenRuEyx!YvZCK*=a4OL0iMvMJM@zE+++BYSI^ z(3dYi`070JmzIPvcQNBnY9*f?^ha#3aEnK8y!I7dB#=b8a70M52ZX0avU0fyjI~c3 zGA&@EJTkHUNt*!f2=SkK^D)w0QRT8F#=xlHHKJyj$<+_1-TV?|BdjWE}rI7aAz$PX_KFpMq< z#{fgr(?!>*Svq}q_J|A>orz(wAw-KJF)dz+SbJ&*3xaLKz0$4sfg1Anx0C!N4 zax5BI*t=5fNJK47S2h5s3{os$kKVoAB9)xs(y@rJuON*|wZHtb<(f-eho=|ZKhV&0 ztSUqkmX}OwYQy-hfiFGTu0XQ0jezn_hh=)TWd6CRtQgySbm7N0*lJL#jwL3Ue9o_D zCL1LBv-af!#X5&v5`{Q@B5tzu!xcm8qR3fSui6;-Or`S$(iHN>xU~j~xwz77)!R5Q zcc6O$GXbwsQVzJ>+_DFR_%j%Zt2jy=5*3ve*u&mEsseSz14E06bvemC$Iuw~oUQYz zL&}A&Gm)<@qD=)6oM>zLT%PWytMz<6$)>FF7Yo`ONwQEI3Q8M@iZEzdp1A~b}11?^;BFH`1DCv+}- zQ{p~dhMG>@&vztw7f96{v8VAe?0klWuPBWg3};Znd5_xp;gS*hGT5Hs)k=&M(FXj7 z4+E=)ulh$w^y-CVweVQ%W)JE~ow?Zm7EIO-0;i2A; z6EY`$WBRr8#^M)Clkzr`MoHsD$}mq#lDJ1bRod59-aV|liCpsuzzy-9cu}9~%cL>7 zbC~eP3Wa9&Ki>p!bpOlN*g->!K=`4G=Fk0OYXm3llj~+rs)TToIv~$+hyfQ+aV#Tv zj0E9`RKO;KZOLn-mf9Szov}58VTLJ(472Yf2D|3cC0)(Y zY;que$q!U>HiTyy%o*SKbIrlFwJSiAY5EIJcbgQrxXhTQ1h?6iLD+2>>H|q8@<;EQ zOMgfa{USmI+0OaI$JilqP!;Y}Regm&h}#$LP)*RrjZ##2)$N zCzg@caYraPCp*%&ll6Eowm(^gBvP`yeAJ6a7J(-c=^w2zorgj(kYa#lPH?Hl$sQbK2y?)M<(DjwRa&pHe-JV6P+pf( zD|UhDwon@KX~}P`*@SBmj3STuWADdBieD7|sU}<2gBLg)>lPn8xQ3(rXlSr4$r`FF zGGThdUGzHn8o}l_n8%i2w(0sf)_0|}YF7qW;sm3}b8gILjG5p)n-6Z|PoF zjQVq!SJ)P6soY0@SZT)XM!BKw-Kd|A>OCF^sd1#_3uYhctCR+GYcFCg;uM>KIN$r# z!^-Gcn_bn5=tNWxH+Dl{+4Yi{#Uya`&PlVk-(9Wq!R?rRkaj+3tGI_~aR0^+^}yZ2+fK_GzQI3@Jd>|EpmrLg zJtmFAao*eG`BYtU{(YDmT^E+9qHCWhX69*^ucT0X7ykY(!Ye$EBF1x3ce+F{z{Qm@KaMJV`qxu$M+B?9 zK@{1%tP-{bQXxhV3;0Vn0qg@}LU@que~xf4gA_X>EeIAE1X0ad=D8DnVGGiF zPAX1{bsIm&pVK`rf1X99s!N7kOrgOXKk@y?3Ky&+L>6k;ti%P!0d@swrBle@R4}TP z0A68xCvDPZ)Q1dmNuq};Dw#93F7*`bQEu;KH{$(qju_3VW!gs!pOKgoO!lzSKT2Jf zQhaA#LR!;iNQMe>%b+9lB4COy-25(T5GNZOvj&7GE@n=xk%-Hzi&*tsiZ$ufT#I?1 zRWV37;*%HCUsme#GW{{3-N(jtL5A_l9(v-hq8nW6K>V>C_k?5 z>7(V*7Uu{IeC&^c={xj)Db%m!+NVqlcEU+PiS5Cu^;+?%9V@* z?>ATHsR*$EH`-o|-j0YQOx{dOEc3>HSl{RGT>Wi^&A(Y|{7