blob: c75ac17ecd0e561b1e73bee3bcc34a7e250a1399 [file] [log] [blame]
/*
* Licensed to the Apache Software Foundation (ASF) under one or more
* contributor license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright ownership.
* The ASF licenses this file to You under the Apache License, Version 2.0
* (the "License"); you may not use this file except in compliance with
* the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.apache.commons.imaging.formats.pcx;
import java.awt.Dimension;
import java.awt.Transparency;
import java.awt.color.ColorSpace;
import java.awt.image.BufferedImage;
import java.awt.image.ColorModel;
import java.awt.image.ComponentColorModel;
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.File;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.io.PrintWriter;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.HashMap;
import java.util.Map;
import java.util.Properties;
import org.apache.commons.imaging.ImageFormat;
import org.apache.commons.imaging.ImageFormats;
import org.apache.commons.imaging.ImageInfo;
import org.apache.commons.imaging.ImageParser;
import org.apache.commons.imaging.ImageReadException;
import org.apache.commons.imaging.ImageWriteException;
import org.apache.commons.imaging.common.ByteOrder;
import org.apache.commons.imaging.common.IImageMetadata;
import org.apache.commons.imaging.common.bytesource.ByteSource;
import org.apache.commons.imaging.util.IoUtils;
public class PcxImageParser extends ImageParser implements PcxConstants {
// ZSoft's official spec is at http://www.qzx.com/pc-gpe/pcx.txt
// (among other places) but it's pretty thin. The fileformat.info document
// at http://www.fileformat.info/format/pcx/egff.htm is a little better
// but their gray sample image seems corrupt. PCX files themselves are
// the ultimate test but pretty hard to find nowdays, so the best
// test is against other image viewers (Irfanview is pretty good).
//
// Open source projects are generally poor at parsing PCX,
// SDL_Image/gdk-pixbuf/Eye of Gnome/GIMP/F-Spot all only do some formats,
// don't support uncompressed PCX, and/or don't handle black and white
// images properly.
public PcxImageParser() {
super.setByteOrder(ByteOrder.LITTLE_ENDIAN);
}
@Override
public String getName() {
return "Pcx-Custom";
}
@Override
public String getDefaultExtension() {
return DEFAULT_EXTENSION;
}
private static final String DEFAULT_EXTENSION = ".pcx";
private static final String ACCEPTED_EXTENSIONS[] = { ".pcx", ".pcc", };
@Override
protected String[] getAcceptedExtensions() {
return ACCEPTED_EXTENSIONS;
}
@Override
protected ImageFormat[] getAcceptedTypes() {
return new ImageFormat[] { ImageFormats.PCX, //
};
}
@Override
public boolean embedICCProfile(final File src, final File dst, final byte profile[]) {
return false;
}
@Override
public IImageMetadata getMetadata(final ByteSource byteSource, final Map<String,Object> params)
throws ImageReadException, IOException {
return null;
}
@Override
public ImageInfo getImageInfo(final ByteSource byteSource, final Map<String,Object> params)
throws ImageReadException, IOException {
final PcxHeader pcxHeader = readPcxHeader(byteSource);
final Dimension size = getImageSize(byteSource, params);
return new ImageInfo(
"PCX",
pcxHeader.nPlanes * pcxHeader.bitsPerPixel,
new ArrayList<String>(),
ImageFormats.PCX,
"ZSoft PCX Image",
size.height,
"image/x-pcx",
1,
pcxHeader.vDpi,
Math.round(size.getHeight() / pcxHeader.vDpi),
pcxHeader.hDpi,
Math.round(size.getWidth() / pcxHeader.hDpi),
size.width,
false,
false,
!(pcxHeader.nPlanes == 3 && pcxHeader.bitsPerPixel == 8),
ImageInfo.COLOR_TYPE_RGB,
pcxHeader.encoding == PcxHeader.ENCODING_RLE ? ImageInfo.COMPRESSION_ALGORITHM_RLE
: ImageInfo.COMPRESSION_ALGORITHM_NONE);
}
@Override
public Dimension getImageSize(final ByteSource byteSource, final Map<String,Object> params)
throws ImageReadException, IOException {
final PcxHeader pcxHeader = readPcxHeader(byteSource);
final int xSize = pcxHeader.xMax - pcxHeader.xMin + 1;
if (xSize < 0) {
throw new ImageReadException("Image width is negative");
}
final int ySize = pcxHeader.yMax - pcxHeader.yMin + 1;
if (ySize < 0) {
throw new ImageReadException("Image height is negative");
}
return new Dimension(xSize, ySize);
}
@Override
public byte[] getICCProfileBytes(final ByteSource byteSource, final Map<String,Object> params)
throws ImageReadException, IOException {
return null;
}
static class PcxHeader {
public static final int ENCODING_UNCOMPRESSED = 0;
public static final int ENCODING_RLE = 1;
public static final int PALETTE_INFO_COLOR = 1;
public static final int PALETTE_INFO_GRAYSCALE = 2;
public final int manufacturer; // Always 10 = ZSoft .pcx
public final int version; // 0 = PC Paintbrush 2.5
// 2 = PC Paintbrush 2.8 with palette
// 3 = PC Paintbrush 2.8 w/o palette
// 4 = PC Paintbrush for Windows
// 5 = PC Paintbrush >= 3.0
public final int encoding; // 0 = very old uncompressed format, 1 = .pcx
// run length encoding
public final int bitsPerPixel; // Bits ***PER PLANE*** for each pixel
public final int xMin; // window
public final int yMin;
public final int xMax;
public final int yMax;
public final int hDpi; // horizontal dpi
public final int vDpi; // vertical dpi
public final int[] colormap; // palette for <= 16 colors
public final int reserved; // Always 0
public final int nPlanes; // Number of color planes
public final int bytesPerLine; // Number of bytes per scanline plane,
// must be an even number.
public final int paletteInfo; // 1 = Color/BW, 2 = Grayscale, ignored in
// Paintbrush IV/IV+
public final int hScreenSize; // horizontal screen size, in pixels.
// PaintBrush >= IV only.
public final int vScreenSize; // vertical screen size, in pixels.
// PaintBrush >= IV only.
public PcxHeader(final int manufacturer, final int version,
final int encoding, final int bitsPerPixel, final int xMin,
final int yMin, final int xMax, final int yMax, final int hDpi,
final int vDpi, final int[] colormap, final int reserved,
final int nPlanes, final int bytesPerLine,
final int paletteInfo, final int hScreenSize,
final int vScreenSize) {
this.manufacturer = manufacturer;
this.version = version;
this.encoding = encoding;
this.bitsPerPixel = bitsPerPixel;
this.xMin = xMin;
this.yMin = yMin;
this.xMax = xMax;
this.yMax = yMax;
this.hDpi = hDpi;
this.vDpi = vDpi;
this.colormap = colormap;
this.reserved = reserved;
this.nPlanes = nPlanes;
this.bytesPerLine = bytesPerLine;
this.paletteInfo = paletteInfo;
this.hScreenSize = hScreenSize;
this.vScreenSize = vScreenSize;
}
public void dump(final PrintWriter pw) {
pw.println("PcxHeader");
pw.println("Manufacturer: " + manufacturer);
pw.println("Version: " + version);
pw.println("Encoding: " + encoding);
pw.println("BitsPerPixel: " + bitsPerPixel);
pw.println("xMin: " + xMin);
pw.println("yMin: " + yMin);
pw.println("xMax: " + xMax);
pw.println("yMax: " + yMax);
pw.println("hDpi: " + hDpi);
pw.println("vDpi: " + vDpi);
pw.print("ColorMap: ");
for (int i = 0; i < colormap.length; i++) {
if (i > 0) {
pw.print(",");
}
pw.print("(" + (0xff & (colormap[i] >> 16)) + ","
+ (0xff & (colormap[i] >> 8)) + ","
+ (0xff & colormap[i]) + ")");
}
pw.println();
pw.println("Reserved: " + reserved);
pw.println("nPlanes: " + nPlanes);
pw.println("BytesPerLine: " + bytesPerLine);
pw.println("PaletteInfo: " + paletteInfo);
pw.println("hScreenSize: " + hScreenSize);
pw.println("vScreenSize: " + vScreenSize);
pw.println();
}
}
private PcxHeader readPcxHeader(final ByteSource byteSource)
throws ImageReadException, IOException {
InputStream is = null;
boolean canThrow = false;
try {
is = byteSource.getInputStream();
final PcxHeader ret = readPcxHeader(is, false);
canThrow = true;
return ret;
} finally {
IoUtils.closeQuietly(canThrow, is);
}
}
private PcxHeader readPcxHeader(final InputStream is, final boolean isStrict)
throws ImageReadException, IOException {
final byte[] pcxHeaderBytes = readBytes("PcxHeader", is, 128,
"Not a Valid PCX File");
final int manufacturer = 0xff & pcxHeaderBytes[0];
final int version = 0xff & pcxHeaderBytes[1];
final int encoding = 0xff & pcxHeaderBytes[2];
final int bitsPerPixel = 0xff & pcxHeaderBytes[3];
final int xMin = toUInt16(pcxHeaderBytes, 4);
final int yMin = toUInt16(pcxHeaderBytes, 6);
final int xMax = toUInt16(pcxHeaderBytes, 8);
final int yMax = toUInt16(pcxHeaderBytes, 10);
final int hDpi = toUInt16(pcxHeaderBytes, 12);
final int vDpi = toUInt16(pcxHeaderBytes, 14);
final int[] colormap = new int[16];
for (int i = 0; i < 16; i++) {
colormap[i] = 0xff000000
| ((0xff & pcxHeaderBytes[16 + 3 * i]) << 16)
| ((0xff & pcxHeaderBytes[16 + 3 * i + 1]) << 8)
| (0xff & pcxHeaderBytes[16 + 3 * i + 2]);
}
final int reserved = 0xff & pcxHeaderBytes[64];
final int nPlanes = 0xff & pcxHeaderBytes[65];
final int bytesPerLine = toUInt16(pcxHeaderBytes, 66);
final int paletteInfo = toUInt16(pcxHeaderBytes, 68);
final int hScreenSize = toUInt16(pcxHeaderBytes, 70);
final int vScreenSize = toUInt16(pcxHeaderBytes, 72);
if (manufacturer != 10) {
throw new ImageReadException(
"Not a Valid PCX File: manufacturer is " + manufacturer);
}
if (isStrict) {
// Note that reserved is sometimes set to a non-zero value
// by Paintbrush itself, so it shouldn't be enforced.
if (bytesPerLine % 2 != 0) {
throw new ImageReadException(
"Not a Valid PCX File: bytesPerLine is odd");
}
}
return new PcxHeader(manufacturer, version, encoding, bitsPerPixel,
xMin, yMin, xMax, yMax, hDpi, vDpi, colormap, reserved,
nPlanes, bytesPerLine, paletteInfo, hScreenSize, vScreenSize);
}
@Override
public boolean dumpImageFile(final PrintWriter pw, final ByteSource byteSource)
throws ImageReadException, IOException {
readPcxHeader(byteSource).dump(pw);
return true;
}
private void readScanLine(final PcxHeader pcxHeader, final InputStream is,
final byte[] samples) throws IOException, ImageReadException {
if (pcxHeader.encoding == PcxHeader.ENCODING_UNCOMPRESSED) {
int r;
for (int bytesRead = 0; bytesRead < samples.length; bytesRead += r) {
r = is.read(samples, bytesRead, samples.length - bytesRead);
if (r < 0) {
throw new ImageReadException(
"Premature end of file reading image data");
}
}
} else {
if (pcxHeader.encoding == PcxHeader.ENCODING_RLE) {
for (int bytesRead = 0; bytesRead < samples.length;) {
final byte b = readByte("Pixel", is, "Error reading image data");
int count;
byte sample;
if ((b & 0xc0) == 0xc0) {
count = b & 0x3f;
sample = readByte("Pixel", is,
"Error reading image data");
} else {
count = 1;
sample = b;
}
for (int i = 0; i < count && bytesRead + i < samples.length; i++) {
samples[bytesRead + i] = sample;
}
bytesRead += count;
}
} else {
throw new ImageReadException("Invalid PCX encoding "
+ pcxHeader.encoding);
}
}
}
private int[] read256ColorPalette(final InputStream stream) throws IOException {
final byte[] paletteBytes = readBytes("Palette", stream, 769,
"Error reading palette");
if (paletteBytes[0] != 12) {
return null;
}
final int[] palette = new int[256];
for (int i = 0; i < palette.length; i++) {
palette[i] = ((0xff & paletteBytes[1 + 3 * i]) << 16)
| ((0xff & paletteBytes[1 + 3 * i + 1]) << 8)
| (0xff & paletteBytes[1 + 3 * i + 2]);
}
return palette;
}
private int[] read256ColorPaletteFromEndOfFile(final ByteSource byteSource)
throws IOException {
InputStream stream = null;
boolean canThrow = false;
try {
stream = byteSource.getInputStream();
final long toSkip = byteSource.getLength() - 769;
skipBytes(stream, (int) toSkip);
final int[] ret = read256ColorPalette(stream);
canThrow = true;
return ret;
} finally {
IoUtils.closeQuietly(canThrow, stream);
}
}
private BufferedImage readImage(final PcxHeader pcxHeader, final InputStream is,
final ByteSource byteSource) throws ImageReadException, IOException {
final int xSize = pcxHeader.xMax - pcxHeader.xMin + 1;
if (xSize < 0) {
throw new ImageReadException("Image width is negative");
}
final int ySize = pcxHeader.yMax - pcxHeader.yMin + 1;
if (ySize < 0) {
throw new ImageReadException("Image height is negative");
}
final int scanlineLength = pcxHeader.bytesPerLine * pcxHeader.nPlanes;
final byte[] scanline = new byte[scanlineLength];
if ((pcxHeader.bitsPerPixel == 1 || pcxHeader.bitsPerPixel == 2
|| pcxHeader.bitsPerPixel == 4 || pcxHeader.bitsPerPixel == 8)
&& pcxHeader.nPlanes == 1) {
final int bytesPerImageRow = (xSize * pcxHeader.bitsPerPixel + 7) / 8;
final byte[] image = new byte[ySize * bytesPerImageRow];
for (int y = 0; y < ySize; y++) {
readScanLine(pcxHeader, is, scanline);
System.arraycopy(scanline, 0, image, y * bytesPerImageRow,
bytesPerImageRow);
}
final DataBufferByte dataBuffer = new DataBufferByte(image, image.length);
int[] palette;
if (pcxHeader.bitsPerPixel == 1) {
palette = new int[] { 0x000000, 0xffffff };
} else if (pcxHeader.bitsPerPixel == 8) {
// Normally the palette is read 769 bytes from the end of the
// file.
// However DCX files have multiple PCX images in one file, so
// there could be extra data before the end! So try look for the
// palette
// immediately after the image data first.
palette = read256ColorPalette(is);
if (palette == null) {
palette = read256ColorPaletteFromEndOfFile(byteSource);
}
if (palette == null) {
throw new ImageReadException(
"No 256 color palette found in image that needs it");
}
} else {
palette = pcxHeader.colormap;
}
WritableRaster raster;
if (pcxHeader.bitsPerPixel == 8) {
raster = Raster.createInterleavedRaster(dataBuffer,
xSize, ySize, bytesPerImageRow, 1, new int[] { 0 },
null);
} else {
raster = Raster.createPackedRaster(dataBuffer, xSize,
ySize, pcxHeader.bitsPerPixel, null);
}
final IndexColorModel colorModel = new IndexColorModel(
pcxHeader.bitsPerPixel, 1 << pcxHeader.bitsPerPixel,
palette, 0, false, -1, DataBuffer.TYPE_BYTE);
return new BufferedImage(colorModel, raster,
colorModel.isAlphaPremultiplied(), new Properties());
} else if (pcxHeader.bitsPerPixel == 1 && 2 <= pcxHeader.nPlanes
&& pcxHeader.nPlanes <= 4) {
final IndexColorModel colorModel = new IndexColorModel(pcxHeader.nPlanes,
1 << pcxHeader.nPlanes, pcxHeader.colormap, 0, false, -1,
DataBuffer.TYPE_BYTE);
final BufferedImage image = new BufferedImage(xSize, ySize,
BufferedImage.TYPE_BYTE_BINARY, colorModel);
final byte[] unpacked = new byte[xSize];
for (int y = 0; y < ySize; y++) {
readScanLine(pcxHeader, is, scanline);
int nextByte = 0;
Arrays.fill(unpacked, (byte) 0);
for (int plane = 0; plane < pcxHeader.nPlanes; plane++) {
for (int i = 0; i < pcxHeader.bytesPerLine; i++) {
final int b = 0xff & scanline[nextByte++];
for (int j = 0; j < 8 && 8 * i + j < unpacked.length; j++) {
unpacked[8 * i + j] |= (byte) (((b >> (7 - j)) & 0x1) << plane);
}
}
}
image.getRaster().setDataElements(0, y, xSize, 1, unpacked);
}
return image;
} else if (pcxHeader.bitsPerPixel == 8 && pcxHeader.nPlanes == 3) {
final byte[][] image = new byte[3][];
image[0] = new byte[xSize * ySize];
image[1] = new byte[xSize * ySize];
image[2] = new byte[xSize * ySize];
for (int y = 0; y < ySize; y++) {
readScanLine(pcxHeader, is, scanline);
System.arraycopy(scanline, 0, image[0], y * xSize, xSize);
System.arraycopy(scanline, pcxHeader.bytesPerLine, image[1], y
* xSize, xSize);
System.arraycopy(scanline, 2 * pcxHeader.bytesPerLine,
image[2], y * xSize, xSize);
}
final DataBufferByte dataBuffer = new DataBufferByte(image,
image[0].length);
final WritableRaster raster = Raster.createBandedRaster(
dataBuffer, xSize, ySize, xSize, new int[] { 0, 1, 2 },
new int[] { 0, 0, 0 }, null);
final ColorModel colorModel = new ComponentColorModel(
ColorSpace.getInstance(ColorSpace.CS_sRGB), false, false,
Transparency.OPAQUE, DataBuffer.TYPE_BYTE);
return new BufferedImage(colorModel, raster,
colorModel.isAlphaPremultiplied(), new Properties());
} else if ((pcxHeader.bitsPerPixel == 24 && pcxHeader.nPlanes == 1)
|| (pcxHeader.bitsPerPixel == 32 && pcxHeader.nPlanes == 1)) {
final int rowLength = 3 * xSize;
final byte[] image = new byte[rowLength * ySize];
for (int y = 0; y < ySize; y++) {
readScanLine(pcxHeader, is, scanline);
if (pcxHeader.bitsPerPixel == 24) {
System.arraycopy(scanline, 0, image, y * rowLength,
rowLength);
} else {
for (int x = 0; x < xSize; x++) {
image[y * rowLength + 3 * x] = scanline[4 * x];
image[y * rowLength + 3 * x + 1] = scanline[4 * x + 1];
image[y * rowLength + 3 * x + 2] = scanline[4 * x + 2];
}
}
}
final DataBufferByte dataBuffer = new DataBufferByte(image, image.length);
final WritableRaster raster = Raster.createInterleavedRaster(
dataBuffer, xSize, ySize, rowLength, 3,
new int[] { 2, 1, 0 }, null);
final ColorModel colorModel = new ComponentColorModel(
ColorSpace.getInstance(ColorSpace.CS_sRGB), false, false,
Transparency.OPAQUE, DataBuffer.TYPE_BYTE);
return new BufferedImage(colorModel, raster,
colorModel.isAlphaPremultiplied(), new Properties());
} else {
throw new ImageReadException(
"Invalid/unsupported image with bitsPerPixel "
+ pcxHeader.bitsPerPixel + " and planes "
+ pcxHeader.nPlanes);
}
}
@Override
public final BufferedImage getBufferedImage(final ByteSource byteSource,
Map<String,Object> params) throws ImageReadException, IOException {
params = (params == null) ? new HashMap<String,Object>() : new HashMap<String,Object>(params);
boolean isStrict = false;
final Object strictness = params.get(PARAM_KEY_STRICT);
if (strictness != null) {
isStrict = ((Boolean) strictness).booleanValue();
}
InputStream is = null;
boolean canThrow = false;
try {
is = byteSource.getInputStream();
final PcxHeader pcxHeader = readPcxHeader(is, isStrict);
final BufferedImage ret = readImage(pcxHeader, is, byteSource);
canThrow = true;
return ret;
} finally {
IoUtils.closeQuietly(canThrow, is);
}
}
@Override
public void writeImage(final BufferedImage src, final OutputStream os, final Map<String,Object> params)
throws ImageWriteException, IOException {
new PcxWriter(params).writeImage(src, os);
}
/**
* Extracts embedded XML metadata as XML string.
* <p>
*
* @param byteSource
* File containing image data.
* @param params
* Map of optional parameters, defined in ImagingConstants.
* @return Xmp Xml as String, if present. Otherwise, returns null.
*/
@Override
public String getXmpXml(final ByteSource byteSource, final Map<String,Object> params)
throws ImageReadException, IOException {
return null;
}
}