001/* 002 * Licensed to the Apache Software Foundation (ASF) under one or more 003 * contributor license agreements. See the NOTICE file distributed with 004 * this work for additional information regarding copyright ownership. 005 * The ASF licenses this file to You under the Apache License, Version 2.0 006 * (the "License"); you may not use this file except in compliance with 007 * the License. You may obtain a copy of the License at 008 * 009 * http://www.apache.org/licenses/LICENSE-2.0 010 * 011 * Unless required by applicable law or agreed to in writing, software 012 * distributed under the License is distributed on an "AS IS" BASIS, 013 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 014 * See the License for the specific language governing permissions and 015 * limitations under the License. 016 */ 017package org.apache.commons.imaging.formats.ico; 018 019import static org.apache.commons.imaging.ImagingConstants.PARAM_KEY_FORMAT; 020import static org.apache.commons.imaging.ImagingConstants.PARAM_KEY_PIXEL_DENSITY; 021import static org.apache.commons.imaging.common.BinaryFunctions.read2Bytes; 022import static org.apache.commons.imaging.common.BinaryFunctions.read4Bytes; 023import static org.apache.commons.imaging.common.BinaryFunctions.readByte; 024import static org.apache.commons.imaging.common.BinaryFunctions.readBytes; 025 026import java.awt.Dimension; 027import java.awt.image.BufferedImage; 028import java.io.ByteArrayInputStream; 029import java.io.ByteArrayOutputStream; 030import java.io.IOException; 031import java.io.InputStream; 032import java.io.OutputStream; 033import java.io.PrintWriter; 034import java.nio.ByteOrder; 035import java.util.ArrayList; 036import java.util.HashMap; 037import java.util.List; 038import java.util.Map; 039 040import org.apache.commons.imaging.ImageFormat; 041import org.apache.commons.imaging.ImageFormats; 042import org.apache.commons.imaging.ImageInfo; 043import org.apache.commons.imaging.ImageParser; 044import org.apache.commons.imaging.ImageReadException; 045import org.apache.commons.imaging.ImageWriteException; 046import org.apache.commons.imaging.Imaging; 047import org.apache.commons.imaging.PixelDensity; 048import org.apache.commons.imaging.common.BinaryOutputStream; 049import org.apache.commons.imaging.common.ImageMetadata; 050import org.apache.commons.imaging.common.bytesource.ByteSource; 051import org.apache.commons.imaging.formats.bmp.BmpImageParser; 052import org.apache.commons.imaging.palette.PaletteFactory; 053import org.apache.commons.imaging.palette.SimplePalette; 054 055public class IcoImageParser extends ImageParser { 056 private static final String DEFAULT_EXTENSION = ".ico"; 057 private static final String[] ACCEPTED_EXTENSIONS = { ".ico", ".cur", }; 058 059 public IcoImageParser() { 060 super.setByteOrder(ByteOrder.LITTLE_ENDIAN); 061 } 062 063 @Override 064 public String getName() { 065 return "ico-Custom"; 066 } 067 068 @Override 069 public String getDefaultExtension() { 070 return DEFAULT_EXTENSION; 071 } 072 073 @Override 074 protected String[] getAcceptedExtensions() { 075 return ACCEPTED_EXTENSIONS; 076 } 077 078 @Override 079 protected ImageFormat[] getAcceptedTypes() { 080 return new ImageFormat[] { ImageFormats.ICO, // 081 }; 082 } 083 084 // TODO should throw UOE 085 @Override 086 public ImageMetadata getMetadata(final ByteSource byteSource, final Map<String, Object> params) 087 throws ImageReadException, IOException { 088 return null; 089 } 090 091 // TODO should throw UOE 092 @Override 093 public ImageInfo getImageInfo(final ByteSource byteSource, final Map<String, Object> params) 094 throws ImageReadException, IOException { 095 return null; 096 } 097 098 // TODO should throw UOE 099 @Override 100 public Dimension getImageSize(final ByteSource byteSource, final Map<String, Object> params) 101 throws ImageReadException, IOException { 102 return null; 103 } 104 105 // TODO should throw UOE 106 @Override 107 public byte[] getICCProfileBytes(final ByteSource byteSource, final Map<String, Object> params) 108 throws ImageReadException, IOException { 109 return null; 110 } 111 112 private static class FileHeader { 113 public final int reserved; // Reserved (2 bytes), always 0 114 public final int iconType; // IconType (2 bytes), if the image is an 115 // icon it?s 1, for cursors the value is 2. 116 public final int iconCount; // IconCount (2 bytes), number of icons in 117 // this file. 118 119 FileHeader(final int reserved, final int iconType, final int iconCount) { 120 this.reserved = reserved; 121 this.iconType = iconType; 122 this.iconCount = iconCount; 123 } 124 125 public void dump(final PrintWriter pw) { 126 pw.println("FileHeader"); 127 pw.println("Reserved: " + reserved); 128 pw.println("IconType: " + iconType); 129 pw.println("IconCount: " + iconCount); 130 pw.println(); 131 } 132 } 133 134 private FileHeader readFileHeader(final InputStream is) throws ImageReadException, IOException { 135 final int reserved = read2Bytes("Reserved", is, "Not a Valid ICO File", getByteOrder()); 136 final int iconType = read2Bytes("IconType", is, "Not a Valid ICO File", getByteOrder()); 137 final int iconCount = read2Bytes("IconCount", is, "Not a Valid ICO File", getByteOrder()); 138 139 if (reserved != 0) { 140 throw new ImageReadException("Not a Valid ICO File: reserved is " + reserved); 141 } 142 if (iconType != 1 && iconType != 2) { 143 throw new ImageReadException("Not a Valid ICO File: icon type is " + iconType); 144 } 145 146 return new FileHeader(reserved, iconType, iconCount); 147 148 } 149 150 private static class IconInfo { 151 public final byte width; 152 public final byte height; 153 public final byte colorCount; 154 public final byte reserved; 155 public final int planes; 156 public final int bitCount; 157 public final int imageSize; 158 public final int imageOffset; 159 160 IconInfo(final byte width, final byte height, 161 final byte colorCount, final byte reserved, final int planes, 162 final int bitCount, final int imageSize, final int imageOffset) { 163 this.width = width; 164 this.height = height; 165 this.colorCount = colorCount; 166 this.reserved = reserved; 167 this.planes = planes; 168 this.bitCount = bitCount; 169 this.imageSize = imageSize; 170 this.imageOffset = imageOffset; 171 } 172 173 public void dump(final PrintWriter pw) { 174 pw.println("IconInfo"); 175 pw.println("Width: " + width); 176 pw.println("Height: " + height); 177 pw.println("ColorCount: " + colorCount); 178 pw.println("Reserved: " + reserved); 179 pw.println("Planes: " + planes); 180 pw.println("BitCount: " + bitCount); 181 pw.println("ImageSize: " + imageSize); 182 pw.println("ImageOffset: " + imageOffset); 183 } 184 } 185 186 private IconInfo readIconInfo(final InputStream is) throws IOException { 187 // Width (1 byte), Width of Icon (1 to 255) 188 final byte width = readByte("Width", is, "Not a Valid ICO File"); 189 // Height (1 byte), Height of Icon (1 to 255) 190 final byte height = readByte("Height", is, "Not a Valid ICO File"); 191 // ColorCount (1 byte), Number of colors, either 192 // 0 for 24 bit or higher, 193 // 2 for monochrome or 16 for 16 color images. 194 final byte colorCount = readByte("ColorCount", is, "Not a Valid ICO File"); 195 // Reserved (1 byte), Not used (always 0) 196 final byte reserved = readByte("Reserved", is, "Not a Valid ICO File"); 197 // Planes (2 bytes), always 1 198 final int planes = read2Bytes("Planes", is, "Not a Valid ICO File", getByteOrder()); 199 // BitCount (2 bytes), number of bits per pixel (1 for monochrome, 200 // 4 for 16 colors, 8 for 256 colors, 24 for true colors, 201 // 32 for true colors + alpha channel) 202 final int bitCount = read2Bytes("BitCount", is, "Not a Valid ICO File", getByteOrder()); 203 // ImageSize (4 bytes), Length of resource in bytes 204 final int imageSize = read4Bytes("ImageSize", is, "Not a Valid ICO File", getByteOrder()); 205 // ImageOffset (4 bytes), start of the image in the file 206 final int imageOffset = read4Bytes("ImageOffset", is, "Not a Valid ICO File", getByteOrder()); 207 208 return new IconInfo(width, height, colorCount, reserved, planes, bitCount, imageSize, imageOffset); 209 } 210 211 private static class BitmapHeader { 212 public final int size; 213 public final int width; 214 public final int height; 215 public final int planes; 216 public final int bitCount; 217 public final int compression; 218 public final int sizeImage; 219 public final int xPelsPerMeter; 220 public final int yPelsPerMeter; 221 public final int colorsUsed; 222 public final int colorsImportant; 223 224 BitmapHeader(final int size, final int width, final int height, 225 final int planes, final int bitCount, final int compression, 226 final int sizeImage, final int pelsPerMeter, 227 final int pelsPerMeter2, final int colorsUsed, 228 final int colorsImportant) { 229 this.size = size; 230 this.width = width; 231 this.height = height; 232 this.planes = planes; 233 this.bitCount = bitCount; 234 this.compression = compression; 235 this.sizeImage = sizeImage; 236 xPelsPerMeter = pelsPerMeter; 237 yPelsPerMeter = pelsPerMeter2; 238 this.colorsUsed = colorsUsed; 239 this.colorsImportant = colorsImportant; 240 } 241 242 public void dump(final PrintWriter pw) { 243 pw.println("BitmapHeader"); 244 245 pw.println("Size: " + size); 246 pw.println("Width: " + width); 247 pw.println("Height: " + height); 248 pw.println("Planes: " + planes); 249 pw.println("BitCount: " + bitCount); 250 pw.println("Compression: " + compression); 251 pw.println("SizeImage: " + sizeImage); 252 pw.println("XPelsPerMeter: " + xPelsPerMeter); 253 pw.println("YPelsPerMeter: " + yPelsPerMeter); 254 pw.println("ColorsUsed: " + colorsUsed); 255 pw.println("ColorsImportant: " + colorsImportant); 256 } 257 } 258 259 private abstract static class IconData { 260 public final IconInfo iconInfo; 261 262 IconData(final IconInfo iconInfo) { 263 this.iconInfo = iconInfo; 264 } 265 266 public void dump(final PrintWriter pw) { 267 iconInfo.dump(pw); 268 pw.println(); 269 dumpSubclass(pw); 270 } 271 272 protected abstract void dumpSubclass(PrintWriter pw); 273 274 public abstract BufferedImage readBufferedImage() 275 throws ImageReadException; 276 } 277 278 private static class BitmapIconData extends IconData { 279 public final BitmapHeader header; 280 public final BufferedImage bufferedImage; 281 282 BitmapIconData(final IconInfo iconInfo, 283 final BitmapHeader header, final BufferedImage bufferedImage) { 284 super(iconInfo); 285 this.header = header; 286 this.bufferedImage = bufferedImage; 287 } 288 289 @Override 290 public BufferedImage readBufferedImage() throws ImageReadException { 291 return bufferedImage; 292 } 293 294 @Override 295 protected void dumpSubclass(final PrintWriter pw) { 296 pw.println("BitmapIconData"); 297 header.dump(pw); 298 pw.println(); 299 } 300 } 301 302 private static class PNGIconData extends IconData { 303 public final BufferedImage bufferedImage; 304 305 PNGIconData(final IconInfo iconInfo, 306 final BufferedImage bufferedImage) { 307 super(iconInfo); 308 this.bufferedImage = bufferedImage; 309 } 310 311 @Override 312 public BufferedImage readBufferedImage() { 313 return bufferedImage; 314 } 315 316 @Override 317 protected void dumpSubclass(final PrintWriter pw) { 318 pw.println("PNGIconData"); 319 pw.println(); 320 } 321 } 322 323 private IconData readBitmapIconData(final byte[] iconData, final IconInfo fIconInfo) 324 throws ImageReadException, IOException { 325 final ByteArrayInputStream is = new ByteArrayInputStream(iconData); 326 final int size = read4Bytes("size", is, "Not a Valid ICO File", getByteOrder()); // Size (4 327 // bytes), 328 // size of 329 // this 330 // structure 331 // (always 332 // 40) 333 final int width = read4Bytes("width", is, "Not a Valid ICO File", getByteOrder()); // Width (4 334 // bytes), 335 // width of 336 // the 337 // image 338 // (same as 339 // iconinfo.width) 340 final int height = read4Bytes("height", is, "Not a Valid ICO File", getByteOrder()); // Height 341 // (4 342 // bytes), 343 // scanlines 344 // in the 345 // color 346 // map + 347 // transparent 348 // map 349 // (iconinfo.height 350 // * 2) 351 final int planes = read2Bytes("planes", is, "Not a Valid ICO File", getByteOrder()); // Planes 352 // (2 353 // bytes), 354 // always 355 // 1 356 final int bitCount = read2Bytes("bitCount", is, "Not a Valid ICO File", getByteOrder()); // BitCount 357 // (2 358 // bytes), 359 // 1,4,8,16,24,32 360 // (see 361 // iconinfo 362 // for 363 // details) 364 int compression = read4Bytes("compression", is, "Not a Valid ICO File", getByteOrder()); // Compression 365 // (4 366 // bytes), 367 // we 368 // don?t 369 // use 370 // this 371 // (0) 372 final int sizeImage = read4Bytes("sizeImage", is, "Not a Valid ICO File", getByteOrder()); // SizeImage 373 // (4 374 // bytes), 375 // we 376 // don?t 377 // use 378 // this 379 // (0) 380 final int xPelsPerMeter = read4Bytes("xPelsPerMeter", is, 381 "Not a Valid ICO File", getByteOrder()); // XPelsPerMeter (4 bytes), we don?t 382 // use this (0) 383 final int yPelsPerMeter = read4Bytes("yPelsPerMeter", is, 384 "Not a Valid ICO File", getByteOrder()); // YPelsPerMeter (4 bytes), we don?t 385 // use this (0) 386 final int colorsUsed = read4Bytes("colorsUsed", is, "Not a Valid ICO File", getByteOrder()); // ColorsUsed 387 // (4 388 // bytes), 389 // we 390 // don?t 391 // use 392 // this 393 // (0) 394 final int colorsImportant = read4Bytes("ColorsImportant", is, 395 "Not a Valid ICO File", getByteOrder()); // ColorsImportant (4 bytes), we don?t 396 // use this (0) 397 int redMask = 0; 398 int greenMask = 0; 399 int blueMask = 0; 400 int alphaMask = 0; 401 if (compression == 3) { 402 redMask = read4Bytes("redMask", is, "Not a Valid ICO File", getByteOrder()); 403 greenMask = read4Bytes("greenMask", is, "Not a Valid ICO File", getByteOrder()); 404 blueMask = read4Bytes("blueMask", is, "Not a Valid ICO File", getByteOrder()); 405 } 406 final byte[] restOfFile = readBytes("RestOfFile", is, is.available()); 407 408 if (size != 40) { 409 throw new ImageReadException("Not a Valid ICO File: Wrong bitmap header size " + size); 410 } 411 if (planes != 1) { 412 throw new ImageReadException("Not a Valid ICO File: Planes can't be " + planes); 413 } 414 415 if (compression == 0 && bitCount == 32) { 416 // 32 BPP RGB icons need an alpha channel, but BMP files don't have 417 // one unless BI_BITFIELDS is used... 418 compression = 3; 419 redMask = 0x00ff0000; 420 greenMask = 0x0000ff00; 421 blueMask = 0x000000ff; 422 alphaMask = 0xff000000; 423 } 424 425 final BitmapHeader header = new BitmapHeader(size, width, height, planes, 426 bitCount, compression, sizeImage, xPelsPerMeter, yPelsPerMeter, 427 colorsUsed, colorsImportant); 428 429 final int bitmapPixelsOffset = 14 + 56 + 4 * ((colorsUsed == 0 && bitCount <= 8) ? (1 << bitCount) 430 : colorsUsed); 431 final int bitmapSize = 14 + 56 + restOfFile.length; 432 433 final ByteArrayOutputStream baos = new ByteArrayOutputStream(bitmapSize); 434 try (BinaryOutputStream bos = new BinaryOutputStream(baos, ByteOrder.LITTLE_ENDIAN)) { 435 bos.write('B'); 436 bos.write('M'); 437 bos.write4Bytes(bitmapSize); 438 bos.write4Bytes(0); 439 bos.write4Bytes(bitmapPixelsOffset); 440 441 bos.write4Bytes(56); 442 bos.write4Bytes(width); 443 bos.write4Bytes(height / 2); 444 bos.write2Bytes(planes); 445 bos.write2Bytes(bitCount); 446 bos.write4Bytes(compression); 447 bos.write4Bytes(sizeImage); 448 bos.write4Bytes(xPelsPerMeter); 449 bos.write4Bytes(yPelsPerMeter); 450 bos.write4Bytes(colorsUsed); 451 bos.write4Bytes(colorsImportant); 452 bos.write4Bytes(redMask); 453 bos.write4Bytes(greenMask); 454 bos.write4Bytes(blueMask); 455 bos.write4Bytes(alphaMask); 456 bos.write(restOfFile); 457 bos.flush(); 458 } 459 460 final ByteArrayInputStream bmpInputStream = new ByteArrayInputStream(baos.toByteArray()); 461 final BufferedImage bmpImage = new BmpImageParser().getBufferedImage(bmpInputStream, null); 462 463 // Transparency map is optional with 32 BPP icons, because they already 464 // have 465 // an alpha channel, and Windows only uses the transparency map when it 466 // has to 467 // display the icon on a < 32 BPP screen. But it's still used instead of 468 // alpha 469 // if the image would be completely transparent with alpha... 470 int t_scanline_size = (width + 7) / 8; 471 if ((t_scanline_size % 4) != 0) { 472 t_scanline_size += 4 - (t_scanline_size % 4); // pad scanline to 4 473 // byte size. 474 } 475 final int colorMapSizeBytes = t_scanline_size * (height / 2); 476 byte[] transparencyMap = null; 477 try { 478 transparencyMap = readBytes("transparency_map", 479 bmpInputStream, colorMapSizeBytes, 480 "Not a Valid ICO File"); 481 } catch (final IOException ioEx) { 482 if (bitCount != 32) { 483 throw ioEx; 484 } 485 } 486 487 boolean allAlphasZero = true; 488 if (bitCount == 32) { 489 for (int y = 0; allAlphasZero && y < bmpImage.getHeight(); y++) { 490 for (int x = 0; x < bmpImage.getWidth(); x++) { 491 if ((bmpImage.getRGB(x, y) & 0xff000000) != 0) { 492 allAlphasZero = false; 493 break; 494 } 495 } 496 } 497 } 498 BufferedImage resultImage; 499 if (allAlphasZero) { 500 resultImage = new BufferedImage(bmpImage.getWidth(), 501 bmpImage.getHeight(), BufferedImage.TYPE_INT_ARGB); 502 for (int y = 0; y < resultImage.getHeight(); y++) { 503 for (int x = 0; x < resultImage.getWidth(); x++) { 504 int alpha = 0xff; 505 if (transparencyMap != null) { 506 final int alphaByte = 0xff & transparencyMap[t_scanline_size 507 * (bmpImage.getHeight() - y - 1) + (x / 8)]; 508 alpha = 0x01 & (alphaByte >> (7 - (x % 8))); 509 alpha = (alpha == 0) ? 0xff : 0x00; 510 } 511 resultImage.setRGB(x, y, (alpha << 24) 512 | (0xffffff & bmpImage.getRGB(x, y))); 513 } 514 } 515 } else { 516 resultImage = bmpImage; 517 } 518 return new BitmapIconData(fIconInfo, header, resultImage); 519 } 520 521 private IconData readIconData(final byte[] iconData, final IconInfo fIconInfo) 522 throws ImageReadException, IOException { 523 final ImageFormat imageFormat = Imaging.guessFormat(iconData); 524 if (imageFormat.equals(ImageFormats.PNG)) { 525 final BufferedImage bufferedImage = Imaging.getBufferedImage(iconData); 526 return new PNGIconData(fIconInfo, bufferedImage); 527 } 528 return readBitmapIconData(iconData, fIconInfo); 529 } 530 531 private static class ImageContents { 532 public final FileHeader fileHeader; 533 public final IconData[] iconDatas; 534 535 ImageContents(final FileHeader fileHeader, final IconData[] iconDatas) { 536 super(); 537 this.fileHeader = fileHeader; 538 this.iconDatas = iconDatas; 539 } 540 } 541 542 private ImageContents readImage(final ByteSource byteSource) 543 throws ImageReadException, IOException { 544 try (InputStream is = byteSource.getInputStream()) { 545 final FileHeader fileHeader = readFileHeader(is); 546 547 final IconInfo[] fIconInfos = new IconInfo[fileHeader.iconCount]; 548 for (int i = 0; i < fileHeader.iconCount; i++) { 549 fIconInfos[i] = readIconInfo(is); 550 } 551 552 final IconData[] fIconDatas = new IconData[fileHeader.iconCount]; 553 for (int i = 0; i < fileHeader.iconCount; i++) { 554 final byte[] iconData = byteSource.getBlock( 555 fIconInfos[i].imageOffset, fIconInfos[i].imageSize); 556 fIconDatas[i] = readIconData(iconData, fIconInfos[i]); 557 } 558 559 final ImageContents ret = new ImageContents(fileHeader, fIconDatas); 560 return ret; 561 } 562 } 563 564 @Override 565 public boolean dumpImageFile(final PrintWriter pw, final ByteSource byteSource) 566 throws ImageReadException, IOException { 567 final ImageContents contents = readImage(byteSource); 568 contents.fileHeader.dump(pw); 569 for (final IconData iconData : contents.iconDatas) { 570 iconData.dump(pw); 571 } 572 return true; 573 } 574 575 @Override 576 public final BufferedImage getBufferedImage(final ByteSource byteSource, 577 final Map<String, Object> params) throws ImageReadException, IOException { 578 final ImageContents contents = readImage(byteSource); 579 final FileHeader fileHeader = contents.fileHeader; 580 if (fileHeader.iconCount > 0) { 581 return contents.iconDatas[0].readBufferedImage(); 582 } 583 throw new ImageReadException("No icons in ICO file"); 584 } 585 586 @Override 587 public List<BufferedImage> getAllBufferedImages(final ByteSource byteSource) 588 throws ImageReadException, IOException { 589 final ImageContents contents = readImage(byteSource); 590 591 final FileHeader fileHeader = contents.fileHeader; 592 final List<BufferedImage> result = new ArrayList<>(fileHeader.iconCount); 593 for (int i = 0; i < fileHeader.iconCount; i++) { 594 final IconData iconData = contents.iconDatas[i]; 595 596 final BufferedImage image = iconData.readBufferedImage(); 597 598 result.add(image); 599 } 600 601 return result; 602 } 603 604 // public boolean extractImages(ByteSource byteSource, File dst_dir, 605 // String dst_root, ImageParser encoder) throws ImageReadException, 606 // IOException, ImageWriteException 607 // { 608 // ImageContents contents = readImage(byteSource); 609 // 610 // FileHeader fileHeader = contents.fileHeader; 611 // for (int i = 0; i < fileHeader.iconCount; i++) 612 // { 613 // IconData iconData = contents.iconDatas[i]; 614 // 615 // BufferedImage image = readBufferedImage(iconData); 616 // 617 // int size = Math.max(iconData.iconInfo.Width, 618 // iconData.iconInfo.Height); 619 // File file = new File(dst_dir, dst_root + "_" + size + "_" 620 // + iconData.iconInfo.BitCount 621 // + encoder.getDefaultExtension()); 622 // encoder.writeImage(image, new FileOutputStream(file), null); 623 // } 624 // 625 // return true; 626 // } 627 628 @Override 629 public void writeImage(final BufferedImage src, final OutputStream os, Map<String, Object> params) 630 throws ImageWriteException, IOException { 631 // make copy of params; we'll clear keys as we consume them. 632 params = (params == null) ? new HashMap<>() : new HashMap<>(params); 633 634 // clear format key. 635 if (params.containsKey(PARAM_KEY_FORMAT)) { 636 params.remove(PARAM_KEY_FORMAT); 637 } 638 639 final PixelDensity pixelDensity = (PixelDensity) params.remove(PARAM_KEY_PIXEL_DENSITY); 640 641 if (!params.isEmpty()) { 642 final Object firstKey = params.keySet().iterator().next(); 643 throw new ImageWriteException("Unknown parameter: " + firstKey); 644 } 645 646 final PaletteFactory paletteFactory = new PaletteFactory(); 647 final SimplePalette palette = paletteFactory.makeExactRgbPaletteSimple(src, 256); 648 final int bitCount; 649 // If we can't obtain an exact rgb palette, we set the bit count to either 24 or 32 650 // so there is a relation between having a palette and the bit count. 651 if (palette == null) { 652 final boolean hasTransparency = paletteFactory.hasTransparency(src); 653 if (hasTransparency) { 654 bitCount = 32; 655 } else { 656 bitCount = 24; 657 } 658 } else if (palette.length() <= 2) { 659 bitCount = 1; 660 } else if (palette.length() <= 16) { 661 bitCount = 4; 662 } else { 663 bitCount = 8; 664 } 665 666 final BinaryOutputStream bos = new BinaryOutputStream(os, ByteOrder.LITTLE_ENDIAN); 667 668 int scanline_size = (bitCount * src.getWidth() + 7) / 8; 669 if ((scanline_size % 4) != 0) { 670 scanline_size += 4 - (scanline_size % 4); // pad scanline to 4 byte 671 // size. 672 } 673 int t_scanline_size = (src.getWidth() + 7) / 8; 674 if ((t_scanline_size % 4) != 0) { 675 t_scanline_size += 4 - (t_scanline_size % 4); // pad scanline to 4 676 // byte size. 677 } 678 final int imageSize = 40 + 4 * (bitCount <= 8 ? (1 << bitCount) : 0) 679 + src.getHeight() * scanline_size + src.getHeight() 680 * t_scanline_size; 681 682 // ICONDIR 683 bos.write2Bytes(0); // reserved 684 bos.write2Bytes(1); // 1=ICO, 2=CUR 685 bos.write2Bytes(1); // count 686 687 // ICONDIRENTRY 688 int iconDirEntryWidth = src.getWidth(); 689 int iconDirEntryHeight = src.getHeight(); 690 if (iconDirEntryWidth > 255 || iconDirEntryHeight > 255) { 691 iconDirEntryWidth = 0; 692 iconDirEntryHeight = 0; 693 } 694 bos.write(iconDirEntryWidth); 695 bos.write(iconDirEntryHeight); 696 bos.write((bitCount >= 8) ? 0 : (1 << bitCount)); 697 bos.write(0); // reserved 698 bos.write2Bytes(1); // color planes 699 bos.write2Bytes(bitCount); 700 bos.write4Bytes(imageSize); 701 bos.write4Bytes(22); // image offset 702 703 // BITMAPINFOHEADER 704 bos.write4Bytes(40); // size 705 bos.write4Bytes(src.getWidth()); 706 bos.write4Bytes(2 * src.getHeight()); 707 bos.write2Bytes(1); // planes 708 bos.write2Bytes(bitCount); 709 bos.write4Bytes(0); // compression 710 bos.write4Bytes(0); // image size 711 bos.write4Bytes(pixelDensity == null ? 0 : (int) Math.round(pixelDensity.horizontalDensityMetres())); // x pixels per meter 712 bos.write4Bytes(pixelDensity == null ? 0 : (int) Math.round(pixelDensity.horizontalDensityMetres())); // y pixels per meter 713 bos.write4Bytes(0); // colors used, 0 = (1 << bitCount) (ignored) 714 bos.write4Bytes(0); // colors important 715 716 if (palette != null) { 717 for (int i = 0; i < (1 << bitCount); i++) { 718 if (i < palette.length()) { 719 final int argb = palette.getEntry(i); 720 bos.write3Bytes(argb); 721 bos.write(0); 722 } else { 723 bos.write4Bytes(0); 724 } 725 } 726 } 727 728 int bitCache = 0; 729 int bitsInCache = 0; 730 final int rowPadding = scanline_size - (bitCount * src.getWidth() + 7) / 8; 731 for (int y = src.getHeight() - 1; y >= 0; y--) { 732 for (int x = 0; x < src.getWidth(); x++) { 733 final int argb = src.getRGB(x, y); 734 // Remember there is a relation between having a rgb palette and the bit count, see above comment 735 if (palette == null) { 736 if (bitCount == 24) { 737 bos.write3Bytes(argb); 738 } else if (bitCount == 32) { 739 bos.write4Bytes(argb); 740 } 741 } else { 742 if (bitCount < 8) { 743 final int rgb = 0xffffff & argb; 744 final int index = palette.getPaletteIndex(rgb); 745 bitCache <<= bitCount; 746 bitCache |= index; 747 bitsInCache += bitCount; 748 if (bitsInCache >= 8) { 749 bos.write(0xff & bitCache); 750 bitCache = 0; 751 bitsInCache = 0; 752 } 753 } else if (bitCount == 8) { 754 final int rgb = 0xffffff & argb; 755 final int index = palette.getPaletteIndex(rgb); 756 bos.write(0xff & index); 757 } 758 } 759 } 760 761 if (bitsInCache > 0) { 762 bitCache <<= (8 - bitsInCache); 763 bos.write(0xff & bitCache); 764 bitCache = 0; 765 bitsInCache = 0; 766 } 767 768 for (int x = 0; x < rowPadding; x++) { 769 bos.write(0); 770 } 771 } 772 773 final int t_row_padding = t_scanline_size - (src.getWidth() + 7) / 8; 774 for (int y = src.getHeight() - 1; y >= 0; y--) { 775 for (int x = 0; x < src.getWidth(); x++) { 776 final int argb = src.getRGB(x, y); 777 final int alpha = 0xff & (argb >> 24); 778 bitCache <<= 1; 779 if (alpha == 0) { 780 bitCache |= 1; 781 } 782 bitsInCache++; 783 if (bitsInCache >= 8) { 784 bos.write(0xff & bitCache); 785 bitCache = 0; 786 bitsInCache = 0; 787 } 788 } 789 790 if (bitsInCache > 0) { 791 bitCache <<= (8 - bitsInCache); 792 bos.write(0xff & bitCache); 793 bitCache = 0; 794 bitsInCache = 0; 795 } 796 797 for (int x = 0; x < t_row_padding; x++) { 798 bos.write(0); 799 } 800 } 801 bos.close(); 802 } 803}