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}