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

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

324 statements  

1""" 

2 pygments.formatters.img 

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

4 

5 Formatter for Pixmap output. 

6 

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

8 :license: BSD, see LICENSE for details. 

9""" 

10import os 

11import sys 

12 

13from pygments.formatter import Formatter 

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

15 get_choice_opt 

16 

17import subprocess 

18 

19# Import this carefully 

20try: 

21 from PIL import Image, ImageDraw, ImageFont 

22 pil_available = True 

23except ImportError: 

24 pil_available = False 

25 

26try: 

27 import _winreg 

28except ImportError: 

29 try: 

30 import winreg as _winreg 

31 except ImportError: 

32 _winreg = None 

33 

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

35 'BmpImageFormatter'] 

36 

37 

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

39STYLES = { 

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

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

42 'BOLD': ['Bold'], 

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

44} 

45 

46# A sane default for modern systems 

47DEFAULT_FONT_NAME_NIX = 'DejaVu Sans Mono' 

48DEFAULT_FONT_NAME_WIN = 'Courier New' 

49DEFAULT_FONT_NAME_MAC = 'Menlo' 

50 

51 

52class PilNotAvailable(ImportError): 

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

54 

55 

56class FontNotFound(Exception): 

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

58 

59 

60class FontManager: 

61 """ 

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

63 """ 

64 

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

66 self.font_name = font_name 

67 self.font_size = font_size 

68 self.fonts = {} 

69 self.encoding = None 

70 self.variable = False 

71 if hasattr(font_name, 'read') or os.path.isfile(font_name): 

72 font = ImageFont.truetype(font_name, self.font_size) 

73 self.variable = True 

74 for style in STYLES: 

75 self.fonts[style] = font 

76 

77 return 

78 

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

80 if not font_name: 

81 self.font_name = DEFAULT_FONT_NAME_WIN 

82 self._create_win() 

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

84 if not font_name: 

85 self.font_name = DEFAULT_FONT_NAME_MAC 

86 self._create_mac() 

87 else: 

88 if not font_name: 

89 self.font_name = DEFAULT_FONT_NAME_NIX 

90 self._create_nix() 

91 

92 def _get_nix_font_path(self, name, style): 

93 proc = subprocess.Popen(['fc-list', f"{name}:style={style}", 'file'], 

94 stdout=subprocess.PIPE, stderr=None) 

95 stdout, _ = proc.communicate() 

96 if proc.returncode == 0: 

97 lines = stdout.splitlines() 

98 for line in lines: 

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

100 continue 

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

102 if path: 

103 return path 

104 return None 

105 

106 def _create_nix(self): 

107 for name in STYLES['NORMAL']: 

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

109 if path is not None: 

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

111 break 

112 else: 

113 raise FontNotFound(f'No usable fonts named: "{self.font_name}"') 

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

115 for stylename in STYLES[style]: 

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

117 if path is not None: 

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

119 break 

120 else: 

121 if style == 'BOLDITALIC': 

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

123 else: 

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

125 

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

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

128 

129 def _create_mac(self): 

130 font_map = {} 

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

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

133 font_map.update( 

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

135 for _, _, files in os.walk(font_dir) 

136 for f in files 

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

138 

139 for name in STYLES['NORMAL']: 

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

141 if path is not None: 

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

143 break 

144 else: 

145 raise FontNotFound(f'No usable fonts named: "{self.font_name}"') 

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

147 for stylename in STYLES[style]: 

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

149 if path is not None: 

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

151 break 

152 else: 

153 if style == 'BOLDITALIC': 

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

155 else: 

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

157 

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

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

160 for style in styles: 

161 try: 

162 valname = '{}{}{}'.format(basename, style and ' '+style, suffix) 

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

164 return val 

165 except OSError: 

166 continue 

167 else: 

168 if fail: 

169 raise FontNotFound(f'Font {basename} ({styles[0]}) not found in registry') 

170 return None 

171 

172 def _create_win(self): 

173 lookuperror = None 

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

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

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

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

178 for keyname in keynames: 

179 try: 

180 key = _winreg.OpenKey(*keyname) 

181 try: 

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

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

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

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

186 if path: 

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

188 else: 

189 if style == 'BOLDITALIC': 

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

191 else: 

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

193 return 

194 except FontNotFound as err: 

195 lookuperror = err 

196 finally: 

197 _winreg.CloseKey(key) 

198 except OSError: 

199 pass 

200 else: 

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

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

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

204 # will raise a generic error 

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

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

207 if lookuperror: 

208 raise lookuperror 

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

210 

211 def get_char_size(self): 

212 """ 

213 Get the character size. 

214 """ 

215 return self.get_text_size('M') 

216 

217 def get_text_size(self, text): 

218 """ 

219 Get the text size (width, height). 

220 """ 

221 font = self.fonts['NORMAL'] 

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

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

224 else: 

225 return font.getsize(text) 

226 

227 def get_font(self, bold, oblique): 

228 """ 

229 Get the font based on bold and italic flags. 

230 """ 

231 if bold and oblique: 

232 if self.variable: 

233 return self.get_style('BOLDITALIC') 

234 

235 return self.fonts['BOLDITALIC'] 

236 elif bold: 

237 if self.variable: 

238 return self.get_style('BOLD') 

239 

240 return self.fonts['BOLD'] 

241 elif oblique: 

242 if self.variable: 

243 return self.get_style('ITALIC') 

244 

245 return self.fonts['ITALIC'] 

246 else: 

247 if self.variable: 

248 return self.get_style('NORMAL') 

249 

250 return self.fonts['NORMAL'] 

251 

252 def get_style(self, style): 

253 """ 

254 Get the specified style of the font if it is a variable font. 

255 If not found, return the normal font. 

256 """ 

257 font = self.fonts[style] 

258 for style_name in STYLES[style]: 

259 try: 

260 font.set_variation_by_name(style_name) 

261 return font 

262 except ValueError: 

263 pass 

264 except OSError: 

265 return font 

266 

267 return font 

268 

269 

270class ImageFormatter(Formatter): 

271 """ 

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

273 generate a pixmap from the source code. 

274 

275 .. versionadded:: 0.10 

276 

277 Additional options accepted: 

278 

279 `image_format` 

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

281 

282 * "PNG" (default) 

283 * "JPEG" 

284 * "BMP" 

285 * "GIF" 

286 

287 `line_pad` 

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

289 

290 Default: 2 

291 

292 `font_name` 

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

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

295 monospace font to look sane. 

296 If a filename or a file-like object is specified, the user must 

297 provide different styles of the font. 

298 

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

300 "DejaVu Sans Mono" on \\*nix 

301 

302 `font_size` 

303 The font size in points to be used. 

304 

305 Default: 14 

306 

307 `image_pad` 

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

309 

310 Default: 10 

311 

312 `line_numbers` 

313 Whether line numbers should be shown: True/False 

314 

315 Default: True 

316 

317 `line_number_start` 

318 The line number of the first line. 

319 

320 Default: 1 

321 

322 `line_number_step` 

323 The step used when printing line numbers. 

324 

325 Default: 1 

326 

327 `line_number_bg` 

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

329 None to use the style background color. 

330 

331 Default: "#eed" 

332 

333 `line_number_fg` 

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

335 

336 Default: "#886" 

337 

338 `line_number_chars` 

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

340 margin. 

341 

342 Default: 2 

343 

344 `line_number_bold` 

345 Whether line numbers will be bold: True/False 

346 

347 Default: False 

348 

349 `line_number_italic` 

350 Whether line numbers will be italicized: True/False 

351 

352 Default: False 

353 

354 `line_number_separator` 

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

356 source code area: True/False 

357 

358 Default: True 

359 

360 `line_number_pad` 

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

362 the source code area. 

363 

364 Default: 6 

365 

366 `hl_lines` 

367 Specify a list of lines to be highlighted. 

368 

369 .. versionadded:: 1.2 

370 

371 Default: empty list 

372 

373 `hl_color` 

374 Specify the color for highlighting lines. 

375 

376 .. versionadded:: 1.2 

377 

378 Default: highlight color of the selected style 

379 """ 

380 

381 # Required by the pygments mapper 

382 name = 'img' 

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

384 filenames = ['*.png'] 

385 

386 unicodeoutput = False 

387 

388 default_image_format = 'png' 

389 

390 def __init__(self, **options): 

391 """ 

392 See the class docstring for explanation of options. 

393 """ 

394 if not pil_available: 

395 raise PilNotAvailable( 

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

397 Formatter.__init__(self, **options) 

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

399 # Read the style 

400 self.styles = dict(self.style) 

401 if self.style.background_color is None: 

402 self.background_color = '#fff' 

403 else: 

404 self.background_color = self.style.background_color 

405 # Image options 

406 self.image_format = get_choice_opt( 

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

408 self.default_image_format, normcase=True) 

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

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

411 # The fonts 

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

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

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

415 # Line number options 

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

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

418 self.line_number_chars = get_int_opt(options, 

419 'line_number_chars', 2) 

420 self.line_number_bold = get_bool_opt(options, 

421 'line_number_bold', False) 

422 self.line_number_italic = get_bool_opt(options, 

423 'line_number_italic', False) 

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

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

426 self.line_number_separator = get_bool_opt(options, 

427 'line_number_separator', True) 

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

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

430 if self.line_numbers: 

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

432 self.line_number_pad * 2) 

433 else: 

434 self.line_number_width = 0 

435 self.hl_lines = [] 

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

437 for line in hl_lines_str: 

438 try: 

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

440 except ValueError: 

441 pass 

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

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

444 self.drawables = [] 

445 

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

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

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

449 

450 def _get_line_height(self): 

451 """ 

452 Get the height of a line. 

453 """ 

454 return self.fonth + self.line_pad 

455 

456 def _get_line_y(self, lineno): 

457 """ 

458 Get the Y coordinate of a line number. 

459 """ 

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

461 

462 def _get_char_width(self): 

463 """ 

464 Get the width of a character. 

465 """ 

466 return self.fontw 

467 

468 def _get_char_x(self, linelength): 

469 """ 

470 Get the X coordinate of a character position. 

471 """ 

472 return linelength + self.image_pad + self.line_number_width 

473 

474 def _get_text_pos(self, linelength, lineno): 

475 """ 

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

477 """ 

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

479 

480 def _get_linenumber_pos(self, lineno): 

481 """ 

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

483 """ 

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

485 

486 def _get_text_color(self, style): 

487 """ 

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

489 """ 

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

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

492 else: 

493 fill = '#000' 

494 return fill 

495 

496 def _get_text_bg_color(self, style): 

497 """ 

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

499 """ 

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

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

502 else: 

503 bg_color = None 

504 return bg_color 

505 

506 def _get_style_font(self, style): 

507 """ 

508 Get the correct font for the style. 

509 """ 

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

511 

512 def _get_image_size(self, maxlinelength, maxlineno): 

513 """ 

514 Get the required image size. 

515 """ 

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

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

518 

519 def _draw_linenumber(self, posno, lineno): 

520 """ 

521 Remember a line number drawable to paint later. 

522 """ 

523 self._draw_text( 

524 self._get_linenumber_pos(posno), 

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

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

527 self.line_number_italic), 

528 text_fg=self.line_number_fg, 

529 text_bg=None, 

530 ) 

531 

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

533 """ 

534 Remember a single drawable tuple to paint later. 

535 """ 

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

537 

538 def _create_drawables(self, tokensource): 

539 """ 

540 Create drawables for the token content. 

541 """ 

542 lineno = charno = maxcharno = 0 

543 maxlinelength = linelength = 0 

544 for ttype, value in tokensource: 

545 while ttype not in self.styles: 

546 ttype = ttype.parent 

547 style = self.styles[ttype] 

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

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

550 # quite complex. 

551 value = value.expandtabs(4) 

552 lines = value.splitlines(True) 

553 # print lines 

554 for i, line in enumerate(lines): 

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

556 if temp: 

557 self._draw_text( 

558 self._get_text_pos(linelength, lineno), 

559 temp, 

560 font = self._get_style_font(style), 

561 text_fg = self._get_text_color(style), 

562 text_bg = self._get_text_bg_color(style), 

563 ) 

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

565 linelength += temp_width 

566 maxlinelength = max(maxlinelength, linelength) 

567 charno += len(temp) 

568 maxcharno = max(maxcharno, charno) 

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

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

571 linelength = 0 

572 charno = 0 

573 lineno += 1 

574 self.maxlinelength = maxlinelength 

575 self.maxcharno = maxcharno 

576 self.maxlineno = lineno 

577 

578 def _draw_line_numbers(self): 

579 """ 

580 Create drawables for the line numbers. 

581 """ 

582 if not self.line_numbers: 

583 return 

584 for p in range(self.maxlineno): 

585 n = p + self.line_number_start 

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

587 self._draw_linenumber(p, n) 

588 

589 def _paint_line_number_bg(self, im): 

590 """ 

591 Paint the line number background on the image. 

592 """ 

593 if not self.line_numbers: 

594 return 

595 if self.line_number_fg is None: 

596 return 

597 draw = ImageDraw.Draw(im) 

598 recth = im.size[-1] 

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

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

601 fill=self.line_number_bg) 

602 if self.line_number_separator: 

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

604 del draw 

605 

606 def format(self, tokensource, outfile): 

607 """ 

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

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

610 

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

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

613 """ 

614 self._create_drawables(tokensource) 

615 self._draw_line_numbers() 

616 im = Image.new( 

617 'RGB', 

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

619 self.background_color 

620 ) 

621 self._paint_line_number_bg(im) 

622 draw = ImageDraw.Draw(im) 

623 # Highlight 

624 if self.hl_lines: 

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

626 recth = self._get_line_height() 

627 rectw = im.size[0] - x 

628 for linenumber in self.hl_lines: 

629 y = self._get_line_y(linenumber - 1) 

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

631 fill=self.hl_color) 

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

633 if text_bg: 

634 # see deprecations https://pillow.readthedocs.io/en/stable/releasenotes/9.2.0.html#font-size-and-offset-methods 

635 if hasattr(draw, 'textsize'): 

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

637 else: 

638 text_size = font.getbbox(value)[2:] 

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

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

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

642 

643 

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

645# when used in pygmentize. 

646 

647class GifImageFormatter(ImageFormatter): 

648 """ 

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

650 generate a pixmap from the source code. 

651 

652 .. versionadded:: 1.0 

653 """ 

654 

655 name = 'img_gif' 

656 aliases = ['gif'] 

657 filenames = ['*.gif'] 

658 default_image_format = 'gif' 

659 

660 

661class JpgImageFormatter(ImageFormatter): 

662 """ 

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

664 generate a pixmap from the source code. 

665 

666 .. versionadded:: 1.0 

667 """ 

668 

669 name = 'img_jpg' 

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

671 filenames = ['*.jpg'] 

672 default_image_format = 'jpeg' 

673 

674 

675class BmpImageFormatter(ImageFormatter): 

676 """ 

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

678 generate a pixmap from the source code. 

679 

680 .. versionadded:: 1.0 

681 """ 

682 

683 name = 'img_bmp' 

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

685 filenames = ['*.bmp'] 

686 default_image_format = 'bmp'