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.ImageFormat;
44 import org.apache.commons.imaging.ImageFormats;
45 import org.apache.commons.imaging.ImageInfo;
46 import org.apache.commons.imaging.ImageParser;
47 import org.apache.commons.imaging.ImageReadException;
48 import org.apache.commons.imaging.ImageWriteException;
49 import org.apache.commons.imaging.common.ImageMetadata;
50 import org.apache.commons.imaging.common.bytesource.ByteSource;
51
52 public class PcxImageParser extends ImageParser<PcxImagingParameters> {
53
54
55
56
57
58
59
60
61
62
63
64
65 private static final String DEFAULT_EXTENSION = ImageFormats.PCX.getDefaultExtension();
66 private static final String[] ACCEPTED_EXTENSIONS = ImageFormats.PCX.getExtensions();
67
68 public PcxImageParser() {
69 super.setByteOrder(ByteOrder.LITTLE_ENDIAN);
70 }
71
72 @Override
73 public PcxImagingParameters getDefaultParameters() {
74 return new PcxImagingParameters();
75 }
76
77 @Override
78 public String getName() {
79 return "Pcx-Custom";
80 }
81
82 @Override
83 public String getDefaultExtension() {
84 return DEFAULT_EXTENSION;
85 }
86
87 @Override
88 protected String[] getAcceptedExtensions() {
89 return ACCEPTED_EXTENSIONS;
90 }
91
92 @Override
93 protected ImageFormat[] getAcceptedTypes() {
94 return new ImageFormat[] { ImageFormats.PCX,
95 };
96 }
97
98 @Override
99 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;
159 public final int version;
160
161
162
163
164 public final int encoding;
165
166 public final int bitsPerPixel;
167 public final int xMin;
168 public final int yMin;
169 public final int xMax;
170 public final int yMax;
171 public final int hDpi;
172 public final int vDpi;
173 public final int[] colormap;
174 public final int reserved;
175 public final int nPlanes;
176 public final int bytesPerLine;
177
178 public final int paletteInfo;
179
180 public final int hScreenSize;
181
182 public final int vScreenSize;
183
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
284
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
366
367
368
369
370
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 }