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