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

229 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: IO[bytes]) -> tuple[bytes, int]: 

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

39 

40 

41def read_32t( 

42 fobj: IO[bytes], start_length: tuple[int, int], size: tuple[int, int, int] 

43) -> dict[str, Image.Image]: 

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

45 (start, length) = start_length 

46 fobj.seek(start) 

47 sig = fobj.read(4) 

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

49 msg = "Unknown signature, expecting 0x00000000" 

50 raise SyntaxError(msg) 

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

52 

53 

54def read_32( 

55 fobj: IO[bytes], start_length: tuple[int, int], size: tuple[int, int, int] 

56) -> dict[str, Image.Image]: 

57 """ 

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

59 an RLE packbits-like scheme. 

60 """ 

61 (start, length) = start_length 

62 fobj.seek(start) 

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

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

65 if length == sizesq * 3: 

66 # uncompressed ("RGBRGBGB") 

67 indata = fobj.read(length) 

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

69 else: 

70 # decode image 

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

72 for band_ix in range(3): 

73 data = [] 

74 bytesleft = sizesq 

75 while bytesleft > 0: 

76 byte = fobj.read(1) 

77 if not byte: 

78 break 

79 byte_int = byte[0] 

80 if byte_int & 0x80: 

81 blocksize = byte_int - 125 

82 byte = fobj.read(1) 

83 for i in range(blocksize): 

84 data.append(byte) 

85 else: 

86 blocksize = byte_int + 1 

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

88 bytesleft -= blocksize 

89 if bytesleft <= 0: 

90 break 

91 if bytesleft != 0: 

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

93 raise SyntaxError(msg) 

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

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

96 return {"RGB": im} 

97 

98 

99def read_mk( 

100 fobj: IO[bytes], start_length: tuple[int, int], size: tuple[int, int, int] 

101) -> dict[str, Image.Image]: 

102 # Alpha masks seem to be uncompressed 

103 start = start_length[0] 

104 fobj.seek(start) 

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

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

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

108 return {"A": band} 

109 

110 

111def read_png_or_jpeg2000( 

112 fobj: IO[bytes], start_length: tuple[int, int], size: tuple[int, int, int] 

113) -> dict[str, Image.Image]: 

114 (start, length) = start_length 

115 fobj.seek(start) 

116 sig = fobj.read(12) 

117 

118 im: Image.Image 

119 if sig.startswith(b"\x89PNG\x0d\x0a\x1a\x0a"): 

120 fobj.seek(start) 

121 im = PngImagePlugin.PngImageFile(fobj) 

122 Image._decompression_bomb_check(im.size) 

123 return {"RGBA": im} 

124 elif ( 

125 sig.startswith((b"\xff\x4f\xff\x51", b"\x0d\x0a\x87\x0a")) 

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

127 ): 

128 if not enable_jpeg2k: 

129 msg = ( 

130 "Unsupported icon subimage format (rebuild PIL " 

131 "with JPEG 2000 support to fix this)" 

132 ) 

133 raise ValueError(msg) 

134 # j2k, jpc or j2c 

135 fobj.seek(start) 

136 jp2kstream = fobj.read(length) 

137 f = io.BytesIO(jp2kstream) 

138 im = Jpeg2KImagePlugin.Jpeg2KImageFile(f) 

139 Image._decompression_bomb_check(im.size) 

140 if im.mode != "RGBA": 

141 im = im.convert("RGBA") 

142 return {"RGBA": im} 

143 else: 

144 msg = "Unsupported icon subimage format" 

145 raise ValueError(msg) 

146 

147 

148class IcnsFile: 

149 SIZES = { 

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

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

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

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

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

155 (128, 128, 1): [ 

156 (b"ic07", read_png_or_jpeg2000), 

157 (b"it32", read_32t), 

158 (b"t8mk", read_mk), 

159 ], 

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

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

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

163 (32, 32, 1): [ 

164 (b"icp5", read_png_or_jpeg2000), 

165 (b"il32", read_32), 

166 (b"l8mk", read_mk), 

167 ], 

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

169 (16, 16, 1): [ 

170 (b"icp4", read_png_or_jpeg2000), 

171 (b"is32", read_32), 

172 (b"s8mk", read_mk), 

173 ], 

174 } 

175 

176 def __init__(self, fobj: IO[bytes]) -> None: 

177 """ 

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

179 """ 

180 # signature : (start, length) 

181 self.dct = {} 

182 self.fobj = fobj 

183 sig, filesize = nextheader(fobj) 

184 if not _accept(sig): 

185 msg = "not an icns file" 

186 raise SyntaxError(msg) 

187 i = HEADERSIZE 

188 while i < filesize: 

189 sig, blocksize = nextheader(fobj) 

190 if blocksize <= 0: 

191 msg = "invalid block header" 

192 raise SyntaxError(msg) 

193 i += HEADERSIZE 

194 blocksize -= HEADERSIZE 

195 self.dct[sig] = (i, blocksize) 

196 fobj.seek(blocksize, io.SEEK_CUR) 

197 i += blocksize 

198 

199 def itersizes(self) -> list[tuple[int, int, int]]: 

200 sizes = [] 

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

202 for fmt, reader in fmts: 

203 if fmt in self.dct: 

204 sizes.append(size) 

205 break 

206 return sizes 

207 

208 def bestsize(self) -> tuple[int, int, int]: 

209 sizes = self.itersizes() 

210 if not sizes: 

211 msg = "No 32bit icon resources found" 

212 raise SyntaxError(msg) 

213 return max(sizes) 

214 

215 def dataforsize(self, size: tuple[int, int, int]) -> dict[str, Image.Image]: 

216 """ 

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

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

219 need to be flipped or transposed in some way. 

220 """ 

221 dct = {} 

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

223 desc = self.dct.get(code) 

224 if desc is not None: 

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

226 return dct 

227 

228 def getimage( 

229 self, size: tuple[int, int] | tuple[int, int, int] | None = None 

230 ) -> Image.Image: 

231 if size is None: 

232 size = self.bestsize() 

233 elif len(size) == 2: 

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

235 channels = self.dataforsize(size) 

236 

237 im = channels.get("RGBA") 

238 if im: 

239 return im 

240 

241 im = channels["RGB"].copy() 

242 try: 

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

244 except KeyError: 

245 pass 

246 return im 

247 

248 

249## 

250# Image plugin for Mac OS icons. 

251 

252 

253class IcnsImageFile(ImageFile.ImageFile): 

254 """ 

255 PIL image support for Mac OS .icns files. 

256 Chooses the best resolution, but will possibly load 

257 a different size image if you mutate the size attribute 

258 before calling 'load'. 

259 

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

261 of sizes that the icns file has. 

262 """ 

263 

264 format = "ICNS" 

265 format_description = "Mac OS icns resource" 

266 

267 def _open(self) -> None: 

268 self.icns = IcnsFile(self.fp) 

269 self._mode = "RGBA" 

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

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

272 self.size = ( 

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

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

275 ) 

276 

277 @property 

278 def size(self) -> tuple[int, int]: 

279 return self._size 

280 

281 @size.setter 

282 def size(self, value: tuple[int, int]) -> None: 

283 # Check that a matching size exists, 

284 # or that there is a scale that would create a size that matches 

285 for size in self.info["sizes"]: 

286 simple_size = size[0] * size[2], size[1] * size[2] 

287 scale = simple_size[0] // value[0] 

288 if simple_size[1] / value[1] == scale: 

289 self._size = value 

290 return 

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

292 raise ValueError(msg) 

293 

294 def load(self, scale: int | None = None) -> Image.core.PixelAccess | None: 

295 if scale is not None: 

296 width, height = self.size[:2] 

297 self.size = width * scale, height * scale 

298 self.best_size = width, height, scale 

299 

300 px = Image.Image.load(self) 

301 if self._im is not None and self.im.size == self.size: 

302 # Already loaded 

303 return px 

304 self.load_prepare() 

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

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

307 

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

309 px = im.load() 

310 

311 self.im = im.im 

312 self._mode = im.mode 

313 self.size = im.size 

314 

315 return px 

316 

317 

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

319 """ 

320 Saves the image as a series of PNG files, 

321 that are then combined into a .icns file. 

322 """ 

323 if hasattr(fp, "flush"): 

324 fp.flush() 

325 

326 sizes = { 

327 b"ic07": 128, 

328 b"ic08": 256, 

329 b"ic09": 512, 

330 b"ic10": 1024, 

331 b"ic11": 32, 

332 b"ic12": 64, 

333 b"ic13": 256, 

334 b"ic14": 512, 

335 } 

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

337 size_streams = {} 

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

339 image = ( 

340 provided_images[size] 

341 if size in provided_images 

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

343 ) 

344 

345 temp = io.BytesIO() 

346 image.save(temp, "png") 

347 size_streams[size] = temp.getvalue() 

348 

349 entries = [] 

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

351 stream = size_streams[size] 

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

353 

354 # Header 

355 fp.write(MAGIC) 

356 file_length = HEADERSIZE # Header 

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

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

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

360 

361 # TOC 

362 fp.write(b"TOC ") 

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

364 for entry in entries: 

365 fp.write(entry[0]) 

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

367 

368 # Data 

369 for entry in entries: 

370 fp.write(entry[0]) 

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

372 fp.write(entry[2]) 

373 

374 if hasattr(fp, "flush"): 

375 fp.flush() 

376 

377 

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

379 return prefix.startswith(MAGIC) 

380 

381 

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

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

384 

385Image.register_save(IcnsImageFile.format, _save) 

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

387 

388if __name__ == "__main__": 

389 if len(sys.argv) < 2: 

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

391 sys.exit() 

392 

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

394 imf = IcnsImageFile(fp) 

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

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

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

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

399 im.save("out.png") 

400 if sys.platform == "windows": 

401 os.startfile("out.png")