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