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}