diff --git a/imageio/imageio-pcx/license.txt b/imageio/imageio-pcx/license.txt new file mode 100755 index 00000000..fe399516 --- /dev/null +++ b/imageio/imageio-pcx/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-pcx/pom.xml b/imageio/imageio-pcx/pom.xml new file mode 100755 index 00000000..511724a1 --- /dev/null +++ b/imageio/imageio-pcx/pom.xml @@ -0,0 +1,28 @@ + + + 4.0.0 + + com.twelvemonkeys.imageio + imageio + 3.1-SNAPSHOT + + imageio-pcx + TwelveMonkeys :: ImageIO :: PCX plugin + + ImageIO plugin for ZSoft Paintbrush Format (PCX) + + + + + com.twelvemonkeys.imageio + imageio-core + + + com.twelvemonkeys.imageio + imageio-core + tests + + + diff --git a/imageio/imageio-pcx/src/main/java/com/twelvemonkeys/imageio/plugins/dcx/DCX.java b/imageio/imageio-pcx/src/main/java/com/twelvemonkeys/imageio/plugins/dcx/DCX.java new file mode 100755 index 00000000..9428274d --- /dev/null +++ b/imageio/imageio-pcx/src/main/java/com/twelvemonkeys/imageio/plugins/dcx/DCX.java @@ -0,0 +1,11 @@ +package com.twelvemonkeys.imageio.plugins.dcx; + +/** + * The DXC file format, is just a small header before a number of PCX streams. + * Mostly used as a FAX format. + * + * @see [PCX] Related File Formats + */ +interface DCX { + int MAGIC = 0x3ADE68B1; +} diff --git a/imageio/imageio-pcx/src/main/java/com/twelvemonkeys/imageio/plugins/dcx/DCXHeader.java b/imageio/imageio-pcx/src/main/java/com/twelvemonkeys/imageio/plugins/dcx/DCXHeader.java new file mode 100755 index 00000000..3d3a1dd1 --- /dev/null +++ b/imageio/imageio-pcx/src/main/java/com/twelvemonkeys/imageio/plugins/dcx/DCXHeader.java @@ -0,0 +1,53 @@ +package com.twelvemonkeys.imageio.plugins.dcx; + +import java.io.IOException; +import java.util.Arrays; + +import javax.imageio.IIOException; +import javax.imageio.stream.ImageInputStream; + +final class DCXHeader { + private final int[] offsetTable; + + DCXHeader(final int[] offsetTable) { + this.offsetTable = offsetTable; + } + + public int getCount() { + return offsetTable.length; + } + + public long getOffset(final int index) { + return (0xffffffffL & offsetTable[index]); + } + + public static DCXHeader read(final ImageInputStream imageInput) throws IOException { + // typedef struct _DcxHeader + // { + // DWORD Id; /* DCX Id number */ + // DWORD PageTable[1024]; /* Image offsets */ + // } DCXHEAD; + + int magic = imageInput.readInt(); + if (magic != DCX.MAGIC) { + throw new IIOException(String.format("Not a DCX file. Expected DCX magic %02x, read %02x", DCX.MAGIC, magic)); + } + + int[] offsets = new int[1024]; + + int count = 0; + do { + offsets[count] = imageInput.readInt(); + count++; + } + while (offsets[count - 1] != 0 && count < offsets.length); + + return new DCXHeader(count == offsets.length ? offsets : Arrays.copyOf(offsets, count)); + } + + @Override public String toString() { + return "DCXHeader{" + + "offsetTable=" + Arrays.toString(offsetTable) + + '}'; + } +} diff --git a/imageio/imageio-pcx/src/main/java/com/twelvemonkeys/imageio/plugins/dcx/DCXImageReader.java b/imageio/imageio-pcx/src/main/java/com/twelvemonkeys/imageio/plugins/dcx/DCXImageReader.java new file mode 100755 index 00000000..e3eee236 --- /dev/null +++ b/imageio/imageio-pcx/src/main/java/com/twelvemonkeys/imageio/plugins/dcx/DCXImageReader.java @@ -0,0 +1,205 @@ +package com.twelvemonkeys.imageio.plugins.dcx; + +import java.awt.image.BufferedImage; +import java.io.File; +import java.io.IOException; +import java.nio.ByteOrder; +import java.util.Iterator; + +import javax.imageio.ImageIO; +import javax.imageio.ImageReadParam; +import javax.imageio.ImageReader; +import javax.imageio.ImageTypeSpecifier; +import javax.imageio.event.IIOReadWarningListener; +import javax.imageio.metadata.IIOMetadata; +import javax.imageio.metadata.IIOMetadataFormatImpl; +import javax.imageio.spi.ImageReaderSpi; + +import com.twelvemonkeys.imageio.ImageReaderBase; +import com.twelvemonkeys.imageio.plugins.pcx.PCXImageReader; +import com.twelvemonkeys.imageio.util.ProgressListenerBase; +import com.twelvemonkeys.xml.XMLSerializer; + +public final class DCXImageReader extends ImageReaderBase { + // TODO: Delegate listeners with correct index! + + private DCXHeader header; + + private PCXImageReader readerDelegate; + private ProgressDelegator progressDelegator; + + public DCXImageReader(final ImageReaderSpi provider) { + super(provider); + readerDelegate = new PCXImageReader(provider); + + progressDelegator = new ProgressDelegator(); + installListeners(); + } + + private void installListeners() { + readerDelegate.addIIOReadProgressListener(progressDelegator); + readerDelegate.addIIOReadWarningListener(progressDelegator); + } + + @Override protected void resetMembers() { + header = null; + + readerDelegate.reset(); + installListeners(); + } + + @Override public void dispose() { + super.dispose(); + + readerDelegate.dispose(); + readerDelegate = null; + } + + @Override public int getWidth(final int imageIndex) throws IOException { + initIndex(imageIndex); + + return readerDelegate.getWidth(0); + } + + @Override public int getHeight(final int imageIndex) throws IOException { + initIndex(imageIndex); + + return readerDelegate.getHeight(0); + } + + @Override public ImageTypeSpecifier getRawImageType(final int imageIndex) throws IOException { + initIndex(imageIndex); + + return readerDelegate.getRawImageType(0); + } + + @Override public Iterator getImageTypes(final int imageIndex) throws IOException { + initIndex(imageIndex); + + return readerDelegate.getImageTypes(0); + } + + @Override public BufferedImage read(final int imageIndex, final ImageReadParam param) throws IOException { + initIndex(imageIndex); + + return readerDelegate.read(imageIndex, param); + } + + @Override public IIOMetadata getImageMetadata(final int imageIndex) throws IOException { + initIndex(imageIndex); + + return readerDelegate.getImageMetadata(0); + } + + @Override public synchronized void abort() { + super.abort(); + readerDelegate.abort(); + } + + @Override public int getNumImages(final boolean allowSearch) throws IOException { + readHeader(); + + return header.getCount(); + } + + private void initIndex(final int imageIndex) throws IOException { + checkBounds(imageIndex); + + imageInput.seek(header.getOffset(imageIndex)); + progressDelegator.index = imageIndex; + readerDelegate.setInput(imageInput); + } + + private void readHeader() throws IOException { + assertInput(); + + if (header == null) { + imageInput.setByteOrder(ByteOrder.LITTLE_ENDIAN); + header = DCXHeader.read(imageInput); +// System.err.println("header: " + header); + imageInput.flushBefore(imageInput.getStreamPosition()); + } + + imageInput.seek(imageInput.getFlushedPosition()); + } + + private class ProgressDelegator extends ProgressListenerBase implements IIOReadWarningListener { + private int index; + + @Override + public void imageComplete(ImageReader source) { + processImageComplete(); + } + + @Override + public void imageProgress(ImageReader source, float percentageDone) { + processImageProgress(percentageDone); + } + + @Override + public void imageStarted(ImageReader source, int imageIndex) { + processImageStarted(index); + } + + @Override + public void readAborted(ImageReader source) { + processReadAborted(); + } + + @Override + public void sequenceComplete(ImageReader source) { + processSequenceComplete(); + } + + @Override + public void sequenceStarted(ImageReader source, int minIndex) { + processSequenceStarted(index); + } + + public void warningOccurred(ImageReader source, String warning) { + processWarningOccurred(warning); + } + } + + + public static void main(String[] args) throws IOException { + DCXImageReader reader = new DCXImageReader(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.getHdpi(0); +// int height = reader.getVdpi(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)); + + System.err.println("header: " + reader.header); + + 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-pcx/src/main/java/com/twelvemonkeys/imageio/plugins/dcx/DCXImageReaderSpi.java b/imageio/imageio-pcx/src/main/java/com/twelvemonkeys/imageio/plugins/dcx/DCXImageReaderSpi.java new file mode 100755 index 00000000..cd7902ad --- /dev/null +++ b/imageio/imageio-pcx/src/main/java/com/twelvemonkeys/imageio/plugins/dcx/DCXImageReaderSpi.java @@ -0,0 +1,79 @@ +package com.twelvemonkeys.imageio.plugins.dcx; + +import java.io.IOException; +import java.nio.ByteOrder; +import java.util.Locale; + +import javax.imageio.ImageReader; +import javax.imageio.spi.ImageReaderSpi; +import javax.imageio.stream.ImageInputStream; + +import com.twelvemonkeys.imageio.spi.ProviderInfo; +import com.twelvemonkeys.imageio.util.IIOUtil; + +public final class DCXImageReaderSpi extends ImageReaderSpi { + + /** + * Creates a {@code DCXImageReaderSpi}. + */ + public DCXImageReaderSpi() { + this(IIOUtil.getProviderInfo(DCXImageReaderSpi.class)); + } + + private DCXImageReaderSpi(final ProviderInfo providerInfo) { + super( + providerInfo.getVendorName(), + providerInfo.getVersion(), + new String[]{ + "dcx", + "DCX" + }, + new String[]{"dcx"}, + new String[]{ + // No official IANA record exists + "image/dcx", + "image/x-dcx", + }, + "com.twelvemkonkeys.imageio.plugins.dcx.DCXImageReader", + 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 { + ByteOrder originalByteOrder = stream.getByteOrder(); + stream.setByteOrder(ByteOrder.LITTLE_ENDIAN); + + try { + return stream.readInt() == DCX.MAGIC; + } + finally { + stream.setByteOrder(originalByteOrder); + } + } + finally{ + stream.reset(); + } + } + + @Override public ImageReader createReaderInstance(final Object extension) throws IOException { + return new DCXImageReader(this); + } + + @Override public String getDescription(final Locale locale) { + return "Multi-page PCX fax document (DCX) image reader"; + }} diff --git a/imageio/imageio-pcx/src/main/java/com/twelvemonkeys/imageio/plugins/pcx/BitRotator.java b/imageio/imageio-pcx/src/main/java/com/twelvemonkeys/imageio/plugins/pcx/BitRotator.java new file mode 100755 index 00000000..3a6702d0 --- /dev/null +++ b/imageio/imageio-pcx/src/main/java/com/twelvemonkeys/imageio/plugins/pcx/BitRotator.java @@ -0,0 +1,99 @@ +package com.twelvemonkeys.imageio.plugins.pcx; + +/** + * IFFUtil + *

+ * Bit rotate methods based on Sue-Ken Yap, "A Fast 90-Degree Bitmap Rotator," + * in GRAPHICS GEMS II, James Arvo ed., Academic Press, 1991, ISBN 0-12-064480-0. + * + * @author Unascribed (C version) + * @author Harald Kuhr (Java port) + * @version $Id: IFFUtil.java,v 1.0 06.mar.2006 13:31:35 haku Exp$ + */ +final class BitRotator { + // TODO: Extract and merge with IFFUtil + + /** + * Creates a rotation table + * @param n number of bits -1 + * + * @return the rotation table + */ + private static long[] rtable(int n) { + return new long[]{ + 0x00000000l << n, 0x00000001l << n, 0x00000100l << n, 0x00000101l << n, + 0x00010000l << n, 0x00010001l << n, 0x00010100l << n, 0x00010101l << n, + 0x01000000l << n, 0x01000001l << n, 0x01000100l << n, 0x01000101l << n, + 0x01010000l << n, 0x01010001l << n, 0x01010100l << n, 0x01010101l << n + }; + } + + private static final long[][] RTABLE = { + rtable(0), rtable(1), rtable(2), rtable(3), + rtable(4), rtable(5), rtable(6), rtable(7) + }; + + /** + * Rotate bits clockwise. + * The IFFImageReader uses this to convert pixel bits from planar to chunky. + * Bits from the source are rotated 90 degrees clockwise written to the + * destination. + * + * @param pSrc source pixel data + * @param pSrcPos starting index of 8 x 8 bit source tile + * @param pSrcStep byte offset between adjacent rows in source + * @param pDst destination pixel data + * @param pDstPos starting index of 8 x 8 bit destination tile + * @param pDstStep byte offset between adjacent rows in destination + */ + static void bitRotateCW(final byte[] pSrc, int pSrcPos, int pSrcStep, + final byte[] pDst, int pDstPos, int pDstStep) { + int idx = pSrcPos; + + int lonyb; + int hinyb; + long lo = 0; + long hi = 0; + + for (int i = 0; i < 8; i++) { + lonyb = pSrc[idx] & 0xF; + hinyb = (pSrc[idx] >> 4) & 0xF; + lo |= RTABLE[i][lonyb]; + hi |= RTABLE[i][hinyb]; + idx += pSrcStep; + } + + idx = pDstPos; + + pDst[idx] = (byte)((hi >> 24) & 0xFF); + idx += pDstStep; + if (idx < pDst.length) { + pDst[idx] = (byte)((hi >> 16) & 0xFF); + idx += pDstStep; + if (idx < pDst.length) { + pDst[idx] = (byte)((hi >> 8) & 0xFF); + idx += pDstStep; + if (idx < pDst.length) { + pDst[idx] = (byte)(hi & 0xFF); + idx += pDstStep; + } + } + } + + if (idx < pDst.length) { + pDst[idx] = (byte)((lo >> 24) & 0xFF); + idx += pDstStep; + if (idx < pDst.length) { + pDst[idx] = (byte)((lo >> 16) & 0xFF); + idx += pDstStep; + if (idx < pDst.length) { + pDst[idx] = (byte)((lo >> 8) & 0xFF); + idx += pDstStep; + if (idx < pDst.length) { + pDst[idx] = (byte)(lo & 0xFF); + } + } + } + } + } +} diff --git a/imageio/imageio-pcx/src/main/java/com/twelvemonkeys/imageio/plugins/pcx/CGAColorModel.java b/imageio/imageio-pcx/src/main/java/com/twelvemonkeys/imageio/plugins/pcx/CGAColorModel.java new file mode 100755 index 00000000..9977c5d6 --- /dev/null +++ b/imageio/imageio-pcx/src/main/java/com/twelvemonkeys/imageio/plugins/pcx/CGAColorModel.java @@ -0,0 +1,83 @@ +package com.twelvemonkeys.imageio.plugins.pcx; + +import java.awt.image.DataBuffer; +import java.awt.image.IndexColorModel; + +final class CGAColorModel { + + // http://en.wikipedia.org/wiki/Color_Graphics_Adapter#Color_palette + private static final int[] CGA_PALETTE = { + // black, blue, green, cyan, red, magenta, brown, light gray + 0x000000, 0x0000aa, 0x00aa00, 0x00aaaa, 0xaa0000, 0xaa00aa, 0xaa5500, 0xaaaaaa, + // gray, light b, light g, light c, light r, light m, yellow, white + 0x555555, 0x5555ff, 0x55ff55, 0x55ffff, 0xff5555, 0xff55ff, 0xffff55, 0xffffff + }; + + static IndexColorModel create(final byte[] cgaMode, final int bitsPerPixel) { + int[] cmap = new int[1 << bitsPerPixel]; + + byte byte0 = cgaMode[0]; + int background = (byte0 & 0xf0) >> 4; + cmap[0] = CGA_PALETTE[background]; + + if (bitsPerPixel == 1) { + // Monochrome + cmap[1] = CGA_PALETTE[0]; + } + else { + // Configured palette + byte byte3 = cgaMode[3]; + + System.err.printf("background: %d\n", background); + System.err.printf("cgaMode: %02x\n", (byte3 & 0xff)); + System.err.printf("cgaMode: %d\n", (byte3 & 0x80) >> 7); + System.err.printf("cgaMode: %d\n", (byte3 & 0x40) >> 6); + System.err.printf("cgaMode: %d\n", (byte3 & 0x20) >> 5); + + boolean colorBurstEnable = (byte3 & 0x80) == 0; + boolean paletteValue = (byte3 & 0x40) != 0; + boolean intensityValue = (byte3 & 0x20) != 0; + + System.err.println("colorBurstEnable: " + colorBurstEnable); + System.err.println("paletteValue: " + paletteValue); + System.err.println("intensityValue: " + intensityValue); + + // Set up the fixed part of the palette + if (colorBurstEnable) { + if (paletteValue) { + if (intensityValue) { + cmap[1] = CGA_PALETTE[11]; + cmap[2] = CGA_PALETTE[13]; + cmap[3] = CGA_PALETTE[15]; + } else { + cmap[1] = CGA_PALETTE[3]; + cmap[2] = CGA_PALETTE[5]; + cmap[3] = CGA_PALETTE[7]; + } + } else { + if (intensityValue) { + cmap[1] = CGA_PALETTE[10]; + cmap[2] = CGA_PALETTE[12]; + cmap[3] = CGA_PALETTE[14]; + } else { + cmap[1] = CGA_PALETTE[2]; + cmap[2] = CGA_PALETTE[4]; + cmap[3] = CGA_PALETTE[6]; + } + } + } else { + if (intensityValue) { + cmap[1] = CGA_PALETTE[11]; + cmap[2] = CGA_PALETTE[12]; + cmap[3] = CGA_PALETTE[15]; + } else { + cmap[1] = CGA_PALETTE[4]; + cmap[2] = CGA_PALETTE[5]; + cmap[3] = CGA_PALETTE[7]; + } + } + } + + return new IndexColorModel(bitsPerPixel, cmap.length, cmap, 0, false, -1, DataBuffer.TYPE_BYTE); + } +} diff --git a/imageio/imageio-pcx/src/main/java/com/twelvemonkeys/imageio/plugins/pcx/PCX.java b/imageio/imageio-pcx/src/main/java/com/twelvemonkeys/imageio/plugins/pcx/PCX.java new file mode 100755 index 00000000..3199b2e0 --- /dev/null +++ b/imageio/imageio-pcx/src/main/java/com/twelvemonkeys/imageio/plugins/pcx/PCX.java @@ -0,0 +1,27 @@ +package com.twelvemonkeys.imageio.plugins.pcx; + +interface PCX { + byte MAGIC = 0x0A; + + int HEADER_SIZE = 128; + + byte VERSION_2_5 = 0; + byte VERSION_2_8_PALETTE = 2; + byte VERSION_2_8_NO_PALETTE = 3; + byte VERSION_2_X_WINDOWS = 4; + byte VERSION_3 = 5; + + /** 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; + + /** Color or BW. */ + int PALETTEINFO_COLOR = 1; + /** Gray. */ + int PALETTEINFO_GRAY = 2; + + /** Magic identifier for VGA palette. */ + byte VGA_PALETTE_MAGIC = 0x0c; +} diff --git a/imageio/imageio-pcx/src/main/java/com/twelvemonkeys/imageio/plugins/pcx/PCXHeader.java b/imageio/imageio-pcx/src/main/java/com/twelvemonkeys/imageio/plugins/pcx/PCXHeader.java new file mode 100755 index 00000000..f2c3eb52 --- /dev/null +++ b/imageio/imageio-pcx/src/main/java/com/twelvemonkeys/imageio/plugins/pcx/PCXHeader.java @@ -0,0 +1,155 @@ +package com.twelvemonkeys.imageio.plugins.pcx; + +import javax.imageio.IIOException; +import javax.imageio.stream.ImageInputStream; + +import java.awt.Rectangle; +import java.awt.image.ColorModel; +import java.awt.image.IndexColorModel; +import java.io.IOException; +import java.util.Arrays; + +final class PCXHeader { + private int version; + private int compression; + private int bitsPerPixel; + private int width; + private int height; + private int hdpi; + private int vdpi; + private byte[] palette; + private int channels; + private int bytesPerLine; + private int paletteInfo; + private int hScreenSize; + private int vScreenSize; + + public int getVersion() { + return version; + } + + public int getCompression() { + return compression; + } + + public int getBitsPerPixel() { + return bitsPerPixel; + } + + public int getWidth() { + return width; + } + + public int getHeight() { + return height; + } + + public int getHdpi() { + return hdpi; + } + + public int getVdpi() { + return vdpi; + } + + public int getChannels() { + return channels; + } + + public int getBytesPerLine() { + return bytesPerLine; + } + + public int getPaletteInfo() { + return paletteInfo; + } + + public IndexColorModel getEGAPalette() { + // TODO: Figure out when/how to enable CGA palette... The test below isn't good enough. +// if (channels == 1 && (bitsPerPixel == 1 || bitsPerPixel == 2)) { +// return CGAColorModel.create(palette, bitsPerPixel); +// } + + int bits = channels * bitsPerPixel; + return new IndexColorModel(bits, Math.min(16, 1 << bits), palette, 0, false); + } + + @Override public String toString() { + return "PCXHeader{" + + "version=" + version + + ", compression=" + compression + + ", bitsPerPixel=" + bitsPerPixel + + ", width=" + width + + ", height=" + height + + ", hdpi=" + hdpi + + ", vdpi=" + vdpi + + ", channels=" + channels + + ", bytesPerLine=" + bytesPerLine + + ", paletteInfo=" + paletteInfo + + ", hScreenSize=" + hScreenSize + + ", vScreenSize=" + vScreenSize + + ", palette=" + Arrays.toString(palette) + + '}'; + } + + public static PCXHeader read(final ImageInputStream imageInput) throws IOException { +// typedef struct _PcxHeader +// { +// BYTE Identifier; /* PCX Id Number (Always 0x0A) */ +// BYTE Version; /* Version Number */ +// BYTE Encoding; /* Encoding Format */ +// BYTE BitsPerPixel; /* Bits per Pixel */ +// WORD XStart; /* Left of image */ +// WORD YStart; /* Top of Image */ +// WORD XEnd; /* Right of Image +// WORD YEnd; /* Bottom of image */ +// WORD HorzRes; /* Horizontal Resolution */ +// WORD VertRes; /* Vertical Resolution */ +// BYTE Palette[48]; /* 16-Color EGA Palette */ +// BYTE Reserved1; /* Reserved (Always 0) */ +// BYTE NumBitPlanes; /* Number of Bit Planes */ +// WORD BytesPerLine; /* Bytes per Scan-line */ +// WORD PaletteType; /* Palette Type */ +// WORD HorzScreenSize; /* Horizontal Screen Size */ +// WORD VertScreenSize; /* Vertical Screen Size */ +// BYTE Reserved2[54]; /* Reserved (Always 0) */ +// } PCXHEAD; + + byte magic = imageInput.readByte(); + if (magic != PCX.MAGIC) { + throw new IIOException(String.format("Not a PCX image. Expected PCX magic %02x, read %02x", PCX.MAGIC, magic)); + } + + PCXHeader header = new PCXHeader(); + + header.version = imageInput.readUnsignedByte(); + header.compression = imageInput.readUnsignedByte(); + header.bitsPerPixel = imageInput.readUnsignedByte(); + + int xStart = imageInput.readUnsignedShort(); + int yStart = imageInput.readUnsignedShort(); + header.width = imageInput.readUnsignedShort() - xStart + 1; + header.height = imageInput.readUnsignedShort() - yStart + 1; + + header.hdpi = imageInput.readUnsignedShort(); + header.vdpi = imageInput.readUnsignedShort(); + + byte[] palette = new byte[48]; + imageInput.readFully(palette); // 16 RGB triplets + header.palette = palette; + + imageInput.readUnsignedByte(); // Reserved, should be 0 + + header.channels = imageInput.readUnsignedByte(); + header.bytesPerLine = imageInput.readUnsignedShort(); // Must be even! + + header.paletteInfo = imageInput.readUnsignedShort(); // 1 == Color/BW, 2 == Gray + + header.hScreenSize = imageInput.readUnsignedShort(); + header.vScreenSize = imageInput.readUnsignedShort(); + + imageInput.skipBytes(PCX.HEADER_SIZE - imageInput.getStreamPosition()); + + return header; + } +} diff --git a/imageio/imageio-pcx/src/main/java/com/twelvemonkeys/imageio/plugins/pcx/PCXImageReader.java b/imageio/imageio-pcx/src/main/java/com/twelvemonkeys/imageio/plugins/pcx/PCXImageReader.java new file mode 100755 index 00000000..dfcca950 --- /dev/null +++ b/imageio/imageio-pcx/src/main/java/com/twelvemonkeys/imageio/plugins/pcx/PCXImageReader.java @@ -0,0 +1,425 @@ +package com.twelvemonkeys.imageio.plugins.pcx; + +import java.awt.Rectangle; +import java.awt.color.ColorSpace; +import java.awt.image.BufferedImage; +import java.awt.image.DataBuffer; +import java.awt.image.DataBufferByte; +import java.awt.image.IndexColorModel; +import java.awt.image.Raster; +import java.awt.image.WritableRaster; +import java.io.DataInput; +import java.io.DataInputStream; +import java.io.EOFException; +import java.io.File; +import java.io.IOException; +import java.nio.ByteOrder; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Iterator; +import java.util.List; + +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 com.twelvemonkeys.imageio.ImageReaderBase; +import com.twelvemonkeys.imageio.util.IIOUtil; +import com.twelvemonkeys.imageio.util.IndexedImageTypeSpecifier; +import com.twelvemonkeys.io.enc.DecoderStream; +import com.twelvemonkeys.xml.XMLSerializer; + +public final class PCXImageReader extends ImageReaderBase { + /** 8 bit ImageTypeSpecifer used for reading bitplane images. */ + private static final ImageTypeSpecifier GRAYSCALE = ImageTypeSpecifier.createGrayscale(8, DataBuffer.TYPE_BYTE, false); + + private PCXHeader header; + private boolean readPalette; + private IndexColorModel vgaPalette; + + public PCXImageReader(final ImageReaderSpi provider) { + super(provider); + } + + @Override + protected void resetMembers() { + header = null; + readPalette = false; + vgaPalette = 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(); + + int channels = header.getChannels(); + int paletteInfo = header.getPaletteInfo(); + ColorSpace cs = paletteInfo == PCX.PALETTEINFO_GRAY ? ColorSpace.getInstance(ColorSpace.CS_GRAY) : ColorSpace.getInstance(ColorSpace.CS_sRGB); + + switch (header.getBitsPerPixel()) { + case 1: + case 2: + case 4: + return IndexedImageTypeSpecifier.createFromIndexColorModel(header.getEGAPalette()); + case 8: + // We may have IndexColorModel here for 1 channel images + if (channels == 1 && paletteInfo != PCX.PALETTEINFO_GRAY) { + IndexColorModel palette = getVGAPalette(); + if (palette == null) { + throw new IIOException("Expected VGA palette not found"); + } + + return IndexedImageTypeSpecifier.createFromIndexColorModel(palette); + } + + // PCX has 1 or 3 channels for 8 bit gray or 24 bit RGB, will be validated by ImageTypeSpecifier + return ImageTypeSpecifier.createBanded(cs, createIndices(channels, 1), createIndices(channels, 0), DataBuffer.TYPE_BYTE, false, false); + case 24: + // Some sources says this is possible... Untested. + return ImageTypeSpecifier.createInterleaved(cs, createIndices(channels, 0), DataBuffer.TYPE_BYTE, false, false); + default: + throw new IIOException("Unknown number of bytes per pixel: " + header.getBitsPerPixel()); + } + } + + 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.getPaletteInfo() != PCX.PALETTEINFO_COLOR && header.getPaletteInfo() != PCX.PALETTEINFO_GRAY) { + processWarningOccurred(String.format("Unsupported color mode: %d, colors may look incorrect", header.getPaletteInfo())); + } + + 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()); + + int compression = header.getCompression(); + + // Wrap input (COMPRESSION_RLE is really the only value allowed) + DataInput input = compression == PCX.COMPRESSION_RLE + ? new DataInputStream(new DecoderStream(IIOUtil.createStreamAdapter(imageInput), new RLEDecoder())) + : imageInput; + + int xSub = param != null ? param.getSourceXSubsampling() : 1; + int ySub = param != null ? param.getSourceYSubsampling() : 1; + + processImageStarted(imageIndex); + + if (rawType.getColorModel() instanceof IndexColorModel && header.getChannels() > 1) { + // Bit planes! + // Create raster from a default 8 bit layout + WritableRaster rowRaster = GRAYSCALE.createBufferedImage(header.getWidth(), 1).getRaster(); + + // Clip to source region + Raster clippedRow = clipRowToRect(rowRaster, srcRegion, + param != null ? param.getSourceBands() : null, + param != null ? param.getSourceXSubsampling() : 1); + + int planeWidth = header.getBytesPerLine(); + byte[] planeData = new byte[planeWidth * 8]; + + byte[] rowDataByte = ((DataBufferByte) rowRaster.getDataBuffer()).getData(); + + for (int y = 0; y < height; y++) { + switch (header.getBitsPerPixel()) { + case 1: + readRowByte(input, srcRegion, xSub, ySub, planeData, 0, planeWidth * header.getChannels(), destRaster, clippedRow, y); + break; + default: + throw new AssertionError(); + } + + int pixelPos = 0; + for (int planePos = 0; planePos < planeWidth; planePos++) { + BitRotator.bitRotateCW(planeData, planePos, planeWidth, rowDataByte, pixelPos, 1); + pixelPos += 8; + } + + processImageProgress(100f * y / height); + + if (y < srcRegion.y) { + break; + } + + if (abortRequested()) { + processReadAborted(); + break; + } + } + } + else { + // Can't use width here, as we need to take bytesPerLine into account, and re-create a width based on this + int rowWidth = (header.getBytesPerLine() * 8) / header.getBitsPerPixel(); + WritableRaster rowRaster = rawType.createBufferedImage(rowWidth, 1).getRaster(); + + // Clip to source region + Raster clippedRow = clipRowToRect(rowRaster, srcRegion, + param != null ? param.getSourceBands() : null, + param != null ? param.getSourceXSubsampling() : 1); + + for (int y = 0; y < height; y++) { + 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}); + + switch (header.getBitsPerPixel()) { + case 1: + case 2: + case 4: + case 8: + byte[] rowDataByte = ((DataBufferByte) rowRaster.getDataBuffer()).getData(c); + readRowByte(input, srcRegion, xSub, ySub, rowDataByte, 0, rowDataByte.length, destChannel, srcChannel, y); + break; + default: + throw new AssertionError(); + } + + processImageProgress(100f * y / height * c / header.getChannels()); + + if (y < srcRegion.y) { + break; + } + + if (abortRequested()) { + break; + } + } + + if (abortRequested()) { + processReadAborted(); + break; + } + } + } + + processImageComplete(); + + return destination; + } + + private void readRowByte(final DataInput input, + Rectangle srcRegion, + int xSub, + int ySub, + byte[] rowDataByte, final int off, final int length, + WritableRaster destChannel, + Raster srcChannel, + int y) throws IOException { + // If subsampled or outside source region, skip entire row + if (y % ySub != 0 || y < srcRegion.y || y >= srcRegion.y + srcRegion.height) { + input.skipBytes(length); + + return; + } + + input.readFully(rowDataByte, off, length); + + // Subsample horizontal + if (xSub != 1) { + for (int x = 0; x < srcRegion.width / xSub; x++) { + rowDataByte[srcRegion.x + x] = rowDataByte[srcRegion.x + x * xSub]; + } + } + + int dstY = (y - srcRegion.y) / ySub; + destChannel.setDataElements(0, dstY, srcChannel); + } + + 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) { + imageInput.setByteOrder(ByteOrder.LITTLE_ENDIAN); + header = PCXHeader.read(imageInput); + imageInput.flushBefore(imageInput.getStreamPosition()); + } + + imageInput.seek(imageInput.getFlushedPosition()); + } + + @Override public IIOMetadata getImageMetadata(final int imageIndex) throws IOException { + checkBounds(imageIndex); + readHeader(); + + return new PCXMetadata(header, getVGAPalette()); + } + + private IndexColorModel getVGAPalette() throws IOException { + if (!readPalette) { + readHeader(); + + // Mark palette as read, to avoid further attempts + readPalette = true; + + // Wee can't simply skip to an offset, as the RLE compression makes the file size unpredictable + skiptToEOF(imageInput); + + // Seek backwards from EOF + long paletteStart = imageInput.getStreamPosition() - 769; + if (paletteStart <= imageInput.getFlushedPosition()) { + return null; + } + + imageInput.seek(paletteStart); + + byte val = imageInput.readByte(); + + if (val == PCX.VGA_PALETTE_MAGIC) { + byte[] palette = new byte[768]; // 256 * 3 for RGB + imageInput.readFully(palette); + + vgaPalette = new IndexColorModel(8, 256, palette, 0, false); + + return vgaPalette; + } + + return null; + } + + return vgaPalette; + } + + // TODO: Candidate util method + private static long skiptToEOF(final ImageInputStream stream) throws IOException { + long length = stream.length(); + + if (length > 0) { + // Known length, skip there and we're done. + stream.seek(length); + } + else { + // Otherwise, seek to EOF the hard way. + // First, store stream position... + long pos = stream.getStreamPosition(); + + // ...skip 1k blocks until we're passed EOF... + while (stream.skipBytes(1024l) > 0) { + if (stream.read() == -1) { + break; + } + + pos = stream.getStreamPosition(); + } + + // ...go back to last known pos... + stream.seek(pos); + + // ...finally seek until EOF one byte at a time. Done. + while (stream.read() != -1) { + } + } + + return stream.getStreamPosition(); + } + + public static void main(String[] args) throws IOException { + PCXImageReader reader = new PCXImageReader(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.getHdpi(0); +// int height = reader.getVdpi(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)); + + System.err.println("header: " + reader.header); + + 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-pcx/src/main/java/com/twelvemonkeys/imageio/plugins/pcx/PCXImageReaderSpi.java b/imageio/imageio-pcx/src/main/java/com/twelvemonkeys/imageio/plugins/pcx/PCXImageReaderSpi.java new file mode 100755 index 00000000..57b1a8eb --- /dev/null +++ b/imageio/imageio-pcx/src/main/java/com/twelvemonkeys/imageio/plugins/pcx/PCXImageReaderSpi.java @@ -0,0 +1,93 @@ +package com.twelvemonkeys.imageio.plugins.pcx; + +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 PCXImageReaderSpi extends ImageReaderSpi { + + /** + * Creates a {@code PCXImageReaderSpi}. + */ + public PCXImageReaderSpi() { + this(IIOUtil.getProviderInfo(PCXImageReaderSpi.class)); + } + + private PCXImageReaderSpi(final ProviderInfo providerInfo) { + super( + providerInfo.getVendorName(), + providerInfo.getVersion(), + new String[]{ + "pcx", + "PCX" + }, + new String[]{"pcx"}, + new String[]{ + // No official IANA record exists + "image/pcx", + "image/x-pcx", + }, + "com.twelvemkonkeys.imageio.plugins.pcx.PCXImageReader", + 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 { + byte magic = stream.readByte(); + + switch (magic) { + case PCX.MAGIC: + byte version = stream.readByte(); + + switch (version) { + case PCX.VERSION_2_5: + case PCX.VERSION_2_8_PALETTE: + case PCX.VERSION_2_8_NO_PALETTE: + case PCX.VERSION_2_X_WINDOWS: + case PCX.VERSION_3: + byte compression = stream.readByte(); + byte bpp = stream.readByte(); + + return (compression == PCX.COMPRESSION_NONE || compression == PCX.COMPRESSION_RLE) && (bpp == 1 || bpp == 2 || bpp == 4 || bpp == 8); + default: + return false; + } + default: + return false; + } + } + finally { + stream.reset(); + } + } + + @Override public ImageReader createReaderInstance(final Object extension) throws IOException { + return new PCXImageReader(this); + } + + @Override public String getDescription(final Locale locale) { + return "PC Paintbrush (PCX) image reader"; + } +} + diff --git a/imageio/imageio-pcx/src/main/java/com/twelvemonkeys/imageio/plugins/pcx/PCXMetadata.java b/imageio/imageio-pcx/src/main/java/com/twelvemonkeys/imageio/plugins/pcx/PCXMetadata.java new file mode 100755 index 00000000..6144a9c9 --- /dev/null +++ b/imageio/imageio-pcx/src/main/java/com/twelvemonkeys/imageio/plugins/pcx/PCXMetadata.java @@ -0,0 +1,210 @@ +package com.twelvemonkeys.imageio.plugins.pcx; + +import java.awt.image.DataBuffer; +import java.awt.image.IndexColorModel; + +import javax.imageio.IIOException; +import javax.imageio.ImageTypeSpecifier; +import javax.imageio.metadata.IIOMetadata; +import javax.imageio.metadata.IIOMetadataFormatImpl; +import javax.imageio.metadata.IIOMetadataNode; + +import com.twelvemonkeys.imageio.util.IndexedImageTypeSpecifier; +import org.w3c.dom.Node; + +final class PCXMetadata extends IIOMetadata { + // TODO: Clean up & extend AbstractMetadata (after moving from PSD -> Core) + + private final PCXHeader header; + private final IndexColorModel vgaPalette; + + PCXMetadata(final PCXHeader header, final IndexColorModel vgaPalette) { + this.header = header; + this.vgaPalette = vgaPalette; + + 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"); + + IndexColorModel palette = null; + + IIOMetadataNode csType = new IIOMetadataNode("ColorSpaceType"); + switch (header.getBitsPerPixel()) { + case 1: + case 2: + case 4: + palette = header.getEGAPalette(); + csType.setAttribute("name", "RGB"); + break; + case 8: + // We may have IndexColorModel here for 1 channel images + if (header.getChannels() == 1 && header.getPaletteInfo() != PCX.PALETTEINFO_GRAY) { + palette = vgaPalette; + csType.setAttribute("name", "RGB"); + break; + } + if (header.getChannels() == 1) { + csType.setAttribute("name", "GRAY"); + break; + } + csType.setAttribute("name", "RGB"); + break; + + case 24: + // Some sources says this is possible... Untested. + csType.setAttribute("name", "RGB"); + break; + + default: + csType.setAttribute("name", "Unknown"); + } + + chroma.appendChild(csType); + + if (palette != null) { + IIOMetadataNode paletteNode = new IIOMetadataNode("Palette"); + chroma.appendChild(paletteNode); + + for (int i = 0; i < palette.getMapSize(); i++) { + IIOMetadataNode paletteEntry = new IIOMetadataNode("PaletteEntry"); + paletteEntry.setAttribute("index", Integer.toString(i)); + + paletteEntry.setAttribute("red", Integer.toString(palette.getRed(i))); + paletteEntry.setAttribute("green", Integer.toString(palette.getGreen(i))); + paletteEntry.setAttribute("blue", Integer.toString(palette.getBlue(i))); + + paletteNode.appendChild(paletteEntry); + } + } + + // TODO: Channels in chroma node should reflect channels in color model, not data! (see data node) + 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() != PCX.COMPRESSION_NONE) { + IIOMetadataNode node = new IIOMetadataNode("Compression"); + + IIOMetadataNode compressionTypeName = new IIOMetadataNode("CompressionTypeName"); + compressionTypeName.setAttribute("value", header.getCompression() == PCX.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"); + + // Planar configuration only makes sense for multi-channel images + if (header.getChannels() > 1) { + IIOMetadataNode planarConfiguration = new IIOMetadataNode("PlanarConfiguration"); + planarConfiguration.setAttribute("value", "LineInterleaved"); + node.appendChild(planarConfiguration); + } + + // TODO: SampleFormat value = Index if colormapped/palette 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.getBitsPerPixel()))); + node.appendChild(bitsPerSample); + + IIOMetadataNode significantBitsPerSample = new IIOMetadataNode("SignificantBitsPerSample"); + significantBitsPerSample.setAttribute("value", createListValue(header.getChannels(), Integer.toString(header.getBitsPerPixel()))); + node.appendChild(significantBitsPerSample); + + IIOMetadataNode sampleMSB = new IIOMetadataNode("SampleMSB"); + sampleMSB.setAttribute("value", createListValue(header.getChannels(), "0")); + + return node; + } + + 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", "Normal"); + dimension.appendChild(imageOrientation); + + return dimension; + } + + // TODO: document node with version + + // No text node + + // 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-pcx/src/main/java/com/twelvemonkeys/imageio/plugins/pcx/RLEDecoder.java b/imageio/imageio-pcx/src/main/java/com/twelvemonkeys/imageio/plugins/pcx/RLEDecoder.java new file mode 100755 index 00000000..9f6608b8 --- /dev/null +++ b/imageio/imageio-pcx/src/main/java/com/twelvemonkeys/imageio/plugins/pcx/RLEDecoder.java @@ -0,0 +1,43 @@ +package com.twelvemonkeys.imageio.plugins.pcx; + +import com.twelvemonkeys.io.enc.Decoder; + +import java.io.IOException; +import java.io.InputStream; +import java.nio.ByteBuffer; + +final class RLEDecoder implements Decoder { + + static final int COMPRESSED_RUN_MASK = 0xc0; + + // A rather strange and inefficient RLE encoding, but it probably made sense at the time... + // Uses the upper two bits to flag if the next values are to be treated as a compressed run. + // This means that any value above 0b11000000/0xc0/192 must be encoded as a compressed run, + // even if this will make the output larger. + public int decode(final InputStream stream, final ByteBuffer buffer) throws IOException { + while (buffer.remaining() >= 64) { + int val = stream.read(); + if (val < 0) { + break; // EOF + } + + if ((val & COMPRESSED_RUN_MASK) == COMPRESSED_RUN_MASK) { + int count = val & ~COMPRESSED_RUN_MASK; + + int pixel = stream.read(); + if (pixel < 0) { + break; // EOF + } + + for (int i = 0; i < count; i++) { + buffer.put((byte) pixel); + } + } + else { + buffer.put((byte) val); + } + } + + return buffer.position(); + } +} diff --git a/imageio/imageio-pcx/src/main/resources/META-INF/services/javax.imageio.spi.ImageReaderSpi b/imageio/imageio-pcx/src/main/resources/META-INF/services/javax.imageio.spi.ImageReaderSpi new file mode 100755 index 00000000..49b60604 --- /dev/null +++ b/imageio/imageio-pcx/src/main/resources/META-INF/services/javax.imageio.spi.ImageReaderSpi @@ -0,0 +1,2 @@ +com.twelvemonkeys.imageio.plugins.pcx.PCXImageReaderSpi +com.twelvemonkeys.imageio.plugins.dcx.DCXImageReaderSpi diff --git a/imageio/imageio-pcx/src/test/java/com/twelvemonkeys/imageio/plugins/dcx/DCXImageReaderTest.java b/imageio/imageio-pcx/src/test/java/com/twelvemonkeys/imageio/plugins/dcx/DCXImageReaderTest.java new file mode 100755 index 00000000..2aead130 --- /dev/null +++ b/imageio/imageio-pcx/src/test/java/com/twelvemonkeys/imageio/plugins/dcx/DCXImageReaderTest.java @@ -0,0 +1,85 @@ +/* + * 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.dcx; + +import java.awt.Dimension; +import java.util.Arrays; +import java.util.List; + +import javax.imageio.spi.ImageReaderSpi; + +import com.twelvemonkeys.imageio.util.ImageReaderAbstractTestCase; + +/** + * DCXImageReaderTest + * + * @author Harald Kuhr + * @author last modified by $Author: haraldk$ + * @version $Id: DCXImageReaderTest.java,v 1.0 03.07.14 22:28 haraldk Exp$ + */ +public class DCXImageReaderTest extends ImageReaderAbstractTestCase { + @Override + protected List getTestData() { + return Arrays.asList( + new TestData(getClassLoaderResource("/dcx/input.dcx"), new Dimension(70, 46)) // RLE encoded RGB (the only sample I've found) + ); + } + + @Override + protected ImageReaderSpi createProvider() { + return new DCXImageReaderSpi(); + } + + @Override + protected Class getReaderClass() { + return DCXImageReader.class; + } + + @Override + protected DCXImageReader createReader() { + return new DCXImageReader(createProvider()); + } + + @Override + protected List getFormatNames() { + return Arrays.asList("DCX", "dcx"); + } + + @Override + protected List getSuffixes() { + return Arrays.asList("dcx"); + } + + @Override + protected List getMIMETypes() { + return Arrays.asList( + "image/dcx", "image/x-dcx" + ); + } +} diff --git a/imageio/imageio-pcx/src/test/java/com/twelvemonkeys/imageio/plugins/pcx/PCXImageReaderTest.java b/imageio/imageio-pcx/src/test/java/com/twelvemonkeys/imageio/plugins/pcx/PCXImageReaderTest.java new file mode 100755 index 00000000..7fc1e107 --- /dev/null +++ b/imageio/imageio-pcx/src/test/java/com/twelvemonkeys/imageio/plugins/pcx/PCXImageReaderTest.java @@ -0,0 +1,101 @@ +/* + * 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.pcx; + +import com.twelvemonkeys.imageio.util.ImageReaderAbstractTestCase; + +import javax.imageio.spi.ImageReaderSpi; +import java.awt.*; +import java.util.Arrays; +import java.util.List; + +/** + * PCXImageReaderTest + * + * @author Harald Kuhr + * @author last modified by $Author: haraldk$ + * @version $Id: PCXImageReaderTest.java,v 1.0 03.07.14 22:28 haraldk Exp$ + */ +public class PCXImageReaderTest extends ImageReaderAbstractTestCase { + @Override + protected List getTestData() { + return Arrays.asList( + new TestData(getClassLoaderResource("/pcx/MARBLES.PCX"), new Dimension(1419, 1001)), // RLE encoded RGB +// new TestData(getClassLoaderResource("/pcx/GMARBLES.PCX"), new Dimension(1419, 1001)) // RLE encoded gray (seems to be damanged, missing the last few scan lines) + new TestData(getClassLoaderResource("/pcx/lena.pcx"), new Dimension(512, 512)), // RLE encoded RGB + new TestData(getClassLoaderResource("/pcx/lena2.pcx"), new Dimension(512, 512)), // RLE encoded, 256 color indexed (8 bps/1 channel) + new TestData(getClassLoaderResource("/pcx/lena3.pcx"), new Dimension(512, 512)), // RLE encoded, 16 color indexed (4 bps/1 channel) + new TestData(getClassLoaderResource("/pcx/lena4.pcx"), new Dimension(512, 512)), // RLE encoded, 16 color indexed (1 bps/4 channels) + new TestData(getClassLoaderResource("/pcx/lena5.pcx"), new Dimension(512, 512)), // RLE encoded, 256 color indexed (8 bps/1 channel) + new TestData(getClassLoaderResource("/pcx/lena6.pcx"), new Dimension(512, 512)), // RLE encoded, 8 colorindexed (1 bps/3 channels) + new TestData(getClassLoaderResource("/pcx/lena7.pcx"), new Dimension(512, 512)), // RLE encoded, 4 color indexed (1 bps/2 channels) + new TestData(getClassLoaderResource("/pcx/lena8.pcx"), new Dimension(512, 512)), // RLE encoded, 4 color indexed (2 bps/1 channel) + new TestData(getClassLoaderResource("/pcx/lena9.pcx"), new Dimension(512, 512)), // RLE encoded, 2 color indexed (1 bps/1 channel) + new TestData(getClassLoaderResource("/pcx/lena10.pcx"), new Dimension(512, 512)), // RLE encoded, 16 color indexed (4 bps/1 channel) (uses only 8 colors) + new TestData(getClassLoaderResource("/pcx/DARKSTAR.PCX"), new Dimension(88, 52)), // RLE encoded monochrome (1 bps/1 channel) + // TODO: Get correct colors for CGA mode, see cga-pcx.txt (however, the text seems to be in error, the bits are not as described) + new TestData(getClassLoaderResource("/pcx/CGA_BW.PCX"), new Dimension(640, 200)), // RLE encoded indexed (CGA mode) + new TestData(getClassLoaderResource("/pcx/CGA_FSD.PCX"), new Dimension(320, 200)), // RLE encoded indexed (CGA mode) + new TestData(getClassLoaderResource("/pcx/CGA_RGBI.PCX"), new Dimension(320, 200)), // RLE encoded indexed (CGA mode) + new TestData(getClassLoaderResource("/pcx/CGA_TST1.PCX"), new Dimension(320, 200)) // RLE encoded indexed (CGA mode) + ); + } + + @Override + protected ImageReaderSpi createProvider() { + return new PCXImageReaderSpi(); + } + + @Override + protected Class getReaderClass() { + return PCXImageReader.class; + } + + @Override + protected PCXImageReader createReader() { + return new PCXImageReader(createProvider()); + } + + @Override + protected List getFormatNames() { + return Arrays.asList("PCX", "pcx"); + } + + @Override + protected List getSuffixes() { + return Arrays.asList("pcx"); + } + + @Override + protected List getMIMETypes() { + return Arrays.asList( + "image/pcx", "image/x-pcx" + ); + } +} diff --git a/imageio/imageio-pcx/src/test/resources/dcx/input.dcx b/imageio/imageio-pcx/src/test/resources/dcx/input.dcx new file mode 100755 index 00000000..15897a6a Binary files /dev/null and b/imageio/imageio-pcx/src/test/resources/dcx/input.dcx differ diff --git a/imageio/imageio-pcx/src/test/resources/pcx/CGA_BW.PCX b/imageio/imageio-pcx/src/test/resources/pcx/CGA_BW.PCX new file mode 100755 index 00000000..d135810d Binary files /dev/null and b/imageio/imageio-pcx/src/test/resources/pcx/CGA_BW.PCX differ diff --git a/imageio/imageio-pcx/src/test/resources/pcx/CGA_FSD.PCX b/imageio/imageio-pcx/src/test/resources/pcx/CGA_FSD.PCX new file mode 100755 index 00000000..242429a6 Binary files /dev/null and b/imageio/imageio-pcx/src/test/resources/pcx/CGA_FSD.PCX differ diff --git a/imageio/imageio-pcx/src/test/resources/pcx/CGA_RGBI.PCX b/imageio/imageio-pcx/src/test/resources/pcx/CGA_RGBI.PCX new file mode 100755 index 00000000..e341f804 Binary files /dev/null and b/imageio/imageio-pcx/src/test/resources/pcx/CGA_RGBI.PCX differ diff --git a/imageio/imageio-pcx/src/test/resources/pcx/CGA_TST1.PCX b/imageio/imageio-pcx/src/test/resources/pcx/CGA_TST1.PCX new file mode 100755 index 00000000..6f492d91 Binary files /dev/null and b/imageio/imageio-pcx/src/test/resources/pcx/CGA_TST1.PCX differ diff --git a/imageio/imageio-pcx/src/test/resources/pcx/DARKSTAR.PCX b/imageio/imageio-pcx/src/test/resources/pcx/DARKSTAR.PCX new file mode 100755 index 00000000..292cd6d3 Binary files /dev/null and b/imageio/imageio-pcx/src/test/resources/pcx/DARKSTAR.PCX differ diff --git a/imageio/imageio-pcx/src/test/resources/pcx/GMARBLES.PCX b/imageio/imageio-pcx/src/test/resources/pcx/GMARBLES.PCX new file mode 100755 index 00000000..d15658ab Binary files /dev/null and b/imageio/imageio-pcx/src/test/resources/pcx/GMARBLES.PCX differ diff --git a/imageio/imageio-pcx/src/test/resources/pcx/MARBLES.PCX b/imageio/imageio-pcx/src/test/resources/pcx/MARBLES.PCX new file mode 100755 index 00000000..8ddaae4c Binary files /dev/null and b/imageio/imageio-pcx/src/test/resources/pcx/MARBLES.PCX differ diff --git a/imageio/imageio-pcx/src/test/resources/pcx/cga-pcx.txt b/imageio/imageio-pcx/src/test/resources/pcx/cga-pcx.txt new file mode 100755 index 00000000..03f357bd --- /dev/null +++ b/imageio/imageio-pcx/src/test/resources/pcx/cga-pcx.txt @@ -0,0 +1,33 @@ +From Jim Leonard (trixter -at- oldskool.org) + +Here you go. I made these myself, and made sure to flex all of the +PCX format's CGA attributes. These all load perfectly under *REAL* +PC Paintbrush 4.0 in DOS on a CGA card, so if your decoder screws it +up, it's the fault of your decoder! + +They are: + +CGA_FSD.PCX Standard lightcyan-lightmagenta-white palette (intensity +bit set) with black background. When most people think of CGA, they're +thinking of this. + +0x00 +000 0 0000 + +CGA_TST1.PCX Different palette (intensite bit NOT set) and different +background color. If you can read the text inside the box at the bottom, +your decoder is shite :-) + +0xf0 +111 1 0000 + + +CGA_RGBI.PCX Less commonly-used lightred-lightgreen-yellow palette +(intensity bit set) with a blue background. + +0xa4 +101 0 0100 + + +CGA_BW.PCX 640x200 B&W pic in case he didn't have a sample. +Most PCX decoders handle this just fine. diff --git a/imageio/imageio-pcx/src/test/resources/pcx/lena.pcx b/imageio/imageio-pcx/src/test/resources/pcx/lena.pcx new file mode 100755 index 00000000..7a00c7f2 Binary files /dev/null and b/imageio/imageio-pcx/src/test/resources/pcx/lena.pcx differ diff --git a/imageio/imageio-pcx/src/test/resources/pcx/lena10.pcx b/imageio/imageio-pcx/src/test/resources/pcx/lena10.pcx new file mode 100755 index 00000000..91447bc3 Binary files /dev/null and b/imageio/imageio-pcx/src/test/resources/pcx/lena10.pcx differ diff --git a/imageio/imageio-pcx/src/test/resources/pcx/lena2.pcx b/imageio/imageio-pcx/src/test/resources/pcx/lena2.pcx new file mode 100755 index 00000000..00ce7fd9 Binary files /dev/null and b/imageio/imageio-pcx/src/test/resources/pcx/lena2.pcx differ diff --git a/imageio/imageio-pcx/src/test/resources/pcx/lena3.pcx b/imageio/imageio-pcx/src/test/resources/pcx/lena3.pcx new file mode 100755 index 00000000..22499655 Binary files /dev/null and b/imageio/imageio-pcx/src/test/resources/pcx/lena3.pcx differ diff --git a/imageio/imageio-pcx/src/test/resources/pcx/lena4.pcx b/imageio/imageio-pcx/src/test/resources/pcx/lena4.pcx new file mode 100755 index 00000000..30a625c7 Binary files /dev/null and b/imageio/imageio-pcx/src/test/resources/pcx/lena4.pcx differ diff --git a/imageio/imageio-pcx/src/test/resources/pcx/lena5.pcx b/imageio/imageio-pcx/src/test/resources/pcx/lena5.pcx new file mode 100755 index 00000000..00ce7fd9 Binary files /dev/null and b/imageio/imageio-pcx/src/test/resources/pcx/lena5.pcx differ diff --git a/imageio/imageio-pcx/src/test/resources/pcx/lena6.pcx b/imageio/imageio-pcx/src/test/resources/pcx/lena6.pcx new file mode 100755 index 00000000..397c84bf Binary files /dev/null and b/imageio/imageio-pcx/src/test/resources/pcx/lena6.pcx differ diff --git a/imageio/imageio-pcx/src/test/resources/pcx/lena7.pcx b/imageio/imageio-pcx/src/test/resources/pcx/lena7.pcx new file mode 100755 index 00000000..bb209100 Binary files /dev/null and b/imageio/imageio-pcx/src/test/resources/pcx/lena7.pcx differ diff --git a/imageio/imageio-pcx/src/test/resources/pcx/lena8.pcx b/imageio/imageio-pcx/src/test/resources/pcx/lena8.pcx new file mode 100755 index 00000000..efa89d4e Binary files /dev/null and b/imageio/imageio-pcx/src/test/resources/pcx/lena8.pcx differ diff --git a/imageio/imageio-pcx/src/test/resources/pcx/lena9.pcx b/imageio/imageio-pcx/src/test/resources/pcx/lena9.pcx new file mode 100755 index 00000000..064028cd Binary files /dev/null and b/imageio/imageio-pcx/src/test/resources/pcx/lena9.pcx differ diff --git a/imageio/pom.xml b/imageio/pom.xml index ab2d7b0c..391574bf 100644 --- a/imageio/pom.xml +++ b/imageio/pom.xml @@ -32,6 +32,7 @@ imageio-icns imageio-iff imageio-jpeg + imageio-pcx imageio-pdf imageio-pict imageio-pnm