1
2
3
4
5
6
7
8
9
10
11
12
13
14 package org.apache.commons.imaging.formats.xpm;
15
16 import java.awt.Dimension;
17 import java.awt.image.BufferedImage;
18 import java.awt.image.ColorModel;
19 import java.awt.image.DataBuffer;
20 import java.awt.image.DirectColorModel;
21 import java.awt.image.IndexColorModel;
22 import java.awt.image.Raster;
23 import java.awt.image.WritableRaster;
24 import java.io.BufferedReader;
25 import java.io.ByteArrayInputStream;
26 import java.io.ByteArrayOutputStream;
27 import java.io.IOException;
28 import java.io.InputStream;
29 import java.io.InputStreamReader;
30 import java.io.OutputStream;
31 import java.io.PrintWriter;
32 import java.nio.charset.StandardCharsets;
33 import java.util.ArrayList;
34 import java.util.Arrays;
35 import java.util.HashMap;
36 import java.util.Locale;
37 import java.util.Map;
38 import java.util.Map.Entry;
39 import java.util.Properties;
40 import java.util.UUID;
41
42 import org.apache.commons.imaging.AbstractImageParser;
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.ImagingException;
47 import org.apache.commons.imaging.bytesource.ByteSource;
48 import org.apache.commons.imaging.common.Allocator;
49 import org.apache.commons.imaging.common.BasicCParser;
50 import org.apache.commons.imaging.common.ImageMetadata;
51 import org.apache.commons.imaging.palette.PaletteFactory;
52 import org.apache.commons.imaging.palette.SimplePalette;
53
54 public class XpmImageParser extends AbstractImageParser<XpmImagingParameters> {
55
56 private static final class PaletteEntry {
57 int colorArgb;
58 int gray4LevelArgb;
59 int grayArgb;
60 boolean haveColor;
61 boolean haveGray;
62 boolean haveGray4Level;
63 boolean haveMono;
64 int index;
65 int monoArgb;
66
67 int getBestArgb() {
68 if (haveColor) {
69 return colorArgb;
70 }
71 if (haveGray) {
72 return grayArgb;
73 }
74 if (haveGray4Level) {
75 return gray4LevelArgb;
76 }
77 if (haveMono) {
78 return monoArgb;
79 }
80 return 0x00000000;
81 }
82 }
83
84 private static final class XpmHeader {
85 final int height;
86 final int numCharsPerPixel;
87 final int numColors;
88 final Map<Object, PaletteEntry> palette = new HashMap<>();
89 final int width;
90 int xHotSpot = -1;
91 final boolean xpmExt;
92
93 int yHotSpot = -1;
94
95 XpmHeader(final int width, final int height, final int numColors, final int numCharsPerPixel, final int xHotSpot, final int yHotSpot,
96 final boolean xpmExt) {
97 this.width = width;
98 this.height = height;
99 this.numColors = numColors;
100 this.numCharsPerPixel = numCharsPerPixel;
101 this.xHotSpot = xHotSpot;
102 this.yHotSpot = yHotSpot;
103 this.xpmExt = xpmExt;
104 }
105
106 public void dump(final PrintWriter pw) {
107 pw.println("XpmHeader");
108 pw.println("Width: " + width);
109 pw.println("Height: " + height);
110 pw.println("NumColors: " + numColors);
111 pw.println("NumCharsPerPixel: " + numCharsPerPixel);
112 if (xHotSpot != -1 && yHotSpot != -1) {
113 pw.println("X hotspot: " + xHotSpot);
114 pw.println("Y hotspot: " + yHotSpot);
115 }
116 pw.println("XpmExt: " + xpmExt);
117 }
118 }
119
120 private static final class XpmParseResult {
121 BasicCParser cParser;
122 XpmHeader xpmHeader;
123 }
124
125 private static final String[] ACCEPTED_EXTENSIONS = ImageFormats.XPM.getExtensions();
126 private static Map<String, Integer> colorNames;
127
128 private static final String DEFAULT_EXTENSION = ImageFormats.XPM.getDefaultExtension();
129
130 private static final char[] WRITE_PALETTE = { ' ', '.', 'X', 'o', 'O', '+', '@', '#', '$', '%', '&', '*', '=', '-', ';', ':', '>', ',', '<', '1', '2', '3',
131 '4', '5', '6', '7', '8', '9', '0', 'q', 'w', 'e', 'r', 't', 'y', 'u', 'i', 'p', 'a', 's', 'd', 'f', 'g', 'h', 'j', 'k', 'l', 'z', 'x', 'c', 'v',
132 'b', 'n', 'm', 'M', 'N', 'B', 'V', 'C', 'Z', 'A', 'S', 'D', 'F', 'G', 'H', 'J', 'K', 'L', 'P', 'I', 'U', 'Y', 'T', 'R', 'E', 'W', 'Q', '!', '~',
133 '^', '/', '(', ')', '_', '`', '\'', ']', '[', '{', '}', '|', };
134
135 private static void loadColorNames() throws ImagingException {
136 synchronized (XpmImageParser.class) {
137 if (colorNames != null) {
138 return;
139 }
140
141 try {
142 final InputStream rgbTxtStream = XpmImageParser.class.getResourceAsStream("rgb.txt");
143 if (rgbTxtStream == null) {
144 throw new ImagingException("Couldn't find rgb.txt in our resources");
145 }
146 final Map<String, Integer> colors = new HashMap<>();
147 try (InputStreamReader isReader = new InputStreamReader(rgbTxtStream, StandardCharsets.US_ASCII);
148 BufferedReader reader = new BufferedReader(isReader)) {
149 String line;
150 while ((line = reader.readLine()) != null) {
151 if (line.charAt(0) == '!') {
152 continue;
153 }
154 try {
155 final int red = Integer.parseInt(line.substring(0, 3).trim());
156 final int green = Integer.parseInt(line.substring(4, 7).trim());
157 final int blue = Integer.parseInt(line.substring(8, 11).trim());
158 final String colorName = line.substring(11).trim();
159 colors.put(colorName.toLowerCase(Locale.ENGLISH), 0xff000000 | red << 16 | green << 8 | blue);
160 } catch (final NumberFormatException nfe) {
161 throw new ImagingException("Couldn't parse color in rgb.txt", nfe);
162 }
163 }
164 }
165 colorNames = colors;
166 } catch (final IOException ioException) {
167 throw new ImagingException("Could not parse rgb.txt", ioException);
168 }
169 }
170 }
171
172 @Override
173 public boolean dumpImageFile(final PrintWriter pw, final ByteSource byteSource) throws ImagingException, IOException {
174 readXpmHeader(byteSource).dump(pw);
175 return true;
176 }
177
178 @Override
179 protected String[] getAcceptedExtensions() {
180 return ACCEPTED_EXTENSIONS;
181 }
182
183 @Override
184 protected ImageFormat[] getAcceptedTypes() {
185 return new ImageFormat[] { ImageFormats.XPM,
186 };
187 }
188
189 @Override
190 public final BufferedImage getBufferedImage(final ByteSource byteSource, final XpmImagingParameters params) throws ImagingException, IOException {
191 final XpmParseResult result = parseXpmHeader(byteSource);
192 return readXpmImage(result.xpmHeader, result.cParser);
193 }
194
195 @Override
196 public String getDefaultExtension() {
197 return DEFAULT_EXTENSION;
198 }
199
200 @Override
201 public XpmImagingParameters getDefaultParameters() {
202 return new XpmImagingParameters();
203 }
204
205 @Override
206 public byte[] getIccProfileBytes(final ByteSource byteSource, final XpmImagingParameters params) throws ImagingException, IOException {
207 return null;
208 }
209
210 @Override
211 public ImageInfo getImageInfo(final ByteSource byteSource, final XpmImagingParameters params) throws ImagingException, IOException {
212 final XpmHeader xpmHeader = readXpmHeader(byteSource);
213 boolean transparent = false;
214 ImageInfo.ColorType colorType = ImageInfo.ColorType.BW;
215 for (final Entry<Object, PaletteEntry> entry : xpmHeader.palette.entrySet()) {
216 final PaletteEntry paletteEntry = entry.getValue();
217 if ((paletteEntry.getBestArgb() & 0xff000000) != 0xff000000) {
218 transparent = true;
219 }
220 if (paletteEntry.haveColor) {
221 colorType = ImageInfo.ColorType.RGB;
222 } else if (colorType != ImageInfo.ColorType.RGB && (paletteEntry.haveGray || paletteEntry.haveGray4Level)) {
223 colorType = ImageInfo.ColorType.GRAYSCALE;
224 }
225 }
226 return new ImageInfo("XPM version 3", xpmHeader.numCharsPerPixel * 8, new ArrayList<>(), ImageFormats.XPM, "X PixMap", xpmHeader.height,
227 "image/x-xpixmap", 1, 0, 0, 0, 0, xpmHeader.width, false, transparent, true, colorType, ImageInfo.CompressionAlgorithm.NONE);
228 }
229
230 @Override
231 public Dimension getImageSize(final ByteSource byteSource, final XpmImagingParameters params) throws ImagingException, IOException {
232 final XpmHeader xpmHeader = readXpmHeader(byteSource);
233 return new Dimension(xpmHeader.width, xpmHeader.height);
234 }
235
236 @Override
237 public ImageMetadata getMetadata(final ByteSource byteSource, final XpmImagingParameters params) throws ImagingException, IOException {
238 return null;
239 }
240
241 @Override
242 public String getName() {
243 return "X PixMap";
244 }
245
246 private int parseColor(String color) throws ImagingException {
247 if (color.charAt(0) == '#') {
248 color = color.substring(1);
249 if (color.length() == 3) {
250 final int red = Integer.parseInt(color.substring(0, 1), 16);
251 final int green = Integer.parseInt(color.substring(1, 2), 16);
252 final int blue = Integer.parseInt(color.substring(2, 3), 16);
253 return 0xff000000 | red << 20 | green << 12 | blue << 4;
254 }
255 if (color.length() == 6) {
256 return 0xff000000 | Integer.parseInt(color, 16);
257 }
258 if (color.length() == 9) {
259 final int red = Integer.parseInt(color.substring(0, 1), 16);
260 final int green = Integer.parseInt(color.substring(3, 4), 16);
261 final int blue = Integer.parseInt(color.substring(6, 7), 16);
262 return 0xff000000 | red << 16 | green << 8 | blue;
263 }
264 if (color.length() == 12) {
265 final int red = Integer.parseInt(color.substring(0, 1), 16);
266 final int green = Integer.parseInt(color.substring(4, 5), 16);
267 final int blue = Integer.parseInt(color.substring(8, 9), 16);
268 return 0xff000000 | red << 16 | green << 8 | blue;
269 }
270 if (color.length() == 24) {
271 final int red = Integer.parseInt(color.substring(0, 1), 16);
272 final int green = Integer.parseInt(color.substring(8, 9), 16);
273 final int blue = Integer.parseInt(color.substring(16, 17), 16);
274 return 0xff000000 | red << 16 | green << 8 | blue;
275 }
276 return 0x00000000;
277 }
278 if (color.charAt(0) == '%') {
279 throw new ImagingException("HSV colors are not implemented " + "even in the XPM specification!");
280 }
281 if ("None".equals(color)) {
282 return 0x00000000;
283 }
284 loadColorNames();
285 final String colorLowercase = color.toLowerCase(Locale.ENGLISH);
286 return colorNames.getOrDefault(colorLowercase, 0x00000000);
287 }
288
289 private boolean parseNextString(final BasicCParser cParser, final StringBuilder stringBuilder) throws IOException, ImagingException {
290 stringBuilder.setLength(0);
291 String token = cParser.nextToken();
292 if (token.charAt(0) != '"') {
293 throw new ImagingException("Parsing XPM file failed, " + "no string found where expected");
294 }
295 BasicCParser.unescapeString(stringBuilder, token);
296 for (token = cParser.nextToken(); token.charAt(0) == '"'; token = cParser.nextToken()) {
297 BasicCParser.unescapeString(stringBuilder, token);
298 }
299 if (",".equals(token)) {
300 return true;
301 }
302 if ("}".equals(token)) {
303 return false;
304 }
305 throw new ImagingException("Parsing XPM file failed, " + "no ',' or '}' found where expected");
306 }
307
308 private void parsePaletteEntries(final XpmHeader xpmHeader, final BasicCParser cParser) throws IOException, ImagingException {
309 final StringBuilder row = new StringBuilder();
310 for (int i = 0; i < xpmHeader.numColors; i++) {
311 row.setLength(0);
312 final boolean hasMore = parseNextString(cParser, row);
313 if (!hasMore) {
314 throw new ImagingException("Parsing XPM file failed, " + "file ended while reading palette");
315 }
316 final String name = row.substring(0, xpmHeader.numCharsPerPixel);
317 final String[] tokens = BasicCParser.tokenizeRow(row.substring(xpmHeader.numCharsPerPixel));
318 final PaletteEntry paletteEntry = new PaletteEntry();
319 paletteEntry.index = i;
320 int previousKeyIndex = Integer.MIN_VALUE;
321 final StringBuilder colorBuffer = new StringBuilder();
322 for (int j = 0; j < tokens.length; j++) {
323 final String token = tokens[j];
324 boolean isKey = false;
325 if (previousKeyIndex < j - 1 && "m".equals(token) || "g4".equals(token) || "g".equals(token) || "c".equals(token) || "s".equals(token)) {
326 isKey = true;
327 }
328 if (isKey) {
329 if (previousKeyIndex >= 0) {
330 final String key = tokens[previousKeyIndex];
331 final String color = colorBuffer.toString();
332 colorBuffer.setLength(0);
333 populatePaletteEntry(paletteEntry, key, color);
334 }
335 previousKeyIndex = j;
336 } else {
337 if (previousKeyIndex < 0) {
338 break;
339 }
340 if (colorBuffer.length() > 0) {
341 colorBuffer.append(' ');
342 }
343 colorBuffer.append(token);
344 }
345 }
346 if (previousKeyIndex >= 0 && colorBuffer.length() > 0) {
347 final String key = tokens[previousKeyIndex];
348 final String color = colorBuffer.toString();
349 colorBuffer.setLength(0);
350 populatePaletteEntry(paletteEntry, key, color);
351 }
352 xpmHeader.palette.put(name, paletteEntry);
353 }
354 }
355
356 private XpmHeader parseXpmHeader(final BasicCParser cParser) throws ImagingException, IOException {
357 String name;
358 String token;
359 token = cParser.nextToken();
360 if (!"static".equals(token)) {
361 throw new ImagingException("Parsing XPM file failed, no 'static' token");
362 }
363 token = cParser.nextToken();
364 if (!"char".equals(token)) {
365 throw new ImagingException("Parsing XPM file failed, no 'char' token");
366 }
367 token = cParser.nextToken();
368 if (!"*".equals(token)) {
369 throw new ImagingException("Parsing XPM file failed, no '*' token");
370 }
371 name = cParser.nextToken();
372 if (name == null) {
373 throw new ImagingException("Parsing XPM file failed, no variable name");
374 }
375 if (name.charAt(0) != '_' && !Character.isLetter(name.charAt(0))) {
376 throw new ImagingException("Parsing XPM file failed, variable name " + "doesn't start with letter or underscore");
377 }
378 for (int i = 0; i < name.length(); i++) {
379 final char c = name.charAt(i);
380 if (!Character.isLetterOrDigit(c) && c != '_') {
381 throw new ImagingException("Parsing XPM file failed, variable name " + "contains non-letter non-digit non-underscore");
382 }
383 }
384 token = cParser.nextToken();
385 if (!"[".equals(token)) {
386 throw new ImagingException("Parsing XPM file failed, no '[' token");
387 }
388 token = cParser.nextToken();
389 if (!"]".equals(token)) {
390 throw new ImagingException("Parsing XPM file failed, no ']' token");
391 }
392 token = cParser.nextToken();
393 if (!"=".equals(token)) {
394 throw new ImagingException("Parsing XPM file failed, no '=' token");
395 }
396 token = cParser.nextToken();
397 if (!"{".equals(token)) {
398 throw new ImagingException("Parsing XPM file failed, no '{' token");
399 }
400
401 final StringBuilder row = new StringBuilder();
402 final boolean hasMore = parseNextString(cParser, row);
403 if (!hasMore) {
404 throw new ImagingException("Parsing XPM file failed, " + "file too short");
405 }
406 final XpmHeader xpmHeader = parseXpmValuesSection(row.toString());
407 parsePaletteEntries(xpmHeader, cParser);
408 return xpmHeader;
409 }
410
411 private XpmParseResult parseXpmHeader(final ByteSource byteSource) throws ImagingException, IOException {
412 try (InputStream is = byteSource.getInputStream()) {
413 final StringBuilder firstComment = new StringBuilder();
414 final ByteArrayOutputStream preprocessedFile = BasicCParser.preprocess(is, firstComment, null);
415 if (!"XPM".equals(firstComment.toString().trim())) {
416 throw new ImagingException("Parsing XPM file failed, " + "signature isn't '/* XPM */'");
417 }
418
419 final XpmParseResult xpmParseResult = new XpmParseResult();
420 xpmParseResult.cParser = new BasicCParser(new ByteArrayInputStream(preprocessedFile.toByteArray()));
421 xpmParseResult.xpmHeader = parseXpmHeader(xpmParseResult.cParser);
422 return xpmParseResult;
423 }
424 }
425
426 private XpmHeader parseXpmValuesSection(final String row) throws ImagingException {
427 final String[] tokens = BasicCParser.tokenizeRow(row);
428 if (tokens.length < 4 || tokens.length > 7) {
429 throw new ImagingException("Parsing XPM file failed, " + "<Values> section has incorrect tokens");
430 }
431 try {
432 final int width = Integer.parseInt(tokens[0]);
433 final int height = Integer.parseInt(tokens[1]);
434 final int numColors = Integer.parseInt(tokens[2]);
435 final int numCharsPerPixel = Integer.parseInt(tokens[3]);
436 int xHotSpot = -1;
437 int yHotSpot = -1;
438 boolean xpmExt = false;
439 if (tokens.length >= 6) {
440 xHotSpot = Integer.parseInt(tokens[4]);
441 yHotSpot = Integer.parseInt(tokens[5]);
442 }
443 if (tokens.length == 5 || tokens.length == 7) {
444 if (!"XPMEXT".equals(tokens[tokens.length - 1])) {
445 throw new ImagingException("Parsing XPM file failed, " + "can't parse <Values> section XPMEXT");
446 }
447 xpmExt = true;
448 }
449 return new XpmHeader(width, height, numColors, numCharsPerPixel, xHotSpot, yHotSpot, xpmExt);
450 } catch (final NumberFormatException nfe) {
451 throw new ImagingException("Parsing XPM file failed, " + "error parsing <Values> section", nfe);
452 }
453 }
454
455 private String pixelsForIndex(int index, final int charsPerPixel) {
456 final StringBuilder stringBuilder = new StringBuilder();
457 int highestPower = 1;
458 for (int i = 1; i < charsPerPixel; i++) {
459 highestPower *= WRITE_PALETTE.length;
460 }
461 for (int i = 0; i < charsPerPixel; i++) {
462 final int multiple = index / highestPower;
463 index -= multiple * highestPower;
464 highestPower /= WRITE_PALETTE.length;
465 stringBuilder.append(WRITE_PALETTE[multiple]);
466 }
467 return stringBuilder.toString();
468 }
469
470 private void populatePaletteEntry(final PaletteEntry paletteEntry, final String key, final String color) throws ImagingException {
471 if ("m".equals(key)) {
472 paletteEntry.monoArgb = parseColor(color);
473 paletteEntry.haveMono = true;
474 } else if ("g4".equals(key)) {
475 paletteEntry.gray4LevelArgb = parseColor(color);
476 paletteEntry.haveGray4Level = true;
477 } else if ("g".equals(key)) {
478 paletteEntry.grayArgb = parseColor(color);
479 paletteEntry.haveGray = true;
480 } else if ("s".equals(key) || "c".equals(key)) {
481 paletteEntry.colorArgb = parseColor(color);
482 paletteEntry.haveColor = true;
483 }
484 }
485
486 private String randomName() {
487 final UUID uuid = UUID.randomUUID();
488 final StringBuilder stringBuilder = new StringBuilder("a");
489 long bits = uuid.getMostSignificantBits();
490
491 for (int i = 64 - 8; i >= 0; i -= 8) {
492 stringBuilder.append(Integer.toHexString((int) (bits >> i & 0xff)));
493 }
494 bits = uuid.getLeastSignificantBits();
495 for (int i = 64 - 8; i >= 0; i -= 8) {
496 stringBuilder.append(Integer.toHexString((int) (bits >> i & 0xff)));
497 }
498 return stringBuilder.toString();
499 }
500
501 private XpmHeader readXpmHeader(final ByteSource byteSource) throws ImagingException, IOException {
502 return parseXpmHeader(byteSource).xpmHeader;
503 }
504
505 private BufferedImage readXpmImage(final XpmHeader xpmHeader, final BasicCParser cParser) throws ImagingException, IOException {
506 ColorModel colorModel;
507 WritableRaster raster;
508 int bpp;
509 if (xpmHeader.palette.size() <= 1 << 8) {
510 final int[] palette = Allocator.intArray(xpmHeader.palette.size());
511 for (final Entry<Object, PaletteEntry> entry : xpmHeader.palette.entrySet()) {
512 final PaletteEntry paletteEntry = entry.getValue();
513 palette[paletteEntry.index] = paletteEntry.getBestArgb();
514 }
515 colorModel = new IndexColorModel(8, xpmHeader.palette.size(), palette, 0, true, -1, DataBuffer.TYPE_BYTE);
516
517 final int bands = 1;
518 final int scanlineStride = xpmHeader.width * bands;
519 final int pixelStride = bands;
520 final int size = scanlineStride * (xpmHeader.height - 1) +
521 pixelStride * xpmHeader.width;
522 Allocator.check(Byte.SIZE, size);
523 raster = Raster.createInterleavedRaster(DataBuffer.TYPE_BYTE, xpmHeader.width, xpmHeader.height, bands, null);
524 bpp = 8;
525 } else if (xpmHeader.palette.size() <= 1 << 16) {
526 final int[] palette = Allocator.intArray(xpmHeader.palette.size());
527 for (final Entry<Object, PaletteEntry> entry : xpmHeader.palette.entrySet()) {
528 final PaletteEntry paletteEntry = entry.getValue();
529 palette[paletteEntry.index] = paletteEntry.getBestArgb();
530 }
531 colorModel = new IndexColorModel(16, xpmHeader.palette.size(), palette, 0, true, -1, DataBuffer.TYPE_USHORT);
532
533 final int bands = 1;
534 final int scanlineStride = xpmHeader.width * bands;
535 final int pixelStride = bands;
536 final int size = scanlineStride * (xpmHeader.height - 1) +
537 pixelStride * xpmHeader.width;
538 Allocator.check(Short.SIZE, size);
539 raster = Raster.createInterleavedRaster(DataBuffer.TYPE_USHORT, xpmHeader.width, xpmHeader.height, bands, null);
540 bpp = 16;
541 } else {
542 colorModel = new DirectColorModel(32, 0x00ff0000, 0x0000ff00, 0x000000ff, 0xff000000);
543 Allocator.check(Integer.SIZE, xpmHeader.width * xpmHeader.height);
544 raster = Raster.createPackedRaster(DataBuffer.TYPE_INT, xpmHeader.width, xpmHeader.height,
545 new int[] { 0x00ff0000, 0x0000ff00, 0x000000ff, 0xff000000 }, null);
546 bpp = 32;
547 }
548
549 final BufferedImage image = new BufferedImage(colorModel, raster, colorModel.isAlphaPremultiplied(), new Properties());
550 final DataBuffer dataBuffer = raster.getDataBuffer();
551 final StringBuilder row = new StringBuilder();
552 boolean hasMore = true;
553 for (int y = 0; y < xpmHeader.height; y++) {
554 row.setLength(0);
555 hasMore = parseNextString(cParser, row);
556 if (y < xpmHeader.height - 1 && !hasMore) {
557 throw new ImagingException("Parsing XPM file failed, " + "insufficient image rows in file");
558 }
559 final int rowOffset = y * xpmHeader.width;
560 for (int x = 0; x < xpmHeader.width; x++) {
561 final String index = row.substring(x * xpmHeader.numCharsPerPixel, (x + 1) * xpmHeader.numCharsPerPixel);
562 final PaletteEntry paletteEntry = xpmHeader.palette.get(index);
563 if (paletteEntry == null) {
564 throw new ImagingException("No palette entry was defined " + "for " + index);
565 }
566 if (bpp <= 16) {
567 dataBuffer.setElem(rowOffset + x, paletteEntry.index);
568 } else {
569 dataBuffer.setElem(rowOffset + x, paletteEntry.getBestArgb());
570 }
571 }
572 }
573
574 while (hasMore) {
575 row.setLength(0);
576 hasMore = parseNextString(cParser, row);
577 }
578
579 final String token = cParser.nextToken();
580 if (!";".equals(token)) {
581 throw new ImagingException("Last token wasn't ';'");
582 }
583
584 return image;
585 }
586
587 private String toColor(final int color) {
588 final String hex = Integer.toHexString(color);
589 if (hex.length() < 6) {
590 final char[] zeroes = Allocator.charArray(6 - hex.length());
591 Arrays.fill(zeroes, '0');
592 return "#" + new String(zeroes) + hex;
593 }
594 return "#" + hex;
595 }
596
597 @Override
598 public void writeImage(final BufferedImage src, final OutputStream os, final XpmImagingParameters params) throws ImagingException, IOException {
599 final PaletteFactory paletteFactory = new PaletteFactory();
600 final boolean hasTransparency = paletteFactory.hasTransparency(src, 1);
601 SimplePalette palette = null;
602 int maxColors = WRITE_PALETTE.length;
603 int charsPerPixel = 1;
604 while (palette == null) {
605 palette = paletteFactory.makeExactRgbPaletteSimple(src, hasTransparency ? maxColors - 1 : maxColors);
606
607
608
609 final long nextMaxColors = maxColors * WRITE_PALETTE.length;
610 final long nextCharsPerPixel = charsPerPixel + 1;
611 if (nextMaxColors > Integer.MAX_VALUE) {
612 throw new ImagingException("Xpm: Can't write images with more than Integer.MAX_VALUE colors.");
613 }
614 if (nextCharsPerPixel > Integer.MAX_VALUE) {
615 throw new ImagingException("Xpm: Can't write images with more than Integer.MAX_VALUE chars per pixel.");
616 }
617
618 if (palette == null) {
619 maxColors *= WRITE_PALETTE.length;
620 charsPerPixel++;
621 }
622 }
623 int colors = palette.length();
624 if (hasTransparency) {
625 ++colors;
626 }
627
628 String line = "/* XPM */\n";
629 os.write(line.getBytes(StandardCharsets.US_ASCII));
630 line = "static char *" + randomName() + "[] = {\n";
631 os.write(line.getBytes(StandardCharsets.US_ASCII));
632 line = "\"" + src.getWidth() + " " + src.getHeight() + " " + colors + " " + charsPerPixel + "\",\n";
633 os.write(line.getBytes(StandardCharsets.US_ASCII));
634
635 for (int i = 0; i < colors; i++) {
636 String color;
637 if (i < palette.length()) {
638 color = toColor(palette.getEntry(i));
639 } else {
640 color = "None";
641 }
642 line = "\"" + pixelsForIndex(i, charsPerPixel) + " c " + color + "\",\n";
643 os.write(line.getBytes(StandardCharsets.US_ASCII));
644 }
645
646 String separator = "";
647 for (int y = 0; y < src.getHeight(); y++) {
648 os.write(separator.getBytes(StandardCharsets.US_ASCII));
649 separator = ",\n";
650 line = "\"";
651 os.write(line.getBytes(StandardCharsets.US_ASCII));
652 for (int x = 0; x < src.getWidth(); x++) {
653 final int argb = src.getRGB(x, y);
654 if ((argb & 0xff000000) == 0) {
655 line = pixelsForIndex(palette.length(), charsPerPixel);
656 } else {
657 line = pixelsForIndex(palette.getPaletteIndex(0xffffff & argb), charsPerPixel);
658 }
659 os.write(line.getBytes(StandardCharsets.US_ASCII));
660 }
661 line = "\"";
662 os.write(line.getBytes(StandardCharsets.US_ASCII));
663 }
664
665 line = "\n};\n";
666 os.write(line.getBytes(StandardCharsets.US_ASCII));
667 }
668 }