View Javadoc
1   /*
2    * Licensed to the Apache Software Foundation (ASF) under one or more
3    * contributor license agreements.  See the NOTICE file distributed with
4    * this work for additional information regarding copyright ownership.
5    * The ASF licenses this file to You under the Apache License, Version 2.0
6    * (the "License"); you may not use this file except in compliance with
7    * the License.  You may obtain a copy of the License at
8    *
9    *      http://www.apache.org/licenses/LICENSE-2.0
10   *
11   * Unless required by applicable law or agreed to in writing, software
12   * distributed under the License is distributed on an "AS IS" BASIS,
13   * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14   * See the License for the specific language governing permissions and
15   * limitations under the License.
16   */
17  package org.apache.commons.imaging.formats.ico;
18  
19  import static org.apache.commons.imaging.ImagingConstants.PARAM_KEY_FORMAT;
20  import static org.apache.commons.imaging.ImagingConstants.PARAM_KEY_PIXEL_DENSITY;
21  import static org.apache.commons.imaging.common.BinaryFunctions.read2Bytes;
22  import static org.apache.commons.imaging.common.BinaryFunctions.read4Bytes;
23  import static org.apache.commons.imaging.common.BinaryFunctions.readByte;
24  import static org.apache.commons.imaging.common.BinaryFunctions.readBytes;
25  
26  import java.awt.Dimension;
27  import java.awt.image.BufferedImage;
28  import java.io.ByteArrayInputStream;
29  import java.io.ByteArrayOutputStream;
30  import java.io.IOException;
31  import java.io.InputStream;
32  import java.io.OutputStream;
33  import java.io.PrintWriter;
34  import java.nio.ByteOrder;
35  import java.util.ArrayList;
36  import java.util.HashMap;
37  import java.util.List;
38  import java.util.Map;
39  
40  import org.apache.commons.imaging.ImageFormat;
41  import org.apache.commons.imaging.ImageFormats;
42  import org.apache.commons.imaging.ImageInfo;
43  import org.apache.commons.imaging.ImageParser;
44  import org.apache.commons.imaging.ImageReadException;
45  import org.apache.commons.imaging.ImageWriteException;
46  import org.apache.commons.imaging.Imaging;
47  import org.apache.commons.imaging.PixelDensity;
48  import org.apache.commons.imaging.common.BinaryOutputStream;
49  import org.apache.commons.imaging.common.ImageMetadata;
50  import org.apache.commons.imaging.common.bytesource.ByteSource;
51  import org.apache.commons.imaging.formats.bmp.BmpImageParser;
52  import org.apache.commons.imaging.palette.PaletteFactory;
53  import org.apache.commons.imaging.palette.SimplePalette;
54  
55  public class IcoImageParser extends ImageParser {
56      private static final String DEFAULT_EXTENSION = ".ico";
57      private static final String[] ACCEPTED_EXTENSIONS = { ".ico", ".cur", };
58  
59      public IcoImageParser() {
60          super.setByteOrder(ByteOrder.LITTLE_ENDIAN);
61      }
62  
63      @Override
64      public String getName() {
65          return "ico-Custom";
66      }
67  
68      @Override
69      public String getDefaultExtension() {
70          return DEFAULT_EXTENSION;
71      }
72  
73      @Override
74      protected String[] getAcceptedExtensions() {
75          return ACCEPTED_EXTENSIONS;
76      }
77  
78      @Override
79      protected ImageFormat[] getAcceptedTypes() {
80          return new ImageFormat[] { ImageFormats.ICO, //
81          };
82      }
83  
84      // TODO should throw UOE
85      @Override
86      public ImageMetadata getMetadata(final ByteSource byteSource, final Map<String, Object> params)
87              throws ImageReadException, IOException {
88          return null;
89      }
90  
91      // TODO should throw UOE
92      @Override
93      public ImageInfo getImageInfo(final ByteSource byteSource, final Map<String, Object> params)
94              throws ImageReadException, IOException {
95          return null;
96      }
97  
98      // TODO should throw UOE
99      @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         public 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         public 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 monchrome,
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         public 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 static abstract class IconData {
260         public final IconInfo iconInfo;
261 
262         public 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         public 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         public 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         public ImageContents(final FileHeader fileHeader,
536                 final IconData[] iconDatas) {
537             super();
538             this.fileHeader = fileHeader;
539             this.iconDatas = iconDatas;
540         }
541     }
542 
543     private ImageContents readImage(final ByteSource byteSource)
544             throws ImageReadException, IOException {
545         try (InputStream is = byteSource.getInputStream()) {
546             final FileHeader fileHeader = readFileHeader(is);
547 
548             final IconInfo[] fIconInfos = new IconInfo[fileHeader.iconCount];
549             for (int i = 0; i < fileHeader.iconCount; i++) {
550                 fIconInfos[i] = readIconInfo(is);
551             }
552 
553             final IconData[] fIconDatas = new IconData[fileHeader.iconCount];
554             for (int i = 0; i < fileHeader.iconCount; i++) {
555                 final byte[] iconData = byteSource.getBlock(
556                         fIconInfos[i].imageOffset, fIconInfos[i].imageSize);
557                 fIconDatas[i] = readIconData(iconData, fIconInfos[i]);
558             }
559 
560             final ImageContents ret = new ImageContents(fileHeader, fIconDatas);
561             return ret;
562         }
563     }
564 
565     @Override
566     public boolean dumpImageFile(final PrintWriter pw, final ByteSource byteSource)
567             throws ImageReadException, IOException {
568         final ImageContents contents = readImage(byteSource);
569         contents.fileHeader.dump(pw);
570         for (final IconData iconData : contents.iconDatas) {
571             iconData.dump(pw);
572         }
573         return true;
574     }
575 
576     @Override
577     public final BufferedImage getBufferedImage(final ByteSource byteSource,
578             final Map<String, Object> params) throws ImageReadException, IOException {
579         final ImageContents contents = readImage(byteSource);
580         final FileHeader fileHeader = contents.fileHeader;
581         if (fileHeader.iconCount > 0) {
582             return contents.iconDatas[0].readBufferedImage();
583         }
584         throw new ImageReadException("No icons in ICO file");
585     }
586 
587     @Override
588     public List<BufferedImage> getAllBufferedImages(final ByteSource byteSource)
589             throws ImageReadException, IOException {
590         final List<BufferedImage> result = new ArrayList<>();
591         final ImageContents contents = readImage(byteSource);
592 
593         final FileHeader fileHeader = contents.fileHeader;
594         for (int i = 0; i < fileHeader.iconCount; i++) {
595             final IconData iconData = contents.iconDatas[i];
596 
597             final BufferedImage image = iconData.readBufferedImage();
598 
599             result.add(image);
600         }
601 
602         return result;
603     }
604 
605     // public boolean extractImages(ByteSource byteSource, File dst_dir,
606     // String dst_root, ImageParser encoder) throws ImageReadException,
607     // IOException, ImageWriteException
608     // {
609     // ImageContents contents = readImage(byteSource);
610     //
611     // FileHeader fileHeader = contents.fileHeader;
612     // for (int i = 0; i < fileHeader.iconCount; i++)
613     // {
614     // IconData iconData = contents.iconDatas[i];
615     //
616     // BufferedImage image = readBufferedImage(iconData);
617     //
618     // int size = Math.max(iconData.iconInfo.Width,
619     // iconData.iconInfo.Height);
620     // File file = new File(dst_dir, dst_root + "_" + size + "_"
621     // + iconData.iconInfo.BitCount
622     // + encoder.getDefaultExtension());
623     // encoder.writeImage(image, new FileOutputStream(file), null);
624     // }
625     //
626     // return true;
627     // }
628 
629     @Override
630     public void writeImage(final BufferedImage src, final OutputStream os, Map<String, Object> params)
631             throws ImageWriteException, IOException {
632         // make copy of params; we'll clear keys as we consume them.
633         params = (params == null) ? new HashMap<String, Object>() : new HashMap<>(params);
634 
635         // clear format key.
636         if (params.containsKey(PARAM_KEY_FORMAT)) {
637             params.remove(PARAM_KEY_FORMAT);
638         }
639         
640         final PixelDensity pixelDensity = (PixelDensity) params.remove(PARAM_KEY_PIXEL_DENSITY);
641 
642         if (!params.isEmpty()) {
643             final Object firstKey = params.keySet().iterator().next();
644             throw new ImageWriteException("Unknown parameter: " + firstKey);
645         }
646 
647         final PaletteFactory paletteFactory = new PaletteFactory();
648         final SimplePalette palette = paletteFactory.makeExactRgbPaletteSimple(src, 256);
649         final int bitCount;
650         final boolean hasTransparency = paletteFactory.hasTransparency(src);
651         if (palette == null) {
652             if (hasTransparency) {
653                 bitCount = 32;
654             } else {
655                 bitCount = 24;
656             }
657         } else if (palette.length() <= 2) {
658             bitCount = 1;
659         } else if (palette.length() <= 16) {
660             bitCount = 4;
661         } else {
662             bitCount = 8;
663         }
664 
665         final BinaryOutputStream bos = new BinaryOutputStream(os, ByteOrder.LITTLE_ENDIAN);
666 
667         int scanline_size = (bitCount * src.getWidth() + 7) / 8;
668         if ((scanline_size % 4) != 0) {
669             scanline_size += 4 - (scanline_size % 4); // pad scanline to 4 byte
670                                                       // size.
671         }
672         int t_scanline_size = (src.getWidth() + 7) / 8;
673         if ((t_scanline_size % 4) != 0) {
674             t_scanline_size += 4 - (t_scanline_size % 4); // pad scanline to 4
675                                                           // byte size.
676         }
677         final int imageSize = 40 + 4 * (bitCount <= 8 ? (1 << bitCount) : 0)
678                 + src.getHeight() * scanline_size + src.getHeight()
679                 * t_scanline_size;
680 
681         // ICONDIR
682         bos.write2Bytes(0); // reserved
683         bos.write2Bytes(1); // 1=ICO, 2=CUR
684         bos.write2Bytes(1); // count
685 
686         // ICONDIRENTRY
687         int iconDirEntryWidth = src.getWidth();
688         int iconDirEntryHeight = src.getHeight();
689         if (iconDirEntryWidth > 255 || iconDirEntryHeight > 255) {
690             iconDirEntryWidth = 0;
691             iconDirEntryHeight = 0;
692         }
693         bos.write(iconDirEntryWidth);
694         bos.write(iconDirEntryHeight);
695         bos.write((bitCount >= 8) ? 0 : (1 << bitCount));
696         bos.write(0); // reserved
697         bos.write2Bytes(1); // color planes
698         bos.write2Bytes(bitCount);
699         bos.write4Bytes(imageSize);
700         bos.write4Bytes(22); // image offset
701 
702         // BITMAPINFOHEADER
703         bos.write4Bytes(40); // size
704         bos.write4Bytes(src.getWidth());
705         bos.write4Bytes(2 * src.getHeight());
706         bos.write2Bytes(1); // planes
707         bos.write2Bytes(bitCount);
708         bos.write4Bytes(0); // compression
709         bos.write4Bytes(0); // image size
710         bos.write4Bytes(pixelDensity == null ? 0 : (int) Math.round(pixelDensity.horizontalDensityMetres())); // x pixels per meter
711         bos.write4Bytes(pixelDensity == null ? 0 : (int) Math.round(pixelDensity.horizontalDensityMetres())); // y pixels per meter
712         bos.write4Bytes(0); // colors used, 0 = (1 << bitCount) (ignored)
713         bos.write4Bytes(0); // colors important
714 
715         if (palette != null) {
716             for (int i = 0; i < (1 << bitCount); i++) {
717                 if (i < palette.length()) {
718                     final int argb = palette.getEntry(i);
719                     bos.write(0xff & argb);
720                     bos.write(0xff & (argb >> 8));
721                     bos.write(0xff & (argb >> 16));
722                     bos.write(0);
723                 } else {
724                     bos.write(0);
725                     bos.write(0);
726                     bos.write(0);
727                     bos.write(0);
728                 }
729             }
730         }
731 
732         int bitCache = 0;
733         int bitsInCache = 0;
734         final int rowPadding = scanline_size - (bitCount * src.getWidth() + 7) / 8;
735         for (int y = src.getHeight() - 1; y >= 0; y--) {
736             for (int x = 0; x < src.getWidth(); x++) {
737                 final int argb = src.getRGB(x, y);
738                 if (bitCount < 8) {
739                     final int rgb = 0xffffff & argb;
740                     final int index = palette.getPaletteIndex(rgb);
741                     bitCache <<= bitCount;
742                     bitCache |= index;
743                     bitsInCache += bitCount;
744                     if (bitsInCache >= 8) {
745                         bos.write(0xff & bitCache);
746                         bitCache = 0;
747                         bitsInCache = 0;
748                     }
749                 } else if (bitCount == 8) {
750                     final int rgb = 0xffffff & argb;
751                     final int index = palette.getPaletteIndex(rgb);
752                     bos.write(0xff & index);
753                 } else if (bitCount == 24) {
754                     bos.write(0xff & argb);
755                     bos.write(0xff & (argb >> 8));
756                     bos.write(0xff & (argb >> 16));
757                 } else if (bitCount == 32) {
758                     bos.write(0xff & argb);
759                     bos.write(0xff & (argb >> 8));
760                     bos.write(0xff & (argb >> 16));
761                     bos.write(0xff & (argb >> 24));
762                 }
763             }
764 
765             if (bitsInCache > 0) {
766                 bitCache <<= (8 - bitsInCache);
767                 bos.write(0xff & bitCache);
768                 bitCache = 0;
769                 bitsInCache = 0;
770             }
771 
772             for (int x = 0; x < rowPadding; x++) {
773                 bos.write(0);
774             }
775         }
776 
777         final int t_row_padding = t_scanline_size - (src.getWidth() + 7) / 8;
778         for (int y = src.getHeight() - 1; y >= 0; y--) {
779             for (int x = 0; x < src.getWidth(); x++) {
780                 final int argb = src.getRGB(x, y);
781                 final int alpha = 0xff & (argb >> 24);
782                 bitCache <<= 1;
783                 if (alpha == 0) {
784                     bitCache |= 1;
785                 }
786                 bitsInCache++;
787                 if (bitsInCache >= 8) {
788                     bos.write(0xff & bitCache);
789                     bitCache = 0;
790                     bitsInCache = 0;
791                 }
792             }
793 
794             if (bitsInCache > 0) {
795                 bitCache <<= (8 - bitsInCache);
796                 bos.write(0xff & bitCache);
797                 bitCache = 0;
798                 bitsInCache = 0;
799             }
800 
801             for (int x = 0; x < t_row_padding; x++) {
802                 bos.write(0);
803             }
804         }
805         bos.close();
806     }
807 
808     /**
809      * Extracts embedded XML metadata as XML string.
810      * <p>
811      * 
812      * @param byteSource
813      *            File containing image data.
814      * @param params
815      *            Map of optional parameters, defined in ImagingConstants.
816      * @return Xmp Xml as String, if present. Otherwise, returns null.
817      */
818     @Override
819     public String getXmpXml(final ByteSource byteSource, final Map<String, Object> params)
820             throws ImageReadException, IOException {
821         return null;
822     }
823 }