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}