Coverage for /pythoncovmergedfiles/medio/medio/usr/local/lib/python3.11/site-packages/PIL/PpmImagePlugin.py: 74%

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

238 statements  

1# 

2# The Python Imaging Library. 

3# $Id$ 

4# 

5# PPM support for PIL 

6# 

7# History: 

8# 96-03-24 fl Created 

9# 98-03-06 fl Write RGBA images (as RGB, that is) 

10# 

11# Copyright (c) Secret Labs AB 1997-98. 

12# Copyright (c) Fredrik Lundh 1996. 

13# 

14# See the README file for information on usage and redistribution. 

15# 

16from __future__ import annotations 

17 

18import math 

19from typing import IO 

20 

21from . import Image, ImageFile 

22from ._binary import i16be as i16 

23from ._binary import o8 

24from ._binary import o32le as o32 

25 

26# 

27# -------------------------------------------------------------------- 

28 

29b_whitespace = b"\x20\x09\x0a\x0b\x0c\x0d" 

30 

31MODES = { 

32 # standard 

33 b"P1": "1", 

34 b"P2": "L", 

35 b"P3": "RGB", 

36 b"P4": "1", 

37 b"P5": "L", 

38 b"P6": "RGB", 

39 # extensions 

40 b"P0CMYK": "CMYK", 

41 b"Pf": "F", 

42 # PIL extensions (for test purposes only) 

43 b"PyP": "P", 

44 b"PyRGBA": "RGBA", 

45 b"PyCMYK": "CMYK", 

46} 

47 

48 

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

50 return prefix.startswith(b"P") and prefix[1] in b"0123456fy" 

51 

52 

53## 

54# Image plugin for PBM, PGM, and PPM images. 

55 

56 

57class PpmImageFile(ImageFile.ImageFile): 

58 format = "PPM" 

59 format_description = "Pbmplus image" 

60 

61 def _read_magic(self) -> bytes: 

62 assert self.fp is not None 

63 

64 magic = b"" 

65 # read until whitespace or longest available magic number 

66 for _ in range(6): 

67 c = self.fp.read(1) 

68 if not c or c in b_whitespace: 

69 break 

70 magic += c 

71 return magic 

72 

73 def _read_token(self) -> bytes: 

74 assert self.fp is not None 

75 

76 token = b"" 

77 while len(token) <= 10: # read until next whitespace or limit of 10 characters 

78 c = self.fp.read(1) 

79 if not c: 

80 break 

81 elif c in b_whitespace: # token ended 

82 if not token: 

83 # skip whitespace at start 

84 continue 

85 break 

86 elif c == b"#": 

87 # ignores rest of the line; stops at CR, LF or EOF 

88 while self.fp.read(1) not in b"\r\n": 

89 pass 

90 continue 

91 token += c 

92 if not token: 

93 # Token was not even 1 byte 

94 msg = "Reached EOF while reading header" 

95 raise ValueError(msg) 

96 elif len(token) > 10: 

97 msg_too_long = b"Token too long in file header: %s" % token 

98 raise ValueError(msg_too_long) 

99 return token 

100 

101 def _open(self) -> None: 

102 assert self.fp is not None 

103 

104 magic_number = self._read_magic() 

105 try: 

106 mode = MODES[magic_number] 

107 except KeyError: 

108 msg = "not a PPM file" 

109 raise SyntaxError(msg) 

110 self._mode = mode 

111 

112 if magic_number in (b"P1", b"P4"): 

113 self.custom_mimetype = "image/x-portable-bitmap" 

114 elif magic_number in (b"P2", b"P5"): 

115 self.custom_mimetype = "image/x-portable-graymap" 

116 elif magic_number in (b"P3", b"P6"): 

117 self.custom_mimetype = "image/x-portable-pixmap" 

118 

119 self._size = int(self._read_token()), int(self._read_token()) 

120 

121 decoder_name = "raw" 

122 if magic_number in (b"P1", b"P2", b"P3"): 

123 decoder_name = "ppm_plain" 

124 

125 args: str | tuple[str | int, ...] 

126 if mode == "1": 

127 args = "1;I" 

128 elif mode == "F": 

129 scale = float(self._read_token()) 

130 if scale == 0.0 or not math.isfinite(scale): 

131 msg = "scale must be finite and non-zero" 

132 raise ValueError(msg) 

133 self.info["scale"] = abs(scale) 

134 

135 rawmode = "F;32F" if scale < 0 else "F;32BF" 

136 args = (rawmode, 0, -1) 

137 else: 

138 maxval = int(self._read_token()) 

139 if not 0 < maxval < 65536: 

140 msg = "maxval must be greater than 0 and less than 65536" 

141 raise ValueError(msg) 

142 if maxval > 255 and mode == "L": 

143 self._mode = "I" 

144 

145 rawmode = mode 

146 if decoder_name != "ppm_plain": 

147 # If maxval matches a bit depth, use the raw decoder directly 

148 if maxval == 65535 and mode == "L": 

149 rawmode = "I;16B" 

150 elif maxval != 255: 

151 decoder_name = "ppm" 

152 

153 args = rawmode if decoder_name == "raw" else (rawmode, maxval) 

154 self.tile = [ 

155 ImageFile._Tile(decoder_name, (0, 0) + self.size, self.fp.tell(), args) 

156 ] 

157 

158 

159# 

160# -------------------------------------------------------------------- 

161 

162 

163class PpmPlainDecoder(ImageFile.PyDecoder): 

164 _pulls_fd = True 

165 _comment_spans: bool 

166 

167 def _read_block(self) -> bytes: 

168 assert self.fd is not None 

169 

170 return self.fd.read(ImageFile.SAFEBLOCK) 

171 

172 def _find_comment_end(self, block: bytes, start: int = 0) -> int: 

173 a = block.find(b"\n", start) 

174 b = block.find(b"\r", start) 

175 return min(a, b) if a * b > 0 else max(a, b) # lowest nonnegative index (or -1) 

176 

177 def _ignore_comments(self, block: bytes) -> bytes: 

178 if self._comment_spans: 

179 # Finish current comment 

180 while block: 

181 comment_end = self._find_comment_end(block) 

182 if comment_end != -1: 

183 # Comment ends in this block 

184 # Delete tail of comment 

185 block = block[comment_end + 1 :] 

186 break 

187 else: 

188 # Comment spans whole block 

189 # So read the next block, looking for the end 

190 block = self._read_block() 

191 

192 # Search for any further comments 

193 self._comment_spans = False 

194 while True: 

195 comment_start = block.find(b"#") 

196 if comment_start == -1: 

197 # No comment found 

198 break 

199 comment_end = self._find_comment_end(block, comment_start) 

200 if comment_end != -1: 

201 # Comment ends in this block 

202 # Delete comment 

203 block = block[:comment_start] + block[comment_end + 1 :] 

204 else: 

205 # Comment continues to next block(s) 

206 block = block[:comment_start] 

207 self._comment_spans = True 

208 break 

209 return block 

210 

211 def _decode_bitonal(self) -> bytearray: 

212 """ 

213 This is a separate method because in the plain PBM format, all data tokens are 

214 exactly one byte, so the inter-token whitespace is optional. 

215 """ 

216 data = bytearray() 

217 total_bytes = self.state.xsize * self.state.ysize 

218 

219 while len(data) != total_bytes: 

220 block = self._read_block() # read next block 

221 if not block: 

222 # eof 

223 break 

224 

225 block = self._ignore_comments(block) 

226 

227 tokens = b"".join(block.split()) 

228 for token in tokens: 

229 if token not in (48, 49): 

230 msg = b"Invalid token for this mode: %s" % bytes([token]) 

231 raise ValueError(msg) 

232 data = (data + tokens)[:total_bytes] 

233 invert = bytes.maketrans(b"01", b"\xff\x00") 

234 return data.translate(invert) 

235 

236 def _decode_blocks(self, maxval: int) -> bytearray: 

237 data = bytearray() 

238 max_len = 10 

239 out_byte_count = 4 if self.mode == "I" else 1 

240 out_max = 65535 if self.mode == "I" else 255 

241 bands = Image.getmodebands(self.mode) 

242 total_bytes = self.state.xsize * self.state.ysize * bands * out_byte_count 

243 

244 half_token = b"" 

245 while len(data) != total_bytes: 

246 block = self._read_block() # read next block 

247 if not block: 

248 if half_token: 

249 block = bytearray(b" ") # flush half_token 

250 else: 

251 # eof 

252 break 

253 

254 block = self._ignore_comments(block) 

255 

256 if half_token: 

257 block = half_token + block # stitch half_token to new block 

258 half_token = b"" 

259 

260 tokens = block.split() 

261 

262 if block and not block[-1:].isspace(): # block might split token 

263 half_token = tokens.pop() # save half token for later 

264 if len(half_token) > max_len: # prevent buildup of half_token 

265 msg = ( 

266 b"Token too long found in data: %s" % half_token[: max_len + 1] 

267 ) 

268 raise ValueError(msg) 

269 

270 for token in tokens: 

271 if len(token) > max_len: 

272 msg = b"Token too long found in data: %s" % token[: max_len + 1] 

273 raise ValueError(msg) 

274 value = int(token) 

275 if value < 0: 

276 msg_str = f"Channel value is negative: {value}" 

277 raise ValueError(msg_str) 

278 if value > maxval: 

279 msg_str = f"Channel value too large for this mode: {value}" 

280 raise ValueError(msg_str) 

281 value = round(value / maxval * out_max) 

282 data += o32(value) if self.mode == "I" else o8(value) 

283 if len(data) == total_bytes: # finished! 

284 break 

285 return data 

286 

287 def decode(self, buffer: bytes | Image.SupportsArrayInterface) -> tuple[int, int]: 

288 self._comment_spans = False 

289 if self.mode == "1": 

290 data = self._decode_bitonal() 

291 rawmode = "1;8" 

292 else: 

293 maxval = self.args[-1] 

294 data = self._decode_blocks(maxval) 

295 rawmode = "I;32" if self.mode == "I" else self.mode 

296 self.set_as_raw(bytes(data), rawmode) 

297 return -1, 0 

298 

299 

300class PpmDecoder(ImageFile.PyDecoder): 

301 _pulls_fd = True 

302 

303 def decode(self, buffer: bytes | Image.SupportsArrayInterface) -> tuple[int, int]: 

304 assert self.fd is not None 

305 

306 data = bytearray() 

307 maxval = self.args[-1] 

308 in_byte_count = 1 if maxval < 256 else 2 

309 out_byte_count = 4 if self.mode == "I" else 1 

310 out_max = 65535 if self.mode == "I" else 255 

311 bands = Image.getmodebands(self.mode) 

312 dest_length = self.state.xsize * self.state.ysize * bands * out_byte_count 

313 while len(data) < dest_length: 

314 pixels = self.fd.read(in_byte_count * bands) 

315 if len(pixels) < in_byte_count * bands: 

316 # eof 

317 break 

318 for b in range(bands): 

319 value = ( 

320 pixels[b] if in_byte_count == 1 else i16(pixels, b * in_byte_count) 

321 ) 

322 value = min(out_max, round(value / maxval * out_max)) 

323 data += o32(value) if self.mode == "I" else o8(value) 

324 rawmode = "I;32" if self.mode == "I" else self.mode 

325 self.set_as_raw(bytes(data), rawmode) 

326 return -1, 0 

327 

328 

329# 

330# -------------------------------------------------------------------- 

331 

332 

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

334 if im.mode == "1": 

335 rawmode, head = "1;I", b"P4" 

336 elif im.mode == "L": 

337 rawmode, head = "L", b"P5" 

338 elif im.mode in ("I", "I;16"): 

339 rawmode, head = "I;16B", b"P5" 

340 elif im.mode in ("RGB", "RGBA"): 

341 rawmode, head = "RGB", b"P6" 

342 elif im.mode == "F": 

343 rawmode, head = "F;32F", b"Pf" 

344 else: 

345 msg = f"cannot write mode {im.mode} as PPM" 

346 raise OSError(msg) 

347 fp.write(head + b"\n%d %d\n" % im.size) 

348 if head == b"P6": 

349 fp.write(b"255\n") 

350 elif head == b"P5": 

351 if rawmode == "L": 

352 fp.write(b"255\n") 

353 else: 

354 fp.write(b"65535\n") 

355 elif head == b"Pf": 

356 fp.write(b"-1.0\n") 

357 row_order = -1 if im.mode == "F" else 1 

358 ImageFile._save( 

359 im, fp, [ImageFile._Tile("raw", (0, 0) + im.size, 0, (rawmode, 0, row_order))] 

360 ) 

361 

362 

363# 

364# -------------------------------------------------------------------- 

365 

366 

367Image.register_open(PpmImageFile.format, PpmImageFile, _accept) 

368Image.register_save(PpmImageFile.format, _save) 

369 

370Image.register_decoder("ppm", PpmDecoder) 

371Image.register_decoder("ppm_plain", PpmPlainDecoder) 

372 

373Image.register_extensions(PpmImageFile.format, [".pbm", ".pgm", ".ppm", ".pnm", ".pfm"]) 

374 

375Image.register_mime(PpmImageFile.format, "image/x-portable-anymap")