TMI-PCX: Initial commit.

This commit is contained in:
Harald Kuhr
2014-10-01 14:34:51 +02:00
parent 74637105b8
commit 5313ffd6fc
37 changed files with 1758 additions and 0 deletions
@@ -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 <a href="http://www.fileformat.info/format/pcx/egff.htm#PCX-DMYID.3.8">[PCX] Related File Formats</a>
*/
interface DCX {
int MAGIC = 0x3ADE68B1;
}
@@ -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) +
'}';
}
}
@@ -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<ImageTypeSpecifier> 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;
}
}
}
@@ -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";
}}
@@ -0,0 +1,99 @@
package com.twelvemonkeys.imageio.plugins.pcx;
/**
* IFFUtil
* <p/>
* 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);
}
}
}
}
}
}
@@ -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);
}
}
@@ -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;
}
@@ -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;
}
}
@@ -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<ImageTypeSpecifier> getImageTypes(final int imageIndex) throws IOException {
ImageTypeSpecifier rawType = getRawImageType(imageIndex);
List<ImageTypeSpecifier> specifiers = new ArrayList<ImageTypeSpecifier>();
// 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<ImageTypeSpecifier> 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;
}
}
}
@@ -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";
}
}
@@ -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;
}
}
@@ -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();
}
}
@@ -0,0 +1,2 @@
com.twelvemonkeys.imageio.plugins.pcx.PCXImageReaderSpi
com.twelvemonkeys.imageio.plugins.dcx.DCXImageReaderSpi