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 data.extend([byte] * blocksize) 

84 else: 

85 blocksize = byte_int + 1 

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

87 bytesleft -= blocksize 

88 if bytesleft <= 0: 

89 break 

90 if bytesleft != 0: 

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

92 raise SyntaxError(msg) 

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

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

95 return {"RGB": im} 

96 

97 

98def read_mk( 

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

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

101 # Alpha masks seem to be uncompressed 

102 start = start_length[0] 

103 fobj.seek(start) 

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

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

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

107 return {"A": band} 

108 

109 

110def read_png_or_jpeg2000( 

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

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

113 start, length = start_length 

114 fobj.seek(start) 

115 sig = fobj.read(12) 

116 

117 im: Image.Image 

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

119 fobj.seek(start) 

120 im = PngImagePlugin.PngImageFile(fobj) 

121 Image._decompression_bomb_check(im.size) 

122 return {"RGBA": im} 

123 elif ( 

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

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

126 ): 

127 if not enable_jpeg2k: 

128 msg = ( 

129 "Unsupported icon subimage format (rebuild PIL " 

130 "with JPEG 2000 support to fix this)" 

131 ) 

132 raise ValueError(msg) 

133 # j2k, jpc or j2c 

134 fobj.seek(start) 

135 jp2kstream = fobj.read(length) 

136 f = io.BytesIO(jp2kstream) 

137 im = Jpeg2KImagePlugin.Jpeg2KImageFile(f) 

138 Image._decompression_bomb_check(im.size) 

139 if im.mode != "RGBA": 

140 im = im.convert("RGBA") 

141 return {"RGBA": im} 

142 else: 

143 msg = "Unsupported icon subimage format" 

144 raise ValueError(msg) 

145 

146 

147class IcnsFile: 

148 SIZES = { 

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

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

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

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

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

154 (128, 128, 1): [ 

155 (b"ic07", read_png_or_jpeg2000), 

156 (b"it32", read_32t), 

157 (b"t8mk", read_mk), 

158 ], 

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

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

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

162 (32, 32, 1): [ 

163 (b"icp5", read_png_or_jpeg2000), 

164 (b"il32", read_32), 

165 (b"l8mk", read_mk), 

166 ], 

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

168 (16, 16, 1): [ 

169 (b"icp4", read_png_or_jpeg2000), 

170 (b"is32", read_32), 

171 (b"s8mk", read_mk), 

172 ], 

173 } 

174 

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

176 """ 

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

178 """ 

179 # signature : (start, length) 

180 self.dct = {} 

181 self.fobj = fobj 

182 sig, filesize = nextheader(fobj) 

183 if not _accept(sig): 

184 msg = "not an icns file" 

185 raise SyntaxError(msg) 

186 i = HEADERSIZE 

187 while i < filesize: 

188 sig, blocksize = nextheader(fobj) 

189 if blocksize <= 0: 

190 msg = "invalid block header" 

191 raise SyntaxError(msg) 

192 i += HEADERSIZE 

193 blocksize -= HEADERSIZE 

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

195 fobj.seek(blocksize, io.SEEK_CUR) 

196 i += blocksize 

197 

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

199 sizes = [] 

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

201 for fmt, reader in fmts: 

202 if fmt in self.dct: 

203 sizes.append(size) 

204 break 

205 return sizes 

206 

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

208 sizes = self.itersizes() 

209 if not sizes: 

210 msg = "No 32bit icon resources found" 

211 raise SyntaxError(msg) 

212 return max(sizes) 

213 

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

215 """ 

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

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

218 need to be flipped or transposed in some way. 

219 """ 

220 dct = {} 

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

222 desc = self.dct.get(code) 

223 if desc is not None: 

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

225 return dct 

226 

227 def getimage( 

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

229 ) -> Image.Image: 

230 if size is None: 

231 size = self.bestsize() 

232 elif len(size) == 2: 

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

234 channels = self.dataforsize(size) 

235 

236 im = channels.get("RGBA") 

237 if im: 

238 return im 

239 

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

241 try: 

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

243 except KeyError: 

244 pass 

245 return im 

246 

247 

248## 

249# Image plugin for Mac OS icons. 

250 

251 

252class IcnsImageFile(ImageFile.ImageFile): 

253 """ 

254 PIL image support for Mac OS .icns files. 

255 Chooses the best resolution, but will possibly load 

256 a different size image if you mutate the size attribute 

257 before calling 'load'. 

258 

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

260 of sizes that the icns file has. 

261 """ 

262 

263 format = "ICNS" 

264 format_description = "Mac OS icns resource" 

265 

266 def _open(self) -> None: 

267 assert self.fp is not 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")