diff --git a/imageio/imageio-cr2/pom.xml b/imageio/imageio-cr2/pom.xml
new file mode 100644
index 00000000..6116f59a
--- /dev/null
+++ b/imageio/imageio-cr2/pom.xml
@@ -0,0 +1,65 @@
+
+
+
+
+
+ imageio
+ com.twelvemonkeys.imageio
+ 3.1-SNAPSHOT
+
+ 4.0.0
+
+ imageio-cr2
+ TwelveMonkeys :: ImageIO :: CR2 plugin
+ ImageIO plugin for Canon RAW (CR2) format.
+
+
+
+ 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-cr2/src/main/java/com/twelvemonkeys/imageio/plugins/cr2/CR2ImageReader.java b/imageio/imageio-cr2/src/main/java/com/twelvemonkeys/imageio/plugins/cr2/CR2ImageReader.java
new file mode 100644
index 00000000..f0a3fd92
--- /dev/null
+++ b/imageio/imageio-cr2/src/main/java/com/twelvemonkeys/imageio/plugins/cr2/CR2ImageReader.java
@@ -0,0 +1,487 @@
+/*
+ * 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.cr2;
+
+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.metadata.jpeg.JPEGSegment;
+import com.twelvemonkeys.imageio.metadata.jpeg.JPEGSegmentUtil;
+import com.twelvemonkeys.imageio.plugins.jpeg.LosslessJPEGDecoder;
+import com.twelvemonkeys.imageio.stream.SubImageInputStream;
+
+import javax.imageio.IIOException;
+import javax.imageio.ImageIO;
+import javax.imageio.ImageReadParam;
+import javax.imageio.ImageTypeSpecifier;
+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.File;
+import java.io.IOException;
+import java.nio.ByteBuffer;
+import java.util.Arrays;
+import java.util.Iterator;
+import java.util.List;
+
+/**
+ * Canon CR2 RAW ImageReader.
+ *
+ * Acknowledgement:
+ * This ImageReader is based on the excellent work of Laurent Clevy, and would probably not exist without it.
+ *
+ * @see Understanding What is stored in a Canon RAW .CR2 file, How and Why
+ * @author Harald Kuhr
+ * @author last modified by $Author: haraldk$
+ * @version $Id: CR2ImageReader.java,v 1.0 07.04.14 21:31 haraldk Exp$
+ */
+public final class CR2ImageReader extends ImageReaderBase {
+ // See http://lclevy.free.fr/dng/
+ // TODO: Avoid duped code from 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...
+
+ final static boolean DEBUG = true; //"true".equalsIgnoreCase(System.getProperty("com.twelvemonkeys.imageio.plugins.dng.debug"));
+
+ // Thumbnail is in IFD1 (2nd entry)
+ private final static int THUMBNAIL_IFD = 1;
+
+ private CompoundDirectory IFDs;
+ private Directory currentIFD;
+
+ CR2ImageReader(final ImageReaderSpi provider) {
+ super(provider);
+ }
+
+ @Override
+ protected void resetMembers() {
+ IFDs = null;
+ currentIFD = null;
+ }
+
+ private void readMetadata() throws IOException {
+ if (imageInput == null) {
+ throw new IllegalStateException("input not set");
+ }
+
+ if (IFDs == null) {
+ // We'll validate the TIFF structure later, for now just see if we have 'CR'0x0200 at the right place
+
+ imageInput.skipBytes(8); // TIFF byte order mark + magic + IFD0 offset
+
+ if (imageInput.readByte() != 'C' || imageInput.readByte() != 'R') {
+ throw new IIOException("Not a valid CR2 structure: Missing CR magic ('CR')");
+ }
+
+ int version = imageInput.readUnsignedByte();
+ int revision = imageInput.readUnsignedByte();
+
+ // TODO: Choke on anything but 2.0? All sample data from 400D until 5D mk III has 2.0 version...
+
+ imageInput.seek(0);
+
+ IFDs = (CompoundDirectory) new EXIFReader().read(imageInput); // NOTE: Sets byte order as a side effect
+
+ if (DEBUG) {
+ System.err.println("Byte order: " + imageInput.getByteOrder());
+ System.err.println("Version: " + version + "." + revision);
+ 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));
+ }
+ }
+ }
+ }
+
+ private void readIFD(final int ifdIndex) throws IOException {
+ readMetadata();
+
+ if (ifdIndex < 0) {
+ throw new IndexOutOfBoundsException("index < minIndex");
+ }
+ else if (ifdIndex >= IFDs.directoryCount()) {
+ throw new IndexOutOfBoundsException("index >= numImages (" + ifdIndex + " >= " + IFDs.directoryCount() + ")");
+ }
+
+ currentIFD = IFDs.getDirectory(ifdIndex);
+ }
+
+ @Override
+ public int getNumImages(final boolean allowSearch) throws IOException {
+ readMetadata();
+
+ // This validation is maybe a little too restrictive, but ok for now
+ if (IFDs.directoryCount() != 4) {
+ throw new IIOException("Unexpected number of IFDs in CR2: " + IFDs.directoryCount());
+ }
+
+ return IFDs.directoryCount() - 1;
+ }
+
+ @Override
+ public int getNumThumbnails(int imageIndex) throws IOException {
+ readMetadata();
+ checkBounds(imageIndex);
+
+ return imageIndex == 0 ? 1 : 0;
+ }
+
+ @Override
+ public boolean readerSupportsThumbnails() {
+ return true;
+ }
+
+ @Override
+ public int getThumbnailWidth(int imageIndex, int thumbnailIndex) throws IOException {
+ readMetadata();
+ checkBounds(imageIndex);
+
+ // TODO: Need to get from JPEGImageReader (no ImageWidth tag), this is an ok (but lame) implementation for now
+ return super.getThumbnailWidth(imageIndex, thumbnailIndex);
+ }
+
+ @Override
+ public int getThumbnailHeight(int imageIndex, int thumbnailIndex) throws IOException {
+ readMetadata();
+ checkBounds(imageIndex);
+
+ // TODO: Need to get from JPEGImageReader (no ImageHeight tag), this is an ok (but lame) implementation for now
+ return super.getThumbnailHeight(imageIndex, thumbnailIndex);
+ }
+
+ @Override
+ public BufferedImage readThumbnail(int imageIndex, int thumbnailIndex) throws IOException {
+ readIFD(THUMBNAIL_IFD);
+
+ if (imageIndex != 0) {
+ throw new IndexOutOfBoundsException("No thumbnail for imageIndex: " + imageIndex);
+ }
+ if (thumbnailIndex != 0) {
+ throw new IndexOutOfBoundsException("thumbnailIndex out of bounds: " + thumbnailIndex);
+ }
+
+ // This IFD (IFD1) lacks Compression 6 (old JPEG), but has the relevant tags for a JPEG/EXIF thumbnail
+ int jpegOffset = getValueAsInt(TIFF.TAG_JPEG_INTERCHANGE_FORMAT, "JPEGInterchangeFormat");
+ int jpegLength = getValueAsInt(TIFF.TAG_JPEG_INTERCHANGE_FORMAT_LENGTH, "JPEGInterchangeFormatLength");
+
+ imageInput.seek(jpegOffset);
+
+ // TODO: Consider using cached JPEGImageReader directly
+ return ImageIO.read(new SubImageInputStream(imageInput, jpegLength));
+ }
+
+ 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 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 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) {
+ return imageIndex >= THUMBNAIL_IFD ? 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 Iterator getImageTypes(int imageIndex) throws IOException {
+ readIFD(imageIndexToIFDNumber(imageIndex));
+
+ // TODO: For IFD0, get from JPEGImageReader delagate
+
+// return Arrays.asList(ImageTypeSpecifier.createFromBufferedImageType(BufferedImage.TYPE_3BYTE_BGR)).iterator();
+ Entry bitsPerSample = currentIFD.getEntryById(TIFF.TAG_BITS_PER_SAMPLE);
+ if (bitsPerSample == null) {
+ // TODO: FixME!
+ return Arrays.asList(ImageTypeSpecifier.createFromBufferedImageType(BufferedImage.TYPE_3BYTE_BGR)).iterator();
+ }
+
+ // For IFD1, create linear RGB, but we don't really know...
+ int bitDepth = ((int[]) bitsPerSample.getValue())[0]; // Assume all equal!
+ if (bitDepth == 8) {
+ return Arrays.asList(ImageTypeSpecifier.createInterleaved(ColorSpace.getInstance(ColorSpace.CS_LINEAR_RGB), new int [] {0, 1, 2}, DataBuffer.TYPE_BYTE, false, false)).iterator();
+ }
+ else if (bitDepth == 16) {
+ return Arrays.asList(ImageTypeSpecifier.createInterleaved(ColorSpace.getInstance(ColorSpace.CS_LINEAR_RGB), new int [] {0, 1, 2}, DataBuffer.TYPE_USHORT, false, false)).iterator();
+ }
+
+ throw new IIOException("Unsupported bit depth: " + bitDepth);
+ }
+
+ @Override
+ public BufferedImage read(int imageIndex, ImageReadParam param) throws IOException {
+ readIFD(imageIndexToIFDNumber(imageIndex));
+
+ if (imageIndex == 0) {
+ // This one says Compression 6 (old JPEG) and contains normal JPEG data at StripOffsets (but has no JPEGInterchangeFormat tag)
+ int compression = getValueAsInt(TIFF.TAG_COMPRESSION, "Compression");
+ if (compression != 6) {
+ throw new IIOException("Unknown TIFF compression for CR2 IFD0: " + compression);
+ }
+
+ int stripOffsets = getValueAsInt(TIFF.TAG_STRIP_OFFSETS, "StripOffsets");
+ int stripByteCounts = getValueAsInt(TIFF.TAG_STRIP_BYTE_COUNTS, "StripByteCounts");
+ imageInput.seek(stripOffsets);
+
+ List segments = JPEGSegmentUtil.readSegments(new SubImageInputStream(imageInput, stripByteCounts), JPEGSegmentUtil.ALL_SEGMENTS);
+ System.err.println("segments: " + segments);
+
+ imageInput.seek(stripOffsets);
+ BufferedImage image = ImageIO.read(new SubImageInputStream(imageInput, stripByteCounts));
+ System.err.println("image: " + image);
+ return image;
+ }
+
+ if (imageIndex == 1) {
+ // This one is semi-ok, for older cameras is says Compression 6 (old JPEG), for 7D it says Compression 1 (None)
+ // We'll just ignore the compression and assume it's 1 (None).
+ // TODO: Probably a good idea to verify that we have no other compression than 6/1
+ // TODO: Consider just masking out this image, as it's not of much use...
+
+ int width = getWidth(imageIndex);
+ int height = getHeight(imageIndex);
+
+ BufferedImage destination = getDestination(param, getImageTypes(imageIndex), width, height);
+ WritableRaster raster;
+
+ int dataType = destination.getSampleModel().getDataType();
+ if (dataType == DataBuffer.TYPE_BYTE) {
+ // Emulate raw type (as dest, but RGB instead of BGR)
+ raster = Raster.createInterleavedRaster(DataBuffer.TYPE_BYTE, width, 1, width * 3, 3, new int[] {0, 1, 2}, null);
+ }
+ else if (dataType == DataBuffer.TYPE_USHORT) {
+ // Emulate raw type (as dest, but RGB instead of BGR)
+ raster = Raster.createInterleavedRaster(DataBuffer.TYPE_USHORT, width, 1, width * 3, 3, new int[] {0, 1, 2}, null);
+ }
+ else {
+ throw new AssertionError();
+ }
+
+ DataBuffer dataBuffer = raster.getDataBuffer();
+
+ SampleModel destSampleModel = destination.getSampleModel();
+ DataBuffer destBuffer = destination.getRaster().getDataBuffer();
+ SampleModel srcSampleModel = raster.getSampleModel();
+
+ if (destBuffer.getSize() != getValueAsInt(TIFF.TAG_STRIP_BYTE_COUNTS, "StripByteCounts")) {
+ System.err.println("dataBuffer: " + dataBuffer.getSize());
+ System.err.println("StripByteCounts: " + getValueAsInt(TIFF.TAG_STRIP_BYTE_COUNTS, "StripByteCounts"));
+ }
+
+ int stripOffsets = getValueAsInt(TIFF.TAG_STRIP_OFFSETS, "StripOffsets");
+ imageInput.seek(stripOffsets);
+
+ Object data = null;
+ for (int y = 0; y < height; y++) {
+ if (dataType == DataBuffer.TYPE_BYTE) {
+ imageInput.readFully(((DataBufferByte) dataBuffer).getData());
+ }
+ else {
+ imageInput.readFully(((DataBufferUShort) dataBuffer).getData(), 0, dataBuffer.getSize());
+ }
+
+ data = srcSampleModel.getDataElements(0, 0, width, 1, data, dataBuffer);
+ destSampleModel.setDataElements(0, y, width, 1, data, destBuffer);
+ }
+
+ // TODO: This seems to work as crop values, if so correct w/h from getImageWidth/Height??
+ Entry unknown = currentIFD.getEntryById(50908);
+ if (unknown != null) {
+ Graphics2D g = destination.createGraphics();
+ try {
+ long[] values = (long[]) unknown.getValue();
+
+ g.setPaint(new Color(63, 223, 88, 128));
+// g.drawRect((int) values[2], (int) values[3], (int) values[0], (int) values[1]);
+// g.fillRect((int) values[2], (int) values[3], (int) values[0], (int) values[1]);
+ g.fillRect(0, 0, width, (int) values[3]);
+ g.fillRect(0, (int) values[3], (int) values[2], (int) (values[1] + values[3]));
+ g.fillRect((int) (values[2] + values[0]), (int) values[3], (int) values[2], (int) (values[1] + values[3]));
+ g.fillRect(0, (int) (values[3] + values[1]), width, height - (int) (values[3] + values[1]));
+ }
+ finally {
+ g.dispose();
+ }
+ }
+
+ return destination;
+ }
+
+ if (imageIndex == 2) {
+ // TODO: This is the real RAW data. It's supposed to be lossless JPEG encoded, single channel,
+ // and has to be interpolated to become full 3 channel data from the Bayer CFA array,
+ // then further processed with white balance correction, black subtraction and color scaling.
+ // At least. ;-)
+
+ // We should probably just mask this image out, until we can read it
+
+ int stripOffsets = getValueAsInt(TIFF.TAG_STRIP_OFFSETS, "StripOffsets");
+ int stripByteCounts = getValueAsInt(TIFF.TAG_STRIP_BYTE_COUNTS, "StripByteCounts");
+ long[] slices = getValueAsLongArray(50752, "Slices", true);
+
+ // Format of this array, is slices[0] = N, slices[1] = slice0.width ... slices[N + 1] = sliceN.width
+ if (slices[0] != slices.length - 2) {
+ throw new IIOException("Unexpected slices array: " + Arrays.toString(slices));
+ }
+ // TODO: We really have multiple slices...
+
+ // TODO: Get correct dimensions (sensor size?)
+ int width = getWidth(0);
+ int height = getHeight(0);
+
+ imageInput.seek(stripOffsets);
+ byte[] data = new LosslessJPEGDecoder().decompress(new SubImageInputStream(imageInput, stripByteCounts), null);
+
+ // TODO: We really have 2 bytes/sample
+ short[] data2 = new short[data.length / 2];
+ ByteBuffer wrap = ByteBuffer.wrap(data);
+ wrap.asShortBuffer().get(data2);
+
+ System.err.println("data.length: " + data2.length);
+ System.err.println("width x height: " + width * height);
+
+ DataBuffer dataBuffer = new DataBufferUShort(data2, data2.length);
+ WritableRaster raster = Raster.createInterleavedRaster(dataBuffer, width, height, width, 1, new int[] {0}, null);
+ ColorModel cm = new ComponentColorModel(ColorSpace.getInstance(ColorSpace.CS_GRAY), false, false, Transparency.OPAQUE, raster.getTransferType());
+ BufferedImage image = new BufferedImage(cm, raster, cm.isAlphaPremultiplied(), null);
+
+ System.err.println("image: " + image);
+
+ return image;
+ }
+
+ return null;
+ }
+
+ public static void main(String[] args) throws IOException {
+ CR2ImageReader reader = new CR2ImageReader(new CR2ImageReaderSpi());
+
+ 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);
+ }
+ }
+ }
+}
diff --git a/imageio/imageio-cr2/src/main/java/com/twelvemonkeys/imageio/plugins/cr2/CR2ImageReaderSpi.java b/imageio/imageio-cr2/src/main/java/com/twelvemonkeys/imageio/plugins/cr2/CR2ImageReaderSpi.java
new file mode 100644
index 00000000..3f57f441
--- /dev/null
+++ b/imageio/imageio-cr2/src/main/java/com/twelvemonkeys/imageio/plugins/cr2/CR2ImageReaderSpi.java
@@ -0,0 +1,131 @@
+/*
+ * 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.cr2;
+
+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 CR2ImageReaderSpi extends ImageReaderSpi {
+ public CR2ImageReaderSpi() {
+ this(IIOUtil.getProviderInfo(CR2ImageReaderSpi.class));
+ }
+
+ private CR2ImageReaderSpi(final ProviderInfo pProviderInfo) {
+ super(
+ pProviderInfo.getVendorName(),
+ pProviderInfo.getVersion(),
+ new String[]{"cr2", "CR2"},
+ new String[]{"cr2"},
+ new String[]{
+ "image/x-canon-raw", // TODO: Look up
+ },
+ "com.twelvemonkeys.imageio.plugins.cr2.CR2ImageReader",
+ 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;
+ }
+
+ stream.skipBytes(4); // TIFF IFD0 offset
+
+ if (stream.readByte() != 'C' || stream.readByte() != 'R') {
+ return false;
+ }
+
+ // Version 2.0
+ return stream.readUnsignedByte() == 2 && stream.readUnsignedByte() == 0;
+ }
+ finally {
+ stream.setByteOrder(originalOrder);
+ }
+ }
+ finally {
+ stream.reset();
+ }
+ }
+
+ @Override
+ public ImageReader createReaderInstance(Object extension) throws IOException {
+ return new CR2ImageReader(this);
+ }
+
+ @Override
+ public String getDescription(Locale locale) {
+ return "Canon RAW (CR2) format Reader";
+ }
+
+}
diff --git a/imageio/imageio-cr2/src/main/resources/META-INF/services/javax.imageio.spi.ImageReaderSpi b/imageio/imageio-cr2/src/main/resources/META-INF/services/javax.imageio.spi.ImageReaderSpi
new file mode 100644
index 00000000..3c963c9a
--- /dev/null
+++ b/imageio/imageio-cr2/src/main/resources/META-INF/services/javax.imageio.spi.ImageReaderSpi
@@ -0,0 +1 @@
+com.twelvemonkeys.imageio.plugins.cr2.CR2ImageReaderSpi
diff --git a/imageio/imageio-cr2/src/test/java/com/twelvemonkeys/imageio/plugins/cr2/CR2ImageReaderTest.java b/imageio/imageio-cr2/src/test/java/com/twelvemonkeys/imageio/plugins/cr2/CR2ImageReaderTest.java
new file mode 100644
index 00000000..e6431642
--- /dev/null
+++ b/imageio/imageio-cr2/src/test/java/com/twelvemonkeys/imageio/plugins/cr2/CR2ImageReaderTest.java
@@ -0,0 +1,111 @@
+/*
+ * 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.cr2;
+
+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$
+ */
+@Ignore
+public class CR2ImageReaderTest extends ImageReaderAbstractTestCase {
+ @Override
+ protected List getTestData() {
+ return Arrays.asList(
+ new TestData(
+ getClassLoaderResource("/cr2/IMG_3483.CR2"), // Canon EOS 400D
+ new Dimension(3888, 2592) // This is what the TIFF structure says...
+ /* from http://lclevy.free.fr/dng/:
+ new Dimension(3888 / 4, 2592 / 4), // The image is supposed to be 1/4 of the size in the TIFF tags...
+ new Dimension(-1, -1), // "small version", no size information, only JPEG data (EXIF thumbnail)
+ new Dimension(384, 256), // according to http://lclevy.free.fr/dng/ this is not compressed, but TIFF structure says JPEG... Perhaps JPEG lossless?
+ new Dimension(3888, 2592) // Full size image, no size information
+ */
+ )
+ // TODO: EOS 7D sample
+ );
+ }
+
+ @Override
+ protected ImageReaderSpi createProvider() {
+ return new CR2ImageReaderSpi();
+ }
+
+ @Override
+ protected ImageReader createReader() {
+ return new CR2ImageReader(createProvider());
+ }
+
+ @Override
+ protected Class getReaderClass() {
+ return CR2ImageReader.class;
+ }
+
+ @Override
+ protected List getFormatNames() {
+ return Arrays.asList("cr2");
+ }
+
+ @Override
+ protected List getSuffixes() {
+ return Arrays.asList("cr2");
+ }
+
+ @Override
+ protected List getMIMETypes() {
+ return Arrays.asList("image/x-canon-raw");
+ }
+
+ @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-cr2/src/test/resources/cr2/IMG_3483.CR2 b/imageio/imageio-cr2/src/test/resources/cr2/IMG_3483.CR2
new file mode 100644
index 00000000..392f617f
Binary files /dev/null and b/imageio/imageio-cr2/src/test/resources/cr2/IMG_3483.CR2 differ
diff --git a/imageio/imageio-cr2/src/test/resources/cr2/IMG_4841.CR2 b/imageio/imageio-cr2/src/test/resources/cr2/IMG_4841.CR2
new file mode 100644
index 00000000..2504be23
Binary files /dev/null and b/imageio/imageio-cr2/src/test/resources/cr2/IMG_4841.CR2 differ
diff --git a/imageio/imageio-cr2/src/test/resources/cr2/IMG_6933.CR2 b/imageio/imageio-cr2/src/test/resources/cr2/IMG_6933.CR2
new file mode 100644
index 00000000..beb5911d
Binary files /dev/null and b/imageio/imageio-cr2/src/test/resources/cr2/IMG_6933.CR2 differ
diff --git a/imageio/imageio-cr2/src/test/resources/cr2/readme.txt b/imageio/imageio-cr2/src/test/resources/cr2/readme.txt
new file mode 100644
index 00000000..7dc99480
--- /dev/null
+++ b/imageio/imageio-cr2/src/test/resources/cr2/readme.txt
@@ -0,0 +1,7 @@
+Contents:
+IMG_3483.CR2 - Rusty chain at Vækerø harbour, Oslo, Norway, Canon 400D, by me
+IMG_4841.CR2 - Chimneys at Casa Mila, Barcelona, Spain, Canon 400D, by me
+IMG_6933.CR2 - Panda PEZ dispenser at home, Oslo Noway, Canon 7D, by me
+
+Above mentioned images are freely distributable for testing purposes.
+-- Harald K