Merge pull request #4526 from newpanjing/master

This commit is contained in:
Hugo van Kemenade 2021-06-30 17:13:43 +03:00 committed by GitHub
commit 06f88ddff4
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
3 changed files with 55 additions and 58 deletions

View File

@ -1,5 +1,4 @@
import io import io
import sys
import pytest import pytest
@ -28,7 +27,6 @@ def test_sanity():
assert im.format == "ICNS" assert im.format == "ICNS"
@pytest.mark.skipif(sys.platform != "darwin", reason="Requires macOS")
def test_save(tmp_path): def test_save(tmp_path):
temp_file = str(tmp_path / "temp.icns") temp_file = str(tmp_path / "temp.icns")
@ -41,7 +39,6 @@ def test_save(tmp_path):
assert reread.format == "ICNS" assert reread.format == "ICNS"
@pytest.mark.skipif(sys.platform != "darwin", reason="Requires macOS")
def test_save_append_images(tmp_path): def test_save_append_images(tmp_path):
temp_file = str(tmp_path / "temp.icns") temp_file = str(tmp_path / "temp.icns")
provided_im = Image.new("RGBA", (32, 32), (255, 0, 0, 128)) provided_im = Image.new("RGBA", (32, 32), (255, 0, 0, 128))
@ -57,7 +54,6 @@ def test_save_append_images(tmp_path):
assert_image_equal(reread, provided_im) assert_image_equal(reread, provided_im)
@pytest.mark.skipif(sys.platform != "darwin", reason="Requires macOS")
def test_save_fp(): def test_save_fp():
fp = io.BytesIO() fp = io.BytesIO()

View File

@ -215,12 +215,16 @@ attributes before loading the file::
ICNS 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 largest available icon is read, though you can override this by setting the
:py:attr:`~PIL.Image.Image.size` property before calling :py:attr:`~PIL.Image.Image.size` property before calling
:py:meth:`~PIL.Image.Image.load`. The :py:meth:`~PIL.Image.open` method :py:meth:`~PIL.Image.Image.load`. The :py:meth:`~PIL.Image.open` method
sets the following :py:attr:`~PIL.Image.Image.info` property: 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** **sizes**
A list of supported sizes found in this icon file; these are a 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 3-tuple, ``(width, height, scale)``, where ``scale`` is 2 for a retina

View File

@ -6,22 +6,21 @@
# #
# history: # history:
# 2004-10-09 fl Turned into a PIL plugin; removed 2.3 dependencies. # 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 Bob Ippolito.
# Copyright (c) 2004 by Secret Labs. # Copyright (c) 2004 by Secret Labs.
# Copyright (c) 2004 by Fredrik Lundh. # Copyright (c) 2004 by Fredrik Lundh.
# Copyright (c) 2014 by Alastair Houghton. # Copyright (c) 2014 by Alastair Houghton.
# Copyright (c) 2020 by Pan Jing.
# #
# See the README file for information on usage and redistribution. # See the README file for information on usage and redistribution.
# #
import io import io
import os import os
import shutil
import struct import struct
import subprocess
import sys import sys
import tempfile
from PIL import Image, ImageFile, PngImagePlugin, features from PIL import Image, ImageFile, PngImagePlugin, features
@ -29,6 +28,7 @@ enable_jpeg2k = features.check_codec("jpg_2000")
if enable_jpeg2k: if enable_jpeg2k:
from PIL import Jpeg2KImagePlugin from PIL import Jpeg2KImagePlugin
MAGIC = b"icns"
HEADERSIZE = 8 HEADERSIZE = 8
@ -167,7 +167,7 @@ class IcnsFile:
self.dct = dct = {} self.dct = dct = {}
self.fobj = fobj self.fobj = fobj
sig, filesize = nextheader(fobj) sig, filesize = nextheader(fobj)
if sig != b"icns": if sig != MAGIC:
raise SyntaxError("not an icns file") raise SyntaxError("not an icns file")
i = HEADERSIZE i = HEADERSIZE
while i < filesize: while i < filesize:
@ -306,74 +306,71 @@ class IcnsImageFile(ImageFile.ImageFile):
def _save(im, fp, filename): def _save(im, fp, filename):
""" """
Saves the image as a series of PNG files, Saves the image as a series of PNG files,
that are then converted to a .icns file that are then combined into a .icns file.
using the macOS command line utility 'iconutil'.
macOS only.
""" """
if hasattr(fp, "flush"): if hasattr(fp, "flush"):
fp.flush() fp.flush()
# create the temporary set of pngs sizes = {
with tempfile.TemporaryDirectory(".iconset") as iconset: b"ic07": 128,
provided_images = { b"ic08": 256,
im.width: im for im in im.encoderinfo.get("append_images", []) b"ic09": 512,
b"ic10": 1024,
b"ic11": 32,
b"ic12": 64,
b"ic13": 256,
b"ic14": 512,
} }
last_w = None provided_images = {im.width: im for im in im.encoderinfo.get("append_images", [])}
second_path = None size_streams = {}
for w in [16, 32, 128, 256, 512]: for size in set(sizes.values()):
prefix = f"icon_{w}x{w}" image = (
provided_images[size]
first_path = os.path.join(iconset, prefix + ".png") if size in provided_images
if last_w == w: else im.resize((size, size))
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
) )
convert_proc.stdout.close() temp = io.BytesIO()
image.save(temp, "png")
size_streams[size] = temp.getvalue()
retcode = convert_proc.wait() entries = []
for type, size in sizes.items():
stream = size_streams[size]
entries.append({"type": type, "size": len(stream), "stream": stream})
if retcode: # Header
raise subprocess.CalledProcessError(retcode, convert_cmd) fp.write(MAGIC)
fp.write(struct.pack(">i", sum(entry["size"] for entry in entries)))
if fp_only: # TOC
with open(filename, "rb") as f: fp.write(b"TOC ")
fp.write(f.read()) 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"]))
# 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): def _accept(prefix):
return prefix[:4] == b"icns" return prefix[:4] == MAGIC
Image.register_open(IcnsImageFile.format, IcnsImageFile, _accept) Image.register_open(IcnsImageFile.format, IcnsImageFile, _accept)
Image.register_extension(IcnsImageFile.format, ".icns") Image.register_extension(IcnsImageFile.format, ".icns")
if sys.platform == "darwin": Image.register_save(IcnsImageFile.format, _save)
Image.register_save(IcnsImageFile.format, _save) Image.register_mime(IcnsImageFile.format, "image/icns")
Image.register_mime(IcnsImageFile.format, "image/icns")
if __name__ == "__main__": if __name__ == "__main__":
if len(sys.argv) < 2: if len(sys.argv) < 2:
print("Syntax: python3 IcnsImagePlugin.py [file]") print("Syntax: python3 IcnsImagePlugin.py [file]")
sys.exit() sys.exit()