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.ROOT), 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
173
174
175 public XpmImageParser() {
176
177 }
178
179 @Override
180 public boolean dumpImageFile(final PrintWriter pw, final ByteSource byteSource) throws ImagingException, IOException {
181 readXpmHeader(byteSource).dump(pw);
182 return true;
183 }
184
185 @Override
186 protected String[] getAcceptedExtensions() {
187 return ACCEPTED_EXTENSIONS;
188 }
189
190 @Override
191 protected ImageFormat[] getAcceptedTypes() {
192 return new ImageFormat[] { ImageFormats.XPM,
193 };
194 }
195
196 @Override
197 public final BufferedImage getBufferedImage(final ByteSource byteSource, final XpmImagingParameters params) throws ImagingException, IOException {
198 final XpmParseResult result = parseXpmHeader(byteSource);
199 return readXpmImage(result.xpmHeader, result.cParser);
200 }
201
202 @Override
203 public String getDefaultExtension() {
204 return DEFAULT_EXTENSION;
205 }
206
207 @Override
208 public XpmImagingParameters getDefaultParameters() {
209 return new XpmImagingParameters();
210 }
211
212 @Override
213 public byte[] getIccProfileBytes(final ByteSource byteSource, final XpmImagingParameters params) throws ImagingException, IOException {
214 return null;
215 }
216
217 @Override
218 public ImageInfo getImageInfo(final ByteSource byteSource, final XpmImagingParameters params) throws ImagingException, IOException {
219 final XpmHeader xpmHeader = readXpmHeader(byteSource);
220 boolean transparent = false;
221 ImageInfo.ColorType colorType = ImageInfo.ColorType.BW;
222 for (final Entry<Object, PaletteEntry> entry : xpmHeader.palette.entrySet()) {
223 final PaletteEntry paletteEntry = entry.getValue();
224 if ((paletteEntry.getBestArgb() & 0xff000000) != 0xff000000) {
225 transparent = true;
226 }
227 if (paletteEntry.haveColor) {
228 colorType = ImageInfo.ColorType.RGB;
229 } else if (colorType != ImageInfo.ColorType.RGB && (paletteEntry.haveGray || paletteEntry.haveGray4Level)) {
230 colorType = ImageInfo.ColorType.GRAYSCALE;
231 }
232 }
233 return new ImageInfo("XPM version 3", xpmHeader.numCharsPerPixel * 8, new ArrayList<>(), ImageFormats.XPM, "X PixMap", xpmHeader.height,
234 "image/x-xpixmap", 1, 0, 0, 0, 0, xpmHeader.width, false, transparent, true, colorType, ImageInfo.CompressionAlgorithm.NONE);
235 }
236
237 @Override
238 public Dimension getImageSize(final ByteSource byteSource, final XpmImagingParameters params) throws ImagingException, IOException {
239 final XpmHeader xpmHeader = readXpmHeader(byteSource);
240 return new Dimension(xpmHeader.width, xpmHeader.height);
241 }
242
243 @Override
244 public ImageMetadata getMetadata(final ByteSource byteSource, final XpmImagingParameters params) throws ImagingException, IOException {
245 return null;
246 }
247
248 @Override
249 public String getName() {
250 return "X PixMap";
251 }
252
253 private int parseColor(String color) throws ImagingException {
254 if (color.charAt(0) == '#') {
255 color = color.substring(1);
256 if (color.length() == 3) {
257 final int red = Integer.parseInt(color.substring(0, 1), 16);
258 final int green = Integer.parseInt(color.substring(1, 2), 16);
259 final int blue = Integer.parseInt(color.substring(2, 3), 16);
260 return 0xff000000 | red << 20 | green << 12 | blue << 4;
261 }
262 if (color.length() == 6) {
263 return 0xff000000 | Integer.parseInt(color, 16);
264 }
265 if (color.length() == 9) {
266 final int red = Integer.parseInt(color.substring(0, 1), 16);
267 final int green = Integer.parseInt(color.substring(3, 4), 16);
268 final int blue = Integer.parseInt(color.substring(6, 7), 16);
269 return 0xff000000 | red << 16 | green << 8 | blue;
270 }
271 if (color.length() == 12) {
272 final int red = Integer.parseInt(color.substring(0, 1), 16);
273 final int green = Integer.parseInt(color.substring(4, 5), 16);
274 final int blue = Integer.parseInt(color.substring(8, 9), 16);
275 return 0xff000000 | red << 16 | green << 8 | blue;
276 }
277 if (color.length() == 24) {
278 final int red = Integer.parseInt(color.substring(0, 1), 16);
279 final int green = Integer.parseInt(color.substring(8, 9), 16);
280 final int blue = Integer.parseInt(color.substring(16, 17), 16);
281 return 0xff000000 | red << 16 | green << 8 | blue;
282 }
283 return 0x00000000;
284 }
285 if (color.charAt(0) == '%') {
286 throw new ImagingException("HSV colors are not implemented " + "even in the XPM specification!");
287 }
288 if ("None".equals(color)) {
289 return 0x00000000;
290 }
291 loadColorNames();
292 final String colorLowercase = color.toLowerCase(Locale.ROOT);
293 return colorNames.getOrDefault(colorLowercase, 0x00000000);
294 }
295
296 private boolean parseNextString(final BasicCParser cParser, final StringBuilder stringBuilder) throws IOException, ImagingException {
297 stringBuilder.setLength(0);
298 String token = cParser.nextToken();
299 if (token.charAt(0) != '"') {
300 throw new ImagingException("Parsing XPM file failed, " + "no string found where expected");
301 }
302 BasicCParser.unescapeString(stringBuilder, token);
303 for (token = cParser.nextToken(); token.charAt(0) == '"'; token = cParser.nextToken()) {
304 BasicCParser.unescapeString(stringBuilder, token);
305 }
306 if (",".equals(token)) {
307 return true;
308 }
309 if ("}".equals(token)) {
310 return false;
311 }
312 throw new ImagingException("Parsing XPM file failed, " + "no ',' or '}' found where expected");
313 }
314
315 private void parsePaletteEntries(final XpmHeader xpmHeader, final BasicCParser cParser) throws IOException, ImagingException {
316 final StringBuilder row = new StringBuilder();
317 for (int i = 0; i < xpmHeader.numColors; i++) {
318 row.setLength(0);
319 final boolean hasMore = parseNextString(cParser, row);
320 if (!hasMore) {
321 throw new ImagingException("Parsing XPM file failed, " + "file ended while reading palette");
322 }
323 final String name = row.substring(0, xpmHeader.numCharsPerPixel);
324 final String[] tokens = BasicCParser.tokenizeRow(row.substring(xpmHeader.numCharsPerPixel));
325 final PaletteEntry paletteEntry = new PaletteEntry();
326 paletteEntry.index = i;
327 int previousKeyIndex = Integer.MIN_VALUE;
328 final StringBuilder colorBuffer = new StringBuilder();
329 for (int j = 0; j < tokens.length; j++) {
330 final String token = tokens[j];
331 boolean isKey = false;
332 if (previousKeyIndex < j - 1 && "m".equals(token) || "g4".equals(token) || "g".equals(token) || "c".equals(token) || "s".equals(token)) {
333 isKey = true;
334 }
335 if (isKey) {
336 if (previousKeyIndex >= 0) {
337 final String key = tokens[previousKeyIndex];
338 final String color = colorBuffer.toString();
339 colorBuffer.setLength(0);
340 populatePaletteEntry(paletteEntry, key, color);
341 }
342 previousKeyIndex = j;
343 } else {
344 if (previousKeyIndex < 0) {
345 break;
346 }
347 if (colorBuffer.length() > 0) {
348 colorBuffer.append(' ');
349 }
350 colorBuffer.append(token);
351 }
352 }
353 if (previousKeyIndex >= 0 && colorBuffer.length() > 0) {
354 final String key = tokens[previousKeyIndex];
355 final String color = colorBuffer.toString();
356 colorBuffer.setLength(0);
357 populatePaletteEntry(paletteEntry, key, color);
358 }
359 xpmHeader.palette.put(name, paletteEntry);
360 }
361 }
362
363 private XpmHeader parseXpmHeader(final BasicCParser cParser) throws ImagingException, IOException {
364 final String name;
365 String token;
366 token = cParser.nextToken();
367 if (!"static".equals(token)) {
368 throw new ImagingException("Parsing XPM file failed, no 'static' token");
369 }
370 token = cParser.nextToken();
371 if (!"char".equals(token)) {
372 throw new ImagingException("Parsing XPM file failed, no 'char' token");
373 }
374 token = cParser.nextToken();
375 if (!"*".equals(token)) {
376 throw new ImagingException("Parsing XPM file failed, no '*' token");
377 }
378 name = cParser.nextToken();
379 if (name == null) {
380 throw new ImagingException("Parsing XPM file failed, no variable name");
381 }
382 if (name.charAt(0) != '_' && !Character.isLetter(name.charAt(0))) {
383 throw new ImagingException("Parsing XPM file failed, variable name " + "doesn't start with letter or underscore");
384 }
385 for (int i = 0; i < name.length(); i++) {
386 final char c = name.charAt(i);
387 if (!Character.isLetterOrDigit(c) && c != '_') {
388 throw new ImagingException("Parsing XPM file failed, variable name " + "contains non-letter non-digit non-underscore");
389 }
390 }
391 token = cParser.nextToken();
392 if (!"[".equals(token)) {
393 throw new ImagingException("Parsing XPM file failed, no '[' token");
394 }
395 token = cParser.nextToken();
396 if (!"]".equals(token)) {
397 throw new ImagingException("Parsing XPM file failed, no ']' token");
398 }
399 token = cParser.nextToken();
400 if (!"=".equals(token)) {
401 throw new ImagingException("Parsing XPM file failed, no '=' token");
402 }
403 token = cParser.nextToken();
404 if (!"{".equals(token)) {
405 throw new ImagingException("Parsing XPM file failed, no '{' token");
406 }
407
408 final StringBuilder row = new StringBuilder();
409 final boolean hasMore = parseNextString(cParser, row);
410 if (!hasMore) {
411 throw new ImagingException("Parsing XPM file failed, " + "file too short");
412 }
413 final XpmHeader xpmHeader = parseXpmValuesSection(row.toString());
414 parsePaletteEntries(xpmHeader, cParser);
415 return xpmHeader;
416 }
417
418 private XpmParseResult parseXpmHeader(final ByteSource byteSource) throws ImagingException, IOException {
419 try (InputStream is = byteSource.getInputStream()) {
420 final StringBuilder firstComment = new StringBuilder();
421 final ByteArrayOutputStream preprocessedFile = BasicCParser.preprocess(is, firstComment, null);
422 if (!"XPM".equals(firstComment.toString().trim())) {
423 throw new ImagingException("Parsing XPM file failed, " + "signature isn't '/* XPM */'");
424 }
425
426 final XpmParseResult xpmParseResult = new XpmParseResult();
427 xpmParseResult.cParser = new BasicCParser(new ByteArrayInputStream(preprocessedFile.toByteArray()));
428 xpmParseResult.xpmHeader = parseXpmHeader(xpmParseResult.cParser);
429 return xpmParseResult;
430 }
431 }
432
433 private XpmHeader parseXpmValuesSection(final String row) throws ImagingException {
434 final String[] tokens = BasicCParser.tokenizeRow(row);
435 if (tokens.length < 4 || tokens.length > 7) {
436 throw new ImagingException("Parsing XPM file failed, " + "<Values> section has incorrect tokens");
437 }
438 try {
439 final int width = Integer.parseInt(tokens[0]);
440 final int height = Integer.parseInt(tokens[1]);
441 final int numColors = Integer.parseInt(tokens[2]);
442 final int numCharsPerPixel = Integer.parseInt(tokens[3]);
443 int xHotSpot = -1;
444 int yHotSpot = -1;
445 boolean xpmExt = false;
446 if (tokens.length >= 6) {
447 xHotSpot = Integer.parseInt(tokens[4]);
448 yHotSpot = Integer.parseInt(tokens[5]);
449 }
450 if (tokens.length == 5 || tokens.length == 7) {
451 if (!"XPMEXT".equals(tokens[tokens.length - 1])) {
452 throw new ImagingException("Parsing XPM file failed, " + "can't parse <Values> section XPMEXT");
453 }
454 xpmExt = true;
455 }
456 return new XpmHeader(width, height, numColors, numCharsPerPixel, xHotSpot, yHotSpot, xpmExt);
457 } catch (final NumberFormatException nfe) {
458 throw new ImagingException("Parsing XPM file failed, " + "error parsing <Values> section", nfe);
459 }
460 }
461
462 private String pixelsForIndex(int index, final int charsPerPixel) {
463 final StringBuilder stringBuilder = new StringBuilder();
464 int highestPower = 1;
465 for (int i = 1; i < charsPerPixel; i++) {
466 highestPower *= WRITE_PALETTE.length;
467 }
468 for (int i = 0; i < charsPerPixel; i++) {
469 final int multiple = index / highestPower;
470 index -= multiple * highestPower;
471 highestPower /= WRITE_PALETTE.length;
472 stringBuilder.append(WRITE_PALETTE[multiple]);
473 }
474 return stringBuilder.toString();
475 }
476
477 private void populatePaletteEntry(final PaletteEntry paletteEntry, final String key, final String color) throws ImagingException {
478 switch (key) {
479 case "m":
480 paletteEntry.monoArgb = parseColor(color);
481 paletteEntry.haveMono = true;
482 break;
483 case "g4":
484 paletteEntry.gray4LevelArgb = parseColor(color);
485 paletteEntry.haveGray4Level = true;
486 break;
487 case "g":
488 paletteEntry.grayArgb = parseColor(color);
489 paletteEntry.haveGray = true;
490 break;
491 case "s":
492 case "c":
493 paletteEntry.colorArgb = parseColor(color);
494 paletteEntry.haveColor = true;
495 break;
496 default:
497 break;
498 }
499 }
500
501 private String randomName() {
502 final UUID uuid = UUID.randomUUID();
503 final StringBuilder stringBuilder = new StringBuilder("a");
504 long bits = uuid.getMostSignificantBits();
505
506 for (int i = 64 - 8; i >= 0; i -= 8) {
507 stringBuilder.append(Integer.toHexString((int) (bits >> i & 0xff)));
508 }
509 bits = uuid.getLeastSignificantBits();
510 for (int i = 64 - 8; i >= 0; i -= 8) {
511 stringBuilder.append(Integer.toHexString((int) (bits >> i & 0xff)));
512 }
513 return stringBuilder.toString();
514 }
515
516 private XpmHeader readXpmHeader(final ByteSource byteSource) throws ImagingException, IOException {
517 return parseXpmHeader(byteSource).xpmHeader;
518 }
519
520 private BufferedImage readXpmImage(final XpmHeader xpmHeader, final BasicCParser cParser) throws ImagingException, IOException {
521 final ColorModel colorModel;
522 final WritableRaster raster;
523 final int bpp;
524 if (xpmHeader.palette.size() <= 1 << 8) {
525 final int[] palette = Allocator.intArray(xpmHeader.palette.size());
526 for (final Entry<Object, PaletteEntry> entry : xpmHeader.palette.entrySet()) {
527 final PaletteEntry paletteEntry = entry.getValue();
528 palette[paletteEntry.index] = paletteEntry.getBestArgb();
529 }
530 colorModel = new IndexColorModel(8, xpmHeader.palette.size(), palette, 0, true, -1, DataBuffer.TYPE_BYTE);
531
532 final int bands = 1;
533 final int scanlineStride = xpmHeader.width * bands;
534 final int pixelStride = bands;
535 final int size = scanlineStride * (xpmHeader.height - 1) +
536 pixelStride * xpmHeader.width;
537 Allocator.check(Byte.SIZE, size);
538 raster = Raster.createInterleavedRaster(DataBuffer.TYPE_BYTE, xpmHeader.width, xpmHeader.height, bands, null);
539 bpp = 8;
540 } else if (xpmHeader.palette.size() <= 1 << 16) {
541 final int[] palette = Allocator.intArray(xpmHeader.palette.size());
542 for (final Entry<Object, PaletteEntry> entry : xpmHeader.palette.entrySet()) {
543 final PaletteEntry paletteEntry = entry.getValue();
544 palette[paletteEntry.index] = paletteEntry.getBestArgb();
545 }
546 colorModel = new IndexColorModel(16, xpmHeader.palette.size(), palette, 0, true, -1, DataBuffer.TYPE_USHORT);
547
548 final int bands = 1;
549 final int scanlineStride = xpmHeader.width * bands;
550 final int pixelStride = bands;
551 final int size = scanlineStride * (xpmHeader.height - 1) +
552 pixelStride * xpmHeader.width;
553 Allocator.check(Short.SIZE, size);
554 raster = Raster.createInterleavedRaster(DataBuffer.TYPE_USHORT, xpmHeader.width, xpmHeader.height, bands, null);
555 bpp = 16;
556 } else {
557 colorModel = new DirectColorModel(32, 0x00ff0000, 0x0000ff00, 0x000000ff, 0xff000000);
558 Allocator.check(Integer.SIZE, xpmHeader.width * xpmHeader.height);
559 raster = Raster.createPackedRaster(DataBuffer.TYPE_INT, xpmHeader.width, xpmHeader.height,
560 new int[] { 0x00ff0000, 0x0000ff00, 0x000000ff, 0xff000000 }, null);
561 bpp = 32;
562 }
563
564 final BufferedImage image = new BufferedImage(colorModel, raster, colorModel.isAlphaPremultiplied(), new Properties());
565 final DataBuffer dataBuffer = raster.getDataBuffer();
566 final StringBuilder row = new StringBuilder();
567 boolean hasMore = true;
568 for (int y = 0; y < xpmHeader.height; y++) {
569 row.setLength(0);
570 hasMore = parseNextString(cParser, row);
571 if (y < xpmHeader.height - 1 && !hasMore) {
572 throw new ImagingException("Parsing XPM file failed, " + "insufficient image rows in file");
573 }
574 final int rowOffset = y * xpmHeader.width;
575 for (int x = 0; x < xpmHeader.width; x++) {
576 final String index = row.substring(x * xpmHeader.numCharsPerPixel, (x + 1) * xpmHeader.numCharsPerPixel);
577 final PaletteEntry paletteEntry = xpmHeader.palette.get(index);
578 if (paletteEntry == null) {
579 throw new ImagingException("No palette entry was defined " + "for " + index);
580 }
581 if (bpp <= 16) {
582 dataBuffer.setElem(rowOffset + x, paletteEntry.index);
583 } else {
584 dataBuffer.setElem(rowOffset + x, paletteEntry.getBestArgb());
585 }
586 }
587 }
588
589 while (hasMore) {
590 row.setLength(0);
591 hasMore = parseNextString(cParser, row);
592 }
593
594 final String token = cParser.nextToken();
595 if (!";".equals(token)) {
596 throw new ImagingException("Last token wasn't ';'");
597 }
598
599 return image;
600 }
601
602 private String toColor(final int color) {
603 final String hex = Integer.toHexString(color);
604 if (hex.length() < 6) {
605 final char[] zeroes = Allocator.charArray(6 - hex.length());
606 Arrays.fill(zeroes, '0');
607 return "#" + new String(zeroes) + hex;
608 }
609 return "#" + hex;
610 }
611
612 @Override
613 public void writeImage(final BufferedImage src, final OutputStream os, final XpmImagingParameters params) throws ImagingException, IOException {
614 final PaletteFactory paletteFactory = new PaletteFactory();
615 final boolean hasTransparency = paletteFactory.hasTransparency(src, 1);
616 SimplePalette palette = null;
617 int maxColors = WRITE_PALETTE.length;
618 int charsPerPixel = 1;
619 while (palette == null) {
620 palette = paletteFactory.makeExactRgbPaletteSimple(src, hasTransparency ? maxColors - 1 : maxColors);
621
622
623
624 final long nextMaxColors = maxColors * WRITE_PALETTE.length;
625 final long nextCharsPerPixel = charsPerPixel + 1;
626 if (nextMaxColors > Integer.MAX_VALUE) {
627 throw new ImagingException("Xpm: Can't write images with more than Integer.MAX_VALUE colors.");
628 }
629 if (nextCharsPerPixel > Integer.MAX_VALUE) {
630 throw new ImagingException("Xpm: Can't write images with more than Integer.MAX_VALUE chars per pixel.");
631 }
632
633 if (palette == null) {
634 maxColors *= WRITE_PALETTE.length;
635 charsPerPixel++;
636 }
637 }
638 int colors = palette.length();
639 if (hasTransparency) {
640 ++colors;
641 }
642
643 String line = "/* XPM */\n";
644 os.write(line.getBytes(StandardCharsets.US_ASCII));
645 line = "static char *" + randomName() + "[] = {\n";
646 os.write(line.getBytes(StandardCharsets.US_ASCII));
647 line = "\"" + src.getWidth() + " " + src.getHeight() + " " + colors + " " + charsPerPixel + "\",\n";
648 os.write(line.getBytes(StandardCharsets.US_ASCII));
649
650 for (int i = 0; i < colors; i++) {
651 final String color;
652 if (i < palette.length()) {
653 color = toColor(palette.getEntry(i));
654 } else {
655 color = "None";
656 }
657 line = "\"" + pixelsForIndex(i, charsPerPixel) + " c " + color + "\",\n";
658 os.write(line.getBytes(StandardCharsets.US_ASCII));
659 }
660
661 String separator = "";
662 for (int y = 0; y < src.getHeight(); y++) {
663 os.write(separator.getBytes(StandardCharsets.US_ASCII));
664 separator = ",\n";
665 line = "\"";
666 os.write(line.getBytes(StandardCharsets.US_ASCII));
667 for (int x = 0; x < src.getWidth(); x++) {
668 final int argb = src.getRGB(x, y);
669 if ((argb & 0xff000000) == 0) {
670 line = pixelsForIndex(palette.length(), charsPerPixel);
671 } else {
672 line = pixelsForIndex(palette.getPaletteIndex(0xffffff & argb), charsPerPixel);
673 }
674 os.write(line.getBytes(StandardCharsets.US_ASCII));
675 }
676 line = "\"";
677 os.write(line.getBytes(StandardCharsets.US_ASCII));
678 }
679
680 line = "\n};\n";
681 os.write(line.getBytes(StandardCharsets.US_ASCII));
682 }
683 }