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