You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

295 lines
9.4 KiB

4 years ago
  1. #
  2. # The Python Imaging Library.
  3. # $Id$
  4. #
  5. # Windows Icon support for PIL
  6. #
  7. # History:
  8. # 96-05-27 fl Created
  9. #
  10. # Copyright (c) Secret Labs AB 1997.
  11. # Copyright (c) Fredrik Lundh 1996.
  12. #
  13. # See the README file for information on usage and redistribution.
  14. #
  15. # This plugin is a refactored version of Win32IconImagePlugin by Bryan Davis
  16. # <casadebender@gmail.com>.
  17. # https://code.google.com/archive/p/casadebender/wikis/Win32IconImagePlugin.wiki
  18. #
  19. # Icon format references:
  20. # * https://en.wikipedia.org/wiki/ICO_(file_format)
  21. # * https://msdn.microsoft.com/en-us/library/ms997538.aspx
  22. import struct
  23. from io import BytesIO
  24. from . import Image, ImageFile, BmpImagePlugin, PngImagePlugin
  25. from ._binary import i8, i16le as i16, i32le as i32
  26. from math import log, ceil
  27. __version__ = "0.1"
  28. #
  29. # --------------------------------------------------------------------
  30. _MAGIC = b"\0\0\1\0"
  31. def _save(im, fp, filename):
  32. fp.write(_MAGIC) # (2+2)
  33. sizes = im.encoderinfo.get("sizes",
  34. [(16, 16), (24, 24), (32, 32), (48, 48),
  35. (64, 64), (128, 128), (256, 256)])
  36. width, height = im.size
  37. sizes = filter(lambda x: False if (x[0] > width or x[1] > height or
  38. x[0] > 256 or x[1] > 256) else True,
  39. sizes)
  40. sizes = list(sizes)
  41. fp.write(struct.pack("<H", len(sizes))) # idCount(2)
  42. offset = fp.tell() + len(sizes)*16
  43. for size in sizes:
  44. width, height = size
  45. # 0 means 256
  46. fp.write(struct.pack("B", width if width < 256 else 0)) # bWidth(1)
  47. fp.write(struct.pack("B", height if height < 256 else 0)) # bHeight(1)
  48. fp.write(b"\0") # bColorCount(1)
  49. fp.write(b"\0") # bReserved(1)
  50. fp.write(b"\0\0") # wPlanes(2)
  51. fp.write(struct.pack("<H", 32)) # wBitCount(2)
  52. image_io = BytesIO()
  53. tmp = im.copy()
  54. tmp.thumbnail(size, Image.LANCZOS)
  55. tmp.save(image_io, "png")
  56. image_io.seek(0)
  57. image_bytes = image_io.read()
  58. bytes_len = len(image_bytes)
  59. fp.write(struct.pack("<I", bytes_len)) # dwBytesInRes(4)
  60. fp.write(struct.pack("<I", offset)) # dwImageOffset(4)
  61. current = fp.tell()
  62. fp.seek(offset)
  63. fp.write(image_bytes)
  64. offset = offset + bytes_len
  65. fp.seek(current)
  66. def _accept(prefix):
  67. return prefix[:4] == _MAGIC
  68. class IcoFile(object):
  69. def __init__(self, buf):
  70. """
  71. Parse image from file-like object containing ico file data
  72. """
  73. # check magic
  74. s = buf.read(6)
  75. if not _accept(s):
  76. raise SyntaxError("not an ICO file")
  77. self.buf = buf
  78. self.entry = []
  79. # Number of items in file
  80. self.nb_items = i16(s[4:])
  81. # Get headers for each item
  82. for i in range(self.nb_items):
  83. s = buf.read(16)
  84. icon_header = {
  85. 'width': i8(s[0]),
  86. 'height': i8(s[1]),
  87. 'nb_color': i8(s[2]), # No. of colors in image (0 if >=8bpp)
  88. 'reserved': i8(s[3]),
  89. 'planes': i16(s[4:]),
  90. 'bpp': i16(s[6:]),
  91. 'size': i32(s[8:]),
  92. 'offset': i32(s[12:])
  93. }
  94. # See Wikipedia
  95. for j in ('width', 'height'):
  96. if not icon_header[j]:
  97. icon_header[j] = 256
  98. # See Wikipedia notes about color depth.
  99. # We need this just to differ images with equal sizes
  100. icon_header['color_depth'] = (icon_header['bpp'] or
  101. (icon_header['nb_color'] != 0 and
  102. ceil(log(icon_header['nb_color'],
  103. 2))) or 256)
  104. icon_header['dim'] = (icon_header['width'], icon_header['height'])
  105. icon_header['square'] = (icon_header['width'] *
  106. icon_header['height'])
  107. self.entry.append(icon_header)
  108. self.entry = sorted(self.entry, key=lambda x: x['color_depth'])
  109. # ICO images are usually squares
  110. # self.entry = sorted(self.entry, key=lambda x: x['width'])
  111. self.entry = sorted(self.entry, key=lambda x: x['square'])
  112. self.entry.reverse()
  113. def sizes(self):
  114. """
  115. Get a list of all available icon sizes and color depths.
  116. """
  117. return {(h['width'], h['height']) for h in self.entry}
  118. def getimage(self, size, bpp=False):
  119. """
  120. Get an image from the icon
  121. """
  122. for (i, h) in enumerate(self.entry):
  123. if size == h['dim'] and (bpp is False or bpp == h['color_depth']):
  124. return self.frame(i)
  125. return self.frame(0)
  126. def frame(self, idx):
  127. """
  128. Get an image from frame idx
  129. """
  130. header = self.entry[idx]
  131. self.buf.seek(header['offset'])
  132. data = self.buf.read(8)
  133. self.buf.seek(header['offset'])
  134. if data[:8] == PngImagePlugin._MAGIC:
  135. # png frame
  136. im = PngImagePlugin.PngImageFile(self.buf)
  137. else:
  138. # XOR + AND mask bmp frame
  139. im = BmpImagePlugin.DibImageFile(self.buf)
  140. # change tile dimension to only encompass XOR image
  141. im._size = (im.size[0], int(im.size[1] / 2))
  142. d, e, o, a = im.tile[0]
  143. im.tile[0] = d, (0, 0) + im.size, o, a
  144. # figure out where AND mask image starts
  145. mode = a[0]
  146. bpp = 8
  147. for k, v in BmpImagePlugin.BIT2MODE.items():
  148. if mode == v[1]:
  149. bpp = k
  150. break
  151. if 32 == bpp:
  152. # 32-bit color depth icon image allows semitransparent areas
  153. # PIL's DIB format ignores transparency bits, recover them.
  154. # The DIB is packed in BGRX byte order where X is the alpha
  155. # channel.
  156. # Back up to start of bmp data
  157. self.buf.seek(o)
  158. # extract every 4th byte (eg. 3,7,11,15,...)
  159. alpha_bytes = self.buf.read(im.size[0] * im.size[1] * 4)[3::4]
  160. # convert to an 8bpp grayscale image
  161. mask = Image.frombuffer(
  162. 'L', # 8bpp
  163. im.size, # (w, h)
  164. alpha_bytes, # source chars
  165. 'raw', # raw decoder
  166. ('L', 0, -1) # 8bpp inverted, unpadded, reversed
  167. )
  168. else:
  169. # get AND image from end of bitmap
  170. w = im.size[0]
  171. if (w % 32) > 0:
  172. # bitmap row data is aligned to word boundaries
  173. w += 32 - (im.size[0] % 32)
  174. # the total mask data is
  175. # padded row size * height / bits per char
  176. and_mask_offset = o + int(im.size[0] * im.size[1] *
  177. (bpp / 8.0))
  178. total_bytes = int((w * im.size[1]) / 8)
  179. self.buf.seek(and_mask_offset)
  180. mask_data = self.buf.read(total_bytes)
  181. # convert raw data to image
  182. mask = Image.frombuffer(
  183. '1', # 1 bpp
  184. im.size, # (w, h)
  185. mask_data, # source chars
  186. 'raw', # raw decoder
  187. ('1;I', int(w/8), -1) # 1bpp inverted, padded, reversed
  188. )
  189. # now we have two images, im is XOR image and mask is AND image
  190. # apply mask image as alpha channel
  191. im = im.convert('RGBA')
  192. im.putalpha(mask)
  193. return im
  194. ##
  195. # Image plugin for Windows Icon files.
  196. class IcoImageFile(ImageFile.ImageFile):
  197. """
  198. PIL read-only image support for Microsoft Windows .ico files.
  199. By default the largest resolution image in the file will be loaded. This
  200. can be changed by altering the 'size' attribute before calling 'load'.
  201. The info dictionary has a key 'sizes' that is a list of the sizes available
  202. in the icon file.
  203. Handles classic, XP and Vista icon formats.
  204. This plugin is a refactored version of Win32IconImagePlugin by Bryan Davis
  205. <casadebender@gmail.com>.
  206. https://code.google.com/archive/p/casadebender/wikis/Win32IconImagePlugin.wiki
  207. """
  208. format = "ICO"
  209. format_description = "Windows Icon"
  210. def _open(self):
  211. self.ico = IcoFile(self.fp)
  212. self.info['sizes'] = self.ico.sizes()
  213. self.size = self.ico.entry[0]['dim']
  214. self.load()
  215. @property
  216. def size(self):
  217. return self._size
  218. @size.setter
  219. def size(self, value):
  220. if value not in self.info['sizes']:
  221. raise ValueError(
  222. "This is not one of the allowed sizes of this image")
  223. self._size = value
  224. def load(self):
  225. im = self.ico.getimage(self.size)
  226. # if tile is PNG, it won't really be loaded yet
  227. im.load()
  228. self.im = im.im
  229. self.mode = im.mode
  230. self.size = im.size
  231. def load_seek(self):
  232. # Flag the ImageFile.Parser so that it
  233. # just does all the decode at the end.
  234. pass
  235. #
  236. # --------------------------------------------------------------------
  237. Image.register_open(IcoImageFile.format, IcoImageFile, _accept)
  238. Image.register_save(IcoImageFile.format, _save)
  239. Image.register_extension(IcoImageFile.format, ".ico")