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