001/*
002 *  Licensed under the Apache License, Version 2.0 (the "License");
003 *  you may not use this file except in compliance with the License.
004 *  You may obtain a copy of the License at
005 *
006 *       http://www.apache.org/licenses/LICENSE-2.0
007 *
008 *  Unless required by applicable law or agreed to in writing, software
009 *  distributed under the License is distributed on an "AS IS" BASIS,
010 *  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
011 *  See the License for the specific language governing permissions and
012 *  limitations under the License.
013 */
014package org.apache.commons.imaging.formats.xpm;
015
016import java.awt.Dimension;
017import java.awt.image.BufferedImage;
018import java.awt.image.ColorModel;
019import java.awt.image.DataBuffer;
020import java.awt.image.DirectColorModel;
021import java.awt.image.IndexColorModel;
022import java.awt.image.Raster;
023import java.awt.image.WritableRaster;
024import java.io.BufferedReader;
025import java.io.ByteArrayInputStream;
026import java.io.ByteArrayOutputStream;
027import java.io.IOException;
028import java.io.InputStream;
029import java.io.InputStreamReader;
030import java.io.OutputStream;
031import java.io.PrintWriter;
032import java.nio.charset.StandardCharsets;
033import java.util.ArrayList;
034import java.util.Arrays;
035import java.util.HashMap;
036import java.util.Locale;
037import java.util.Map;
038import java.util.Map.Entry;
039import java.util.Properties;
040import java.util.UUID;
041
042import org.apache.commons.imaging.AbstractImageParser;
043import org.apache.commons.imaging.ImageFormat;
044import org.apache.commons.imaging.ImageFormats;
045import org.apache.commons.imaging.ImageInfo;
046import org.apache.commons.imaging.ImagingException;
047import org.apache.commons.imaging.bytesource.ByteSource;
048import org.apache.commons.imaging.common.Allocator;
049import org.apache.commons.imaging.common.BasicCParser;
050import org.apache.commons.imaging.common.ImageMetadata;
051import org.apache.commons.imaging.palette.PaletteFactory;
052import org.apache.commons.imaging.palette.SimplePalette;
053
054public class XpmImageParser extends AbstractImageParser<XpmImagingParameters> {
055
056    private static final class PaletteEntry {
057        int colorArgb;
058        int gray4LevelArgb;
059        int grayArgb;
060        boolean haveColor;
061        boolean haveGray;
062        boolean haveGray4Level;
063        boolean haveMono;
064        int index;
065        int monoArgb;
066
067        int getBestArgb() {
068            if (haveColor) {
069                return colorArgb;
070            }
071            if (haveGray) {
072                return grayArgb;
073            }
074            if (haveGray4Level) {
075                return gray4LevelArgb;
076            }
077            if (haveMono) {
078                return monoArgb;
079            }
080            return 0x00000000;
081        }
082    }
083
084    private static final class XpmHeader {
085        final int height;
086        final int numCharsPerPixel;
087        final int numColors;
088        final Map<Object, PaletteEntry> palette = new HashMap<>();
089        final int width;
090        int xHotSpot = -1;
091        final boolean xpmExt;
092
093        int yHotSpot = -1;
094
095        XpmHeader(final int width, final int height, final int numColors, final int numCharsPerPixel, final int xHotSpot, final int yHotSpot,
096                final boolean xpmExt) {
097            this.width = width;
098            this.height = height;
099            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}