IcnsImagePlugin.py 13 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411
  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.startswith(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.startswith((b"\xff\x4f\xff\x51", b"\x0d\x0a\x87\x0a"))
  112. or sig == b"\x00\x00\x00\x0cjP \x0d\x0a\x87\x0a"
  113. ):
  114. if not enable_jpeg2k:
  115. msg = (
  116. "Unsupported icon subimage format (rebuild PIL "
  117. "with JPEG 2000 support to fix this)"
  118. )
  119. raise ValueError(msg)
  120. # j2k, jpc or j2c
  121. fobj.seek(start)
  122. jp2kstream = fobj.read(length)
  123. f = io.BytesIO(jp2kstream)
  124. im = Jpeg2KImagePlugin.Jpeg2KImageFile(f)
  125. Image._decompression_bomb_check(im.size)
  126. if im.mode != "RGBA":
  127. im = im.convert("RGBA")
  128. return {"RGBA": im}
  129. else:
  130. msg = "Unsupported icon subimage format"
  131. raise ValueError(msg)
  132. class IcnsFile:
  133. SIZES = {
  134. (512, 512, 2): [(b"ic10", read_png_or_jpeg2000)],
  135. (512, 512, 1): [(b"ic09", read_png_or_jpeg2000)],
  136. (256, 256, 2): [(b"ic14", read_png_or_jpeg2000)],
  137. (256, 256, 1): [(b"ic08", read_png_or_jpeg2000)],
  138. (128, 128, 2): [(b"ic13", read_png_or_jpeg2000)],
  139. (128, 128, 1): [
  140. (b"ic07", read_png_or_jpeg2000),
  141. (b"it32", read_32t),
  142. (b"t8mk", read_mk),
  143. ],
  144. (64, 64, 1): [(b"icp6", read_png_or_jpeg2000)],
  145. (32, 32, 2): [(b"ic12", read_png_or_jpeg2000)],
  146. (48, 48, 1): [(b"ih32", read_32), (b"h8mk", read_mk)],
  147. (32, 32, 1): [
  148. (b"icp5", read_png_or_jpeg2000),
  149. (b"il32", read_32),
  150. (b"l8mk", read_mk),
  151. ],
  152. (16, 16, 2): [(b"ic11", read_png_or_jpeg2000)],
  153. (16, 16, 1): [
  154. (b"icp4", read_png_or_jpeg2000),
  155. (b"is32", read_32),
  156. (b"s8mk", read_mk),
  157. ],
  158. }
  159. def __init__(self, fobj: IO[bytes]) -> None:
  160. """
  161. fobj is a file-like object as an icns resource
  162. """
  163. # signature : (start, length)
  164. self.dct = {}
  165. self.fobj = fobj
  166. sig, filesize = nextheader(fobj)
  167. if not _accept(sig):
  168. msg = "not an icns file"
  169. raise SyntaxError(msg)
  170. i = HEADERSIZE
  171. while i < filesize:
  172. sig, blocksize = nextheader(fobj)
  173. if blocksize <= 0:
  174. msg = "invalid block header"
  175. raise SyntaxError(msg)
  176. i += HEADERSIZE
  177. blocksize -= HEADERSIZE
  178. self.dct[sig] = (i, blocksize)
  179. fobj.seek(blocksize, io.SEEK_CUR)
  180. i += blocksize
  181. def itersizes(self) -> list[tuple[int, int, int]]:
  182. sizes = []
  183. for size, fmts in self.SIZES.items():
  184. for fmt, reader in fmts:
  185. if fmt in self.dct:
  186. sizes.append(size)
  187. break
  188. return sizes
  189. def bestsize(self) -> tuple[int, int, int]:
  190. sizes = self.itersizes()
  191. if not sizes:
  192. msg = "No 32bit icon resources found"
  193. raise SyntaxError(msg)
  194. return max(sizes)
  195. def dataforsize(self, size: tuple[int, int, int]) -> dict[str, Image.Image]:
  196. """
  197. Get an icon resource as {channel: array}. Note that
  198. the arrays are bottom-up like windows bitmaps and will likely
  199. need to be flipped or transposed in some way.
  200. """
  201. dct = {}
  202. for code, reader in self.SIZES[size]:
  203. desc = self.dct.get(code)
  204. if desc is not None:
  205. dct.update(reader(self.fobj, desc, size))
  206. return dct
  207. def getimage(
  208. self, size: tuple[int, int] | tuple[int, int, int] | None = None
  209. ) -> Image.Image:
  210. if size is None:
  211. size = self.bestsize()
  212. elif len(size) == 2:
  213. size = (size[0], size[1], 1)
  214. channels = self.dataforsize(size)
  215. im = channels.get("RGBA")
  216. if im:
  217. return im
  218. im = channels["RGB"].copy()
  219. try:
  220. im.putalpha(channels["A"])
  221. except KeyError:
  222. pass
  223. return im
  224. ##
  225. # Image plugin for Mac OS icons.
  226. class IcnsImageFile(ImageFile.ImageFile):
  227. """
  228. PIL image support for Mac OS .icns files.
  229. Chooses the best resolution, but will possibly load
  230. a different size image if you mutate the size attribute
  231. before calling 'load'.
  232. The info dictionary has a key 'sizes' that is a list
  233. of sizes that the icns file has.
  234. """
  235. format = "ICNS"
  236. format_description = "Mac OS icns resource"
  237. def _open(self) -> None:
  238. self.icns = IcnsFile(self.fp)
  239. self._mode = "RGBA"
  240. self.info["sizes"] = self.icns.itersizes()
  241. self.best_size = self.icns.bestsize()
  242. self.size = (
  243. self.best_size[0] * self.best_size[2],
  244. self.best_size[1] * self.best_size[2],
  245. )
  246. @property # type: ignore[override]
  247. def size(self) -> tuple[int, int] | tuple[int, int, int]:
  248. return self._size
  249. @size.setter
  250. def size(self, value: tuple[int, int] | tuple[int, int, int]) -> None:
  251. if len(value) == 3:
  252. deprecate("Setting size to (width, height, scale)", 12, "load(scale)")
  253. if value in self.info["sizes"]:
  254. self._size = value # type: ignore[assignment]
  255. return
  256. else:
  257. # Check that a matching size exists,
  258. # or that there is a scale that would create a size that matches
  259. for size in self.info["sizes"]:
  260. simple_size = size[0] * size[2], size[1] * size[2]
  261. scale = simple_size[0] // value[0]
  262. if simple_size[1] / value[1] == scale:
  263. self._size = value
  264. return
  265. msg = "This is not one of the allowed sizes of this image"
  266. raise ValueError(msg)
  267. def load(self, scale: int | None = None) -> Image.core.PixelAccess | None:
  268. if scale is not None or len(self.size) == 3:
  269. if scale is None and len(self.size) == 3:
  270. scale = self.size[2]
  271. assert scale is not None
  272. width, height = self.size[:2]
  273. self.size = width * scale, height * scale
  274. self.best_size = width, height, scale
  275. px = Image.Image.load(self)
  276. if self._im is not None and self.im.size == self.size:
  277. # Already loaded
  278. return px
  279. self.load_prepare()
  280. # This is likely NOT the best way to do it, but whatever.
  281. im = self.icns.getimage(self.best_size)
  282. # If this is a PNG or JPEG 2000, it won't be loaded yet
  283. px = im.load()
  284. self.im = im.im
  285. self._mode = im.mode
  286. self.size = im.size
  287. return px
  288. def _save(im: Image.Image, fp: IO[bytes], filename: str | bytes) -> None:
  289. """
  290. Saves the image as a series of PNG files,
  291. that are then combined into a .icns file.
  292. """
  293. if hasattr(fp, "flush"):
  294. fp.flush()
  295. sizes = {
  296. b"ic07": 128,
  297. b"ic08": 256,
  298. b"ic09": 512,
  299. b"ic10": 1024,
  300. b"ic11": 32,
  301. b"ic12": 64,
  302. b"ic13": 256,
  303. b"ic14": 512,
  304. }
  305. provided_images = {im.width: im for im in im.encoderinfo.get("append_images", [])}
  306. size_streams = {}
  307. for size in set(sizes.values()):
  308. image = (
  309. provided_images[size]
  310. if size in provided_images
  311. else im.resize((size, size))
  312. )
  313. temp = io.BytesIO()
  314. image.save(temp, "png")
  315. size_streams[size] = temp.getvalue()
  316. entries = []
  317. for type, size in sizes.items():
  318. stream = size_streams[size]
  319. entries.append((type, HEADERSIZE + len(stream), stream))
  320. # Header
  321. fp.write(MAGIC)
  322. file_length = HEADERSIZE # Header
  323. file_length += HEADERSIZE + 8 * len(entries) # TOC
  324. file_length += sum(entry[1] for entry in entries)
  325. fp.write(struct.pack(">i", file_length))
  326. # TOC
  327. fp.write(b"TOC ")
  328. fp.write(struct.pack(">i", HEADERSIZE + len(entries) * HEADERSIZE))
  329. for entry in entries:
  330. fp.write(entry[0])
  331. fp.write(struct.pack(">i", entry[1]))
  332. # Data
  333. for entry in entries:
  334. fp.write(entry[0])
  335. fp.write(struct.pack(">i", entry[1]))
  336. fp.write(entry[2])
  337. if hasattr(fp, "flush"):
  338. fp.flush()
  339. def _accept(prefix: bytes) -> bool:
  340. return prefix.startswith(MAGIC)
  341. Image.register_open(IcnsImageFile.format, IcnsImageFile, _accept)
  342. Image.register_extension(IcnsImageFile.format, ".icns")
  343. Image.register_save(IcnsImageFile.format, _save)
  344. Image.register_mime(IcnsImageFile.format, "image/icns")
  345. if __name__ == "__main__":
  346. if len(sys.argv) < 2:
  347. print("Syntax: python3 IcnsImagePlugin.py [file]")
  348. sys.exit()
  349. with open(sys.argv[1], "rb") as fp:
  350. imf = IcnsImageFile(fp)
  351. for size in imf.info["sizes"]:
  352. width, height, scale = imf.size = size
  353. imf.save(f"out-{width}-{height}-{scale}.png")
  354. with Image.open(sys.argv[1]) as im:
  355. im.save("out.png")
  356. if sys.platform == "windows":
  357. os.startfile("out.png")