mirror of
https://github.com/haraldk/TwelveMonkeys.git
synced 2025-08-04 03:55:28 -04:00
#173 Support for PhotometricInterpretation 8/CIELab, 9/ICCLab and 10/ITULab
This commit is contained in:
parent
30a7283b35
commit
8a38b2fde6
@ -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
|
||||
|
||||
|
@ -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);
|
||||
}
|
||||
}
|
@ -0,0 +1,81 @@
|
||||
package com.twelvemonkeys.imageio.color;
|
||||
|
||||
/**
|
||||
* Fast YCbCr to RGB conversion.
|
||||
*
|
||||
* @author <a href="mailto:harald.kuhr@gmail.com">Harald Kuhr</a>
|
||||
* @author Original code by Werner Randelshofer (used by permission).
|
||||
*/
|
||||
public final class YCbCrConverter {
|
||||
/**
|
||||
* Define tables for YCC->RGB color space conversion.
|
||||
*/
|
||||
private final static int SCALEBITS = 16;
|
||||
private final static int MAXJSAMPLE = 255;
|
||||
private final static int CENTERJSAMPLE = 128;
|
||||
private final static int ONE_HALF = 1 << (SCALEBITS - 1);
|
||||
|
||||
private final static int[] Cr_R_LUT = new int[MAXJSAMPLE + 1];
|
||||
private final static int[] Cb_B_LUT = new int[MAXJSAMPLE + 1];
|
||||
private final static int[] Cr_G_LUT = new int[MAXJSAMPLE + 1];
|
||||
private final static int[] Cb_G_LUT = new int[MAXJSAMPLE + 1];
|
||||
|
||||
/**
|
||||
* Initializes tables for YCC->RGB color space conversion.
|
||||
*/
|
||||
private static void buildYCCtoRGBtable() {
|
||||
if (ColorSpaces.DEBUG) {
|
||||
System.err.println("Building YCC conversion table");
|
||||
}
|
||||
|
||||
for (int i = 0, x = -CENTERJSAMPLE; i <= MAXJSAMPLE; i++, x++) {
|
||||
// i is the actual input pixel value, in the range 0..MAXJSAMPLE
|
||||
// The Cb or Cr value we are thinking of is x = i - CENTERJSAMPLE
|
||||
// Cr=>R value is nearest int to 1.40200 * x
|
||||
Cr_R_LUT[i] = (int) ((1.40200 * (1 << SCALEBITS) + 0.5) * x + ONE_HALF) >> SCALEBITS;
|
||||
// Cb=>B value is nearest int to 1.77200 * x
|
||||
Cb_B_LUT[i] = (int) ((1.77200 * (1 << SCALEBITS) + 0.5) * x + ONE_HALF) >> SCALEBITS;
|
||||
// Cr=>G value is scaled-up -0.71414 * x
|
||||
Cr_G_LUT[i] = -(int) (0.71414 * (1 << SCALEBITS) + 0.5) * x;
|
||||
// Cb=>G value is scaled-up -0.34414 * x
|
||||
// We also add in ONE_HALF so that need not do it in inner loop
|
||||
Cb_G_LUT[i] = -(int) ((0.34414) * (1 << SCALEBITS) + 0.5) * x + ONE_HALF;
|
||||
}
|
||||
}
|
||||
|
||||
static {
|
||||
buildYCCtoRGBtable();
|
||||
}
|
||||
|
||||
public static void convertYCbCr2RGB(final byte[] yCbCr, final byte[] rgb, final double[] coefficients, final int offset) {
|
||||
double y = (yCbCr[offset] & 0xff);
|
||||
double cb = (yCbCr[offset + 1] & 0xff) - 128;
|
||||
double cr = (yCbCr[offset + 2] & 0xff) - 128;
|
||||
|
||||
double lumaRed = coefficients[0];
|
||||
double lumaGreen = coefficients[1];
|
||||
double lumaBlue = coefficients[2];
|
||||
|
||||
int red = (int) Math.round(cr * (2 - 2 * lumaRed) + y);
|
||||
int blue = (int) Math.round(cb * (2 - 2 * lumaBlue) + y);
|
||||
int green = (int) Math.round((y - lumaRed * red - lumaBlue * blue) / lumaGreen);
|
||||
|
||||
rgb[offset] = clamp(red);
|
||||
rgb[offset + 2] = clamp(blue);
|
||||
rgb[offset + 1] = clamp(green);
|
||||
}
|
||||
|
||||
public static void convertYCbCr2RGB(final byte[] yCbCr, final byte[] rgb, final int offset) {
|
||||
int y = yCbCr[offset] & 0xff;
|
||||
int cr = yCbCr[offset + 2] & 0xff;
|
||||
int cb = yCbCr[offset + 1] & 0xff;
|
||||
|
||||
rgb[offset] = clamp(y + Cr_R_LUT[cr]);
|
||||
rgb[offset + 1] = clamp(y + (Cb_G_LUT[cb] + Cr_G_LUT[cr] >> SCALEBITS));
|
||||
rgb[offset + 2] = clamp(y + Cb_B_LUT[cb]);
|
||||
}
|
||||
|
||||
private static byte clamp(int val) {
|
||||
return (byte) Math.max(0, Math.min(255, val));
|
||||
}
|
||||
}
|
@ -0,0 +1,68 @@
|
||||
package com.twelvemonkeys.imageio.color;
|
||||
|
||||
import com.twelvemonkeys.imageio.color.CIELabColorConverter.Illuminant;
|
||||
import org.junit.Test;
|
||||
|
||||
import static org.junit.Assert.assertArrayEquals;
|
||||
|
||||
/**
|
||||
* CIELabColorConverterTest.
|
||||
*
|
||||
* @author <a href="mailto:harald.kuhr@gmail.com">Harald Kuhr</a>
|
||||
* @author last modified by $Author: harald.kuhr$
|
||||
* @version $Id: CIELabColorConverterTest.java,v 1.0 22/10/15 harald.kuhr Exp$
|
||||
*/
|
||||
public class CIELabColorConverterTest {
|
||||
@Test(expected = IllegalArgumentException.class)
|
||||
public void testNoIllumninant() {
|
||||
new CIELabColorConverter(null);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testD50() {
|
||||
CIELabColorConverter converter = new CIELabColorConverter(Illuminant.D50);
|
||||
float[] rgb = new float[3];
|
||||
|
||||
converter.toRGB(100, -128, -128, rgb);
|
||||
assertArrayEquals(new float[] {0, 255, 255}, rgb, 1);
|
||||
|
||||
converter.toRGB(100, 0, 0, rgb);
|
||||
assertArrayEquals(new float[] {255, 252, 220}, rgb, 5);
|
||||
|
||||
converter.toRGB(0, 0, 0, rgb);
|
||||
assertArrayEquals(new float[] {0, 0, 0}, rgb, 1);
|
||||
|
||||
converter.toRGB(100, 0, 127, rgb);
|
||||
assertArrayEquals(new float[] {255, 249, 0}, rgb, 5);
|
||||
|
||||
converter.toRGB(50, -128, 127, rgb);
|
||||
assertArrayEquals(new float[] {0, 152, 0}, rgb, 2);
|
||||
|
||||
converter.toRGB(50, 127, -128, rgb);
|
||||
assertArrayEquals(new float[] {222, 0, 255}, rgb, 2);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testD65() {
|
||||
CIELabColorConverter converter = new CIELabColorConverter(Illuminant.D65);
|
||||
float[] rgb = new float[3];
|
||||
|
||||
converter.toRGB(100, -128, -128, rgb);
|
||||
assertArrayEquals(new float[] {0, 255, 255}, rgb, 1);
|
||||
|
||||
converter.toRGB(100, 0, 0, rgb);
|
||||
assertArrayEquals(new float[] {255, 252, 255}, rgb, 5);
|
||||
|
||||
converter.toRGB(0, 0, 0, rgb);
|
||||
assertArrayEquals(new float[] {0, 0, 0}, rgb, 1);
|
||||
|
||||
converter.toRGB(100, 0, 127, rgb);
|
||||
assertArrayEquals(new float[] {255, 250, 0}, rgb, 5);
|
||||
|
||||
converter.toRGB(50, -128, 127, rgb);
|
||||
assertArrayEquals(new float[] {0, 152, 0}, rgb, 3);
|
||||
|
||||
converter.toRGB(50, 127, -128, rgb);
|
||||
assertArrayEquals(new float[] {184, 0, 255}, rgb, 5);
|
||||
}
|
||||
}
|
@ -28,6 +28,7 @@
|
||||
|
||||
package com.twelvemonkeys.imageio.plugins.jpeg;
|
||||
|
||||
import com.twelvemonkeys.imageio.color.YCbCrConverter;
|
||||
import com.twelvemonkeys.imageio.metadata.Directory;
|
||||
import com.twelvemonkeys.imageio.metadata.Entry;
|
||||
import com.twelvemonkeys.imageio.metadata.exif.TIFF;
|
||||
@ -102,7 +103,7 @@ final class EXIFThumbnailReader extends ThumbnailReader {
|
||||
thumbnail = readJPEG();
|
||||
}
|
||||
|
||||
cachedThumbnail = pixelsExposed ? null : new SoftReference<BufferedImage>(thumbnail);
|
||||
cachedThumbnail = pixelsExposed ? null : new SoftReference<>(thumbnail);
|
||||
|
||||
return thumbnail;
|
||||
}
|
||||
@ -132,14 +133,10 @@ final class EXIFThumbnailReader extends ThumbnailReader {
|
||||
input = new SequenceInputStream(new ByteArrayInputStream(fakeEmptyExif), input);
|
||||
|
||||
try {
|
||||
MemoryCacheImageInputStream stream = new MemoryCacheImageInputStream(input);
|
||||
|
||||
try {
|
||||
try (MemoryCacheImageInputStream stream = new MemoryCacheImageInputStream(input)) {
|
||||
return readJPEGThumbnail(reader, stream);
|
||||
}
|
||||
finally {
|
||||
stream.close();
|
||||
}
|
||||
}
|
||||
finally {
|
||||
input.close();
|
||||
@ -195,15 +192,15 @@ final class EXIFThumbnailReader extends ThumbnailReader {
|
||||
break;
|
||||
case 6:
|
||||
// YCbCr
|
||||
for (int i = 0, thumbDataLength = thumbData.length; i < thumbDataLength; i += 3) {
|
||||
JPEGImageReader.YCbCrConverter.convertYCbCr2RGB(thumbData, thumbData, i);
|
||||
for (int i = 0; i < thumbSize; i += 3) {
|
||||
YCbCrConverter.convertYCbCr2RGB(thumbData, thumbData, i);
|
||||
}
|
||||
break;
|
||||
default:
|
||||
throw new IIOException("Unknown PhotometricInterpretation value for uncompressed EXIF thumbnail (expected 2 or 6): " + interpretation);
|
||||
}
|
||||
|
||||
return ThumbnailReader.readRawThumbnail(thumbData, thumbData.length, 0, w, h);
|
||||
return ThumbnailReader.readRawThumbnail(thumbData, thumbSize, 0, w, h);
|
||||
}
|
||||
|
||||
throw new IIOException("Missing StripOffsets tag for uncompressed EXIF thumbnail");
|
||||
|
@ -30,6 +30,7 @@ package com.twelvemonkeys.imageio.plugins.jpeg;
|
||||
|
||||
import com.twelvemonkeys.imageio.ImageReaderBase;
|
||||
import com.twelvemonkeys.imageio.color.ColorSpaces;
|
||||
import com.twelvemonkeys.imageio.color.YCbCrConverter;
|
||||
import com.twelvemonkeys.imageio.metadata.CompoundDirectory;
|
||||
import com.twelvemonkeys.imageio.metadata.Directory;
|
||||
import com.twelvemonkeys.imageio.metadata.Entry;
|
||||
@ -462,14 +463,13 @@ public class JPEGImageReader extends ImageReaderBase {
|
||||
|
||||
// Apply source color conversion from implicit color space
|
||||
if (csType == JPEGColorSpace.YCbCr || csType == JPEGColorSpace.YCbCrA) {
|
||||
YCbCrConverter.convertYCbCr2RGB(raster);
|
||||
convertYCbCr2RGB(raster);
|
||||
}
|
||||
else if (csType == JPEGColorSpace.YCCK) {
|
||||
// TODO: Need to rethink this (non-) inversion, see #147
|
||||
// TODO: Allow param to specify inversion, or possibly the PDF decode array
|
||||
// flag0 bit 15, blend = 1 see http://graphicdesign.stackexchange.com/questions/12894/cmyk-jpegs-extracted-from-pdf-appear-inverted
|
||||
boolean invert = true;// || (adobeDCT.flags0 & 0x8000) == 0;
|
||||
YCbCrConverter.convertYCCK2CMYK(raster, invert);
|
||||
convertYCCK2CMYK(raster);
|
||||
}
|
||||
else if (csType == JPEGColorSpace.CMYK) {
|
||||
invertCMYK(raster);
|
||||
@ -1107,130 +1107,32 @@ public class JPEGImageReader extends ImageReaderBase {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Static inner class for lazy-loading of conversion tables.
|
||||
*
|
||||
* @author <a href="mailto:harald.kuhr@gmail.com">Harald Kuhr</a>
|
||||
* @author Original code by Werner Randelshofer
|
||||
*/
|
||||
static final class YCbCrConverter {
|
||||
/** Define tables for YCC->RGB color space conversion. */
|
||||
private final static int SCALEBITS = 16;
|
||||
private final static int MAXJSAMPLE = 255;
|
||||
private final static int CENTERJSAMPLE = 128;
|
||||
private final static int ONE_HALF = 1 << (SCALEBITS - 1);
|
||||
public static void convertYCbCr2RGB(final Raster raster) {
|
||||
final int height = raster.getHeight();
|
||||
final int width = raster.getWidth();
|
||||
final byte[] data = ((DataBufferByte) raster.getDataBuffer()).getData();
|
||||
|
||||
private final static int[] Cr_R_LUT = new int[MAXJSAMPLE + 1];
|
||||
private final static int[] Cb_B_LUT = new int[MAXJSAMPLE + 1];
|
||||
private final static int[] Cr_G_LUT = new int[MAXJSAMPLE + 1];
|
||||
private final static int[] Cb_G_LUT = new int[MAXJSAMPLE + 1];
|
||||
|
||||
/**
|
||||
* Initializes tables for YCC->RGB color space conversion.
|
||||
*/
|
||||
private static void buildYCCtoRGBtable() {
|
||||
if (DEBUG) {
|
||||
System.err.println("Building YCC conversion table");
|
||||
}
|
||||
|
||||
for (int i = 0, x = -CENTERJSAMPLE; i <= MAXJSAMPLE; i++, x++) {
|
||||
// i is the actual input pixel value, in the range 0..MAXJSAMPLE
|
||||
// The Cb or Cr value we are thinking of is x = i - CENTERJSAMPLE
|
||||
// Cr=>R value is nearest int to 1.40200 * x
|
||||
Cr_R_LUT[i] = (int) ((1.40200 * (1 << SCALEBITS) + 0.5) * x + ONE_HALF) >> SCALEBITS;
|
||||
// Cb=>B value is nearest int to 1.77200 * x
|
||||
Cb_B_LUT[i] = (int) ((1.77200 * (1 << SCALEBITS) + 0.5) * x + ONE_HALF) >> SCALEBITS;
|
||||
// Cr=>G value is scaled-up -0.71414 * x
|
||||
Cr_G_LUT[i] = -(int) (0.71414 * (1 << SCALEBITS) + 0.5) * x;
|
||||
// Cb=>G value is scaled-up -0.34414 * x
|
||||
// We also add in ONE_HALF so that need not do it in inner loop
|
||||
Cb_G_LUT[i] = -(int) ((0.34414) * (1 << SCALEBITS) + 0.5) * x + ONE_HALF;
|
||||
for (int y = 0; y < height; y++) {
|
||||
for (int x = 0; x < width; x++) {
|
||||
YCbCrConverter.convertYCbCr2RGB(data, data, (x + y * width) * 3);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
static {
|
||||
buildYCCtoRGBtable();
|
||||
}
|
||||
public static void convertYCCK2CMYK(final Raster raster) {
|
||||
final int height = raster.getHeight();
|
||||
final int width = raster.getWidth();
|
||||
final byte[] data = ((DataBufferByte) raster.getDataBuffer()).getData();
|
||||
|
||||
static void convertYCbCr2RGB(final Raster raster) {
|
||||
final int height = raster.getHeight();
|
||||
final int width = raster.getWidth();
|
||||
final byte[] data = ((DataBufferByte) raster.getDataBuffer()).getData();
|
||||
|
||||
for (int y = 0; y < height; y++) {
|
||||
for (int x = 0; x < width; x++) {
|
||||
convertYCbCr2RGB(data, data, (x + y * width) * 3);
|
||||
}
|
||||
for (int y = 0; y < height; y++) {
|
||||
for (int x = 0; x < width; x++) {
|
||||
int offset = (x + y * width) * 4;
|
||||
// YCC -> CMY
|
||||
YCbCrConverter.convertYCbCr2RGB(data, data, offset);
|
||||
// Inverse K
|
||||
data[offset + 3] = (byte) (0xff - data[offset + 3] & 0xff);
|
||||
}
|
||||
}
|
||||
|
||||
static void convertYCbCr2RGB(final byte[] yCbCr, final byte[] rgb, final int offset) {
|
||||
int y = yCbCr[offset ] & 0xff;
|
||||
int cr = yCbCr[offset + 2] & 0xff;
|
||||
int cb = yCbCr[offset + 1] & 0xff;
|
||||
|
||||
rgb[offset ] = clamp(y + Cr_R_LUT[cr]);
|
||||
rgb[offset + 1] = clamp(y + (Cb_G_LUT[cb] + Cr_G_LUT[cr] >> SCALEBITS));
|
||||
rgb[offset + 2] = clamp(y + Cb_B_LUT[cb]);
|
||||
}
|
||||
|
||||
static void convertYCCK2CMYK(final Raster raster, final boolean invert) {
|
||||
final int height = raster.getHeight();
|
||||
final int width = raster.getWidth();
|
||||
final byte[] data = ((DataBufferByte) raster.getDataBuffer()).getData();
|
||||
|
||||
if (invert) {
|
||||
for (int y = 0; y < height; y++) {
|
||||
for (int x = 0; x < width; x++) {
|
||||
convertYCCK2CMYKInverted(data, data, (x + y * width) * 4);
|
||||
}
|
||||
}
|
||||
}
|
||||
else {
|
||||
for (int y = 0; y < height; y++) {
|
||||
for (int x = 0; x < width; x++) {
|
||||
convertYCCK2CMYK(data, data, (x + y * width) * 4);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private static void convertYCCK2CMYKInverted(byte[] ycck, byte[] cmyk, int offset) {
|
||||
// Inverted
|
||||
int y = 255 - ycck[offset ] & 0xff;
|
||||
int cb = 255 - ycck[offset + 1] & 0xff;
|
||||
int cr = 255 - ycck[offset + 2] & 0xff;
|
||||
int k = 255 - ycck[offset + 3] & 0xff;
|
||||
|
||||
int cmykC = MAXJSAMPLE - (y + Cr_R_LUT[cr]);
|
||||
int cmykM = MAXJSAMPLE - (y + (Cb_G_LUT[cb] + Cr_G_LUT[cr] >> SCALEBITS));
|
||||
int cmykY = MAXJSAMPLE - (y + Cb_B_LUT[cb]);
|
||||
|
||||
cmyk[offset ] = clamp(cmykC);
|
||||
cmyk[offset + 1] = clamp(cmykM);
|
||||
cmyk[offset + 2] = clamp(cmykY);
|
||||
cmyk[offset + 3] = (byte) k; // K passes through unchanged
|
||||
}
|
||||
|
||||
private static void convertYCCK2CMYK(byte[] ycck, byte[] cmyk, int offset) {
|
||||
int y = ycck[offset ] & 0xff;
|
||||
int cb = ycck[offset + 1] & 0xff;
|
||||
int cr = ycck[offset + 2] & 0xff;
|
||||
int k = ycck[offset + 3] & 0xff;
|
||||
|
||||
int cmykC = MAXJSAMPLE - (y + Cr_R_LUT[cr]);
|
||||
int cmykM = MAXJSAMPLE - (y + (Cb_G_LUT[cb] + Cr_G_LUT[cr] >> SCALEBITS));
|
||||
int cmykY = MAXJSAMPLE - (y + Cb_B_LUT[cb]);
|
||||
|
||||
cmyk[offset ] = clamp(cmykC);
|
||||
cmyk[offset + 1] = clamp(cmykM);
|
||||
cmyk[offset + 2] = clamp(cmykY);
|
||||
cmyk[offset + 3] = (byte) k; // K passes through unchanged
|
||||
}
|
||||
|
||||
private static byte clamp(int val) {
|
||||
return (byte) Math.max(0, Math.min(255, val));
|
||||
}
|
||||
}
|
||||
|
||||
private class ProgressDelegator extends ProgressListenerBase implements IIOReadUpdateListener, IIOReadWarningListener {
|
||||
|
@ -29,7 +29,10 @@
|
||||
package com.twelvemonkeys.imageio.plugins.tiff;
|
||||
|
||||
import com.twelvemonkeys.imageio.ImageReaderBase;
|
||||
import com.twelvemonkeys.imageio.color.CIELabColorConverter;
|
||||
import com.twelvemonkeys.imageio.color.CIELabColorConverter.Illuminant;
|
||||
import com.twelvemonkeys.imageio.color.ColorSpaces;
|
||||
import com.twelvemonkeys.imageio.color.YCbCrConverter;
|
||||
import com.twelvemonkeys.imageio.metadata.CompoundDirectory;
|
||||
import com.twelvemonkeys.imageio.metadata.Directory;
|
||||
import com.twelvemonkeys.imageio.metadata.Entry;
|
||||
@ -49,12 +52,10 @@ import com.twelvemonkeys.io.FastByteArrayOutputStream;
|
||||
import com.twelvemonkeys.io.LittleEndianDataInputStream;
|
||||
import com.twelvemonkeys.io.enc.DecoderStream;
|
||||
import com.twelvemonkeys.io.enc.PackBitsDecoder;
|
||||
import com.twelvemonkeys.xml.XMLSerializer;
|
||||
|
||||
import javax.imageio.*;
|
||||
import javax.imageio.event.IIOReadWarningListener;
|
||||
import javax.imageio.metadata.IIOMetadata;
|
||||
import javax.imageio.metadata.IIOMetadataFormatImpl;
|
||||
import javax.imageio.plugins.jpeg.JPEGImageReadParam;
|
||||
import javax.imageio.spi.IIORegistry;
|
||||
import javax.imageio.spi.ImageReaderSpi;
|
||||
@ -145,6 +146,9 @@ public class TIFFImageReader extends ImageReaderBase {
|
||||
|
||||
final static boolean DEBUG = "true".equalsIgnoreCase(System.getProperty("com.twelvemonkeys.imageio.plugins.tiff.debug"));
|
||||
|
||||
// NOTE: DO NOT MODIFY OR EXPOSE THIS ARRAY OUTSIDE PACKAGE!
|
||||
static final double[] CCIR_601_1_COEFFICIENTS = new double[] {299.0 / 1000.0, 587.0 / 1000.0, 114.0 / 1000.0};
|
||||
|
||||
private CompoundDirectory IFDs;
|
||||
private Directory currentIFD;
|
||||
|
||||
@ -506,15 +510,29 @@ public class TIFFImageReader extends ImageReaderBase {
|
||||
|
||||
default:
|
||||
throw new IIOException(
|
||||
String.format("Unsupported TIFF SamplesPerPixels/BitsPerSample combination for Separated TIFF (expected 4/8, 4/16, 5/8 or 5/16): %d/%s", samplesPerPixel, bitsPerSample)
|
||||
String.format("Unsupported SamplesPerPixels/BitsPerSample combination for Separated TIFF (expected 4/8, 4/16, 5/8 or 5/16): %d/%s", samplesPerPixel, bitsPerSample)
|
||||
);
|
||||
}
|
||||
case TIFFExtension.PHOTOMETRIC_CIELAB:
|
||||
case TIFFExtension.PHOTOMETRIC_ICCLAB:
|
||||
case TIFFExtension.PHOTOMETRIC_ITULAB:
|
||||
// TODO: Would probably be more correct to handle using a CIELabColorSpace for RAW type?
|
||||
// L*a*b* color. Handled using conversion to sRGB
|
||||
cs = ColorSpace.getInstance(ColorSpace.CS_sRGB);
|
||||
switch (planarConfiguration) {
|
||||
case TIFFBaseline.PLANARCONFIG_CHUNKY:
|
||||
return ImageTypeSpecifiers.createInterleaved(cs, new int[] {0, 1, 2}, dataType, false, false);
|
||||
case TIFFExtension.PLANARCONFIG_PLANAR:
|
||||
// TODO: Reading works fine, but we can't convert the Lab values properly yet. Need to rewrite normalizeColor
|
||||
//return ImageTypeSpecifiers.createBanded(cs, new int[] {0, 1, 2}, new int[] {0, 0, 0}, dataType, false, false);
|
||||
default:
|
||||
throw new IIOException(
|
||||
String.format("Unsupported PlanarConfiguration for Lab color TIFF (expected 1): %d", planarConfiguration)
|
||||
);
|
||||
}
|
||||
case TIFFBaseline.PHOTOMETRIC_MASK:
|
||||
// Transparency mask
|
||||
case TIFFExtension.PHOTOMETRIC_CIELAB:
|
||||
case TIFFExtension.PHOTOMETRIC_ICCLAB:
|
||||
case TIFFExtension.PHOTOMETRIC_ITULAB:
|
||||
// L*a*b* color. Handled using conversion to linear RGB
|
||||
// TODO: Treat as grey?
|
||||
case TIFFCustom.PHOTOMETRIC_LOGL:
|
||||
case TIFFCustom.PHOTOMETRIC_LOGLUV:
|
||||
// Log
|
||||
@ -542,6 +560,12 @@ public class TIFFImageReader extends ImageReaderBase {
|
||||
return 3;
|
||||
case TIFFExtension.PHOTOMETRIC_SEPARATED:
|
||||
return getValueAsIntWithDefault(TIFF.TAG_NUMBER_OF_INKS, 4);
|
||||
|
||||
case TIFFCustom.PHOTOMETRIC_LOGL:
|
||||
case TIFFCustom.PHOTOMETRIC_LOGLUV:
|
||||
case TIFFCustom.PHOTOMETRIC_CFA:
|
||||
case TIFFCustom.PHOTOMETRIC_LINEAR_RAW:
|
||||
throw new IIOException("Unsupported TIFF PhotometricInterpretation value: " + photometricInterpretation);
|
||||
default:
|
||||
throw new IIOException("Unknown TIFF PhotometricInterpretation value: " + photometricInterpretation);
|
||||
}
|
||||
@ -549,6 +573,12 @@ public class TIFFImageReader extends ImageReaderBase {
|
||||
|
||||
private int getDataType(int sampleFormat, int bitsPerSample) throws IIOException {
|
||||
switch (sampleFormat) {
|
||||
case TIFFExtension.SAMPLEFORMAT_UNDEFINED:
|
||||
// Spec says:
|
||||
// A field value of “undefined” is a statement by the writer that it did not know how
|
||||
// to interpret the data samples; for example, if it were copying an existing image. A
|
||||
// reader would typically treat an image with “undefined” data as if the field were
|
||||
// not present (i.e. as unsigned integer data).
|
||||
case TIFFBaseline.SAMPLEFORMAT_UINT:
|
||||
return bitsPerSample <= 8 ? DataBuffer.TYPE_BYTE : bitsPerSample <= 16 ? DataBuffer.TYPE_USHORT : DataBuffer.TYPE_INT;
|
||||
case TIFFExtension.SAMPLEFORMAT_INT:
|
||||
@ -569,15 +599,6 @@ public class TIFFImageReader extends ImageReaderBase {
|
||||
}
|
||||
|
||||
throw new IIOException("Unsupported BitsPerSample for SampleFormat 3/Floating Point (expected 32): " + bitsPerSample);
|
||||
|
||||
case TIFFExtension.SAMPLEFORMAT_UNDEFINED:
|
||||
// Spec says:
|
||||
// A field value of “undefined” is a statement by the writer that it did not know how
|
||||
// to interpret the data samples; for example, if it were copying an existing image. A
|
||||
// reader would typically treat an image with “undefined” data as if the field were
|
||||
// not present (i.e. as unsigned integer data).
|
||||
// TODO: We should probably issue a warning instead, and assume SAMPLEFORMAT_UINT
|
||||
throw new IIOException("Unsupported TIFF SampleFormat: 4 (Undefined)");
|
||||
default:
|
||||
throw new IIOException("Unknown TIFF SampleFormat (expected 1, 2, 3 or 4): " + sampleFormat);
|
||||
}
|
||||
@ -671,7 +692,6 @@ public class TIFFImageReader extends ImageReaderBase {
|
||||
Set<ImageTypeSpecifier> specs = new LinkedHashSet<>(5);
|
||||
|
||||
// TODO: Based on raw type, we can probably convert to most RGB types at least, maybe gray etc
|
||||
// TODO: Planar to chunky by default
|
||||
if (rawType.getColorModel().getColorSpace().getType() == ColorSpace.TYPE_RGB) {
|
||||
if (rawType.getNumBands() == 3 && rawType.getBitsPerBand(0) == 8) {
|
||||
specs.add(ImageTypeSpecifier.createFromBufferedImageType(BufferedImage.TYPE_3BYTE_BGR));
|
||||
@ -776,7 +796,7 @@ public class TIFFImageReader extends ImageReaderBase {
|
||||
|
||||
int[] yCbCrSubsampling = null;
|
||||
int yCbCrPos = 1;
|
||||
double[] yCbCrCoefficients = null;
|
||||
// double[] yCbCrCoefficients = null;
|
||||
if (interpretation == TIFFExtension.PHOTOMETRIC_YCBCR) {
|
||||
// getRawImageType does the lookup/conversion for these
|
||||
if (rowRaster.getNumBands() != 3) {
|
||||
@ -815,15 +835,15 @@ public class TIFFImageReader extends ImageReaderBase {
|
||||
yCbCrSubsampling = new int[] {2, 2};
|
||||
}
|
||||
|
||||
Entry coefficients = currentIFD.getEntryById(TIFF.TAG_YCBCR_COEFFICIENTS);
|
||||
if (coefficients != null) {
|
||||
Rational[] value = (Rational[]) coefficients.getValue();
|
||||
yCbCrCoefficients = new double[] {value[0].doubleValue(), value[1].doubleValue(), value[2].doubleValue()};
|
||||
}
|
||||
else {
|
||||
// Default to y CCIR Recommendation 601-1 values
|
||||
yCbCrCoefficients = YCbCrUpsamplerStream.CCIR_601_1_COEFFICIENTS;
|
||||
}
|
||||
// Entry coefficients = currentIFD.getEntryById(TIFF.TAG_YCBCR_COEFFICIENTS);
|
||||
// if (coefficients != null) {
|
||||
// Rational[] value = (Rational[]) coefficients.getValue();
|
||||
// yCbCrCoefficients = new double[] {value[0].doubleValue(), value[1].doubleValue(), value[2].doubleValue()};
|
||||
// }
|
||||
// else {
|
||||
// // Default to y CCIR Recommendation 601-1 values
|
||||
// yCbCrCoefficients = YCbCrUpsamplerStream.CCIR_601_1_COEFFICIENTS;
|
||||
// }
|
||||
}
|
||||
|
||||
// Read data
|
||||
@ -854,10 +874,10 @@ public class TIFFImageReader extends ImageReaderBase {
|
||||
adapter = createUnpredictorStream(predictor, stripTileWidth, numBands, getBitsPerSample(), adapter, imageInput.getByteOrder());
|
||||
|
||||
if (interpretation == TIFFExtension.PHOTOMETRIC_YCBCR && rowRaster.getTransferType() == DataBuffer.TYPE_BYTE) {
|
||||
adapter = new YCbCrUpsamplerStream(adapter, yCbCrSubsampling, yCbCrPos, colsInTile, yCbCrCoefficients);
|
||||
adapter = new YCbCrUpsamplerStream(adapter, yCbCrSubsampling, yCbCrPos, colsInTile);
|
||||
}
|
||||
else if (interpretation == TIFFExtension.PHOTOMETRIC_YCBCR && rowRaster.getTransferType() == DataBuffer.TYPE_USHORT) {
|
||||
adapter = new YCbCr16UpsamplerStream(adapter, yCbCrSubsampling, yCbCrPos, colsInTile, yCbCrCoefficients, imageInput.getByteOrder());
|
||||
adapter = new YCbCr16UpsamplerStream(adapter, yCbCrSubsampling, yCbCrPos, colsInTile, imageInput.getByteOrder());
|
||||
}
|
||||
else if (interpretation == TIFFExtension.PHOTOMETRIC_YCBCR) {
|
||||
// Handled in getRawImageType
|
||||
@ -939,13 +959,14 @@ public class TIFFImageReader extends ImageReaderBase {
|
||||
// Read only tiles that lies within region
|
||||
if (new Rectangle(col, row, colsInTile, rowsInTile).intersects(srcRegion)) {
|
||||
imageInput.seek(stripTileOffsets[i]);
|
||||
ImageInputStream subStream = new SubImageInputStream(imageInput, stripTileByteCounts != null ? (int) stripTileByteCounts[i] : Short.MAX_VALUE);
|
||||
|
||||
try {
|
||||
int length = stripTileByteCounts != null ? (int) stripTileByteCounts[i] : Short.MAX_VALUE;
|
||||
|
||||
try (ImageInputStream subStream = new SubImageInputStream(imageInput, length)) {
|
||||
jpegReader.setInput(subStream);
|
||||
jpegParam.setSourceRegion(new Rectangle(0, 0, colsInTile, rowsInTile));
|
||||
|
||||
if (interpretation == TIFFExtension.PHOTOMETRIC_YCBCR) {
|
||||
if (interpretation == TIFFExtension.PHOTOMETRIC_YCBCR || interpretation == TIFFBaseline.PHOTOMETRIC_RGB) {
|
||||
jpegParam.setDestinationOffset(new Point(col - srcRegion.x, row - srcRegion.y));
|
||||
jpegParam.setDestination(destination);
|
||||
jpegReader.read(0, jpegParam);
|
||||
@ -954,12 +975,10 @@ public class TIFFImageReader extends ImageReaderBase {
|
||||
// Otherwise, it's likely CMYK or some other interpretation we don't need to convert.
|
||||
// We'll have to use readAsRaster and later apply color space conversion ourselves
|
||||
Raster raster = jpegReader.readRaster(0, jpegParam);
|
||||
normalizeColor(interpretation, ((DataBufferByte) raster.getDataBuffer()).getData());
|
||||
destination.getRaster().setDataElements(col - srcRegion.x, row - srcRegion.y, raster);
|
||||
}
|
||||
}
|
||||
finally {
|
||||
subStream.close();
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@ -1012,7 +1031,6 @@ public class TIFFImageReader extends ImageReaderBase {
|
||||
// 517/JPEGLosslessPredictors
|
||||
// 518/JPEGPointTransforms
|
||||
|
||||
ImageInputStream stream;
|
||||
|
||||
if (jpegOffset != -1) {
|
||||
// Straight forward case: We're good to go! We'll disregard tiling and any tables tags
|
||||
@ -1053,16 +1071,17 @@ public class TIFFImageReader extends ImageReaderBase {
|
||||
|
||||
imageInput.seek(realJPEGOffset);
|
||||
|
||||
stream = new SubImageInputStream(imageInput, jpegLenght != -1 ? jpegLenght : Integer.MAX_VALUE);
|
||||
jpegReader.setInput(stream);
|
||||
|
||||
// Read data
|
||||
processImageStarted(imageIndex); // Better yet, would be to delegate read progress here...
|
||||
|
||||
try {
|
||||
int length = jpegLenght != -1 ? jpegLenght : Integer.MAX_VALUE;
|
||||
|
||||
try (ImageInputStream stream = new SubImageInputStream(imageInput, length)) {
|
||||
jpegReader.setInput(stream);
|
||||
jpegParam.setSourceRegion(new Rectangle(0, 0, width, height));
|
||||
|
||||
if (interpretation == TIFFExtension.PHOTOMETRIC_YCBCR) {
|
||||
if (interpretation == TIFFExtension.PHOTOMETRIC_YCBCR || interpretation == TIFFBaseline.PHOTOMETRIC_RGB) {
|
||||
jpegParam.setDestination(destination);
|
||||
jpegReader.read(0, jpegParam);
|
||||
}
|
||||
@ -1073,9 +1092,6 @@ public class TIFFImageReader extends ImageReaderBase {
|
||||
destination.getRaster().setDataElements(0, 0, raster);
|
||||
}
|
||||
}
|
||||
finally {
|
||||
stream.close();
|
||||
}
|
||||
|
||||
processImageProgress(100f);
|
||||
|
||||
@ -1149,26 +1165,31 @@ public class TIFFImageReader extends ImageReaderBase {
|
||||
// Read only tiles that lies within region
|
||||
if (new Rectangle(col, row, colsInTile, rowsInTile).intersects(srcRegion)) {
|
||||
imageInput.seek(stripTileOffsets[i]);
|
||||
stream = ImageIO.createImageInputStream(new SequenceInputStream(Collections.enumeration(
|
||||
|
||||
try (ImageInputStream stream = ImageIO.createImageInputStream(new SequenceInputStream(Collections.enumeration(
|
||||
Arrays.asList(
|
||||
createJFIFStream(destRaster, stripTileWidth, stripTileHeight, qTables, dcTables, acTables),
|
||||
IIOUtil.createStreamAdapter(imageInput, stripTileByteCounts != null ? (int) stripTileByteCounts[i] : Short.MAX_VALUE),
|
||||
IIOUtil.createStreamAdapter(imageInput, stripTileByteCounts != null
|
||||
? (int) stripTileByteCounts[i]
|
||||
: Short.MAX_VALUE),
|
||||
new ByteArrayInputStream(new byte[] {(byte) 0xff, (byte) 0xd9}) // EOI
|
||||
)
|
||||
)));
|
||||
|
||||
jpegReader.setInput(stream);
|
||||
|
||||
try {
|
||||
)))) {
|
||||
jpegReader.setInput(stream);
|
||||
jpegParam.setSourceRegion(new Rectangle(0, 0, colsInTile, rowsInTile));
|
||||
jpegParam.setDestinationOffset(new Point(col - srcRegion.x, row - srcRegion.y));
|
||||
jpegParam.setDestination(destination);
|
||||
// TODO: This works only if Gray/YCbCr/RGB, not CMYK/LAB/etc...
|
||||
// In the latter case we will have to use readAsRaster and do color conversion ourselves
|
||||
jpegReader.read(0, jpegParam);
|
||||
}
|
||||
finally {
|
||||
stream.close();
|
||||
|
||||
if (interpretation == TIFFExtension.PHOTOMETRIC_YCBCR || interpretation == TIFFBaseline.PHOTOMETRIC_RGB) {
|
||||
jpegParam.setDestination(destination);
|
||||
jpegReader.read(0, jpegParam);
|
||||
}
|
||||
else {
|
||||
// Otherwise, it's likely CMYK or some other interpretation we don't need to convert.
|
||||
// We'll have to use readAsRaster and later apply color space conversion ourselves
|
||||
Raster raster = jpegReader.readRaster(0, jpegParam);
|
||||
destination.getRaster().setDataElements(0, 0, raster);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -1348,7 +1369,8 @@ public class TIFFImageReader extends ImageReaderBase {
|
||||
case DataBuffer.TYPE_BYTE:
|
||||
|
||||
for (int band = 0; band < bands; band++) {
|
||||
byte[] rowDataByte = ((DataBufferByte) dataBuffer).getData(band);
|
||||
int bank = banded ? ((BandedSampleModel) tileRowRaster.getSampleModel()).getBankIndices()[band] : band;
|
||||
byte[] rowDataByte = ((DataBufferByte) dataBuffer).getData(bank);
|
||||
WritableRaster destChannel = banded
|
||||
? raster.createWritableChild(raster.getMinX(), raster.getMinY(), raster.getWidth(), raster.getHeight(), 0, 0, new int[] {band})
|
||||
: raster;
|
||||
@ -1364,7 +1386,9 @@ public class TIFFImageReader extends ImageReaderBase {
|
||||
input.readFully(rowDataByte);
|
||||
|
||||
if (row % ySub == 0 && row >= srcRegion.y) {
|
||||
normalizeBlack(interpretation, rowDataByte);
|
||||
if (!banded) {
|
||||
normalizeColor(interpretation, rowDataByte);
|
||||
}
|
||||
|
||||
// Subsample horizontal
|
||||
if (xSub != 1) {
|
||||
@ -1379,6 +1403,11 @@ public class TIFFImageReader extends ImageReaderBase {
|
||||
}
|
||||
}
|
||||
|
||||
// if (banded) {
|
||||
// // TODO: Normalize colors for tile (need to know tile region and sample model)
|
||||
// // Unfortunately, this will disable acceleration...
|
||||
// }
|
||||
|
||||
break;
|
||||
case DataBuffer.TYPE_USHORT:
|
||||
case DataBuffer.TYPE_SHORT:
|
||||
@ -1402,7 +1431,7 @@ public class TIFFImageReader extends ImageReaderBase {
|
||||
readFully(input, rowDataShort);
|
||||
|
||||
if (row >= srcRegion.y) {
|
||||
normalizeBlack(interpretation, rowDataShort);
|
||||
normalizeColor(interpretation, rowDataShort);
|
||||
|
||||
// Subsample horizontal
|
||||
if (xSub != 1) {
|
||||
@ -1439,7 +1468,7 @@ public class TIFFImageReader extends ImageReaderBase {
|
||||
readFully(input, rowDataInt);
|
||||
|
||||
if (row >= srcRegion.y) {
|
||||
normalizeBlack(interpretation, rowDataInt);
|
||||
normalizeColor(interpretation, rowDataInt);
|
||||
|
||||
// Subsample horizontal
|
||||
if (xSub != 1) {
|
||||
@ -1477,6 +1506,7 @@ public class TIFFImageReader extends ImageReaderBase {
|
||||
if (row >= srcRegion.y) {
|
||||
// TODO: Allow param to decide tone mapping strategy, like in the HDRImageReader
|
||||
clamp(rowDataFloat);
|
||||
normalizeColor(interpretation, rowDataFloat);
|
||||
|
||||
// Subsample horizontal
|
||||
if (xSub != 1) {
|
||||
@ -1542,33 +1572,203 @@ public class TIFFImageReader extends ImageReaderBase {
|
||||
}
|
||||
}
|
||||
|
||||
private void normalizeBlack(int photometricInterpretation, short[] data) {
|
||||
if (photometricInterpretation == TIFFBaseline.PHOTOMETRIC_WHITE_IS_ZERO) {
|
||||
// Inverse values
|
||||
for (int i = 0; i < data.length; i++) {
|
||||
data[i] = (short) (0xffff - data[i] & 0xffff);
|
||||
}
|
||||
private void normalizeColor(int photometricInterpretation, byte[] data) {
|
||||
switch (photometricInterpretation) {
|
||||
case TIFFBaseline.PHOTOMETRIC_WHITE_IS_ZERO:
|
||||
// Inverse values
|
||||
for (int i = 0; i < data.length; i++) {
|
||||
data[i] ^= -1;
|
||||
}
|
||||
|
||||
break;
|
||||
|
||||
case TIFFExtension.PHOTOMETRIC_CIELAB:
|
||||
case TIFFExtension.PHOTOMETRIC_ICCLAB:
|
||||
case TIFFExtension.PHOTOMETRIC_ITULAB:
|
||||
CIELabColorConverter converter = new CIELabColorConverter(
|
||||
photometricInterpretation != TIFFExtension.PHOTOMETRIC_ITULAB
|
||||
? Illuminant.D65
|
||||
: Illuminant.D50
|
||||
);
|
||||
float[] temp = new float[3];
|
||||
|
||||
for (int i = 0; i < data.length; i += 3) {
|
||||
// Unsigned scaled form 0...100
|
||||
float LStar = (data[i] & 0xff) * 100f / 255.0f;
|
||||
float aStar;
|
||||
float bStar;
|
||||
|
||||
if (photometricInterpretation == TIFFExtension.PHOTOMETRIC_CIELAB) {
|
||||
// -128...127
|
||||
aStar = data[i + 1];
|
||||
bStar = data[i + 2];
|
||||
}
|
||||
else {
|
||||
// Assumes same data for ICC and ITU (unsigned)
|
||||
// 0...255
|
||||
aStar = (data[i + 1] & 0xff) - 128;
|
||||
bStar = (data[i + 2] & 0xff) - 128;
|
||||
}
|
||||
|
||||
converter.toRGB(LStar, aStar, bStar, temp);
|
||||
|
||||
data[i ] = (byte) temp[0];
|
||||
data[i + 1] = (byte) temp[1];
|
||||
data[i + 2] = (byte) temp[2];
|
||||
}
|
||||
|
||||
break;
|
||||
|
||||
case TIFFExtension.PHOTOMETRIC_YCBCR:
|
||||
Entry coefficients = currentIFD.getEntryById(TIFF.TAG_YCBCR_COEFFICIENTS);
|
||||
|
||||
if (coefficients == null) {
|
||||
for (int i = 0; i < data.length; i += 3) {
|
||||
YCbCrConverter.convertYCbCr2RGB(data, data, i);
|
||||
}
|
||||
}
|
||||
else {
|
||||
Rational[] value = (Rational[]) coefficients.getValue();
|
||||
double[] yCbCrCoefficients = new double[] {value[0].doubleValue(), value[1].doubleValue(), value[2].doubleValue()};
|
||||
|
||||
for (int i = 0; i < data.length; i += 3) {
|
||||
YCbCrConverter.convertYCbCr2RGB(data, data, yCbCrCoefficients, i);
|
||||
}
|
||||
}
|
||||
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
private void normalizeBlack(int photometricInterpretation, int[] data) {
|
||||
if (photometricInterpretation == TIFFBaseline.PHOTOMETRIC_WHITE_IS_ZERO) {
|
||||
// Inverse values
|
||||
for (int i = 0; i < data.length; i++) {
|
||||
data[i] = (0xffffffff - data[i]);
|
||||
}
|
||||
private void normalizeColor(int photometricInterpretation, short[] data) {
|
||||
switch (photometricInterpretation) {
|
||||
case TIFFBaseline.PHOTOMETRIC_WHITE_IS_ZERO:
|
||||
// Inverse values
|
||||
for (int i = 0; i < data.length; i++) {
|
||||
data[i] ^= -1;
|
||||
}
|
||||
|
||||
break;
|
||||
|
||||
case TIFFExtension.PHOTOMETRIC_CIELAB:
|
||||
case TIFFExtension.PHOTOMETRIC_ICCLAB:
|
||||
case TIFFExtension.PHOTOMETRIC_ITULAB:
|
||||
CIELabColorConverter converter = new CIELabColorConverter(
|
||||
photometricInterpretation != TIFFExtension.PHOTOMETRIC_ITULAB
|
||||
? Illuminant.D65
|
||||
: Illuminant.D50
|
||||
);
|
||||
|
||||
float[] temp = new float[3];
|
||||
float scaleL = photometricInterpretation == TIFFExtension.PHOTOMETRIC_CIELAB ? 65535f : 65280f; // Is for ICC lab, assumes the same for ITU....
|
||||
|
||||
for (int i = 0; i < data.length; i += 3) {
|
||||
// Unsigned scaled form 0...100
|
||||
float LStar = (data[i] & 0xffff) * 100.0f / scaleL;
|
||||
float aStar;
|
||||
float bStar;
|
||||
|
||||
if (photometricInterpretation == TIFFExtension.PHOTOMETRIC_CIELAB) {
|
||||
// -32768...32767
|
||||
aStar = data[i + 1] / 256f;
|
||||
bStar = data[i + 2] / 256f;
|
||||
}
|
||||
else {
|
||||
// Assumes same data for ICC and ITU (unsigned)
|
||||
// 0...65535f
|
||||
aStar = ((data[i + 1] & 0xffff) - 32768) / 256f;
|
||||
bStar = ((data[i + 2] & 0xffff) - 32768) / 256f;
|
||||
}
|
||||
|
||||
converter.toRGB(LStar, aStar, bStar, temp);
|
||||
|
||||
data[i ] = (short) (temp[0] * 257f);
|
||||
data[i + 1] = (short) (temp[1] * 257f);
|
||||
data[i + 2] = (short) (temp[2] * 257f);
|
||||
}
|
||||
|
||||
break;
|
||||
|
||||
case TIFFExtension.PHOTOMETRIC_YCBCR:
|
||||
double[] coefficients;
|
||||
|
||||
Entry coefficientsTag = currentIFD.getEntryById(TIFF.TAG_YCBCR_COEFFICIENTS);
|
||||
if (coefficientsTag != null) {
|
||||
Rational[] value = (Rational[]) coefficientsTag.getValue();
|
||||
coefficients = new double[] {value[0].doubleValue(), value[1].doubleValue(), value[2].doubleValue()};
|
||||
}
|
||||
else {
|
||||
coefficients = CCIR_601_1_COEFFICIENTS;
|
||||
}
|
||||
|
||||
for (int i = 0; i < data.length; i += 3) {
|
||||
convertYCbCr2RGB(data, data, coefficients, i);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void normalizeBlack(int photometricInterpretation, byte[] data) {
|
||||
if (photometricInterpretation == TIFFBaseline.PHOTOMETRIC_WHITE_IS_ZERO) {
|
||||
// Inverse values
|
||||
for (int i = 0; i < data.length; i++) {
|
||||
data[i] = (byte) (0xff - data[i] & 0xff);
|
||||
}
|
||||
private void normalizeColor(int photometricInterpretation, int[] data) {
|
||||
switch (photometricInterpretation) {
|
||||
case TIFFBaseline.PHOTOMETRIC_WHITE_IS_ZERO:
|
||||
// Inverse values
|
||||
for (int i = 0; i < data.length; i++) {
|
||||
data[i] ^= -1;
|
||||
}
|
||||
|
||||
break;
|
||||
|
||||
case TIFFExtension.PHOTOMETRIC_CIELAB:
|
||||
case TIFFExtension.PHOTOMETRIC_ICCLAB:
|
||||
case TIFFExtension.PHOTOMETRIC_ITULAB:
|
||||
case TIFFExtension.PHOTOMETRIC_YCBCR:
|
||||
// Not supported
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
private void normalizeColor(int photometricInterpretation, float[] data) {
|
||||
switch (photometricInterpretation) {
|
||||
case TIFFBaseline.PHOTOMETRIC_WHITE_IS_ZERO:
|
||||
case TIFFExtension.PHOTOMETRIC_CIELAB:
|
||||
case TIFFExtension.PHOTOMETRIC_ICCLAB:
|
||||
case TIFFExtension.PHOTOMETRIC_ITULAB:
|
||||
case TIFFExtension.PHOTOMETRIC_YCBCR:
|
||||
// Not supported
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
private void convertYCbCr2RGB(final short[] yCbCr, final short[] rgb, final double[] coefficients, final int offset) {
|
||||
int y;
|
||||
int cb;
|
||||
int cr;
|
||||
|
||||
y = (yCbCr[offset + 0] & 0xffff);
|
||||
cb = (yCbCr[offset + 1] & 0xffff) - 32768;
|
||||
cr = (yCbCr[offset + 2] & 0xffff) - 32768;
|
||||
|
||||
double lumaRed = coefficients[0];
|
||||
double lumaGreen = coefficients[1];
|
||||
double lumaBlue = coefficients[2];
|
||||
|
||||
int red = (int) Math.round(cr * (2 - 2 * lumaRed) + y);
|
||||
int blue = (int) Math.round(cb * (2 - 2 * lumaBlue) + y);
|
||||
int green = (int) Math.round((y - lumaRed * (red) - lumaBlue * (blue)) / lumaGreen);
|
||||
|
||||
short r = clampShort(red);
|
||||
short g = clampShort(green);
|
||||
short b = clampShort(blue);
|
||||
|
||||
// Short values, depends on byte order!
|
||||
rgb[offset] = r;
|
||||
rgb[offset + 1] = g;
|
||||
rgb[offset + 2] = b;
|
||||
}
|
||||
|
||||
private short clampShort(int val) {
|
||||
return (short) Math.max(0, Math.min(0xffff, val));
|
||||
}
|
||||
|
||||
private InputStream createDecompressorStream(final int compression, final int width, final int bands, final InputStream stream) throws IOException {
|
||||
switch (compression) {
|
||||
case TIFFBaseline.COMPRESSION_NONE:
|
||||
@ -1776,16 +1976,16 @@ public class TIFFImageReader extends ImageReaderBase {
|
||||
BufferedImage image = reader.read(imageNo, param);
|
||||
System.err.println("Read time: " + (System.currentTimeMillis() - start) + " ms");
|
||||
|
||||
IIOMetadata metadata = reader.getImageMetadata(imageNo);
|
||||
if (metadata != null) {
|
||||
if (metadata.getNativeMetadataFormatName() != null) {
|
||||
new XMLSerializer(System.out, "UTF-8").serialize(metadata.getAsTree(metadata.getNativeMetadataFormatName()), false);
|
||||
}
|
||||
/*else*/
|
||||
if (metadata.isStandardMetadataFormatSupported()) {
|
||||
new XMLSerializer(System.out, "UTF-8").serialize(metadata.getAsTree(IIOMetadataFormatImpl.standardMetadataFormatName), false);
|
||||
}
|
||||
}
|
||||
// IIOMetadata metadata = reader.getImageMetadata(imageNo);
|
||||
// if (metadata != null) {
|
||||
// if (metadata.getNativeMetadataFormatName() != null) {
|
||||
// new XMLSerializer(System.out, "UTF-8").serialize(metadata.getAsTree(metadata.getNativeMetadataFormatName()), false);
|
||||
// }
|
||||
// /*else*/
|
||||
// if (metadata.isStandardMetadataFormatSupported()) {
|
||||
// new XMLSerializer(System.out, "UTF-8").serialize(metadata.getAsTree(IIOMetadataFormatImpl.standardMetadataFormatName), false);
|
||||
// }
|
||||
// }
|
||||
|
||||
System.err.println("image: " + image);
|
||||
|
||||
|
@ -37,21 +37,17 @@ import java.io.InputStream;
|
||||
import java.nio.ByteOrder;
|
||||
|
||||
/**
|
||||
* Input stream that provides on-the-fly conversion and upsampling of TIFF subsampled YCbCr 16 bit samples
|
||||
* to (raw) RGB 16 bit samples.
|
||||
* Input stream that provides on-the-fly upsampling of TIFF subsampled YCbCr 16 bit samples.
|
||||
*
|
||||
* @author <a href="mailto:harald.kuhr@gmail.com">Harald Kuhr</a>
|
||||
* @author last modified by $Author: haraldk$
|
||||
* @version $Id: YCbCrUpsamplerStream.java,v 1.0 31.01.13 09:25 haraldk Exp$
|
||||
*/
|
||||
final class YCbCr16UpsamplerStream extends FilterInputStream {
|
||||
// TODO: As we deal with short/16 bit samples, we need to take byte order into account
|
||||
private final int horizChromaSub;
|
||||
private final int vertChromaSub;
|
||||
private final int yCbCrPos;
|
||||
private final int columns;
|
||||
private final double[] coefficients;
|
||||
private final ByteOrder byteOrder;
|
||||
|
||||
private final int units;
|
||||
private final int unitSize;
|
||||
@ -65,7 +61,7 @@ final class YCbCr16UpsamplerStream extends FilterInputStream {
|
||||
int bufferLength;
|
||||
int bufferPos;
|
||||
|
||||
public YCbCr16UpsamplerStream(final InputStream stream, final int[] chromaSub, final int yCbCrPos, final int columns, final double[] coefficients, final ByteOrder byteOrder) {
|
||||
public YCbCr16UpsamplerStream(final InputStream stream, final int[] chromaSub, final int yCbCrPos, final int columns, final ByteOrder byteOrder) {
|
||||
super(Validate.notNull(stream, "stream"));
|
||||
|
||||
Validate.notNull(chromaSub, "chromaSub");
|
||||
@ -76,8 +72,6 @@ final class YCbCr16UpsamplerStream extends FilterInputStream {
|
||||
this.vertChromaSub = chromaSub[1];
|
||||
this.yCbCrPos = yCbCrPos;
|
||||
this.columns = columns;
|
||||
this.coefficients = coefficients == null ? YCbCrUpsamplerStream.CCIR_601_1_COEFFICIENTS : coefficients;
|
||||
this.byteOrder = byteOrder;
|
||||
|
||||
// In TIFF, subsampled streams are stored in "units" of horiz * vert pixels.
|
||||
// For a 4:2 subsampled stream like this:
|
||||
@ -150,7 +144,7 @@ final class YCbCr16UpsamplerStream extends FilterInputStream {
|
||||
decodedRows[pixelOff + 5] = cr2;
|
||||
|
||||
// Convert to RGB
|
||||
convertYCbCr2RGB(decodedRows, decodedRows, coefficients, pixelOff);
|
||||
// convertYCbCr2RGB(decodedRows, decodedRows, coefficients, pixelOff);
|
||||
}
|
||||
}
|
||||
|
||||
@ -228,56 +222,4 @@ final class YCbCr16UpsamplerStream extends FilterInputStream {
|
||||
public synchronized void reset() throws IOException {
|
||||
throw new IOException("mark/reset not supported");
|
||||
}
|
||||
|
||||
private void convertYCbCr2RGB(final byte[] yCbCr, final byte[] rgb, final double[] coefficients, final int offset) {
|
||||
int y;
|
||||
int cb;
|
||||
int cr;
|
||||
|
||||
// Short values, depends on byte order!
|
||||
if (byteOrder == ByteOrder.BIG_ENDIAN) {
|
||||
y = ((yCbCr[offset ] & 0xff) << 8) | (yCbCr[offset + 1] & 0xff);
|
||||
cb = (((yCbCr[offset + 2] & 0xff) << 8) | (yCbCr[offset + 3] & 0xff)) - 32768;
|
||||
cr = (((yCbCr[offset + 4] & 0xff) << 8) | (yCbCr[offset + 5] & 0xff)) - 32768;
|
||||
}
|
||||
else {
|
||||
y = ((yCbCr[offset + 1] & 0xff) << 8) | (yCbCr[offset ] & 0xff);
|
||||
cb = (((yCbCr[offset + 3] & 0xff) << 8) | (yCbCr[offset + 2] & 0xff)) - 32768;
|
||||
cr = (((yCbCr[offset + 5] & 0xff) << 8) | (yCbCr[offset + 4] & 0xff)) - 32768;
|
||||
}
|
||||
|
||||
double lumaRed = coefficients[0];
|
||||
double lumaGreen = coefficients[1];
|
||||
double lumaBlue = coefficients[2];
|
||||
|
||||
int red = (int) Math.round(cr * (2 - 2 * lumaRed) + y);
|
||||
int blue = (int) Math.round(cb * (2 - 2 * lumaBlue) + y);
|
||||
int green = (int) Math.round((y - lumaRed * (red) - lumaBlue * (blue)) / lumaGreen);
|
||||
|
||||
short r = clampShort(red);
|
||||
short g = clampShort(green);
|
||||
short b = clampShort(blue);
|
||||
|
||||
// Short values, depends on byte order!
|
||||
if (byteOrder == ByteOrder.BIG_ENDIAN) {
|
||||
rgb[offset ] = (byte) ((r >>> 8) & 0xff);
|
||||
rgb[offset + 1] = (byte) (r & 0xff);
|
||||
rgb[offset + 2] = (byte) ((g >>> 8) & 0xff);
|
||||
rgb[offset + 3] = (byte) (g & 0xff);
|
||||
rgb[offset + 4] = (byte) ((b >>> 8) & 0xff);
|
||||
rgb[offset + 5] = (byte) (b & 0xff);
|
||||
}
|
||||
else {
|
||||
rgb[offset ] = (byte) (r & 0xff);
|
||||
rgb[offset + 1] = (byte) ((r >>> 8) & 0xff);
|
||||
rgb[offset + 2] = (byte) (g & 0xff);
|
||||
rgb[offset + 3] = (byte) ((g >>> 8) & 0xff);
|
||||
rgb[offset + 4] = (byte) (b & 0xff);
|
||||
rgb[offset + 5] = (byte) ((b >>> 8) & 0xff);
|
||||
}
|
||||
}
|
||||
|
||||
private short clampShort(int val) {
|
||||
return (short) Math.max(0, Math.min(0xffff, val));
|
||||
}
|
||||
}
|
||||
|
@ -30,30 +30,24 @@ package com.twelvemonkeys.imageio.plugins.tiff;
|
||||
|
||||
import com.twelvemonkeys.lang.Validate;
|
||||
|
||||
import java.awt.image.DataBufferByte;
|
||||
import java.awt.image.Raster;
|
||||
import java.io.EOFException;
|
||||
import java.io.FilterInputStream;
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
import java.util.Arrays;
|
||||
|
||||
/**
|
||||
* Input stream that provides on-the-fly conversion and upsampling of TIFF subsampled YCbCr samples to (raw) RGB samples.
|
||||
* Input stream that provides on-the-fly upsampling of TIFF subsampled YCbCr samples.
|
||||
*
|
||||
* @author <a href="mailto:harald.kuhr@gmail.com">Harald Kuhr</a>
|
||||
* @author last modified by $Author: haraldk$
|
||||
* @version $Id: YCbCrUpsamplerStream.java,v 1.0 31.01.13 09:25 haraldk Exp$
|
||||
*/
|
||||
final class YCbCrUpsamplerStream extends FilterInputStream {
|
||||
// NOTE: DO NOT MODIFY OR EXPOSE THIS ARRAY OUTSIDE PACKAGE!
|
||||
static final double[] CCIR_601_1_COEFFICIENTS = new double[] {299.0 / 1000.0, 587.0 / 1000.0, 114.0 / 1000.0};
|
||||
|
||||
private final int horizChromaSub;
|
||||
private final int vertChromaSub;
|
||||
private final int yCbCrPos;
|
||||
private final int columns;
|
||||
private final double[] coefficients;
|
||||
|
||||
private final int units;
|
||||
private final int unitSize;
|
||||
@ -66,7 +60,7 @@ final class YCbCrUpsamplerStream extends FilterInputStream {
|
||||
int bufferLength;
|
||||
int bufferPos;
|
||||
|
||||
public YCbCrUpsamplerStream(final InputStream stream, final int[] chromaSub, final int yCbCrPos, final int columns, final double[] coefficients) {
|
||||
public YCbCrUpsamplerStream(final InputStream stream, final int[] chromaSub, final int yCbCrPos, final int columns) {
|
||||
super(Validate.notNull(stream, "stream"));
|
||||
|
||||
Validate.notNull(chromaSub, "chromaSub");
|
||||
@ -76,7 +70,6 @@ final class YCbCrUpsamplerStream extends FilterInputStream {
|
||||
this.vertChromaSub = chromaSub[1];
|
||||
this.yCbCrPos = yCbCrPos;
|
||||
this.columns = columns;
|
||||
this.coefficients = Arrays.equals(CCIR_601_1_COEFFICIENTS, coefficients) ? null : coefficients;
|
||||
|
||||
// In TIFF, subsampled streams are stored in "units" of horiz * vert pixels.
|
||||
// For a 4:2 subsampled stream like this:
|
||||
@ -141,14 +134,6 @@ final class YCbCrUpsamplerStream extends FilterInputStream {
|
||||
decodedRows[pixelOff] = buffer[bufferPos++];
|
||||
decodedRows[pixelOff + 1] = cb;
|
||||
decodedRows[pixelOff + 2] = cr;
|
||||
|
||||
// Convert to RGB
|
||||
if (coefficients == null) {
|
||||
YCbCrConverter.convertYCbCr2RGB(decodedRows, decodedRows, pixelOff);
|
||||
}
|
||||
else {
|
||||
convertYCbCr2RGB(decodedRows, decodedRows, coefficients, pixelOff);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -227,120 +212,7 @@ final class YCbCrUpsamplerStream extends FilterInputStream {
|
||||
throw new IOException("mark/reset not supported");
|
||||
}
|
||||
|
||||
private void convertYCbCr2RGB(final byte[] yCbCr, final byte[] rgb, final double[] coefficients, final int offset) {
|
||||
double y = (yCbCr[offset ] & 0xff);
|
||||
double cb = (yCbCr[offset + 1] & 0xff) - 128;
|
||||
double cr = (yCbCr[offset + 2] & 0xff) - 128;
|
||||
|
||||
double lumaRed = coefficients[0];
|
||||
double lumaGreen = coefficients[1];
|
||||
double lumaBlue = coefficients[2];
|
||||
|
||||
int red = (int) Math.round(cr * (2 - 2 * lumaRed) + y);
|
||||
int blue = (int) Math.round(cb * (2 - 2 * lumaBlue) + y);
|
||||
int green = (int) Math.round((y - lumaRed * red - lumaBlue * blue) / lumaGreen);
|
||||
|
||||
rgb[offset ] = clamp(red);
|
||||
rgb[offset + 2] = clamp(blue);
|
||||
rgb[offset + 1] = clamp(green);
|
||||
}
|
||||
|
||||
private static byte clamp(int val) {
|
||||
return (byte) Math.max(0, Math.min(255, val));
|
||||
}
|
||||
|
||||
// TODO: This code is copied from JPEG package, make it "more" public: com.tm.imageio.color package?
|
||||
/**
|
||||
* Static inner class for lazy-loading of conversion tables.
|
||||
*/
|
||||
static final class YCbCrConverter {
|
||||
/** Define tables for YCC->RGB color space conversion. */
|
||||
private final static int SCALEBITS = 16;
|
||||
private final static int MAXJSAMPLE = 255;
|
||||
private final static int CENTERJSAMPLE = 128;
|
||||
private final static int ONE_HALF = 1 << (SCALEBITS - 1);
|
||||
|
||||
private final static int[] Cr_R_LUT = new int[MAXJSAMPLE + 1];
|
||||
private final static int[] Cb_B_LUT = new int[MAXJSAMPLE + 1];
|
||||
private final static int[] Cr_G_LUT = new int[MAXJSAMPLE + 1];
|
||||
private final static int[] Cb_G_LUT = new int[MAXJSAMPLE + 1];
|
||||
|
||||
/**
|
||||
* Initializes tables for YCC->RGB color space conversion.
|
||||
*/
|
||||
private static void buildYCCtoRGBtable() {
|
||||
if (TIFFImageReader.DEBUG) {
|
||||
System.err.println("Building YCC conversion table");
|
||||
}
|
||||
|
||||
for (int i = 0, x = -CENTERJSAMPLE; i <= MAXJSAMPLE; i++, x++) {
|
||||
// i is the actual input pixel value, in the range 0..MAXJSAMPLE
|
||||
// The Cb or Cr value we are thinking of is x = i - CENTERJSAMPLE
|
||||
// Cr=>R value is nearest int to 1.40200 * x
|
||||
Cr_R_LUT[i] = (int) ((1.40200 * (1 << SCALEBITS) + 0.5) * x + ONE_HALF) >> SCALEBITS;
|
||||
// Cb=>B value is nearest int to 1.77200 * x
|
||||
Cb_B_LUT[i] = (int) ((1.77200 * (1 << SCALEBITS) + 0.5) * x + ONE_HALF) >> SCALEBITS;
|
||||
// Cr=>G value is scaled-up -0.71414 * x
|
||||
Cr_G_LUT[i] = -(int) (0.71414 * (1 << SCALEBITS) + 0.5) * x;
|
||||
// Cb=>G value is scaled-up -0.34414 * x
|
||||
// We also add in ONE_HALF so that need not do it in inner loop
|
||||
Cb_G_LUT[i] = -(int) ((0.34414) * (1 << SCALEBITS) + 0.5) * x + ONE_HALF;
|
||||
}
|
||||
}
|
||||
|
||||
static {
|
||||
buildYCCtoRGBtable();
|
||||
}
|
||||
|
||||
static void convertYCbCr2RGB(final Raster raster) {
|
||||
final int height = raster.getHeight();
|
||||
final int width = raster.getWidth();
|
||||
final byte[] data = ((DataBufferByte) raster.getDataBuffer()).getData();
|
||||
|
||||
for (int y = 0; y < height; y++) {
|
||||
for (int x = 0; x < width; x++) {
|
||||
convertYCbCr2RGB(data, data, (x + y * width) * 3);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
static void convertYCbCr2RGB(final byte[] yCbCr, final byte[] rgb, final int offset) {
|
||||
int y = yCbCr[offset ] & 0xff;
|
||||
int cr = yCbCr[offset + 2] & 0xff;
|
||||
int cb = yCbCr[offset + 1] & 0xff;
|
||||
|
||||
rgb[offset ] = clamp(y + Cr_R_LUT[cr]);
|
||||
rgb[offset + 1] = clamp(y + (Cb_G_LUT[cb] + Cr_G_LUT[cr] >> SCALEBITS));
|
||||
rgb[offset + 2] = clamp(y + Cb_B_LUT[cb]);
|
||||
}
|
||||
|
||||
static void convertYCCK2CMYK(final Raster raster) {
|
||||
final int height = raster.getHeight();
|
||||
final int width = raster.getWidth();
|
||||
final byte[] data = ((DataBufferByte) raster.getDataBuffer()).getData();
|
||||
|
||||
for (int y = 0; y < height; y++) {
|
||||
for (int x = 0; x < width; x++) {
|
||||
convertYCCK2CMYK(data, data, (x + y * width) * 4);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private static void convertYCCK2CMYK(byte[] ycck, byte[] cmyk, int offset) {
|
||||
// Inverted
|
||||
int y = 255 - ycck[offset ] & 0xff;
|
||||
int cb = 255 - ycck[offset + 1] & 0xff;
|
||||
int cr = 255 - ycck[offset + 2] & 0xff;
|
||||
int k = 255 - ycck[offset + 3] & 0xff;
|
||||
|
||||
int cmykC = MAXJSAMPLE - (y + Cr_R_LUT[cr]);
|
||||
int cmykM = MAXJSAMPLE - (y + (Cb_G_LUT[cb] + Cr_G_LUT[cr] >> SCALEBITS));
|
||||
int cmykY = MAXJSAMPLE - (y + Cb_B_LUT[cb]);
|
||||
|
||||
cmyk[offset ] = clamp(cmykC);
|
||||
cmyk[offset + 1] = clamp(cmykM);
|
||||
cmyk[offset + 2] = clamp(cmykY);
|
||||
cmyk[offset + 3] = (byte) k; // K passes through unchanged
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -96,6 +96,8 @@ public class TIFFImageReaderTest extends ImageReaderAbstractTest<TIFFImageReader
|
||||
new TestData(getClassLoaderResource("/tiff/ccitt/group3_2d_lsb2msb.tif"), new Dimension(6, 4)), // B/W, CCITT T4 2D, LSB
|
||||
new TestData(getClassLoaderResource("/tiff/ccitt/group4.tif"), new Dimension(6, 4)), // B/W, CCITT T6 1D
|
||||
new TestData(getClassLoaderResource("/tiff/fivepages-scan-causingerrors.tif"), new Dimension(2480, 3518)), // B/W, CCITT T4
|
||||
// CIELab
|
||||
new TestData(getClassLoaderResource("/tiff/ColorCheckerCalculator.tif"), new Dimension(798, 546)), // CIELab 8 bit/sample
|
||||
// Gray
|
||||
new TestData(getClassLoaderResource("/tiff/depth/flower-minisblack-02.tif"), new Dimension(73, 43)), // Gray 2 bit/sample
|
||||
new TestData(getClassLoaderResource("/tiff/depth/flower-minisblack-04.tif"), new Dimension(73, 43)), // Gray 4 bit/sample
|
||||
@ -120,8 +122,8 @@ public class TIFFImageReaderTest extends ImageReaderAbstractTest<TIFFImageReader
|
||||
new TestData(getClassLoaderResource("/tiff/depth/flower-separated-contig-16.tif"), new Dimension(73, 43)), // CMYK 16 bit/sample
|
||||
// Separated (CMYK) Planar (PlanarConfiguration: 2)
|
||||
new TestData(getClassLoaderResource("/tiff/depth/flower-separated-planar-08.tif"), new Dimension(73, 43)), // CMYK 8 bit/sample
|
||||
new TestData(getClassLoaderResource("/tiff/depth/flower-separated-planar-16.tif"), new Dimension(73, 43)) // CMYK 16 bit/sample
|
||||
);
|
||||
new TestData(getClassLoaderResource("/tiff/depth/flower-separated-planar-16.tif"), new Dimension(73, 43)) // CMYK 16 bit/sample
|
||||
);
|
||||
}
|
||||
|
||||
@Override
|
||||
|
@ -38,7 +38,8 @@ import java.io.InputStream;
|
||||
import java.nio.ByteBuffer;
|
||||
import java.nio.ByteOrder;
|
||||
|
||||
import static org.junit.Assert.*;
|
||||
import static org.junit.Assert.assertArrayEquals;
|
||||
import static org.junit.Assert.assertEquals;
|
||||
|
||||
/**
|
||||
* YCbCr16UpsamplerStreamTest
|
||||
@ -50,22 +51,22 @@ import static org.junit.Assert.*;
|
||||
public class YCbCr16UpsamplerStreamTest {
|
||||
@Test(expected = IllegalArgumentException.class)
|
||||
public void testCreateNullStream() {
|
||||
new YCbCr16UpsamplerStream(null, new int[2], 7, 5, null, ByteOrder.LITTLE_ENDIAN);
|
||||
new YCbCr16UpsamplerStream(null, new int[2], 7, 5, ByteOrder.LITTLE_ENDIAN);
|
||||
}
|
||||
|
||||
@Test(expected = IllegalArgumentException.class)
|
||||
public void testCreateNullChroma() {
|
||||
new YCbCr16UpsamplerStream(new ByteArrayInputStream(new byte[0]), new int[3], 7, 5, null, ByteOrder.LITTLE_ENDIAN);
|
||||
new YCbCr16UpsamplerStream(new ByteArrayInputStream(new byte[0]), new int[3], 7, 5, ByteOrder.LITTLE_ENDIAN);
|
||||
}
|
||||
|
||||
@Test(expected = IllegalArgumentException.class)
|
||||
public void testCreateShortChroma() {
|
||||
new YCbCr16UpsamplerStream(new ByteArrayInputStream(new byte[0]), new int[1], 7, 5, null, ByteOrder.LITTLE_ENDIAN);
|
||||
new YCbCr16UpsamplerStream(new ByteArrayInputStream(new byte[0]), new int[1], 7, 5, ByteOrder.LITTLE_ENDIAN);
|
||||
}
|
||||
|
||||
@Test(expected = IllegalArgumentException.class)
|
||||
public void testCreateNoByteOrder() {
|
||||
new YCbCr16UpsamplerStream(new ByteArrayInputStream(new byte[0]), new int[] {2, 2}, 7, 5, null, null);
|
||||
new YCbCr16UpsamplerStream(new ByteArrayInputStream(new byte[0]), new int[] {2, 2}, 7, 5, null);
|
||||
}
|
||||
|
||||
// TODO: The expected values seems bogus...
|
||||
@ -81,11 +82,11 @@ public class YCbCr16UpsamplerStreamTest {
|
||||
byte[] bytes = new byte[shorts.length * 2];
|
||||
ByteBuffer.wrap(bytes).order(ByteOrder.LITTLE_ENDIAN).asShortBuffer().put(shorts);
|
||||
|
||||
InputStream stream = new YCbCr16UpsamplerStream(new ByteArrayInputStream(bytes), new int[] {2, 2}, TIFFExtension.YCBCR_POSITIONING_CENTERED, 8, null, ByteOrder.LITTLE_ENDIAN);
|
||||
InputStream stream = new YCbCr16UpsamplerStream(new ByteArrayInputStream(bytes), new int[] {2, 2}, TIFFExtension.YCBCR_POSITIONING_CENTERED, 8, ByteOrder.LITTLE_ENDIAN);
|
||||
|
||||
short[] expected = new short[] {
|
||||
0, -30864, 0, 0, -30863, 0, 0, -30966, 0, 0, -30965, 0, 0, -30870, 0, 0, -30869, 0, 0, -30815, 0, 0, -30761, 0,
|
||||
0, -30862, 0, 0, -30861, 0, 0, -30931, 0, 0, -30877, 0, 0, -30868, 0, 0, -30867, 0, 0, -30858, 0, 0, -30858, 0
|
||||
1, 5, 6, 2, 5, 6, 7, 108, 109, 8, 108, 109, 110, 114, 115, 111, 114, 115, 43, 0, 0, 97, 0, 0,
|
||||
3, 5, 6, 4, 5, 6, 42, 108, 109, 96, 108, 109, 112, 114, 115, 113, 114, 115, 0, 0, 0, 0, 0, 0
|
||||
};
|
||||
short[] upsampled = new short[expected.length];
|
||||
|
||||
@ -107,10 +108,10 @@ public class YCbCr16UpsamplerStreamTest {
|
||||
|
||||
byte[] bytes = new byte[shorts.length * 2];
|
||||
ByteBuffer.wrap(bytes).order(ByteOrder.BIG_ENDIAN).asShortBuffer().put(shorts);
|
||||
InputStream stream = new YCbCr16UpsamplerStream(new ByteArrayInputStream(bytes), new int[] {2, 1}, TIFFExtension.YCBCR_POSITIONING_CENTERED, 4, null, ByteOrder.BIG_ENDIAN);
|
||||
InputStream stream = new YCbCr16UpsamplerStream(new ByteArrayInputStream(bytes), new int[] {2, 1}, TIFFExtension.YCBCR_POSITIONING_CENTERED, 4, ByteOrder.BIG_ENDIAN);
|
||||
|
||||
short[] expected = new short[] {
|
||||
0, -30861, 0, 0, -30860, 0, 0, -30923, 0, 0, -30869, 0, 0, -30816, 0, 0, -30815, 0, 0, -30868, 0, 0, -30922, 0
|
||||
1, 3, 4, 2, 3, 4, 42, 77, 112, 96, 77, 112, 113, 115, 43, 114, 115, 43, 97, 77, 112, 43, 77, 112
|
||||
};
|
||||
short[] upsampled = new short[expected.length];
|
||||
|
||||
@ -132,10 +133,10 @@ public class YCbCr16UpsamplerStreamTest {
|
||||
|
||||
byte[] bytes = new byte[shorts.length * 2];
|
||||
ByteBuffer.wrap(bytes).order(ByteOrder.BIG_ENDIAN).asShortBuffer().put(shorts);
|
||||
InputStream stream = new YCbCr16UpsamplerStream(new ByteArrayInputStream(bytes), new int[] {1, 2}, TIFFExtension.YCBCR_POSITIONING_CENTERED, 4, null, ByteOrder.BIG_ENDIAN);
|
||||
InputStream stream = new YCbCr16UpsamplerStream(new ByteArrayInputStream(bytes), new int[] {1, 2}, TIFFExtension.YCBCR_POSITIONING_CENTERED, 4, ByteOrder.BIG_ENDIAN);
|
||||
|
||||
short[] expected = new short[] {
|
||||
0, -30861, 0, 0, -30923, 0, 0, -30816, 0, 0, -30761, 0, 0, -30860, 0, 0, -30869, 0, 0, -30815, 0, 0, -30815, 0
|
||||
1, 3, 4, 42, 77, 112, 113, 115, 43, 97, 0, 0, 2, 3, 4, 96, 77, 112, 114, 115, 43, 43, 0, 0
|
||||
};
|
||||
short[] upsampled = new short[expected.length];
|
||||
|
||||
|
@ -35,7 +35,8 @@ import java.io.DataInputStream;
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
|
||||
import static org.junit.Assert.*;
|
||||
import static org.junit.Assert.assertArrayEquals;
|
||||
import static org.junit.Assert.assertEquals;
|
||||
|
||||
/**
|
||||
* YCbCrUpsamplerStreamTest
|
||||
@ -47,17 +48,17 @@ import static org.junit.Assert.*;
|
||||
public class YCbCrUpsamplerStreamTest {
|
||||
@Test(expected = IllegalArgumentException.class)
|
||||
public void testCreateNullStream() {
|
||||
new YCbCrUpsamplerStream(null, new int[2], 7, 5, null);
|
||||
new YCbCrUpsamplerStream(null, new int[2], 7, 5);
|
||||
}
|
||||
|
||||
@Test(expected = IllegalArgumentException.class)
|
||||
public void testCreateNullChroma() {
|
||||
new YCbCrUpsamplerStream(new ByteArrayInputStream(new byte[0]), new int[3], 7, 5, null);
|
||||
new YCbCrUpsamplerStream(new ByteArrayInputStream(new byte[0]), new int[3], 7, 5);
|
||||
}
|
||||
|
||||
@Test(expected = IllegalArgumentException.class)
|
||||
public void testCreateShortChroma() {
|
||||
new YCbCrUpsamplerStream(new ByteArrayInputStream(new byte[0]), new int[1], 7, 5, null);
|
||||
new YCbCrUpsamplerStream(new ByteArrayInputStream(new byte[0]), new int[1], 7, 5);
|
||||
}
|
||||
|
||||
@Test
|
||||
@ -66,16 +67,17 @@ public class YCbCrUpsamplerStreamTest {
|
||||
1, 2, 3, 4, 5, 6, 7, 8, 42, 96,
|
||||
108, 109, 110, 111, 112, 113, 114, 115, 43, 97
|
||||
};
|
||||
InputStream stream = new YCbCrUpsamplerStream(new ByteArrayInputStream(bytes), new int[] {2, 2}, TIFFExtension.YCBCR_POSITIONING_CENTERED, 8, null);
|
||||
InputStream stream = new YCbCrUpsamplerStream(new ByteArrayInputStream(bytes), new int[] {2, 2}, TIFFExtension.YCBCR_POSITIONING_CENTERED, 8);
|
||||
|
||||
byte[] expected = new byte[] {
|
||||
0, -126, 0, 0, -125, 0, 0, 27, 0, 0, 28, 0, 92, 124, 85, 93, 125, 86, 0, -78, 0, 0, -24, 0,
|
||||
0, -124, 0, 0, -123, 0, 15, 62, 7, 69, 116, 61, 94, 126, 87, 95, 127, 88, 0, -121, 0, 0, -121, 0
|
||||
1, 5, 6, 2, 5, 6, 7, 108, 109, 8, 108, 109, 110, 114, 115, 111, 114, 115, 43, 0, 0, 97, 0, 0,
|
||||
3, 5, 6, 4, 5, 6, 42, 108, 109, 96, 108, 109, 112, 114, 115, 113, 114, 115, 0, 0, 0, 0, 0, 0
|
||||
};
|
||||
byte[] upsampled = new byte[expected.length];
|
||||
|
||||
new DataInputStream(stream).readFully(upsampled);
|
||||
|
||||
|
||||
assertArrayEquals(expected, upsampled);
|
||||
assertEquals(-1, stream.read());
|
||||
}
|
||||
@ -86,10 +88,10 @@ public class YCbCrUpsamplerStreamTest {
|
||||
1, 2, 3, 4, 42, 96, 77,
|
||||
112, 113, 114, 115, 43, 97, 43
|
||||
};
|
||||
InputStream stream = new YCbCrUpsamplerStream(new ByteArrayInputStream(bytes), new int[] {2, 1}, TIFFExtension.YCBCR_POSITIONING_CENTERED, 4, null);
|
||||
InputStream stream = new YCbCrUpsamplerStream(new ByteArrayInputStream(bytes), new int[] {2, 1}, TIFFExtension.YCBCR_POSITIONING_CENTERED, 4);
|
||||
|
||||
byte[] expected = new byte[] {
|
||||
0, -123, 0, 0, -122, 0, 20, 71, 0, 74, 125, 6, 0, -78, 90, 0, -77, 91, 75, 126, 7, 21, 72, 0
|
||||
1, 3, 4, 2, 3, 4, 42, 77, 112, 96, 77, 112, 113, 115, 43, 114, 115, 43, 97, 77, 112, 43, 77, 112
|
||||
};
|
||||
byte[] upsampled = new byte[expected.length];
|
||||
|
||||
@ -105,10 +107,10 @@ public class YCbCrUpsamplerStreamTest {
|
||||
1, 2, 3, 4, 42, 96, 77,
|
||||
112, 113, 114, 115, 43, 97, 43
|
||||
};
|
||||
InputStream stream = new YCbCrUpsamplerStream(new ByteArrayInputStream(bytes), new int[] {1, 2}, TIFFExtension.YCBCR_POSITIONING_CENTERED, 4, null);
|
||||
InputStream stream = new YCbCrUpsamplerStream(new ByteArrayInputStream(bytes), new int[] {1, 2}, TIFFExtension.YCBCR_POSITIONING_CENTERED, 4);
|
||||
|
||||
byte[] expected = new byte[] {
|
||||
0, -123, 0, 20, 71, 0, 0, -78, 90, 0, -24, 0, 0, -122, 0, 74, 125, 6, 0, -77, 91, 0, -78, 0
|
||||
1, 3, 4, 42, 77, 112, 113, 115, 43, 97, 0, 0, 2, 3, 4, 96, 77, 112, 114, 115, 43, 43, 0, 0
|
||||
};
|
||||
byte[] upsampled = new byte[expected.length];
|
||||
|
||||
|
BIN
imageio/imageio-tiff/src/test/resources/tiff/ColorCheckerCalculator.tif
Executable file
BIN
imageio/imageio-tiff/src/test/resources/tiff/ColorCheckerCalculator.tif
Executable file
Binary file not shown.
Loading…
x
Reference in New Issue
Block a user