Skip to content

Commit

Permalink
Added support for Windows 1.0 icons
Browse files Browse the repository at this point in the history
  • Loading branch information
radarhere committed Sep 29, 2023
1 parent a088d54 commit 505c848
Show file tree
Hide file tree
Showing 5 changed files with 133 additions and 2 deletions.
Binary file added Tests/images/ico1.ico
Binary file not shown.
Binary file added Tests/images/ico1.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
28 changes: 28 additions & 0 deletions Tests/test_file_ico.py
Original file line number Diff line number Diff line change
Expand Up @@ -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")
21 changes: 19 additions & 2 deletions docs/handbook/image-file-formats.rst
Original file line number Diff line number Diff line change
Expand Up @@ -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
^^

Expand Down Expand Up @@ -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.
Expand Down Expand Up @@ -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
Expand Down
86 changes: 86 additions & 0 deletions src/PIL/IcoImagePlugin.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -347,6 +348,85 @@ def load_seek(self):
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)])


#
# --------------------------------------------------------------------

Expand All @@ -356,3 +436,9 @@ def load_seek(self):
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)

0 comments on commit 505c848

Please sign in to comment.