    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}