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