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.ImagingConstants.PARAM_KEY_STRICT;
020import static org.apache.commons.imaging.common.BinaryFunctions.readBytes;
021import static org.apache.commons.imaging.common.BinaryFunctions.skipBytes;
022import static org.apache.commons.imaging.common.ByteConversions.toUInt16;
023
024import java.awt.Dimension;
025import java.awt.Transparency;
026import java.awt.color.ColorSpace;
027import java.awt.image.BufferedImage;
028import java.awt.image.ColorModel;
029import java.awt.image.ComponentColorModel;
030import java.awt.image.DataBuffer;
031import java.awt.image.DataBufferByte;
032import java.awt.image.IndexColorModel;
033import java.awt.image.Raster;
034import java.awt.image.WritableRaster;
035import java.io.IOException;
036import java.io.InputStream;
037import java.io.OutputStream;
038import java.io.PrintWriter;
039import java.nio.ByteOrder;
040import java.util.ArrayList;
041import java.util.Arrays;
042import java.util.HashMap;
043import java.util.Map;
044import java.util.Properties;
045
046import org.apache.commons.imaging.ImageFormat;
047import org.apache.commons.imaging.ImageFormats;
048import org.apache.commons.imaging.ImageInfo;
049import org.apache.commons.imaging.ImageParser;
050import org.apache.commons.imaging.ImageReadException;
051import org.apache.commons.imaging.ImageWriteException;
052import org.apache.commons.imaging.common.ImageMetadata;
053import org.apache.commons.imaging.common.bytesource.ByteSource;
054
055public class PcxImageParser extends ImageParser {
056    // ZSoft's official spec is at http://www.qzx.com/pc-gpe/pcx.txt
057    // (among other places) but it's pretty thin. The fileformat.fine document
058    // at http://www.fileformat.fine/format/pcx/egff.htm is a little better
059    // but their gray sample image seems corrupt. PCX files themselves are
060    // the ultimate test but pretty hard to find nowadays, so the best
061    // test is against other image viewers (Irfanview is pretty good).
062    //
063    // Open source projects are generally poor at parsing PCX,
064    // SDL_Image/gdk-pixbuf/Eye of Gnome/GIMP/F-Spot all only do some formats,
065    // don't support uncompressed PCX, and/or don't handle black and white
066    // images properly.
067
068    private static final String DEFAULT_EXTENSION = ".pcx";
069    private static final String[] ACCEPTED_EXTENSIONS = { ".pcx", ".pcc", };
070
071    public PcxImageParser() {
072        super.setByteOrder(ByteOrder.LITTLE_ENDIAN);
073    }
074
075    @Override
076    public String getName() {
077        return "Pcx-Custom";
078    }
079
080    @Override
081    public String getDefaultExtension() {
082        return DEFAULT_EXTENSION;
083    }
084
085    @Override
086    protected String[] getAcceptedExtensions() {
087        return ACCEPTED_EXTENSIONS;
088    }
089
090    @Override
091    protected ImageFormat[] getAcceptedTypes() {
092        return new ImageFormat[] { ImageFormats.PCX, //
093        };
094    }
095
096    @Override
097    public ImageMetadata getMetadata(final ByteSource byteSource, final Map<String, Object> params)
098            throws ImageReadException, IOException {
099        return null;
100    }
101
102    @Override
103    public ImageInfo getImageInfo(final ByteSource byteSource, final Map<String, Object> params)
104            throws ImageReadException, IOException {
105        final PcxHeader pcxHeader = readPcxHeader(byteSource);
106        final Dimension size = getImageSize(byteSource, params);
107        return new ImageInfo(
108                "PCX",
109                pcxHeader.nPlanes * pcxHeader.bitsPerPixel,
110                new ArrayList<String>(),
111                ImageFormats.PCX,
112                "ZSoft PCX Image",
113                size.height,
114                "image/x-pcx",
115                1,
116                pcxHeader.vDpi,
117                Math.round(size.getHeight() / pcxHeader.vDpi),
118                pcxHeader.hDpi,
119                Math.round(size.getWidth() / pcxHeader.hDpi),
120                size.width,
121                false,
122                false,
123                !(pcxHeader.nPlanes == 3 && pcxHeader.bitsPerPixel == 8),
124                ImageInfo.ColorType.RGB,
125                pcxHeader.encoding == PcxHeader.ENCODING_RLE ? ImageInfo.CompressionAlgorithm.RLE
126                        : ImageInfo.CompressionAlgorithm.NONE);
127    }
128
129    @Override
130    public Dimension getImageSize(final ByteSource byteSource, final Map<String, Object> params)
131            throws ImageReadException, IOException {
132        final PcxHeader pcxHeader = readPcxHeader(byteSource);
133        final int xSize = pcxHeader.xMax - pcxHeader.xMin + 1;
134        if (xSize < 0) {
135            throw new ImageReadException("Image width is negative");
136        }
137        final int ySize = pcxHeader.yMax - pcxHeader.yMin + 1;
138        if (ySize < 0) {
139            throw new ImageReadException("Image height is negative");
140        }
141        return new Dimension(xSize, ySize);
142    }
143
144    @Override
145    public byte[] getICCProfileBytes(final ByteSource byteSource, final Map<String, Object> params)
146            throws ImageReadException, IOException {
147        return null;
148    }
149
150    static class PcxHeader {
151
152        public static final int ENCODING_UNCOMPRESSED = 0;
153        public static final int ENCODING_RLE = 1;
154        public static final int PALETTE_INFO_COLOR = 1;
155        public static final int PALETTE_INFO_GRAYSCALE = 2;
156        public final int manufacturer; // Always 10 = ZSoft .pcx
157        public final int version; // 0 = PC Paintbrush 2.5
158                                  // 2 = PC Paintbrush 2.8 with palette
159                                  // 3 = PC Paintbrush 2.8 w/o palette
160                                  // 4 = PC Paintbrush for Windows
161                                  // 5 = PC Paintbrush >= 3.0
162        public final int encoding; // 0 = very old uncompressed format, 1 = .pcx
163                                   // run length encoding
164        public final int bitsPerPixel; // Bits ***PER PLANE*** for each pixel
165        public final int xMin; // window
166        public final int yMin;
167        public final int xMax;
168        public final int yMax;
169        public final int hDpi; // horizontal dpi
170        public final int vDpi; // vertical dpi
171        public final int[] colormap; // palette for <= 16 colors
172        public final int reserved; // Always 0
173        public final int nPlanes; // Number of color planes
174        public final int bytesPerLine; // Number of bytes per scanline plane,
175                                       // must be an even number.
176        public final int paletteInfo; // 1 = Color/BW, 2 = Grayscale, ignored in
177                                      // Paintbrush IV/IV+
178        public final int hScreenSize; // horizontal screen size, in pixels.
179                                      // PaintBrush >= IV only.
180        public final int vScreenSize; // vertical screen size, in pixels.
181                                      // PaintBrush >= IV only.
182
183        PcxHeader(final int manufacturer, final int version,
184                final int encoding, final int bitsPerPixel, final int xMin,
185                final int yMin, final int xMax, final int yMax, final int hDpi,
186                final int vDpi, final int[] colormap, final int reserved,
187                final int nPlanes, final int bytesPerLine,
188                final int paletteInfo, final int hScreenSize,
189                final int vScreenSize) {
190            this.manufacturer = manufacturer;
191            this.version = version;
192            this.encoding = encoding;
193            this.bitsPerPixel = bitsPerPixel;
194            this.xMin = xMin;
195            this.yMin = yMin;
196            this.xMax = xMax;
197            this.yMax = yMax;
198            this.hDpi = hDpi;
199            this.vDpi = vDpi;
200            this.colormap = colormap;
201            this.reserved = reserved;
202            this.nPlanes = nPlanes;
203            this.bytesPerLine = bytesPerLine;
204            this.paletteInfo = paletteInfo;
205            this.hScreenSize = hScreenSize;
206            this.vScreenSize = vScreenSize;
207        }
208
209        public void dump(final PrintWriter pw) {
210            pw.println("PcxHeader");
211            pw.println("Manufacturer: " + manufacturer);
212            pw.println("Version: " + version);
213            pw.println("Encoding: " + encoding);
214            pw.println("BitsPerPixel: " + bitsPerPixel);
215            pw.println("xMin: " + xMin);
216            pw.println("yMin: " + yMin);
217            pw.println("xMax: " + xMax);
218            pw.println("yMax: " + yMax);
219            pw.println("hDpi: " + hDpi);
220            pw.println("vDpi: " + vDpi);
221            pw.print("ColorMap: ");
222            for (int i = 0; i < colormap.length; i++) {
223                if (i > 0) {
224                    pw.print(",");
225                }
226                pw.print("(" + (0xff & (colormap[i] >> 16)) + ","
227                        + (0xff & (colormap[i] >> 8)) + ","
228                        + (0xff & colormap[i]) + ")");
229            }
230            pw.println();
231            pw.println("Reserved: " + reserved);
232            pw.println("nPlanes: " + nPlanes);
233            pw.println("BytesPerLine: " + bytesPerLine);
234            pw.println("PaletteInfo: " + paletteInfo);
235            pw.println("hScreenSize: " + hScreenSize);
236            pw.println("vScreenSize: " + vScreenSize);
237            pw.println();
238        }
239    }
240
241    private PcxHeader readPcxHeader(final ByteSource byteSource)
242            throws ImageReadException, IOException {
243        try (InputStream is = byteSource.getInputStream()) {
244            return readPcxHeader(is, false);
245        }
246    }
247
248    private PcxHeader readPcxHeader(final InputStream is, final boolean isStrict)
249            throws ImageReadException, IOException {
250        final byte[] pcxHeaderBytes = readBytes("PcxHeader", is, 128,
251                "Not a Valid PCX File");
252        final int manufacturer = 0xff & pcxHeaderBytes[0];
253        final int version = 0xff & pcxHeaderBytes[1];
254        final int encoding = 0xff & pcxHeaderBytes[2];
255        final int bitsPerPixel = 0xff & pcxHeaderBytes[3];
256        final int xMin = toUInt16(pcxHeaderBytes, 4, getByteOrder());
257        final int yMin = toUInt16(pcxHeaderBytes, 6, getByteOrder());
258        final int xMax = toUInt16(pcxHeaderBytes, 8, getByteOrder());
259        final int yMax = toUInt16(pcxHeaderBytes, 10, getByteOrder());
260        final int hDpi = toUInt16(pcxHeaderBytes, 12, getByteOrder());
261        final int vDpi = toUInt16(pcxHeaderBytes, 14, getByteOrder());
262        final int[] colormap = new int[16];
263        for (int i = 0; i < 16; i++) {
264            colormap[i] = 0xff000000
265                    | ((0xff & pcxHeaderBytes[16 + 3 * i]) << 16)
266                    | ((0xff & pcxHeaderBytes[16 + 3 * i + 1]) << 8)
267                    | (0xff & pcxHeaderBytes[16 + 3 * i + 2]);
268        }
269        final int reserved = 0xff & pcxHeaderBytes[64];
270        final int nPlanes = 0xff & pcxHeaderBytes[65];
271        final int bytesPerLine = toUInt16(pcxHeaderBytes, 66, getByteOrder());
272        final int paletteInfo = toUInt16(pcxHeaderBytes, 68, getByteOrder());
273        final int hScreenSize = toUInt16(pcxHeaderBytes, 70, getByteOrder());
274        final int vScreenSize = toUInt16(pcxHeaderBytes, 72, getByteOrder());
275
276        if (manufacturer != 10) {
277            throw new ImageReadException(
278                    "Not a Valid PCX File: manufacturer is " + manufacturer);
279        }
280        if (isStrict) {
281            // Note that reserved is sometimes set to a non-zero value
282            // by Paintbrush itself, so it shouldn't be enforced.
283            if (bytesPerLine % 2 != 0) {
284                throw new ImageReadException(
285                        "Not a Valid PCX File: bytesPerLine is odd");
286            }
287        }
288
289        return new PcxHeader(manufacturer, version, encoding, bitsPerPixel,
290                xMin, yMin, xMax, yMax, hDpi, vDpi, colormap, reserved,
291                nPlanes, bytesPerLine, paletteInfo, hScreenSize, vScreenSize);
292    }
293
294    @Override
295    public boolean dumpImageFile(final PrintWriter pw, final ByteSource byteSource)
296            throws ImageReadException, IOException {
297        readPcxHeader(byteSource).dump(pw);
298        return true;
299    }
300
301    private int[] read256ColorPalette(final InputStream stream) throws IOException {
302        final byte[] paletteBytes = readBytes("Palette", stream, 769,
303                "Error reading palette");
304        if (paletteBytes[0] != 12) {
305            return null;
306        }
307        final int[] palette = new int[256];
308        for (int i = 0; i < palette.length; i++) {
309            palette[i] = ((0xff & paletteBytes[1 + 3 * i]) << 16)
310                    | ((0xff & paletteBytes[1 + 3 * i + 1]) << 8)
311                    | (0xff & paletteBytes[1 + 3 * i + 2]);
312        }
313        return palette;
314    }
315
316    private int[] read256ColorPaletteFromEndOfFile(final ByteSource byteSource)
317            throws IOException {
318        try (InputStream stream = byteSource.getInputStream()) {
319            final long toSkip = byteSource.getLength() - 769;
320            skipBytes(stream, (int) toSkip);
321            final int[] ret = read256ColorPalette(stream);
322            return ret;
323        }
324    }
325
326    private BufferedImage readImage(final PcxHeader pcxHeader, final InputStream is,
327            final ByteSource byteSource) throws ImageReadException, IOException {
328        final int xSize = pcxHeader.xMax - pcxHeader.xMin + 1;
329        if (xSize < 0) {
330            throw new ImageReadException("Image width is negative");
331        }
332        final int ySize = pcxHeader.yMax - pcxHeader.yMin + 1;
333        if (ySize < 0) {
334            throw new ImageReadException("Image height is negative");
335        }
336        if (pcxHeader.nPlanes <= 0 || 4 < pcxHeader.nPlanes) {
337            throw new ImageReadException("Unsupported/invalid image with " + pcxHeader.nPlanes + " planes");
338        }
339        final RleReader rleReader;
340        if (pcxHeader.encoding == PcxHeader.ENCODING_UNCOMPRESSED) {
341            rleReader = new RleReader(false);
342        } else if (pcxHeader.encoding == PcxHeader.ENCODING_RLE) {
343            rleReader = new RleReader(true);
344        } else {
345            throw new ImageReadException("Unsupported/invalid image encoding " + pcxHeader.encoding);
346        }
347        final int scanlineLength = pcxHeader.bytesPerLine * pcxHeader.nPlanes;
348        final byte[] scanline = new byte[scanlineLength];
349        if ((pcxHeader.bitsPerPixel == 1 || pcxHeader.bitsPerPixel == 2
350                || pcxHeader.bitsPerPixel == 4 || pcxHeader.bitsPerPixel == 8)
351                && pcxHeader.nPlanes == 1) {
352            final int bytesPerImageRow = (xSize * pcxHeader.bitsPerPixel + 7) / 8;
353            final byte[] image = new byte[ySize * bytesPerImageRow];
354            for (int y = 0; y < ySize; y++) {
355                rleReader.read(is, scanline);
356                System.arraycopy(scanline, 0, image, y * bytesPerImageRow,
357                        bytesPerImageRow);
358            }
359            final DataBufferByte dataBuffer = new DataBufferByte(image, image.length);
360            int[] palette;
361            if (pcxHeader.bitsPerPixel == 1) {
362                palette = new int[] { 0x000000, 0xffffff };
363            } else if (pcxHeader.bitsPerPixel == 8) {
364                // Normally the palette is read 769 bytes from the end of the
365                // file.
366                // However DCX files have multiple PCX images in one file, so
367                // there could be extra data before the end! So try look for the
368                // palette
369                // immediately after the image data first.
370                palette = read256ColorPalette(is);
371                if (palette == null) {
372                    palette = read256ColorPaletteFromEndOfFile(byteSource);
373                }
374                if (palette == null) {
375                    throw new ImageReadException(
376                            "No 256 color palette found in image that needs it");
377                }
378            } else {
379                palette = pcxHeader.colormap;
380            }
381            WritableRaster raster;
382            if (pcxHeader.bitsPerPixel == 8) {
383                raster = Raster.createInterleavedRaster(dataBuffer,
384                        xSize, ySize, bytesPerImageRow, 1, new int[] { 0 },
385                        null);
386            } else {
387                raster = Raster.createPackedRaster(dataBuffer, xSize,
388                        ySize, pcxHeader.bitsPerPixel, null);
389            }
390            final IndexColorModel colorModel = new IndexColorModel(
391                    pcxHeader.bitsPerPixel, 1 << pcxHeader.bitsPerPixel,
392                    palette, 0, false, -1, DataBuffer.TYPE_BYTE);
393            return new BufferedImage(colorModel, raster,
394                    colorModel.isAlphaPremultiplied(), new Properties());
395        } else if (pcxHeader.bitsPerPixel == 1 && 2 <= pcxHeader.nPlanes
396                && pcxHeader.nPlanes <= 4) {
397            final IndexColorModel colorModel = new IndexColorModel(pcxHeader.nPlanes,
398                    1 << pcxHeader.nPlanes, pcxHeader.colormap, 0, false, -1,
399                    DataBuffer.TYPE_BYTE);
400            final BufferedImage image = new BufferedImage(xSize, ySize,
401                    BufferedImage.TYPE_BYTE_BINARY, colorModel);
402            final byte[] unpacked = new byte[xSize];
403            for (int y = 0; y < ySize; y++) {
404                rleReader.read(is, scanline);
405                int nextByte = 0;
406                Arrays.fill(unpacked, (byte) 0);
407                for (int plane = 0; plane < pcxHeader.nPlanes; plane++) {
408                    for (int i = 0; i < pcxHeader.bytesPerLine; i++) {
409                        final int b = 0xff & scanline[nextByte++];
410                        for (int j = 0; j < 8 && 8 * i + j < unpacked.length; j++) {
411                            unpacked[8 * i + j] |= (byte) (((b >> (7 - j)) & 0x1) << plane);
412                        }
413                    }
414                }
415                image.getRaster().setDataElements(0, y, xSize, 1, unpacked);
416            }
417            return image;
418        } else if (pcxHeader.bitsPerPixel == 8 && pcxHeader.nPlanes == 3) {
419            final byte[][] image = new byte[3][];
420            image[0] = new byte[xSize * ySize];
421            image[1] = new byte[xSize * ySize];
422            image[2] = new byte[xSize * ySize];
423            for (int y = 0; y < ySize; y++) {
424                rleReader.read(is, scanline);
425                System.arraycopy(scanline, 0, image[0], y * xSize, xSize);
426                System.arraycopy(scanline, pcxHeader.bytesPerLine, image[1], y
427                        * xSize, xSize);
428                System.arraycopy(scanline, 2 * pcxHeader.bytesPerLine,
429                        image[2], y * xSize, xSize);
430            }
431            final DataBufferByte dataBuffer = new DataBufferByte(image,
432                    image[0].length);
433            final WritableRaster raster = Raster.createBandedRaster(
434                    dataBuffer, xSize, ySize, xSize, new int[] { 0, 1, 2 },
435                    new int[] { 0, 0, 0 }, null);
436            final ColorModel colorModel = new ComponentColorModel(
437                    ColorSpace.getInstance(ColorSpace.CS_sRGB), false, false,
438                    Transparency.OPAQUE, DataBuffer.TYPE_BYTE);
439            return new BufferedImage(colorModel, raster,
440                    colorModel.isAlphaPremultiplied(), new Properties());
441        } else if ((pcxHeader.bitsPerPixel == 24 && pcxHeader.nPlanes == 1)
442                || (pcxHeader.bitsPerPixel == 32 && pcxHeader.nPlanes == 1)) {
443            final int rowLength = 3 * xSize;
444            final byte[] image = new byte[rowLength * ySize];
445            for (int y = 0; y < ySize; y++) {
446                rleReader.read(is, scanline);
447                if (pcxHeader.bitsPerPixel == 24) {
448                    System.arraycopy(scanline, 0, image, y * rowLength,
449                            rowLength);
450                } else {
451                    for (int x = 0; x < xSize; x++) {
452                        image[y * rowLength + 3 * x] = scanline[4 * x];
453                        image[y * rowLength + 3 * x + 1] = scanline[4 * x + 1];
454                        image[y * rowLength + 3 * x + 2] = scanline[4 * x + 2];
455                    }
456                }
457            }
458            final DataBufferByte dataBuffer = new DataBufferByte(image, image.length);
459            final WritableRaster raster = Raster.createInterleavedRaster(
460                    dataBuffer, xSize, ySize, rowLength, 3,
461                    new int[] { 2, 1, 0 }, null);
462            final ColorModel colorModel = new ComponentColorModel(
463                    ColorSpace.getInstance(ColorSpace.CS_sRGB), false, false,
464                    Transparency.OPAQUE, DataBuffer.TYPE_BYTE);
465            return new BufferedImage(colorModel, raster,
466                    colorModel.isAlphaPremultiplied(), new Properties());
467        } else {
468            throw new ImageReadException(
469                    "Invalid/unsupported image with bitsPerPixel "
470                            + pcxHeader.bitsPerPixel + " and planes "
471                            + pcxHeader.nPlanes);
472        }
473    }
474
475    @Override
476    public final BufferedImage getBufferedImage(final ByteSource byteSource,
477            Map<String, Object> params) throws ImageReadException, IOException {
478        params = (params == null) ? new HashMap<>() : new HashMap<>(params);
479        boolean isStrict = false;
480        final Object strictness = params.get(PARAM_KEY_STRICT);
481        if (strictness != null) {
482            isStrict = ((Boolean) strictness).booleanValue();
483        }
484
485        try (InputStream is = byteSource.getInputStream()) {
486            final PcxHeader pcxHeader = readPcxHeader(is, isStrict);
487            final BufferedImage ret = readImage(pcxHeader, is, byteSource);
488            return ret;
489        }
490    }
491
492    @Override
493    public void writeImage(final BufferedImage src, final OutputStream os, final Map<String, Object> params)
494            throws ImageWriteException, IOException {
495        new PcxWriter(params).writeImage(src, os);
496    }
497}