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:
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
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
^^
@ -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
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**
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 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
https://github.com/spider-em/SPIDER

View File

@ -22,6 +22,7 @@
# * https://msdn.microsoft.com/en-us/library/ms997538.aspx
import os
import warnings
from io import BytesIO
from math import ceil, log
@ -347,6 +348,85 @@ class IcoImageFile(ImageFile.ImageFile):
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_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)