From 6ce5bc17abb2ea74de4fc26a1981e9b1f414c558 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Sat, 12 Mar 2022 07:30:49 +1100 Subject: [PATCH] Save multiple images at different bit depths if provided --- Tests/test_file_ico.py | 18 +++++++++++ src/PIL/IcoImagePlugin.py | 66 +++++++++++++++++++++++---------------- 2 files changed, 57 insertions(+), 27 deletions(-) diff --git a/Tests/test_file_ico.py b/Tests/test_file_ico.py index 2f0df577a52..ae070f8dfed 100644 --- a/Tests/test_file_ico.py +++ b/Tests/test_file_ico.py @@ -85,6 +85,24 @@ 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) + + @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..76fd6b644a0 100644 --- a/src/PIL/IcoImagePlugin.py +++ b/src/PIL/IcoImagePlugin.py @@ -41,52 +41,64 @@ 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: + 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: