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