diff --git a/imageio/imageio-dng/pom.xml b/imageio/imageio-dng/pom.xml
new file mode 100644
index 00000000..8b7a74f1
--- /dev/null
+++ b/imageio/imageio-dng/pom.xml
@@ -0,0 +1,65 @@
+
+
+
+
+
+ imageio
+ com.twelvemonkeys.imageio
+ 3.1-SNAPSHOT
+
+ 4.0.0
+
+ imageio-dng
+ TwelveMonkeys :: ImageIO :: DNG plugin
+ ImageIO plugin for Adobe Digital Negative and TIFF/EP (DNG).
+
+
+
+ com.twelvemonkeys.imageio
+ imageio-core
+
+
+ com.twelvemonkeys.imageio
+ imageio-core
+ tests
+
+
+ com.twelvemonkeys.imageio
+ imageio-metadata
+
+
+ ${project.version}
+ com.twelvemonkeys.imageio
+ imageio-jpeg
+
+
+
+
\ No newline at end of file
diff --git a/imageio/imageio-dng/src/main/java/com/twelvemonkeys/imageio/plugins/dng/DNG.java b/imageio/imageio-dng/src/main/java/com/twelvemonkeys/imageio/plugins/dng/DNG.java
new file mode 100644
index 00000000..0948c6a8
--- /dev/null
+++ b/imageio/imageio-dng/src/main/java/com/twelvemonkeys/imageio/plugins/dng/DNG.java
@@ -0,0 +1,127 @@
+package com.twelvemonkeys.imageio.plugins.dng;
+
+/**
+ * DNG
+ *
+ * @author Harald Kuhr
+ * @author last modified by $Author: haraldk$
+ * @version $Id: DNG.java,v 1.0 03.10.14 10:49 haraldk Exp$
+ */
+interface DNG {
+ // TODO: Some (all?) of these tags are defined by TIFF/EP, should we reflect that in package/class names?
+
+ /** CFA (Color Filter Array). */
+ int PHOTOMETRIC_CFA = 32803;
+ /** LinearRaw. */
+ int PHOTOMETRIC_LINEAR_RAW = 34892;
+
+ /**
+ * Lossy JPEG.
+ *
+ * Lossy JPEG (34892) is allowed for IFDs that use PhotometricInterpretation = 34892
+ * (LinearRaw) and 8-bit integer data. This new compression code is required to let the DNG
+ * reader know to use a lossy JPEG decoder rather than a lossless JPEG decoder for this
+ * combination of PhotometricInterpretation and BitsPerSample.
+ */
+ int COMPRESSION_LOSSY_JPEG = 34892;
+
+ /**
+ * CFARepeatPatternDim
+ *
+ * This tag encodes the number of pixels horizontally and vertically that are needed to uniquely define the repeat
+ * pattern of the color filter array (CFA) pattern used in the color image sensor. It is mandatory when
+ * PhotometricInterpretation = 32803, and there are no defaults allowed. It is optional when
+ * PhotometricInterpretation = 2 or 6 and SensingMethod = 2, where it can be used to indicate the original sensor
+ * sampling positions.
+ */
+ int TAG_CFA_REPEAT_PATTERN_DIM = 33421;
+ /**
+ * Indicates the color filter array (CFA) geometric pattern of the image sensor
+ * when a one-chip color area sensor is used.
+ * NOTE: This tag (defined in TIFF/EP) is different from the CFAPattern defined in normal TIFF.
+ */
+ int TAG_CFA_PATTERN = 33422;
+
+ /** Indicates the image sensor type on the camera or input device. */
+ int TAG_SENSING_METHOD = 37399;
+
+ // From http://www.awaresystems.be/imaging/tiff/tifftags/privateifd/exif/cfapattern.html
+ byte CFA_PATTERN_RED = 0;
+ byte CFA_PATTERN_GREEN = 1;
+ byte CFA_PATTERN_BLUE = 2;
+ byte CFA_PATTERN_CYAN = 3;
+ byte CFA_PATTERN_MAGENTA = 4;
+ byte CFA_PATTERN_YELLOW = 5;
+ byte CFA_PATTERN_WHITE = 6; // ???? Should be KEY?
+
+ /**
+ * This tag encodes the DNG four-tier version number. For files compliant with this version of
+ * the DNG specification (1.4.0.0), this tag should contain the bytes: 1, 4, 0, 0.
+ */
+ int TAG_DNG_VERSION = 50706;
+ int TAG_DNG_BACKWARD_VERSION = 50707;
+
+ /** UniqueCameraModel defines a unique, non-localized name for the camera model that created the image in the raw file. */
+ int TAG_UNIQUE_CAMERA_MODEL = 50708;
+ int TAG_LOCALIZED_CAMERA_MODEL = 50709;
+
+ /** CFA plane to RGB mapping (default: [0, 1, 2]). */
+ int TAG_CFA_PLANE_COLOR = 50710;
+ /** CFA spatial layout (default: 1). */
+ int TAG_CFA_LAYOUT = 50711;
+
+ /** 1 = Rectangular (or square) layout. */
+ int CFA_LAYOUT_RECTANGULAR = 1;
+ /** 2 = Staggered layout A: even columns are offset down by 1/2 row. */
+ int CFA_LAYOUT_STAGGERED_A = 2;
+ /** 3 = Staggered layout B: even columns are offset up by 1/2 row. */
+ int CFA_LAYOUT_STAGGERED_B = 3;
+ /** 4 = Staggered layout C: even rows are offset right by 1/2 column. */
+ int CFA_LAYOUT_STAGGERED_C = 4;
+ /** 5 = Staggered layout D: even rows are offset left by 1/2 column. */
+ int CFA_LAYOUT_STAGGERED_D = 5;
+ /** 6 = Staggered layout E: even rows are offset up by 1/2 row, even columns are offset left by 1/2 column. */
+ int CFA_LAYOUT_STAGGERED_E = 6;
+ /** 7 = Staggered layout F: even rows are offset up by 1/2 row, even columns are offset right by 1/2 column. */
+ int CFA_LAYOUT_STAGGERED_F = 7;
+ /** 8 = Staggered layout G: even rows are offset down by 1/2 row, even columns are offset left by 1/2 column. */
+ int CFA_LAYOUT_STAGGERED_G = 8;
+ /** 9 = Staggered layout H: even rows are offset down by 1/2 row, even columns are offset right by 1/2 column. */
+ int CFA_LAYOUT_STAGGERED_H = 9;
+
+ /** LinearizationTable describes a lookup table that maps stored values into linear values. */
+ int TAG_LINEARIZATION_TABLE = 50712;
+
+ /** This tag specifies repeat pattern size for the BlackLevel tag. Default: [1, 1]. */
+ int TAG_BLACK_LEVEL_REPEAT_DIM = 50713;
+
+ /**
+ * This tag specifies the zero light (a.k.a. thermal black or black current) encoding level,
+ * as a repeating pattern. Default: 0.
+ */
+ int TAG_BLACK_LEVEL = 50714;
+
+ // TODO: Rest of DNG tags.
+ // ...
+
+ /**
+ * Horizontal Difference X2.
+ * Same as Horizontal Difference except the pixel two to the left is used rather than the pixel one to the left.
+ */
+ int PREDICTOR_HORIZONTAL_X2 = 34892;
+ /**
+ * Horizontal Difference X4.
+ * Same as Horizontal Difference except the pixel four to the left is used rather than the pixel one to the left.
+ */
+ int PREDICTOR_HORIZONTAL_X4 = 34893;
+ /**
+ * Floating Point X2.
+ * Same as Floating Point except the pixel two to the left is used rather than the pixel one to the left.
+ */
+ int PREDICTOR_FLOATINGPOINT_X2 = 34894;
+ /**
+ * Floating Point X4.
+ * Same as Floating Point except the pixel four to the left is used rather than the pixel one to the left
+ */
+ int PREDICTOR_FLOATINGPOINT_X4 = 34895;
+}
diff --git a/imageio/imageio-dng/src/main/java/com/twelvemonkeys/imageio/plugins/dng/DNGImageReader.java b/imageio/imageio-dng/src/main/java/com/twelvemonkeys/imageio/plugins/dng/DNGImageReader.java
new file mode 100644
index 00000000..fe4c86b1
--- /dev/null
+++ b/imageio/imageio-dng/src/main/java/com/twelvemonkeys/imageio/plugins/dng/DNGImageReader.java
@@ -0,0 +1,1051 @@
+/*
+ * Copyright (c) 2014, Harald Kuhr
+ * All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions are met:
+ * * Redistributions of source code must retain the above copyright
+ * notice, this list of conditions and the following disclaimer.
+ * * Redistributions in binary form must reproduce the above copyright
+ * notice, this list of conditions and the following disclaimer in the
+ * documentation and/or other materials provided with the distribution.
+ * * Neither the name "TwelveMonkeys" nor the
+ * names of its contributors may be used to endorse or promote products
+ * derived from this software without specific prior written permission.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+ * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+ * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
+ * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR
+ * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL,
+ * EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
+ * PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR
+ * PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF
+ * LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING
+ * NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
+ * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+
+package com.twelvemonkeys.imageio.plugins.dng;
+
+import com.twelvemonkeys.imageio.ImageReaderBase;
+import com.twelvemonkeys.imageio.metadata.CompoundDirectory;
+import com.twelvemonkeys.imageio.metadata.Directory;
+import com.twelvemonkeys.imageio.metadata.Entry;
+import com.twelvemonkeys.imageio.metadata.exif.EXIFReader;
+import com.twelvemonkeys.imageio.metadata.exif.TIFF;
+import com.twelvemonkeys.imageio.stream.SubImageInputStream;
+import com.twelvemonkeys.imageio.util.IIOUtil;
+import com.twelvemonkeys.io.LittleEndianDataInputStream;
+
+import javax.imageio.*;
+import javax.imageio.spi.ImageReaderSpi;
+import javax.imageio.stream.ImageInputStream;
+import java.awt.*;
+import java.awt.color.ColorSpace;
+import java.awt.image.*;
+import java.io.*;
+import java.nio.ByteOrder;
+import java.util.*;
+import java.util.List;
+import java.util.zip.Inflater;
+import java.util.zip.InflaterInputStream;
+
+/**
+ * Adobe Digital Negative DNG ImageReader.
+ *
+ *
+ * @see Digital Negative (DNG) Specification
+ * @author Harald Kuhr
+ * @author last modified by $Author: haraldk$
+ * @version $Id: DNGImageReader.java,v 1.0 07.04.14 21:31 haraldk Exp$
+ */
+public final class DNGImageReader extends ImageReaderBase {
+ // SEE: http://wwwimages.adobe.com/content/dam/Adobe/en/products/photoshop/pdfs/dng_spec_1.4.0.0.pdf
+ // TODO: Avoid duped code from TIFFImageReader
+ // TODO: DNG is tightly tied to TIFF/EP. Would it make more sense to include all the functionality into the TIFFImageReader?
+ // TODO: Probably a good idea to move some of the getAsShort/Int/Long/Array to TIFF/EXIF metadata module
+ // TODO: Automatic EXIF rotation, if we find a good way to do that for JPEG/EXIF/TIFF and keeping the metadata sane...
+
+ static final boolean DEBUG = true; //"true".equalsIgnoreCase(System.getProperty("com.twelvemonkeys.imageio.plugins.dng.debug"));
+
+ /** Somewhat arbitrary, but it's the current "largest" icon size for Apple icons, AppStore and Google Play. */
+ private static final int MAX_THUMBNAIL_SIZE = 512;
+
+ private static final ImageTypeSpecifier THUMB_SPEC = ImageTypeSpecifier.createInterleaved(ColorSpace.getInstance(ColorSpace.CS_sRGB), new int[]{0, 1, 2}, DataBuffer.TYPE_BYTE, false, false);
+
+ private CompoundDirectory IFDs;
+ private List subIFDs;
+ private Directory currentIFD;
+
+ private int thumbnailIFD = -1;
+
+ DNGImageReader(final ImageReaderSpi provider) {
+ super(provider);
+ }
+
+ @Override
+ protected void resetMembers() {
+ IFDs = null;
+ subIFDs = null;
+ thumbnailIFD = -1;
+
+ currentIFD = null;
+ }
+
+ private void readMetadata() throws IOException {
+ if (imageInput == null) {
+ throw new IllegalStateException("input not set");
+ }
+
+ if (IFDs == null) {
+ imageInput.seek(0);
+
+ IFDs = (CompoundDirectory) new EXIFReader().read(imageInput); // NOTE: Sets byte order as a side effect
+
+ // Pull up the sub-ifds now, as the DNG spec "recommends the use of SubIFD trees,
+ // as described in the TIFF-EP specification. SubIFD chains are not supported".
+ Entry subIFDEntry = IFDs.getEntryById(TIFF.TAG_SUB_IFD);
+
+ if (subIFDEntry != null) {
+ Object subIFD = subIFDEntry.getValue();
+
+ if (subIFD instanceof Directory) {
+ subIFDs = Collections.singletonList((Directory) subIFD);
+ }
+ else {
+ Directory[] directories = (Directory[]) subIFD;
+ subIFDs = Arrays.asList(directories);
+ }
+ }
+ else {
+ subIFDs = Collections.emptyList();
+ }
+
+ // Find which, if any, (sub-) IFD contains the thumbnail
+ // (bit 1 is set also for the JPEG preview, that is too large for a thumbnail)
+ currentIFD = IFDs.getDirectory(0);
+ determineThumbnailIFD(0); // Most likely, it's in IFD0
+
+ for (int i = 0; i < subIFDs.size(); i++) {
+ if (thumbnailIFD != -1) {
+ break;
+ }
+
+ currentIFD = subIFDs.get(i);
+ determineThumbnailIFD(i + 1);
+ }
+
+ if (DEBUG) {
+ System.err.println("Byte order: " + imageInput.getByteOrder());
+ System.err.println("Number of IFDs: " + IFDs.directoryCount());
+
+ for (int i = 0; i < IFDs.directoryCount(); i++) {
+ System.err.printf("IFD %d: %s\n", i, IFDs.getDirectory(i));
+ }
+
+ System.err.println("thumbnailIFD: " + thumbnailIFD);
+ }
+ }
+ }
+
+ private void determineThumbnailIFD(final int thumbnailIFD1) throws IIOException {
+ // Look at TIFF.TAG_SUBFILE_TYPE. If the first bit is 1, this is a "reduced" version,
+ // that MIGHT be the thumbnail (typical value is 1).
+ // TODO: There could be more thumbnails, using the value 0x10001 ("alternate preview").
+ Entry subFileType = currentIFD.getEntryById(TIFF.TAG_SUBFILE_TYPE);
+
+ if (subFileType != null && ((Number) subFileType.getValue()).intValue() == 1) {
+ int imageWidth = getValueAsInt(TIFF.TAG_IMAGE_WIDTH, "ImageWidth");
+ int imageHeight= getValueAsInt(TIFF.TAG_IMAGE_HEIGHT, "ImageHeight");
+
+ // Use heuristic: h & w <= 512 --> thumbnail
+ if (imageWidth <= MAX_THUMBNAIL_SIZE && imageHeight <= MAX_THUMBNAIL_SIZE) {
+ thumbnailIFD = thumbnailIFD1;
+ }
+ }
+ }
+
+ private void readIFD(final int ifdIndex) throws IOException {
+ readMetadata();
+
+ if (ifdIndex < 0) {
+ throw new IndexOutOfBoundsException("index < minIndex");
+ }
+ else {
+ int numIFDs = IFDs.directoryCount() + subIFDs.size();
+
+ if (ifdIndex >= numIFDs) {
+ throw new IndexOutOfBoundsException("index >= numIFDs (" + ifdIndex + " >= " + numIFDs + ")");
+ }
+ }
+
+ // Depth first (...but a DNG should only contain one IFD with subIFDs)
+ if (ifdIndex == 0) {
+ currentIFD = IFDs.getDirectory(ifdIndex);
+ }
+ else if (ifdIndex <= subIFDs.size()) {
+ currentIFD = subIFDs.get(ifdIndex - 1);
+ }
+ else {
+ currentIFD = IFDs.getDirectory(ifdIndex - subIFDs.size());
+ }
+ }
+
+ @Override
+ public int getNumImages(final boolean allowSearch) throws IOException {
+ readMetadata();
+
+ int numIFDs = IFDs.directoryCount() + subIFDs.size();
+
+ return thumbnailIFD != -1 ? numIFDs - 1 : numIFDs;
+ }
+
+ @Override
+ public int getNumThumbnails(int imageIndex) throws IOException {
+ readMetadata();
+ checkBounds(imageIndex);
+
+ return imageIndex == 0 && thumbnailIFD >= 0 ? 1 : 0;
+ }
+
+ @Override
+ public boolean readerSupportsThumbnails() {
+ return true;
+ }
+
+ @Override
+ public int getThumbnailWidth(int imageIndex, int thumbnailIndex) throws IOException {
+ readIFD(thumbnailIFD != -1 ? thumbnailIFD : 0); // Avoid reading bad thumbnail index, but still doing proper verifications
+
+ if (imageIndex != 0 || thumbnailIFD == -1) {
+ throw new IndexOutOfBoundsException("No thumbnail for imageIndex: " + imageIndex);
+ }
+ if (thumbnailIndex != 0) {
+ throw new IndexOutOfBoundsException("thumbnailIndex out of bounds: " + thumbnailIndex);
+ }
+
+ return getValueAsInt(TIFF.TAG_IMAGE_WIDTH, "ImageWidth");
+ }
+
+ @Override
+ public int getThumbnailHeight(int imageIndex, int thumbnailIndex) throws IOException {
+ readIFD(thumbnailIFD != -1 ? thumbnailIFD : 0); // Avoid reading bad thumbnail index, but still doing proper verifications
+
+ if (imageIndex != 0 || thumbnailIFD == -1) {
+ throw new IndexOutOfBoundsException("No thumbnail for imageIndex: " + imageIndex);
+ }
+ if (thumbnailIndex != 0) {
+ throw new IndexOutOfBoundsException("thumbnailIndex out of bounds: " + thumbnailIndex);
+ }
+
+ return getValueAsInt(TIFF.TAG_IMAGE_HEIGHT, "ImageHeight");
+ }
+
+ @Override
+ public BufferedImage readThumbnail(int imageIndex, int thumbnailIndex) throws IOException {
+ readIFD(thumbnailIFD != -1 ? thumbnailIFD : 0); // Avoid reading bad thumbnail index, but still doing proper verifications
+
+ if (imageIndex != 0 || thumbnailIFD == -1) {
+ throw new IndexOutOfBoundsException("No thumbnail for imageIndex: " + imageIndex);
+ }
+ if (thumbnailIndex != 0) {
+ throw new IndexOutOfBoundsException("thumbnailIndex out of bounds: " + thumbnailIndex);
+ }
+
+ // Read uncompressed RGB
+ int imageWidth = getValueAsInt(TIFF.TAG_IMAGE_WIDTH, "ImageWidth");
+ int imageHeight = getValueAsInt(TIFF.TAG_IMAGE_HEIGHT, "ImageHeight");
+
+ // NEF thumbnail simplification: single strip
+ long stripOffset = getValueAsLong(TIFF.TAG_STRIP_OFFSETS, "StripOffsets");
+ long stripCount = getValueAsLong(TIFF.TAG_STRIP_BYTE_COUNTS, "StripByteCounts");
+
+ BufferedImage thumbnail = THUMB_SPEC.createBufferedImage(imageWidth, imageHeight);
+
+ WritableRaster raster = thumbnail.getRaster();
+ DataBufferByte dataBuffer = (DataBufferByte) raster.getDataBuffer();
+
+ imageInput.seek(stripOffset);
+ ImageInputStream stream = new SubImageInputStream(imageInput, stripCount);
+
+ try {
+ stream.readFully(dataBuffer.getData());
+ }
+ finally {
+ stream.close();
+ }
+
+ return thumbnail;
+ }
+
+ private long[] getValueAsLongArray(final int tag, final String tagName, boolean required) throws IIOException {
+ Entry entry = currentIFD.getEntryById(tag);
+ if (entry == null) {
+ if (required) {
+ throw new IIOException("Missing TIFF tag " + tagName);
+ }
+
+ return null;
+ }
+
+ long[] value;
+
+ if (entry.valueCount() == 1) {
+ // For single entries, this will be a boxed type
+ value = new long[] {((Number) entry.getValue()).longValue()};
+ }
+ else if (entry.getValue() instanceof short[]) {
+ short[] shorts = (short[]) entry.getValue();
+ value = new long[shorts.length];
+
+ for (int i = 0, length = value.length; i < length; i++) {
+ value[i] = shorts[i];
+ }
+ }
+ else if (entry.getValue() instanceof int[]) {
+ int[] ints = (int[]) entry.getValue();
+ value = new long[ints.length];
+
+ for (int i = 0, length = value.length; i < length; i++) {
+ value[i] = ints[i];
+ }
+ }
+ else if (entry.getValue() instanceof long[]) {
+ value = (long[]) entry.getValue();
+ }
+ else {
+ throw new IIOException(String.format("Unsupported %s type: %s (%s)", tagName, entry.getTypeName(), entry.getValue().getClass()));
+ }
+
+ return value;
+ }
+
+ private int[] getValueAsIntArrayWithDefault(final int tag, final String tagName, int[] defaultValue) throws IIOException {
+ Entry entry = currentIFD.getEntryById(tag);
+ if (entry == null) {
+ if (defaultValue == null) {
+ throw new IIOException("Missing TIFF tag " + tagName);
+ }
+
+ return defaultValue;
+ }
+
+ int[] value;
+
+ if (entry.valueCount() == 1) {
+ // For single entries, this will be a boxed type
+ value = new int[] {((Number) entry.getValue()).intValue()};
+ }
+ else if (entry.getValue() instanceof short[]) {
+ short[] shorts = (short[]) entry.getValue();
+ value = new int[shorts.length];
+
+ for (int i = 0, length = value.length; i < length; i++) {
+ value[i] = shorts[i];
+ }
+ }
+ else if (entry.getValue() instanceof int[]) {
+ value = (int[]) entry.getValue();
+ }
+ else {
+ throw new IIOException(String.format("Unsupported %s type: %s (%s)", tagName, entry.getTypeName(), entry.getValue().getClass()));
+ }
+
+ return value;
+ }
+
+ private Number getValueAsNumberWithDefault(final int tag, final String tagName, final Number defaultValue) throws IIOException {
+ Entry entry = currentIFD.getEntryById(tag);
+
+ if (entry == null) {
+ if (defaultValue != null) {
+ return defaultValue;
+ }
+
+ throw new IIOException("Missing TIFF tag: " + (tagName != null ? tagName : tag));
+ }
+
+ return (Number) entry.getValue();
+ }
+
+ private long getValueAsLongWithDefault(final int tag, final String tagName, final Long defaultValue) throws IIOException {
+ return getValueAsNumberWithDefault(tag, tagName, defaultValue).longValue();
+ }
+
+ private long getValueAsLongWithDefault(final int tag, final Long defaultValue) throws IIOException {
+ return getValueAsLongWithDefault(tag, null, defaultValue);
+ }
+
+ private long getValueAsLong(final int tag, String tagName) throws IIOException {
+ return getValueAsLongWithDefault(tag, tagName, null);
+ }
+
+ private int getValueAsIntWithDefault(final int tag, final String tagName, final Integer defaultValue) throws IIOException {
+ return getValueAsNumberWithDefault(tag, tagName, defaultValue).intValue();
+ }
+
+ private int getValueAsIntWithDefault(final int tag, Integer defaultValue) throws IIOException {
+ return getValueAsIntWithDefault(tag, null, defaultValue);
+ }
+
+ private int getValueAsInt(final int tag, String tagName) throws IIOException {
+ return getValueAsIntWithDefault(tag, tagName, null);
+ }
+
+ private int imageIndexToIFDNumber(int imageIndex) throws IOException {
+ readMetadata();
+
+ return thumbnailIFD != -1 && imageIndex >= thumbnailIFD ? imageIndex + 1 : imageIndex;
+ }
+
+ @Override
+ public int getWidth(int imageIndex) throws IOException {
+ readIFD(imageIndexToIFDNumber(imageIndex));
+
+ return getValueAsInt(TIFF.TAG_IMAGE_WIDTH, "ImageWidth");
+ }
+
+ @Override
+ public int getHeight(int imageIndex) throws IOException {
+ readIFD(imageIndexToIFDNumber(imageIndex));
+
+ return getValueAsInt(TIFF.TAG_IMAGE_HEIGHT, "ImageHeight");
+ }
+
+ @Override
+ public ImageTypeSpecifier getRawImageType(int imageIndex) throws IOException {
+ readIFD(imageIndexToIFDNumber(imageIndex));
+
+ // TODO: For compression 7/34892, get from JPEGImageReader delegate?
+
+ int photometricInterpretation = getValueAsInt(TIFF.TAG_PHOTOMETRIC_INTERPRETATION, "PhotometricInterpretation");
+ int samplesPerPixel = getValueAsInt(TIFF.TAG_SAMPLES_PER_PIXEL, "SamplesPerPixel");
+ long[] bitsPerSample = getValueAsLongArray(TIFF.TAG_BITS_PER_SAMPLE, "BitsPerSample", true);
+ int bitDepth = (int) bitsPerSample[0]; // Assume all equal!
+
+
+ switch (photometricInterpretation) {
+ case 1: // BlackIsZero
+ if (samplesPerPixel == 1 && bitDepth == 8) {
+ return ImageTypeSpecifier.createGrayscale(bitDepth, DataBuffer.TYPE_BYTE, false);
+ }
+ else if (samplesPerPixel == 1 && bitDepth > 8 && bitDepth <= 16) {
+ return ImageTypeSpecifier.createGrayscale(bitDepth, DataBuffer.TYPE_USHORT, false);
+ }
+
+ break;
+
+ case 2: // RGB
+ ColorSpace cs = ColorSpace.getInstance(ColorSpace.CS_sRGB);
+
+ if (samplesPerPixel == 3 && bitDepth == 8) {
+ return ImageTypeSpecifier.createInterleaved(cs, new int [] {0, 1, 2}, DataBuffer.TYPE_BYTE, false, false);
+ }
+ else if (samplesPerPixel == 3 && bitDepth > 8 && bitDepth <= 16) {
+ return ImageTypeSpecifier.createInterleaved(cs, new int [] {0, 1, 2}, DataBuffer.TYPE_USHORT, false, false);
+ }
+
+ break;
+
+ case DNG.PHOTOMETRIC_CFA: // CFA
+ // Return null as there really isn't a good Java ColorSpace for it...
+ if (samplesPerPixel == 1) {
+ return null;
+ }
+
+ break;
+
+ default:
+ throw new IIOException("Unsupported photometricInterpretation: " + photometricInterpretation);
+ }
+
+ throw new IIOException(String.format("Unsupported bitsPerSample/photometricInterpretation: %s/%s", Arrays.toString(bitsPerSample), photometricInterpretation));
+ }
+
+ @Override
+ public Iterator getImageTypes(int imageIndex) throws IOException {
+ readIFD(imageIndexToIFDNumber(imageIndex));
+
+ // TODO: For compression 7/34892, get from JPEGImageReader delegate?
+
+ ImageTypeSpecifier rawImageType = getRawImageType(imageIndex);
+
+ int photometricInterpretation = getValueAsInt(TIFF.TAG_PHOTOMETRIC_INTERPRETATION, "PhotometricInterpretation");
+ long[] bitsPerSample = getValueAsLongArray(TIFF.TAG_BITS_PER_SAMPLE, "BitsPerSample", true);
+ int bitDepth = (int) bitsPerSample[0]; // Assume all equal!
+
+
+ List specs = new ArrayList();
+
+ switch (photometricInterpretation) {
+ case 1: // BlackIsZero
+ case 2: // RGB
+ specs.add(rawImageType);
+ break;
+
+ case DNG.PHOTOMETRIC_CFA: // CFA
+ // Will be expanded to RGB
+ ColorSpace cs = ColorSpace.getInstance(ColorSpace.CS_LINEAR_RGB);
+
+ if (bitDepth == 8) {
+ specs.add(ImageTypeSpecifier.createInterleaved(cs, new int [] {0, 1, 2}, DataBuffer.TYPE_BYTE, false, false));
+ }
+ else if (bitDepth > 8 && bitDepth <= 16) {
+ specs.add(ImageTypeSpecifier.createInterleaved(cs, new int [] {0, 1, 2}, DataBuffer.TYPE_USHORT, false, false));
+ }
+
+ // TODO: Support reading as raw grayscale as well...
+ specs.add(rawImageType);
+
+ break;
+ default:
+ throw new IIOException("Unsupported photometricInterpretation: " + photometricInterpretation);
+ }
+
+ return specs.iterator();
+ }
+
+ @Override
+ public BufferedImage read(int imageIndex, ImageReadParam param) throws IOException {
+ readIFD(imageIndexToIFDNumber(imageIndex));
+
+ // TODO: Look at TIFF.TAG_SUBFILE_TYPE,
+ // TODO: If subFileType == 0, this is the main image,
+ // otherwise, it's a "reduced" (compressed or smaller dimensions) version
+ // (bit 1 is set also for the JPEG preview, that is too large for a thumbnail) or Page/Mask
+ // (not sure if these are supported for DNG)
+ // See: http://www.awaresystems.be/imaging/tiff/tifftags/newsubfiletype.html
+// FILETYPE_REDUCEDIMAGE = 1;
+// FILETYPE_PAGE = 2;
+// FILETYPE_MASK = 4;
+
+ // DNG spec:
+ // The highest-resolution and quality IFD should use NewSubFileType equal to 0. Reduced
+ // resolution (or quality) thumbnails or previews, if any, should use NewSubFileType equal to 1
+ // (for a primary preview) or 10001.H (for an alternate preview).
+
+
+ int width = getValueAsInt(TIFF.TAG_IMAGE_WIDTH, "ImageWidth");
+ int height = getValueAsInt(TIFF.TAG_IMAGE_HEIGHT, "ImageHeight");
+
+
+ // TODO: Support compressions 1 (None), 7 (JPEG), 8 (Deflate) and 34892 (Lossy JPEG, only for PhotometricInterpretation = 34892/Linear RAW)
+ int compression = getValueAsInt(TIFF.TAG_COMPRESSION, "Compression");
+ int photometricInterpretation = getValueAsInt(TIFF.TAG_PHOTOMETRIC_INTERPRETATION, "PhotometricInterpretation");
+
+// System.err.println("compression: " + compression);
+// System.err.println("photometricInterpretation: " + photometricInterpretation);
+
+ // NOTE: We handle strips as tiles of tileWidth == width by tileHeight == rowsPerStrip
+ // Strips are top/down, tiles are left/right, top/down
+ int stripTileWidth = width;
+ long rowsPerStrip = getValueAsLongWithDefault(TIFF.TAG_ROWS_PER_STRIP, (1l << 32) - 1);
+ int stripTileHeight = rowsPerStrip < height ? (int) rowsPerStrip : height;
+
+ long[] stripTileOffsets = getValueAsLongArray(TIFF.TAG_TILE_OFFSETS, "TileOffsets", false);
+ long[] stripTileByteCounts;
+
+ if (stripTileOffsets != null) {
+ stripTileByteCounts = getValueAsLongArray(TIFF.TAG_TILE_BYTE_COUNTS, "TileByteCounts", false);
+ if (stripTileByteCounts == null) {
+ processWarningOccurred("Missing TileByteCounts for tiled TIFF with compression: " + compression);
+ }
+
+ stripTileWidth = getValueAsInt(TIFF.TAG_TILE_WIDTH, "TileWidth");
+ stripTileHeight = getValueAsInt(TIFF.TAG_TILE_HEIGTH, "TileHeight");
+ }
+ else {
+ stripTileOffsets = getValueAsLongArray(TIFF.TAG_STRIP_OFFSETS, "StripOffsets", true);
+ stripTileByteCounts = getValueAsLongArray(TIFF.TAG_STRIP_BYTE_COUNTS, "StripByteCounts", false);
+ if (stripTileByteCounts == null) {
+ processWarningOccurred("Missing StripByteCounts for TIFF with compression: " + compression);
+ }
+
+ // NOTE: This is really against the spec, but libTiff seems to handle it. TIFF 6.0 says:
+ // "Do not use both strip- oriented and tile-oriented fields in the same TIFF file".
+ stripTileWidth = getValueAsIntWithDefault(TIFF.TAG_TILE_WIDTH, "TileWidth", stripTileWidth);
+ stripTileHeight = getValueAsIntWithDefault(TIFF.TAG_TILE_HEIGTH, "TileHeight", stripTileHeight);
+ }
+
+ int tilesAcross = (width + stripTileWidth - 1) / stripTileWidth;
+ int tilesDown = (height + stripTileHeight - 1) / stripTileHeight;
+
+ switch (compression) {
+ case 8:
+ // Deflate (handled below)
+ case 1:
+ // NONE
+ // TODO: Read as uncompressed TIFF (share code with TIFFImageReader?)
+ // TODO: Remove duped code!!
+ BufferedImage destination = getDestination(param, getImageTypes(imageIndex), width, height);
+ BufferedImage temp;
+
+ ImageTypeSpecifier rawType = getRawImageType(imageIndex);
+
+ if (photometricInterpretation == DNG.PHOTOMETRIC_CFA) {
+ // If CFA, read as single channel (gray), expand to RGB after reading
+ long[] bitsPerSample = getValueAsLongArray(TIFF.TAG_BITS_PER_SAMPLE, "BitsPerSample", true);
+ int bitDepth = (int) bitsPerSample[0]; // Assume all equal!
+
+ checkReadParamBandSettings(param, 3, destination.getSampleModel().getNumBands());
+
+ if (bitDepth == 8) {
+ rawType = ImageTypeSpecifier.createGrayscale(bitDepth, DataBuffer.TYPE_BYTE, false);
+ }
+ else if (bitDepth > 8 && bitDepth <= 16) {
+ rawType = ImageTypeSpecifier.createGrayscale(bitDepth, DataBuffer.TYPE_USHORT, false);
+ }
+
+ temp = rawType.createBufferedImage(destination.getWidth(), destination.getHeight());
+ }
+ else {
+ checkReadParamBandSettings(param, rawType.getNumBands(), destination.getSampleModel().getNumBands());
+ temp = destination;
+ }
+
+ final Rectangle srcRegion = new Rectangle();
+ final Rectangle dstRegion = new Rectangle();
+ computeRegions(param, width, height, destination, srcRegion, dstRegion);
+
+ int xSub = param != null ? param.getSourceXSubsampling() : 1;
+ int ySub = param != null ? param.getSourceYSubsampling() : 1;
+
+// WritableRaster destRaster = clipToRect(destination.getRaster(), dstRegion, param != null ? param.getDestinationBands() : null);
+ WritableRaster destRaster = clipToRect(temp.getRaster(), dstRegion, param != null ? param.getDestinationBands() : null);
+
+ final int interpretation = getValueAsInt(TIFF.TAG_PHOTOMETRIC_INTERPRETATION, "PhotometricInterpretation");
+ final int predictor = getValueAsIntWithDefault(TIFF.TAG_PREDICTOR, 1);
+ final int numBands = rawType.getNumBands();
+
+ WritableRaster rowRaster = rawType.getColorModel().createCompatibleWritableRaster(stripTileWidth, 1);
+ int row = 0;
+
+ // General uncompressed/compressed reading
+ for (int y = 0; y < tilesDown; y++) {
+ int col = 0;
+ int rowsInTile = Math.min(stripTileHeight, height - row);
+
+ for (int x = 0; x < tilesAcross; x++) {
+ int colsInTile = Math.min(stripTileWidth, width - col);
+ int i = y * tilesAcross + x;
+
+ imageInput.seek(stripTileOffsets[i]);
+
+ DataInput input;
+ if (compression == 1 /*&& interpretation != TIFFExtension.PHOTOMETRIC_YCBCR*/) {
+ // No need for transformation, fast forward
+ input = imageInput;
+ }
+ else {
+ InputStream adapter = stripTileByteCounts != null
+ ? IIOUtil.createStreamAdapter(imageInput, stripTileByteCounts[i])
+ : IIOUtil.createStreamAdapter(imageInput);
+
+ adapter = createDecompressorStream(compression, width, adapter);
+ adapter = createUnpredictorStream(predictor, width, numBands, getBitsPerSample(), adapter, imageInput.getByteOrder());
+
+ // According to the spec, short/long/etc should follow order of containing stream
+ input = imageInput.getByteOrder() == ByteOrder.BIG_ENDIAN
+ ? new DataInputStream(adapter)
+ : new LittleEndianDataInputStream(adapter);
+ }
+
+ // Read a full strip/tile
+ Raster clippedRow = clipRowToRect(rowRaster, srcRegion,
+ param != null ? param.getSourceBands() : null,
+ param != null ? param.getSourceXSubsampling() : 1);
+ readStripTileData(clippedRow, srcRegion, xSub, ySub, numBands, interpretation, destRaster, col, row, colsInTile, rowsInTile, input);
+
+ if (abortRequested()) {
+ break;
+ }
+
+ col += colsInTile;
+ }
+
+ processImageProgress(100f * row / height);
+
+ if (abortRequested()) {
+ processReadAborted();
+ break;
+ }
+
+ row += rowsInTile;
+ }
+
+ if (photometricInterpretation == DNG.PHOTOMETRIC_CFA) {
+ int layout = getValueAsIntWithDefault(DNG.TAG_CFA_LAYOUT, "CFALayout", 1);
+ int[] planeColor = getValueAsIntArrayWithDefault(DNG.TAG_CFA_PLANE_COLOR, "CFAPlaneColor", new int[]{0, 1, 2});
+ Entry cfaPatternDim = currentIFD.getEntryById(DNG.TAG_CFA_REPEAT_PATTERN_DIM);
+ if (cfaPatternDim == null || cfaPatternDim.valueCount() != 2 || !(cfaPatternDim.getValue() instanceof int[])) {
+ throw new IIOException("Missing/bad CFARepeatPatternDim tag for CFA DNG: " + cfaPatternDim);
+ }
+
+ int patternWidth = ((int[]) cfaPatternDim.getValue())[0];
+ int patternHeight = ((int[]) cfaPatternDim.getValue())[1];
+
+ Entry cfaPattern = currentIFD.getEntryById(DNG.TAG_CFA_PATTERN);
+ if (cfaPattern == null || cfaPattern.valueCount() != patternWidth * patternHeight || !(cfaPattern.getValue() instanceof byte[])) {
+ throw new IIOException("Missing/bad CFAPattern tag for CFA DNG: " + cfaPattern);
+ }
+
+ byte[] pattern = (byte[]) cfaPattern.getValue();
+
+ interpolateCFA2RGB(temp.getRaster(), destination.getRaster(), patternWidth, patternHeight, pattern, planeColor, layout);
+ }
+
+ return destination;
+ case 7:
+ case DNG.COMPRESSION_LOSSY_JPEG:
+ // JPEG
+ if (photometricInterpretation == 6 || photometricInterpretation == DNG.PHOTOMETRIC_LINEAR_RAW) {
+ // TODO: Merge strips/tiles into one image...
+ // "TIFF/EP_1 uses the TIFF/JPEG specification as described in
+ // Adobe Photoshop: TIFF Technical Notes (March 22, 2002).
+ // This method differs from the JPEG method described in the original TIFF 6.0 specification.
+ // In the method used within TIFF/EP_1, each image segment (tile or strip) contains a
+ // complete JPEG data stream that is valid according to the ISO JPEG standard (ISO/IEC 10918-1)."
+ for (int k = 0; k < stripTileOffsets.length; k++) {
+ imageInput.seek(stripTileOffsets[k]);
+
+ SubImageInputStream stream = new SubImageInputStream(imageInput, stripTileByteCounts[k]);
+ Iterator readers = ImageIO.getImageReaders(stream); // TODO: Prefer default JPEGImageReader
+ if (!readers.hasNext()) {
+ throw new IIOException("Could not find delegate reader for JPEG format!");
+ }
+
+ ImageReader reader = readers.next();
+
+ try {
+ if (param == null) {
+ param = reader.getDefaultReadParam();
+ }
+
+ reader.setInput(stream);
+ return reader.read(0, param);
+ }
+ finally {
+ reader.dispose(); // TODO: Don't dispose until this instance is disposed
+ }
+ }
+ }
+ else if (photometricInterpretation == DNG.PHOTOMETRIC_CFA) {
+ // Otherwise, this is lossless encoded linear or CFA
+ // TODO: Read JPEG Lossless!
+
+ return param != null ? param.getDestination() : null;
+ }
+ throw new IIOException("Unsupported photometricInterpretation for JPEG compressed DNG: " + photometricInterpretation);
+
+ default:
+ throw new IIOException("Unsupported compression for DNG: " + compression);
+ }
+ }
+
+ private void interpolateCFA2RGB(final Raster cfa, final WritableRaster rgb,
+ final int patternWidth, final int patternHeight, final byte[] pattern,
+ final int[] planeColor, final int layout) {
+ if (DEBUG) {
+ System.err.println("patternWidth: " + patternWidth);
+ System.err.println("patternHeight: " + patternHeight);
+ System.err.println("pattern: " + Arrays.toString(pattern));
+ System.err.println("planeColor: " + Arrays.toString(planeColor));
+ System.err.println("layout: " + layout);
+ }
+
+ // Expand CFA to RGB (for now, using nearest neighbour type interpolation)
+ // TODO: Properly interpolate (in some way).
+ // TODO: What interpolation works best, depends on pattern/sensor/scene... (see dcraw?)
+
+ byte[] cfaData = new byte[patternWidth * patternHeight];
+ byte[] rgbData = new byte[patternWidth * patternHeight * 3];
+
+ for (int y = 0; y < rgb.getHeight(); y += patternHeight) {
+ for (int x = 0; x < rgb.getWidth(); x += patternWidth) {
+ cfa.getDataElements(x, y, patternWidth, patternHeight, cfaData);
+
+ // TODO: Take layout into consideration (for now, assume default: 1)
+
+ for (int patternY = 0; patternY < patternHeight; patternY++) {
+ for (int patternX = 0; patternX < patternWidth; patternX++) {
+ int patternIndex = patternX + patternY * patternWidth;
+
+ int plane = pattern[patternIndex];
+
+ // TODO: This doesn't work properly for non-standard or non-2x2 patterns...
+ // How do we know how many times each plane/color should be repeated?
+ switch (planeColor[plane]) {
+ case 0: // Red
+ rgbData[0] = cfaData[patternIndex]; // *
+ rgbData[3] = cfaData[patternIndex];
+ rgbData[6] = cfaData[patternIndex];
+ rgbData[9] = cfaData[patternIndex];
+ break;
+ case 1: // Green
+ if (patternY == 0) {
+ rgbData[1] = cfaData[patternIndex];
+ rgbData[4] = cfaData[patternIndex]; // *
+ }
+ else {
+ rgbData[7] = cfaData[patternIndex]; // *
+ rgbData[10] = cfaData[patternIndex];
+ }
+ break;
+ case 2: // Blue
+ rgbData[2] = cfaData[patternIndex];
+ rgbData[5] = cfaData[patternIndex];
+ rgbData[8] = cfaData[patternIndex];
+ rgbData[11] = cfaData[patternIndex]; // *
+ break;
+ }
+
+ }
+ }
+
+ rgb.setDataElements(x, y, patternWidth, patternHeight, rgbData);
+ }
+ }
+ }
+
+ public static void main(String[] args) throws IOException {
+ DNGImageReader reader = new DNGImageReader(new DNGImageReaderSpi());
+
+ for (String arg : args) {
+ ImageInputStream stream = ImageIO.createImageInputStream(new File(arg));
+ reader.setInput(stream);
+
+ int numImages = reader.getNumImages(true);
+
+ for (int i = 0; i < numImages; i++) {
+ int numThumbnails = reader.getNumThumbnails(i);
+ for (int n = 0; n < numThumbnails; n++) {
+ showIt(reader.readThumbnail(i, n), arg + " image thumbnail" + n);
+ }
+
+ showIt(reader.read(i), arg + " image " + i);
+ }
+ }
+ }
+
+ ///////////////////////////////////////////////////////////////////////////////////////////////////////////////////
+ // DUPED CODE BELOW //// DUPED CODE BELOW //// DUPED CODE BELOW //// DUPED CODE BELOW //
+ ///////////////////////////////////////////////////////////////////////////////////////////////////////////////////
+
+
+ private int getBitsPerSample() throws IIOException {
+ long[] value = getValueAsLongArray(TIFF.TAG_BITS_PER_SAMPLE, "BitsPerSample", false);
+
+ if (value == null || value.length == 0) {
+ return 1;
+ }
+ else {
+ int bitsPerSample = (int) value[0];
+
+ for (int i = 1; i < value.length; i++) {
+ if (value[i] != bitsPerSample) {
+ throw new IIOException("Variable BitsPerSample not supported: " + Arrays.toString(value));
+ }
+ }
+
+ return bitsPerSample;
+ }
+ }
+
+ private Raster clipRowToRect(final Raster raster, final Rectangle rect, final int[] bands, final int xSub) {
+ if (rect.contains(raster.getMinX(), 0, raster.getWidth(), 1)
+ && xSub == 1
+ && bands == null /* TODO: Compare bands with that of raster */) {
+ return raster;
+ }
+
+ return raster.createChild(rect.x / xSub, 0, rect.width / xSub, 1, 0, 0, bands);
+ }
+
+ private WritableRaster clipToRect(final WritableRaster raster, final Rectangle rect, final int[] bands) {
+ if (rect.contains(raster.getMinX(), raster.getMinY(), raster.getWidth(), raster.getHeight())
+ && bands == null /* TODO: Compare bands with that of raster */) {
+ return raster;
+ }
+
+ return raster.createWritableChild(rect.x, rect.y, rect.width, rect.height, 0, 0, bands);
+ }
+
+ private void readStripTileData(final Raster tileRowRaster, final Rectangle srcRegion, final int xSub, final int ySub,
+ final int numBands, final int interpretation,
+ final WritableRaster raster, final int startCol, final int startRow,
+ final int colsInTile, final int rowsInTile, final DataInput input)
+ throws IOException {
+
+ switch (tileRowRaster.getTransferType()) {
+ case DataBuffer.TYPE_BYTE:
+ byte[] rowDataByte = ((DataBufferByte) tileRowRaster.getDataBuffer()).getData();
+
+ for (int row = startRow; row < startRow + rowsInTile; row++) {
+ if (row >= srcRegion.y + srcRegion.height) {
+ break; // We're done with this tile
+ }
+
+ input.readFully(rowDataByte);
+
+ if (row % ySub == 0 && row >= srcRegion.y) {
+ normalizeBlack(interpretation, rowDataByte);
+
+ // Subsample horizontal
+ if (xSub != 1) {
+ for (int x = srcRegion.x / xSub * numBands; x < ((srcRegion.x + srcRegion.width) / xSub) * numBands; x += numBands) {
+ for (int b = 0; b < numBands; b++) {
+ rowDataByte[x + b] = rowDataByte[x * xSub + b];
+ }
+ }
+ }
+
+ raster.setDataElements(startCol, (row - srcRegion.y) / ySub, tileRowRaster);
+ }
+ // Else skip data
+ }
+
+ break;
+ case DataBuffer.TYPE_USHORT:
+ short[] rowDataShort = ((DataBufferUShort) tileRowRaster.getDataBuffer()).getData();
+
+ for (int row = startRow; row < startRow + rowsInTile; row++) {
+ if (row >= srcRegion.y + srcRegion.height) {
+ break; // We're done with this tile
+ }
+
+ readFully(input, rowDataShort);
+
+ if (row >= srcRegion.y) {
+ normalizeBlack(interpretation, rowDataShort);
+
+ // Subsample horizontal
+ if (xSub != 1) {
+ for (int x = srcRegion.x / xSub * numBands; x < ((srcRegion.x + srcRegion.width) / xSub) * numBands; x += numBands) {
+ for (int b = 0; b < numBands; b++) {
+ rowDataShort[x + b] = rowDataShort[x * xSub + b];
+ }
+ }
+ }
+
+ raster.setDataElements(startCol, row - srcRegion.y, tileRowRaster);
+ }
+ // Else skip data
+ }
+
+ break;
+ case DataBuffer.TYPE_INT:
+ int[] rowDataInt = ((DataBufferInt) tileRowRaster.getDataBuffer()).getData();
+
+ for (int row = startRow; row < startRow + rowsInTile; row++) {
+ if (row >= srcRegion.y + srcRegion.height) {
+ break; // We're done with this tile
+ }
+
+ readFully(input, rowDataInt);
+
+ if (row >= srcRegion.y) {
+ normalizeBlack(interpretation, rowDataInt);
+
+ // Subsample horizontal
+ if (xSub != 1) {
+ for (int x = srcRegion.x / xSub * numBands; x < ((srcRegion.x + srcRegion.width) / xSub) * numBands; x += numBands) {
+ for (int b = 0; b < numBands; b++) {
+ rowDataInt[x + b] = rowDataInt[x * xSub + b];
+ }
+ }
+ }
+
+ raster.setDataElements(startCol, row - srcRegion.y, tileRowRaster);
+ }
+ // Else skip data
+ }
+
+ break;
+ }
+ }
+
+ // TODO: Candidate util method (with off/len + possibly byte order)
+ private void readFully(final DataInput input, final int[] rowDataInt) throws IOException {
+ if (input instanceof ImageInputStream) {
+ ImageInputStream imageInputStream = (ImageInputStream) input;
+ imageInputStream.readFully(rowDataInt, 0, rowDataInt.length);
+ }
+ else {
+ for (int k = 0; k < rowDataInt.length; k++) {
+ rowDataInt[k] = input.readInt();
+ }
+ }
+ }
+
+ // TODO: Candidate util method (with off/len + possibly byte order)
+ private void readFully(final DataInput input, final short[] rowDataShort) throws IOException {
+ if (input instanceof ImageInputStream) {
+ ImageInputStream imageInputStream = (ImageInputStream) input;
+ imageInputStream.readFully(rowDataShort, 0, rowDataShort.length);
+ }
+ else {
+ for (int k = 0; k < rowDataShort.length; k++) {
+ rowDataShort[k] = input.readShort();
+ }
+ }
+ }
+
+ private void normalizeBlack(int photometricInterpretation, short[] data) {
+ if (photometricInterpretation == 0 /*TIFFBaseline.PHOTOMETRIC_WHITE_IS_ZERO*/) {
+ // Inverse values
+ for (int i = 0; i < data.length; i++) {
+ data[i] = (short) (0xffff - data[i] & 0xffff);
+ }
+ }
+ }
+
+ private void normalizeBlack(int photometricInterpretation, int[] data) {
+ if (photometricInterpretation == 0 /*TIFFBaseline.PHOTOMETRIC_WHITE_IS_ZERO*/) {
+ // Inverse values
+ for (int i = 0; i < data.length; i++) {
+ data[i] = (0xffffffff - data[i]);
+ }
+ }
+ }
+
+ private void normalizeBlack(int photometricInterpretation, byte[] data) {
+ if (photometricInterpretation == 0/*TIFFBaseline.PHOTOMETRIC_WHITE_IS_ZERO*/) {
+ // Inverse values
+ for (int i = 0; i < data.length; i++) {
+ data[i] = (byte) (0xff - data[i] & 0xff);
+ }
+ }
+ }
+
+ private InputStream createDecompressorStream(final int compression, final int width, final InputStream stream) throws IOException {
+ switch (compression) {
+// case TIFFBaseline.COMPRESSION_NONE:
+ case 1:
+ return stream;
+// case TIFFBaseline.COMPRESSION_PACKBITS:
+// case TIFFExtension.COMPRESSION_ZLIB:
+ case 8:
+ // TIFFphotoshop.pdf (aka TIFF specification, supplement 2) says ZLIB (8) and DEFLATE (32946) algorithms are identical
+ return new InflaterInputStream(stream, new Inflater(), 1024);
+ default:
+ throw new IllegalArgumentException("Unsupported TIFF compression: " + compression);
+ }
+ }
+
+ private InputStream createUnpredictorStream(final int predictor, final int width, final int samplesPerPixel, final int bitsPerSample, final InputStream stream, final ByteOrder byteOrder) throws IOException {
+ switch (predictor) {
+// case TIFFBaseline.PREDICTOR_NONE:
+ case 1:
+ return stream;
+// case TIFFExtension.PREDICTOR_HORIZONTAL_DIFFERENCING:
+ case 2:
+// return new HorizontalDeDifferencingStream(stream, width, samplesPerPixel, bitsPerSample, byteOrder);
+// case TIFFExtension.PREDICTOR_HORIZONTAL_FLOATINGPOINT:
+ case 3:
+ throw new IIOException("Unsupported TIFF Predictor value: " + predictor);
+ default:
+ throw new IIOException("Unknown TIFF Predictor value: " + predictor);
+ }
+ }
+}
diff --git a/imageio/imageio-dng/src/main/java/com/twelvemonkeys/imageio/plugins/dng/DNGImageReaderSpi.java b/imageio/imageio-dng/src/main/java/com/twelvemonkeys/imageio/plugins/dng/DNGImageReaderSpi.java
new file mode 100644
index 00000000..fba4ac76
--- /dev/null
+++ b/imageio/imageio-dng/src/main/java/com/twelvemonkeys/imageio/plugins/dng/DNGImageReaderSpi.java
@@ -0,0 +1,125 @@
+/*
+ * Copyright (c) 2014, Harald Kuhr
+ * All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions are met:
+ * * Redistributions of source code must retain the above copyright
+ * notice, this list of conditions and the following disclaimer.
+ * * Redistributions in binary form must reproduce the above copyright
+ * notice, this list of conditions and the following disclaimer in the
+ * documentation and/or other materials provided with the distribution.
+ * * Neither the name "TwelveMonkeys" nor the
+ * names of its contributors may be used to endorse or promote products
+ * derived from this software without specific prior written permission.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+ * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+ * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
+ * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR
+ * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL,
+ * EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
+ * PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR
+ * PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF
+ * LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING
+ * NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
+ * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+
+package com.twelvemonkeys.imageio.plugins.dng;
+
+import com.twelvemonkeys.imageio.metadata.exif.TIFF;
+import com.twelvemonkeys.imageio.spi.ProviderInfo;
+import com.twelvemonkeys.imageio.util.IIOUtil;
+
+import javax.imageio.ImageReader;
+import javax.imageio.spi.ImageReaderSpi;
+import javax.imageio.stream.ImageInputStream;
+import java.io.IOException;
+import java.nio.ByteOrder;
+import java.util.Locale;
+
+/**
+ * CR2ImageReaderSpi
+ *
+ * @author Harald Kuhr
+ * @author last modified by $Author: haraldk$
+ * @version $Id: CR2ImageReaderSpi.java,v 1.0 07.04.14 21:26 haraldk Exp$
+ */
+public final class DNGImageReaderSpi extends ImageReaderSpi {
+ public DNGImageReaderSpi() {
+ this(IIOUtil.getProviderInfo(DNGImageReaderSpi.class));
+ }
+
+ private DNGImageReaderSpi(final ProviderInfo pProviderInfo) {
+ super(
+ pProviderInfo.getVendorName(),
+ pProviderInfo.getVersion(),
+ new String[]{"dng", "NDG"},
+ new String[]{"dng"},
+ new String[]{
+ "image/x-adobe-dng", // TODO: Look up
+ },
+ "com.twelvemonkeys.imageio.plugins.dng.DNGImageReader",
+ new Class[] {ImageInputStream.class},
+ null,
+ true, null, null, null, null,
+ true,
+ null, null,
+ null, null
+ );
+ }
+
+ public boolean canDecodeInput(final Object pSource) throws IOException {
+ if (!(pSource instanceof ImageInputStream)) {
+ return false;
+ }
+
+ ImageInputStream stream = (ImageInputStream) pSource;
+
+ stream.mark();
+ try {
+ byte[] bom = new byte[2];
+ stream.readFully(bom);
+
+ ByteOrder originalOrder = stream.getByteOrder();
+
+ try {
+ if (bom[0] == 'I' && bom[1] == 'I') {
+ stream.setByteOrder(ByteOrder.LITTLE_ENDIAN);
+ }
+ else if (bom[0] == 'M' && bom[1] == 'M') {
+ stream.setByteOrder(ByteOrder.BIG_ENDIAN);
+ }
+ else {
+ return false;
+ }
+
+ int tiffMagic = stream.readUnsignedShort();
+ if (tiffMagic != TIFF.TIFF_MAGIC) {
+ return false;
+ }
+
+ // TODO: This is not different from a normal TIFF...
+
+ return true;
+ }
+ finally {
+ stream.setByteOrder(originalOrder);
+ }
+ }
+ finally {
+ stream.reset();
+ }
+ }
+
+ @Override
+ public ImageReader createReaderInstance(Object extension) throws IOException {
+ return new DNGImageReader(this);
+ }
+
+ @Override
+ public String getDescription(Locale locale) {
+ return "Adobe Digital Negative (DNG) format Reader";
+ }
+}
diff --git a/imageio/imageio-dng/src/test/java/com/twelvemonkeys/imageio/plugins/dng/DNGImageReaderTest.java b/imageio/imageio-dng/src/test/java/com/twelvemonkeys/imageio/plugins/dng/DNGImageReaderTest.java
new file mode 100644
index 00000000..a4ee366b
--- /dev/null
+++ b/imageio/imageio-dng/src/test/java/com/twelvemonkeys/imageio/plugins/dng/DNGImageReaderTest.java
@@ -0,0 +1,109 @@
+/*
+ * Copyright (c) 2014, Harald Kuhr
+ * All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions are met:
+ * * Redistributions of source code must retain the above copyright
+ * notice, this list of conditions and the following disclaimer.
+ * * Redistributions in binary form must reproduce the above copyright
+ * notice, this list of conditions and the following disclaimer in the
+ * documentation and/or other materials provided with the distribution.
+ * * Neither the name "TwelveMonkeys" nor the
+ * names of its contributors may be used to endorse or promote products
+ * derived from this software without specific prior written permission.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+ * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+ * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
+ * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR
+ * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL,
+ * EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
+ * PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR
+ * PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF
+ * LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING
+ * NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
+ * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+
+package com.twelvemonkeys.imageio.plugins.dng;
+
+import com.twelvemonkeys.imageio.util.ImageReaderAbstractTestCase;
+import org.junit.Ignore;
+import org.junit.Test;
+
+import javax.imageio.ImageReader;
+import javax.imageio.spi.ImageReaderSpi;
+import java.awt.*;
+import java.io.IOException;
+import java.util.Arrays;
+import java.util.List;
+
+/**
+ * CR2ImageReaderTest
+ *
+ * @author Harald Kuhr
+ * @author last modified by $Author: haraldk$
+ * @version $Id: CR2ImageReaderTest.java,v 1.0 07.04.14 21:52 haraldk Exp$
+ */
+public class DNGImageReaderTest extends ImageReaderAbstractTestCase {
+ @Override
+ protected List getTestData() {
+ return Arrays.asList(
+ new TestData(getClassLoaderResource("/dng/L1004220.DNG"),
+ // Uncompressed RGB (thumbnail), Ucompressed CFA
+// new Dimension(320, 216),
+ new Dimension(5216, 3472)),
+ new TestData(getClassLoaderResource("/dng/IMG_2224.dng"),
+ // Uncompressed RGB (thumbnail), JPEG Lossless CFA, JPEG DCT YCbCr
+// new Dimension(256, 171),
+ new Dimension(3516, 2328),
+ new Dimension(1024, 683))
+// new TestData(getClassLoaderResource("/dng/test.dng"), new Dimension(2, 2)) // Only JPEG Lossless CFA
+ );
+ }
+
+ @Override
+ protected ImageReaderSpi createProvider() {
+ return new DNGImageReaderSpi();
+ }
+
+ @Override
+ protected ImageReader createReader() {
+ return new com.twelvemonkeys.imageio.plugins.dng.DNGImageReader(createProvider());
+ }
+
+ @Override
+ protected Class getReaderClass() {
+ return com.twelvemonkeys.imageio.plugins.dng.DNGImageReader.class;
+ }
+
+ @Override
+ protected List getFormatNames() {
+ return Arrays.asList("dng");
+ }
+
+ @Override
+ protected List getSuffixes() {
+ return Arrays.asList("dng");
+ }
+
+ @Override
+ protected List getMIMETypes() {
+ return Arrays.asList("image/x-dng");
+ }
+
+ @Test
+ @Ignore("Known issue: Subsampled reading not supported")
+ @Override
+ public void testReadWithSubsampleParamPixels() throws IOException {
+ super.testReadWithSubsampleParamPixels();
+ }
+
+ @Test
+ @Ignore("Known issue: Source region reading not supported")
+ @Override
+ public void testReadWithSourceRegionParamEqualImage() throws IOException {
+ super.testReadWithSourceRegionParamEqualImage();
+ }
+}
diff --git a/imageio/imageio-dng/src/test/resources/dng/IMG_2224.dng b/imageio/imageio-dng/src/test/resources/dng/IMG_2224.dng
new file mode 100755
index 00000000..e732e009
Binary files /dev/null and b/imageio/imageio-dng/src/test/resources/dng/IMG_2224.dng differ
diff --git a/imageio/imageio-dng/src/test/resources/dng/L1004220.DNG b/imageio/imageio-dng/src/test/resources/dng/L1004220.DNG
new file mode 100644
index 00000000..530a09b6
Binary files /dev/null and b/imageio/imageio-dng/src/test/resources/dng/L1004220.DNG differ
diff --git a/imageio/imageio-dng/src/test/resources/dng/L1004235.DNG b/imageio/imageio-dng/src/test/resources/dng/L1004235.DNG
new file mode 100644
index 00000000..d989789e
Binary files /dev/null and b/imageio/imageio-dng/src/test/resources/dng/L1004235.DNG differ
diff --git a/imageio/imageio-dng/src/test/resources/dng/L1004432.DNG b/imageio/imageio-dng/src/test/resources/dng/L1004432.DNG
new file mode 100644
index 00000000..6a97863e
Binary files /dev/null and b/imageio/imageio-dng/src/test/resources/dng/L1004432.DNG differ
diff --git a/imageio/imageio-dng/src/test/resources/dng/test.dng b/imageio/imageio-dng/src/test/resources/dng/test.dng
new file mode 100644
index 00000000..de6711b0
Binary files /dev/null and b/imageio/imageio-dng/src/test/resources/dng/test.dng differ