Merge pull request #5513 from radarhere/ico_bmp

Added ICO saving in BMP format
This commit is contained in:
Hugo van Kemenade 2021-06-06 18:12:33 +03:00 committed by GitHub
commit 2a7eb5415e
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
4 changed files with 61 additions and 3 deletions

View File

@ -56,6 +56,35 @@ def test_save_to_bytes():
assert_image_equal(reloaded, hopper().resize((32, 32), Image.LANCZOS)) 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(): def test_incorrect_size():
with Image.open(TEST_ICO_FILE) as im: with Image.open(TEST_ICO_FILE) as im:
with pytest.raises(ValueError): with pytest.raises(ValueError):

View File

@ -247,6 +247,12 @@ The :py:meth:`~PIL.Image.Image.save` method can take the following keyword argum
.. versionadded:: 8.1.0 .. 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 IM
^^ ^^

View File

@ -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 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``. 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 Security
======== ========

View File

@ -30,6 +30,7 @@ from math import ceil, log
from . import BmpImagePlugin, Image, ImageFile, PngImagePlugin from . import BmpImagePlugin, Image, ImageFile, PngImagePlugin
from ._binary import i16le as i16 from ._binary import i16le as i16
from ._binary import i32le as i32 from ._binary import i32le as i32
from ._binary import o32le as o32
# #
# -------------------------------------------------------------------- # --------------------------------------------------------------------
@ -53,6 +54,7 @@ def _save(im, fp, filename):
sizes = list(sizes) sizes = list(sizes)
fp.write(struct.pack("<H", len(sizes))) # idCount(2) fp.write(struct.pack("<H", len(sizes))) # idCount(2)
offset = fp.tell() + len(sizes) * 16 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.encoderinfo.get("append_images", [])}
for size in sizes: for size in sizes:
width, height = size width, height = size
@ -62,17 +64,30 @@ def _save(im, fp, filename):
fp.write(b"\0") # bColorCount(1) fp.write(b"\0") # bColorCount(1)
fp.write(b"\0") # bReserved(1) fp.write(b"\0") # bReserved(1)
fp.write(b"\0\0") # wPlanes(2) fp.write(b"\0\0") # wPlanes(2)
fp.write(struct.pack("<H", 32)) # wBitCount(2)
image_io = BytesIO()
tmp = provided_images.get(size) tmp = provided_images.get(size)
if not tmp: if not tmp:
# TODO: invent a more convenient method for proportional scalings # TODO: invent a more convenient method for proportional scalings
tmp = im.copy() tmp = im.copy()
tmp.thumbnail(size, Image.LANCZOS, reducing_gap=None) 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_io.seek(0)
image_bytes = image_io.read() image_bytes = image_io.read()
if bmp:
image_bytes = image_bytes[:8] + o32(height * 2) + image_bytes[12:]
bytes_len = len(image_bytes) bytes_len = len(image_bytes)
fp.write(struct.pack("<I", bytes_len)) # dwBytesInRes(4) fp.write(struct.pack("<I", bytes_len)) # dwBytesInRes(4)
fp.write(struct.pack("<I", offset)) # dwImageOffset(4) fp.write(struct.pack("<I", offset)) # dwImageOffset(4)