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