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     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 }