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

232 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 

21__lazy_modules__ = {"io", "struct"} 

22 

23import io 

24import os 

25import struct 

26import sys 

27 

28from . import Image, ImageFile, PngImagePlugin, features 

29 

30TYPE_CHECKING = False 

31if TYPE_CHECKING: 

32 from typing import IO 

33 

34enable_jpeg2k = features.check_codec("jpg_2000") 

35if enable_jpeg2k: 

36 from . import Jpeg2KImagePlugin 

37 

38MAGIC = b"icns" 

39HEADERSIZE = 8 

40 

41 

42def nextheader(fobj: IO[bytes]) -> tuple[bytes, int]: 

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

44 

45 

46def read_32t( 

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

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

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

50 start, length = start_length 

51 fobj.seek(start) 

52 sig = fobj.read(4) 

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

54 msg = "Unknown signature, expecting 0x00000000" 

55 raise SyntaxError(msg) 

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

57 

58 

59def read_32( 

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

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

62 """ 

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

64 an RLE packbits-like scheme. 

65 """ 

66 start, length = start_length 

67 fobj.seek(start) 

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

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

70 if length == sizesq * 3: 

71 # uncompressed ("RGBRGBGB") 

72 indata = fobj.read(length) 

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

74 else: 

75 # decode image 

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

77 for band_ix in range(3): 

78 data = [] 

79 bytesleft = sizesq 

80 while bytesleft > 0: 

81 byte = fobj.read(1) 

82 if not byte: 

83 break 

84 byte_int = byte[0] 

85 if byte_int & 0x80: 

86 blocksize = byte_int - 125 

87 byte = fobj.read(1) 

88 data.extend([byte] * blocksize) 

89 else: 

90 blocksize = byte_int + 1 

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

92 bytesleft -= blocksize 

93 if bytesleft <= 0: 

94 break 

95 if bytesleft != 0: 

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

97 raise SyntaxError(msg) 

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

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

100 return {"RGB": im} 

101 

102 

103def read_mk( 

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

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

106 # Alpha masks seem to be uncompressed 

107 start = start_length[0] 

108 fobj.seek(start) 

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

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

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

112 return {"A": band} 

113 

114 

115def read_png_or_jpeg2000( 

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

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

118 start, length = start_length 

119 fobj.seek(start) 

120 sig = fobj.read(12) 

121 

122 im: Image.Image 

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

124 fobj.seek(start) 

125 im = PngImagePlugin.PngImageFile(fobj) 

126 Image._decompression_bomb_check(im.size) 

127 return {"RGBA": im} 

128 elif ( 

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

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

131 ): 

132 if not enable_jpeg2k: 

133 msg = ( 

134 "Unsupported icon subimage format (rebuild PIL " 

135 "with JPEG 2000 support to fix this)" 

136 ) 

137 raise ValueError(msg) 

138 # j2k, jpc or j2c 

139 fobj.seek(start) 

140 jp2kstream = fobj.read(length) 

141 f = io.BytesIO(jp2kstream) 

142 im = Jpeg2KImagePlugin.Jpeg2KImageFile(f) 

143 Image._decompression_bomb_check(im.size) 

144 if im.mode != "RGBA": 

145 im = im.convert("RGBA") 

146 return {"RGBA": im} 

147 else: 

148 msg = "Unsupported icon subimage format" 

149 raise ValueError(msg) 

150 

151 

152class IcnsFile: 

153 SIZES = { 

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

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

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

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

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

159 (128, 128, 1): [ 

160 (b"ic07", read_png_or_jpeg2000), 

161 (b"it32", read_32t), 

162 (b"t8mk", read_mk), 

163 ], 

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

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

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

167 (32, 32, 1): [ 

168 (b"icp5", read_png_or_jpeg2000), 

169 (b"il32", read_32), 

170 (b"l8mk", read_mk), 

171 ], 

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

173 (16, 16, 1): [ 

174 (b"icp4", read_png_or_jpeg2000), 

175 (b"is32", read_32), 

176 (b"s8mk", read_mk), 

177 ], 

178 } 

179 

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

181 """ 

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

183 """ 

184 # signature : (start, length) 

185 self.dct = {} 

186 self.fobj = fobj 

187 sig, filesize = nextheader(fobj) 

188 if not _accept(sig): 

189 msg = "not an icns file" 

190 raise SyntaxError(msg) 

191 i = HEADERSIZE 

192 while i < filesize: 

193 sig, blocksize = nextheader(fobj) 

194 if blocksize <= 0: 

195 msg = "invalid block header" 

196 raise SyntaxError(msg) 

197 i += HEADERSIZE 

198 blocksize -= HEADERSIZE 

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

200 fobj.seek(blocksize, io.SEEK_CUR) 

201 i += blocksize 

202 

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

204 sizes = [] 

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

206 for fmt, reader in fmts: 

207 if fmt in self.dct: 

208 sizes.append(size) 

209 break 

210 return sizes 

211 

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

213 sizes = self.itersizes() 

214 if not sizes: 

215 msg = "No 32bit icon resources found" 

216 raise SyntaxError(msg) 

217 return max(sizes) 

218 

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

220 """ 

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

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

223 need to be flipped or transposed in some way. 

224 """ 

225 dct = {} 

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

227 desc = self.dct.get(code) 

228 if desc is not None: 

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

230 return dct 

231 

232 def getimage( 

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

234 ) -> Image.Image: 

235 if size is None: 

236 size = self.bestsize() 

237 elif len(size) == 2: 

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

239 channels = self.dataforsize(size) 

240 

241 im = channels.get("RGBA") 

242 if im: 

243 return im 

244 

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

246 try: 

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

248 except KeyError: 

249 pass 

250 return im 

251 

252 

253## 

254# Image plugin for Mac OS icons. 

255 

256 

257class IcnsImageFile(ImageFile.ImageFile): 

258 """ 

259 PIL image support for Mac OS .icns files. 

260 Chooses the best resolution, but will possibly load 

261 a different size image if you mutate the size attribute 

262 before calling 'load'. 

263 

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

265 of sizes that the icns file has. 

266 """ 

267 

268 format = "ICNS" 

269 format_description = "Mac OS icns resource" 

270 

271 def _open(self) -> None: 

272 assert self.fp is not None 

273 self.icns = IcnsFile(self.fp) 

274 self._mode = "RGBA" 

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

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

277 self.size = ( 

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

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

280 ) 

281 

282 @property 

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

284 return self._size 

285 

286 @size.setter 

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

288 # Check that a matching size exists, 

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

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

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

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

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

294 self._size = value 

295 return 

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

297 raise ValueError(msg) 

298 

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

300 if scale is not None: 

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

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

303 self.best_size = width, height, scale 

304 

305 px = Image.Image.load(self) 

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

307 # Already loaded 

308 return px 

309 self.load_prepare() 

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

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

312 

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

314 px = im.load() 

315 

316 self.im = im.im 

317 self._mode = im.mode 

318 self.size = im.size 

319 

320 return px 

321 

322 

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

324 """ 

325 Saves the image as a series of PNG files, 

326 that are then combined into a .icns file. 

327 """ 

328 if hasattr(fp, "flush"): 

329 fp.flush() 

330 

331 sizes = { 

332 b"ic07": 128, 

333 b"ic08": 256, 

334 b"ic09": 512, 

335 b"ic10": 1024, 

336 b"ic11": 32, 

337 b"ic12": 64, 

338 b"ic13": 256, 

339 b"ic14": 512, 

340 } 

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

342 size_streams = {} 

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

344 image = ( 

345 provided_images[size] 

346 if size in provided_images 

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

348 ) 

349 

350 temp = io.BytesIO() 

351 image.save(temp, "png") 

352 size_streams[size] = temp.getvalue() 

353 

354 entries = [] 

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

356 stream = size_streams[size] 

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

358 

359 # Header 

360 fp.write(MAGIC) 

361 file_length = HEADERSIZE # Header 

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

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

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

365 

366 # TOC 

367 fp.write(b"TOC ") 

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

369 for entry in entries: 

370 fp.write(entry[0]) 

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

372 

373 # Data 

374 for entry in entries: 

375 fp.write(entry[0]) 

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

377 fp.write(entry[2]) 

378 

379 if hasattr(fp, "flush"): 

380 fp.flush() 

381 

382 

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

384 return prefix.startswith(MAGIC) 

385 

386 

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

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

389 

390Image.register_save(IcnsImageFile.format, _save) 

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

392 

393if __name__ == "__main__": 

394 if len(sys.argv) < 2: 

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

396 sys.exit() 

397 

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

399 imf = IcnsImageFile(fp) 

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

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

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

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

404 im.save("out.png") 

405 if sys.platform == "windows": 

406 os.startfile("out.png")