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 public PcxImageParser() { 155 super(ByteOrder.LITTLE_ENDIAN); 156 } 157 158 @Override 159 public boolean dumpImageFile(final PrintWriter pw, final ByteSource byteSource) throws ImagingException, IOException { 160 readPcxHeader(byteSource).dump(pw); 161 return true; 162 } 163 164 @Override 165 protected String[] getAcceptedExtensions() { 166 return ACCEPTED_EXTENSIONS; 167 } 168 169 @Override 170 protected ImageFormat[] getAcceptedTypes() { 171 return new ImageFormat[] { ImageFormats.PCX, // 172 }; 173 } 174 175 @Override 176 public final BufferedImage getBufferedImage(final ByteSource byteSource, PcxImagingParameters params) throws ImagingException, IOException { 177 if (params == null) { 178 params = new PcxImagingParameters(); 179 } 180 try (InputStream is = byteSource.getInputStream()) { 181 final PcxHeader pcxHeader = readPcxHeader(is, params.isStrict()); 182 return readImage(pcxHeader, is, byteSource); 183 } 184 } 185 186 @Override 187 public String getDefaultExtension() { 188 return DEFAULT_EXTENSION; 189 } 190 191 @Override 192 public PcxImagingParameters getDefaultParameters() { 193 return new PcxImagingParameters(); 194 } 195 196 @Override 197 public byte[] getIccProfileBytes(final ByteSource byteSource, final PcxImagingParameters params) throws ImagingException, IOException { 198 return null; 199 } 200 201 @Override 202 public ImageInfo getImageInfo(final ByteSource byteSource, final PcxImagingParameters params) throws ImagingException, IOException { 203 final PcxHeader pcxHeader = readPcxHeader(byteSource); 204 final Dimension size = getImageSize(byteSource, params); 205 return new ImageInfo("PCX", pcxHeader.nPlanes * pcxHeader.bitsPerPixel, new ArrayList<>(), ImageFormats.PCX, "ZSoft PCX Image", size.height, 206 "image/x-pcx", 1, pcxHeader.vDpi, Math.round(size.getHeight() / pcxHeader.vDpi), pcxHeader.hDpi, Math.round(size.getWidth() / pcxHeader.hDpi), 207 size.width, false, false, !(pcxHeader.nPlanes == 3 && pcxHeader.bitsPerPixel == 8), ImageInfo.ColorType.RGB, 208 pcxHeader.encoding == PcxHeader.ENCODING_RLE ? ImageInfo.CompressionAlgorithm.RLE : ImageInfo.CompressionAlgorithm.NONE); 209 } 210 211 @Override 212 public Dimension getImageSize(final ByteSource byteSource, final PcxImagingParameters params) throws ImagingException, IOException { 213 final PcxHeader pcxHeader = readPcxHeader(byteSource); 214 final int xSize = pcxHeader.xMax - pcxHeader.xMin + 1; 215 if (xSize < 0) { 216 throw new ImagingException("Image width is negative"); 217 } 218 final int ySize = pcxHeader.yMax - pcxHeader.yMin + 1; 219 if (ySize < 0) { 220 throw new ImagingException("Image height is negative"); 221 } 222 return new Dimension(xSize, ySize); 223 } 224 225 @Override 226 public ImageMetadata getMetadata(final ByteSource byteSource, final PcxImagingParameters params) throws ImagingException, IOException { 227 return null; 228 } 229 230 @Override 231 public String getName() { 232 return "Pcx-Custom"; 233 } 234 235 private int[] read256ColorPalette(final InputStream stream) throws IOException { 236 final byte[] paletteBytes = readBytes("Palette", stream, 769, "Error reading palette"); 237 if (paletteBytes[0] != 12) { 238 return null; 239 } 240 final int[] palette = new int[256]; 241 for (int i = 0; i < palette.length; i++) { 242 palette[i] = (0xff & paletteBytes[1 + 3 * i]) << 16 | (0xff & paletteBytes[1 + 3 * i + 1]) << 8 | 0xff & paletteBytes[1 + 3 * i + 2]; 243 } 244 return palette; 245 } 246 247 private int[] read256ColorPaletteFromEndOfFile(final ByteSource byteSource) throws IOException { 248 try (InputStream stream = byteSource.getInputStream()) { 249 final long toSkip = byteSource.size() - 769; 250 skipBytes(stream, (int) toSkip); 251 return read256ColorPalette(stream); 252 } 253 } 254 255 private BufferedImage readImage(final PcxHeader pcxHeader, final InputStream is, final ByteSource byteSource) throws ImagingException, IOException { 256 final int xSize = pcxHeader.xMax - pcxHeader.xMin + 1; 257 if (xSize < 0) { 258 throw new ImagingException("Image width is negative"); 259 } 260 final int ySize = pcxHeader.yMax - pcxHeader.yMin + 1; 261 if (ySize < 0) { 262 throw new ImagingException("Image height is negative"); 263 } 264 if (pcxHeader.nPlanes <= 0 || 4 < pcxHeader.nPlanes) { 265 throw new ImagingException("Unsupported/invalid image with " + pcxHeader.nPlanes + " planes"); 266 } 267 final RleReader rleReader; 268 if (pcxHeader.encoding == PcxHeader.ENCODING_UNCOMPRESSED) { 269 rleReader = new RleReader(false); 270 } else if (pcxHeader.encoding == PcxHeader.ENCODING_RLE) { 271 rleReader = new RleReader(true); 272 } else { 273 throw new ImagingException("Unsupported/invalid image encoding " + pcxHeader.encoding); 274 } 275 final int scanlineLength = pcxHeader.bytesPerLine * pcxHeader.nPlanes; 276 final byte[] scanline = Allocator.byteArray(scanlineLength); 277 if ((pcxHeader.bitsPerPixel == 1 || pcxHeader.bitsPerPixel == 2 || pcxHeader.bitsPerPixel == 4 || pcxHeader.bitsPerPixel == 8) 278 && pcxHeader.nPlanes == 1) { 279 final int bytesPerImageRow = (xSize * pcxHeader.bitsPerPixel + 7) / 8; 280 final byte[] image = Allocator.byteArray(ySize * bytesPerImageRow); 281 for (int y = 0; y < ySize; y++) { 282 rleReader.read(is, scanline); 283 System.arraycopy(scanline, 0, image, y * bytesPerImageRow, bytesPerImageRow); 284 } 285 final DataBufferByte dataBuffer = new DataBufferByte(image, image.length); 286 int[] palette; 287 if (pcxHeader.bitsPerPixel == 1) { 288 palette = new int[] { 0x000000, 0xffffff }; 289 } else if (pcxHeader.bitsPerPixel == 8) { 290 // Normally the palette is read 769 bytes from the end of the 291 // file. 292 // However DCX files have multiple PCX images in one file, so 293 // there could be extra data before the end! So try look for the 294 // palette 295 // immediately after the image data first. 296 palette = read256ColorPalette(is); 297 if (palette == null) { 298 palette = read256ColorPaletteFromEndOfFile(byteSource); 299 } 300 if (palette == null) { 301 throw new ImagingException("No 256 color palette found in image that needs it"); 302 } 303 } else { 304 palette = pcxHeader.colormap; 305 } 306 WritableRaster raster; 307 if (pcxHeader.bitsPerPixel == 8) { 308 raster = Raster.createInterleavedRaster(dataBuffer, xSize, ySize, bytesPerImageRow, 1, new int[] { 0 }, null); 309 } else { 310 raster = Raster.createPackedRaster(dataBuffer, xSize, ySize, pcxHeader.bitsPerPixel, null); 311 } 312 final IndexColorModel colorModel = new IndexColorModel(pcxHeader.bitsPerPixel, 1 << pcxHeader.bitsPerPixel, palette, 0, false, -1, 313 DataBuffer.TYPE_BYTE); 314 return new BufferedImage(colorModel, raster, colorModel.isAlphaPremultiplied(), new Properties()); 315 } 316 if (pcxHeader.bitsPerPixel == 1 && 2 <= pcxHeader.nPlanes && pcxHeader.nPlanes <= 4) { 317 final IndexColorModel colorModel = new IndexColorModel(pcxHeader.nPlanes, 1 << pcxHeader.nPlanes, pcxHeader.colormap, 0, false, -1, 318 DataBuffer.TYPE_BYTE); 319 final BufferedImage image = new BufferedImage(xSize, ySize, BufferedImage.TYPE_BYTE_BINARY, colorModel); 320 final byte[] unpacked = Allocator.byteArray(xSize); 321 for (int y = 0; y < ySize; y++) { 322 rleReader.read(is, scanline); 323 int nextByte = 0; 324 Arrays.fill(unpacked, (byte) 0); 325 for (int plane = 0; plane < pcxHeader.nPlanes; plane++) { 326 for (int i = 0; i < pcxHeader.bytesPerLine; i++) { 327 final int b = 0xff & scanline[nextByte++]; 328 for (int j = 0; j < 8 && 8 * i + j < unpacked.length; j++) { 329 unpacked[8 * i + j] |= (byte) ((b >> 7 - j & 0x1) << plane); 330 } 331 } 332 } 333 image.getRaster().setDataElements(0, y, xSize, 1, unpacked); 334 } 335 return image; 336 } 337 if (pcxHeader.bitsPerPixel == 8 && pcxHeader.nPlanes == 3) { 338 final byte[][] image = new byte[3][]; 339 final int xySize = xSize * ySize; 340 image[0] = Allocator.byteArray(xySize); 341 image[1] = Allocator.byteArray(xySize); 342 image[2] = Allocator.byteArray(xySize); 343 for (int y = 0; y < ySize; y++) { 344 rleReader.read(is, scanline); 345 System.arraycopy(scanline, 0, image[0], y * xSize, xSize); 346 System.arraycopy(scanline, pcxHeader.bytesPerLine, image[1], y * xSize, xSize); 347 System.arraycopy(scanline, 2 * pcxHeader.bytesPerLine, image[2], y * xSize, xSize); 348 } 349 final DataBufferByte dataBuffer = new DataBufferByte(image, image[0].length); 350 final WritableRaster raster = Raster.createBandedRaster(dataBuffer, xSize, ySize, xSize, new int[] { 0, 1, 2 }, new int[] { 0, 0, 0 }, null); 351 final ColorModel colorModel = new ComponentColorModel(ColorSpace.getInstance(ColorSpace.CS_sRGB), false, false, Transparency.OPAQUE, 352 DataBuffer.TYPE_BYTE); 353 return new BufferedImage(colorModel, raster, colorModel.isAlphaPremultiplied(), new Properties()); 354 } 355 if ((pcxHeader.bitsPerPixel != 24 || pcxHeader.nPlanes != 1) && (pcxHeader.bitsPerPixel != 32 || pcxHeader.nPlanes != 1)) { 356 throw new ImagingException("Invalid/unsupported image with bitsPerPixel " + pcxHeader.bitsPerPixel + " and planes " + pcxHeader.nPlanes); 357 } 358 final int rowLength = 3 * xSize; 359 final byte[] image = Allocator.byteArray(rowLength * ySize); 360 for (int y = 0; y < ySize; y++) { 361 rleReader.read(is, scanline); 362 if (pcxHeader.bitsPerPixel == 24) { 363 System.arraycopy(scanline, 0, image, y * rowLength, rowLength); 364 } else { 365 for (int x = 0; x < xSize; x++) { 366 image[y * rowLength + 3 * x] = scanline[4 * x]; 367 image[y * rowLength + 3 * x + 1] = scanline[4 * x + 1]; 368 image[y * rowLength + 3 * x + 2] = scanline[4 * x + 2]; 369 } 370 } 371 } 372 final DataBufferByte dataBuffer = new DataBufferByte(image, image.length); 373 final WritableRaster raster = Raster.createInterleavedRaster(dataBuffer, xSize, ySize, rowLength, 3, new int[] { 2, 1, 0 }, null); 374 final ColorModel colorModel = new ComponentColorModel(ColorSpace.getInstance(ColorSpace.CS_sRGB), false, false, Transparency.OPAQUE, 375 DataBuffer.TYPE_BYTE); 376 return new BufferedImage(colorModel, raster, colorModel.isAlphaPremultiplied(), new Properties()); 377 } 378 379 private PcxHeader readPcxHeader(final ByteSource byteSource) throws ImagingException, IOException { 380 try (InputStream is = byteSource.getInputStream()) { 381 return readPcxHeader(is, false); 382 } 383 } 384 385 private PcxHeader readPcxHeader(final InputStream is, final boolean isStrict) throws ImagingException, IOException { 386 final byte[] pcxHeaderBytes = readBytes("PcxHeader", is, 128, "Not a Valid PCX File"); 387 final int manufacturer = 0xff & pcxHeaderBytes[0]; 388 final int version = 0xff & pcxHeaderBytes[1]; 389 final int encoding = 0xff & pcxHeaderBytes[2]; 390 final int bitsPerPixel = 0xff & pcxHeaderBytes[3]; 391 final int xMin = toUInt16(pcxHeaderBytes, 4, getByteOrder()); 392 final int yMin = toUInt16(pcxHeaderBytes, 6, getByteOrder()); 393 final int xMax = toUInt16(pcxHeaderBytes, 8, getByteOrder()); 394 final int yMax = toUInt16(pcxHeaderBytes, 10, getByteOrder()); 395 final int hDpi = toUInt16(pcxHeaderBytes, 12, getByteOrder()); 396 final int vDpi = toUInt16(pcxHeaderBytes, 14, getByteOrder()); 397 final int[] colormap = new int[16]; 398 Arrays.setAll(colormap, i -> 0xff000000 | (0xff & pcxHeaderBytes[16 + 3 * i]) << 16 | (0xff & pcxHeaderBytes[16 + 3 * i + 1]) << 8 399 | 0xff & pcxHeaderBytes[16 + 3 * i + 2]); 400 final int reserved = 0xff & pcxHeaderBytes[64]; 401 final int nPlanes = 0xff & pcxHeaderBytes[65]; 402 final int bytesPerLine = toUInt16(pcxHeaderBytes, 66, getByteOrder()); 403 final int paletteInfo = toUInt16(pcxHeaderBytes, 68, getByteOrder()); 404 final int hScreenSize = toUInt16(pcxHeaderBytes, 70, getByteOrder()); 405 final int vScreenSize = toUInt16(pcxHeaderBytes, 72, getByteOrder()); 406 407 if (manufacturer != 10) { 408 throw new ImagingException("Not a Valid PCX File: manufacturer is " + manufacturer); 409 } 410 if (isStrict) { 411 // Note that reserved is sometimes set to a non-zero value 412 // by Paintbrush itself, so it shouldn't be enforced. 413 if (bytesPerLine % 2 != 0) { 414 throw new ImagingException("Not a Valid PCX File: bytesPerLine is odd"); 415 } 416 } 417 418 return new PcxHeader(manufacturer, version, encoding, bitsPerPixel, xMin, yMin, xMax, yMax, hDpi, vDpi, colormap, reserved, nPlanes, bytesPerLine, 419 paletteInfo, hScreenSize, vScreenSize); 420 } 421 422 @Override 423 public void writeImage(final BufferedImage src, final OutputStream os, final PcxImagingParameters params) throws ImagingException, IOException { 424 new PcxWriter(params).writeImage(src, os); 425 } 426}