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