#400 Better handling of colors, now uses fallback B/W for most bi-level images.

Gray image now uses palette if present.
PaletteInfo header flag is ignored and no longer outputs warning.
This commit is contained in:
Harald Kuhr 2018-01-09 19:49:38 +01:00
parent c8a19418eb
commit 27a6ae6ffc
5 changed files with 162 additions and 104 deletions

View File

@ -56,30 +56,25 @@ final class CGAColorModel {
// Configured palette // Configured palette
byte byte3 = cgaMode[3]; byte byte3 = cgaMode[3];
System.err.printf("background: %d\n", background); boolean colorBurstEnable = (byte3 & 0x80) != 0;
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 paletteValue = (byte3 & 0x40) != 0;
boolean intensityValue = (byte3 & 0x20) != 0; boolean intensityValue = (byte3 & 0x20) != 0;
if (PCXImageReader.DEBUG) {
System.err.println("colorBurstEnable: " + colorBurstEnable); System.err.println("colorBurstEnable: " + colorBurstEnable);
System.err.println("paletteValue: " + paletteValue); System.err.println("paletteValue: " + paletteValue);
System.err.println("intensityValue: " + intensityValue); System.err.println("intensityValue: " + intensityValue);
}
// Set up the fixed part of the palette // Set up the fixed part of the palette
if (colorBurstEnable) {
if (paletteValue) { if (paletteValue) {
if (intensityValue) { if (intensityValue) {
cmap[1] = CGA_PALETTE[11]; cmap[1] = CGA_PALETTE[11];
cmap[2] = CGA_PALETTE[13]; cmap[2] = colorBurstEnable ? CGA_PALETTE[13] : CGA_PALETTE[12];
cmap[3] = CGA_PALETTE[15]; cmap[3] = CGA_PALETTE[15];
} else { } else {
cmap[1] = CGA_PALETTE[3]; cmap[1] = CGA_PALETTE[3];
cmap[2] = CGA_PALETTE[5]; cmap[2] = colorBurstEnable ? CGA_PALETTE[5] : CGA_PALETTE[4];
cmap[3] = CGA_PALETTE[7]; cmap[3] = CGA_PALETTE[7];
} }
} else { } else {
@ -93,17 +88,6 @@ final class CGAColorModel {
cmap[3] = CGA_PALETTE[6]; 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); return new IndexColorModel(bitsPerPixel, cmap.length, cmap, 0, false, -1, DataBuffer.TYPE_BYTE);

View File

@ -30,11 +30,14 @@ package com.twelvemonkeys.imageio.plugins.pcx;
import javax.imageio.IIOException; import javax.imageio.IIOException;
import javax.imageio.stream.ImageInputStream; import javax.imageio.stream.ImageInputStream;
import java.awt.image.DataBuffer;
import java.awt.image.IndexColorModel; import java.awt.image.IndexColorModel;
import java.io.IOException; import java.io.IOException;
import java.util.Arrays; import java.util.Arrays;
final class PCXHeader { final class PCXHeader {
private static final IndexColorModel MONOCHROME = new IndexColorModel(1, 2, new int[] {0, -1}, 0, false, -1, DataBuffer.TYPE_BYTE);
private int version; private int version;
private int compression; private int compression;
private int bitsPerPixel; private int bitsPerPixel;
@ -69,14 +72,6 @@ final class PCXHeader {
return height; return height;
} }
public int getHdpi() {
return hdpi;
}
public int getVdpi() {
return vdpi;
}
public int getChannels() { public int getChannels() {
return channels; return channels;
} }
@ -85,22 +80,63 @@ final class PCXHeader {
return bytesPerLine; return bytesPerLine;
} }
public int getPaletteInfo() { public IndexColorModel getEGAPalette() {
return paletteInfo; // Test for CGA modes
if (isCGAVideoMode4() || isCGAVideoMode5() || isCGAVideoMode6()) {
return CGAColorModel.create(palette, bitsPerPixel);
} }
public IndexColorModel getEGAPalette() { // Test if we should use a default B/W palette
// TODO: Figure out when/how to enable CGA palette... The test below isn't good enough. if (bitsPerPixel == 1 && channels == 1 && (version < PCX.VERSION_2_X_WINDOWS || isDummyPalette())) {
// if (channels == 1 && (bitsPerPixel == 1 || bitsPerPixel == 2)) { return MONOCHROME;
// return CGAColorModel.create(palette, bitsPerPixel); }
// }
int bits = channels * bitsPerPixel; int bits = channels * bitsPerPixel;
return new IndexColorModel(bits, Math.min(16, 1 << bits), palette, 0, false); return new IndexColorModel(bits, Math.min(16, 1 << bits), palette, 0, false);
} }
private boolean isCGAVideoMode4() {
return bitsPerPixel * channels == 2 && width == 320 && hdpi == 320 && height == 200 && vdpi == 200;
}
private boolean isCGAVideoMode5() {
return bitsPerPixel == 1 && channels == 1 && width == 320 && hdpi == 320 && height == 200 && vdpi == 200;
}
private boolean isCGAVideoMode6() {
return bitsPerPixel == 1 && channels == 1 && width == 640 && hdpi == 640 && height == 200 && vdpi == 200;
}
private boolean isDummyPalette() {
return isEmptyPalette() || isPhotoshopPalette();
}
private boolean isEmptyPalette() {
// All black
for (int i = 0; i < 48; i++) {
if (palette[i] != 0) {
return false;
}
}
return true;
}
private boolean isPhotoshopPalette() {
// Written by Photoshop: 15,15,15, 14,14,14, ... 0,0,0
for (int i = 0; i < 16; i++) {
int off = i * 3;
if (palette[off] != 15 - i || palette[off + 1] != 15 - i || palette[off + 2] != 15 - i) {
return false;
}
}
return true;
}
@Override public String toString() { @Override public String toString() {
return "PCXHeader{" + return "PCXHeader[" +
"version=" + version + "version=" + version +
", compression=" + compression + ", compression=" + compression +
", bitsPerPixel=" + bitsPerPixel + ", bitsPerPixel=" + bitsPerPixel +
@ -114,7 +150,7 @@ final class PCXHeader {
", hScreenSize=" + hScreenSize + ", hScreenSize=" + hScreenSize +
", vScreenSize=" + vScreenSize + ", vScreenSize=" + vScreenSize +
", palette=" + Arrays.toString(palette) + ", palette=" + Arrays.toString(palette) +
'}'; ']';
} }
public static PCXHeader read(final ImageInputStream imageInput) throws IOException { public static PCXHeader read(final ImageInputStream imageInput) throws IOException {
@ -142,7 +178,7 @@ final class PCXHeader {
byte magic = imageInput.readByte(); byte magic = imageInput.readByte();
if (magic != PCX.MAGIC) { if (magic != PCX.MAGIC) {
throw new IIOException(String.format("Not a PCX image. Expected PCX magic %02x, read %02x", PCX.MAGIC, magic)); throw new IIOException(String.format("Not a PCX image. Expected PCX magic 0x%02x: 0x%02x", PCX.MAGIC, magic));
} }
PCXHeader header = new PCXHeader(); PCXHeader header = new PCXHeader();
@ -165,10 +201,10 @@ final class PCXHeader {
imageInput.readUnsignedByte(); // Reserved, should be 0 imageInput.readUnsignedByte(); // Reserved, should be 0
header.channels = imageInput.readUnsignedByte(); header.channels = imageInput.readUnsignedByte(); // Channels or Bit planes
header.bytesPerLine = imageInput.readUnsignedShort(); // Must be even! header.bytesPerLine = imageInput.readUnsignedShort(); // Must be even!
header.paletteInfo = imageInput.readUnsignedShort(); // 1 == Color/BW, 2 == Gray header.paletteInfo = imageInput.readUnsignedShort() & 0x2; // 1 == Color/BW, 2 == Gray. Ignored
header.hScreenSize = imageInput.readUnsignedShort(); header.hScreenSize = imageInput.readUnsignedShort();
header.vScreenSize = imageInput.readUnsignedShort(); header.vScreenSize = imageInput.readUnsignedShort();

View File

@ -54,7 +54,15 @@ import java.util.ArrayList;
import java.util.Iterator; import java.util.Iterator;
import java.util.List; import java.util.List;
/**
* ImageReader for ZSoft PC Paintbrush (PCX) format.
*
* @see <a href="http://www.drdobbs.com/pcx-graphics/184402396">PCX Graphics</a>
*/
public final class PCXImageReader extends ImageReaderBase { public final class PCXImageReader extends ImageReaderBase {
final static boolean DEBUG = "true".equalsIgnoreCase(System.getProperty("com.twelvemonkeys.imageio.plugins.pcx.debug"));
/** 8 bit ImageTypeSpecifer used for reading bitplane images. */ /** 8 bit ImageTypeSpecifer used for reading bitplane images. */
private static final ImageTypeSpecifier GRAYSCALE = ImageTypeSpecifiers.createGrayscale(8, DataBuffer.TYPE_BYTE); private static final ImageTypeSpecifier GRAYSCALE = ImageTypeSpecifiers.createGrayscale(8, DataBuffer.TYPE_BYTE);
@ -93,7 +101,7 @@ public final class PCXImageReader extends ImageReaderBase {
public Iterator<ImageTypeSpecifier> getImageTypes(final int imageIndex) throws IOException { public Iterator<ImageTypeSpecifier> getImageTypes(final int imageIndex) throws IOException {
ImageTypeSpecifier rawType = getRawImageType(imageIndex); ImageTypeSpecifier rawType = getRawImageType(imageIndex);
List<ImageTypeSpecifier> specifiers = new ArrayList<ImageTypeSpecifier>(); List<ImageTypeSpecifier> specifiers = new ArrayList<>();
// TODO: Implement // TODO: Implement
specifiers.add(rawType); specifiers.add(rawType);
@ -107,27 +115,30 @@ public final class PCXImageReader extends ImageReaderBase {
readHeader(); readHeader();
int channels = header.getChannels(); 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()) { switch (header.getBitsPerPixel()) {
case 1: case 1:
case 2: case 2:
case 4: case 4:
// TODO: If there's a VGA palette here, use it?
return ImageTypeSpecifiers.createFromIndexColorModel(header.getEGAPalette()); return ImageTypeSpecifiers.createFromIndexColorModel(header.getEGAPalette());
case 8: case 8:
if (channels == 1) {
// We may have IndexColorModel here for 1 channel images // We may have IndexColorModel here for 1 channel images
if (channels == 1 && paletteInfo != PCX.PALETTEINFO_GRAY) {
IndexColorModel palette = getVGAPalette(); IndexColorModel palette = getVGAPalette();
if (palette == null) {
throw new IIOException("Expected VGA palette not found");
}
if (palette != null) {
return ImageTypeSpecifiers.createFromIndexColorModel(palette); return ImageTypeSpecifiers.createFromIndexColorModel(palette);
} }
else {
// PCX Gray has 1 channel and no palette
return ImageTypeSpecifiers.createGrayscale(8, DataBuffer.TYPE_BYTE);
}
}
// PCX has 1 or 3 channels for 8 bit gray or 24 bit RGB, will be validated by ImageTypeSpecifier // PCX RGB has channels for 24 bit RGB, will be validated by ImageTypeSpecifier
return ImageTypeSpecifiers.createBanded(cs, createIndices(channels, 1), createIndices(channels, 0), DataBuffer.TYPE_BYTE, false, false); return ImageTypeSpecifiers.createBanded(ColorSpace.getInstance(ColorSpace.CS_sRGB), createIndices(channels, 1), createIndices(channels, 0), DataBuffer.TYPE_BYTE, channels == 4, false);
case 24: case 24:
// Some sources says this is possible... // Some sources says this is possible...
return ImageTypeSpecifiers.createFromBufferedImageType(BufferedImage.TYPE_3BYTE_BGR); return ImageTypeSpecifiers.createFromBufferedImageType(BufferedImage.TYPE_3BYTE_BGR);
@ -154,10 +165,6 @@ public final class PCXImageReader extends ImageReaderBase {
Iterator<ImageTypeSpecifier> imageTypes = getImageTypes(imageIndex); Iterator<ImageTypeSpecifier> imageTypes = getImageTypes(imageIndex);
ImageTypeSpecifier rawType = getRawImageType(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 width = getWidth(imageIndex);
int height = getHeight(imageIndex); int height = getHeight(imageIndex);
@ -351,7 +358,11 @@ public final class PCXImageReader extends ImageReaderBase {
if (header == null) { if (header == null) {
imageInput.setByteOrder(ByteOrder.LITTLE_ENDIAN); imageInput.setByteOrder(ByteOrder.LITTLE_ENDIAN);
header = PCXHeader.read(imageInput); header = PCXHeader.read(imageInput);
// System.err.println("header: " + header);
if (DEBUG) {
System.err.println("header: " + header);
}
imageInput.flushBefore(imageInput.getStreamPosition()); imageInput.flushBefore(imageInput.getStreamPosition());
} }
@ -372,29 +383,27 @@ public final class PCXImageReader extends ImageReaderBase {
// Mark palette as read, to avoid further attempts // Mark palette as read, to avoid further attempts
readPalette = true; readPalette = true;
// Wee can't simply skip to an offset, as the RLE compression makes the file size unpredictable if (header.getVersion() >= PCX.VERSION_3 || header.getVersion() == PCX.VERSION_2_8_PALETTE) {
// We can't simply skip to an offset, as the RLE compression makes the file size unpredictable
skipToEOF(imageInput); skipToEOF(imageInput);
// Seek backwards from EOF int paletteSize = 256 * 3; // 256 * 3 for RGB
long paletteStart = imageInput.getStreamPosition() - 769;
if (paletteStart <= imageInput.getFlushedPosition()) {
return null;
}
// Seek backwards from EOF
long paletteStart = imageInput.getStreamPosition() - paletteSize - 1;
if (paletteStart > imageInput.getFlushedPosition()) {
imageInput.seek(paletteStart); imageInput.seek(paletteStart);
byte val = imageInput.readByte(); byte val = imageInput.readByte();
if (val == PCX.VGA_PALETTE_MAGIC) { if (val == PCX.VGA_PALETTE_MAGIC) {
byte[] palette = new byte[768]; // 256 * 3 for RGB byte[] palette = new byte[paletteSize];
imageInput.readFully(palette); imageInput.readFully(palette);
vgaPalette = new IndexColorModel(8, 256, palette, 0, false); vgaPalette = new IndexColorModel(8, 256, palette, 0, false);
return vgaPalette;
} }
}
return null; }
} }
return vgaPalette; return vgaPalette;
@ -414,7 +423,7 @@ public final class PCXImageReader extends ImageReaderBase {
long pos = stream.getStreamPosition(); long pos = stream.getStreamPosition();
// ...skip 1k blocks until we're passed EOF... // ...skip 1k blocks until we're passed EOF...
while (stream.skipBytes(1024l) > 0) { while (stream.skipBytes(1024L) > 0) {
if (stream.read() == -1) { if (stream.read() == -1) {
break; break;
} }

View File

@ -87,7 +87,7 @@ public final class PCXImageReaderSpi extends ImageReaderSpiBase {
} }
@Override public String getDescription(final Locale locale) { @Override public String getDescription(final Locale locale) {
return "PC Paintbrush (PCX) image reader"; return "ZSoft PC Paintbrush (PCX) image reader";
} }
} }

View File

@ -31,13 +31,20 @@ package com.twelvemonkeys.imageio.plugins.pcx;
import com.twelvemonkeys.imageio.util.ImageReaderAbstractTest; import com.twelvemonkeys.imageio.util.ImageReaderAbstractTest;
import org.junit.Test; import org.junit.Test;
import javax.imageio.ImageIO;
import javax.imageio.ImageReadParam;
import javax.imageio.spi.ImageReaderSpi; import javax.imageio.spi.ImageReaderSpi;
import javax.imageio.stream.ImageInputStream;
import java.awt.*; import java.awt.*;
import java.awt.image.BufferedImage;
import java.io.IOException; import java.io.IOException;
import java.util.Arrays; import java.util.Arrays;
import java.util.Collections; import java.util.Collections;
import java.util.List; import java.util.List;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertNotNull;
/** /**
* PCXImageReaderTest * PCXImageReaderTest
* *
@ -50,7 +57,6 @@ public class PCXImageReaderTest extends ImageReaderAbstractTest<PCXImageReader>
protected List<TestData> getTestData() { protected List<TestData> getTestData() {
return Arrays.asList( return Arrays.asList(
new TestData(getClassLoaderResource("/pcx/MARBLES.PCX"), new Dimension(1419, 1001)), // RLE encoded RGB 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 damaged, missing the last few scan lines)
new TestData(getClassLoaderResource("/pcx/lena.pcx"), new Dimension(512, 512)), // RLE encoded RGB 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/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/lena3.pcx"), new Dimension(512, 512)), // RLE encoded, 16 color indexed (4 bps/1 channel)
@ -62,7 +68,7 @@ public class PCXImageReaderTest extends ImageReaderAbstractTest<PCXImageReader>
new TestData(getClassLoaderResource("/pcx/lena9.pcx"), new Dimension(512, 512)), // RLE encoded, 2 color indexed (1 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/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) 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) // See cga-pcx.txt, however, the text seems to be in error, the bits can not not as described
new TestData(getClassLoaderResource("/pcx/CGA_BW.PCX"), new Dimension(640, 200)), // RLE encoded indexed (CGA mode) 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_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_RGBI.PCX"), new Dimension(320, 200)), // RLE encoded indexed (CGA mode)
@ -102,6 +108,29 @@ public class PCXImageReaderTest extends ImageReaderAbstractTest<PCXImageReader>
); );
} }
@Test
public void testReadGray() throws IOException {
// Seems like the last scan lines have been overwritten by an unnecessary 768 byte palette + 1 byte magic...
try (ImageInputStream input = ImageIO.createImageInputStream(getClassLoaderResource("/pcx/GMARBLES.PCX"))) {
PCXImageReader reader = createReader();
reader.setInput(input);
assertEquals(1, reader.getNumImages(true));
assertEquals(1419, reader.getWidth(0));
assertEquals(1001, reader.getHeight(0));
ImageReadParam param = reader.getDefaultReadParam();
param.setSourceRegion(new Rectangle(1419, 1000)); // Ignore the last garbled line
BufferedImage image = reader.read(0, param);
assertNotNull(image);
assertEquals(BufferedImage.TYPE_BYTE_INDEXED, image.getType());
assertEquals(1419, image.getWidth());
assertEquals(1000, image.getHeight());
}
}
@Test @Test
public void testReadWithSourceRegionParamEqualImage() throws IOException { public void testReadWithSourceRegionParamEqualImage() throws IOException {
TestData data = getTestData().get(1); TestData data = getTestData().get(1);