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}