View Javadoc
1   /*
2    *  Licensed under the Apache License, Version 2.0 (the "License");
3    *  you may not use this file except in compliance with the License.
4    *  You may obtain a copy of the License at
5    *
6    *       http://www.apache.org/licenses/LICENSE-2.0
7    *
8    *  Unless required by applicable law or agreed to in writing, software
9    *  distributed under the License is distributed on an "AS IS" BASIS,
10   *  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
11   *  See the License for the specific language governing permissions and
12   *  limitations under the License.
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      * Constructs a new instance with the big-endian byte order.
174      */
175     public XpmImageParser() {
176         // empty
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         // Long.toHexString() breaks for very big numbers
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             // Check allocation
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) + // first (h - 1) scans
536                     pixelStride * xpmHeader.width; // last scan
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             // Check allocation
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) + // first (h - 1) scans
552                     pixelStride * xpmHeader.width; // last scan
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             // leave the loop if numbers would go beyond Integer.MAX_VALUE to avoid infinite loops
623             // test every operation from below if it would increase an int value beyond Integer.MAX_VALUE
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             // the code above makes sure that we never go beyond Integer.MAX_VALUE here
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 }