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.

394 lines
12 KiB

4 years ago
  1. #
  2. # The Python Imaging Library.
  3. # $Id$
  4. #
  5. # macOS icns file decoder, based on icns.py by Bob Ippolito.
  6. #
  7. # history:
  8. # 2004-10-09 fl Turned into a PIL plugin; removed 2.3 dependencies.
  9. #
  10. # Copyright (c) 2004 by Bob Ippolito.
  11. # Copyright (c) 2004 by Secret Labs.
  12. # Copyright (c) 2004 by Fredrik Lundh.
  13. # Copyright (c) 2014 by Alastair Houghton.
  14. #
  15. # See the README file for information on usage and redistribution.
  16. #
  17. from PIL import Image, ImageFile, PngImagePlugin
  18. from PIL._binary import i8
  19. import io
  20. import os
  21. import shutil
  22. import struct
  23. import sys
  24. import tempfile
  25. enable_jpeg2k = hasattr(Image.core, 'jp2klib_version')
  26. if enable_jpeg2k:
  27. from PIL import Jpeg2KImagePlugin
  28. HEADERSIZE = 8
  29. def nextheader(fobj):
  30. return struct.unpack('>4sI', fobj.read(HEADERSIZE))
  31. def read_32t(fobj, start_length, size):
  32. # The 128x128 icon seems to have an extra header for some reason.
  33. (start, length) = start_length
  34. fobj.seek(start)
  35. sig = fobj.read(4)
  36. if sig != b'\x00\x00\x00\x00':
  37. raise SyntaxError('Unknown signature, expecting 0x00000000')
  38. return read_32(fobj, (start + 4, length - 4), size)
  39. def read_32(fobj, start_length, size):
  40. """
  41. Read a 32bit RGB icon resource. Seems to be either uncompressed or
  42. an RLE packbits-like scheme.
  43. """
  44. (start, length) = start_length
  45. fobj.seek(start)
  46. pixel_size = (size[0] * size[2], size[1] * size[2])
  47. sizesq = pixel_size[0] * pixel_size[1]
  48. if length == sizesq * 3:
  49. # uncompressed ("RGBRGBGB")
  50. indata = fobj.read(length)
  51. im = Image.frombuffer("RGB", pixel_size, indata, "raw", "RGB", 0, 1)
  52. else:
  53. # decode image
  54. im = Image.new("RGB", pixel_size, None)
  55. for band_ix in range(3):
  56. data = []
  57. bytesleft = sizesq
  58. while bytesleft > 0:
  59. byte = fobj.read(1)
  60. if not byte:
  61. break
  62. byte = i8(byte)
  63. if byte & 0x80:
  64. blocksize = byte - 125
  65. byte = fobj.read(1)
  66. for i in range(blocksize):
  67. data.append(byte)
  68. else:
  69. blocksize = byte + 1
  70. data.append(fobj.read(blocksize))
  71. bytesleft -= blocksize
  72. if bytesleft <= 0:
  73. break
  74. if bytesleft != 0:
  75. raise SyntaxError(
  76. "Error reading channel [%r left]" % bytesleft
  77. )
  78. band = Image.frombuffer(
  79. "L", pixel_size, b"".join(data), "raw", "L", 0, 1
  80. )
  81. im.im.putband(band.im, band_ix)
  82. return {"RGB": im}
  83. def read_mk(fobj, start_length, size):
  84. # Alpha masks seem to be uncompressed
  85. start = start_length[0]
  86. fobj.seek(start)
  87. pixel_size = (size[0] * size[2], size[1] * size[2])
  88. sizesq = pixel_size[0] * pixel_size[1]
  89. band = Image.frombuffer(
  90. "L", pixel_size, fobj.read(sizesq), "raw", "L", 0, 1
  91. )
  92. return {"A": band}
  93. def read_png_or_jpeg2000(fobj, start_length, size):
  94. (start, length) = start_length
  95. fobj.seek(start)
  96. sig = fobj.read(12)
  97. if sig[:8] == b'\x89PNG\x0d\x0a\x1a\x0a':
  98. fobj.seek(start)
  99. im = PngImagePlugin.PngImageFile(fobj)
  100. return {"RGBA": im}
  101. elif sig[:4] == b'\xff\x4f\xff\x51' \
  102. or sig[:4] == b'\x0d\x0a\x87\x0a' \
  103. or sig == b'\x00\x00\x00\x0cjP \x0d\x0a\x87\x0a':
  104. if not enable_jpeg2k:
  105. raise ValueError('Unsupported icon subimage format (rebuild PIL '
  106. 'with JPEG 2000 support to fix this)')
  107. # j2k, jpc or j2c
  108. fobj.seek(start)
  109. jp2kstream = fobj.read(length)
  110. f = io.BytesIO(jp2kstream)
  111. im = Jpeg2KImagePlugin.Jpeg2KImageFile(f)
  112. if im.mode != 'RGBA':
  113. im = im.convert('RGBA')
  114. return {"RGBA": im}
  115. else:
  116. raise ValueError('Unsupported icon subimage format')
  117. class IcnsFile(object):
  118. SIZES = {
  119. (512, 512, 2): [
  120. (b'ic10', read_png_or_jpeg2000),
  121. ],
  122. (512, 512, 1): [
  123. (b'ic09', read_png_or_jpeg2000),
  124. ],
  125. (256, 256, 2): [
  126. (b'ic14', read_png_or_jpeg2000),
  127. ],
  128. (256, 256, 1): [
  129. (b'ic08', read_png_or_jpeg2000),
  130. ],
  131. (128, 128, 2): [
  132. (b'ic13', read_png_or_jpeg2000),
  133. ],
  134. (128, 128, 1): [
  135. (b'ic07', read_png_or_jpeg2000),
  136. (b'it32', read_32t),
  137. (b't8mk', read_mk),
  138. ],
  139. (64, 64, 1): [
  140. (b'icp6', read_png_or_jpeg2000),
  141. ],
  142. (32, 32, 2): [
  143. (b'ic12', read_png_or_jpeg2000),
  144. ],
  145. (48, 48, 1): [
  146. (b'ih32', read_32),
  147. (b'h8mk', read_mk),
  148. ],
  149. (32, 32, 1): [
  150. (b'icp5', read_png_or_jpeg2000),
  151. (b'il32', read_32),
  152. (b'l8mk', read_mk),
  153. ],
  154. (16, 16, 2): [
  155. (b'ic11', read_png_or_jpeg2000),
  156. ],
  157. (16, 16, 1): [
  158. (b'icp4', read_png_or_jpeg2000),
  159. (b'is32', read_32),
  160. (b's8mk', read_mk),
  161. ],
  162. }
  163. def __init__(self, fobj):
  164. """
  165. fobj is a file-like object as an icns resource
  166. """
  167. # signature : (start, length)
  168. self.dct = dct = {}
  169. self.fobj = fobj
  170. sig, filesize = nextheader(fobj)
  171. if sig != b'icns':
  172. raise SyntaxError('not an icns file')
  173. i = HEADERSIZE
  174. while i < filesize:
  175. sig, blocksize = nextheader(fobj)
  176. if blocksize <= 0:
  177. raise SyntaxError('invalid block header')
  178. i += HEADERSIZE
  179. blocksize -= HEADERSIZE
  180. dct[sig] = (i, blocksize)
  181. fobj.seek(blocksize, 1)
  182. i += blocksize
  183. def itersizes(self):
  184. sizes = []
  185. for size, fmts in self.SIZES.items():
  186. for (fmt, reader) in fmts:
  187. if fmt in self.dct:
  188. sizes.append(size)
  189. break
  190. return sizes
  191. def bestsize(self):
  192. sizes = self.itersizes()
  193. if not sizes:
  194. raise SyntaxError("No 32bit icon resources found")
  195. return max(sizes)
  196. def dataforsize(self, size):
  197. """
  198. Get an icon resource as {channel: array}. Note that
  199. the arrays are bottom-up like windows bitmaps and will likely
  200. need to be flipped or transposed in some way.
  201. """
  202. dct = {}
  203. for code, reader in self.SIZES[size]:
  204. desc = self.dct.get(code)
  205. if desc is not None:
  206. dct.update(reader(self.fobj, desc, size))
  207. return dct
  208. def getimage(self, size=None):
  209. if size is None:
  210. size = self.bestsize()
  211. if len(size) == 2:
  212. size = (size[0], size[1], 1)
  213. channels = self.dataforsize(size)
  214. im = channels.get('RGBA', None)
  215. if im:
  216. return im
  217. im = channels.get("RGB").copy()
  218. try:
  219. im.putalpha(channels["A"])
  220. except KeyError:
  221. pass
  222. return im
  223. ##
  224. # Image plugin for Mac OS icons.
  225. class IcnsImageFile(ImageFile.ImageFile):
  226. """
  227. PIL image support for Mac OS .icns files.
  228. Chooses the best resolution, but will possibly load
  229. a different size image if you mutate the size attribute
  230. before calling 'load'.
  231. The info dictionary has a key 'sizes' that is a list
  232. of sizes that the icns file has.
  233. """
  234. format = "ICNS"
  235. format_description = "Mac OS icns resource"
  236. def _open(self):
  237. self.icns = IcnsFile(self.fp)
  238. self.mode = 'RGBA'
  239. self.info['sizes'] = self.icns.itersizes()
  240. self.best_size = self.icns.bestsize()
  241. self.size = (self.best_size[0] * self.best_size[2],
  242. self.best_size[1] * self.best_size[2])
  243. # Just use this to see if it's loaded or not yet.
  244. self.tile = ('',)
  245. @property
  246. def size(self):
  247. return self._size
  248. @size.setter
  249. def size(self, value):
  250. info_size = value
  251. if info_size not in self.info['sizes'] and len(info_size) == 2:
  252. info_size = (info_size[0], info_size[1], 1)
  253. if info_size not in self.info['sizes'] and len(info_size) == 3 and \
  254. info_size[2] == 1:
  255. simple_sizes = [(size[0] * size[2], size[1] * size[2])
  256. for size in self.info['sizes']]
  257. if value in simple_sizes:
  258. info_size = self.info['sizes'][simple_sizes.index(value)]
  259. if info_size not in self.info['sizes']:
  260. raise ValueError(
  261. "This is not one of the allowed sizes of this image")
  262. self._size = value
  263. def load(self):
  264. if len(self.size) == 3:
  265. self.best_size = self.size
  266. self.size = (self.best_size[0] * self.best_size[2],
  267. self.best_size[1] * self.best_size[2])
  268. Image.Image.load(self)
  269. if not self.tile:
  270. return
  271. self.load_prepare()
  272. # This is likely NOT the best way to do it, but whatever.
  273. im = self.icns.getimage(self.best_size)
  274. # If this is a PNG or JPEG 2000, it won't be loaded yet
  275. im.load()
  276. self.im = im.im
  277. self.mode = im.mode
  278. self.size = im.size
  279. self.fp = None
  280. self.icns = None
  281. self.tile = ()
  282. self.load_end()
  283. def _save(im, fp, filename):
  284. """
  285. Saves the image as a series of PNG files,
  286. that are then converted to a .icns file
  287. using the macOS command line utility 'iconutil'.
  288. macOS only.
  289. """
  290. if hasattr(fp, "flush"):
  291. fp.flush()
  292. # create the temporary set of pngs
  293. iconset = tempfile.mkdtemp('.iconset')
  294. provided_images = {im.width: im
  295. for im in im.encoderinfo.get("append_images", [])}
  296. last_w = None
  297. for w in [16, 32, 128, 256, 512]:
  298. prefix = 'icon_{}x{}'.format(w, w)
  299. first_path = os.path.join(iconset, prefix+'.png')
  300. if last_w == w:
  301. shutil.copyfile(second_path, first_path)
  302. else:
  303. im_w = provided_images.get(w, im.resize((w, w), Image.LANCZOS))
  304. im_w.save(first_path)
  305. second_path = os.path.join(iconset, prefix+'@2x.png')
  306. im_w2 = provided_images.get(w*2, im.resize((w*2, w*2), Image.LANCZOS))
  307. im_w2.save(second_path)
  308. last_w = w*2
  309. # iconutil -c icns -o {} {}
  310. from subprocess import Popen, PIPE, CalledProcessError
  311. convert_cmd = ["iconutil", "-c", "icns", "-o", filename, iconset]
  312. with open(os.devnull, 'wb') as devnull:
  313. convert_proc = Popen(convert_cmd, stdout=PIPE, stderr=devnull)
  314. convert_proc.stdout.close()
  315. retcode = convert_proc.wait()
  316. # remove the temporary files
  317. shutil.rmtree(iconset)
  318. if retcode:
  319. raise CalledProcessError(retcode, convert_cmd)
  320. Image.register_open(IcnsImageFile.format, IcnsImageFile,
  321. lambda x: x[:4] == b'icns')
  322. Image.register_extension(IcnsImageFile.format, '.icns')
  323. if sys.platform == 'darwin':
  324. Image.register_save(IcnsImageFile.format, _save)
  325. Image.register_mime(IcnsImageFile.format, "image/icns")
  326. if __name__ == '__main__':
  327. if len(sys.argv) < 2:
  328. print("Syntax: python IcnsImagePlugin.py [file]")
  329. sys.exit()
  330. imf = IcnsImageFile(open(sys.argv[1], 'rb'))
  331. for size in imf.info['sizes']:
  332. imf.size = size
  333. imf.load()
  334. im = imf.im
  335. im.save('out-%s-%s-%s.png' % size)
  336. im = Image.open(sys.argv[1])
  337. im.save("out.png")
  338. if sys.platform == 'windows':
  339. os.startfile("out.png")