View Javadoc
1   /*
2    * Licensed to the Apache Software Foundation (ASF) under one or more
3    * contributor license agreements.  See the NOTICE file distributed with
4    * this work for additional information regarding copyright ownership.
5    * The ASF licenses this file to You under the Apache License, Version 2.0
6    * (the "License"); you may not use this file except in compliance with
7    * the License.  You may obtain a copy of the License at
8    *
9    *      http://www.apache.org/licenses/LICENSE-2.0
10   *
11   * Unless required by applicable law or agreed to in writing, software
12   * distributed under the License is distributed on an "AS IS" BASIS,
13   * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14   * See the License for the specific language governing permissions and
15   * limitations under the License.
16   */
17  package org.apache.commons.imaging.formats.pcx;
18  
19  import static org.apache.commons.imaging.common.BinaryFunctions.readBytes;
20  import static org.apache.commons.imaging.common.BinaryFunctions.skipBytes;
21  import static org.apache.commons.imaging.common.ByteConversions.toUInt16;
22  
23  import java.awt.Dimension;
24  import java.awt.Transparency;
25  import java.awt.color.ColorSpace;
26  import java.awt.image.BufferedImage;
27  import java.awt.image.ColorModel;
28  import java.awt.image.ComponentColorModel;
29  import java.awt.image.DataBuffer;
30  import java.awt.image.DataBufferByte;
31  import java.awt.image.IndexColorModel;
32  import java.awt.image.Raster;
33  import java.awt.image.WritableRaster;
34  import java.io.IOException;
35  import java.io.InputStream;
36  import java.io.OutputStream;
37  import java.io.PrintWriter;
38  import java.nio.ByteOrder;
39  import java.util.ArrayList;
40  import java.util.Arrays;
41  import java.util.Properties;
42  
43  import org.apache.commons.imaging.ImageFormat;
44  import org.apache.commons.imaging.ImageFormats;
45  import org.apache.commons.imaging.ImageInfo;
46  import org.apache.commons.imaging.ImageParser;
47  import org.apache.commons.imaging.ImageReadException;
48  import org.apache.commons.imaging.ImageWriteException;
49  import org.apache.commons.imaging.common.ImageMetadata;
50  import org.apache.commons.imaging.common.bytesource.ByteSource;
51  
52  public class PcxImageParser extends ImageParser<PcxImagingParameters> {
53      // ZSoft's official spec is at http://www.qzx.com/pc-gpe/pcx.txt
54      // (among other places) but it's pretty thin. The fileformat.fine document
55      // at http://www.fileformat.fine/format/pcx/egff.htm is a little better
56      // but their gray sample image seems corrupt. PCX files themselves are
57      // the ultimate test but pretty hard to find nowadays, so the best
58      // test is against other image viewers (Irfanview is pretty good).
59      //
60      // Open source projects are generally poor at parsing PCX,
61      // SDL_Image/gdk-pixbuf/Eye of Gnome/GIMP/F-Spot all only do some formats,
62      // don't support uncompressed PCX, and/or don't handle black and white
63      // images properly.
64  
65      private static final String DEFAULT_EXTENSION = ImageFormats.PCX.getDefaultExtension();
66      private static final String[] ACCEPTED_EXTENSIONS = ImageFormats.PCX.getExtensions();
67  
68      public PcxImageParser() {
69          super.setByteOrder(ByteOrder.LITTLE_ENDIAN);
70      }
71  
72      @Override
73      public PcxImagingParameters getDefaultParameters() {
74          return new PcxImagingParameters();
75      }
76  
77      @Override
78      public String getName() {
79          return "Pcx-Custom";
80      }
81  
82      @Override
83      public String getDefaultExtension() {
84          return DEFAULT_EXTENSION;
85      }
86  
87      @Override
88      protected String[] getAcceptedExtensions() {
89          return ACCEPTED_EXTENSIONS;
90      }
91  
92      @Override
93      protected ImageFormat[] getAcceptedTypes() {
94          return new ImageFormat[] { ImageFormats.PCX, //
95          };
96      }
97  
98      @Override
99      public ImageMetadata getMetadata(final ByteSource byteSource, final PcxImagingParameters params)
100             throws ImageReadException, IOException {
101         return null;
102     }
103 
104     @Override
105     public ImageInfo getImageInfo(final ByteSource byteSource, final PcxImagingParameters params)
106             throws ImageReadException, IOException {
107         final PcxHeader pcxHeader = readPcxHeader(byteSource);
108         final Dimension size = getImageSize(byteSource, params);
109         return new ImageInfo(
110                 "PCX",
111                 pcxHeader.nPlanes * pcxHeader.bitsPerPixel,
112                 new ArrayList<>(),
113                 ImageFormats.PCX,
114                 "ZSoft PCX Image",
115                 size.height,
116                 "image/x-pcx",
117                 1,
118                 pcxHeader.vDpi,
119                 Math.round(size.getHeight() / pcxHeader.vDpi),
120                 pcxHeader.hDpi,
121                 Math.round(size.getWidth() / pcxHeader.hDpi),
122                 size.width,
123                 false,
124                 false,
125                 !(pcxHeader.nPlanes == 3 && pcxHeader.bitsPerPixel == 8),
126                 ImageInfo.ColorType.RGB,
127                 pcxHeader.encoding == PcxHeader.ENCODING_RLE ? ImageInfo.CompressionAlgorithm.RLE
128                         : ImageInfo.CompressionAlgorithm.NONE);
129     }
130 
131     @Override
132     public Dimension getImageSize(final ByteSource byteSource, final PcxImagingParameters params)
133             throws ImageReadException, IOException {
134         final PcxHeader pcxHeader = readPcxHeader(byteSource);
135         final int xSize = pcxHeader.xMax - pcxHeader.xMin + 1;
136         if (xSize < 0) {
137             throw new ImageReadException("Image width is negative");
138         }
139         final int ySize = pcxHeader.yMax - pcxHeader.yMin + 1;
140         if (ySize < 0) {
141             throw new ImageReadException("Image height is negative");
142         }
143         return new Dimension(xSize, ySize);
144     }
145 
146     @Override
147     public byte[] getICCProfileBytes(final ByteSource byteSource, final PcxImagingParameters params)
148             throws ImageReadException, IOException {
149         return null;
150     }
151 
152     static class PcxHeader {
153 
154         public static final int ENCODING_UNCOMPRESSED = 0;
155         public static final int ENCODING_RLE = 1;
156         public static final int PALETTE_INFO_COLOR = 1;
157         public static final int PALETTE_INFO_GRAYSCALE = 2;
158         public final int manufacturer; // Always 10 = ZSoft .pcx
159         public final int version; // 0 = PC Paintbrush 2.5
160                                   // 2 = PC Paintbrush 2.8 with palette
161                                   // 3 = PC Paintbrush 2.8 w/o palette
162                                   // 4 = PC Paintbrush for Windows
163                                   // 5 = PC Paintbrush >= 3.0
164         public final int encoding; // 0 = very old uncompressed format, 1 = .pcx
165                                    // run length encoding
166         public final int bitsPerPixel; // Bits ***PER PLANE*** for each pixel
167         public final int xMin; // window
168         public final int yMin;
169         public final int xMax;
170         public final int yMax;
171         public final int hDpi; // horizontal dpi
172         public final int vDpi; // vertical dpi
173         public final int[] colormap; // palette for <= 16 colors
174         public final int reserved; // Always 0
175         public final int nPlanes; // Number of color planes
176         public final int bytesPerLine; // Number of bytes per scanline plane,
177                                        // must be an even number.
178         public final int paletteInfo; // 1 = Color/BW, 2 = Grayscale, ignored in
179                                       // Paintbrush IV/IV+
180         public final int hScreenSize; // horizontal screen size, in pixels.
181                                       // PaintBrush >= IV only.
182         public final int vScreenSize; // vertical screen size, in pixels.
183                                       // PaintBrush >= IV only.
184 
185         PcxHeader(final int manufacturer, final int version,
186                 final int encoding, final int bitsPerPixel, final int xMin,
187                 final int yMin, final int xMax, final int yMax, final int hDpi,
188                 final int vDpi, final int[] colormap, final int reserved,
189                 final int nPlanes, final int bytesPerLine,
190                 final int paletteInfo, final int hScreenSize,
191                 final int vScreenSize) {
192             this.manufacturer = manufacturer;
193             this.version = version;
194             this.encoding = encoding;
195             this.bitsPerPixel = bitsPerPixel;
196             this.xMin = xMin;
197             this.yMin = yMin;
198             this.xMax = xMax;
199             this.yMax = yMax;
200             this.hDpi = hDpi;
201             this.vDpi = vDpi;
202             this.colormap = colormap;
203             this.reserved = reserved;
204             this.nPlanes = nPlanes;
205             this.bytesPerLine = bytesPerLine;
206             this.paletteInfo = paletteInfo;
207             this.hScreenSize = hScreenSize;
208             this.vScreenSize = vScreenSize;
209         }
210 
211         public void dump(final PrintWriter pw) {
212             pw.println("PcxHeader");
213             pw.println("Manufacturer: " + manufacturer);
214             pw.println("Version: " + version);
215             pw.println("Encoding: " + encoding);
216             pw.println("BitsPerPixel: " + bitsPerPixel);
217             pw.println("xMin: " + xMin);
218             pw.println("yMin: " + yMin);
219             pw.println("xMax: " + xMax);
220             pw.println("yMax: " + yMax);
221             pw.println("hDpi: " + hDpi);
222             pw.println("vDpi: " + vDpi);
223             pw.print("ColorMap: ");
224             for (int i = 0; i < colormap.length; i++) {
225                 if (i > 0) {
226                     pw.print(",");
227                 }
228                 pw.print("(" + (0xff & (colormap[i] >> 16)) + ","
229                         + (0xff & (colormap[i] >> 8)) + ","
230                         + (0xff & colormap[i]) + ")");
231             }
232             pw.println();
233             pw.println("Reserved: " + reserved);
234             pw.println("nPlanes: " + nPlanes);
235             pw.println("BytesPerLine: " + bytesPerLine);
236             pw.println("PaletteInfo: " + paletteInfo);
237             pw.println("hScreenSize: " + hScreenSize);
238             pw.println("vScreenSize: " + vScreenSize);
239             pw.println();
240         }
241     }
242 
243     private PcxHeader readPcxHeader(final ByteSource byteSource)
244             throws ImageReadException, IOException {
245         try (InputStream is = byteSource.getInputStream()) {
246             return readPcxHeader(is, false);
247         }
248     }
249 
250     private PcxHeader readPcxHeader(final InputStream is, final boolean isStrict)
251             throws ImageReadException, IOException {
252         final byte[] pcxHeaderBytes = readBytes("PcxHeader", is, 128,
253                 "Not a Valid PCX File");
254         final int manufacturer = 0xff & pcxHeaderBytes[0];
255         final int version = 0xff & pcxHeaderBytes[1];
256         final int encoding = 0xff & pcxHeaderBytes[2];
257         final int bitsPerPixel = 0xff & pcxHeaderBytes[3];
258         final int xMin = toUInt16(pcxHeaderBytes, 4, getByteOrder());
259         final int yMin = toUInt16(pcxHeaderBytes, 6, getByteOrder());
260         final int xMax = toUInt16(pcxHeaderBytes, 8, getByteOrder());
261         final int yMax = toUInt16(pcxHeaderBytes, 10, getByteOrder());
262         final int hDpi = toUInt16(pcxHeaderBytes, 12, getByteOrder());
263         final int vDpi = toUInt16(pcxHeaderBytes, 14, getByteOrder());
264         final int[] colormap = new int[16];
265         for (int i = 0; i < 16; i++) {
266             colormap[i] = 0xff000000
267                     | ((0xff & pcxHeaderBytes[16 + 3 * i]) << 16)
268                     | ((0xff & pcxHeaderBytes[16 + 3 * i + 1]) << 8)
269                     | (0xff & pcxHeaderBytes[16 + 3 * i + 2]);
270         }
271         final int reserved = 0xff & pcxHeaderBytes[64];
272         final int nPlanes = 0xff & pcxHeaderBytes[65];
273         final int bytesPerLine = toUInt16(pcxHeaderBytes, 66, getByteOrder());
274         final int paletteInfo = toUInt16(pcxHeaderBytes, 68, getByteOrder());
275         final int hScreenSize = toUInt16(pcxHeaderBytes, 70, getByteOrder());
276         final int vScreenSize = toUInt16(pcxHeaderBytes, 72, getByteOrder());
277 
278         if (manufacturer != 10) {
279             throw new ImageReadException(
280                     "Not a Valid PCX File: manufacturer is " + manufacturer);
281         }
282         if (isStrict) {
283             // Note that reserved is sometimes set to a non-zero value
284             // by Paintbrush itself, so it shouldn't be enforced.
285             if (bytesPerLine % 2 != 0) {
286                 throw new ImageReadException(
287                         "Not a Valid PCX File: bytesPerLine is odd");
288             }
289         }
290 
291         return new PcxHeader(manufacturer, version, encoding, bitsPerPixel,
292                 xMin, yMin, xMax, yMax, hDpi, vDpi, colormap, reserved,
293                 nPlanes, bytesPerLine, paletteInfo, hScreenSize, vScreenSize);
294     }
295 
296     @Override
297     public boolean dumpImageFile(final PrintWriter pw, final ByteSource byteSource)
298             throws ImageReadException, IOException {
299         readPcxHeader(byteSource).dump(pw);
300         return true;
301     }
302 
303     private int[] read256ColorPalette(final InputStream stream) throws IOException {
304         final byte[] paletteBytes = readBytes("Palette", stream, 769,
305                 "Error reading palette");
306         if (paletteBytes[0] != 12) {
307             return null;
308         }
309         final int[] palette = new int[256];
310         for (int i = 0; i < palette.length; i++) {
311             palette[i] = ((0xff & paletteBytes[1 + 3 * i]) << 16)
312                     | ((0xff & paletteBytes[1 + 3 * i + 1]) << 8)
313                     | (0xff & paletteBytes[1 + 3 * i + 2]);
314         }
315         return palette;
316     }
317 
318     private int[] read256ColorPaletteFromEndOfFile(final ByteSource byteSource)
319             throws IOException {
320         try (InputStream stream = byteSource.getInputStream()) {
321             final long toSkip = byteSource.getLength() - 769;
322             skipBytes(stream, (int) toSkip);
323             return read256ColorPalette(stream);
324         }
325     }
326 
327     private BufferedImage readImage(final PcxHeader pcxHeader, final InputStream is,
328             final ByteSource byteSource) throws ImageReadException, IOException {
329         final int xSize = pcxHeader.xMax - pcxHeader.xMin + 1;
330         if (xSize < 0) {
331             throw new ImageReadException("Image width is negative");
332         }
333         final int ySize = pcxHeader.yMax - pcxHeader.yMin + 1;
334         if (ySize < 0) {
335             throw new ImageReadException("Image height is negative");
336         }
337         if (pcxHeader.nPlanes <= 0 || 4 < pcxHeader.nPlanes) {
338             throw new ImageReadException("Unsupported/invalid image with " + pcxHeader.nPlanes + " planes");
339         }
340         final RleReader rleReader;
341         if (pcxHeader.encoding == PcxHeader.ENCODING_UNCOMPRESSED) {
342             rleReader = new RleReader(false);
343         } else if (pcxHeader.encoding == PcxHeader.ENCODING_RLE) {
344             rleReader = new RleReader(true);
345         } else {
346             throw new ImageReadException("Unsupported/invalid image encoding " + pcxHeader.encoding);
347         }
348         final int scanlineLength = pcxHeader.bytesPerLine * pcxHeader.nPlanes;
349         final byte[] scanline = new byte[scanlineLength];
350         if ((pcxHeader.bitsPerPixel == 1 || pcxHeader.bitsPerPixel == 2
351                 || pcxHeader.bitsPerPixel == 4 || pcxHeader.bitsPerPixel == 8)
352                 && pcxHeader.nPlanes == 1) {
353             final int bytesPerImageRow = (xSize * pcxHeader.bitsPerPixel + 7) / 8;
354             final byte[] image = new byte[ySize * bytesPerImageRow];
355             for (int y = 0; y < ySize; y++) {
356                 rleReader.read(is, scanline);
357                 System.arraycopy(scanline, 0, image, y * bytesPerImageRow,
358                         bytesPerImageRow);
359             }
360             final DataBufferByte dataBuffer = new DataBufferByte(image, image.length);
361             int[] palette;
362             if (pcxHeader.bitsPerPixel == 1) {
363                 palette = new int[] { 0x000000, 0xffffff };
364             } else if (pcxHeader.bitsPerPixel == 8) {
365                 // Normally the palette is read 769 bytes from the end of the
366                 // file.
367                 // However DCX files have multiple PCX images in one file, so
368                 // there could be extra data before the end! So try look for the
369                 // palette
370                 // immediately after the image data first.
371                 palette = read256ColorPalette(is);
372                 if (palette == null) {
373                     palette = read256ColorPaletteFromEndOfFile(byteSource);
374                 }
375                 if (palette == null) {
376                     throw new ImageReadException(
377                             "No 256 color palette found in image that needs it");
378                 }
379             } else {
380                 palette = pcxHeader.colormap;
381             }
382             WritableRaster raster;
383             if (pcxHeader.bitsPerPixel == 8) {
384                 raster = Raster.createInterleavedRaster(dataBuffer,
385                         xSize, ySize, bytesPerImageRow, 1, new int[] { 0 },
386                         null);
387             } else {
388                 raster = Raster.createPackedRaster(dataBuffer, xSize,
389                         ySize, pcxHeader.bitsPerPixel, null);
390             }
391             final IndexColorModel colorModel = new IndexColorModel(
392                     pcxHeader.bitsPerPixel, 1 << pcxHeader.bitsPerPixel,
393                     palette, 0, false, -1, DataBuffer.TYPE_BYTE);
394             return new BufferedImage(colorModel, raster,
395                     colorModel.isAlphaPremultiplied(), new Properties());
396         }
397         if (pcxHeader.bitsPerPixel == 1 && 2 <= pcxHeader.nPlanes
398                 && pcxHeader.nPlanes <= 4) {
399             final IndexColorModel colorModel = new IndexColorModel(pcxHeader.nPlanes,
400                     1 << pcxHeader.nPlanes, pcxHeader.colormap, 0, false, -1,
401                     DataBuffer.TYPE_BYTE);
402             final BufferedImage image = new BufferedImage(xSize, ySize,
403                     BufferedImage.TYPE_BYTE_BINARY, colorModel);
404             final byte[] unpacked = new byte[xSize];
405             for (int y = 0; y < ySize; y++) {
406                 rleReader.read(is, scanline);
407                 int nextByte = 0;
408                 Arrays.fill(unpacked, (byte) 0);
409                 for (int plane = 0; plane < pcxHeader.nPlanes; plane++) {
410                     for (int i = 0; i < pcxHeader.bytesPerLine; i++) {
411                         final int b = 0xff & scanline[nextByte++];
412                         for (int j = 0; j < 8 && 8 * i + j < unpacked.length; j++) {
413                             unpacked[8 * i + j] |= (byte) (((b >> (7 - j)) & 0x1) << plane);
414                         }
415                     }
416                 }
417                 image.getRaster().setDataElements(0, y, xSize, 1, unpacked);
418             }
419             return image;
420         }
421         if (pcxHeader.bitsPerPixel == 8 && pcxHeader.nPlanes == 3) {
422             final byte[][] image = new byte[3][];
423             image[0] = new byte[xSize * ySize];
424             image[1] = new byte[xSize * ySize];
425             image[2] = new byte[xSize * ySize];
426             for (int y = 0; y < ySize; y++) {
427                 rleReader.read(is, scanline);
428                 System.arraycopy(scanline, 0, image[0], y * xSize, xSize);
429                 System.arraycopy(scanline, pcxHeader.bytesPerLine, image[1], y
430                         * xSize, xSize);
431                 System.arraycopy(scanline, 2 * pcxHeader.bytesPerLine,
432                         image[2], y * xSize, xSize);
433             }
434             final DataBufferByte dataBuffer = new DataBufferByte(image,
435                     image[0].length);
436             final WritableRaster raster = Raster.createBandedRaster(
437                     dataBuffer, xSize, ySize, xSize, new int[] { 0, 1, 2 },
438                     new int[] { 0, 0, 0 }, null);
439             final ColorModel colorModel = new ComponentColorModel(
440                     ColorSpace.getInstance(ColorSpace.CS_sRGB), false, false,
441                     Transparency.OPAQUE, DataBuffer.TYPE_BYTE);
442             return new BufferedImage(colorModel, raster,
443                     colorModel.isAlphaPremultiplied(), new Properties());
444         }
445         if (((pcxHeader.bitsPerPixel != 24) || (pcxHeader.nPlanes != 1)) && ((pcxHeader.bitsPerPixel != 32) || (pcxHeader.nPlanes != 1))) {
446             throw new ImageReadException(
447                     "Invalid/unsupported image with bitsPerPixel "
448                             + pcxHeader.bitsPerPixel + " and planes "
449                             + pcxHeader.nPlanes);
450         }
451         final int rowLength = 3 * xSize;
452         final byte[] image = new byte[rowLength * ySize];
453         for (int y = 0; y < ySize; y++) {
454             rleReader.read(is, scanline);
455             if (pcxHeader.bitsPerPixel == 24) {
456                 System.arraycopy(scanline, 0, image, y * rowLength,
457                         rowLength);
458             } else {
459                 for (int x = 0; x < xSize; x++) {
460                     image[y * rowLength + 3 * x] = scanline[4 * x];
461                     image[y * rowLength + 3 * x + 1] = scanline[4 * x + 1];
462                     image[y * rowLength + 3 * x + 2] = scanline[4 * x + 2];
463                 }
464             }
465         }
466         final DataBufferByte dataBuffer = new DataBufferByte(image, image.length);
467         final WritableRaster raster = Raster.createInterleavedRaster(
468                 dataBuffer, xSize, ySize, rowLength, 3,
469                 new int[] { 2, 1, 0 }, null);
470         final ColorModel colorModel = new ComponentColorModel(
471                 ColorSpace.getInstance(ColorSpace.CS_sRGB), false, false,
472                 Transparency.OPAQUE, DataBuffer.TYPE_BYTE);
473         return new BufferedImage(colorModel, raster,
474                 colorModel.isAlphaPremultiplied(), new Properties());
475     }
476 
477     @Override
478     public final BufferedImage getBufferedImage(final ByteSource byteSource, PcxImagingParameters params) throws ImageReadException, IOException {
479         if (params == null) {
480             params = new PcxImagingParameters();
481         }
482         try (InputStream is = byteSource.getInputStream()) {
483             final PcxHeader pcxHeader = readPcxHeader(is, params.isStrict());
484             return readImage(pcxHeader, is, byteSource);
485         }
486     }
487 
488     @Override
489     public void writeImage(final BufferedImage src, final OutputStream os, final PcxImagingParameters params)
490             throws ImageWriteException, IOException {
491         new PcxWriter(params).writeImage(src, os);
492     }
493 }