IcnsImagePlugin.py 12 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401
  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 typing import IO
  25. from . import Image, ImageFile, PngImagePlugin, features
  26. enable_jpeg2k = features.check_codec("jpg_2000")
  27. if enable_jpeg2k:
  28. from . import Jpeg2KImagePlugin
  29. MAGIC = b"icns"
  30. HEADERSIZE = 8
  31. def nextheader(fobj: IO[bytes]) -> tuple[bytes, int]:
  32. return struct.unpack(">4sI", fobj.read(HEADERSIZE))
  33. def read_32t(
  34. fobj: IO[bytes], start_length: tuple[int, int], size: tuple[int, int, int]
  35. ) -> dict[str, Image.Image]:
  36. # The 128x128 icon seems to have an extra header for some reason.
  37. (start, length) = start_length
  38. fobj.seek(start)
  39. sig = fobj.read(4)
  40. if sig != b"\x00\x00\x00\x00":
  41. msg = "Unknown signature, expecting 0x00000000"
  42. raise SyntaxError(msg)
  43. return read_32(fobj, (start + 4, length - 4), size)
  44. def read_32(
  45. fobj: IO[bytes], start_length: tuple[int, int], size: tuple[int, int, int]
  46. ) -> dict[str, Image.Image]:
  47. """
  48. Read a 32bit RGB icon resource. Seems to be either uncompressed or
  49. an RLE packbits-like scheme.
  50. """
  51. (start, length) = start_length
  52. fobj.seek(start)
  53. pixel_size = (size[0] * size[2], size[1] * size[2])
  54. sizesq = pixel_size[0] * pixel_size[1]
  55. if length == sizesq * 3:
  56. # uncompressed ("RGBRGBGB")
  57. indata = fobj.read(length)
  58. im = Image.frombuffer("RGB", pixel_size, indata, "raw", "RGB", 0, 1)
  59. else:
  60. # decode image
  61. im = Image.new("RGB", pixel_size, None)
  62. for band_ix in range(3):
  63. data = []
  64. bytesleft = sizesq
  65. while bytesleft > 0:
  66. byte = fobj.read(1)
  67. if not byte:
  68. break
  69. byte_int = byte[0]
  70. if byte_int & 0x80:
  71. blocksize = byte_int - 125
  72. byte = fobj.read(1)
  73. for i in range(blocksize):
  74. data.append(byte)
  75. else:
  76. blocksize = byte_int + 1
  77. data.append(fobj.read(blocksize))
  78. bytesleft -= blocksize
  79. if bytesleft <= 0:
  80. break
  81. if bytesleft != 0:
  82. msg = f"Error reading channel [{repr(bytesleft)} left]"
  83. raise SyntaxError(msg)
  84. band = Image.frombuffer("L", pixel_size, b"".join(data), "raw", "L", 0, 1)
  85. im.im.putband(band.im, band_ix)
  86. return {"RGB": im}
  87. def read_mk(
  88. fobj: IO[bytes], start_length: tuple[int, int], size: tuple[int, int, int]
  89. ) -> dict[str, Image.Image]:
  90. # Alpha masks seem to be uncompressed
  91. start = start_length[0]
  92. fobj.seek(start)
  93. pixel_size = (size[0] * size[2], size[1] * size[2])
  94. sizesq = pixel_size[0] * pixel_size[1]
  95. band = Image.frombuffer("L", pixel_size, fobj.read(sizesq), "raw", "L", 0, 1)
  96. return {"A": band}
  97. def read_png_or_jpeg2000(
  98. fobj: IO[bytes], start_length: tuple[int, int], size: tuple[int, int, int]
  99. ) -> dict[str, Image.Image]:
  100. (start, length) = start_length
  101. fobj.seek(start)
  102. sig = fobj.read(12)
  103. im: Image.Image
  104. if sig.startswith(b"\x89PNG\x0d\x0a\x1a\x0a"):
  105. fobj.seek(start)
  106. im = PngImagePlugin.PngImageFile(fobj)
  107. Image._decompression_bomb_check(im.size)
  108. return {"RGBA": im}
  109. elif (
  110. sig.startswith((b"\xff\x4f\xff\x51", b"\x0d\x0a\x87\x0a"))
  111. or sig == b"\x00\x00\x00\x0cjP \x0d\x0a\x87\x0a"
  112. ):
  113. if not enable_jpeg2k:
  114. msg = (
  115. "Unsupported icon subimage format (rebuild PIL "
  116. "with JPEG 2000 support to fix this)"
  117. )
  118. raise ValueError(msg)
  119. # j2k, jpc or j2c
  120. fobj.seek(start)
  121. jp2kstream = fobj.read(length)
  122. f = io.BytesIO(jp2kstream)
  123. im = Jpeg2KImagePlugin.Jpeg2KImageFile(f)
  124. Image._decompression_bomb_check(im.size)
  125. if im.mode != "RGBA":
  126. im = im.convert("RGBA")
  127. return {"RGBA": im}
  128. else:
  129. msg = "Unsupported icon subimage format"
  130. raise ValueError(msg)
  131. class IcnsFile:
  132. SIZES = {
  133. (512, 512, 2): [(b"ic10", read_png_or_jpeg2000)],
  134. (512, 512, 1): [(b"ic09", read_png_or_jpeg2000)],
  135. (256, 256, 2): [(b"ic14", read_png_or_jpeg2000)],
  136. (256, 256, 1): [(b"ic08", read_png_or_jpeg2000)],
  137. (128, 128, 2): [(b"ic13", read_png_or_jpeg2000)],
  138. (128, 128, 1): [
  139. (b"ic07", read_png_or_jpeg2000),
  140. (b"it32", read_32t),
  141. (b"t8mk", read_mk),
  142. ],
  143. (64, 64, 1): [(b"icp6", read_png_or_jpeg2000)],
  144. (32, 32, 2): [(b"ic12", read_png_or_jpeg2000)],
  145. (48, 48, 1): [(b"ih32", read_32), (b"h8mk", read_mk)],
  146. (32, 32, 1): [
  147. (b"icp5", read_png_or_jpeg2000),
  148. (b"il32", read_32),
  149. (b"l8mk", read_mk),
  150. ],
  151. (16, 16, 2): [(b"ic11", read_png_or_jpeg2000)],
  152. (16, 16, 1): [
  153. (b"icp4", read_png_or_jpeg2000),
  154. (b"is32", read_32),
  155. (b"s8mk", read_mk),
  156. ],
  157. }
  158. def __init__(self, fobj: IO[bytes]) -> None:
  159. """
  160. fobj is a file-like object as an icns resource
  161. """
  162. # signature : (start, length)
  163. self.dct = {}
  164. self.fobj = fobj
  165. sig, filesize = nextheader(fobj)
  166. if not _accept(sig):
  167. msg = "not an icns file"
  168. raise SyntaxError(msg)
  169. i = HEADERSIZE
  170. while i < filesize:
  171. sig, blocksize = nextheader(fobj)
  172. if blocksize <= 0:
  173. msg = "invalid block header"
  174. raise SyntaxError(msg)
  175. i += HEADERSIZE
  176. blocksize -= HEADERSIZE
  177. self.dct[sig] = (i, blocksize)
  178. fobj.seek(blocksize, io.SEEK_CUR)
  179. i += blocksize
  180. def itersizes(self) -> list[tuple[int, int, int]]:
  181. sizes = []
  182. for size, fmts in self.SIZES.items():
  183. for fmt, reader in fmts:
  184. if fmt in self.dct:
  185. sizes.append(size)
  186. break
  187. return sizes
  188. def bestsize(self) -> tuple[int, int, int]:
  189. sizes = self.itersizes()
  190. if not sizes:
  191. msg = "No 32bit icon resources found"
  192. raise SyntaxError(msg)
  193. return max(sizes)
  194. def dataforsize(self, size: tuple[int, int, int]) -> dict[str, Image.Image]:
  195. """
  196. Get an icon resource as {channel: array}. Note that
  197. the arrays are bottom-up like windows bitmaps and will likely
  198. need to be flipped or transposed in some way.
  199. """
  200. dct = {}
  201. for code, reader in self.SIZES[size]:
  202. desc = self.dct.get(code)
  203. if desc is not None:
  204. dct.update(reader(self.fobj, desc, size))
  205. return dct
  206. def getimage(
  207. self, size: tuple[int, int] | tuple[int, int, int] | None = None
  208. ) -> Image.Image:
  209. if size is None:
  210. size = self.bestsize()
  211. elif len(size) == 2:
  212. size = (size[0], size[1], 1)
  213. channels = self.dataforsize(size)
  214. im = channels.get("RGBA")
  215. if im:
  216. return im
  217. im = channels["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) -> None:
  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 = (
  242. self.best_size[0] * self.best_size[2],
  243. self.best_size[1] * self.best_size[2],
  244. )
  245. @property
  246. def size(self) -> tuple[int, int]:
  247. return self._size
  248. @size.setter
  249. def size(self, value: tuple[int, int]) -> None:
  250. # Check that a matching size exists,
  251. # or that there is a scale that would create a size that matches
  252. for size in self.info["sizes"]:
  253. simple_size = size[0] * size[2], size[1] * size[2]
  254. scale = simple_size[0] // value[0]
  255. if simple_size[1] / value[1] == scale:
  256. self._size = value
  257. return
  258. msg = "This is not one of the allowed sizes of this image"
  259. raise ValueError(msg)
  260. def load(self, scale: int | None = None) -> Image.core.PixelAccess | None:
  261. if scale is not None:
  262. width, height = self.size[:2]
  263. self.size = width * scale, height * scale
  264. self.best_size = width, height, scale
  265. px = Image.Image.load(self)
  266. if self._im is not None and self.im.size == self.size:
  267. # Already loaded
  268. return px
  269. self.load_prepare()
  270. # This is likely NOT the best way to do it, but whatever.
  271. im = self.icns.getimage(self.best_size)
  272. # If this is a PNG or JPEG 2000, it won't be loaded yet
  273. px = im.load()
  274. self.im = im.im
  275. self._mode = im.mode
  276. self.size = im.size
  277. return px
  278. def _save(im: Image.Image, fp: IO[bytes], filename: str | bytes) -> None:
  279. """
  280. Saves the image as a series of PNG files,
  281. that are then combined into a .icns file.
  282. """
  283. if hasattr(fp, "flush"):
  284. fp.flush()
  285. sizes = {
  286. b"ic07": 128,
  287. b"ic08": 256,
  288. b"ic09": 512,
  289. b"ic10": 1024,
  290. b"ic11": 32,
  291. b"ic12": 64,
  292. b"ic13": 256,
  293. b"ic14": 512,
  294. }
  295. provided_images = {im.width: im for im in im.encoderinfo.get("append_images", [])}
  296. size_streams = {}
  297. for size in set(sizes.values()):
  298. image = (
  299. provided_images[size]
  300. if size in provided_images
  301. else im.resize((size, size))
  302. )
  303. temp = io.BytesIO()
  304. image.save(temp, "png")
  305. size_streams[size] = temp.getvalue()
  306. entries = []
  307. for type, size in sizes.items():
  308. stream = size_streams[size]
  309. entries.append((type, HEADERSIZE + len(stream), stream))
  310. # Header
  311. fp.write(MAGIC)
  312. file_length = HEADERSIZE # Header
  313. file_length += HEADERSIZE + 8 * len(entries) # TOC
  314. file_length += sum(entry[1] 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[0])
  321. fp.write(struct.pack(">i", entry[1]))
  322. # Data
  323. for entry in entries:
  324. fp.write(entry[0])
  325. fp.write(struct.pack(">i", entry[1]))
  326. fp.write(entry[2])
  327. if hasattr(fp, "flush"):
  328. fp.flush()
  329. def _accept(prefix: bytes) -> bool:
  330. return prefix.startswith(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")