Coverage for /pythoncovmergedfiles/medio/medio/usr/local/lib/python3.8/site-packages/pillow-10.4.0-py3.8-linux-x86_64.egg/PIL/IcnsImagePlugin.py: 18%

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

230 statements  

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 

20 

21import io 

22import os 

23import struct 

24import sys 

25from typing import IO 

26 

27from . import Image, ImageFile, PngImagePlugin, features 

28 

29enable_jpeg2k = features.check_codec("jpg_2000") 

30if enable_jpeg2k: 

31 from . import Jpeg2KImagePlugin 

32 

33MAGIC = b"icns" 

34HEADERSIZE = 8 

35 

36 

37def nextheader(fobj): 

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

39 

40 

41def read_32t(fobj, start_length, size): 

42 # The 128x128 icon seems to have an extra header for some reason. 

43 (start, length) = start_length 

44 fobj.seek(start) 

45 sig = fobj.read(4) 

46 if sig != b"\x00\x00\x00\x00": 

47 msg = "Unknown signature, expecting 0x00000000" 

48 raise SyntaxError(msg) 

49 return read_32(fobj, (start + 4, length - 4), size) 

50 

51 

52def read_32(fobj, start_length, size): 

53 """ 

54 Read a 32bit RGB icon resource. Seems to be either uncompressed or 

55 an RLE packbits-like scheme. 

56 """ 

57 (start, length) = start_length 

58 fobj.seek(start) 

59 pixel_size = (size[0] * size[2], size[1] * size[2]) 

60 sizesq = pixel_size[0] * pixel_size[1] 

61 if length == sizesq * 3: 

62 # uncompressed ("RGBRGBGB") 

63 indata = fobj.read(length) 

64 im = Image.frombuffer("RGB", pixel_size, indata, "raw", "RGB", 0, 1) 

65 else: 

66 # decode image 

67 im = Image.new("RGB", pixel_size, None) 

68 for band_ix in range(3): 

69 data = [] 

70 bytesleft = sizesq 

71 while bytesleft > 0: 

72 byte = fobj.read(1) 

73 if not byte: 

74 break 

75 byte = byte[0] 

76 if byte & 0x80: 

77 blocksize = byte - 125 

78 byte = fobj.read(1) 

79 for i in range(blocksize): 

80 data.append(byte) 

81 else: 

82 blocksize = byte + 1 

83 data.append(fobj.read(blocksize)) 

84 bytesleft -= blocksize 

85 if bytesleft <= 0: 

86 break 

87 if bytesleft != 0: 

88 msg = f"Error reading channel [{repr(bytesleft)} left]" 

89 raise SyntaxError(msg) 

90 band = Image.frombuffer("L", pixel_size, b"".join(data), "raw", "L", 0, 1) 

91 im.im.putband(band.im, band_ix) 

92 return {"RGB": im} 

93 

94 

95def read_mk(fobj, start_length, size): 

96 # Alpha masks seem to be uncompressed 

97 start = start_length[0] 

98 fobj.seek(start) 

99 pixel_size = (size[0] * size[2], size[1] * size[2]) 

100 sizesq = pixel_size[0] * pixel_size[1] 

101 band = Image.frombuffer("L", pixel_size, fobj.read(sizesq), "raw", "L", 0, 1) 

102 return {"A": band} 

103 

104 

105def read_png_or_jpeg2000(fobj, start_length, size): 

106 (start, length) = start_length 

107 fobj.seek(start) 

108 sig = fobj.read(12) 

109 if sig[:8] == b"\x89PNG\x0d\x0a\x1a\x0a": 

110 fobj.seek(start) 

111 im = PngImagePlugin.PngImageFile(fobj) 

112 Image._decompression_bomb_check(im.size) 

113 return {"RGBA": im} 

114 elif ( 

115 sig[:4] == b"\xff\x4f\xff\x51" 

116 or sig[:4] == b"\x0d\x0a\x87\x0a" 

117 or sig == b"\x00\x00\x00\x0cjP \x0d\x0a\x87\x0a" 

118 ): 

119 if not enable_jpeg2k: 

120 msg = ( 

121 "Unsupported icon subimage format (rebuild PIL " 

122 "with JPEG 2000 support to fix this)" 

123 ) 

124 raise ValueError(msg) 

125 # j2k, jpc or j2c 

126 fobj.seek(start) 

127 jp2kstream = fobj.read(length) 

128 f = io.BytesIO(jp2kstream) 

129 im = Jpeg2KImagePlugin.Jpeg2KImageFile(f) 

130 Image._decompression_bomb_check(im.size) 

131 if im.mode != "RGBA": 

132 im = im.convert("RGBA") 

133 return {"RGBA": im} 

134 else: 

135 msg = "Unsupported icon subimage format" 

136 raise ValueError(msg) 

137 

138 

139class IcnsFile: 

140 SIZES = { 

141 (512, 512, 2): [(b"ic10", read_png_or_jpeg2000)], 

142 (512, 512, 1): [(b"ic09", read_png_or_jpeg2000)], 

143 (256, 256, 2): [(b"ic14", read_png_or_jpeg2000)], 

144 (256, 256, 1): [(b"ic08", read_png_or_jpeg2000)], 

145 (128, 128, 2): [(b"ic13", read_png_or_jpeg2000)], 

146 (128, 128, 1): [ 

147 (b"ic07", read_png_or_jpeg2000), 

148 (b"it32", read_32t), 

149 (b"t8mk", read_mk), 

150 ], 

151 (64, 64, 1): [(b"icp6", read_png_or_jpeg2000)], 

152 (32, 32, 2): [(b"ic12", read_png_or_jpeg2000)], 

153 (48, 48, 1): [(b"ih32", read_32), (b"h8mk", read_mk)], 

154 (32, 32, 1): [ 

155 (b"icp5", read_png_or_jpeg2000), 

156 (b"il32", read_32), 

157 (b"l8mk", read_mk), 

158 ], 

159 (16, 16, 2): [(b"ic11", read_png_or_jpeg2000)], 

160 (16, 16, 1): [ 

161 (b"icp4", read_png_or_jpeg2000), 

162 (b"is32", read_32), 

163 (b"s8mk", read_mk), 

164 ], 

165 } 

166 

167 def __init__(self, fobj): 

168 """ 

169 fobj is a file-like object as an icns resource 

170 """ 

171 # signature : (start, length) 

172 self.dct = dct = {} 

173 self.fobj = fobj 

174 sig, filesize = nextheader(fobj) 

175 if not _accept(sig): 

176 msg = "not an icns file" 

177 raise SyntaxError(msg) 

178 i = HEADERSIZE 

179 while i < filesize: 

180 sig, blocksize = nextheader(fobj) 

181 if blocksize <= 0: 

182 msg = "invalid block header" 

183 raise SyntaxError(msg) 

184 i += HEADERSIZE 

185 blocksize -= HEADERSIZE 

186 dct[sig] = (i, blocksize) 

187 fobj.seek(blocksize, io.SEEK_CUR) 

188 i += blocksize 

189 

190 def itersizes(self): 

191 sizes = [] 

192 for size, fmts in self.SIZES.items(): 

193 for fmt, reader in fmts: 

194 if fmt in self.dct: 

195 sizes.append(size) 

196 break 

197 return sizes 

198 

199 def bestsize(self): 

200 sizes = self.itersizes() 

201 if not sizes: 

202 msg = "No 32bit icon resources found" 

203 raise SyntaxError(msg) 

204 return max(sizes) 

205 

206 def dataforsize(self, size): 

207 """ 

208 Get an icon resource as {channel: array}. Note that 

209 the arrays are bottom-up like windows bitmaps and will likely 

210 need to be flipped or transposed in some way. 

211 """ 

212 dct = {} 

213 for code, reader in self.SIZES[size]: 

214 desc = self.dct.get(code) 

215 if desc is not None: 

216 dct.update(reader(self.fobj, desc, size)) 

217 return dct 

218 

219 def getimage(self, size=None): 

220 if size is None: 

221 size = self.bestsize() 

222 if len(size) == 2: 

223 size = (size[0], size[1], 1) 

224 channels = self.dataforsize(size) 

225 

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

227 if im: 

228 return im 

229 

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

231 try: 

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

233 except KeyError: 

234 pass 

235 return im 

236 

237 

238## 

239# Image plugin for Mac OS icons. 

240 

241 

242class IcnsImageFile(ImageFile.ImageFile): 

243 """ 

244 PIL image support for Mac OS .icns files. 

245 Chooses the best resolution, but will possibly load 

246 a different size image if you mutate the size attribute 

247 before calling 'load'. 

248 

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

250 of sizes that the icns file has. 

251 """ 

252 

253 format = "ICNS" 

254 format_description = "Mac OS icns resource" 

255 

256 def _open(self) -> None: 

257 self.icns = IcnsFile(self.fp) 

258 self._mode = "RGBA" 

259 self.info["sizes"] = self.icns.itersizes() 

260 self.best_size = self.icns.bestsize() 

261 self.size = ( 

262 self.best_size[0] * self.best_size[2], 

263 self.best_size[1] * self.best_size[2], 

264 ) 

265 

266 @property 

267 def size(self): 

268 return self._size 

269 

270 @size.setter 

271 def size(self, value): 

272 info_size = value 

273 if info_size not in self.info["sizes"] and len(info_size) == 2: 

274 info_size = (info_size[0], info_size[1], 1) 

275 if ( 

276 info_size not in self.info["sizes"] 

277 and len(info_size) == 3 

278 and info_size[2] == 1 

279 ): 

280 simple_sizes = [ 

281 (size[0] * size[2], size[1] * size[2]) for size in self.info["sizes"] 

282 ] 

283 if value in simple_sizes: 

284 info_size = self.info["sizes"][simple_sizes.index(value)] 

285 if info_size not in self.info["sizes"]: 

286 msg = "This is not one of the allowed sizes of this image" 

287 raise ValueError(msg) 

288 self._size = value 

289 

290 def load(self): 

291 if len(self.size) == 3: 

292 self.best_size = self.size 

293 self.size = ( 

294 self.best_size[0] * self.best_size[2], 

295 self.best_size[1] * self.best_size[2], 

296 ) 

297 

298 px = Image.Image.load(self) 

299 if self.im is not None and self.im.size == self.size: 

300 # Already loaded 

301 return px 

302 self.load_prepare() 

303 # This is likely NOT the best way to do it, but whatever. 

304 im = self.icns.getimage(self.best_size) 

305 

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

307 px = im.load() 

308 

309 self.im = im.im 

310 self._mode = im.mode 

311 self.size = im.size 

312 

313 return px 

314 

315 

316def _save(im: Image.Image, fp: IO[bytes], filename: str | bytes) -> None: 

317 """ 

318 Saves the image as a series of PNG files, 

319 that are then combined into a .icns file. 

320 """ 

321 if hasattr(fp, "flush"): 

322 fp.flush() 

323 

324 sizes = { 

325 b"ic07": 128, 

326 b"ic08": 256, 

327 b"ic09": 512, 

328 b"ic10": 1024, 

329 b"ic11": 32, 

330 b"ic12": 64, 

331 b"ic13": 256, 

332 b"ic14": 512, 

333 } 

334 provided_images = {im.width: im for im in im.encoderinfo.get("append_images", [])} 

335 size_streams = {} 

336 for size in set(sizes.values()): 

337 image = ( 

338 provided_images[size] 

339 if size in provided_images 

340 else im.resize((size, size)) 

341 ) 

342 

343 temp = io.BytesIO() 

344 image.save(temp, "png") 

345 size_streams[size] = temp.getvalue() 

346 

347 entries = [] 

348 for type, size in sizes.items(): 

349 stream = size_streams[size] 

350 entries.append((type, HEADERSIZE + len(stream), stream)) 

351 

352 # Header 

353 fp.write(MAGIC) 

354 file_length = HEADERSIZE # Header 

355 file_length += HEADERSIZE + 8 * len(entries) # TOC 

356 file_length += sum(entry[1] for entry in entries) 

357 fp.write(struct.pack(">i", file_length)) 

358 

359 # TOC 

360 fp.write(b"TOC ") 

361 fp.write(struct.pack(">i", HEADERSIZE + len(entries) * HEADERSIZE)) 

362 for entry in entries: 

363 fp.write(entry[0]) 

364 fp.write(struct.pack(">i", entry[1])) 

365 

366 # Data 

367 for entry in entries: 

368 fp.write(entry[0]) 

369 fp.write(struct.pack(">i", entry[1])) 

370 fp.write(entry[2]) 

371 

372 if hasattr(fp, "flush"): 

373 fp.flush() 

374 

375 

376def _accept(prefix: bytes) -> bool: 

377 return prefix[:4] == MAGIC 

378 

379 

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

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

382 

383Image.register_save(IcnsImageFile.format, _save) 

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

385 

386if __name__ == "__main__": 

387 if len(sys.argv) < 2: 

388 print("Syntax: python3 IcnsImagePlugin.py [file]") 

389 sys.exit() 

390 

391 with open(sys.argv[1], "rb") as fp: 

392 imf = IcnsImageFile(fp) 

393 for size in imf.info["sizes"]: 

394 width, height, scale = imf.size = size 

395 imf.save(f"out-{width}-{height}-{scale}.png") 

396 with Image.open(sys.argv[1]) as im: 

397 im.save("out.png") 

398 if sys.platform == "windows": 

399 os.startfile("out.png")