TMI-18: Fix for images/thumbnails get inverted colors.

This commit is contained in:
Harald Kuhr 2012-04-16 22:53:17 +02:00
parent 24db7e847c
commit c16ffaca13
6 changed files with 241 additions and 88 deletions

View File

@ -71,6 +71,7 @@ import java.util.List;
public class JPEGImageReader extends ImageReaderBase {
// TODO: Fix the (stream) metadata inconsistency issues.
// - Sun JPEGMetadata class does not (and can not be made to) support CMYK data.. We need to create all new metadata classes.. :-/
// TODO: Split thumbnail reading into separate class
private final static boolean DEBUG = "true".equalsIgnoreCase(System.getProperty("com.twelvemonkeys.imageio.plugins.jpeg.debug"));
@ -306,7 +307,7 @@ public class JPEGImageReader extends ImageReaderBase {
// NOTE: Reading the metadata here chokes on some images. Instead, parse the Adobe App14 segment and read transform directly
// TODO: If cmyk and no ICC profile, just use FastCMYKToRGB, without attempting loading Generic CMYK profile first?
// TODO: Also, don't get generic CMYK if we already have a profile...
// TODO: Don't get generic CMYK if we already have a CMYK profile...
srcCs = ColorSpaces.getColorSpace(ColorSpaces.CS_GENERIC_CMYK);
imageTypes = Arrays.asList(
ImageTypeSpecifier.createFromBufferedImageType(BufferedImage.TYPE_3BYTE_BGR),
@ -544,83 +545,7 @@ public class JPEGImageReader extends ImageReaderBase {
CompoundDirectory exifMetadata = (CompoundDirectory) new EXIFReader().read(stream);
if (exifMetadata.directoryCount() == 2) {
Directory ifd1 = exifMetadata.getDirectory(1);
Entry compression = ifd1.getEntryById(TIFF.TAG_COMPRESSION);
if (compression != null && compression.getValue().equals(1)) {
// Read ImageWidth, ImageLength (height) and BitsPerSample (=8 8 8, always)
// PhotometricInterpretation (2=RGB, 6=YCbCr), SamplesPerPixel (=3, always),
Entry width = ifd1.getEntryById(TIFF.TAG_IMAGE_WIDTH);
Entry height = ifd1.getEntryById(TIFF.TAG_IMAGE_HEIGHT);
if (width == null || height == null) {
throw new IIOException("Missing dimensions for RAW EXIF thumbnail");
}
Entry bitsPerSample = ifd1.getEntryById(TIFF.TAG_BITS_PER_SAMPLE);
Entry samplesPerPixel = ifd1.getEntryById(TIFF.TAG_SAMPLES_PER_PIXELS);
Entry photometricInterpretation = ifd1.getEntryById(TIFF.TAG_PHOTOMETRIC_INTERPRETATION);
// Required
int w = ((Number) width.getValue()).intValue();
int h = ((Number) height.getValue()).intValue();
if (bitsPerSample != null) {
int[] bpp = (int[]) bitsPerSample.getValue();
if (!Arrays.equals(bpp, new int[]{8, 8, 8})) {
throw new IIOException("Unknown bits per sample for RAW EXIF thumbnail: " + bitsPerSample.getValueAsString());
}
}
if (samplesPerPixel != null && (Integer) samplesPerPixel.getValue() != 3) {
throw new IIOException("Unknown samples per pixel for RAW EXIF thumbnail: " + samplesPerPixel.getValueAsString());
}
int interpretation = photometricInterpretation != null ? ((Number) photometricInterpretation.getValue()).intValue() : 2;
// IFD1 should contain strip offsets for uncompressed images
Entry offset = ifd1.getEntryById(TIFF.TAG_STRIP_OFFSETS);
if (offset != null) {
stream.seek(((Number) offset.getValue()).longValue());
// Read raw image data, either RGB or YCbCr
int thumbSize = w * h * 3;
byte[] thumbData = readFully(stream, thumbSize);
switch (interpretation) {
case 2:
// RGB
break;
case 6:
// YCbCr
for (int i = 0, thumbDataLength = thumbData.length; i < thumbDataLength; i++) {
YCbCrConverter.convertYCbCr2RGB(thumbData, thumbData, i);
}
break;
default:
throw new IIOException("Unknown photometric interpretation for RAW EXIF thumbnail: " + interpretation);
}
thumbnails.add(readRawThumbnail(thumbData, thumbData.length, 0, w, h));
}
}
else if (compression == null || compression.getValue().equals(6)) {
Entry jpegOffset = ifd1.getEntryById(TIFF.TAG_JPEG_INTERCHANGE_FORMAT);
// IFD1 should contain jpeg offset for JPEG thumbnail
if (jpegOffset != null) {
stream.seek(((Number) jpegOffset.getValue()).longValue());
InputStream adapter = IIOUtil.createStreamAdapter(stream);
BufferedImage exifThumb = ImageIO.read(adapter);
if (exifThumb != null) {
thumbnails.add(exifThumb);
}
adapter.close();
}
}
}
extractEXIFThumbnails(stream, exifMetadata);
return exifMetadata;
}
@ -628,6 +553,102 @@ public class JPEGImageReader extends ImageReaderBase {
return null;
}
private void extractEXIFThumbnails(ImageInputStream stream, CompoundDirectory exifMetadata) throws IOException {
if (exifMetadata.directoryCount() == 2) {
Directory ifd1 = exifMetadata.getDirectory(1);
Entry compression = ifd1.getEntryById(TIFF.TAG_COMPRESSION);
if (compression != null && compression.getValue().equals(1)) { // 1 = no compression
// Read ImageWidth, ImageLength (height) and BitsPerSample (=8 8 8, always)
// PhotometricInterpretation (2=RGB, 6=YCbCr), SamplesPerPixel (=3, always),
Entry width = ifd1.getEntryById(TIFF.TAG_IMAGE_WIDTH);
Entry height = ifd1.getEntryById(TIFF.TAG_IMAGE_HEIGHT);
if (width == null || height == null) {
throw new IIOException("Missing dimensions for RAW EXIF thumbnail");
}
Entry bitsPerSample = ifd1.getEntryById(TIFF.TAG_BITS_PER_SAMPLE);
Entry samplesPerPixel = ifd1.getEntryById(TIFF.TAG_SAMPLES_PER_PIXELS);
Entry photometricInterpretation = ifd1.getEntryById(TIFF.TAG_PHOTOMETRIC_INTERPRETATION);
// Required
int w = ((Number) width.getValue()).intValue();
int h = ((Number) height.getValue()).intValue();
if (bitsPerSample != null) {
int[] bpp = (int[]) bitsPerSample.getValue();
if (!Arrays.equals(bpp, new int[] {8, 8, 8})) {
throw new IIOException("Unknown bits per sample for RAW EXIF thumbnail: " + bitsPerSample.getValueAsString());
}
}
if (samplesPerPixel != null && (Integer) samplesPerPixel.getValue() != 3) {
throw new IIOException("Unknown samples per pixel for RAW EXIF thumbnail: " + samplesPerPixel.getValueAsString());
}
int interpretation = photometricInterpretation != null ? ((Number) photometricInterpretation.getValue()).intValue() : 2;
// IFD1 should contain strip offsets for uncompressed images
Entry offset = ifd1.getEntryById(TIFF.TAG_STRIP_OFFSETS);
if (offset != null) {
stream.seek(((Number) offset.getValue()).longValue());
// Read raw image data, either RGB or YCbCr
int thumbSize = w * h * 3;
byte[] thumbData = readFully(stream, thumbSize);
switch (interpretation) {
case 2:
// RGB
break;
case 6:
// YCbCr
for (int i = 0, thumbDataLength = thumbData.length; i < thumbDataLength; i += 3) {
YCbCrConverter.convertYCbCr2RGB(thumbData, thumbData, i);
}
break;
default:
throw new IIOException("Unknown photometric interpretation for RAW EXIF thumbnail: " + interpretation);
}
thumbnails.add(readRawThumbnail(thumbData, thumbData.length, 0, w, h));
}
}
else if (compression == null || compression.getValue().equals(6)) { // 6 = JPEG compression
// IFD1 should contain JPEG offset for JPEG thumbnail
Entry jpegOffset = ifd1.getEntryById(TIFF.TAG_JPEG_INTERCHANGE_FORMAT);
if (jpegOffset != null) {
stream.seek(((Number) jpegOffset.getValue()).longValue());
InputStream input = IIOUtil.createStreamAdapter(stream);
// For certain EXIF files (encoded with TIFF.TAG_YCBCR_POSITIONING = 2?), we need
// EXIF information to read the thumbnail correctly (otherwise the colors are messed up).
// HACK: Splice empty EXIF information into the thumbnail stream
byte[] fakeEmptyExif = {
// SOI (from original data)
(byte) input.read(), (byte) input.read(),
// APP1 + len (016) + 'Exif' + 0-term + pad
(byte) 0xFF, (byte) 0xE1, 0, 16, 'E', 'x', 'i', 'f', 0, 0,
// Big-endian BOM (MM), TIFF magic (042), offset (0000)
'M', 'M', 0, 42, 0, 0, 0, 0,
};
input = new SequenceInputStream(new ByteArrayInputStream(fakeEmptyExif), input);
BufferedImage exifThumb = ImageIO.read(input);
if (exifThumb != null) {
thumbnails.add(exifThumb);
}
input.close();
}
}
}
}
private List<JPEGSegment> getAppSegments(final int marker, final String identifier) throws IOException {
initHeader();
@ -1362,14 +1383,19 @@ public class JPEGImageReader extends ImageReaderBase {
public static void main(final String[] args) throws IOException {
for (final String arg : args) {
// File file = new File(args[0]);
File file = new File(arg);
ImageInputStream input = ImageIO.createImageInputStream(file);
if (input == null) {
System.err.println("Could not read file: " + file);
continue;
}
Iterator<ImageReader> readers = ImageIO.getImageReaders(input);
if (!readers.hasNext()) {
System.err.println("No reader for: " + file);
System.exit(1);
continue;
}
ImageReader reader = readers.next();
@ -1411,7 +1437,6 @@ public class JPEGImageReader extends ImageReaderBase {
}
});
reader.setInput(input);
try {
@ -1452,6 +1477,7 @@ public class JPEGImageReader extends ImageReaderBase {
int numThumbnails = reader.getNumThumbnails(0);
for (int i = 0; i < numThumbnails; i++) {
BufferedImage thumbnail = reader.readThumbnail(0, i);
// System.err.println("thumbnail: " + thumbnail);
showIt(thumbnail, String.format("Thumbnail: %s [%d x %d]", file.getName(), thumbnail.getWidth(), thumbnail.getHeight()));
}
}

View File

@ -29,7 +29,6 @@
package com.twelvemonkeys.imageio.plugins.jpeg;
import com.twelvemonkeys.imageio.metadata.jpeg.JPEG;
import com.twelvemonkeys.lang.Validate;
import javax.imageio.IIOException;
import javax.imageio.stream.ImageInputStream;
@ -141,7 +140,7 @@ final class JPEGSegmentImageInputStream extends ImageInputStreamImpl {
}
private static boolean isAppSegmentWithId(String segmentId, ImageInputStream stream) throws IOException {
Validate.notNull(segmentId, "segmentId");
notNull(segmentId, "segmentId");
stream.mark();
@ -160,7 +159,7 @@ final class JPEGSegmentImageInputStream extends ImageInputStreamImpl {
static String asNullTerminatedAsciiString(final byte[] data, final int offset) {
for (int i = 0; i < data.length - offset; i++) {
if (data[i] == 0 || i > 255) {
if (data[offset + i] == 0 || i > 255) {
return asAsciiString(data, offset, offset + i);
}
}

View File

@ -43,6 +43,7 @@ import java.util.Arrays;
import java.util.List;
import static org.junit.Assert.*;
import static org.junit.Assert.assertEquals;
/**
* JPEGImageReaderTest
@ -135,9 +136,13 @@ public class JPEGImageReaderTest extends ImageReaderAbstractTestCase<JPEGImageRe
byte[] expectedData = {34, 37, 34, 47, 47, 44, 22, 26, 28, 23, 26, 28, 20, 23, 26, 20, 22, 25, 22, 25, 27, 18, 21, 24};
assertEquals(expectedData.length, data.length);
for (int i = 0; i < expectedData.length; i++) {
assertEquals(expectedData[i], data[i], 5);
assertJPEGPixelsEqual(expectedData, data, 0);
}
private static void assertJPEGPixelsEqual(byte[] expected, byte[] actual, int actualOffset) {
for (int i = 0; i < expected.length; i++) {
assertEquals(expected[i], actual[i + actualOffset], 5);
}
}
@ -333,4 +338,65 @@ public class JPEGImageReaderTest extends ImageReaderAbstractTestCase<JPEGImageRe
assertEquals(96, thumbnail.getWidth());
assertEquals(72, thumbnail.getHeight());
}
@Test
public void testInvertedColors() throws IOException {
JPEGImageReader reader = createReader();
reader.setInput(ImageIO.createImageInputStream(getClassLoaderResource("/jpeg/exif-jpeg-thumbnail-sony-dsc-p150-inverted-colors.jpg")));
assertEquals(2437, reader.getWidth(0));
assertEquals(1662, reader.getHeight(0));
ImageReadParam param = reader.getDefaultReadParam();
param.setSourceRegion(new Rectangle(0, 0, reader.getWidth(0), 8));
BufferedImage strip = reader.read(0, param);
assertNotNull(strip);
assertEquals(2437, strip.getWidth());
assertEquals(8, strip.getHeight());
int[] expectedRGB = new int[] {
0xffe9d0bc, 0xfff3decd, 0xfff5e6d3, 0xfff8ecdc, 0xfff8f0e5, 0xffe3ceb9, 0xff6d3923, 0xff5a2d18,
0xff00170b, 0xff131311, 0xff52402c, 0xff624a30, 0xff6a4f34, 0xfffbf8f1, 0xfff4efeb, 0xffefeae6,
0xffebe6e2, 0xffe3e0d9, 0xffe1d6d0, 0xff10100e
};
// Validate strip colors
for (int i = 0; i < strip.getWidth() / 128; i++) {
int actualRGB = strip.getRGB(i * 128, 4);
assertEquals((actualRGB >> 16) & 0xff, (expectedRGB[i] >> 16) & 0xff, 5);
assertEquals((actualRGB >> 8) & 0xff, (expectedRGB[i] >> 8) & 0xff, 5);
assertEquals((actualRGB) & 0xff, (expectedRGB[i]) & 0xff, 5);
}
}
@Test
public void testThumbnailInvertedColors() throws IOException {
JPEGImageReader reader = createReader();
reader.setInput(ImageIO.createImageInputStream(getClassLoaderResource("/jpeg/exif-jpeg-thumbnail-sony-dsc-p150-inverted-colors.jpg")));
assertTrue(reader.hasThumbnails(0));
assertEquals(1, reader.getNumThumbnails(0));
assertEquals(160, reader.getThumbnailWidth(0, 0));
assertEquals(109, reader.getThumbnailHeight(0, 0));
BufferedImage thumbnail = reader.readThumbnail(0, 0);
assertNotNull(thumbnail);
assertEquals(160, thumbnail.getWidth());
assertEquals(109, thumbnail.getHeight());
int[] expectedRGB = new int[] {
0xffefd5c4, 0xffead3b1, 0xff55392d, 0xff55403b, 0xff6d635a, 0xff7b726b, 0xff68341f, 0xff5c2f1c,
0xff250f12, 0xff6d7c77, 0xff414247, 0xff6a4f3a, 0xff6a4e39, 0xff564438, 0xfffcf7f1, 0xffefece7,
0xfff0ebe7, 0xff464040, 0xffe3deda, 0xffd4cfc9,
};
// Validate strip colors
for (int i = 0; i < thumbnail.getWidth() / 8; i++) {
int actualRGB = thumbnail.getRGB(i * 8, 4);
assertEquals((actualRGB >> 16) & 0xff, (expectedRGB[i] >> 16) & 0xff, 5);
assertEquals((actualRGB >> 8) & 0xff, (expectedRGB[i] >> 8) & 0xff, 5);
assertEquals((actualRGB) & 0xff, (expectedRGB[i]) & 0xff, 5);
}
}
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 1013 KiB

View File

@ -59,6 +59,10 @@ final class EXIFEntry extends AbstractEntry {
switch ((Integer) getIdentifier()) {
case TIFF.TAG_EXIF_IFD:
return "EXIF";
case TIFF.TAG_INTEROP_IFD:
return "Interoperability";
case TIFF.TAG_GPS_IFD:
return "GPS";
case TIFF.TAG_XMP:
return "XMP";
case TIFF.TAG_IPTC:
@ -112,6 +116,10 @@ final class EXIFEntry extends AbstractEntry {
return "HostComputer";
case TIFF.TAG_COPYRIGHT:
return "Copyright";
case TIFF.TAG_YCBCR_SUB_SAMPLING:
return "YCbCrSubSampling";
case TIFF.TAG_YCBCR_POSITIONING:
return "YCbCrPositioning";
case EXIF.TAG_EXPOSURE_TIME:
return "ExposureTime";
@ -121,6 +129,55 @@ final class EXIFEntry extends AbstractEntry {
return "ExposureProgram";
case EXIF.TAG_ISO_SPEED_RATINGS:
return "ISOSpeedRatings";
case EXIF.TAG_SHUTTER_SPEED_VALUE:
return "ShutterSpeedValue";
case EXIF.TAG_APERTURE_VALUE:
return "ApertureValue";
case EXIF.TAG_BRIGHTNESS_VALUE:
return "BrightnessValue";
case EXIF.TAG_EXPOSURE_BIAS_VALUE:
return "ExposureBiasValue";
case EXIF.TAG_MAX_APERTURE_VALUE:
return "MaxApertureValue";
case EXIF.TAG_SUBJECT_DISTANCE:
return "SubjectDistance";
case EXIF.TAG_METERING_MODE:
return "MeteringMode";
case EXIF.TAG_LIGHT_SOURCE:
return "LightSource";
case EXIF.TAG_FLASH:
return "Flash";
case EXIF.TAG_FOCAL_LENGTH:
return "FocalLength";
case EXIF.TAG_FILE_SOURCE:
return "FileSource";
case EXIF.TAG_SCENE_TYPE:
return "SceneType";
case EXIF.TAG_CFA_PATTERN:
return "CFAPattern";
case EXIF.TAG_CUSTOM_RENDERED:
return "CustomRendered";
case EXIF.TAG_EXPOSURE_MODE:
return "ExposureMode";
case EXIF.TAG_WHITE_BALANCE:
return "WhiteBalance";
case EXIF.TAG_DIGITAL_ZOOM_RATIO:
return "DigitalZoomRation";
case EXIF.TAG_FOCAL_LENGTH_IN_35_MM_FILM:
return "FocalLengthIn35mmFilm";
case EXIF.TAG_SCENE_CAPTURE_TYPE:
return "SceneCaptureType";
case EXIF.TAG_GAIN_CONTROL:
return "GainControl";
case EXIF.TAG_CONTRAST:
return "Contrast";
case EXIF.TAG_SATURATION:
return "Saturation";
case EXIF.TAG_SHARPNESS:
return "Sharpness";
case EXIF.TAG_FLASHPIX_VERSION:
return "FlashpixVersion";
case EXIF.TAG_EXIF_VERSION:
return "ExifVersion";
@ -133,6 +190,11 @@ final class EXIFEntry extends AbstractEntry {
case EXIF.TAG_USER_COMMENT:
return "UserComment";
case EXIF.TAG_COMPONENTS_CONFIGURATION:
return "ComponentsConfiguration";
case EXIF.TAG_COMPRESSED_BITS_PER_PIXEL:
return "CompressedBitsPerPixel";
case EXIF.TAG_COLOR_SPACE:
return "ColorSpace";
case EXIF.TAG_PIXEL_X_DIMENSION:

View File

@ -133,7 +133,7 @@ public final class JPEGSegmentUtil {
static String asNullTerminatedAsciiString(final byte[] data, final int offset) {
for (int i = 0; i < data.length - offset; i++) {
if (data[i] == 0 || i > 255) {
if (data[offset + i] == 0 || i > 255) {
return asAsciiString(data, offset, offset + i);
}
}