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 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
291
292
293
294
295
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
412
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 }