Skip to content

Commit

Permalink
Merge pull request #5513 from radarhere/ico_bmp
Browse files Browse the repository at this point in the history
Added ICO saving in BMP format
  • Loading branch information
hugovk committed Jun 6, 2021
2 parents ab52e83 + b17a7dd commit 2a7eb54
Show file tree
Hide file tree
Showing 4 changed files with 61 additions and 3 deletions.
29 changes: 29 additions & 0 deletions Tests/test_file_ico.py
Expand Up @@ -56,6 +56,35 @@ def test_save_to_bytes():
assert_image_equal(reloaded, hopper().resize((32, 32), Image.LANCZOS))


@pytest.mark.parametrize("mode", ("1", "L", "P", "RGB", "RGBA"))
def test_save_to_bytes_bmp(mode):
output = io.BytesIO()
im = hopper(mode)
im.save(output, "ico", bitmap_format="bmp", sizes=[(32, 32), (64, 64)])

# The default image
output.seek(0)
with Image.open(output) as reloaded:
assert reloaded.info["sizes"] == {(32, 32), (64, 64)}

assert "RGBA" == reloaded.mode
assert (64, 64) == reloaded.size
assert reloaded.format == "ICO"
im = hopper(mode).resize((64, 64), Image.LANCZOS).convert("RGBA")
assert_image_equal(reloaded, im)

# The other one
output.seek(0)
with Image.open(output) as reloaded:
reloaded.size = (32, 32)

assert "RGBA" == reloaded.mode
assert (32, 32) == reloaded.size
assert reloaded.format == "ICO"
im = hopper(mode).resize((32, 32), Image.LANCZOS).convert("RGBA")
assert_image_equal(reloaded, im)


def test_incorrect_size():
with Image.open(TEST_ICO_FILE) as im:
with pytest.raises(ValueError):
Expand Down
6 changes: 6 additions & 0 deletions docs/handbook/image-file-formats.rst
Expand Up @@ -247,6 +247,12 @@ The :py:meth:`~PIL.Image.Image.save` method can take the following keyword argum

.. versionadded:: 8.1.0

**bitmap_format**
By default, the image data will be saved in PNG format. With a bitmap format of
"bmp", image data will be saved in BMP format instead.

.. versionadded:: 8.3.0

IM
^^

Expand Down
8 changes: 8 additions & 0 deletions docs/releasenotes/8.3.0.rst
Expand Up @@ -50,6 +50,14 @@ To compare it to other ImageOps methods:
does not fill the extra space. Instead, the original aspect ratio is maintained. So
unlike the other two methods, it is not guaranteed to return an image of ``size``.

ICO saving: bitmap_format argument
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^

By default, Pillow saves ICO files in the PNG format. They can now also be saved in BMP
format, through the new ``bitmap_format`` argument::

im.save("out.ico", bitmap_format="bmp")

Security
========

Expand Down
21 changes: 18 additions & 3 deletions src/PIL/IcoImagePlugin.py
Expand Up @@ -30,6 +30,7 @@
from . import BmpImagePlugin, Image, ImageFile, PngImagePlugin
from ._binary import i16le as i16
from ._binary import i32le as i32
from ._binary import o32le as o32

#
# --------------------------------------------------------------------
Expand All @@ -53,6 +54,7 @@ def _save(im, fp, filename):
sizes = list(sizes)
fp.write(struct.pack("<H", 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", [])}
for size in sizes:
width, height = size
Expand All @@ -62,17 +64,30 @@ def _save(im, fp, filename):
fp.write(b"\0") # bColorCount(1)
fp.write(b"\0") # bReserved(1)
fp.write(b"\0\0") # wPlanes(2)
fp.write(struct.pack("<H", 32)) # wBitCount(2)

image_io = BytesIO()
tmp = provided_images.get(size)
if not tmp:
# TODO: invent a more convenient method for proportional scalings
tmp = im.copy()
tmp.thumbnail(size, Image.LANCZOS, reducing_gap=None)
tmp.save(image_io, "png")
bits = BmpImagePlugin.SAVE[tmp.mode][1] if bmp else 32
fp.write(struct.pack("<H", bits)) # wBitCount(2)

image_io = BytesIO()
if bmp:
tmp.save(image_io, "dib")

if bits != 32:
and_mask = Image.new("1", tmp.size)
ImageFile._save(
and_mask, image_io, [("raw", (0, 0) + tmp.size, 0, ("1", 0, -1))]
)
else:
tmp.save(image_io, "png")
image_io.seek(0)
image_bytes = image_io.read()
if bmp:
image_bytes = image_bytes[:8] + o32(height * 2) + image_bytes[12:]
bytes_len = len(image_bytes)
fp.write(struct.pack("<I", bytes_len)) # dwBytesInRes(4)
fp.write(struct.pack("<I", offset)) # dwImageOffset(4)
Expand Down

0 comments on commit 2a7eb54

Please sign in to comment.