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