Repository: python-pillow/Pillow

Issue:
App icns modification breaks the icon display on macOS
<!--
Thank you for reporting an issue.

Follow these guidelines to ensure your issue is handled properly.

If you have a ...

1.  General question: consider asking the question on Stack Overflow
    with the python-imaging-library tag:

    * https://stackoverflow.com/questions/tagged/python-imaging-library

    Do not ask a question in both places.

    If you think you have found a bug or have an unexplained exception
    then file a bug report here.

2.  Bug report: include a self-contained, copy-pastable example that
    generates the issue if possible. Be concise with code posted.
    Guidelines on how to provide a good bug report:

    * https://stackoverflow.com/help/mcve

    Bug reports which follow these guidelines are easier to diagnose,
    and are often handled much more quickly.

3.  Feature request: do a quick search of existing issues
    to make sure this has not been asked before.

We know asking good questions takes effort, and we appreciate your time.
Thank you.
-->

### What did you do?
Modified `*.icns` file inside the app on MacOS

### What did you expect to happen?
Updated icon to be visible using Finder/Get Info.

### What actually happened?
After upgrading Pillow to 8.3.0 version, updating `*.icns` file inside the application on MacOS, makes the app icon not visible/invalid in Finder, though all the icons are visible if you open the `icns` file directly.

### What are your OS, Python and Pillow versions?

* OS: MacOS 10.15 Catalina
* Python: 3.7.7, 3.9.7
* Pillow: 8.3.0 or 8.3.1, 8.4.0

<!--
Please include **code** that reproduces the issue and whenever possible, an **image** that demonstrates the issue. Please upload images to GitHub, not to third-party file hosting sites. If necessary, add the image to a zip or tar archive.

The best reproductions are self-contained scripts with minimal dependencies. If you are using a framework such as Plone, Django, or Buildout, try to replicate the issue just using Pillow.
-->

```python
with Image.open(image_path) as image:
    draw=ImageDraw.Draw(image)
    draw.rectangle([(0,0), (image.width,image.height)], fill=(255,0,0))
    image.save(image.filename)
```



Discussion:
Using an application on my computer, I'm not able to replicate this. Could you upload the original ICNS file? What version of macOS do you have?
@radarhere Sorry about the invalid example, I've rechecked it and it requires drawing prior to saving in order to break the icon (Finder may cache the old icon, so it's easier to see the change using Get Info option on the application). Not really sure if it's related to https://github.com/python-pillow/Pillow/pull/4526 change, but the issue appeared after upgrading to Pillow 8.3.0
@radarhere I've updated the example section of description. It was tested with both MacOS 10.15 Catalina and 11.6 Big Sur. The issue is present in 8.4.0 as well.

Resolving patch:
diff --git a/src/PIL/IcnsImagePlugin.py b/src/PIL/IcnsImagePlugin.py
--- a/src/PIL/IcnsImagePlugin.py
+++ b/src/PIL/IcnsImagePlugin.py
@@ -337,23 +337,28 @@ def _save(im, fp, filename):
     entries = []
     for type, size in sizes.items():
         stream = size_streams[size]
-        entries.append({"type": type, "size": len(stream), "stream": stream})
+        entries.append(
+            {"type": type, "size": HEADERSIZE + len(stream), "stream": stream}
+        )
 
     # Header
     fp.write(MAGIC)
-    fp.write(struct.pack(">i", sum(entry["size"] for entry in entries)))
+    file_length = HEADERSIZE  # Header
+    file_length += HEADERSIZE + 8 * len(entries)  # TOC
+    file_length += sum(entry["size"] for entry in entries)
+    fp.write(struct.pack(">i", file_length))
 
     # TOC
     fp.write(b"TOC ")
     fp.write(struct.pack(">i", HEADERSIZE + len(entries) * HEADERSIZE))
     for entry in entries:
         fp.write(entry["type"])
-        fp.write(struct.pack(">i", HEADERSIZE + entry["size"]))
+        fp.write(struct.pack(">i", entry["size"]))
 
     # Data
     for entry in entries:
         fp.write(entry["type"])
-        fp.write(struct.pack(">i", HEADERSIZE + entry["size"]))
+        fp.write(struct.pack(">i", entry["size"]))
         fp.write(entry["stream"])
 
     if hasattr(fp, "flush"):


Test patch:
diff --git a/Tests/test_file_icns.py b/Tests/test_file_icns.py
--- a/Tests/test_file_icns.py
+++ b/Tests/test_file_icns.py
@@ -1,8 +1,9 @@
 import io
+import os
 
 import pytest
 
-from PIL import IcnsImagePlugin, Image, features
+from PIL import IcnsImagePlugin, Image, _binary, features
 
 from .helper import assert_image_equal, assert_image_similar_tofile
 
@@ -38,6 +39,11 @@ def test_save(tmp_path):
         assert reread.size == (1024, 1024)
         assert reread.format == "ICNS"
 
+    file_length = os.path.getsize(temp_file)
+    with open(temp_file, "rb") as fp:
+        fp.seek(4)
+        assert _binary.i32be(fp.read(4)) == file_length
+
 
 def test_save_append_images(tmp_path):
     temp_file = str(tmp_path / "temp.icns")