diff --git a/imageio/imageio-sgi/license.txt b/imageio/imageio-sgi/license.txt
new file mode 100755
index 00000000..fe399516
--- /dev/null
+++ b/imageio/imageio-sgi/license.txt
@@ -0,0 +1,25 @@
+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.
\ No newline at end of file
diff --git a/imageio/imageio-sgi/pom.xml b/imageio/imageio-sgi/pom.xml
new file mode 100755
index 00000000..6dee4429
--- /dev/null
+++ b/imageio/imageio-sgi/pom.xml
@@ -0,0 +1,28 @@
+
+
+ 4.0.0
+
+ com.twelvemonkeys.imageio
+ imageio
+ 3.1-SNAPSHOT
+
+ imageio-sgi
+ TwelveMonkeys :: ImageIO :: SGI plugin
+
+ ImageIO plugin for Silicon Graphics Image Format (SGI)
+
+
+
+
+ com.twelvemonkeys.imageio
+ imageio-core
+
+
+ com.twelvemonkeys.imageio
+ imageio-core
+ tests
+
+
+
diff --git a/imageio/imageio-sgi/src/main/java/com/twelvemonkeys/imageio/plugins/sgi/RLEDecoder.java b/imageio/imageio-sgi/src/main/java/com/twelvemonkeys/imageio/plugins/sgi/RLEDecoder.java
new file mode 100755
index 00000000..8b0a8e93
--- /dev/null
+++ b/imageio/imageio-sgi/src/main/java/com/twelvemonkeys/imageio/plugins/sgi/RLEDecoder.java
@@ -0,0 +1,49 @@
+package com.twelvemonkeys.imageio.plugins.sgi;
+
+import com.twelvemonkeys.io.enc.Decoder;
+
+import java.io.IOException;
+import java.io.InputStream;
+import java.nio.ByteBuffer;
+
+final class RLEDecoder implements Decoder {
+
+ public int decode(final InputStream stream, final ByteBuffer buffer) throws IOException {
+ // Adapted from c code sample in tgaffs.pdf
+ while (buffer.remaining() >= 0x7f) {
+ int val = stream.read();
+ if (val < 0) {
+ break; // EOF
+ }
+
+ int count = val & 0x7f;
+
+ if (count == 0) {
+ break; // No more data
+ }
+
+ if ((val & 0x80) != 0) {
+ for (int i = 0; i < count; i++) {
+ int pixel = stream.read();
+ if (pixel < 0) {
+ break; // EOF
+ }
+
+ buffer.put((byte) pixel);
+ }
+ }
+ else {
+ int pixel = stream.read();
+ if (pixel < 0) {
+ break; // EOF
+ }
+
+ for (int i = 0; i < count; i++) {
+ buffer.put((byte) pixel);
+ }
+ }
+ }
+
+ return buffer.position();
+ }
+}
diff --git a/imageio/imageio-sgi/src/main/java/com/twelvemonkeys/imageio/plugins/sgi/SGI.java b/imageio/imageio-sgi/src/main/java/com/twelvemonkeys/imageio/plugins/sgi/SGI.java
new file mode 100755
index 00000000..8f34a530
--- /dev/null
+++ b/imageio/imageio-sgi/src/main/java/com/twelvemonkeys/imageio/plugins/sgi/SGI.java
@@ -0,0 +1,20 @@
+package com.twelvemonkeys.imageio.plugins.sgi;
+
+interface SGI {
+ short MAGIC = 474; // 0x1da
+
+ /** No compression, channels stored verbatim. */
+ byte COMPRESSION_NONE = 0;
+ /** Runlength encoed compression,
+ * channels are prepended by one offset and length tables (one entry in each per scanline). */
+ byte COMPRESSION_RLE = 1;
+
+ /** Only ColorMode NORMAL should be used. */
+ int COLORMODE_NORMAL = 0;
+ /** Obsolete. */
+ int COLORMODE_DITHERED = 1;
+ /** Obsolete. */
+ int COLORMODE_SCREEN = 2;
+ /** Obsolete. */
+ int COLORMODE_COLORMAP = 3;
+}
diff --git a/imageio/imageio-sgi/src/main/java/com/twelvemonkeys/imageio/plugins/sgi/SGIHeader.java b/imageio/imageio-sgi/src/main/java/com/twelvemonkeys/imageio/plugins/sgi/SGIHeader.java
new file mode 100755
index 00000000..5b47239f
--- /dev/null
+++ b/imageio/imageio-sgi/src/main/java/com/twelvemonkeys/imageio/plugins/sgi/SGIHeader.java
@@ -0,0 +1,131 @@
+package com.twelvemonkeys.imageio.plugins.sgi;
+
+import javax.imageio.IIOException;
+import javax.imageio.stream.ImageInputStream;
+import java.io.IOException;
+import java.nio.charset.Charset;
+
+final class SGIHeader {
+ private int compression;
+ private int bytesPerPixel;
+ private int dimensions;
+ private int width;
+ private int height;
+ private int channels;
+ private int minValue;
+ private int maxValue;
+ private String name;
+ private int colorMode;
+
+ public int getCompression() {
+ return compression;
+ }
+
+ public int getBytesPerPixel() {
+ return bytesPerPixel;
+ }
+
+ public int getWidth() {
+ return width;
+ }
+
+ public int getHeight() {
+ return height;
+ }
+
+ public int getChannels() {
+ return channels;
+ }
+
+ public int getMinValue() {
+ return minValue;
+ }
+
+ public int getMaxValue() {
+ return maxValue;
+ }
+
+ public String getName() {
+ return name;
+ }
+
+ public int getColorMode() {
+ return colorMode;
+ }
+
+ @Override public String toString() {
+ return "SGIHeader{" +
+ "compression=" + compression +
+ ", bytesPerPixel=" + bytesPerPixel +
+ ", dimensions=" + dimensions +
+ ", width=" + width +
+ ", height=" + height +
+ ", channels=" + channels +
+ ", minValue=" + minValue +
+ ", maxValue=" + maxValue +
+ ", name='" + name + '\'' +
+ ", colorMode=" + colorMode +
+ '}';
+ }
+
+ public static SGIHeader read(final ImageInputStream imageInput) throws IOException {
+// typedef struct _SGIHeader
+// {
+// SHORT Magic; /* Identification number (474) */
+// CHAR Storage; /* Compression flag */
+// CHAR Bpc; /* Bytes per pixel */
+// WORD Dimension; /* Number of image dimensions */
+// WORD XSize; /* Width of image in pixels */
+// WORD YSize; /* Height of image in pixels */
+// WORD ZSize; /* Number of bit channels */
+// LONG PixMin; /* Smallest pixel value */
+// LONG PixMax; /* Largest pixel value */
+// CHAR Dummy1[4]; /* Not used */
+// CHAR ImageName[80]; /* Name of image */
+// LONG ColorMap; /* Format of pixel data */
+// CHAR Dummy2[404]; /* Not used */
+// } SGIHEAD;
+ short magic = imageInput.readShort();
+ if (magic != SGI.MAGIC) {
+ throw new IIOException(String.format("Not an SGI image. Expected SGI magic %04x, read %04x", SGI.MAGIC, magic));
+ }
+
+ SGIHeader header = new SGIHeader();
+
+ header.compression = imageInput.readUnsignedByte();
+ header.bytesPerPixel = imageInput.readUnsignedByte();
+
+ header.dimensions = imageInput.readUnsignedShort();
+ header.width = imageInput.readUnsignedShort();
+ header.height = imageInput.readUnsignedShort();
+ header.channels = imageInput.readUnsignedShort();
+
+ header.minValue = imageInput.readInt();
+ header.maxValue = imageInput.readInt();
+
+ imageInput.readInt(); // Ignore
+
+ byte[] nameBytes = new byte[80];
+ imageInput.readFully(nameBytes);
+ header.name = toAsciiString(nameBytes);
+
+ header.colorMode = imageInput.readInt();
+
+ imageInput.skipBytes(404);
+
+ return header;
+ }
+
+ private static String toAsciiString(final byte[] bytes) {
+ // Find null-terminator
+ int len = bytes.length;
+ for (int i = 0; i < bytes.length; i++) {
+ if (bytes[i] == 0) {
+ len = i;
+ break;
+ }
+ }
+
+ return new String(bytes, 0, len, Charset.forName("ASCII"));
+ }
+}
diff --git a/imageio/imageio-sgi/src/main/java/com/twelvemonkeys/imageio/plugins/sgi/SGIImageReader.java b/imageio/imageio-sgi/src/main/java/com/twelvemonkeys/imageio/plugins/sgi/SGIImageReader.java
new file mode 100755
index 00000000..5edf27da
--- /dev/null
+++ b/imageio/imageio-sgi/src/main/java/com/twelvemonkeys/imageio/plugins/sgi/SGIImageReader.java
@@ -0,0 +1,369 @@
+package com.twelvemonkeys.imageio.plugins.sgi;
+
+import com.twelvemonkeys.imageio.ImageReaderBase;
+import com.twelvemonkeys.imageio.util.IIOUtil;
+import com.twelvemonkeys.io.enc.DecoderStream;
+import com.twelvemonkeys.xml.XMLSerializer;
+
+import javax.imageio.IIOException;
+import javax.imageio.ImageIO;
+import javax.imageio.ImageReadParam;
+import javax.imageio.ImageTypeSpecifier;
+import javax.imageio.metadata.IIOMetadata;
+import javax.imageio.metadata.IIOMetadataFormatImpl;
+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.DataInput;
+import java.io.DataInputStream;
+import java.io.File;
+import java.io.IOException;
+import java.util.ArrayList;
+import java.util.Iterator;
+import java.util.List;
+
+public final class SGIImageReader extends ImageReaderBase {
+
+ private SGIHeader header;
+
+ protected SGIImageReader(final ImageReaderSpi provider) {
+ super(provider);
+ }
+
+ @Override
+ protected void resetMembers() {
+ header = null;
+ }
+
+ @Override
+ public int getWidth(final int imageIndex) throws IOException {
+ checkBounds(imageIndex);
+ readHeader();
+
+ return header.getWidth();
+ }
+
+ @Override
+ public int getHeight(final int imageIndex) throws IOException {
+ checkBounds(imageIndex);
+ readHeader();
+
+ return header.getHeight();
+ }
+
+ @Override
+ public Iterator getImageTypes(final int imageIndex) throws IOException {
+ ImageTypeSpecifier rawType = getRawImageType(imageIndex);
+
+ List specifiers = new ArrayList();
+
+ // TODO: Implement
+ specifiers.add(rawType);
+
+ return specifiers.iterator();
+ }
+
+ @Override
+ public ImageTypeSpecifier getRawImageType(final int imageIndex) throws IOException {
+ checkBounds(imageIndex);
+ readHeader();
+
+ // NOTE: There doesn't seem to be any god way to determine color space, other than by convention
+ // 1 channel: Gray, 2 channel: Gray + Alpha, 3 channel: RGB, 4 channel: RGBA (hopefully never CMYK...)
+
+ int channels = header.getChannels();
+
+ ColorSpace cs = channels < 3 ? ColorSpace.getInstance(ColorSpace.CS_GRAY) : ColorSpace.getInstance(ColorSpace.CS_sRGB);
+
+ switch (header.getBytesPerPixel()) {
+ case 1:
+ return ImageTypeSpecifier.createBanded(cs, createIndices(channels, 1), createIndices(channels, 0), DataBuffer.TYPE_BYTE, channels == 2 || channels == 4, false);
+ case 2:
+ return ImageTypeSpecifier.createBanded(cs, createIndices(channels, 1), createIndices(channels, 0), DataBuffer.TYPE_USHORT, channels == 2 || channels == 4, false);
+ default:
+ throw new IIOException("Unknown number of bytes per pixel: " + header.getBytesPerPixel());
+ }
+ }
+
+ private int[] createIndices(final int bands, int increment) {
+ int[] indices = new int[bands];
+
+ for (int i = 0; i < bands; i++) {
+ indices[i] = i * increment;
+ }
+
+ return indices;
+ }
+
+ @Override
+ public BufferedImage read(final int imageIndex, final ImageReadParam param) throws IOException {
+ Iterator imageTypes = getImageTypes(imageIndex);
+ ImageTypeSpecifier rawType = getRawImageType(imageIndex);
+
+ if (header.getColorMode() != SGI.COLORMODE_NORMAL) {
+ processWarningOccurred(String.format("Unsupported color mode: %d, colors may look incorrect", header.getColorMode()));
+ }
+
+ int width = getWidth(imageIndex);
+ int height = getHeight(imageIndex);
+
+ BufferedImage destination = getDestination(param, imageTypes, width, height);
+
+ Rectangle srcRegion = new Rectangle();
+ Rectangle destRegion = new Rectangle();
+ computeRegions(param, width, height, destination, srcRegion, destRegion);
+
+ WritableRaster destRaster = clipToRect(destination.getRaster(), destRegion, param != null ? param.getDestinationBands() : null);
+ checkReadParamBandSettings(param, rawType.getNumBands(), destRaster.getNumBands());
+
+ WritableRaster rowRaster = rawType.createBufferedImage(width, 1).getRaster();
+ // Clip to source region
+ Raster clippedRow = clipRowToRect(rowRaster, srcRegion,
+ param != null ? param.getSourceBands() : null,
+ param != null ? param.getSourceXSubsampling() : 1);
+
+ int[] scanlineOffsets;
+ int[] scanlineLengths;
+
+ int compression = header.getCompression();
+ if (compression == SGI.COMPRESSION_RLE) {
+ scanlineOffsets = new int[height * header.getChannels()];
+ scanlineLengths = new int[height * header.getChannels()];
+ imageInput.readFully(scanlineOffsets, 0, scanlineOffsets.length);
+ imageInput.readFully(scanlineLengths, 0, scanlineLengths.length);
+ }
+ else {
+ scanlineOffsets = null;
+ scanlineLengths = null;
+ }
+
+ int xSub = param != null ? param.getSourceXSubsampling() : 1;
+ int ySub = param != null ? param.getSourceYSubsampling() : 1;
+
+ processImageStarted(imageIndex);
+
+ for (int c = 0; c < header.getChannels(); c++) {
+ WritableRaster destChannel = destRaster.createWritableChild(destRaster.getMinX(), destRaster.getMinY(), destRaster.getWidth(), destRaster.getHeight(), 0, 0, new int[] {c});
+ Raster srcChannel = clippedRow.createChild(clippedRow.getMinX(), 0, clippedRow.getWidth(), 1, 0, 0, new int[] {c});
+
+ // NOTE: SGI images are store bottom/up, thus y value is opposite of destination y
+ for (int y = 0; y < height; y++) {
+ switch (header.getBytesPerPixel()) {
+ case 1:
+ byte[] rowDataByte = ((DataBufferByte) rowRaster.getDataBuffer()).getData(c);
+ readRowByte(height, srcRegion, scanlineOffsets, scanlineLengths, compression, xSub, ySub, c, rowDataByte, destChannel, srcChannel, y);
+ break;
+ case 2:
+ short[] rowDataUShort = ((DataBufferUShort) rowRaster.getDataBuffer()).getData(c);
+ readRowUShort(height, srcRegion, scanlineOffsets, scanlineLengths, compression, xSub, ySub, c, rowDataUShort, destChannel, srcChannel, y);
+ break;
+ default:
+ throw new AssertionError();
+ }
+
+ processImageProgress(100f * y / height * c / header.getChannels());
+
+ if (height - 1 - y < srcRegion.y) {
+ break;
+ }
+
+ if (abortRequested()) {
+ break;
+ }
+ }
+
+ if (abortRequested()) {
+ processReadAborted();
+ break;
+ }
+ }
+
+ processImageComplete();
+
+ return destination;
+ }
+
+ private void readRowByte(int height, Rectangle srcRegion, int[] scanlineOffsets, int[] scanlineLengths, int compression, int xSub, int ySub, int c, byte[] rowDataByte, WritableRaster destChannel, Raster srcChannel, int y) throws IOException {
+ // If subsampled or outside source region, skip entire row
+ if (y % ySub != 0 || height - 1 - y < srcRegion.y || height - 1 - y >= srcRegion.y + srcRegion.height) {
+ if (compression == SGI.COMPRESSION_NONE) {
+ imageInput.skipBytes(rowDataByte.length);
+ }
+
+ return;
+ }
+
+ // Wrap input
+ DataInput input;
+ if (compression == SGI.COMPRESSION_RLE) {
+ int scanLineIndex = c * height + y;
+ imageInput.seek(scanlineOffsets[scanLineIndex]);
+ input = new DataInputStream(new DecoderStream(IIOUtil.createStreamAdapter(imageInput, scanlineLengths[scanLineIndex]), new RLEDecoder()));
+ } else {
+ input = imageInput;
+ }
+
+ input.readFully(rowDataByte, 0, rowDataByte.length);
+
+ // Subsample horizontal
+ if (xSub != 1) {
+ for (int x = 0; x < srcRegion.width / xSub; x++) {
+ rowDataByte[srcRegion.x + x] = rowDataByte[srcRegion.x + x * xSub];
+ }
+ }
+
+ normalize(rowDataByte, 9, srcRegion.width / xSub);
+
+ // Flip into position (SGI images are stored bottom/up)
+ int dstY = (height - 1 - y - srcRegion.y) / ySub;
+ destChannel.setDataElements(0, dstY, srcChannel);
+ }
+
+ private void readRowUShort(int height, Rectangle srcRegion, int[] scanlineOffsets, int[] scanlineLengths, int compression, int xSub, int ySub, int c, short[] rowDataUShort, WritableRaster destChannel, Raster srcChannel, int y) throws IOException {
+ // If subsampled or outside source region, skip entire row
+ if (y % ySub != 0 || height - 1 - y < srcRegion.y || height - 1 - y >= srcRegion.y + srcRegion.height) {
+ if (compression == SGI.COMPRESSION_NONE) {
+ imageInput.skipBytes(rowDataUShort.length * 2);
+ }
+
+ return;
+ }
+
+ // Wrap input
+ DataInput input;
+ if (compression == SGI.COMPRESSION_RLE) {
+ int scanLineIndex = c * height + y;
+ imageInput.seek(scanlineOffsets[scanLineIndex]);
+ input = new DataInputStream(new DecoderStream(IIOUtil.createStreamAdapter(imageInput, scanlineLengths[scanLineIndex]), new RLEDecoder()));
+ } else {
+ input = imageInput;
+ }
+
+ readFully(input, rowDataUShort);
+
+ // Subsample horizontal
+ if (xSub != 1) {
+ for (int x = 0; x < srcRegion.width / xSub; x++) {
+ rowDataUShort[srcRegion.x + x] = rowDataUShort[srcRegion.x + x * xSub];
+ }
+ }
+
+ normalize(rowDataUShort, 9, srcRegion.width / xSub);
+
+ // Flip into position (SGI images are stored bottom/up)
+ int dstY = (height - 1 - y - srcRegion.y) / ySub;
+ destChannel.setDataElements(0, dstY, srcChannel);
+ }
+
+ // TODO: Candidate util method
+ private static void readFully(final DataInput input, final short[] shorts) throws IOException {
+ if (input instanceof ImageInputStream) {
+ // Optimization for ImageInputStreams, read all in one go
+ ((ImageInputStream) input).readFully(shorts, 0, shorts.length);
+ }
+ else {
+ for (int i = 0; i < shorts.length; i++) {
+ shorts[i] = input.readShort();
+ }
+ }
+ }
+
+ private void normalize(final byte[] rowData, final int start, final int length) {
+ int minValue = header.getMinValue();
+ int maxValue = header.getMaxValue();
+ if (minValue != 0 && maxValue != 0xff) {
+ // Normalize
+ for (int i = start; i < length; i++) {
+ rowData[i] = (byte) (((rowData[i] - minValue) * 0xff) / maxValue);
+ }
+ }
+ }
+
+ private void normalize(final short[] rowData, final int start, final int length) {
+ int minValue = header.getMinValue();
+ int maxValue = header.getMaxValue();
+ if (minValue != 0 && maxValue != 0xff) {
+ // Normalize
+ for (int i = start; i < length; i++) {
+ rowData[i] = (byte) (((rowData[i] - minValue) * 0xff) / maxValue);
+ }
+ }
+ }
+
+ 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 readHeader() throws IOException {
+ if (header == null) {
+ header = SGIHeader.read(imageInput);
+
+// System.err.println("header: " + header);
+
+ imageInput.flushBefore(imageInput.getStreamPosition());
+ }
+
+ imageInput.seek(imageInput.getFlushedPosition());
+ }
+
+ @Override public IIOMetadata getImageMetadata(final int imageIndex) throws IOException {
+ checkBounds(imageIndex);
+ readHeader();
+
+ return new SGIMetadata(header);
+ }
+
+ public static void main(String[] args) throws IOException {
+ SGIImageReader reader = new SGIImageReader(null);
+
+ for (String arg : args) {
+ File in = new File(arg);
+ reader.setInput(ImageIO.createImageInputStream(in));
+
+ ImageReadParam param = reader.getDefaultReadParam();
+ param.setDestinationType(reader.getImageTypes(0).next());
+// param.setSourceSubsampling(2, 3, 0, 0);
+// param.setSourceSubsampling(2, 1, 0, 0);
+//
+// int width = reader.getWidth(0);
+// int height = reader.getHeight(0);
+//
+// param.setSourceRegion(new Rectangle(width / 4, height / 4, width / 2, height / 2));
+// param.setSourceRegion(new Rectangle(width / 2, height / 2));
+// param.setSourceRegion(new Rectangle(width / 2, height / 2, width / 2, height / 2));
+
+ BufferedImage image = reader.read(0, param);
+
+ System.err.println("image: " + image);
+
+ showIt(image, in.getName());
+
+ new XMLSerializer(System.out, System.getProperty("file.encoding")).serialize(reader.getImageMetadata(0).getAsTree(IIOMetadataFormatImpl.standardMetadataFormatName), false);
+
+// File reference = new File(in.getParent() + "/../reference", in.getName().replaceAll("\\.p(a|b|g|p)m", ".png"));
+// if (reference.exists()) {
+// System.err.println("reference.getAbsolutePath(): " + reference.getAbsolutePath());
+// showIt(ImageIO.read(reference), reference.getName());
+// }
+
+// break;
+ }
+ }
+}
diff --git a/imageio/imageio-sgi/src/main/java/com/twelvemonkeys/imageio/plugins/sgi/SGIImageReaderSpi.java b/imageio/imageio-sgi/src/main/java/com/twelvemonkeys/imageio/plugins/sgi/SGIImageReaderSpi.java
new file mode 100755
index 00000000..56f07fe4
--- /dev/null
+++ b/imageio/imageio-sgi/src/main/java/com/twelvemonkeys/imageio/plugins/sgi/SGIImageReaderSpi.java
@@ -0,0 +1,82 @@
+package com.twelvemonkeys.imageio.plugins.sgi;
+
+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.util.Locale;
+
+public final class SGIImageReaderSpi extends ImageReaderSpi {
+
+ /**
+ * Creates a {@code SGIImageReaderSpi}.
+ */
+ public SGIImageReaderSpi() {
+ this(IIOUtil.getProviderInfo(SGIImageReaderSpi.class));
+ }
+
+ private SGIImageReaderSpi(final ProviderInfo providerInfo) {
+ super(
+ providerInfo.getVendorName(),
+ providerInfo.getVersion(),
+ new String[]{
+ "sgi",
+ "SGI"
+ },
+ new String[]{"sgi"},
+ new String[]{
+ // No official IANA record exists
+ "image/sgi",
+ "image/x-sgi",
+ },
+ "com.twelvemkonkeys.imageio.plugins.sgi.SGIImageReader",
+ new Class[] {ImageInputStream.class},
+ null,
+ true, // supports standard stream metadata
+ null, null, // native stream format name and class
+ null, null, // extra stream formats
+ true, // supports standard image metadata
+ null, null,
+ null, null // extra image metadata formats
+ );
+ }
+
+ @Override public boolean canDecodeInput(final Object source) throws IOException {
+ if (!(source instanceof ImageInputStream)) {
+ return false;
+ }
+
+ ImageInputStream stream = (ImageInputStream) source;
+
+ stream.mark();
+
+ try {
+ short magic = stream.readShort();
+
+ switch (magic) {
+ case SGI.MAGIC:
+ byte compression = stream.readByte();
+ byte bpp = stream.readByte();
+
+ return (compression == SGI.COMPRESSION_NONE || compression == SGI.COMPRESSION_RLE) && (bpp == 1 || bpp == 2);
+ default:
+ return false;
+ }
+ }
+ finally {
+ stream.reset();
+ }
+ }
+
+ @Override public ImageReader createReaderInstance(final Object extension) throws IOException {
+ return new SGIImageReader(this);
+ }
+
+ @Override public String getDescription(final Locale locale) {
+ return "Silicon Graphics (SGI) image reader";
+ }
+}
+
diff --git a/imageio/imageio-sgi/src/main/java/com/twelvemonkeys/imageio/plugins/sgi/SGIMetadata.java b/imageio/imageio-sgi/src/main/java/com/twelvemonkeys/imageio/plugins/sgi/SGIMetadata.java
new file mode 100755
index 00000000..35c17b6d
--- /dev/null
+++ b/imageio/imageio-sgi/src/main/java/com/twelvemonkeys/imageio/plugins/sgi/SGIMetadata.java
@@ -0,0 +1,199 @@
+package com.twelvemonkeys.imageio.plugins.sgi;
+
+import javax.imageio.metadata.IIOMetadata;
+import javax.imageio.metadata.IIOMetadataFormatImpl;
+import javax.imageio.metadata.IIOMetadataNode;
+
+import org.w3c.dom.Node;
+
+final class SGIMetadata extends IIOMetadata {
+ // TODO: Clean up & extend AbstractMetadata (after moving from PSD -> Core)
+
+ private final SGIHeader header;
+
+ SGIMetadata(final SGIHeader header) {
+ this.header = header;
+ standardFormatSupported = true;
+ }
+
+ @Override public boolean isReadOnly() {
+ return true;
+ }
+
+ @Override public Node getAsTree(final String formatName) {
+ if (formatName.equals(IIOMetadataFormatImpl.standardMetadataFormatName)) {
+ return getStandardTree();
+ }
+ else {
+ throw new IllegalArgumentException("Unsupported metadata format: " + formatName);
+ }
+ }
+
+ @Override public void mergeTree(final String formatName, final Node root) {
+ if (isReadOnly()) {
+ throw new IllegalStateException("Metadata is read-only");
+ }
+ }
+
+ @Override public void reset() {
+ if (isReadOnly()) {
+ throw new IllegalStateException("Metadata is read-only");
+ }
+ }
+
+ @Override protected IIOMetadataNode getStandardChromaNode() {
+ IIOMetadataNode chroma = new IIOMetadataNode("Chroma");
+
+ // NOTE: There doesn't seem to be any god way to determine color space, other than by convention
+ // 1 channel: Gray, 2 channel: Gray + Alpha, 3 channel: RGB, 4 channel: RGBA (hopefully never CMYK...)
+ IIOMetadataNode csType = new IIOMetadataNode("ColorSpaceType");
+ switch (header.getColorMode()) {
+ case SGI.COLORMODE_NORMAL:
+ switch (header.getChannels()) {
+ case 1:
+ case 2:
+ csType.setAttribute("name", "GRAY");
+ break;
+ case 3:
+ case 4:
+ csType.setAttribute("name", "RGB");
+ break;
+ default:
+ csType.setAttribute("name", Integer.toHexString(header.getChannels()).toUpperCase() + "CLR");
+ break;
+ }
+ break;
+
+ // SGIIMAGE.TXT describes these as RGB
+ case SGI.COLORMODE_DITHERED:
+ case SGI.COLORMODE_SCREEN:
+ case SGI.COLORMODE_COLORMAP:
+ csType.setAttribute("name", "RGB");
+ break;
+ }
+
+ if (csType.getAttribute("name") != null) {
+ chroma.appendChild(csType);
+ }
+
+ IIOMetadataNode numChannels = new IIOMetadataNode("NumChannels");
+ numChannels.setAttribute("value", Integer.toString(header.getChannels()));
+ chroma.appendChild(numChannels);
+
+ IIOMetadataNode blackIsZero = new IIOMetadataNode("BlackIsZero");
+ blackIsZero.setAttribute("value", "TRUE");
+ chroma.appendChild(blackIsZero);
+
+ return chroma;
+ }
+
+ // No compression
+
+ @Override protected IIOMetadataNode getStandardCompressionNode() {
+ if (header.getCompression() != SGI.COMPRESSION_NONE) {
+ IIOMetadataNode node = new IIOMetadataNode("Compression");
+
+ IIOMetadataNode compressionTypeName = new IIOMetadataNode("CompressionTypeName");
+ compressionTypeName.setAttribute("value", header.getCompression() == SGI.COMPRESSION_RLE ? "RLE" : "Uknown");
+ node.appendChild(compressionTypeName);
+
+ IIOMetadataNode lossless = new IIOMetadataNode("Lossless");
+ lossless.setAttribute("value", "TRUE");
+ node.appendChild(lossless);
+
+ return node;
+ }
+
+ return null;
+ }
+
+ @Override protected IIOMetadataNode getStandardDataNode() {
+ IIOMetadataNode node = new IIOMetadataNode("Data");
+
+ IIOMetadataNode sampleFormat = new IIOMetadataNode("SampleFormat");
+ sampleFormat.setAttribute("value", "UnsignedIntegral");
+ node.appendChild(sampleFormat);
+
+ IIOMetadataNode bitsPerSample = new IIOMetadataNode("BitsPerSample");
+ bitsPerSample.setAttribute("value", createListValue(header.getChannels(), Integer.toString(header.getBytesPerPixel() * 8)));
+ node.appendChild(bitsPerSample);
+
+ IIOMetadataNode significantBitsPerSample = new IIOMetadataNode("SignificantBitsPerSample");
+ significantBitsPerSample.setAttribute("value", createListValue(header.getChannels(), Integer.toString(computeSignificantBits())));
+ node.appendChild(significantBitsPerSample);
+
+ IIOMetadataNode sampleMSB = new IIOMetadataNode("SampleMSB");
+ sampleMSB.setAttribute("value", createListValue(header.getChannels(), "0"));
+
+ return node;
+ }
+
+ private int computeSignificantBits() {
+ int significantBits = 0;
+
+ int maxSample = header.getMaxValue();
+
+ while (maxSample > 0) {
+ maxSample >>>= 1;
+ significantBits++;
+ }
+
+ return significantBits;
+ }
+
+ private String createListValue(final int itemCount, final String... values) {
+ StringBuilder buffer = new StringBuilder();
+
+ for (int i = 0; i < itemCount; i++) {
+ if (buffer.length() > 0) {
+ buffer.append(' ');
+ }
+
+ buffer.append(values[i % values.length]);
+ }
+
+ return buffer.toString();
+ }
+
+ @Override protected IIOMetadataNode getStandardDimensionNode() {
+ IIOMetadataNode dimension = new IIOMetadataNode("Dimension");
+
+ IIOMetadataNode imageOrientation = new IIOMetadataNode("ImageOrientation");
+ imageOrientation.setAttribute("value", "FlipV");
+ dimension.appendChild(imageOrientation);
+
+ return dimension;
+ }
+
+ // No document node
+
+ @Override protected IIOMetadataNode getStandardTextNode() {
+ if (!header.getName().isEmpty()) {
+ IIOMetadataNode text = new IIOMetadataNode("Text");
+
+ IIOMetadataNode textEntry = new IIOMetadataNode("TextEntry");
+ textEntry.setAttribute("keyword", "name");
+ textEntry.setAttribute("value", header.getName());
+ text.appendChild(textEntry);
+
+ return text;
+ }
+
+ return null;
+ }
+
+ // No tiling
+
+ @Override protected IIOMetadataNode getStandardTransparencyNode() {
+ // NOTE: There doesn't seem to be any god way to determine transparency, other than by convention
+ // 1 channel: Gray, 2 channel: Gray + Alpha, 3 channel: RGB, 4 channel: RGBA (hopefully never CMYK...)
+
+ IIOMetadataNode transparency = new IIOMetadataNode("Transparency");
+
+ IIOMetadataNode alpha = new IIOMetadataNode("Alpha");
+ alpha.setAttribute("value", header.getChannels() == 1 || header.getChannels() == 3 ? "none" : "nonpremultiplied");
+ transparency.appendChild(alpha);
+
+ return transparency;
+ }
+}
diff --git a/imageio/imageio-sgi/src/main/resources/META-INF/services/javax.imageio.spi.ImageReaderSpi b/imageio/imageio-sgi/src/main/resources/META-INF/services/javax.imageio.spi.ImageReaderSpi
new file mode 100755
index 00000000..2d16cd54
--- /dev/null
+++ b/imageio/imageio-sgi/src/main/resources/META-INF/services/javax.imageio.spi.ImageReaderSpi
@@ -0,0 +1 @@
+com.twelvemonkeys.imageio.plugins.sgi.SGIImageReaderSpi
diff --git a/imageio/imageio-sgi/src/test/java/com/twelvemonkeys/imageio/plugins/sgi/SGIImageReaderTest.java b/imageio/imageio-sgi/src/test/java/com/twelvemonkeys/imageio/plugins/sgi/SGIImageReaderTest.java
new file mode 100755
index 00000000..209d8efd
--- /dev/null
+++ b/imageio/imageio-sgi/src/test/java/com/twelvemonkeys/imageio/plugins/sgi/SGIImageReaderTest.java
@@ -0,0 +1,86 @@
+/*
+ * 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.sgi;
+
+import com.twelvemonkeys.imageio.util.ImageReaderAbstractTestCase;
+
+import javax.imageio.spi.ImageReaderSpi;
+import java.awt.*;
+import java.util.Arrays;
+import java.util.List;
+
+/**
+ * SGIImageReaderTest
+ *
+ * @author Harald Kuhr
+ * @author last modified by $Author: haraldk$
+ * @version $Id: SGIImageReaderTest.java,v 1.0 03.07.14 22:28 haraldk Exp$
+ */
+public class SGIImageReaderTest extends ImageReaderAbstractTestCase {
+ @Override
+ protected List getTestData() {
+ return Arrays.asList(
+ new TestData(getClassLoaderResource("/sgi/MARBLES.SGI"), new Dimension(1419, 1001)) // RLE encoded RGB
+ );
+ }
+
+ @Override
+ protected ImageReaderSpi createProvider() {
+ return new SGIImageReaderSpi();
+ }
+
+ @Override
+ protected Class getReaderClass() {
+ return SGIImageReader.class;
+ }
+
+ @Override
+ protected SGIImageReader createReader() {
+ return new SGIImageReader(createProvider());
+ }
+
+ @Override
+ protected List getFormatNames() {
+ return Arrays.asList("SGI", "sgi");
+ }
+
+ @Override
+ protected List getSuffixes() {
+ return Arrays.asList(
+ "sgi"
+ );
+ }
+
+ @Override
+ protected List getMIMETypes() {
+ return Arrays.asList(
+ "image/sgi", "image/x-sgi"
+ );
+ }
+}
diff --git a/imageio/imageio-sgi/src/test/resources/sgi/MARBLES.SGI b/imageio/imageio-sgi/src/test/resources/sgi/MARBLES.SGI
new file mode 100755
index 00000000..4a54d37e
Binary files /dev/null and b/imageio/imageio-sgi/src/test/resources/sgi/MARBLES.SGI differ
diff --git a/imageio/pom.xml b/imageio/pom.xml
index b5d1d5a3..ab2d7b0c 100644
--- a/imageio/pom.xml
+++ b/imageio/pom.xml
@@ -36,6 +36,7 @@
imageio-pict
imageio-pnm
imageio-psd
+ imageio-sgi
imageio-tga
imageio-thumbsdb
imageio-tiff