mirror of
https://github.com/haraldk/TwelveMonkeys.git
synced 2025-08-05 04:25:29 -04:00
#253: Fix for non-subsampled YCbCr encoded JPEG-in-TIFF being decoded as RGB.
This commit is contained in:
parent
788b11e4fa
commit
458ef92af5
@ -47,17 +47,30 @@ public final class YCbCrConverter {
|
|||||||
buildYCCtoRGBtable();
|
buildYCCtoRGBtable();
|
||||||
}
|
}
|
||||||
|
|
||||||
public static void convertYCbCr2RGB(final byte[] yCbCr, final byte[] rgb, final double[] coefficients, final int offset) {
|
public static void convertYCbCr2RGB(final byte[] yCbCr, final byte[] rgb, final double[] coefficients, double[] referenceBW, final int offset) {
|
||||||
double y = (yCbCr[offset] & 0xff);
|
double y;
|
||||||
double cb = (yCbCr[offset + 1] & 0xff) - 128;
|
double cb;
|
||||||
double cr = (yCbCr[offset + 2] & 0xff) - 128;
|
double cr;
|
||||||
|
|
||||||
|
if (referenceBW == null) {
|
||||||
|
// Default case
|
||||||
|
y = (yCbCr[offset] & 0xff);
|
||||||
|
cb = (yCbCr[offset + 1] & 0xff) - 128;
|
||||||
|
cr = (yCbCr[offset + 2] & 0xff) - 128;
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
// Custom values
|
||||||
|
y = ((yCbCr[offset] & 0xff) - referenceBW[0]) * 255.0 / (referenceBW[1] - referenceBW[0]);
|
||||||
|
cb = ((yCbCr[offset + 1] & 0xff) - referenceBW[2]) * 127.0 / (referenceBW[3] - referenceBW[2]);
|
||||||
|
cr = ((yCbCr[offset + 2] & 0xff) - referenceBW[4]) * 127.0 / (referenceBW[5] - referenceBW[4]);
|
||||||
|
}
|
||||||
|
|
||||||
double lumaRed = coefficients[0];
|
double lumaRed = coefficients[0];
|
||||||
double lumaGreen = coefficients[1];
|
double lumaGreen = coefficients[1];
|
||||||
double lumaBlue = coefficients[2];
|
double lumaBlue = coefficients[2];
|
||||||
|
|
||||||
int red = (int) Math.round(cr * (2 - 2 * lumaRed) + y);
|
int red = (int) Math.round(cr * (2.0 - 2.0 * lumaRed) + y);
|
||||||
int blue = (int) Math.round(cb * (2 - 2 * lumaBlue) + y);
|
int blue = (int) Math.round(cb * (2.0 - 2.0 * lumaBlue) + y);
|
||||||
int green = (int) Math.round((y - lumaRed * red - lumaBlue * blue) / lumaGreen);
|
int green = (int) Math.round((y - lumaRed * red - lumaBlue * blue) / lumaGreen);
|
||||||
|
|
||||||
rgb[offset] = clamp(red);
|
rgb[offset] = clamp(red);
|
||||||
|
@ -52,10 +52,13 @@ import com.twelvemonkeys.io.FastByteArrayOutputStream;
|
|||||||
import com.twelvemonkeys.io.LittleEndianDataInputStream;
|
import com.twelvemonkeys.io.LittleEndianDataInputStream;
|
||||||
import com.twelvemonkeys.io.enc.DecoderStream;
|
import com.twelvemonkeys.io.enc.DecoderStream;
|
||||||
import com.twelvemonkeys.io.enc.PackBitsDecoder;
|
import com.twelvemonkeys.io.enc.PackBitsDecoder;
|
||||||
|
import org.w3c.dom.NodeList;
|
||||||
|
|
||||||
import javax.imageio.*;
|
import javax.imageio.*;
|
||||||
import javax.imageio.event.IIOReadWarningListener;
|
import javax.imageio.event.IIOReadWarningListener;
|
||||||
import javax.imageio.metadata.IIOMetadata;
|
import javax.imageio.metadata.IIOMetadata;
|
||||||
|
import javax.imageio.metadata.IIOMetadataFormatImpl;
|
||||||
|
import javax.imageio.metadata.IIOMetadataNode;
|
||||||
import javax.imageio.plugins.jpeg.JPEGImageReadParam;
|
import javax.imageio.plugins.jpeg.JPEGImageReadParam;
|
||||||
import javax.imageio.spi.IIORegistry;
|
import javax.imageio.spi.IIORegistry;
|
||||||
import javax.imageio.spi.ImageReaderSpi;
|
import javax.imageio.spi.ImageReaderSpi;
|
||||||
@ -65,6 +68,7 @@ import java.awt.*;
|
|||||||
import java.awt.color.CMMException;
|
import java.awt.color.CMMException;
|
||||||
import java.awt.color.ColorSpace;
|
import java.awt.color.ColorSpace;
|
||||||
import java.awt.color.ICC_Profile;
|
import java.awt.color.ICC_Profile;
|
||||||
|
import java.awt.geom.Rectangle2D;
|
||||||
import java.awt.image.*;
|
import java.awt.image.*;
|
||||||
import java.io.*;
|
import java.io.*;
|
||||||
import java.lang.reflect.Constructor;
|
import java.lang.reflect.Constructor;
|
||||||
@ -148,6 +152,7 @@ public class TIFFImageReader extends ImageReaderBase {
|
|||||||
|
|
||||||
// NOTE: DO NOT MODIFY OR EXPOSE THIS ARRAY OUTSIDE PACKAGE!
|
// 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};
|
static final double[] CCIR_601_1_COEFFICIENTS = new double[] {299.0 / 1000.0, 587.0 / 1000.0, 114.0 / 1000.0};
|
||||||
|
static final double[] REFERENCE_BLACK_WHITE_YCC_DEFAULT = new double[] {0, 255, 128, 255, 128, 255};
|
||||||
|
|
||||||
private CompoundDirectory IFDs;
|
private CompoundDirectory IFDs;
|
||||||
private Directory currentIFD;
|
private Directory currentIFD;
|
||||||
@ -807,6 +812,7 @@ public class TIFFImageReader extends ImageReaderBase {
|
|||||||
WritableRaster rowRaster = rawType.createBufferedImage(stripTileWidth, 1).getRaster();
|
WritableRaster rowRaster = rawType.createBufferedImage(stripTileWidth, 1).getRaster();
|
||||||
Rectangle clip = new Rectangle(srcRegion);
|
Rectangle clip = new Rectangle(srcRegion);
|
||||||
int row = 0;
|
int row = 0;
|
||||||
|
Boolean needsCSConversion = null;
|
||||||
|
|
||||||
switch (compression) {
|
switch (compression) {
|
||||||
// TIFF Baseline
|
// TIFF Baseline
|
||||||
@ -830,7 +836,7 @@ public class TIFFImageReader extends ImageReaderBase {
|
|||||||
|
|
||||||
int[] yCbCrSubsampling = null;
|
int[] yCbCrSubsampling = null;
|
||||||
int yCbCrPos = 1;
|
int yCbCrPos = 1;
|
||||||
// double[] yCbCrCoefficients = null;
|
|
||||||
if (interpretation == TIFFExtension.PHOTOMETRIC_YCBCR) {
|
if (interpretation == TIFFExtension.PHOTOMETRIC_YCBCR) {
|
||||||
// getRawImageType does the lookup/conversion for these
|
// getRawImageType does the lookup/conversion for these
|
||||||
if (rowRaster.getNumBands() != 3) {
|
if (rowRaster.getNumBands() != 3) {
|
||||||
@ -868,16 +874,6 @@ public class TIFFImageReader extends ImageReaderBase {
|
|||||||
else {
|
else {
|
||||||
yCbCrSubsampling = new int[] {2, 2};
|
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;
|
|
||||||
// }
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Read data
|
// Read data
|
||||||
@ -974,8 +970,8 @@ public class TIFFImageReader extends ImageReaderBase {
|
|||||||
// http://docs.oracle.com/javase/6/docs/api/javax/imageio/metadata/doc-files/jpeg_metadata.html#abbrev
|
// http://docs.oracle.com/javase/6/docs/api/javax/imageio/metadata/doc-files/jpeg_metadata.html#abbrev
|
||||||
jpegReader.getStreamMetadata();
|
jpegReader.getStreamMetadata();
|
||||||
}
|
}
|
||||||
else {
|
else if (tilesDown * tilesAcross > 1) {
|
||||||
processWarningOccurred("Missing JPEGTables for TIFF with compression: 7 (JPEG)");
|
processWarningOccurred("Missing JPEGTables for tiled/striped TIFF with compression: 7 (JPEG)");
|
||||||
// ...and the JPEG reader will probably choke on missing tables...
|
// ...and the JPEG reader will probably choke on missing tables...
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -991,7 +987,8 @@ public class TIFFImageReader extends ImageReaderBase {
|
|||||||
int colsInTile = Math.min(stripTileWidth, width - col);
|
int colsInTile = Math.min(stripTileWidth, width - col);
|
||||||
|
|
||||||
// Read only tiles that lies within region
|
// Read only tiles that lies within region
|
||||||
if (new Rectangle(col, row, colsInTile, rowsInTile).intersects(srcRegion)) {
|
Rectangle tileRect = new Rectangle(col, row, colsInTile, rowsInTile);
|
||||||
|
if (tileRect.intersects(srcRegion)) {
|
||||||
imageInput.seek(stripTileOffsets[i]);
|
imageInput.seek(stripTileOffsets[i]);
|
||||||
|
|
||||||
int length = stripTileByteCounts != null ? (int) stripTileByteCounts[i] : Short.MAX_VALUE;
|
int length = stripTileByteCounts != null ? (int) stripTileByteCounts[i] : Short.MAX_VALUE;
|
||||||
@ -1000,7 +997,13 @@ public class TIFFImageReader extends ImageReaderBase {
|
|||||||
jpegReader.setInput(subStream);
|
jpegReader.setInput(subStream);
|
||||||
jpegParam.setSourceRegion(new Rectangle(0, 0, colsInTile, rowsInTile));
|
jpegParam.setSourceRegion(new Rectangle(0, 0, colsInTile, rowsInTile));
|
||||||
|
|
||||||
if (interpretation == TIFFExtension.PHOTOMETRIC_YCBCR || interpretation == TIFFBaseline.PHOTOMETRIC_RGB) {
|
// TODO: If we have non-standard reference B/W or yCbCr coefficients,
|
||||||
|
// we might still have to do extra color space conversion...
|
||||||
|
if (needsCSConversion == null) {
|
||||||
|
needsCSConversion = needsCSConversion(interpretation, jpegReader.getImageMetadata(0));
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!needsCSConversion) {
|
||||||
jpegParam.setDestinationOffset(new Point(col - srcRegion.x, row - srcRegion.y));
|
jpegParam.setDestinationOffset(new Point(col - srcRegion.x, row - srcRegion.y));
|
||||||
jpegParam.setDestination(destination);
|
jpegParam.setDestination(destination);
|
||||||
jpegReader.read(0, jpegParam);
|
jpegReader.read(0, jpegParam);
|
||||||
@ -1013,7 +1016,6 @@ public class TIFFImageReader extends ImageReaderBase {
|
|||||||
destination.getRaster().setDataElements(col - srcRegion.x, row - srcRegion.y, raster);
|
destination.getRaster().setDataElements(col - srcRegion.x, row - srcRegion.y, raster);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (abortRequested()) {
|
if (abortRequested()) {
|
||||||
@ -1127,13 +1129,18 @@ public class TIFFImageReader extends ImageReaderBase {
|
|||||||
|
|
||||||
// Read data
|
// Read data
|
||||||
processImageStarted(imageIndex); // Better yet, would be to delegate read progress here...
|
processImageStarted(imageIndex); // Better yet, would be to delegate read progress here...
|
||||||
|
|
||||||
imageInput.seek(realJPEGOffset);
|
imageInput.seek(realJPEGOffset);
|
||||||
|
|
||||||
try (ImageInputStream stream = new SubImageInputStream(imageInput, length)) {
|
try (ImageInputStream stream = new SubImageInputStream(imageInput, length)) {
|
||||||
jpegReader.setInput(stream);
|
jpegReader.setInput(stream);
|
||||||
jpegParam.setSourceRegion(srcRegion);
|
jpegParam.setSourceRegion(srcRegion);
|
||||||
|
|
||||||
if (interpretation == TIFFExtension.PHOTOMETRIC_YCBCR || interpretation == TIFFBaseline.PHOTOMETRIC_RGB) {
|
if (needsCSConversion == null) {
|
||||||
|
needsCSConversion = needsCSConversion(interpretation, jpegReader.getImageMetadata(0));
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!needsCSConversion) {
|
||||||
jpegParam.setDestination(destination);
|
jpegParam.setDestination(destination);
|
||||||
jpegReader.read(0, jpegParam);
|
jpegReader.read(0, jpegParam);
|
||||||
}
|
}
|
||||||
@ -1141,6 +1148,7 @@ public class TIFFImageReader extends ImageReaderBase {
|
|||||||
// Otherwise, it's likely CMYK or some other interpretation we don't need to convert.
|
// 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
|
// We'll have to use readAsRaster and later apply color space conversion ourselves
|
||||||
Raster raster = jpegReader.readRaster(0, jpegParam);
|
Raster raster = jpegReader.readRaster(0, jpegParam);
|
||||||
|
normalizeColor(interpretation, ((DataBufferByte) raster.getDataBuffer()).getData());
|
||||||
destination.getRaster().setDataElements(0, 0, raster);
|
destination.getRaster().setDataElements(0, 0, raster);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -1232,7 +1240,11 @@ public class TIFFImageReader extends ImageReaderBase {
|
|||||||
jpegParam.setDestinationOffset(new Point(col - srcRegion.x, row - srcRegion.y));
|
jpegParam.setDestinationOffset(new Point(col - srcRegion.x, row - srcRegion.y));
|
||||||
jpegParam.setDestination(destination);
|
jpegParam.setDestination(destination);
|
||||||
|
|
||||||
if (interpretation == TIFFExtension.PHOTOMETRIC_YCBCR || interpretation == TIFFBaseline.PHOTOMETRIC_RGB) {
|
if (needsCSConversion == null) {
|
||||||
|
needsCSConversion = needsCSConversion(interpretation, jpegReader.getImageMetadata(0));
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!needsCSConversion) {
|
||||||
jpegParam.setDestination(destination);
|
jpegParam.setDestination(destination);
|
||||||
jpegReader.read(0, jpegParam);
|
jpegReader.read(0, jpegParam);
|
||||||
}
|
}
|
||||||
@ -1240,6 +1252,7 @@ public class TIFFImageReader extends ImageReaderBase {
|
|||||||
// Otherwise, it's likely CMYK or some other interpretation we don't need to convert.
|
// 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
|
// We'll have to use readAsRaster and later apply color space conversion ourselves
|
||||||
Raster raster = jpegReader.readRaster(0, jpegParam);
|
Raster raster = jpegReader.readRaster(0, jpegParam);
|
||||||
|
normalizeColor(interpretation, ((DataBufferByte) raster.getDataBuffer()).getData());
|
||||||
destination.getRaster().setDataElements(0, 0, raster);
|
destination.getRaster().setDataElements(0, 0, raster);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -1295,6 +1308,40 @@ public class TIFFImageReader extends ImageReaderBase {
|
|||||||
return destination;
|
return destination;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private boolean needsCSConversion(final int photometricInterpretation, final IIOMetadata imageMetadata) throws IOException {
|
||||||
|
if (imageMetadata == null) {
|
||||||
|
// Assume we're ok
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
IIOMetadataNode stdTree = (IIOMetadataNode) imageMetadata.getAsTree(IIOMetadataFormatImpl.standardMetadataFormatName);
|
||||||
|
|
||||||
|
NodeList csTypes = stdTree.getElementsByTagName("ColorSpaceType");
|
||||||
|
|
||||||
|
if (csTypes != null && csTypes.getLength() > 0) {
|
||||||
|
IIOMetadataNode csType = (IIOMetadataNode) csTypes.item(0);
|
||||||
|
String csName = csType.getAttribute("name");
|
||||||
|
|
||||||
|
if ("YCbCr".equals(csName) && photometricInterpretation == TIFFExtension.PHOTOMETRIC_YCBCR
|
||||||
|
|| "RGB".equals(csName) && photometricInterpretation == TIFFBaseline.PHOTOMETRIC_RGB
|
||||||
|
|| "GRAY".equals(csName) && photometricInterpretation == TIFFBaseline.PHOTOMETRIC_BLACK_IS_ZERO) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
// CMYK, or may happen because the JPEG stream is not subsampled,
|
||||||
|
// fooling the JPEGImageReader to believe the data is RGB, while it is YCbCr
|
||||||
|
if (DEBUG) {
|
||||||
|
System.out.println("Incompatible JPEG CS/PhotometricInterpretation: " + csName + "/" + photometricInterpretation);
|
||||||
|
}
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// We don't really know, assume it's ok...
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
private ImageReader createJPEGDelegate() throws IIOException {
|
private ImageReader createJPEGDelegate() throws IIOException {
|
||||||
// TIFF is strictly ISO JPEG, so we should probably stick to the standard reader
|
// TIFF is strictly ISO JPEG, so we should probably stick to the standard reader
|
||||||
try {
|
try {
|
||||||
@ -1624,7 +1671,7 @@ public class TIFFImageReader extends ImageReaderBase {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private void normalizeColor(int photometricInterpretation, byte[] data) {
|
private void normalizeColor(int photometricInterpretation, byte[] data) throws IIOException {
|
||||||
switch (photometricInterpretation) {
|
switch (photometricInterpretation) {
|
||||||
case TIFFBaseline.PHOTOMETRIC_WHITE_IS_ZERO:
|
case TIFFBaseline.PHOTOMETRIC_WHITE_IS_ZERO:
|
||||||
// Inverse values
|
// Inverse values
|
||||||
@ -1637,7 +1684,7 @@ public class TIFFImageReader extends ImageReaderBase {
|
|||||||
case TIFFExtension.PHOTOMETRIC_CIELAB:
|
case TIFFExtension.PHOTOMETRIC_CIELAB:
|
||||||
case TIFFExtension.PHOTOMETRIC_ICCLAB:
|
case TIFFExtension.PHOTOMETRIC_ICCLAB:
|
||||||
case TIFFExtension.PHOTOMETRIC_ITULAB:
|
case TIFFExtension.PHOTOMETRIC_ITULAB:
|
||||||
// TODO: Whitepoint may be encoded in separate tag
|
// TODO: White point may be encoded in separate tag
|
||||||
CIELabColorConverter converter = new CIELabColorConverter(
|
CIELabColorConverter converter = new CIELabColorConverter(
|
||||||
photometricInterpretation == TIFFExtension.PHOTOMETRIC_CIELAB
|
photometricInterpretation == TIFFExtension.PHOTOMETRIC_CIELAB
|
||||||
? Illuminant.D65
|
? Illuminant.D65
|
||||||
@ -1673,19 +1720,31 @@ public class TIFFImageReader extends ImageReaderBase {
|
|||||||
break;
|
break;
|
||||||
|
|
||||||
case TIFFExtension.PHOTOMETRIC_YCBCR:
|
case TIFFExtension.PHOTOMETRIC_YCBCR:
|
||||||
Entry coefficients = currentIFD.getEntryById(TIFF.TAG_YCBCR_COEFFICIENTS);
|
// Default: CCIR Recommendation 601-1: 299/1000, 587/1000 and 114/1000
|
||||||
|
double[] coefficients = getValueAsDoubleArray(TIFF.TAG_YCBCR_COEFFICIENTS, "YCbCrCoefficients", false, 3);
|
||||||
|
|
||||||
if (coefficients == null) {
|
// "Default" [0, 255, 128, 255, 128, 255] for YCbCr (real default is [0, 255, 0, 255, 0, 255] for RGB)
|
||||||
|
double[] referenceBW = getValueAsDoubleArray(TIFF.TAG_REFERENCE_BLACK_WHITE, "ReferenceBlackWhite", false, 6);
|
||||||
|
|
||||||
|
if ((coefficients == null || Arrays.equals(coefficients, CCIR_601_1_COEFFICIENTS))
|
||||||
|
&& (referenceBW == null || Arrays.equals(referenceBW, REFERENCE_BLACK_WHITE_YCC_DEFAULT))) {
|
||||||
|
// Fast, default conversion
|
||||||
for (int i = 0; i < data.length; i += 3) {
|
for (int i = 0; i < data.length; i += 3) {
|
||||||
YCbCrConverter.convertYCbCr2RGB(data, data, i);
|
YCbCrConverter.convertYCbCr2RGB(data, data, i);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
else {
|
else {
|
||||||
Rational[] value = (Rational[]) coefficients.getValue();
|
// If one of the values are null, we'll need the other here...
|
||||||
double[] yCbCrCoefficients = new double[] {value[0].doubleValue(), value[1].doubleValue(), value[2].doubleValue()};
|
if (coefficients == null) {
|
||||||
|
coefficients = CCIR_601_1_COEFFICIENTS;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (referenceBW != null && Arrays.equals(referenceBW, REFERENCE_BLACK_WHITE_YCC_DEFAULT)) {
|
||||||
|
referenceBW = null;
|
||||||
|
}
|
||||||
|
|
||||||
for (int i = 0; i < data.length; i += 3) {
|
for (int i = 0; i < data.length; i += 3) {
|
||||||
YCbCrConverter.convertYCbCr2RGB(data, data, yCbCrCoefficients, i);
|
YCbCrConverter.convertYCbCr2RGB(data, data, coefficients, referenceBW, i);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -1693,7 +1752,7 @@ public class TIFFImageReader extends ImageReaderBase {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private void normalizeColor(int photometricInterpretation, short[] data) {
|
private void normalizeColor(int photometricInterpretation, short[] data) throws IIOException {
|
||||||
switch (photometricInterpretation) {
|
switch (photometricInterpretation) {
|
||||||
case TIFFBaseline.PHOTOMETRIC_WHITE_IS_ZERO:
|
case TIFFBaseline.PHOTOMETRIC_WHITE_IS_ZERO:
|
||||||
// Inverse values
|
// Inverse values
|
||||||
@ -1706,7 +1765,7 @@ public class TIFFImageReader extends ImageReaderBase {
|
|||||||
case TIFFExtension.PHOTOMETRIC_CIELAB:
|
case TIFFExtension.PHOTOMETRIC_CIELAB:
|
||||||
case TIFFExtension.PHOTOMETRIC_ICCLAB:
|
case TIFFExtension.PHOTOMETRIC_ICCLAB:
|
||||||
case TIFFExtension.PHOTOMETRIC_ITULAB:
|
case TIFFExtension.PHOTOMETRIC_ITULAB:
|
||||||
// TODO: Whitepoint may be encoded in separate tag
|
// TODO: White point may be encoded in separate tag
|
||||||
CIELabColorConverter converter = new CIELabColorConverter(
|
CIELabColorConverter converter = new CIELabColorConverter(
|
||||||
photometricInterpretation == TIFFExtension.PHOTOMETRIC_ITULAB
|
photometricInterpretation == TIFFExtension.PHOTOMETRIC_ITULAB
|
||||||
? Illuminant.D65
|
? Illuminant.D65
|
||||||
@ -1744,23 +1803,26 @@ public class TIFFImageReader extends ImageReaderBase {
|
|||||||
break;
|
break;
|
||||||
|
|
||||||
case TIFFExtension.PHOTOMETRIC_YCBCR:
|
case TIFFExtension.PHOTOMETRIC_YCBCR:
|
||||||
double[] coefficients;
|
// Default: CCIR Recommendation 601-1: 299/1000, 587/1000 and 114/1000
|
||||||
|
double[] coefficients = getValueAsDoubleArray(TIFF.TAG_YCBCR_COEFFICIENTS, "YCbCrCoefficients", false, 3);
|
||||||
|
|
||||||
Entry coefficientsTag = currentIFD.getEntryById(TIFF.TAG_YCBCR_COEFFICIENTS);
|
// "Default" [0, 255, 128, 255, 128, 255] for YCbCr (real default is [0, 255, 0, 255, 0, 255] for RGB)
|
||||||
if (coefficientsTag != null) {
|
double[] referenceBW = getValueAsDoubleArray(TIFF.TAG_REFERENCE_BLACK_WHITE, "ReferenceBlackWhite", false, 6);
|
||||||
Rational[] value = (Rational[]) coefficientsTag.getValue();
|
|
||||||
coefficients = new double[] {value[0].doubleValue(), value[1].doubleValue(), value[2].doubleValue()};
|
// If one of the values are null, we'll need the other here...
|
||||||
}
|
if (coefficients == null) {
|
||||||
else {
|
|
||||||
coefficients = CCIR_601_1_COEFFICIENTS;
|
coefficients = CCIR_601_1_COEFFICIENTS;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (referenceBW != null && Arrays.equals(referenceBW, REFERENCE_BLACK_WHITE_YCC_DEFAULT)) {
|
||||||
|
referenceBW = null;
|
||||||
|
}
|
||||||
|
|
||||||
for (int i = 0; i < data.length; i += 3) {
|
for (int i = 0; i < data.length; i += 3) {
|
||||||
convertYCbCr2RGB(data, data, coefficients, i);
|
convertYCbCr2RGB(data, data, coefficients, referenceBW, i);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private void normalizeColor(int photometricInterpretation, int[] data) {
|
private void normalizeColor(int photometricInterpretation, int[] data) {
|
||||||
switch (photometricInterpretation) {
|
switch (photometricInterpretation) {
|
||||||
case TIFFBaseline.PHOTOMETRIC_WHITE_IS_ZERO:
|
case TIFFBaseline.PHOTOMETRIC_WHITE_IS_ZERO:
|
||||||
@ -1792,21 +1854,30 @@ public class TIFFImageReader extends ImageReaderBase {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private void convertYCbCr2RGB(final short[] yCbCr, final short[] rgb, final double[] coefficients, final int offset) {
|
private void convertYCbCr2RGB(final short[] yCbCr, final short[] rgb, final double[] coefficients, final double[] referenceBW, final int offset) {
|
||||||
int y;
|
double y;
|
||||||
int cb;
|
double cb;
|
||||||
int cr;
|
double cr;
|
||||||
|
|
||||||
y = (yCbCr[offset + 0] & 0xffff);
|
if (referenceBW == null) {
|
||||||
cb = (yCbCr[offset + 1] & 0xffff) - 32768;
|
// Default case
|
||||||
cr = (yCbCr[offset + 2] & 0xffff) - 32768;
|
y = (yCbCr[offset] & 0xffff);
|
||||||
|
cb = (yCbCr[offset + 1] & 0xffff) - 32768;
|
||||||
|
cr = (yCbCr[offset + 2] & 0xffff) - 32768;
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
// Custom values
|
||||||
|
y = ((yCbCr[offset] & 0xffff) - referenceBW[0]) * (65535.0) / (referenceBW[1] - referenceBW[0]);
|
||||||
|
cb = ((yCbCr[offset + 1] & 0xffff) - referenceBW[2]) * 32767.0 / (referenceBW[3] - referenceBW[2]);
|
||||||
|
cr = ((yCbCr[offset + 2] & 0xffff) - referenceBW[4]) * 32767.0 / (referenceBW[5] - referenceBW[4]);
|
||||||
|
}
|
||||||
|
|
||||||
double lumaRed = coefficients[0];
|
double lumaRed = coefficients[0];
|
||||||
double lumaGreen = coefficients[1];
|
double lumaGreen = coefficients[1];
|
||||||
double lumaBlue = coefficients[2];
|
double lumaBlue = coefficients[2];
|
||||||
|
|
||||||
int red = (int) Math.round(cr * (2 - 2 * lumaRed) + y);
|
int red = (int) Math.round(cr * (2.0 - 2.0 * lumaRed) + y);
|
||||||
int blue = (int) Math.round(cb * (2 - 2 * lumaBlue) + y);
|
int blue = (int) Math.round(cb * (2.0 - 2.0 * lumaBlue) + y);
|
||||||
int green = (int) Math.round((y - lumaRed * (red) - lumaBlue * (blue)) / lumaGreen);
|
int green = (int) Math.round((y - lumaRed * (red) - lumaBlue * (blue)) / lumaGreen);
|
||||||
|
|
||||||
short r = clampShort(red);
|
short r = clampShort(red);
|
||||||
@ -1902,6 +1973,57 @@ public class TIFFImageReader extends ImageReaderBase {
|
|||||||
return value;
|
return value;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private double[] getValueAsDoubleArray(final int tag, final String tagName, final boolean required, final int expectedLength) throws IIOException {
|
||||||
|
Entry entry = currentIFD.getEntryById(tag);
|
||||||
|
|
||||||
|
if (entry == null) {
|
||||||
|
if (required) {
|
||||||
|
throw new IIOException("Missing TIFF tag " + tagName);
|
||||||
|
}
|
||||||
|
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (expectedLength > 0 && entry.valueCount() != expectedLength) {
|
||||||
|
if (required) {
|
||||||
|
throw new IIOException(String.format("Unexpected value count for %s: %d (expected %d values)", tagName, entry.valueCount(), expectedLength));
|
||||||
|
}
|
||||||
|
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
double[] value;
|
||||||
|
|
||||||
|
if (entry.valueCount() == 1) {
|
||||||
|
// For single entries, this will be a boxed type
|
||||||
|
value = new double[] {((Number) entry.getValue()).doubleValue()};
|
||||||
|
}
|
||||||
|
else if (entry.getValue() instanceof float[]) {
|
||||||
|
float[] floats = (float[]) entry.getValue();
|
||||||
|
value = new double[floats.length];
|
||||||
|
|
||||||
|
for (int i = 0, length = value.length; i < length; i++) {
|
||||||
|
value[i] = floats[i];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else if (entry.getValue() instanceof double[]) {
|
||||||
|
value = (double[]) entry.getValue();
|
||||||
|
}
|
||||||
|
else if (entry.getValue() instanceof Rational[]) {
|
||||||
|
Rational[] rationals = (Rational[]) entry.getValue();
|
||||||
|
value = new double[rationals.length];
|
||||||
|
|
||||||
|
for (int i = 0, length = value.length; i < length; i++) {
|
||||||
|
value[i] = rationals[i].doubleValue();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
throw new IIOException(String.format("Unsupported %s type: %s (%s)", tagName, entry.getTypeName(), entry.getValue().getClass()));
|
||||||
|
}
|
||||||
|
|
||||||
|
return value;
|
||||||
|
}
|
||||||
|
|
||||||
private ICC_Profile getICCProfile() throws IOException {
|
private ICC_Profile getICCProfile() throws IOException {
|
||||||
Entry entry = currentIFD.getEntryById(TIFF.TAG_ICC_PROFILE);
|
Entry entry = currentIFD.getEntryById(TIFF.TAG_ICC_PROFILE);
|
||||||
|
|
||||||
|
@ -27,6 +27,7 @@ package com.twelvemonkeys.imageio.plugins.tiff;/*
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
import com.twelvemonkeys.imageio.util.ImageReaderAbstractTest;
|
import com.twelvemonkeys.imageio.util.ImageReaderAbstractTest;
|
||||||
|
import org.junit.Ignore;
|
||||||
import org.junit.Test;
|
import org.junit.Test;
|
||||||
|
|
||||||
import javax.imageio.ImageIO;
|
import javax.imageio.ImageIO;
|
||||||
@ -237,6 +238,59 @@ public class TIFFImageReaderTest extends ImageReaderAbstractTest<TIFFImageReader
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void testReadYCbCrJPEGAssumedRGB() throws IOException {
|
||||||
|
// Problematic test data, which is YCbCr encoded (as correctly specified by the PhotometricInterpretation tag,
|
||||||
|
// but the JPEGImageReader will detect the data as RGB due to non-subsampled data and SOF ids.
|
||||||
|
TestData testData = new TestData(getClassLoaderResource("/tiff/xerox-jpeg-ycbcr-weird-coefficients.tif"), new Dimension(2482, 3520));
|
||||||
|
|
||||||
|
try (ImageInputStream stream = testData.getInputStream()) {
|
||||||
|
TIFFImageReader reader = createReader();
|
||||||
|
reader.setInput(stream);
|
||||||
|
|
||||||
|
ImageReadParam param = reader.getDefaultReadParam();
|
||||||
|
// TODO: There's a bug in reading with source region for the raster case...
|
||||||
|
// param.setSourceRegion(new Rectangle(8, 8));
|
||||||
|
BufferedImage image = reader.read(0, param);
|
||||||
|
|
||||||
|
assertNotNull(image);
|
||||||
|
// assertEquals(new Dimension(8, 8), new Dimension(image.getWidth(), image.getHeight()));
|
||||||
|
assertEquals(testData.getDimension(0), new Dimension(image.getWidth(), image.getHeight()));
|
||||||
|
|
||||||
|
// The pixel at 0, 0 should be white(-ish), not red!
|
||||||
|
// NOTE: The image contains some weird custom YCbCr coefficients, which are roughly
|
||||||
|
// 0.299, 0.587, 0.144, instead of the standard 0.299, 0.587, 0.114 (the last/blue coefficient differs)
|
||||||
|
// this will make the background bright purple, rather than pure white as it would have been
|
||||||
|
// with standard coefficients. Could be a typo/bug in the encoder or intentional.
|
||||||
|
// Some/most software ignores the custom coefficients, and decodes the image as white background...
|
||||||
|
int argb = image.getRGB(0, 0);
|
||||||
|
assertEquals("Alpha", 0xff, (argb >>> 24) & 0xff);
|
||||||
|
assertEquals("Red", 0xff, (argb >> 16) & 0xff);
|
||||||
|
assertEquals("Green", 0xf2, (argb >> 8) & 0xff);
|
||||||
|
assertEquals("Blue", 0xff, argb & 0xff);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Ignore("Known issue")
|
||||||
|
@Test
|
||||||
|
public void testReadJPEGRasterCaseWithSrcRegion() throws IOException {
|
||||||
|
// Problematic test data, which is YCbCr encoded (as correctly specified by the PhotometricInterpretation tag,
|
||||||
|
// but the JPEGImageReader will detect the data as RGB due to non-subsampled data and SOF ids.
|
||||||
|
TestData testData = new TestData(getClassLoaderResource("/tiff/xerox-jpeg-ycbcr-weird-coefficients.tif"), new Dimension(2482, 3520));
|
||||||
|
|
||||||
|
try (ImageInputStream stream = testData.getInputStream()) {
|
||||||
|
TIFFImageReader reader = createReader();
|
||||||
|
reader.setInput(stream);
|
||||||
|
|
||||||
|
ImageReadParam param = reader.getDefaultReadParam();
|
||||||
|
param.setSourceRegion(new Rectangle(8, 8));
|
||||||
|
BufferedImage image = reader.read(0, param);
|
||||||
|
|
||||||
|
assertNotNull(image);
|
||||||
|
assertEquals(new Dimension(8, 8), new Dimension(image.getWidth(), image.getHeight()));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
public void testColorMap8Bit() throws IOException {
|
public void testColorMap8Bit() throws IOException {
|
||||||
TestData testData = new TestData(getClassLoaderResource("/tiff/scan-lzw-8bit-colormap.tiff"), new Dimension(2550, 3300));
|
TestData testData = new TestData(getClassLoaderResource("/tiff/scan-lzw-8bit-colormap.tiff"), new Dimension(2550, 3300));
|
||||||
|
Binary file not shown.
Loading…
x
Reference in New Issue
Block a user