View Javadoc
1   /*
2    * Licensed to the Apache Software Foundation (ASF) under one or more
3    * contributor license agreements.  See the NOTICE file distributed with
4    * this work for additional information regarding copyright ownership.
5    * The ASF licenses this file to You under the Apache License, Version 2.0
6    * (the "License"); you may not use this file except in compliance with
7    * the License.  You may obtain a copy of the License at
8    *
9    *      http://www.apache.org/licenses/LICENSE-2.0
10   *
11   * Unless required by applicable law or agreed to in writing, software
12   * distributed under the License is distributed on an "AS IS" BASIS,
13   * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14   * See the License for the specific language governing permissions and
15   * limitations under the License.
16   */
17  package org.apache.commons.imaging.formats.ico;
18  
19  import static org.apache.commons.imaging.common.BinaryFunctions.read2Bytes;
20  import static org.apache.commons.imaging.common.BinaryFunctions.read4Bytes;
21  import static org.apache.commons.imaging.common.BinaryFunctions.readByte;
22  import static org.apache.commons.imaging.common.BinaryFunctions.readBytes;
23  
24  import java.awt.Dimension;
25  import java.awt.image.BufferedImage;
26  import java.io.ByteArrayInputStream;
27  import java.io.ByteArrayOutputStream;
28  import java.io.IOException;
29  import java.io.InputStream;
30  import java.io.OutputStream;
31  import java.io.PrintWriter;
32  import java.nio.ByteOrder;
33  import java.util.List;
34  
35  import org.apache.commons.imaging.AbstractImageParser;
36  import org.apache.commons.imaging.ImageFormat;
37  import org.apache.commons.imaging.ImageFormats;
38  import org.apache.commons.imaging.ImageInfo;
39  import org.apache.commons.imaging.Imaging;
40  import org.apache.commons.imaging.ImagingException;
41  import org.apache.commons.imaging.PixelDensity;
42  import org.apache.commons.imaging.bytesource.ByteSource;
43  import org.apache.commons.imaging.common.Allocator;
44  import org.apache.commons.imaging.common.BinaryOutputStream;
45  import org.apache.commons.imaging.common.ImageMetadata;
46  import org.apache.commons.imaging.formats.bmp.BmpImageParser;
47  import org.apache.commons.imaging.palette.PaletteFactory;
48  import org.apache.commons.imaging.palette.SimplePalette;
49  
50  public class IcoImageParser extends AbstractImageParser<IcoImagingParameters> {
51      private static final class BitmapHeader {
52          public final int size;
53          public final int width;
54          public final int height;
55          public final int planes;
56          public final int bitCount;
57          public final int compression;
58          public final int sizeImage;
59          public final int xPelsPerMeter;
60          public final int yPelsPerMeter;
61          public final int colorsUsed;
62          public final int colorsImportant;
63  
64          BitmapHeader(final int size, final int width, final int height, final int planes, final int bitCount, final int compression, final int sizeImage,
65                  final int pelsPerMeter, final int pelsPerMeter2, final int colorsUsed, final int colorsImportant) {
66              this.size = size;
67              this.width = width;
68              this.height = height;
69              this.planes = planes;
70              this.bitCount = bitCount;
71              this.compression = compression;
72              this.sizeImage = sizeImage;
73              xPelsPerMeter = pelsPerMeter;
74              yPelsPerMeter = pelsPerMeter2;
75              this.colorsUsed = colorsUsed;
76              this.colorsImportant = colorsImportant;
77          }
78  
79          public void dump(final PrintWriter pw) {
80              pw.println("BitmapHeader");
81  
82              pw.println("Size: " + size);
83              pw.println("Width: " + width);
84              pw.println("Height: " + height);
85              pw.println("Planes: " + planes);
86              pw.println("BitCount: " + bitCount);
87              pw.println("Compression: " + compression);
88              pw.println("SizeImage: " + sizeImage);
89              pw.println("XPelsPerMeter: " + xPelsPerMeter);
90              pw.println("YPelsPerMeter: " + yPelsPerMeter);
91              pw.println("ColorsUsed: " + colorsUsed);
92              pw.println("ColorsImportant: " + colorsImportant);
93          }
94      }
95  
96      private static final class BitmapIconData extends IconData {
97          public final BitmapHeader header;
98          public final BufferedImage bufferedImage;
99  
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 }