Skip to content

Commit 06f88dd

Browse files
authored
Merge pull request #4526 from newpanjing/master
2 parents 6fd4c6e + 43f5a5f commit 06f88dd

File tree

3 files changed

+55
-58
lines changed

3 files changed

+55
-58
lines changed

Tests/test_file_icns.py

-4
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,4 @@
11
import io
2-
import sys
32

43
import pytest
54

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

3029

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

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

4341

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

5956

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

docs/handbook/image-file-formats.rst

+5-1
Original file line numberDiff line numberDiff line change
@@ -215,12 +215,16 @@ attributes before loading the file::
215215
ICNS
216216
^^^^
217217

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

224+
.. note::
225+
226+
Prior to version 8.3.0, Pillow could only write ICNS files on macOS.
227+
224228
**sizes**
225229
A list of supported sizes found in this icon file; these are a
226230
3-tuple, ``(width, height, scale)``, where ``scale`` is 2 for a retina

src/PIL/IcnsImagePlugin.py

+50-53
Original file line numberDiff line numberDiff line change
@@ -6,29 +6,29 @@
66
#
77
# history:
88
# 2004-10-09 fl Turned into a PIL plugin; removed 2.3 dependencies.
9+
# 2020-04-04 Allow saving on all operating systems.
910
#
1011
# Copyright (c) 2004 by Bob Ippolito.
1112
# Copyright (c) 2004 by Secret Labs.
1213
# Copyright (c) 2004 by Fredrik Lundh.
1314
# Copyright (c) 2014 by Alastair Houghton.
15+
# Copyright (c) 2020 by Pan Jing.
1416
#
1517
# See the README file for information on usage and redistribution.
1618
#
1719

1820
import io
1921
import os
20-
import shutil
2122
import struct
22-
import subprocess
2323
import sys
24-
import tempfile
2524

2625
from PIL import Image, ImageFile, PngImagePlugin, features
2726

2827
enable_jpeg2k = features.check_codec("jpg_2000")
2928
if enable_jpeg2k:
3029
from PIL import Jpeg2KImagePlugin
3130

31+
MAGIC = b"icns"
3232
HEADERSIZE = 8
3333

3434

@@ -167,7 +167,7 @@ def __init__(self, fobj):
167167
self.dct = dct = {}
168168
self.fobj = fobj
169169
sig, filesize = nextheader(fobj)
170-
if sig != b"icns":
170+
if sig != MAGIC:
171171
raise SyntaxError("not an icns file")
172172
i = HEADERSIZE
173173
while i < filesize:
@@ -306,74 +306,71 @@ def load(self):
306306
def _save(im, fp, filename):
307307
"""
308308
Saves the image as a series of PNG files,
309-
that are then converted to a .icns file
310-
using the macOS command line utility 'iconutil'.
311-
312-
macOS only.
309+
that are then combined into a .icns file.
313310
"""
314311
if hasattr(fp, "flush"):
315312
fp.flush()
316313

317-
# create the temporary set of pngs
318-
with tempfile.TemporaryDirectory(".iconset") as iconset:
319-
provided_images = {
320-
im.width: im for im in im.encoderinfo.get("append_images", [])
321-
}
322-
last_w = None
323-
second_path = None
324-
for w in [16, 32, 128, 256, 512]:
325-
prefix = f"icon_{w}x{w}"
326-
327-
first_path = os.path.join(iconset, prefix + ".png")
328-
if last_w == w:
329-
shutil.copyfile(second_path, first_path)
330-
else:
331-
im_w = provided_images.get(w, im.resize((w, w), Image.LANCZOS))
332-
im_w.save(first_path)
333-
334-
second_path = os.path.join(iconset, prefix + "@2x.png")
335-
im_w2 = provided_images.get(w * 2, im.resize((w * 2, w * 2), Image.LANCZOS))
336-
im_w2.save(second_path)
337-
last_w = w * 2
338-
339-
# iconutil -c icns -o {} {}
340-
341-
fp_only = not filename
342-
if fp_only:
343-
f, filename = tempfile.mkstemp(".icns")
344-
os.close(f)
345-
convert_cmd = ["iconutil", "-c", "icns", "-o", filename, iconset]
346-
convert_proc = subprocess.Popen(
347-
convert_cmd, stdout=subprocess.PIPE, stderr=subprocess.DEVNULL
314+
sizes = {
315+
b"ic07": 128,
316+
b"ic08": 256,
317+
b"ic09": 512,
318+
b"ic10": 1024,
319+
b"ic11": 32,
320+
b"ic12": 64,
321+
b"ic13": 256,
322+
b"ic14": 512,
323+
}
324+
provided_images = {im.width: im for im in im.encoderinfo.get("append_images", [])}
325+
size_streams = {}
326+
for size in set(sizes.values()):
327+
image = (
328+
provided_images[size]
329+
if size in provided_images
330+
else im.resize((size, size))
348331
)
349332

350-
convert_proc.stdout.close()
333+
temp = io.BytesIO()
334+
image.save(temp, "png")
335+
size_streams[size] = temp.getvalue()
336+
337+
entries = []
338+
for type, size in sizes.items():
339+
stream = size_streams[size]
340+
entries.append({"type": type, "size": len(stream), "stream": stream})
351341

352-
retcode = convert_proc.wait()
342+
# Header
343+
fp.write(MAGIC)
344+
fp.write(struct.pack(">i", sum(entry["size"] for entry in entries)))
353345

354-
if retcode:
355-
raise subprocess.CalledProcessError(retcode, convert_cmd)
346+
# TOC
347+
fp.write(b"TOC ")
348+
fp.write(struct.pack(">i", HEADERSIZE + len(entries) * HEADERSIZE))
349+
for entry in entries:
350+
fp.write(entry["type"])
351+
fp.write(struct.pack(">i", HEADERSIZE + entry["size"]))
356352

357-
if fp_only:
358-
with open(filename, "rb") as f:
359-
fp.write(f.read())
353+
# Data
354+
for entry in entries:
355+
fp.write(entry["type"])
356+
fp.write(struct.pack(">i", HEADERSIZE + entry["size"]))
357+
fp.write(entry["stream"])
358+
359+
if hasattr(fp, "flush"):
360+
fp.flush()
360361

361362

362363
def _accept(prefix):
363-
return prefix[:4] == b"icns"
364+
return prefix[:4] == MAGIC
364365

365366

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

369-
if sys.platform == "darwin":
370-
Image.register_save(IcnsImageFile.format, _save)
371-
372-
Image.register_mime(IcnsImageFile.format, "image/icns")
373-
370+
Image.register_save(IcnsImageFile.format, _save)
371+
Image.register_mime(IcnsImageFile.format, "image/icns")
374372

375373
if __name__ == "__main__":
376-
377374
if len(sys.argv) < 2:
378375
print("Syntax: python3 IcnsImagePlugin.py [file]")
379376
sys.exit()

0 commit comments

Comments
 (0)