1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17 package org.apache.commons.imaging.formats.pcx;
18
19 import static org.apache.commons.imaging.common.BinaryFunctions.readBytes;
20 import static org.apache.commons.imaging.common.BinaryFunctions.skipBytes;
21 import static org.apache.commons.imaging.common.ByteConversions.toUInt16;
22
23 import java.awt.Dimension;
24 import java.awt.Transparency;
25 import java.awt.color.ColorSpace;
26 import java.awt.image.BufferedImage;
27 import java.awt.image.ColorModel;
28 import java.awt.image.ComponentColorModel;
29 import java.awt.image.DataBuffer;
30 import java.awt.image.DataBufferByte;
31 import java.awt.image.IndexColorModel;
32 import java.awt.image.Raster;
33 import java.awt.image.WritableRaster;
34 import java.io.IOException;
35 import java.io.InputStream;
36 import java.io.OutputStream;
37 import java.io.PrintWriter;
38 import java.nio.ByteOrder;
39 import java.util.ArrayList;
40 import java.util.Arrays;
41 import java.util.Properties;
42
43 import org.apache.commons.imaging.AbstractImageParser;
44 import org.apache.commons.imaging.ImageFormat;
45 import org.apache.commons.imaging.ImageFormats;
46 import org.apache.commons.imaging.ImageInfo;
47 import org.apache.commons.imaging.ImagingException;
48 import org.apache.commons.imaging.bytesource.ByteSource;
49 import org.apache.commons.imaging.common.Allocator;
50 import org.apache.commons.imaging.common.ImageMetadata;
51
52 public class PcxImageParser extends AbstractImageParser<PcxImagingParameters> {
53
54
55
56
57
58
59
60
61
62
63
64
65 static class PcxHeader {
66
67 public static final int ENCODING_UNCOMPRESSED = 0;
68 public static final int ENCODING_RLE = 1;
69 public static final int PALETTE_INFO_COLOR = 1;
70 public static final int PALETTE_INFO_GRAYSCALE = 2;
71 public final int manufacturer;
72 public final int version;
73
74
75
76
77 public final int encoding;
78
79 public final int bitsPerPixel;
80 public final int xMin;
81 public final int yMin;
82 public final int xMax;
83 public final int yMax;
84 public final int hDpi;
85 public final int vDpi;
86 public final int[] colormap;
87 public final int reserved;
88 public final int nPlanes;
89 public final int bytesPerLine;
90
91 public final int paletteInfo;
92
93 public final int hScreenSize;
94
95 public final int vScreenSize;
96
97
98 PcxHeader(final int manufacturer, final int version, final int encoding, final int bitsPerPixel, final int xMin, final int yMin, final int xMax,
99 final int yMax, final int hDpi, final int vDpi, final int[] colormap, final int reserved, final int nPlanes, final int bytesPerLine,
100 final int paletteInfo, final int hScreenSize, final int vScreenSize) {
101 this.manufacturer = manufacturer;
102 this.version = version;
103 this.encoding = encoding;
104 this.bitsPerPixel = bitsPerPixel;
105 this.xMin = xMin;
106 this.yMin = yMin;
107 this.xMax = xMax;
108 this.yMax = yMax;
109 this.hDpi = hDpi;
110 this.vDpi = vDpi;
111 this.colormap = colormap;
112 this.reserved = reserved;
113 this.nPlanes = nPlanes;
114 this.bytesPerLine = bytesPerLine;
115 this.paletteInfo = paletteInfo;
116 this.hScreenSize = hScreenSize;
117 this.vScreenSize = vScreenSize;
118 }
119
120 public void dump(final PrintWriter pw) {
121 pw.println("PcxHeader");
122 pw.println("Manufacturer: " + manufacturer);
123 pw.println("Version: " + version);
124 pw.println("Encoding: " + encoding);
125 pw.println("BitsPerPixel: " + bitsPerPixel);
126 pw.println("xMin: " + xMin);
127 pw.println("yMin: " + yMin);
128 pw.println("xMax: " + xMax);
129 pw.println("yMax: " + yMax);
130 pw.println("hDpi: " + hDpi);
131 pw.println("vDpi: " + vDpi);
132 pw.print("ColorMap: ");
133 for (int i = 0; i < colormap.length; i++) {
134 if (i > 0) {
135 pw.print(",");
136 }
137 pw.print("(" + (0xff & colormap[i] >> 16) + "," + (0xff & colormap[i] >> 8) + "," + (0xff & colormap[i]) + ")");
138 }
139 pw.println();
140 pw.println("Reserved: " + reserved);
141 pw.println("nPlanes: " + nPlanes);
142 pw.println("BytesPerLine: " + bytesPerLine);
143 pw.println("PaletteInfo: " + paletteInfo);
144 pw.println("hScreenSize: " + hScreenSize);
145 pw.println("vScreenSize: " + vScreenSize);
146 pw.println();
147 }
148 }
149
150 private static final String DEFAULT_EXTENSION = ImageFormats.PCX.getDefaultExtension();
151
152 private static final String[] ACCEPTED_EXTENSIONS = ImageFormats.PCX.getExtensions();
153
154
155
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
294
295
296
297
298
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
414
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 }