Added support for Windows 1.0 icons

This commit is contained in:
Andrew Murray 2023-09-29 23:42:19 +10:00
parent a088d54509
commit 505c848c2b
5 changed files with 133 additions and 2 deletions

BIN
Tests/images/ico1.ico Normal file

Binary file not shown.

BIN
Tests/images/ico1.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 432 B

View File

@ -227,3 +227,31 @@ def test_draw_reloaded(tmp_path):
with Image.open(outfile) as im: with Image.open(outfile) as im:
assert_image_equal_tofile(im, "Tests/images/hopper_draw.ico") assert_image_equal_tofile(im, "Tests/images/hopper_draw.ico")
def test_ico1_open():
with Image.open("Tests/images/ico1.ico") as im:
assert_image_equal_tofile(im, "Tests/images/ico1.png")
with open("Tests/images/flower.jpg", "rb") as fp:
with pytest.raises(SyntaxError):
IcoImagePlugin.Ico1ImageFile(fp)
def test_ico1_save(tmp_path):
outfile = str(tmp_path / "temp.ico")
with Image.open("Tests/images/l_trns.png") as im:
l_channel = im.convert("1").convert("L")
a_channel = im.convert("LA").getchannel("A")
la = Image.merge("LA", (l_channel, a_channel))
la.save(outfile, "ICO1")
with Image.open(outfile) as im:
assert_image_equal(im, la)
# Test saving in an incorrect mode
output = io.BytesIO()
im = hopper()
with pytest.raises(OSError):
im.save(output, "ICO1")

View File

@ -342,6 +342,23 @@ The :py:meth:`~PIL.Image.Image.save` method can take the following keyword argum
.. versionadded:: 8.3.0 .. versionadded:: 8.3.0
ICO1
^^^^
Pillow also reads and writes device-independent Windows 1.0 icons.
.. versionadded:: 10.1.0
.. _ico1-saving:
Saving
~~~~~~
Since the .ico extension is already used for the ICO format, when saving a
Windows 1.0 icon the output format must be specified explicitly::
im.save("newimage.ico", format="ICO1")
IM IM
^^ ^^
@ -446,7 +463,7 @@ The :py:meth:`~PIL.Image.Image.save` method supports the following options:
If this parameter is not provided, the image will be saved with no profile If this parameter is not provided, the image will be saved with no profile
attached. To preserve the existing profile:: attached. To preserve the existing profile::
im.save(filename, 'jpeg', icc_profile=im.info.get('icc_profile')) im.save(filename, "jpeg", icc_profile=im.info.get("icc_profile"))
**exif** **exif**
If present, the image will be stored with the provided raw EXIF data. If present, the image will be stored with the provided raw EXIF data.
@ -910,7 +927,7 @@ Saving
The extension of SPIDER files may be any 3 alphanumeric characters. Therefore The extension of SPIDER files may be any 3 alphanumeric characters. Therefore
the output format must be specified explicitly:: the output format must be specified explicitly::
im.save('newimage.spi', format='SPIDER') im.save("newimage.spi", format="SPIDER")
For more information about the SPIDER image processing package, see For more information about the SPIDER image processing package, see
https://github.com/spider-em/SPIDER https://github.com/spider-em/SPIDER

View File

@ -22,6 +22,7 @@
# * https://msdn.microsoft.com/en-us/library/ms997538.aspx # * https://msdn.microsoft.com/en-us/library/ms997538.aspx
import os
import warnings import warnings
from io import BytesIO from io import BytesIO
from math import ceil, log from math import ceil, log
@ -347,6 +348,85 @@ class IcoImageFile(ImageFile.ImageFile):
pass pass
def _ico1_accept(prefix):
return prefix[:2] == b"\1\0"
class Ico1ImageFile(ImageFile.ImageFile):
format = "ICO1"
format_description = "Windows 1.0 Icon"
def _open(self):
if not _ico1_accept(self.fp.read(2)):
msg = "not an ICO1 file"
raise SyntaxError(msg)
self.fp.seek(4, os.SEEK_CUR)
width = i16(self.fp.read(2))
height = i16(self.fp.read(2))
self._size = (width, height)
self._mode = "LA"
self.tile = [("ico1", (0, 0) + self.size, 14, None)]
class Ico1Decoder(ImageFile.PyDecoder):
_pulls_fd = True
def decode(self, buffer):
data = bytearray()
bitmapLength = self.state.xsize * self.state.ysize // 8
firstBitmap = self.fd.read(bitmapLength)
secondBitmap = self.fd.read(bitmapLength)
for i, byte in enumerate(firstBitmap):
secondByte = byte ^ secondBitmap[i]
for j in reversed(range(8)):
first = byte >> j & 1
second = secondByte >> j & 1
data += b"\x00" if (first == second) else b"\xff"
data += b"\x00" if first else b"\xff"
self.set_as_raw(bytes(data))
return -1, 0
class Ico1Encoder(ImageFile.PyEncoder):
_pushes_fd = True
def encode(self, bufsize):
firstBitmap = bytearray()
secondBitmap = bytearray()
w, h = self.im.size
for y in range(h):
for x in range(w):
l, a = self.im.getpixel((x, y))
if x % 8 == 0:
firstBitmap += b"\x00"
secondBitmap += b"\xff"
if not a:
firstBitmap[-1] ^= 1 << (7 - x % 8)
if not l:
secondBitmap[-1] ^= 1 << (7 - x % 8)
data = firstBitmap + secondBitmap
return len(data), 0, data
def _ico1_save(im, fp, filename):
if im.mode != "LA":
msg = f"cannot write {im.mode} as ICO1"
raise OSError(msg)
fp.write(
o16(1) # device-independent format
+ o32(0) # not used
+ o16(im.size[0]) # width in pixels
+ o16(im.size[1]) # height in pixels
+ o16(im.size[0] // 8) # width in bytes
+ o16(0) # not used
)
ImageFile._save(im, fp, [("ico1", (0, 0) + im.size, 0, None)])
# #
# -------------------------------------------------------------------- # --------------------------------------------------------------------
@ -356,3 +436,9 @@ Image.register_save(IcoImageFile.format, _save)
Image.register_extension(IcoImageFile.format, ".ico") Image.register_extension(IcoImageFile.format, ".ico")
Image.register_mime(IcoImageFile.format, "image/x-icon") Image.register_mime(IcoImageFile.format, "image/x-icon")
Image.register_open(Ico1ImageFile.format, Ico1ImageFile, _ico1_accept)
Image.register_save(Ico1ImageFile.format, _ico1_save)
Image.register_decoder("ico1", Ico1Decoder)
Image.register_encoder("ico1", Ico1Encoder)