1
0

IcnsImagePlugin.py 13 KB

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