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