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.AbstractImageParser;
44  import org.apache.commons.imaging.ImageFormat;
45  import org.apache.commons.imaging.ImageFormats;
46  import org.apache.commons.imaging.ImageInfo;
47  import org.apache.commons.imaging.ImagingException;
48  import org.apache.commons.imaging.bytesource.ByteSource;
49  import org.apache.commons.imaging.common.Allocator;
50  import org.apache.commons.imaging.common.ImageMetadata;
51  
52  public class PcxImageParser extends AbstractImageParser<PcxImagingParameters> {
53      // ZSoft's official spec is at [BROKEN URL] http://www.qzx.com/pc-gpe/pcx.txt
54      // (among other places) but it's pretty thin. The fileformat.fine document
55      // at [BROEKN URL] 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      static class PcxHeader {
66  
67          public static final int ENCODING_UNCOMPRESSED = 0;
68          public static final int ENCODING_RLE = 1;
69          public static final int PALETTE_INFO_COLOR = 1;
70          public static final int PALETTE_INFO_GRAYSCALE = 2;
71          public final int manufacturer; // Always 10 = ZSoft .pcx
72          public final int version; // 0 = PC Paintbrush 2.5
73                                    // 2 = PC Paintbrush 2.8 with palette
74                                    // 3 = PC Paintbrush 2.8 w/o palette
75                                    // 4 = PC Paintbrush for Windows
76                                    // 5 = PC Paintbrush >= 3.0
77          public final int encoding; // 0 = very old uncompressed format, 1 = .pcx
78                                     // run length encoding
79          public final int bitsPerPixel; // Bits ***PER PLANE*** for each pixel
80          public final int xMin; // window
81          public final int yMin;
82          public final int xMax;
83          public final int yMax;
84          public final int hDpi; // horizontal dpi
85          public final int vDpi; // vertical dpi
86          public final int[] colormap; // palette for <= 16 colors
87          public final int reserved; // Always 0
88          public final int nPlanes; // Number of color planes
89          public final int bytesPerLine; // Number of bytes per scanline plane,
90                                         // must be an even number.
91          public final int paletteInfo; // 1 = Color/BW, 2 = Grayscale, ignored in
92                                        // Paintbrush IV/IV+
93          public final int hScreenSize; // horizontal screen size, in pixels.
94                                        // PaintBrush >= IV only.
95          public final int vScreenSize; // vertical screen size, in pixels.
96                                        // PaintBrush >= IV only.
97  
98          PcxHeader(final int manufacturer, final int version, final int encoding, final int bitsPerPixel, final int xMin, final int yMin, final int xMax,
99                  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 }