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.ImageFormat;
043import org.apache.commons.imaging.ImageFormats;
044import org.apache.commons.imaging.ImageInfo;
045import org.apache.commons.imaging.ImageParser;
046import org.apache.commons.imaging.ImageReadException;
047import org.apache.commons.imaging.ImageWriteException;
048import org.apache.commons.imaging.common.BasicCParser;
049import org.apache.commons.imaging.common.ImageMetadata;
050import org.apache.commons.imaging.common.bytesource.ByteSource;
051import org.apache.commons.imaging.palette.PaletteFactory;
052import org.apache.commons.imaging.palette.SimplePalette;
053
054public class XpmImageParser extends ImageParser<XpmImagingParameters> {
055    private static final String DEFAULT_EXTENSION = ImageFormats.XPM.getDefaultExtension();
056    private static final String[] ACCEPTED_EXTENSIONS = ImageFormats.XPM.getExtensions();
057    private static Map<String, Integer> colorNames;
058    private static final char[] WRITE_PALETTE = { ' ', '.', 'X', 'o', 'O', '+',
059        '@', '#', '$', '%', '&', '*', '=', '-', ';', ':', '>', ',', '<',
060        '1', '2', '3', '4', '5', '6', '7', '8', '9', '0', 'q', 'w', 'e',
061        'r', 't', 'y', 'u', 'i', 'p', 'a', 's', 'd', 'f', 'g', 'h', 'j',
062        'k', 'l', 'z', 'x', 'c', 'v', 'b', 'n', 'm', 'M', 'N', 'B', 'V',
063        'C', 'Z', 'A', 'S', 'D', 'F', 'G', 'H', 'J', 'K', 'L', 'P', 'I',
064        'U', 'Y', 'T', 'R', 'E', 'W', 'Q', '!', '~', '^', '/', '(', ')',
065        '_', '`', '\'', ']', '[', '{', '}', '|', };
066
067    private static void loadColorNames() throws ImageReadException {
068        synchronized (XpmImageParser.class) {
069            if (colorNames != null) {
070                return;
071            }
072
073            try {
074                final InputStream rgbTxtStream =
075                        XpmImageParser.class.getResourceAsStream("rgb.txt");
076                if (rgbTxtStream == null) {
077                    throw new ImageReadException("Couldn't find rgb.txt in our resources");
078                }
079                final Map<String, Integer> colors = new HashMap<>();
080                try (InputStreamReader isReader = new InputStreamReader(rgbTxtStream, StandardCharsets.US_ASCII);
081                        BufferedReader reader = new BufferedReader(isReader)) {
082                    String line;
083                    while ((line = reader.readLine()) != null) {
084                        if (line.charAt(0) == '!') {
085                            continue;
086                        }
087                        try {
088                            final int red = Integer.parseInt(line.substring(0, 3).trim());
089                            final int green = Integer.parseInt(line.substring(4, 7).trim());
090                            final int blue = Integer.parseInt(line.substring(8, 11).trim());
091                            final String colorName = line.substring(11).trim();
092                            colors.put(colorName.toLowerCase(Locale.ENGLISH), 0xff000000 | (red << 16)
093                                    | (green << 8) | blue);
094                        } catch (final NumberFormatException nfe) {
095                            throw new ImageReadException("Couldn't parse color in rgb.txt", nfe);
096                        }
097                    }
098                }
099                colorNames = colors;
100            } catch (final IOException ioException) {
101                throw new ImageReadException("Could not parse rgb.txt", ioException);
102            }
103        }
104    }
105
106    @Override
107    public XpmImagingParameters getDefaultParameters() {
108        return new XpmImagingParameters();
109    }
110
111    @Override
112    public String getName() {
113        return "X PixMap";
114    }
115
116    @Override
117    public String getDefaultExtension() {
118        return DEFAULT_EXTENSION;
119    }
120
121    @Override
122    protected String[] getAcceptedExtensions() {
123        return ACCEPTED_EXTENSIONS;
124    }
125
126    @Override
127    protected ImageFormat[] getAcceptedTypes() {
128        return new ImageFormat[] { ImageFormats.XPM, //
129        };
130    }
131
132    @Override
133    public ImageMetadata getMetadata(final ByteSource byteSource, final XpmImagingParameters params)
134            throws ImageReadException, IOException {
135        return null;
136    }
137
138    @Override
139    public ImageInfo getImageInfo(final ByteSource byteSource, final XpmImagingParameters params)
140            throws ImageReadException, IOException {
141        final XpmHeader xpmHeader = readXpmHeader(byteSource);
142        boolean transparent = false;
143        ImageInfo.ColorType colorType = ImageInfo.ColorType.BW;
144        for (final Entry<Object, PaletteEntry> entry : xpmHeader.palette.entrySet()) {
145            final PaletteEntry paletteEntry = entry.getValue();
146            if ((paletteEntry.getBestARGB() & 0xff000000) != 0xff000000) {
147                transparent = true;
148            }
149            if (paletteEntry.haveColor) {
150                colorType = ImageInfo.ColorType.RGB;
151            } else if (colorType != ImageInfo.ColorType.RGB
152                    && (paletteEntry.haveGray || paletteEntry.haveGray4Level)) {
153                colorType = ImageInfo.ColorType.GRAYSCALE;
154            }
155        }
156        return new ImageInfo("XPM version 3", xpmHeader.numCharsPerPixel * 8,
157                new ArrayList<>(), ImageFormats.XPM,
158                "X PixMap", xpmHeader.height, "image/x-xpixmap", 1, 0, 0, 0, 0,
159                xpmHeader.width, false, transparent, true, colorType,
160                ImageInfo.CompressionAlgorithm.NONE);
161    }
162
163    @Override
164    public Dimension getImageSize(final ByteSource byteSource, final XpmImagingParameters params)
165            throws ImageReadException, IOException {
166        final XpmHeader xpmHeader = readXpmHeader(byteSource);
167        return new Dimension(xpmHeader.width, xpmHeader.height);
168    }
169
170    @Override
171    public byte[] getICCProfileBytes(final ByteSource byteSource, final XpmImagingParameters params)
172            throws ImageReadException, IOException {
173        return null;
174    }
175
176    private static class XpmHeader {
177        final int width;
178        final int height;
179        final int numColors;
180        final int numCharsPerPixel;
181        int xHotSpot = -1;
182        int yHotSpot = -1;
183        final boolean xpmExt;
184
185        final  Map<Object, PaletteEntry> palette = new HashMap<>();
186
187        XpmHeader(final int width, final int height, final int numColors,
188                final int numCharsPerPixel, final int xHotSpot, final int yHotSpot, final boolean xpmExt) {
189            this.width = width;
190            this.height = height;
191            this.numColors = numColors;
192            this.numCharsPerPixel = numCharsPerPixel;
193            this.xHotSpot = xHotSpot;
194            this.yHotSpot = yHotSpot;
195            this.xpmExt = xpmExt;
196        }
197
198        public void dump(final PrintWriter pw) {
199            pw.println("XpmHeader");
200            pw.println("Width: " + width);
201            pw.println("Height: " + height);
202            pw.println("NumColors: " + numColors);
203            pw.println("NumCharsPerPixel: " + numCharsPerPixel);
204            if (xHotSpot != -1 && yHotSpot != -1) {
205                pw.println("X hotspot: " + xHotSpot);
206                pw.println("Y hotspot: " + yHotSpot);
207            }
208            pw.println("XpmExt: " + xpmExt);
209        }
210    }
211
212    private static class PaletteEntry {
213        int index;
214        boolean haveColor = false;
215        int colorArgb;
216        boolean haveGray = false;
217        int grayArgb;
218        boolean haveGray4Level = false;
219        int gray4LevelArgb;
220        boolean haveMono = false;
221        int monoArgb;
222
223        int getBestARGB() {
224            if (haveColor) {
225                return colorArgb;
226            }
227            if (haveGray) {
228                return grayArgb;
229            }
230            if (haveGray4Level) {
231                return gray4LevelArgb;
232            }
233            if (haveMono) {
234                return monoArgb;
235            }
236            return 0x00000000;
237        }
238    }
239
240    private static class XpmParseResult {
241        XpmHeader xpmHeader;
242        BasicCParser cParser;
243    }
244
245    private XpmHeader readXpmHeader(final ByteSource byteSource)
246            throws ImageReadException, IOException {
247        return parseXpmHeader(byteSource).xpmHeader;
248    }
249
250    private XpmParseResult parseXpmHeader(final ByteSource byteSource)
251            throws ImageReadException, IOException {
252        try (InputStream is = byteSource.getInputStream()) {
253            final StringBuilder firstComment = new StringBuilder();
254            final ByteArrayOutputStream preprocessedFile = BasicCParser.preprocess(
255                    is, firstComment, null);
256            if (!"XPM".equals(firstComment.toString().trim())) {
257                throw new ImageReadException("Parsing XPM file failed, "
258                        + "signature isn't '/* XPM */'");
259            }
260
261            final XpmParseResult xpmParseResult = new XpmParseResult();
262            xpmParseResult.cParser = new BasicCParser(new ByteArrayInputStream(
263                    preprocessedFile.toByteArray()));
264            xpmParseResult.xpmHeader = parseXpmHeader(xpmParseResult.cParser);
265            return xpmParseResult;
266        }
267    }
268
269    private boolean parseNextString(final BasicCParser cParser,
270            final StringBuilder stringBuilder) throws IOException, ImageReadException {
271        stringBuilder.setLength(0);
272        String token = cParser.nextToken();
273        if (token.charAt(0) != '"') {
274            throw new ImageReadException("Parsing XPM file failed, "
275                    + "no string found where expected");
276        }
277        BasicCParser.unescapeString(stringBuilder, token);
278        for (token = cParser.nextToken(); token.charAt(0) == '"'; token = cParser.nextToken()) {
279            BasicCParser.unescapeString(stringBuilder, token);
280        }
281        if (",".equals(token)) {
282            return true;
283        }
284        if ("}".equals(token)) {
285            return false;
286        }
287        throw new ImageReadException("Parsing XPM file failed, "
288                + "no ',' or '}' found where expected");
289    }
290
291    private XpmHeader parseXpmValuesSection(final String row)
292            throws ImageReadException {
293        final String[] tokens = BasicCParser.tokenizeRow(row);
294        if (tokens.length < 4 || tokens.length > 7) {
295            throw new ImageReadException("Parsing XPM file failed, "
296                    + "<Values> section has incorrect tokens");
297        }
298        try {
299            final int width = Integer.parseInt(tokens[0]);
300            final int height = Integer.parseInt(tokens[1]);
301            final int numColors = Integer.parseInt(tokens[2]);
302            final int numCharsPerPixel = Integer.parseInt(tokens[3]);
303            int xHotSpot = -1;
304            int yHotSpot = -1;
305            boolean xpmExt = false;
306            if (tokens.length >= 6) {
307                xHotSpot = Integer.parseInt(tokens[4]);
308                yHotSpot = Integer.parseInt(tokens[5]);
309            }
310            if (tokens.length == 5 || tokens.length == 7) {
311                if (!"XPMEXT".equals(tokens[tokens.length - 1])) {
312                    throw new ImageReadException("Parsing XPM file failed, "
313                            + "can't parse <Values> section XPMEXT");
314                }
315                xpmExt = true;
316            }
317            return new XpmHeader(width, height, numColors, numCharsPerPixel,
318                    xHotSpot, yHotSpot, xpmExt);
319        } catch (final NumberFormatException nfe) {
320            throw new ImageReadException("Parsing XPM file failed, "
321                    + "error parsing <Values> section", nfe);
322        }
323    }
324
325    private int parseColor(String color) throws ImageReadException {
326        if (color.charAt(0) == '#') {
327            color = color.substring(1);
328            if (color.length() == 3) {
329                final int red = Integer.parseInt(color.substring(0, 1), 16);
330                final int green = Integer.parseInt(color.substring(1, 2), 16);
331                final int blue = Integer.parseInt(color.substring(2, 3), 16);
332                return 0xff000000 | (red << 20) | (green << 12) | (blue << 4);
333            }
334            if (color.length() == 6) {
335                return 0xff000000 | Integer.parseInt(color, 16);
336            }
337            if (color.length() == 9) {
338                final int red = Integer.parseInt(color.substring(0, 1), 16);
339                final int green = Integer.parseInt(color.substring(3, 4), 16);
340                final int blue = Integer.parseInt(color.substring(6, 7), 16);
341                return 0xff000000 | (red << 16) | (green << 8) | blue;
342            }
343            if (color.length() == 12) {
344                final int red = Integer.parseInt(color.substring(0, 1), 16);
345                final int green = Integer.parseInt(color.substring(4, 5), 16);
346                final int blue = Integer.parseInt(color.substring(8, 9), 16);
347                return 0xff000000 | (red << 16) | (green << 8) | blue;
348            }
349            if (color.length() == 24) {
350                final int red = Integer.parseInt(color.substring(0, 1), 16);
351                final int green = Integer.parseInt(color.substring(8, 9), 16);
352                final int blue = Integer.parseInt(color.substring(16, 17), 16);
353                return 0xff000000 | (red << 16) | (green << 8) | blue;
354            }
355            return 0x00000000;
356        }
357        if (color.charAt(0) == '%') {
358            throw new ImageReadException("HSV colors are not implemented "
359                    + "even in the XPM specification!");
360        }
361        if ("None".equals(color)) {
362            return 0x00000000;
363        }
364        loadColorNames();
365        final String colorLowercase = color.toLowerCase(Locale.ENGLISH);
366        if (colorNames.containsKey(colorLowercase)) {
367            return colorNames.get(colorLowercase);
368        }
369        return 0x00000000;
370    }
371
372    private void populatePaletteEntry(final PaletteEntry paletteEntry, final String key, final String color) throws ImageReadException {
373        if ("m".equals(key)) {
374            paletteEntry.monoArgb = parseColor(color);
375            paletteEntry.haveMono = true;
376        } else if ("g4".equals(key)) {
377            paletteEntry.gray4LevelArgb = parseColor(color);
378            paletteEntry.haveGray4Level = true;
379        } else if ("g".equals(key)) {
380            paletteEntry.grayArgb = parseColor(color);
381            paletteEntry.haveGray = true;
382        } else if ("s".equals(key)) {
383            paletteEntry.colorArgb = parseColor(color);
384            paletteEntry.haveColor = true;
385        } else if ("c".equals(key)) {
386            paletteEntry.colorArgb = parseColor(color);
387            paletteEntry.haveColor = true;
388        }
389    }
390
391    private void parsePaletteEntries(final XpmHeader xpmHeader, final BasicCParser cParser)
392            throws IOException, ImageReadException {
393        final StringBuilder row = new StringBuilder();
394        for (int i = 0; i < xpmHeader.numColors; i++) {
395            row.setLength(0);
396            final boolean hasMore = parseNextString(cParser, row);
397            if (!hasMore) {
398                throw new ImageReadException("Parsing XPM file failed, " + "file ended while reading palette");
399            }
400            final String name = row.substring(0, xpmHeader.numCharsPerPixel);
401            final String[] tokens = BasicCParser.tokenizeRow(row.substring(xpmHeader.numCharsPerPixel));
402            final PaletteEntry paletteEntry = new PaletteEntry();
403            paletteEntry.index = i;
404            int previousKeyIndex = Integer.MIN_VALUE;
405            final StringBuilder colorBuffer = new StringBuilder();
406            for (int j = 0; j < tokens.length; j++) {
407                final String token = tokens[j];
408                boolean isKey = false;
409                if (previousKeyIndex < (j - 1)
410                        && "m".equals(token)
411                        || "g4".equals(token)
412                        || "g".equals(token)
413                        || "c".equals(token)
414                        || "s".equals(token)) {
415                    isKey = true;
416                }
417                if (isKey) {
418                    if (previousKeyIndex >= 0) {
419                        final String key = tokens[previousKeyIndex];
420                        final String color = colorBuffer.toString();
421                        colorBuffer.setLength(0);
422                        populatePaletteEntry(paletteEntry, key, color);
423                    }
424                    previousKeyIndex = j;
425                } else {
426                    if (previousKeyIndex < 0) {
427                        break;
428                    }
429                    if (colorBuffer.length() > 0) {
430                        colorBuffer.append(' ');
431                    }
432                    colorBuffer.append(token);
433                }
434            }
435            if (previousKeyIndex >= 0 && colorBuffer.length() > 0) {
436                final String key = tokens[previousKeyIndex];
437                final String color = colorBuffer.toString();
438                colorBuffer.setLength(0);
439                populatePaletteEntry(paletteEntry, key, color);
440            }
441            xpmHeader.palette.put(name, paletteEntry);
442        }
443    }
444
445    private XpmHeader parseXpmHeader(final BasicCParser cParser)
446            throws ImageReadException, IOException {
447        String name;
448        String token;
449        token = cParser.nextToken();
450        if (!"static".equals(token)) {
451            throw new ImageReadException(
452                    "Parsing XPM file failed, no 'static' token");
453        }
454        token = cParser.nextToken();
455        if (!"char".equals(token)) {
456            throw new ImageReadException(
457                    "Parsing XPM file failed, no 'char' token");
458        }
459        token = cParser.nextToken();
460        if (!"*".equals(token)) {
461            throw new ImageReadException(
462                    "Parsing XPM file failed, no '*' token");
463        }
464        name = cParser.nextToken();
465        if (name == null) {
466            throw new ImageReadException(
467                    "Parsing XPM file failed, no variable name");
468        }
469        if (name.charAt(0) != '_' && !Character.isLetter(name.charAt(0))) {
470            throw new ImageReadException(
471                    "Parsing XPM file failed, variable name "
472                            + "doesn't start with letter or underscore");
473        }
474        for (int i = 0; i < name.length(); i++) {
475            final char c = name.charAt(i);
476            if (!Character.isLetterOrDigit(c) && c != '_') {
477                throw new ImageReadException(
478                        "Parsing XPM file failed, variable name "
479                                + "contains non-letter non-digit non-underscore");
480            }
481        }
482        token = cParser.nextToken();
483        if (!"[".equals(token)) {
484            throw new ImageReadException(
485                    "Parsing XPM file failed, no '[' token");
486        }
487        token = cParser.nextToken();
488        if (!"]".equals(token)) {
489            throw new ImageReadException(
490                    "Parsing XPM file failed, no ']' token");
491        }
492        token = cParser.nextToken();
493        if (!"=".equals(token)) {
494            throw new ImageReadException(
495                    "Parsing XPM file failed, no '=' token");
496        }
497        token = cParser.nextToken();
498        if (!"{".equals(token)) {
499            throw new ImageReadException(
500                    "Parsing XPM file failed, no '{' token");
501        }
502
503        final StringBuilder row = new StringBuilder();
504        final boolean hasMore = parseNextString(cParser, row);
505        if (!hasMore) {
506            throw new ImageReadException("Parsing XPM file failed, "
507                    + "file too short");
508        }
509        final XpmHeader xpmHeader = parseXpmValuesSection(row.toString());
510        parsePaletteEntries(xpmHeader, cParser);
511        return xpmHeader;
512    }
513
514    private BufferedImage readXpmImage(final XpmHeader xpmHeader, final BasicCParser cParser)
515            throws ImageReadException, IOException {
516        ColorModel colorModel;
517        WritableRaster raster;
518        int bpp;
519        if (xpmHeader.palette.size() <= (1 << 8)) {
520            final int[] palette = new int[xpmHeader.palette.size()];
521            for (final Entry<Object, PaletteEntry> entry : xpmHeader.palette.entrySet()) {
522                final PaletteEntry paletteEntry = entry.getValue();
523                palette[paletteEntry.index] = paletteEntry.getBestARGB();
524            }
525            colorModel = new IndexColorModel(8, xpmHeader.palette.size(),
526                    palette, 0, true, -1, DataBuffer.TYPE_BYTE);
527            raster = Raster.createInterleavedRaster(
528                    DataBuffer.TYPE_BYTE, xpmHeader.width, xpmHeader.height, 1,
529                    null);
530            bpp = 8;
531        } else if (xpmHeader.palette.size() <= (1 << 16)) {
532            final int[] palette = new int[xpmHeader.palette.size()];
533            for (final Entry<Object, PaletteEntry> entry : xpmHeader.palette.entrySet()) {
534                final PaletteEntry paletteEntry = entry.getValue();
535                palette[paletteEntry.index] = paletteEntry.getBestARGB();
536            }
537            colorModel = new IndexColorModel(16, xpmHeader.palette.size(),
538                    palette, 0, true, -1, DataBuffer.TYPE_USHORT);
539            raster = Raster.createInterleavedRaster(
540                    DataBuffer.TYPE_USHORT, xpmHeader.width, xpmHeader.height,
541                    1, null);
542            bpp = 16;
543        } else {
544            colorModel = new DirectColorModel(32, 0x00ff0000, 0x0000ff00,
545                    0x000000ff, 0xff000000);
546            raster = Raster.createPackedRaster(DataBuffer.TYPE_INT,
547                    xpmHeader.width, xpmHeader.height, new int[] { 0x00ff0000,
548                            0x0000ff00, 0x000000ff, 0xff000000 }, null);
549            bpp = 32;
550        }
551
552        final BufferedImage image = new BufferedImage(colorModel, raster,
553                colorModel.isAlphaPremultiplied(), new Properties());
554        final DataBuffer dataBuffer = raster.getDataBuffer();
555        final StringBuilder row = new StringBuilder();
556        boolean hasMore = true;
557        for (int y = 0; y < xpmHeader.height; y++) {
558            row.setLength(0);
559            hasMore = parseNextString(cParser, row);
560            if (y < (xpmHeader.height - 1) && !hasMore) {
561                throw new ImageReadException("Parsing XPM file failed, "
562                        + "insufficient image rows in file");
563            }
564            final int rowOffset = y * xpmHeader.width;
565            for (int x = 0; x < xpmHeader.width; x++) {
566                final String index = row.substring(x * xpmHeader.numCharsPerPixel,
567                        (x + 1) * xpmHeader.numCharsPerPixel);
568                final PaletteEntry paletteEntry = xpmHeader.palette.get(index);
569                if (paletteEntry == null) {
570                    throw new ImageReadException(
571                            "No palette entry was defined " + "for " + index);
572                }
573                if (bpp <= 16) {
574                    dataBuffer.setElem(rowOffset + x, paletteEntry.index);
575                } else {
576                    dataBuffer.setElem(rowOffset + x,
577                            paletteEntry.getBestARGB());
578                }
579            }
580        }
581
582        while (hasMore) {
583            row.setLength(0);
584            hasMore = parseNextString(cParser, row);
585        }
586
587        final String token = cParser.nextToken();
588        if (!";".equals(token)) {
589            throw new ImageReadException("Last token wasn't ';'");
590        }
591
592        return image;
593    }
594
595    @Override
596    public boolean dumpImageFile(final PrintWriter pw, final ByteSource byteSource)
597            throws ImageReadException, IOException {
598        readXpmHeader(byteSource).dump(pw);
599        return true;
600    }
601
602    @Override
603    public final BufferedImage getBufferedImage(final ByteSource byteSource,
604            final XpmImagingParameters params) throws ImageReadException, IOException {
605        final XpmParseResult result = parseXpmHeader(byteSource);
606        return readXpmImage(result.xpmHeader, result.cParser);
607    }
608
609    private String randomName() {
610        final UUID uuid = UUID.randomUUID();
611        final StringBuilder stringBuilder = new StringBuilder("a");
612        long bits = uuid.getMostSignificantBits();
613        // Long.toHexString() breaks for very big numbers
614        for (int i = 64 - 8; i >= 0; i -= 8) {
615            stringBuilder.append(Integer.toHexString((int) ((bits >> i) & 0xff)));
616        }
617        bits = uuid.getLeastSignificantBits();
618        for (int i = 64 - 8; i >= 0; i -= 8) {
619            stringBuilder.append(Integer.toHexString((int) ((bits >> i) & 0xff)));
620        }
621        return stringBuilder.toString();
622    }
623
624    private String pixelsForIndex(int index, final int charsPerPixel) {
625        final StringBuilder stringBuilder = new StringBuilder();
626        int highestPower = 1;
627        for (int i = 1; i < charsPerPixel; i++) {
628            highestPower *= WRITE_PALETTE.length;
629        }
630        for (int i = 0; i < charsPerPixel; i++) {
631            final int multiple = index / highestPower;
632            index -= (multiple * highestPower);
633            highestPower /= WRITE_PALETTE.length;
634            stringBuilder.append(WRITE_PALETTE[multiple]);
635        }
636        return stringBuilder.toString();
637    }
638
639    private String toColor(final int color) {
640        final String hex = Integer.toHexString(color);
641        if (hex.length() < 6) {
642            final char[] zeroes = new char[6 - hex.length()];
643            Arrays.fill(zeroes, '0');
644            return "#" + new String(zeroes) + hex;
645        }
646        return "#" + hex;
647    }
648
649    @Override
650    public void writeImage(final BufferedImage src, final OutputStream os, XpmImagingParameters params)
651            throws ImageWriteException, IOException {
652        final PaletteFactory paletteFactory = new PaletteFactory();
653        final boolean hasTransparency = paletteFactory.hasTransparency(src, 1);
654        SimplePalette palette = null;
655        int maxColors = WRITE_PALETTE.length;
656        int charsPerPixel = 1;
657        while (palette == null) {
658            palette = paletteFactory.makeExactRgbPaletteSimple(src,
659                    hasTransparency ? maxColors - 1 : maxColors);
660
661            // leave the loop if numbers would go beyond Integer.MAX_VALUE to avoid infinite loops
662            // test every operation from below if it would increase an int value beyond Integer.MAX_VALUE
663            final long nextMaxColors = maxColors * WRITE_PALETTE.length;
664            final long nextCharsPerPixel = charsPerPixel + 1;
665            if (nextMaxColors > Integer.MAX_VALUE) {
666                throw new ImageWriteException("Xpm: Can't write images with more than Integer.MAX_VALUE colors.");
667            }
668            if (nextCharsPerPixel > Integer.MAX_VALUE) {
669                throw new ImageWriteException("Xpm: Can't write images with more than Integer.MAX_VALUE chars per pixel.");
670            }
671            // the code above makes sure that we never go beyond Integer.MAX_VALUE here
672            if (palette == null) {
673                maxColors *= WRITE_PALETTE.length;
674                charsPerPixel++;
675            }
676        }
677        int colors = palette.length();
678        if (hasTransparency) {
679            ++colors;
680        }
681
682        String line = "/* XPM */\n";
683        os.write(line.getBytes(StandardCharsets.US_ASCII));
684        line = "static char *" + randomName() + "[] = {\n";
685        os.write(line.getBytes(StandardCharsets.US_ASCII));
686        line = "\"" + src.getWidth() + " " + src.getHeight() + " " + colors
687                + " " + charsPerPixel + "\",\n";
688        os.write(line.getBytes(StandardCharsets.US_ASCII));
689
690        for (int i = 0; i < colors; i++) {
691            String color;
692            if (i < palette.length()) {
693                color = toColor(palette.getEntry(i));
694            } else {
695                color = "None";
696            }
697            line = "\"" + pixelsForIndex(i, charsPerPixel) + " c " + color
698                    + "\",\n";
699            os.write(line.getBytes(StandardCharsets.US_ASCII));
700        }
701
702        String separator = "";
703        for (int y = 0; y < src.getHeight(); y++) {
704            os.write(separator.getBytes(StandardCharsets.US_ASCII));
705            separator = ",\n";
706            line = "\"";
707            os.write(line.getBytes(StandardCharsets.US_ASCII));
708            for (int x = 0; x < src.getWidth(); x++) {
709                final int argb = src.getRGB(x, y);
710                if ((argb & 0xff000000) == 0) {
711                    line = pixelsForIndex(palette.length(), charsPerPixel);
712                } else {
713                    line = pixelsForIndex(
714                            palette.getPaletteIndex(0xffffff & argb),
715                            charsPerPixel);
716                }
717                os.write(line.getBytes(StandardCharsets.US_ASCII));
718            }
719            line = "\"";
720            os.write(line.getBytes(StandardCharsets.US_ASCII));
721        }
722
723        line = "\n};\n";
724        os.write(line.getBytes(StandardCharsets.US_ASCII));
725    }
726}