Coverage for /pythoncovmergedfiles/medio/medio/usr/local/lib/python3.10/site-packages/pillow-11.0.0-py3.10-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

237 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 

28from ._deprecate import deprecate 

29 

30enable_jpeg2k = features.check_codec("jpg_2000") 

31if enable_jpeg2k: 

32 from . import Jpeg2KImagePlugin 

33 

34MAGIC = b"icns" 

35HEADERSIZE = 8 

36 

37 

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

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

40 

41 

42def read_32t( 

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

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

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

46 (start, length) = start_length 

47 fobj.seek(start) 

48 sig = fobj.read(4) 

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

50 msg = "Unknown signature, expecting 0x00000000" 

51 raise SyntaxError(msg) 

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

53 

54 

55def read_32( 

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

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

58 """ 

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

60 an RLE packbits-like scheme. 

61 """ 

62 (start, length) = start_length 

63 fobj.seek(start) 

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

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

66 if length == sizesq * 3: 

67 # uncompressed ("RGBRGBGB") 

68 indata = fobj.read(length) 

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

70 else: 

71 # decode image 

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

73 for band_ix in range(3): 

74 data = [] 

75 bytesleft = sizesq 

76 while bytesleft > 0: 

77 byte = fobj.read(1) 

78 if not byte: 

79 break 

80 byte_int = byte[0] 

81 if byte_int & 0x80: 

82 blocksize = byte_int - 125 

83 byte = fobj.read(1) 

84 for i in range(blocksize): 

85 data.append(byte) 

86 else: 

87 blocksize = byte_int + 1 

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

89 bytesleft -= blocksize 

90 if bytesleft <= 0: 

91 break 

92 if bytesleft != 0: 

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

94 raise SyntaxError(msg) 

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

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

97 return {"RGB": im} 

98 

99 

100def read_mk( 

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

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

103 # Alpha masks seem to be uncompressed 

104 start = start_length[0] 

105 fobj.seek(start) 

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

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

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

109 return {"A": band} 

110 

111 

112def read_png_or_jpeg2000( 

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

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

115 (start, length) = start_length 

116 fobj.seek(start) 

117 sig = fobj.read(12) 

118 

119 im: Image.Image 

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

121 fobj.seek(start) 

122 im = PngImagePlugin.PngImageFile(fobj) 

123 Image._decompression_bomb_check(im.size) 

124 return {"RGBA": im} 

125 elif ( 

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

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

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

129 ): 

130 if not enable_jpeg2k: 

131 msg = ( 

132 "Unsupported icon subimage format (rebuild PIL " 

133 "with JPEG 2000 support to fix this)" 

134 ) 

135 raise ValueError(msg) 

136 # j2k, jpc or j2c 

137 fobj.seek(start) 

138 jp2kstream = fobj.read(length) 

139 f = io.BytesIO(jp2kstream) 

140 im = Jpeg2KImagePlugin.Jpeg2KImageFile(f) 

141 Image._decompression_bomb_check(im.size) 

142 if im.mode != "RGBA": 

143 im = im.convert("RGBA") 

144 return {"RGBA": im} 

145 else: 

146 msg = "Unsupported icon subimage format" 

147 raise ValueError(msg) 

148 

149 

150class IcnsFile: 

151 SIZES = { 

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

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

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

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

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

157 (128, 128, 1): [ 

158 (b"ic07", read_png_or_jpeg2000), 

159 (b"it32", read_32t), 

160 (b"t8mk", read_mk), 

161 ], 

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

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

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

165 (32, 32, 1): [ 

166 (b"icp5", read_png_or_jpeg2000), 

167 (b"il32", read_32), 

168 (b"l8mk", read_mk), 

169 ], 

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

171 (16, 16, 1): [ 

172 (b"icp4", read_png_or_jpeg2000), 

173 (b"is32", read_32), 

174 (b"s8mk", read_mk), 

175 ], 

176 } 

177 

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

179 """ 

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

181 """ 

182 # signature : (start, length) 

183 self.dct = {} 

184 self.fobj = fobj 

185 sig, filesize = nextheader(fobj) 

186 if not _accept(sig): 

187 msg = "not an icns file" 

188 raise SyntaxError(msg) 

189 i = HEADERSIZE 

190 while i < filesize: 

191 sig, blocksize = nextheader(fobj) 

192 if blocksize <= 0: 

193 msg = "invalid block header" 

194 raise SyntaxError(msg) 

195 i += HEADERSIZE 

196 blocksize -= HEADERSIZE 

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

198 fobj.seek(blocksize, io.SEEK_CUR) 

199 i += blocksize 

200 

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

202 sizes = [] 

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

204 for fmt, reader in fmts: 

205 if fmt in self.dct: 

206 sizes.append(size) 

207 break 

208 return sizes 

209 

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

211 sizes = self.itersizes() 

212 if not sizes: 

213 msg = "No 32bit icon resources found" 

214 raise SyntaxError(msg) 

215 return max(sizes) 

216 

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

218 """ 

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

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

221 need to be flipped or transposed in some way. 

222 """ 

223 dct = {} 

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

225 desc = self.dct.get(code) 

226 if desc is not None: 

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

228 return dct 

229 

230 def getimage( 

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

232 ) -> Image.Image: 

233 if size is None: 

234 size = self.bestsize() 

235 elif len(size) == 2: 

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

237 channels = self.dataforsize(size) 

238 

239 im = channels.get("RGBA") 

240 if im: 

241 return im 

242 

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

244 try: 

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

246 except KeyError: 

247 pass 

248 return im 

249 

250 

251## 

252# Image plugin for Mac OS icons. 

253 

254 

255class IcnsImageFile(ImageFile.ImageFile): 

256 """ 

257 PIL image support for Mac OS .icns files. 

258 Chooses the best resolution, but will possibly load 

259 a different size image if you mutate the size attribute 

260 before calling 'load'. 

261 

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

263 of sizes that the icns file has. 

264 """ 

265 

266 format = "ICNS" 

267 format_description = "Mac OS icns resource" 

268 

269 def _open(self) -> None: 

270 self.icns = IcnsFile(self.fp) 

271 self._mode = "RGBA" 

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

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

274 self.size = ( 

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

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

277 ) 

278 

279 @property # type: ignore[override] 

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

281 return self._size 

282 

283 @size.setter 

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

285 if len(value) == 3: 

286 deprecate("Setting size to (width, height, scale)", 12, "load(scale)") 

287 if value in self.info["sizes"]: 

288 self._size = value # type: ignore[assignment] 

289 return 

290 else: 

291 # Check that a matching size exists, 

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

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

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

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

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

297 self._size = value 

298 return 

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

300 raise ValueError(msg) 

301 

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

303 if scale is not None or len(self.size) == 3: 

304 if scale is None and len(self.size) == 3: 

305 scale = self.size[2] 

306 assert scale is not None 

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

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

309 self.best_size = width, height, scale 

310 

311 px = Image.Image.load(self) 

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

313 # Already loaded 

314 return px 

315 self.load_prepare() 

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

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

318 

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

320 px = im.load() 

321 

322 self.im = im.im 

323 self._mode = im.mode 

324 self.size = im.size 

325 

326 return px 

327 

328 

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

330 """ 

331 Saves the image as a series of PNG files, 

332 that are then combined into a .icns file. 

333 """ 

334 if hasattr(fp, "flush"): 

335 fp.flush() 

336 

337 sizes = { 

338 b"ic07": 128, 

339 b"ic08": 256, 

340 b"ic09": 512, 

341 b"ic10": 1024, 

342 b"ic11": 32, 

343 b"ic12": 64, 

344 b"ic13": 256, 

345 b"ic14": 512, 

346 } 

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

348 size_streams = {} 

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

350 image = ( 

351 provided_images[size] 

352 if size in provided_images 

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

354 ) 

355 

356 temp = io.BytesIO() 

357 image.save(temp, "png") 

358 size_streams[size] = temp.getvalue() 

359 

360 entries = [] 

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

362 stream = size_streams[size] 

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

364 

365 # Header 

366 fp.write(MAGIC) 

367 file_length = HEADERSIZE # Header 

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

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

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

371 

372 # TOC 

373 fp.write(b"TOC ") 

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

375 for entry in entries: 

376 fp.write(entry[0]) 

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

378 

379 # Data 

380 for entry in entries: 

381 fp.write(entry[0]) 

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

383 fp.write(entry[2]) 

384 

385 if hasattr(fp, "flush"): 

386 fp.flush() 

387 

388 

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

390 return prefix[:4] == MAGIC 

391 

392 

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

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

395 

396Image.register_save(IcnsImageFile.format, _save) 

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

398 

399if __name__ == "__main__": 

400 if len(sys.argv) < 2: 

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

402 sys.exit() 

403 

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

405 imf = IcnsImageFile(fp) 

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

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

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

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

410 im.save("out.png") 

411 if sys.platform == "windows": 

412 os.startfile("out.png")