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