Skip to content

Commit

Permalink
Merge pull request #4526 from newpanjing/master
Browse files Browse the repository at this point in the history
  • Loading branch information
hugovk committed Jun 30, 2021
2 parents 6fd4c6e + 43f5a5f commit 06f88dd
Show file tree
Hide file tree
Showing 3 changed files with 55 additions and 58 deletions.
4 changes: 0 additions & 4 deletions Tests/test_file_icns.py
@@ -1,5 +1,4 @@
import io
import sys

import pytest

Expand Down Expand Up @@ -28,7 +27,6 @@ def test_sanity():
assert im.format == "ICNS"


@pytest.mark.skipif(sys.platform != "darwin", reason="Requires macOS")
def test_save(tmp_path):
temp_file = str(tmp_path / "temp.icns")

Expand All @@ -41,7 +39,6 @@ def test_save(tmp_path):
assert reread.format == "ICNS"


@pytest.mark.skipif(sys.platform != "darwin", reason="Requires macOS")
def test_save_append_images(tmp_path):
temp_file = str(tmp_path / "temp.icns")
provided_im = Image.new("RGBA", (32, 32), (255, 0, 0, 128))
Expand All @@ -57,7 +54,6 @@ def test_save_append_images(tmp_path):
assert_image_equal(reread, provided_im)


@pytest.mark.skipif(sys.platform != "darwin", reason="Requires macOS")
def test_save_fp():
fp = io.BytesIO()

Expand Down
6 changes: 5 additions & 1 deletion docs/handbook/image-file-formats.rst
Expand Up @@ -215,12 +215,16 @@ attributes before loading the file::
ICNS
^^^^

Pillow reads and (macOS only) writes macOS ``.icns`` files. By default, the
Pillow reads and writes macOS ``.icns`` files. By default, the
largest available icon is read, though you can override this by setting the
:py:attr:`~PIL.Image.Image.size` property before calling
:py:meth:`~PIL.Image.Image.load`. The :py:meth:`~PIL.Image.open` method
sets the following :py:attr:`~PIL.Image.Image.info` property:

.. note::

Prior to version 8.3.0, Pillow could only write ICNS files on macOS.

**sizes**
A list of supported sizes found in this icon file; these are a
3-tuple, ``(width, height, scale)``, where ``scale`` is 2 for a retina
Expand Down
103 changes: 50 additions & 53 deletions src/PIL/IcnsImagePlugin.py
Expand Up @@ -6,29 +6,29 @@
#
# history:
# 2004-10-09 fl Turned into a PIL plugin; removed 2.3 dependencies.
# 2020-04-04 Allow saving on all operating systems.
#
# Copyright (c) 2004 by Bob Ippolito.
# Copyright (c) 2004 by Secret Labs.
# Copyright (c) 2004 by Fredrik Lundh.
# Copyright (c) 2014 by Alastair Houghton.
# Copyright (c) 2020 by Pan Jing.
#
# See the README file for information on usage and redistribution.
#

import io
import os
import shutil
import struct
import subprocess
import sys
import tempfile

from PIL import Image, ImageFile, PngImagePlugin, features

enable_jpeg2k = features.check_codec("jpg_2000")
if enable_jpeg2k:
from PIL import Jpeg2KImagePlugin

MAGIC = b"icns"
HEADERSIZE = 8


Expand Down Expand Up @@ -167,7 +167,7 @@ def __init__(self, fobj):
self.dct = dct = {}
self.fobj = fobj
sig, filesize = nextheader(fobj)
if sig != b"icns":
if sig != MAGIC:
raise SyntaxError("not an icns file")
i = HEADERSIZE
while i < filesize:
Expand Down Expand Up @@ -306,74 +306,71 @@ def load(self):
def _save(im, fp, filename):
"""
Saves the image as a series of PNG files,
that are then converted to a .icns file
using the macOS command line utility 'iconutil'.
macOS only.
that are then combined into a .icns file.
"""
if hasattr(fp, "flush"):
fp.flush()

# create the temporary set of pngs
with tempfile.TemporaryDirectory(".iconset") as iconset:
provided_images = {
im.width: im for im in im.encoderinfo.get("append_images", [])
}
last_w = None
second_path = None
for w in [16, 32, 128, 256, 512]:
prefix = f"icon_{w}x{w}"

first_path = os.path.join(iconset, prefix + ".png")
if last_w == w:
shutil.copyfile(second_path, first_path)
else:
im_w = provided_images.get(w, im.resize((w, w), Image.LANCZOS))
im_w.save(first_path)

second_path = os.path.join(iconset, prefix + "@2x.png")
im_w2 = provided_images.get(w * 2, im.resize((w * 2, w * 2), Image.LANCZOS))
im_w2.save(second_path)
last_w = w * 2

# iconutil -c icns -o {} {}

fp_only = not filename
if fp_only:
f, filename = tempfile.mkstemp(".icns")
os.close(f)
convert_cmd = ["iconutil", "-c", "icns", "-o", filename, iconset]
convert_proc = subprocess.Popen(
convert_cmd, stdout=subprocess.PIPE, stderr=subprocess.DEVNULL
sizes = {
b"ic07": 128,
b"ic08": 256,
b"ic09": 512,
b"ic10": 1024,
b"ic11": 32,
b"ic12": 64,
b"ic13": 256,
b"ic14": 512,
}
provided_images = {im.width: im for im in im.encoderinfo.get("append_images", [])}
size_streams = {}
for size in set(sizes.values()):
image = (
provided_images[size]
if size in provided_images
else im.resize((size, size))
)

convert_proc.stdout.close()
temp = io.BytesIO()
image.save(temp, "png")
size_streams[size] = temp.getvalue()

entries = []
for type, size in sizes.items():
stream = size_streams[size]
entries.append({"type": type, "size": len(stream), "stream": stream})

retcode = convert_proc.wait()
# Header
fp.write(MAGIC)
fp.write(struct.pack(">i", sum(entry["size"] for entry in entries)))

if retcode:
raise subprocess.CalledProcessError(retcode, convert_cmd)
# 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"]))

if fp_only:
with open(filename, "rb") as f:
fp.write(f.read())
# Data
for entry in entries:
fp.write(entry["type"])
fp.write(struct.pack(">i", HEADERSIZE + entry["size"]))
fp.write(entry["stream"])

if hasattr(fp, "flush"):
fp.flush()


def _accept(prefix):
return prefix[:4] == b"icns"
return prefix[:4] == MAGIC


Image.register_open(IcnsImageFile.format, IcnsImageFile, _accept)
Image.register_extension(IcnsImageFile.format, ".icns")

if sys.platform == "darwin":
Image.register_save(IcnsImageFile.format, _save)

Image.register_mime(IcnsImageFile.format, "image/icns")

Image.register_save(IcnsImageFile.format, _save)
Image.register_mime(IcnsImageFile.format, "image/icns")

if __name__ == "__main__":

if len(sys.argv) < 2:
print("Syntax: python3 IcnsImagePlugin.py [file]")
sys.exit()
Expand Down

0 comments on commit 06f88dd

Please sign in to comment.