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.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         // Long.toHexString() breaks for very big numbers
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             // Check allocation
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) + // first (h - 1) scans
521                     pixelStride * xpmHeader.width; // last scan
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             // Check allocation
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) + // first (h - 1) scans
537                     pixelStride * xpmHeader.width; // last scan
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             // leave the loop if numbers would go beyond Integer.MAX_VALUE to avoid infinite loops
608             // test every operation from below if it would increase an int value beyond Integer.MAX_VALUE
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             // the code above makes sure that we never go beyond Integer.MAX_VALUE here
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 }