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