diff --git a/imageio/imageio-jpeg/src/main/java/com/twelvemonkeys/imageio/plugins/jpeg/JPEGImageReader.java b/imageio/imageio-jpeg/src/main/java/com/twelvemonkeys/imageio/plugins/jpeg/JPEGImageReader.java index 54011061..91b2ac2a 100644 --- a/imageio/imageio-jpeg/src/main/java/com/twelvemonkeys/imageio/plugins/jpeg/JPEGImageReader.java +++ b/imageio/imageio-jpeg/src/main/java/com/twelvemonkeys/imageio/plugins/jpeg/JPEGImageReader.java @@ -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 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 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())); } } diff --git a/imageio/imageio-jpeg/src/main/java/com/twelvemonkeys/imageio/plugins/jpeg/JPEGSegmentImageInputStream.java b/imageio/imageio-jpeg/src/main/java/com/twelvemonkeys/imageio/plugins/jpeg/JPEGSegmentImageInputStream.java index 1b73b948..9ee10865 100644 --- a/imageio/imageio-jpeg/src/main/java/com/twelvemonkeys/imageio/plugins/jpeg/JPEGSegmentImageInputStream.java +++ b/imageio/imageio-jpeg/src/main/java/com/twelvemonkeys/imageio/plugins/jpeg/JPEGSegmentImageInputStream.java @@ -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); } } diff --git a/imageio/imageio-jpeg/src/test/java/com/twelvemonkeys/imageio/plugins/jpeg/JPEGImageReaderTest.java b/imageio/imageio-jpeg/src/test/java/com/twelvemonkeys/imageio/plugins/jpeg/JPEGImageReaderTest.java index 58c23ec0..9f454f27 100644 --- a/imageio/imageio-jpeg/src/test/java/com/twelvemonkeys/imageio/plugins/jpeg/JPEGImageReaderTest.java +++ b/imageio/imageio-jpeg/src/test/java/com/twelvemonkeys/imageio/plugins/jpeg/JPEGImageReaderTest.java @@ -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> 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); + } + } } diff --git a/imageio/imageio-jpeg/src/test/resources/jpeg/exif-jpeg-thumbnail-sony-dsc-p150-inverted-colors.jpg b/imageio/imageio-jpeg/src/test/resources/jpeg/exif-jpeg-thumbnail-sony-dsc-p150-inverted-colors.jpg new file mode 100644 index 00000000..90e18f08 Binary files /dev/null and b/imageio/imageio-jpeg/src/test/resources/jpeg/exif-jpeg-thumbnail-sony-dsc-p150-inverted-colors.jpg differ diff --git a/imageio/imageio-metadata/src/main/java/com/twelvemonkeys/imageio/metadata/exif/EXIFEntry.java b/imageio/imageio-metadata/src/main/java/com/twelvemonkeys/imageio/metadata/exif/EXIFEntry.java index 30da3e80..1fb7a788 100644 --- a/imageio/imageio-metadata/src/main/java/com/twelvemonkeys/imageio/metadata/exif/EXIFEntry.java +++ b/imageio/imageio-metadata/src/main/java/com/twelvemonkeys/imageio/metadata/exif/EXIFEntry.java @@ -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: diff --git a/imageio/imageio-metadata/src/main/java/com/twelvemonkeys/imageio/metadata/jpeg/JPEGSegmentUtil.java b/imageio/imageio-metadata/src/main/java/com/twelvemonkeys/imageio/metadata/jpeg/JPEGSegmentUtil.java index d41c7615..6372737b 100644 --- a/imageio/imageio-metadata/src/main/java/com/twelvemonkeys/imageio/metadata/jpeg/JPEGSegmentUtil.java +++ b/imageio/imageio-metadata/src/main/java/com/twelvemonkeys/imageio/metadata/jpeg/JPEGSegmentUtil.java @@ -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); } }