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