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