IcnsImagePlugin.py 12 KB

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