001/* 002 * Licensed to the Apache Software Foundation (ASF) under one or more 003 * contributor license agreements. See the NOTICE file distributed with 004 * this work for additional information regarding copyright ownership. 005 * The ASF licenses this file to You under the Apache License, Version 2.0 006 * (the "License"); you may not use this file except in compliance with 007 * the License. You may obtain a copy of the License at 008 * 009 * http://www.apache.org/licenses/LICENSE-2.0 010 * 011 * Unless required by applicable law or agreed to in writing, software 012 * distributed under the License is distributed on an "AS IS" BASIS, 013 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 014 * See the License for the specific language governing permissions and 015 * limitations under the License. 016 */ 017package org.apache.commons.imaging.formats.pcx; 018 019import static org.apache.commons.imaging.common.BinaryFunctions.readBytes; 020import static org.apache.commons.imaging.common.BinaryFunctions.skipBytes; 021import static org.apache.commons.imaging.common.ByteConversions.toUInt16; 022 023import java.awt.Dimension; 024import java.awt.Transparency; 025import java.awt.color.ColorSpace; 026import java.awt.image.BufferedImage; 027import java.awt.image.ColorModel; 028import java.awt.image.ComponentColorModel; 029import java.awt.image.DataBuffer; 030import java.awt.image.DataBufferByte; 031import java.awt.image.IndexColorModel; 032import java.awt.image.Raster; 033import java.awt.image.WritableRaster; 034import java.io.IOException; 035import java.io.InputStream; 036import java.io.OutputStream; 037import java.io.PrintWriter; 038import java.nio.ByteOrder; 039import java.util.ArrayList; 040import java.util.Arrays; 041import java.util.Properties; 042 043import org.apache.commons.imaging.AbstractImageParser; 044import org.apache.commons.imaging.ImageFormat; 045import org.apache.commons.imaging.ImageFormats; 046import org.apache.commons.imaging.ImageInfo; 047import org.apache.commons.imaging.ImagingException; 048import org.apache.commons.imaging.bytesource.ByteSource; 049import org.apache.commons.imaging.common.Allocator; 050import org.apache.commons.imaging.common.ImageMetadata; 051 052public class PcxImageParser extends AbstractImageParser<PcxImagingParameters> { 053 // ZSoft's official spec is at [BROKEN URL] http://www.qzx.com/pc-gpe/pcx.txt 054 // (among other places) but it's pretty thin. The fileformat.fine document 055 // at [BROEKN URL] http://www.fileformat.fine/format/pcx/egff.htm is a little better 056 // but their gray sample image seems corrupt. PCX files themselves are 057 // the ultimate test but pretty hard to find nowadays, so the best 058 // test is against other image viewers (Irfanview is pretty good). 059 // 060 // Open source projects are generally poor at parsing PCX, 061 // SDL_Image/gdk-pixbuf/Eye of Gnome/GIMP/F-Spot all only do some formats, 062 // don't support uncompressed PCX, and/or don't handle black and white 063 // images properly. 064 065 static class PcxHeader { 066 067 public static final int ENCODING_UNCOMPRESSED = 0; 068 public static final int ENCODING_RLE = 1; 069 public static final int PALETTE_INFO_COLOR = 1; 070 public static final int PALETTE_INFO_GRAYSCALE = 2; 071 public final int manufacturer; // Always 10 = ZSoft .pcx 072 public final int version; // 0 = PC Paintbrush 2.5 073 // 2 = PC Paintbrush 2.8 with palette 074 // 3 = PC Paintbrush 2.8 w/o palette 075 // 4 = PC Paintbrush for Windows 076 // 5 = PC Paintbrush >= 3.0 077 public final int encoding; // 0 = very old uncompressed format, 1 = .pcx 078 // run length encoding 079 public final int bitsPerPixel; // Bits ***PER PLANE*** for each pixel 080 public final int xMin; // window 081 public final int yMin; 082 public final int xMax; 083 public final int yMax; 084 public final int hDpi; // horizontal dpi 085 public final int vDpi; // vertical dpi 086 public final int[] colormap; // palette for <= 16 colors 087 public final int reserved; // Always 0 088 public final int nPlanes; // Number of color planes 089 public final int bytesPerLine; // Number of bytes per scanline plane, 090 // must be an even number. 091 public final int paletteInfo; // 1 = Color/BW, 2 = Grayscale, ignored in 092 // Paintbrush IV/IV+ 093 public final int hScreenSize; // horizontal screen size, in pixels. 094 // PaintBrush >= IV only. 095 public final int vScreenSize; // vertical screen size, in pixels. 096 // PaintBrush >= IV only. 097 098 PcxHeader(final int manufacturer, final int version, final int encoding, final int bitsPerPixel, final int xMin, final int yMin, final int xMax, 099 final int yMax, final int hDpi, final int vDpi, final int[] colormap, final int reserved, final int nPlanes, final int bytesPerLine, 100 final int paletteInfo, final int hScreenSize, final int vScreenSize) { 101 this.manufacturer = manufacturer; 102 this.version = version; 103 this.encoding = encoding; 104 this.bitsPerPixel = bitsPerPixel; 105 this.xMin = xMin; 106 this.yMin = yMin; 107 this.xMax = xMax; 108 this.yMax = yMax; 109 this.hDpi = hDpi; 110 this.vDpi = vDpi; 111 this.colormap = colormap; 112 this.reserved = reserved; 113 this.nPlanes = nPlanes; 114 this.bytesPerLine = bytesPerLine; 115 this.paletteInfo = paletteInfo; 116 this.hScreenSize = hScreenSize; 117 this.vScreenSize = vScreenSize; 118 } 119 120 public void dump(final PrintWriter pw) { 121 pw.println("PcxHeader"); 122 pw.println("Manufacturer: " + manufacturer); 123 pw.println("Version: " + version); 124 pw.println("Encoding: " + encoding); 125 pw.println("BitsPerPixel: " + bitsPerPixel); 126 pw.println("xMin: " + xMin); 127 pw.println("yMin: " + yMin); 128 pw.println("xMax: " + xMax); 129 pw.println("yMax: " + yMax); 130 pw.println("hDpi: " + hDpi); 131 pw.println("vDpi: " + vDpi); 132 pw.print("ColorMap: "); 133 for (int i = 0; i < colormap.length; i++) { 134 if (i > 0) { 135 pw.print(","); 136 } 137 pw.print("(" + (0xff & colormap[i] >> 16) + "," + (0xff & colormap[i] >> 8) + "," + (0xff & colormap[i]) + ")"); 138 } 139 pw.println(); 140 pw.println("Reserved: " + reserved); 141 pw.println("nPlanes: " + nPlanes); 142 pw.println("BytesPerLine: " + bytesPerLine); 143 pw.println("PaletteInfo: " + paletteInfo); 144 pw.println("hScreenSize: " + hScreenSize); 145 pw.println("vScreenSize: " + vScreenSize); 146 pw.println(); 147 } 148 } 149 150 private static final String DEFAULT_EXTENSION = ImageFormats.PCX.getDefaultExtension(); 151 152 private static final String[] ACCEPTED_EXTENSIONS = ImageFormats.PCX.getExtensions(); 153 154 /** 155 * Constructs a new instance with the little-endian byte order. 156 */ 157 public PcxImageParser() { 158 super(ByteOrder.LITTLE_ENDIAN); 159 } 160 161 @Override 162 public boolean dumpImageFile(final PrintWriter pw, final ByteSource byteSource) throws ImagingException, IOException { 163 readPcxHeader(byteSource).dump(pw); 164 return true; 165 } 166 167 @Override 168 protected String[] getAcceptedExtensions() { 169 return ACCEPTED_EXTENSIONS; 170 } 171 172 @Override 173 protected ImageFormat[] getAcceptedTypes() { 174 return new ImageFormat[] { ImageFormats.PCX, // 175 }; 176 } 177 178 @Override 179 public final BufferedImage getBufferedImage(final ByteSource byteSource, PcxImagingParameters params) throws ImagingException, IOException { 180 if (params == null) { 181 params = new PcxImagingParameters(); 182 } 183 try (InputStream is = byteSource.getInputStream()) { 184 final PcxHeader pcxHeader = readPcxHeader(is, params.isStrict()); 185 return readImage(pcxHeader, is, byteSource); 186 } 187 } 188 189 @Override 190 public String getDefaultExtension() { 191 return DEFAULT_EXTENSION; 192 } 193 194 @Override 195 public PcxImagingParameters getDefaultParameters() { 196 return new PcxImagingParameters(); 197 } 198 199 @Override 200 public byte[] getIccProfileBytes(final ByteSource byteSource, final PcxImagingParameters params) throws ImagingException, IOException { 201 return null; 202 } 203 204 @Override 205 public ImageInfo getImageInfo(final ByteSource byteSource, final PcxImagingParameters params) throws ImagingException, IOException { 206 final PcxHeader pcxHeader = readPcxHeader(byteSource); 207 final Dimension size = getImageSize(byteSource, params); 208 return new ImageInfo("PCX", pcxHeader.nPlanes * pcxHeader.bitsPerPixel, new ArrayList<>(), ImageFormats.PCX, "ZSoft PCX Image", size.height, 209 "image/x-pcx", 1, pcxHeader.vDpi, Math.round(size.getHeight() / pcxHeader.vDpi), pcxHeader.hDpi, Math.round(size.getWidth() / pcxHeader.hDpi), 210 size.width, false, false, !(pcxHeader.nPlanes == 3 && pcxHeader.bitsPerPixel == 8), ImageInfo.ColorType.RGB, 211 pcxHeader.encoding == PcxHeader.ENCODING_RLE ? ImageInfo.CompressionAlgorithm.RLE : ImageInfo.CompressionAlgorithm.NONE); 212 } 213 214 @Override 215 public Dimension getImageSize(final ByteSource byteSource, final PcxImagingParameters params) throws ImagingException, IOException { 216 final PcxHeader pcxHeader = readPcxHeader(byteSource); 217 final int xSize = pcxHeader.xMax - pcxHeader.xMin + 1; 218 if (xSize < 0) { 219 throw new ImagingException("Image width is negative"); 220 } 221 final int ySize = pcxHeader.yMax - pcxHeader.yMin + 1; 222 if (ySize < 0) { 223 throw new ImagingException("Image height is negative"); 224 } 225 return new Dimension(xSize, ySize); 226 } 227 228 @Override 229 public ImageMetadata getMetadata(final ByteSource byteSource, final PcxImagingParameters params) throws ImagingException, IOException { 230 return null; 231 } 232 233 @Override 234 public String getName() { 235 return "Pcx-Custom"; 236 } 237 238 private int[] read256ColorPalette(final InputStream stream) throws IOException { 239 final byte[] paletteBytes = readBytes("Palette", stream, 769, "Error reading palette"); 240 if (paletteBytes[0] != 12) { 241 return null; 242 } 243 final int[] palette = new int[256]; 244 for (int i = 0; i < palette.length; i++) { 245 palette[i] = (0xff & paletteBytes[1 + 3 * i]) << 16 | (0xff & paletteBytes[1 + 3 * i + 1]) << 8 | 0xff & paletteBytes[1 + 3 * i + 2]; 246 } 247 return palette; 248 } 249 250 private int[] read256ColorPaletteFromEndOfFile(final ByteSource byteSource) throws IOException { 251 try (InputStream stream = byteSource.getInputStream()) { 252 final long toSkip = byteSource.size() - 769; 253 skipBytes(stream, (int) toSkip); 254 return read256ColorPalette(stream); 255 } 256 } 257 258 private BufferedImage readImage(final PcxHeader pcxHeader, final InputStream is, final ByteSource byteSource) throws ImagingException, IOException { 259 final int xSize = pcxHeader.xMax - pcxHeader.xMin + 1; 260 if (xSize < 0) { 261 throw new ImagingException("Image width is negative"); 262 } 263 final int ySize = pcxHeader.yMax - pcxHeader.yMin + 1; 264 if (ySize < 0) { 265 throw new ImagingException("Image height is negative"); 266 } 267 if (pcxHeader.nPlanes <= 0 || 4 < pcxHeader.nPlanes) { 268 throw new ImagingException("Unsupported/invalid image with " + pcxHeader.nPlanes + " planes"); 269 } 270 final RleReader rleReader; 271 if (pcxHeader.encoding == PcxHeader.ENCODING_UNCOMPRESSED) { 272 rleReader = new RleReader(false); 273 } else if (pcxHeader.encoding == PcxHeader.ENCODING_RLE) { 274 rleReader = new RleReader(true); 275 } else { 276 throw new ImagingException("Unsupported/invalid image encoding " + pcxHeader.encoding); 277 } 278 final int scanlineLength = pcxHeader.bytesPerLine * pcxHeader.nPlanes; 279 final byte[] scanline = Allocator.byteArray(scanlineLength); 280 if ((pcxHeader.bitsPerPixel == 1 || pcxHeader.bitsPerPixel == 2 || pcxHeader.bitsPerPixel == 4 || pcxHeader.bitsPerPixel == 8) 281 && pcxHeader.nPlanes == 1) { 282 final int bytesPerImageRow = (xSize * pcxHeader.bitsPerPixel + 7) / 8; 283 final byte[] image = Allocator.byteArray(ySize * bytesPerImageRow); 284 for (int y = 0; y < ySize; y++) { 285 rleReader.read(is, scanline); 286 System.arraycopy(scanline, 0, image, y * bytesPerImageRow, bytesPerImageRow); 287 } 288 final DataBufferByte dataBuffer = new DataBufferByte(image, image.length); 289 int[] palette; 290 if (pcxHeader.bitsPerPixel == 1) { 291 palette = new int[] { 0x000000, 0xffffff }; 292 } else if (pcxHeader.bitsPerPixel == 8) { 293 // Normally the palette is read 769 bytes from the end of the 294 // file. 295 // However DCX files have multiple PCX images in one file, so 296 // there could be extra data before the end! So try look for the 297 // palette 298 // immediately after the image data first. 299 palette = read256ColorPalette(is); 300 if (palette == null) { 301 palette = read256ColorPaletteFromEndOfFile(byteSource); 302 } 303 if (palette == null) { 304 throw new ImagingException("No 256 color palette found in image that needs it"); 305 } 306 } else { 307 palette = pcxHeader.colormap; 308 } 309 final WritableRaster raster; 310 if (pcxHeader.bitsPerPixel == 8) { 311 raster = Raster.createInterleavedRaster(dataBuffer, xSize, ySize, bytesPerImageRow, 1, new int[] { 0 }, null); 312 } else { 313 raster = Raster.createPackedRaster(dataBuffer, xSize, ySize, pcxHeader.bitsPerPixel, null); 314 } 315 final IndexColorModel colorModel = new IndexColorModel(pcxHeader.bitsPerPixel, 1 << pcxHeader.bitsPerPixel, palette, 0, false, -1, 316 DataBuffer.TYPE_BYTE); 317 return new BufferedImage(colorModel, raster, colorModel.isAlphaPremultiplied(), new Properties()); 318 } 319 if (pcxHeader.bitsPerPixel == 1 && 2 <= pcxHeader.nPlanes && pcxHeader.nPlanes <= 4) { 320 final IndexColorModel colorModel = new IndexColorModel(pcxHeader.nPlanes, 1 << pcxHeader.nPlanes, pcxHeader.colormap, 0, false, -1, 321 DataBuffer.TYPE_BYTE); 322 final BufferedImage image = new BufferedImage(xSize, ySize, BufferedImage.TYPE_BYTE_BINARY, colorModel); 323 final byte[] unpacked = Allocator.byteArray(xSize); 324 for (int y = 0; y < ySize; y++) { 325 rleReader.read(is, scanline); 326 int nextByte = 0; 327 Arrays.fill(unpacked, (byte) 0); 328 for (int plane = 0; plane < pcxHeader.nPlanes; plane++) { 329 for (int i = 0; i < pcxHeader.bytesPerLine; i++) { 330 final int b = 0xff & scanline[nextByte++]; 331 for (int j = 0; j < 8 && 8 * i + j < unpacked.length; j++) { 332 unpacked[8 * i + j] |= (byte) ((b >> 7 - j & 0x1) << plane); 333 } 334 } 335 } 336 image.getRaster().setDataElements(0, y, xSize, 1, unpacked); 337 } 338 return image; 339 } 340 if (pcxHeader.bitsPerPixel == 8 && pcxHeader.nPlanes == 3) { 341 final byte[][] image = new byte[3][]; 342 final int xySize = xSize * ySize; 343 image[0] = Allocator.byteArray(xySize); 344 image[1] = Allocator.byteArray(xySize); 345 image[2] = Allocator.byteArray(xySize); 346 for (int y = 0; y < ySize; y++) { 347 rleReader.read(is, scanline); 348 System.arraycopy(scanline, 0, image[0], y * xSize, xSize); 349 System.arraycopy(scanline, pcxHeader.bytesPerLine, image[1], y * xSize, xSize); 350 System.arraycopy(scanline, 2 * pcxHeader.bytesPerLine, image[2], y * xSize, xSize); 351 } 352 final DataBufferByte dataBuffer = new DataBufferByte(image, image[0].length); 353 final WritableRaster raster = Raster.createBandedRaster(dataBuffer, xSize, ySize, xSize, new int[] { 0, 1, 2 }, new int[] { 0, 0, 0 }, null); 354 final ColorModel colorModel = new ComponentColorModel(ColorSpace.getInstance(ColorSpace.CS_sRGB), false, false, Transparency.OPAQUE, 355 DataBuffer.TYPE_BYTE); 356 return new BufferedImage(colorModel, raster, colorModel.isAlphaPremultiplied(), new Properties()); 357 } 358 if ((pcxHeader.bitsPerPixel != 24 || pcxHeader.nPlanes != 1) && (pcxHeader.bitsPerPixel != 32 || pcxHeader.nPlanes != 1)) { 359 throw new ImagingException("Invalid/unsupported image with bitsPerPixel " + pcxHeader.bitsPerPixel + " and planes " + pcxHeader.nPlanes); 360 } 361 final int rowLength = 3 * xSize; 362 final byte[] image = Allocator.byteArray(rowLength * ySize); 363 for (int y = 0; y < ySize; y++) { 364 rleReader.read(is, scanline); 365 if (pcxHeader.bitsPerPixel == 24) { 366 System.arraycopy(scanline, 0, image, y * rowLength, rowLength); 367 } else { 368 for (int x = 0; x < xSize; x++) { 369 image[y * rowLength + 3 * x] = scanline[4 * x]; 370 image[y * rowLength + 3 * x + 1] = scanline[4 * x + 1]; 371 image[y * rowLength + 3 * x + 2] = scanline[4 * x + 2]; 372 } 373 } 374 } 375 final DataBufferByte dataBuffer = new DataBufferByte(image, image.length); 376 final WritableRaster raster = Raster.createInterleavedRaster(dataBuffer, xSize, ySize, rowLength, 3, new int[] { 2, 1, 0 }, null); 377 final ColorModel colorModel = new ComponentColorModel(ColorSpace.getInstance(ColorSpace.CS_sRGB), false, false, Transparency.OPAQUE, 378 DataBuffer.TYPE_BYTE); 379 return new BufferedImage(colorModel, raster, colorModel.isAlphaPremultiplied(), new Properties()); 380 } 381 382 private PcxHeader readPcxHeader(final ByteSource byteSource) throws ImagingException, IOException { 383 try (InputStream is = byteSource.getInputStream()) { 384 return readPcxHeader(is, false); 385 } 386 } 387 388 private PcxHeader readPcxHeader(final InputStream is, final boolean isStrict) throws ImagingException, IOException { 389 final byte[] pcxHeaderBytes = readBytes("PcxHeader", is, 128, "Not a Valid PCX File"); 390 final int manufacturer = 0xff & pcxHeaderBytes[0]; 391 final int version = 0xff & pcxHeaderBytes[1]; 392 final int encoding = 0xff & pcxHeaderBytes[2]; 393 final int bitsPerPixel = 0xff & pcxHeaderBytes[3]; 394 final int xMin = toUInt16(pcxHeaderBytes, 4, getByteOrder()); 395 final int yMin = toUInt16(pcxHeaderBytes, 6, getByteOrder()); 396 final int xMax = toUInt16(pcxHeaderBytes, 8, getByteOrder()); 397 final int yMax = toUInt16(pcxHeaderBytes, 10, getByteOrder()); 398 final int hDpi = toUInt16(pcxHeaderBytes, 12, getByteOrder()); 399 final int vDpi = toUInt16(pcxHeaderBytes, 14, getByteOrder()); 400 final int[] colormap = new int[16]; 401 Arrays.setAll(colormap, i -> 0xff000000 | (0xff & pcxHeaderBytes[16 + 3 * i]) << 16 | (0xff & pcxHeaderBytes[16 + 3 * i + 1]) << 8 402 | 0xff & pcxHeaderBytes[16 + 3 * i + 2]); 403 final int reserved = 0xff & pcxHeaderBytes[64]; 404 final int nPlanes = 0xff & pcxHeaderBytes[65]; 405 final int bytesPerLine = toUInt16(pcxHeaderBytes, 66, getByteOrder()); 406 final int paletteInfo = toUInt16(pcxHeaderBytes, 68, getByteOrder()); 407 final int hScreenSize = toUInt16(pcxHeaderBytes, 70, getByteOrder()); 408 final int vScreenSize = toUInt16(pcxHeaderBytes, 72, getByteOrder()); 409 410 if (manufacturer != 10) { 411 throw new ImagingException("Not a Valid PCX File: manufacturer is " + manufacturer); 412 } 413 // Note that reserved is sometimes set to a non-zero value 414 // by Paintbrush itself, so it shouldn't be enforced. 415 if (isStrict && bytesPerLine % 2 != 0) { 416 throw new ImagingException("Not a Valid PCX File: bytesPerLine is odd"); 417 } 418 419 return new PcxHeader(manufacturer, version, encoding, bitsPerPixel, xMin, yMin, xMax, yMax, hDpi, vDpi, colormap, reserved, nPlanes, bytesPerLine, 420 paletteInfo, hScreenSize, vScreenSize); 421 } 422 423 @Override 424 public void writeImage(final BufferedImage src, final OutputStream os, final PcxImagingParameters params) throws ImagingException, IOException { 425 new PcxWriter(params).writeImage(src, os); 426 } 427}