Coverage for /pythoncovmergedfiles/medio/medio/usr/local/lib/python3.8/site-packages/pillow-10.4.0-py3.8-linux-x86_64.egg/PIL/PpmImagePlugin.py: 15%

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

236 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[0:1] == 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 = f"Token too long in file header: {token.decode()}" 

98 raise ValueError(msg) 

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 = [(decoder_name, (0, 0) + self.size, self.fp.tell(), args)] 

155 

156 

157# 

158# -------------------------------------------------------------------- 

159 

160 

161class PpmPlainDecoder(ImageFile.PyDecoder): 

162 _pulls_fd = True 

163 _comment_spans: bool 

164 

165 def _read_block(self) -> bytes: 

166 assert self.fd is not None 

167 

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

169 

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

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

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

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

174 

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

176 if self._comment_spans: 

177 # Finish current comment 

178 while block: 

179 comment_end = self._find_comment_end(block) 

180 if comment_end != -1: 

181 # Comment ends in this block 

182 # Delete tail of comment 

183 block = block[comment_end + 1 :] 

184 break 

185 else: 

186 # Comment spans whole block 

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

188 block = self._read_block() 

189 

190 # Search for any further comments 

191 self._comment_spans = False 

192 while True: 

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

194 if comment_start == -1: 

195 # No comment found 

196 break 

197 comment_end = self._find_comment_end(block, comment_start) 

198 if comment_end != -1: 

199 # Comment ends in this block 

200 # Delete comment 

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

202 else: 

203 # Comment continues to next block(s) 

204 block = block[:comment_start] 

205 self._comment_spans = True 

206 break 

207 return block 

208 

209 def _decode_bitonal(self) -> bytearray: 

210 """ 

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

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

213 """ 

214 data = bytearray() 

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

216 

217 while len(data) != total_bytes: 

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

219 if not block: 

220 # eof 

221 break 

222 

223 block = self._ignore_comments(block) 

224 

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

226 for token in tokens: 

227 if token not in (48, 49): 

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

229 raise ValueError(msg) 

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

231 invert = bytes.maketrans(b"01", b"\xFF\x00") 

232 return data.translate(invert) 

233 

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

235 data = bytearray() 

236 max_len = 10 

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

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

239 bands = Image.getmodebands(self.mode) 

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

241 

242 half_token = b"" 

243 while len(data) != total_bytes: 

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

245 if not block: 

246 if half_token: 

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

248 else: 

249 # eof 

250 break 

251 

252 block = self._ignore_comments(block) 

253 

254 if half_token: 

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

256 half_token = b"" 

257 

258 tokens = block.split() 

259 

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

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

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

263 msg = ( 

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

265 ) 

266 raise ValueError(msg) 

267 

268 for token in tokens: 

269 if len(token) > max_len: 

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

271 raise ValueError(msg) 

272 value = int(token) 

273 if value < 0: 

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

275 raise ValueError(msg_str) 

276 if value > maxval: 

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

278 raise ValueError(msg_str) 

279 value = round(value / maxval * out_max) 

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

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

282 break 

283 return data 

284 

285 def decode(self, buffer: bytes) -> tuple[int, int]: 

286 self._comment_spans = False 

287 if self.mode == "1": 

288 data = self._decode_bitonal() 

289 rawmode = "1;8" 

290 else: 

291 maxval = self.args[-1] 

292 data = self._decode_blocks(maxval) 

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

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

295 return -1, 0 

296 

297 

298class PpmDecoder(ImageFile.PyDecoder): 

299 _pulls_fd = True 

300 

301 def decode(self, buffer: bytes) -> tuple[int, int]: 

302 assert self.fd is not None 

303 

304 data = bytearray() 

305 maxval = self.args[-1] 

306 in_byte_count = 1 if maxval < 256 else 2 

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

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

309 bands = Image.getmodebands(self.mode) 

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

311 while len(data) < dest_length: 

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

313 if len(pixels) < in_byte_count * bands: 

314 # eof 

315 break 

316 for b in range(bands): 

317 value = ( 

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

319 ) 

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

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

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

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

324 return -1, 0 

325 

326 

327# 

328# -------------------------------------------------------------------- 

329 

330 

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

332 if im.mode == "1": 

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

334 elif im.mode == "L": 

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

336 elif im.mode == "I": 

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

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

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

340 elif im.mode == "F": 

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

342 else: 

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

344 raise OSError(msg) 

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

346 if head == b"P6": 

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

348 elif head == b"P5": 

349 if rawmode == "L": 

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

351 else: 

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

353 elif head == b"Pf": 

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

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

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

357 

358 

359# 

360# -------------------------------------------------------------------- 

361 

362 

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

364Image.register_save(PpmImageFile.format, _save) 

365 

366Image.register_decoder("ppm", PpmDecoder) 

367Image.register_decoder("ppm_plain", PpmPlainDecoder) 

368 

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

370 

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