Repository: python-pillow/Pillow

Issue:
Support saving uncompressed ICO
Currently _Pillow_ basically writes multiple _PNGs_ into an _ICO_ container, which is not supported by some applications that can read _ICO_. I'm not familiar with the _ICO_ format, but I think `biCompression` [here](https://msdn.microsoft.com/en-us/library/vs/alm/dd183376(v=vs.85).aspx) references the possible sub-formats.


Discussion:
❓ Is this a feature request?
Duplicate of #2513?
It's a different issue.
> Duplicate of #2513?

@pdknsk  That issue was about sub-images; this issue is not a duplicate. This is a feature-request (which I share) for non-compressed ICO files; could it be re-opened?

-----

The following is [hard-coded](https://github.com/python-pillow/Pillow/blob/8.0.0/src/PIL/IcoImagePlugin.py#L175-L183):

> When saving, PNG compression is used. Support for this was only added in
> Windows Vista.

And, unfortunately, this is triggering https://github.com/mate-desktop/eom/issues/310  
(I'm not saying that that software isn't at fault, just that *this* issue is triggering *that* one)
So from https://docs.microsoft.com/en-us/previous-versions//dd183376(v=vs.85)?redirectedfrom=MSDN, you're after "BI_RGB", "An uncompressed format", yes? Looking at https://en.wikipedia.org/wiki/ICO_(file_format), I'm guessing this would be BMP, instead of PNG. Does that sound right?

Resolving patch:
diff --git a/src/PIL/IcoImagePlugin.py b/src/PIL/IcoImagePlugin.py
--- a/src/PIL/IcoImagePlugin.py
+++ b/src/PIL/IcoImagePlugin.py
@@ -30,6 +30,7 @@
 from . import BmpImagePlugin, Image, ImageFile, PngImagePlugin
 from ._binary import i16le as i16
 from ._binary import i32le as i32
+from ._binary import o32le as o32
 
 #
 # --------------------------------------------------------------------
@@ -53,6 +54,7 @@ def _save(im, fp, filename):
     sizes = list(sizes)
     fp.write(struct.pack("<H", len(sizes)))  # idCount(2)
     offset = fp.tell() + len(sizes) * 16
+    bmp = im.encoderinfo.get("bitmap_format") == "bmp"
     provided_images = {im.size: im for im in im.encoderinfo.get("append_images", [])}
     for size in sizes:
         width, height = size
@@ -62,17 +64,30 @@ def _save(im, fp, filename):
         fp.write(b"\0")  # bColorCount(1)
         fp.write(b"\0")  # bReserved(1)
         fp.write(b"\0\0")  # wPlanes(2)
-        fp.write(struct.pack("<H", 32))  # wBitCount(2)
 
-        image_io = BytesIO()
         tmp = provided_images.get(size)
         if not tmp:
             # TODO: invent a more convenient method for proportional scalings
             tmp = im.copy()
             tmp.thumbnail(size, Image.LANCZOS, reducing_gap=None)
-        tmp.save(image_io, "png")
+        bits = BmpImagePlugin.SAVE[tmp.mode][1] if bmp else 32
+        fp.write(struct.pack("<H", bits))  # wBitCount(2)
+
+        image_io = BytesIO()
+        if bmp:
+            tmp.save(image_io, "dib")
+
+            if bits != 32:
+                and_mask = Image.new("1", tmp.size)
+                ImageFile._save(
+                    and_mask, image_io, [("raw", (0, 0) + tmp.size, 0, ("1", 0, -1))]
+                )
+        else:
+            tmp.save(image_io, "png")
         image_io.seek(0)
         image_bytes = image_io.read()
+        if bmp:
+            image_bytes = image_bytes[:8] + o32(height * 2) + image_bytes[12:]
         bytes_len = len(image_bytes)
         fp.write(struct.pack("<I", bytes_len))  # dwBytesInRes(4)
         fp.write(struct.pack("<I", offset))  # dwImageOffset(4)


Test patch:
diff --git a/Tests/test_file_ico.py b/Tests/test_file_ico.py
--- a/Tests/test_file_ico.py
+++ b/Tests/test_file_ico.py
@@ -56,6 +56,35 @@ def test_save_to_bytes():
         assert_image_equal(reloaded, hopper().resize((32, 32), Image.LANCZOS))
 
 
+@pytest.mark.parametrize("mode", ("1", "L", "P", "RGB", "RGBA"))
+def test_save_to_bytes_bmp(mode):
+    output = io.BytesIO()
+    im = hopper(mode)
+    im.save(output, "ico", bitmap_format="bmp", sizes=[(32, 32), (64, 64)])
+
+    # The default image
+    output.seek(0)
+    with Image.open(output) as reloaded:
+        assert reloaded.info["sizes"] == {(32, 32), (64, 64)}
+
+        assert "RGBA" == reloaded.mode
+        assert (64, 64) == reloaded.size
+        assert reloaded.format == "ICO"
+        im = hopper(mode).resize((64, 64), Image.LANCZOS).convert("RGBA")
+        assert_image_equal(reloaded, im)
+
+    # The other one
+    output.seek(0)
+    with Image.open(output) as reloaded:
+        reloaded.size = (32, 32)
+
+        assert "RGBA" == reloaded.mode
+        assert (32, 32) == reloaded.size
+        assert reloaded.format == "ICO"
+        im = hopper(mode).resize((32, 32), Image.LANCZOS).convert("RGBA")
+        assert_image_equal(reloaded, im)
+
+
 def test_incorrect_size():
     with Image.open(TEST_ICO_FILE) as im:
         with pytest.raises(ValueError):