Coverage for /pythoncovmergedfiles/medio/medio/usr/local/lib/python3.8/site-packages/pygments/formatters/img.py: 25%

295 statements  

« prev     ^ index     » next       coverage.py v7.2.2, created at 2023-03-26 07:45 +0000

1""" 

2 pygments.formatters.img 

3 ~~~~~~~~~~~~~~~~~~~~~~~ 

4 

5 Formatter for Pixmap output. 

6 

7 :copyright: Copyright 2006-2022 by the Pygments team, see AUTHORS. 

8 :license: BSD, see LICENSE for details. 

9""" 

10 

11import os 

12import sys 

13 

14from pygments.formatter import Formatter 

15from pygments.util import get_bool_opt, get_int_opt, get_list_opt, \ 

16 get_choice_opt 

17 

18import subprocess 

19 

20# Import this carefully 

21try: 

22 from PIL import Image, ImageDraw, ImageFont 

23 pil_available = True 

24except ImportError: 

25 pil_available = False 

26 

27try: 

28 import _winreg 

29except ImportError: 

30 try: 

31 import winreg as _winreg 

32 except ImportError: 

33 _winreg = None 

34 

35__all__ = ['ImageFormatter', 'GifImageFormatter', 'JpgImageFormatter', 

36 'BmpImageFormatter'] 

37 

38 

39# For some unknown reason every font calls it something different 

40STYLES = { 

41 'NORMAL': ['', 'Roman', 'Book', 'Normal', 'Regular', 'Medium'], 

42 'ITALIC': ['Oblique', 'Italic'], 

43 'BOLD': ['Bold'], 

44 'BOLDITALIC': ['Bold Oblique', 'Bold Italic'], 

45} 

46 

47# A sane default for modern systems 

48DEFAULT_FONT_NAME_NIX = 'DejaVu Sans Mono' 

49DEFAULT_FONT_NAME_WIN = 'Courier New' 

50DEFAULT_FONT_NAME_MAC = 'Menlo' 

51 

52 

53class PilNotAvailable(ImportError): 

54 """When Python imaging library is not available""" 

55 

56 

57class FontNotFound(Exception): 

58 """When there are no usable fonts specified""" 

59 

60 

61class FontManager: 

62 """ 

63 Manages a set of fonts: normal, italic, bold, etc... 

64 """ 

65 

66 def __init__(self, font_name, font_size=14): 

67 self.font_name = font_name 

68 self.font_size = font_size 

69 self.fonts = {} 

70 self.encoding = None 

71 if sys.platform.startswith('win'): 

72 if not font_name: 

73 self.font_name = DEFAULT_FONT_NAME_WIN 

74 self._create_win() 

75 elif sys.platform.startswith('darwin'): 

76 if not font_name: 

77 self.font_name = DEFAULT_FONT_NAME_MAC 

78 self._create_mac() 

79 else: 

80 if not font_name: 

81 self.font_name = DEFAULT_FONT_NAME_NIX 

82 self._create_nix() 

83 

84 def _get_nix_font_path(self, name, style): 

85 proc = subprocess.Popen(['fc-list', "%s:style=%s" % (name, style), 'file'], 

86 stdout=subprocess.PIPE, stderr=None) 

87 stdout, _ = proc.communicate() 

88 if proc.returncode == 0: 

89 lines = stdout.splitlines() 

90 for line in lines: 

91 if line.startswith(b'Fontconfig warning:'): 

92 continue 

93 path = line.decode().strip().strip(':') 

94 if path: 

95 return path 

96 return None 

97 

98 def _create_nix(self): 

99 for name in STYLES['NORMAL']: 

100 path = self._get_nix_font_path(self.font_name, name) 

101 if path is not None: 

102 self.fonts['NORMAL'] = ImageFont.truetype(path, self.font_size) 

103 break 

104 else: 

105 raise FontNotFound('No usable fonts named: "%s"' % 

106 self.font_name) 

107 for style in ('ITALIC', 'BOLD', 'BOLDITALIC'): 

108 for stylename in STYLES[style]: 

109 path = self._get_nix_font_path(self.font_name, stylename) 

110 if path is not None: 

111 self.fonts[style] = ImageFont.truetype(path, self.font_size) 

112 break 

113 else: 

114 if style == 'BOLDITALIC': 

115 self.fonts[style] = self.fonts['BOLD'] 

116 else: 

117 self.fonts[style] = self.fonts['NORMAL'] 

118 

119 def _get_mac_font_path(self, font_map, name, style): 

120 return font_map.get((name + ' ' + style).strip().lower()) 

121 

122 def _create_mac(self): 

123 font_map = {} 

124 for font_dir in (os.path.join(os.getenv("HOME"), 'Library/Fonts/'), 

125 '/Library/Fonts/', '/System/Library/Fonts/'): 

126 font_map.update( 

127 (os.path.splitext(f)[0].lower(), os.path.join(font_dir, f)) 

128 for f in os.listdir(font_dir) 

129 if f.lower().endswith(('ttf', 'ttc'))) 

130 

131 for name in STYLES['NORMAL']: 

132 path = self._get_mac_font_path(font_map, self.font_name, name) 

133 if path is not None: 

134 self.fonts['NORMAL'] = ImageFont.truetype(path, self.font_size) 

135 break 

136 else: 

137 raise FontNotFound('No usable fonts named: "%s"' % 

138 self.font_name) 

139 for style in ('ITALIC', 'BOLD', 'BOLDITALIC'): 

140 for stylename in STYLES[style]: 

141 path = self._get_mac_font_path(font_map, self.font_name, stylename) 

142 if path is not None: 

143 self.fonts[style] = ImageFont.truetype(path, self.font_size) 

144 break 

145 else: 

146 if style == 'BOLDITALIC': 

147 self.fonts[style] = self.fonts['BOLD'] 

148 else: 

149 self.fonts[style] = self.fonts['NORMAL'] 

150 

151 def _lookup_win(self, key, basename, styles, fail=False): 

152 for suffix in ('', ' (TrueType)'): 

153 for style in styles: 

154 try: 

155 valname = '%s%s%s' % (basename, style and ' '+style, suffix) 

156 val, _ = _winreg.QueryValueEx(key, valname) 

157 return val 

158 except OSError: 

159 continue 

160 else: 

161 if fail: 

162 raise FontNotFound('Font %s (%s) not found in registry' % 

163 (basename, styles[0])) 

164 return None 

165 

166 def _create_win(self): 

167 lookuperror = None 

168 keynames = [ (_winreg.HKEY_CURRENT_USER, r'Software\Microsoft\Windows NT\CurrentVersion\Fonts'), 

169 (_winreg.HKEY_CURRENT_USER, r'Software\Microsoft\Windows\CurrentVersion\Fonts'), 

170 (_winreg.HKEY_LOCAL_MACHINE, r'Software\Microsoft\Windows NT\CurrentVersion\Fonts'), 

171 (_winreg.HKEY_LOCAL_MACHINE, r'Software\Microsoft\Windows\CurrentVersion\Fonts') ] 

172 for keyname in keynames: 

173 try: 

174 key = _winreg.OpenKey(*keyname) 

175 try: 

176 path = self._lookup_win(key, self.font_name, STYLES['NORMAL'], True) 

177 self.fonts['NORMAL'] = ImageFont.truetype(path, self.font_size) 

178 for style in ('ITALIC', 'BOLD', 'BOLDITALIC'): 

179 path = self._lookup_win(key, self.font_name, STYLES[style]) 

180 if path: 

181 self.fonts[style] = ImageFont.truetype(path, self.font_size) 

182 else: 

183 if style == 'BOLDITALIC': 

184 self.fonts[style] = self.fonts['BOLD'] 

185 else: 

186 self.fonts[style] = self.fonts['NORMAL'] 

187 return 

188 except FontNotFound as err: 

189 lookuperror = err 

190 finally: 

191 _winreg.CloseKey(key) 

192 except OSError: 

193 pass 

194 else: 

195 # If we get here, we checked all registry keys and had no luck 

196 # We can be in one of two situations now: 

197 # * All key lookups failed. In this case lookuperror is None and we 

198 # will raise a generic error 

199 # * At least one lookup failed with a FontNotFound error. In this 

200 # case, we will raise that as a more specific error 

201 if lookuperror: 

202 raise lookuperror 

203 raise FontNotFound('Can\'t open Windows font registry key') 

204 

205 def get_char_size(self): 

206 """ 

207 Get the character size. 

208 """ 

209 return self.get_text_size('M') 

210 

211 def get_text_size(self, text): 

212 """ 

213 Get the text size (width, height). 

214 """ 

215 font = self.fonts['NORMAL'] 

216 if hasattr(font, 'getbbox'): # Pillow >= 9.2.0 

217 return font.getbbox(text)[2:4] 

218 else: 

219 return font.getsize(text) 

220 

221 def get_font(self, bold, oblique): 

222 """ 

223 Get the font based on bold and italic flags. 

224 """ 

225 if bold and oblique: 

226 return self.fonts['BOLDITALIC'] 

227 elif bold: 

228 return self.fonts['BOLD'] 

229 elif oblique: 

230 return self.fonts['ITALIC'] 

231 else: 

232 return self.fonts['NORMAL'] 

233 

234 

235class ImageFormatter(Formatter): 

236 """ 

237 Create a PNG image from source code. This uses the Python Imaging Library to 

238 generate a pixmap from the source code. 

239 

240 .. versionadded:: 0.10 

241 

242 Additional options accepted: 

243 

244 `image_format` 

245 An image format to output to that is recognised by PIL, these include: 

246 

247 * "PNG" (default) 

248 * "JPEG" 

249 * "BMP" 

250 * "GIF" 

251 

252 `line_pad` 

253 The extra spacing (in pixels) between each line of text. 

254 

255 Default: 2 

256 

257 `font_name` 

258 The font name to be used as the base font from which others, such as 

259 bold and italic fonts will be generated. This really should be a 

260 monospace font to look sane. 

261 

262 Default: "Courier New" on Windows, "Menlo" on Mac OS, and 

263 "DejaVu Sans Mono" on \\*nix 

264 

265 `font_size` 

266 The font size in points to be used. 

267 

268 Default: 14 

269 

270 `image_pad` 

271 The padding, in pixels to be used at each edge of the resulting image. 

272 

273 Default: 10 

274 

275 `line_numbers` 

276 Whether line numbers should be shown: True/False 

277 

278 Default: True 

279 

280 `line_number_start` 

281 The line number of the first line. 

282 

283 Default: 1 

284 

285 `line_number_step` 

286 The step used when printing line numbers. 

287 

288 Default: 1 

289 

290 `line_number_bg` 

291 The background colour (in "#123456" format) of the line number bar, or 

292 None to use the style background color. 

293 

294 Default: "#eed" 

295 

296 `line_number_fg` 

297 The text color of the line numbers (in "#123456"-like format). 

298 

299 Default: "#886" 

300 

301 `line_number_chars` 

302 The number of columns of line numbers allowable in the line number 

303 margin. 

304 

305 Default: 2 

306 

307 `line_number_bold` 

308 Whether line numbers will be bold: True/False 

309 

310 Default: False 

311 

312 `line_number_italic` 

313 Whether line numbers will be italicized: True/False 

314 

315 Default: False 

316 

317 `line_number_separator` 

318 Whether a line will be drawn between the line number area and the 

319 source code area: True/False 

320 

321 Default: True 

322 

323 `line_number_pad` 

324 The horizontal padding (in pixels) between the line number margin, and 

325 the source code area. 

326 

327 Default: 6 

328 

329 `hl_lines` 

330 Specify a list of lines to be highlighted. 

331 

332 .. versionadded:: 1.2 

333 

334 Default: empty list 

335 

336 `hl_color` 

337 Specify the color for highlighting lines. 

338 

339 .. versionadded:: 1.2 

340 

341 Default: highlight color of the selected style 

342 """ 

343 

344 # Required by the pygments mapper 

345 name = 'img' 

346 aliases = ['img', 'IMG', 'png'] 

347 filenames = ['*.png'] 

348 

349 unicodeoutput = False 

350 

351 default_image_format = 'png' 

352 

353 def __init__(self, **options): 

354 """ 

355 See the class docstring for explanation of options. 

356 """ 

357 if not pil_available: 

358 raise PilNotAvailable( 

359 'Python Imaging Library is required for this formatter') 

360 Formatter.__init__(self, **options) 

361 self.encoding = 'latin1' # let pygments.format() do the right thing 

362 # Read the style 

363 self.styles = dict(self.style) 

364 if self.style.background_color is None: 

365 self.background_color = '#fff' 

366 else: 

367 self.background_color = self.style.background_color 

368 # Image options 

369 self.image_format = get_choice_opt( 

370 options, 'image_format', ['png', 'jpeg', 'gif', 'bmp'], 

371 self.default_image_format, normcase=True) 

372 self.image_pad = get_int_opt(options, 'image_pad', 10) 

373 self.line_pad = get_int_opt(options, 'line_pad', 2) 

374 # The fonts 

375 fontsize = get_int_opt(options, 'font_size', 14) 

376 self.fonts = FontManager(options.get('font_name', ''), fontsize) 

377 self.fontw, self.fonth = self.fonts.get_char_size() 

378 # Line number options 

379 self.line_number_fg = options.get('line_number_fg', '#886') 

380 self.line_number_bg = options.get('line_number_bg', '#eed') 

381 self.line_number_chars = get_int_opt(options, 

382 'line_number_chars', 2) 

383 self.line_number_bold = get_bool_opt(options, 

384 'line_number_bold', False) 

385 self.line_number_italic = get_bool_opt(options, 

386 'line_number_italic', False) 

387 self.line_number_pad = get_int_opt(options, 'line_number_pad', 6) 

388 self.line_numbers = get_bool_opt(options, 'line_numbers', True) 

389 self.line_number_separator = get_bool_opt(options, 

390 'line_number_separator', True) 

391 self.line_number_step = get_int_opt(options, 'line_number_step', 1) 

392 self.line_number_start = get_int_opt(options, 'line_number_start', 1) 

393 if self.line_numbers: 

394 self.line_number_width = (self.fontw * self.line_number_chars + 

395 self.line_number_pad * 2) 

396 else: 

397 self.line_number_width = 0 

398 self.hl_lines = [] 

399 hl_lines_str = get_list_opt(options, 'hl_lines', []) 

400 for line in hl_lines_str: 

401 try: 

402 self.hl_lines.append(int(line)) 

403 except ValueError: 

404 pass 

405 self.hl_color = options.get('hl_color', 

406 self.style.highlight_color) or '#f90' 

407 self.drawables = [] 

408 

409 def get_style_defs(self, arg=''): 

410 raise NotImplementedError('The -S option is meaningless for the image ' 

411 'formatter. Use -O style=<stylename> instead.') 

412 

413 def _get_line_height(self): 

414 """ 

415 Get the height of a line. 

416 """ 

417 return self.fonth + self.line_pad 

418 

419 def _get_line_y(self, lineno): 

420 """ 

421 Get the Y coordinate of a line number. 

422 """ 

423 return lineno * self._get_line_height() + self.image_pad 

424 

425 def _get_char_width(self): 

426 """ 

427 Get the width of a character. 

428 """ 

429 return self.fontw 

430 

431 def _get_char_x(self, linelength): 

432 """ 

433 Get the X coordinate of a character position. 

434 """ 

435 return linelength + self.image_pad + self.line_number_width 

436 

437 def _get_text_pos(self, linelength, lineno): 

438 """ 

439 Get the actual position for a character and line position. 

440 """ 

441 return self._get_char_x(linelength), self._get_line_y(lineno) 

442 

443 def _get_linenumber_pos(self, lineno): 

444 """ 

445 Get the actual position for the start of a line number. 

446 """ 

447 return (self.image_pad, self._get_line_y(lineno)) 

448 

449 def _get_text_color(self, style): 

450 """ 

451 Get the correct color for the token from the style. 

452 """ 

453 if style['color'] is not None: 

454 fill = '#' + style['color'] 

455 else: 

456 fill = '#000' 

457 return fill 

458 

459 def _get_text_bg_color(self, style): 

460 """ 

461 Get the correct background color for the token from the style. 

462 """ 

463 if style['bgcolor'] is not None: 

464 bg_color = '#' + style['bgcolor'] 

465 else: 

466 bg_color = None 

467 return bg_color 

468 

469 def _get_style_font(self, style): 

470 """ 

471 Get the correct font for the style. 

472 """ 

473 return self.fonts.get_font(style['bold'], style['italic']) 

474 

475 def _get_image_size(self, maxlinelength, maxlineno): 

476 """ 

477 Get the required image size. 

478 """ 

479 return (self._get_char_x(maxlinelength) + self.image_pad, 

480 self._get_line_y(maxlineno + 0) + self.image_pad) 

481 

482 def _draw_linenumber(self, posno, lineno): 

483 """ 

484 Remember a line number drawable to paint later. 

485 """ 

486 self._draw_text( 

487 self._get_linenumber_pos(posno), 

488 str(lineno).rjust(self.line_number_chars), 

489 font=self.fonts.get_font(self.line_number_bold, 

490 self.line_number_italic), 

491 text_fg=self.line_number_fg, 

492 text_bg=None, 

493 ) 

494 

495 def _draw_text(self, pos, text, font, text_fg, text_bg): 

496 """ 

497 Remember a single drawable tuple to paint later. 

498 """ 

499 self.drawables.append((pos, text, font, text_fg, text_bg)) 

500 

501 def _create_drawables(self, tokensource): 

502 """ 

503 Create drawables for the token content. 

504 """ 

505 lineno = charno = maxcharno = 0 

506 maxlinelength = linelength = 0 

507 for ttype, value in tokensource: 

508 while ttype not in self.styles: 

509 ttype = ttype.parent 

510 style = self.styles[ttype] 

511 # TODO: make sure tab expansion happens earlier in the chain. It 

512 # really ought to be done on the input, as to do it right here is 

513 # quite complex. 

514 value = value.expandtabs(4) 

515 lines = value.splitlines(True) 

516 # print lines 

517 for i, line in enumerate(lines): 

518 temp = line.rstrip('\n') 

519 if temp: 

520 self._draw_text( 

521 self._get_text_pos(linelength, lineno), 

522 temp, 

523 font = self._get_style_font(style), 

524 text_fg = self._get_text_color(style), 

525 text_bg = self._get_text_bg_color(style), 

526 ) 

527 temp_width, _ = self.fonts.get_text_size(temp) 

528 linelength += temp_width 

529 maxlinelength = max(maxlinelength, linelength) 

530 charno += len(temp) 

531 maxcharno = max(maxcharno, charno) 

532 if line.endswith('\n'): 

533 # add a line for each extra line in the value 

534 linelength = 0 

535 charno = 0 

536 lineno += 1 

537 self.maxlinelength = maxlinelength 

538 self.maxcharno = maxcharno 

539 self.maxlineno = lineno 

540 

541 def _draw_line_numbers(self): 

542 """ 

543 Create drawables for the line numbers. 

544 """ 

545 if not self.line_numbers: 

546 return 

547 for p in range(self.maxlineno): 

548 n = p + self.line_number_start 

549 if (n % self.line_number_step) == 0: 

550 self._draw_linenumber(p, n) 

551 

552 def _paint_line_number_bg(self, im): 

553 """ 

554 Paint the line number background on the image. 

555 """ 

556 if not self.line_numbers: 

557 return 

558 if self.line_number_fg is None: 

559 return 

560 draw = ImageDraw.Draw(im) 

561 recth = im.size[-1] 

562 rectw = self.image_pad + self.line_number_width - self.line_number_pad 

563 draw.rectangle([(0, 0), (rectw, recth)], 

564 fill=self.line_number_bg) 

565 if self.line_number_separator: 

566 draw.line([(rectw, 0), (rectw, recth)], fill=self.line_number_fg) 

567 del draw 

568 

569 def format(self, tokensource, outfile): 

570 """ 

571 Format ``tokensource``, an iterable of ``(tokentype, tokenstring)`` 

572 tuples and write it into ``outfile``. 

573 

574 This implementation calculates where it should draw each token on the 

575 pixmap, then calculates the required pixmap size and draws the items. 

576 """ 

577 self._create_drawables(tokensource) 

578 self._draw_line_numbers() 

579 im = Image.new( 

580 'RGB', 

581 self._get_image_size(self.maxlinelength, self.maxlineno), 

582 self.background_color 

583 ) 

584 self._paint_line_number_bg(im) 

585 draw = ImageDraw.Draw(im) 

586 # Highlight 

587 if self.hl_lines: 

588 x = self.image_pad + self.line_number_width - self.line_number_pad + 1 

589 recth = self._get_line_height() 

590 rectw = im.size[0] - x 

591 for linenumber in self.hl_lines: 

592 y = self._get_line_y(linenumber - 1) 

593 draw.rectangle([(x, y), (x + rectw, y + recth)], 

594 fill=self.hl_color) 

595 for pos, value, font, text_fg, text_bg in self.drawables: 

596 if text_bg: 

597 text_size = draw.textsize(text=value, font=font) 

598 draw.rectangle([pos[0], pos[1], pos[0] + text_size[0], pos[1] + text_size[1]], fill=text_bg) 

599 draw.text(pos, value, font=font, fill=text_fg) 

600 im.save(outfile, self.image_format.upper()) 

601 

602 

603# Add one formatter per format, so that the "-f gif" option gives the correct result 

604# when used in pygmentize. 

605 

606class GifImageFormatter(ImageFormatter): 

607 """ 

608 Create a GIF image from source code. This uses the Python Imaging Library to 

609 generate a pixmap from the source code. 

610 

611 .. versionadded:: 1.0 

612 """ 

613 

614 name = 'img_gif' 

615 aliases = ['gif'] 

616 filenames = ['*.gif'] 

617 default_image_format = 'gif' 

618 

619 

620class JpgImageFormatter(ImageFormatter): 

621 """ 

622 Create a JPEG image from source code. This uses the Python Imaging Library to 

623 generate a pixmap from the source code. 

624 

625 .. versionadded:: 1.0 

626 """ 

627 

628 name = 'img_jpg' 

629 aliases = ['jpg', 'jpeg'] 

630 filenames = ['*.jpg'] 

631 default_image_format = 'jpeg' 

632 

633 

634class BmpImageFormatter(ImageFormatter): 

635 """ 

636 Create a bitmap image from source code. This uses the Python Imaging Library to 

637 generate a pixmap from the source code. 

638 

639 .. versionadded:: 1.0 

640 """ 

641 

642 name = 'img_bmp' 

643 aliases = ['bmp', 'bitmap'] 

644 filenames = ['*.bmp'] 

645 default_image_format = 'bmp'