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.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}