From a8586fe1fff5df52be16b67058d46b68c1d1a77f Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Fri, 11 Mar 2022 21:37:45 +1100 Subject: [PATCH 1/4] Do not save duplicates when duplicate sizes are supplied --- Tests/test_file_ico.py | 15 +++++++++++++++ src/PIL/IcoImagePlugin.py | 2 +- 2 files changed, 16 insertions(+), 1 deletion(-) diff --git a/Tests/test_file_ico.py b/Tests/test_file_ico.py index 12b80fbde27..2f0df577a52 100644 --- a/Tests/test_file_ico.py +++ b/Tests/test_file_ico.py @@ -1,4 +1,5 @@ import io +import os import pytest @@ -70,6 +71,20 @@ def test_save_to_bytes(): ) +def test_no_duplicates(tmp_path): + temp_file = str(tmp_path / "temp.ico") + temp_file2 = str(tmp_path / "temp2.ico") + + im = hopper() + sizes = [(32, 32), (64, 64)] + im.save(temp_file, "ico", sizes=sizes) + + sizes.append(sizes[-1]) + im.save(temp_file2, "ico", sizes=sizes) + + assert os.path.getsize(temp_file) == os.path.getsize(temp_file2) + + @pytest.mark.parametrize("mode", ("1", "L", "P", "RGB", "RGBA")) def test_save_to_bytes_bmp(mode): output = io.BytesIO() diff --git a/src/PIL/IcoImagePlugin.py b/src/PIL/IcoImagePlugin.py index 915e4c92879..af1943447e7 100644 --- a/src/PIL/IcoImagePlugin.py +++ b/src/PIL/IcoImagePlugin.py @@ -51,7 +51,7 @@ def _save(im, fp, filename): else True, sizes, ) - sizes = list(sizes) + sizes = set(sizes) fp.write(struct.pack(" Date: Fri, 11 Mar 2022 20:38:31 +1100 Subject: [PATCH 2/4] Use _binary instead of struct --- src/PIL/IcoImagePlugin.py | 15 ++++++++------- 1 file changed, 8 insertions(+), 7 deletions(-) diff --git a/src/PIL/IcoImagePlugin.py b/src/PIL/IcoImagePlugin.py index af1943447e7..11081e71d8c 100644 --- a/src/PIL/IcoImagePlugin.py +++ b/src/PIL/IcoImagePlugin.py @@ -22,7 +22,6 @@ # * https://msdn.microsoft.com/en-us/library/ms997538.aspx -import struct import warnings from io import BytesIO from math import ceil, log @@ -30,6 +29,8 @@ from . import BmpImagePlugin, Image, ImageFile, PngImagePlugin from ._binary import i16le as i16 from ._binary import i32le as i32 +from ._binary import o8 +from ._binary import o16le as o16 from ._binary import o32le as o32 # @@ -52,15 +53,15 @@ def _save(im, fp, filename): sizes, ) sizes = set(sizes) - fp.write(struct.pack(" Date: Fri, 11 Mar 2022 21:45:37 +1100 Subject: [PATCH 3/4] If primary image is already destination size, do not duplicate --- src/PIL/IcoImagePlugin.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/PIL/IcoImagePlugin.py b/src/PIL/IcoImagePlugin.py index 11081e71d8c..40db16df2de 100644 --- a/src/PIL/IcoImagePlugin.py +++ b/src/PIL/IcoImagePlugin.py @@ -56,7 +56,9 @@ def _save(im, fp, filename): fp.write(o16(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", [])} + provided_images = { + im.size: im for im in [im] + im.encoderinfo.get("append_images", []) + } for size in sizes: width, height = size # 0 means 256 From 59780abd79799db4735c8b94ef32bcf829321ad6 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Sat, 12 Mar 2022 15:49:08 +1100 Subject: [PATCH 4/4] Save multiple images at different bit depths if provided --- Tests/test_file_ico.py | 33 +++++++++++++++++++ src/PIL/IcoImagePlugin.py | 67 +++++++++++++++++++++++---------------- 2 files changed, 73 insertions(+), 27 deletions(-) diff --git a/Tests/test_file_ico.py b/Tests/test_file_ico.py index 2f0df577a52..3fcd5c61f0d 100644 --- a/Tests/test_file_ico.py +++ b/Tests/test_file_ico.py @@ -85,6 +85,39 @@ def test_no_duplicates(tmp_path): assert os.path.getsize(temp_file) == os.path.getsize(temp_file2) +def test_different_bit_depths(tmp_path): + temp_file = str(tmp_path / "temp.ico") + temp_file2 = str(tmp_path / "temp2.ico") + + im = hopper() + im.save(temp_file, "ico", bitmap_format="bmp", sizes=[(128, 128)]) + + hopper("1").save( + temp_file2, + "ico", + bitmap_format="bmp", + sizes=[(128, 128)], + append_images=[im], + ) + + assert os.path.getsize(temp_file) != os.path.getsize(temp_file2) + + # Test that only matching sizes of different bit depths are saved + temp_file3 = str(tmp_path / "temp3.ico") + temp_file4 = str(tmp_path / "temp4.ico") + + im.save(temp_file3, "ico", bitmap_format="bmp", sizes=[(128, 128)]) + im.save( + temp_file4, + "ico", + bitmap_format="bmp", + sizes=[(128, 128)], + append_images=[Image.new("P", (64, 64))], + ) + + assert os.path.getsize(temp_file3) == os.path.getsize(temp_file4) + + @pytest.mark.parametrize("mode", ("1", "L", "P", "RGB", "RGBA")) def test_save_to_bytes_bmp(mode): output = io.BytesIO() diff --git a/src/PIL/IcoImagePlugin.py b/src/PIL/IcoImagePlugin.py index 40db16df2de..17b9855a0a5 100644 --- a/src/PIL/IcoImagePlugin.py +++ b/src/PIL/IcoImagePlugin.py @@ -41,52 +41,65 @@ def _save(im, fp, filename): fp.write(_MAGIC) # (2+2) + bmp = im.encoderinfo.get("bitmap_format") == "bmp" sizes = im.encoderinfo.get( "sizes", [(16, 16), (24, 24), (32, 32), (48, 48), (64, 64), (128, 128), (256, 256)], ) + frames = [] + provided_ims = [im] + im.encoderinfo.get("append_images", []) width, height = im.size - sizes = filter( - lambda x: False - if (x[0] > width or x[1] > height or x[0] > 256 or x[1] > 256) - else True, - sizes, - ) - sizes = set(sizes) - fp.write(o16(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] + im.encoderinfo.get("append_images", []) - } - for size in sizes: - width, height = size + for size in sorted(set(sizes)): + if size[0] > width or size[1] > height or size[0] > 256 or size[1] > 256: + continue + + for provided_im in provided_ims: + if provided_im.size != size: + continue + frames.append(provided_im) + if bmp: + bits = BmpImagePlugin.SAVE[provided_im.mode][1] + bits_used = [bits] + for other_im in provided_ims: + if other_im.size != size: + continue + bits = BmpImagePlugin.SAVE[other_im.mode][1] + if bits not in bits_used: + # Another image has been supplied for this size + # with a different bit depth + frames.append(other_im) + bits_used.append(bits) + break + else: + # TODO: invent a more convenient method for proportional scalings + frame = provided_im.copy() + frame.thumbnail(size, Image.Resampling.LANCZOS, reducing_gap=None) + frames.append(frame) + fp.write(o16(len(frames))) # idCount(2) + offset = fp.tell() + len(frames) * 16 + for frame in frames: + width, height = frame.size # 0 means 256 fp.write(o8(width if width < 256 else 0)) # bWidth(1) fp.write(o8(height if height < 256 else 0)) # bHeight(1) - fp.write(b"\0") # bColorCount(1) + + bits, colors = BmpImagePlugin.SAVE[frame.mode][1:] if bmp else (32, 0) + fp.write(o8(colors)) # bColorCount(1) fp.write(b"\0") # bReserved(1) fp.write(b"\0\0") # wPlanes(2) - - tmp = provided_images.get(size) - if not tmp: - # TODO: invent a more convenient method for proportional scalings - tmp = im.copy() - tmp.thumbnail(size, Image.Resampling.LANCZOS, reducing_gap=None) - bits = BmpImagePlugin.SAVE[tmp.mode][1] if bmp else 32 fp.write(o16(bits)) # wBitCount(2) image_io = BytesIO() if bmp: - tmp.save(image_io, "dib") + frame.save(image_io, "dib") if bits != 32: - and_mask = Image.new("1", tmp.size) + and_mask = Image.new("1", size) ImageFile._save( - and_mask, image_io, [("raw", (0, 0) + tmp.size, 0, ("1", 0, -1))] + and_mask, image_io, [("raw", (0, 0) + size, 0, ("1", 0, -1))] ) else: - tmp.save(image_io, "png") + frame.save(image_io, "png") image_io.seek(0) image_bytes = image_io.read() if bmp: