Coverage for /var/srv/projects/api.amasfac.comuna18.com/tmp/venv/lib/python3.9/site-packages/PIL/IcnsImagePlugin.py: 14%
220 statements
« prev ^ index » next coverage.py v6.4.4, created at 2023-07-17 14:22 -0600
« prev ^ index » next coverage.py v6.4.4, created at 2023-07-17 14:22 -0600
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#
20import io
21import os
22import struct
23import sys
25from PIL import Image, ImageFile, PngImagePlugin, features
27enable_jpeg2k = features.check_codec("jpg_2000")
28if enable_jpeg2k: 28 ↛ 31line 28 didn't jump to line 31, because the condition on line 28 was never false
29 from PIL import Jpeg2KImagePlugin
31MAGIC = b"icns"
32HEADERSIZE = 8
35def nextheader(fobj):
36 return struct.unpack(">4sI", fobj.read(HEADERSIZE))
39def read_32t(fobj, start_length, size):
40 # The 128x128 icon seems to have an extra header for some reason.
41 (start, length) = start_length
42 fobj.seek(start)
43 sig = fobj.read(4)
44 if sig != b"\x00\x00\x00\x00":
45 raise SyntaxError("Unknown signature, expecting 0x00000000")
46 return read_32(fobj, (start + 4, length - 4), size)
49def read_32(fobj, start_length, size):
50 """
51 Read a 32bit RGB icon resource. Seems to be either uncompressed or
52 an RLE packbits-like scheme.
53 """
54 (start, length) = start_length
55 fobj.seek(start)
56 pixel_size = (size[0] * size[2], size[1] * size[2])
57 sizesq = pixel_size[0] * pixel_size[1]
58 if length == sizesq * 3:
59 # uncompressed ("RGBRGBGB")
60 indata = fobj.read(length)
61 im = Image.frombuffer("RGB", pixel_size, indata, "raw", "RGB", 0, 1)
62 else:
63 # decode image
64 im = Image.new("RGB", pixel_size, None)
65 for band_ix in range(3):
66 data = []
67 bytesleft = sizesq
68 while bytesleft > 0:
69 byte = fobj.read(1)
70 if not byte:
71 break
72 byte = byte[0]
73 if byte & 0x80:
74 blocksize = byte - 125
75 byte = fobj.read(1)
76 for i in range(blocksize):
77 data.append(byte)
78 else:
79 blocksize = byte + 1
80 data.append(fobj.read(blocksize))
81 bytesleft -= blocksize
82 if bytesleft <= 0:
83 break
84 if bytesleft != 0:
85 raise SyntaxError(f"Error reading channel [{repr(bytesleft)} left]")
86 band = Image.frombuffer("L", pixel_size, b"".join(data), "raw", "L", 0, 1)
87 im.im.putband(band.im, band_ix)
88 return {"RGB": im}
91def read_mk(fobj, start_length, size):
92 # Alpha masks seem to be uncompressed
93 start = start_length[0]
94 fobj.seek(start)
95 pixel_size = (size[0] * size[2], size[1] * size[2])
96 sizesq = pixel_size[0] * pixel_size[1]
97 band = Image.frombuffer("L", pixel_size, fobj.read(sizesq), "raw", "L", 0, 1)
98 return {"A": band}
101def read_png_or_jpeg2000(fobj, start_length, size):
102 (start, length) = start_length
103 fobj.seek(start)
104 sig = fobj.read(12)
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 raise ValueError(
117 "Unsupported icon subimage format (rebuild PIL "
118 "with JPEG 2000 support to fix this)"
119 )
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 raise ValueError("Unsupported icon subimage format")
133class IcnsFile:
135 SIZES = {
136 (512, 512, 2): [(b"ic10", read_png_or_jpeg2000)],
137 (512, 512, 1): [(b"ic09", read_png_or_jpeg2000)],
138 (256, 256, 2): [(b"ic14", read_png_or_jpeg2000)],
139 (256, 256, 1): [(b"ic08", read_png_or_jpeg2000)],
140 (128, 128, 2): [(b"ic13", read_png_or_jpeg2000)],
141 (128, 128, 1): [
142 (b"ic07", read_png_or_jpeg2000),
143 (b"it32", read_32t),
144 (b"t8mk", read_mk),
145 ],
146 (64, 64, 1): [(b"icp6", read_png_or_jpeg2000)],
147 (32, 32, 2): [(b"ic12", read_png_or_jpeg2000)],
148 (48, 48, 1): [(b"ih32", read_32), (b"h8mk", read_mk)],
149 (32, 32, 1): [
150 (b"icp5", read_png_or_jpeg2000),
151 (b"il32", read_32),
152 (b"l8mk", read_mk),
153 ],
154 (16, 16, 2): [(b"ic11", read_png_or_jpeg2000)],
155 (16, 16, 1): [
156 (b"icp4", read_png_or_jpeg2000),
157 (b"is32", read_32),
158 (b"s8mk", read_mk),
159 ],
160 }
162 def __init__(self, fobj):
163 """
164 fobj is a file-like object as an icns resource
165 """
166 # signature : (start, length)
167 self.dct = dct = {}
168 self.fobj = fobj
169 sig, filesize = nextheader(fobj)
170 if not _accept(sig):
171 raise SyntaxError("not an icns file")
172 i = HEADERSIZE
173 while i < filesize:
174 sig, blocksize = nextheader(fobj)
175 if blocksize <= 0:
176 raise SyntaxError("invalid block header")
177 i += HEADERSIZE
178 blocksize -= HEADERSIZE
179 dct[sig] = (i, blocksize)
180 fobj.seek(blocksize, io.SEEK_CUR)
181 i += blocksize
183 def itersizes(self):
184 sizes = []
185 for size, fmts in self.SIZES.items():
186 for (fmt, reader) in fmts:
187 if fmt in self.dct:
188 sizes.append(size)
189 break
190 return sizes
192 def bestsize(self):
193 sizes = self.itersizes()
194 if not sizes:
195 raise SyntaxError("No 32bit icon resources found")
196 return max(sizes)
198 def dataforsize(self, size):
199 """
200 Get an icon resource as {channel: array}. Note that
201 the arrays are bottom-up like windows bitmaps and will likely
202 need to be flipped or transposed in some way.
203 """
204 dct = {}
205 for code, reader in self.SIZES[size]:
206 desc = self.dct.get(code)
207 if desc is not None:
208 dct.update(reader(self.fobj, desc, size))
209 return dct
211 def getimage(self, size=None):
212 if size is None:
213 size = self.bestsize()
214 if len(size) == 2:
215 size = (size[0], size[1], 1)
216 channels = self.dataforsize(size)
218 im = channels.get("RGBA", None)
219 if im:
220 return im
222 im = channels.get("RGB").copy()
223 try:
224 im.putalpha(channels["A"])
225 except KeyError:
226 pass
227 return im
230##
231# Image plugin for Mac OS icons.
234class IcnsImageFile(ImageFile.ImageFile):
235 """
236 PIL image support for Mac OS .icns files.
237 Chooses the best resolution, but will possibly load
238 a different size image if you mutate the size attribute
239 before calling 'load'.
241 The info dictionary has a key 'sizes' that is a list
242 of sizes that the icns file has.
243 """
245 format = "ICNS"
246 format_description = "Mac OS icns resource"
248 def _open(self):
249 self.icns = IcnsFile(self.fp)
250 self.mode = "RGBA"
251 self.info["sizes"] = self.icns.itersizes()
252 self.best_size = self.icns.bestsize()
253 self.size = (
254 self.best_size[0] * self.best_size[2],
255 self.best_size[1] * self.best_size[2],
256 )
258 @property
259 def size(self):
260 return self._size
262 @size.setter
263 def size(self, value):
264 info_size = value
265 if info_size not in self.info["sizes"] and len(info_size) == 2:
266 info_size = (info_size[0], info_size[1], 1)
267 if (
268 info_size not in self.info["sizes"]
269 and len(info_size) == 3
270 and info_size[2] == 1
271 ):
272 simple_sizes = [
273 (size[0] * size[2], size[1] * size[2]) for size in self.info["sizes"]
274 ]
275 if value in simple_sizes:
276 info_size = self.info["sizes"][simple_sizes.index(value)]
277 if info_size not in self.info["sizes"]:
278 raise ValueError("This is not one of the allowed sizes of this image")
279 self._size = value
281 def load(self):
282 if len(self.size) == 3:
283 self.best_size = self.size
284 self.size = (
285 self.best_size[0] * self.best_size[2],
286 self.best_size[1] * self.best_size[2],
287 )
289 px = Image.Image.load(self)
290 if self.im is not None and self.im.size == self.size:
291 # Already loaded
292 return px
293 self.load_prepare()
294 # This is likely NOT the best way to do it, but whatever.
295 im = self.icns.getimage(self.best_size)
297 # If this is a PNG or JPEG 2000, it won't be loaded yet
298 px = im.load()
300 self.im = im.im
301 self.mode = im.mode
302 self.size = im.size
304 return px
307def _save(im, fp, filename):
308 """
309 Saves the image as a series of PNG files,
310 that are then combined into a .icns file.
311 """
312 if hasattr(fp, "flush"):
313 fp.flush()
315 sizes = {
316 b"ic07": 128,
317 b"ic08": 256,
318 b"ic09": 512,
319 b"ic10": 1024,
320 b"ic11": 32,
321 b"ic12": 64,
322 b"ic13": 256,
323 b"ic14": 512,
324 }
325 provided_images = {im.width: im for im in im.encoderinfo.get("append_images", [])}
326 size_streams = {}
327 for size in set(sizes.values()):
328 image = (
329 provided_images[size]
330 if size in provided_images
331 else im.resize((size, size))
332 )
334 temp = io.BytesIO()
335 image.save(temp, "png")
336 size_streams[size] = temp.getvalue()
338 entries = []
339 for type, size in sizes.items():
340 stream = size_streams[size]
341 entries.append(
342 {"type": type, "size": HEADERSIZE + len(stream), "stream": stream}
343 )
345 # Header
346 fp.write(MAGIC)
347 file_length = HEADERSIZE # Header
348 file_length += HEADERSIZE + 8 * len(entries) # TOC
349 file_length += sum(entry["size"] for entry in entries)
350 fp.write(struct.pack(">i", file_length))
352 # TOC
353 fp.write(b"TOC ")
354 fp.write(struct.pack(">i", HEADERSIZE + len(entries) * HEADERSIZE))
355 for entry in entries:
356 fp.write(entry["type"])
357 fp.write(struct.pack(">i", entry["size"]))
359 # Data
360 for entry in entries:
361 fp.write(entry["type"])
362 fp.write(struct.pack(">i", entry["size"]))
363 fp.write(entry["stream"])
365 if hasattr(fp, "flush"):
366 fp.flush()
369def _accept(prefix):
370 return prefix[:4] == MAGIC
373Image.register_open(IcnsImageFile.format, IcnsImageFile, _accept)
374Image.register_extension(IcnsImageFile.format, ".icns")
376Image.register_save(IcnsImageFile.format, _save)
377Image.register_mime(IcnsImageFile.format, "image/icns")
379if __name__ == "__main__": 379 ↛ 380line 379 didn't jump to line 380, because the condition on line 379 was never true
380 if len(sys.argv) < 2:
381 print("Syntax: python3 IcnsImagePlugin.py [file]")
382 sys.exit()
384 with open(sys.argv[1], "rb") as fp:
385 imf = IcnsImageFile(fp)
386 for size in imf.info["sizes"]:
387 imf.size = size
388 imf.save("out-%s-%s-%s.png" % size)
389 with Image.open(sys.argv[1]) as im:
390 im.save("out.png")
391 if sys.platform == "windows":
392 os.startfile("out.png")