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

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 

20import io 

21import os 

22import struct 

23import sys 

24 

25from PIL import Image, ImageFile, PngImagePlugin, features 

26 

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 

30 

31MAGIC = b"icns" 

32HEADERSIZE = 8 

33 

34 

35def nextheader(fobj): 

36 return struct.unpack(">4sI", fobj.read(HEADERSIZE)) 

37 

38 

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) 

47 

48 

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} 

89 

90 

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} 

99 

100 

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") 

131 

132 

133class IcnsFile: 

134 

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 } 

161 

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 

182 

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 

191 

192 def bestsize(self): 

193 sizes = self.itersizes() 

194 if not sizes: 

195 raise SyntaxError("No 32bit icon resources found") 

196 return max(sizes) 

197 

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 

210 

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) 

217 

218 im = channels.get("RGBA", None) 

219 if im: 

220 return im 

221 

222 im = channels.get("RGB").copy() 

223 try: 

224 im.putalpha(channels["A"]) 

225 except KeyError: 

226 pass 

227 return im 

228 

229 

230## 

231# Image plugin for Mac OS icons. 

232 

233 

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'. 

240 

241 The info dictionary has a key 'sizes' that is a list 

242 of sizes that the icns file has. 

243 """ 

244 

245 format = "ICNS" 

246 format_description = "Mac OS icns resource" 

247 

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 ) 

257 

258 @property 

259 def size(self): 

260 return self._size 

261 

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 

280 

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 ) 

288 

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) 

296 

297 # If this is a PNG or JPEG 2000, it won't be loaded yet 

298 px = im.load() 

299 

300 self.im = im.im 

301 self.mode = im.mode 

302 self.size = im.size 

303 

304 return px 

305 

306 

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() 

314 

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 ) 

333 

334 temp = io.BytesIO() 

335 image.save(temp, "png") 

336 size_streams[size] = temp.getvalue() 

337 

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 ) 

344 

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)) 

351 

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"])) 

358 

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"]) 

364 

365 if hasattr(fp, "flush"): 

366 fp.flush() 

367 

368 

369def _accept(prefix): 

370 return prefix[:4] == MAGIC 

371 

372 

373Image.register_open(IcnsImageFile.format, IcnsImageFile, _accept) 

374Image.register_extension(IcnsImageFile.format, ".icns") 

375 

376Image.register_save(IcnsImageFile.format, _save) 

377Image.register_mime(IcnsImageFile.format, "image/icns") 

378 

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() 

383 

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")