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.AbstractBinaryOutputStream;
044import org.apache.commons.imaging.common.Allocator;
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    /**
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}