img.py 23 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686
  1. """
  2. pygments.formatters.img
  3. ~~~~~~~~~~~~~~~~~~~~~~~
  4. Formatter for Pixmap output.
  5. :copyright: Copyright 2006-2025 by the Pygments team, see AUTHORS.
  6. :license: BSD, see LICENSE for details.
  7. """
  8. import os
  9. import sys
  10. from pygments.formatter import Formatter
  11. from pygments.util import get_bool_opt, get_int_opt, get_list_opt, \
  12. get_choice_opt
  13. import subprocess
  14. # Import this carefully
  15. try:
  16. from PIL import Image, ImageDraw, ImageFont
  17. pil_available = True
  18. except ImportError:
  19. pil_available = False
  20. try:
  21. import _winreg
  22. except ImportError:
  23. try:
  24. import winreg as _winreg
  25. except ImportError:
  26. _winreg = None
  27. __all__ = ['ImageFormatter', 'GifImageFormatter', 'JpgImageFormatter',
  28. 'BmpImageFormatter']
  29. # For some unknown reason every font calls it something different
  30. STYLES = {
  31. 'NORMAL': ['', 'Roman', 'Book', 'Normal', 'Regular', 'Medium'],
  32. 'ITALIC': ['Oblique', 'Italic'],
  33. 'BOLD': ['Bold'],
  34. 'BOLDITALIC': ['Bold Oblique', 'Bold Italic'],
  35. }
  36. # A sane default for modern systems
  37. DEFAULT_FONT_NAME_NIX = 'DejaVu Sans Mono'
  38. DEFAULT_FONT_NAME_WIN = 'Courier New'
  39. DEFAULT_FONT_NAME_MAC = 'Menlo'
  40. class PilNotAvailable(ImportError):
  41. """When Python imaging library is not available"""
  42. class FontNotFound(Exception):
  43. """When there are no usable fonts specified"""
  44. class FontManager:
  45. """
  46. Manages a set of fonts: normal, italic, bold, etc...
  47. """
  48. def __init__(self, font_name, font_size=14):
  49. self.font_name = font_name
  50. self.font_size = font_size
  51. self.fonts = {}
  52. self.encoding = None
  53. self.variable = False
  54. if hasattr(font_name, 'read') or os.path.isfile(font_name):
  55. font = ImageFont.truetype(font_name, self.font_size)
  56. self.variable = True
  57. for style in STYLES:
  58. self.fonts[style] = font
  59. return
  60. if sys.platform.startswith('win'):
  61. if not font_name:
  62. self.font_name = DEFAULT_FONT_NAME_WIN
  63. self._create_win()
  64. elif sys.platform.startswith('darwin'):
  65. if not font_name:
  66. self.font_name = DEFAULT_FONT_NAME_MAC
  67. self._create_mac()
  68. else:
  69. if not font_name:
  70. self.font_name = DEFAULT_FONT_NAME_NIX
  71. self._create_nix()
  72. def _get_nix_font_path(self, name, style):
  73. proc = subprocess.Popen(['fc-list', f"{name}:style={style}", 'file'],
  74. stdout=subprocess.PIPE, stderr=None)
  75. stdout, _ = proc.communicate()
  76. if proc.returncode == 0:
  77. lines = stdout.splitlines()
  78. for line in lines:
  79. if line.startswith(b'Fontconfig warning:'):
  80. continue
  81. path = line.decode().strip().strip(':')
  82. if path:
  83. return path
  84. return None
  85. def _create_nix(self):
  86. for name in STYLES['NORMAL']:
  87. path = self._get_nix_font_path(self.font_name, name)
  88. if path is not None:
  89. self.fonts['NORMAL'] = ImageFont.truetype(path, self.font_size)
  90. break
  91. else:
  92. raise FontNotFound(f'No usable fonts named: "{self.font_name}"')
  93. for style in ('ITALIC', 'BOLD', 'BOLDITALIC'):
  94. for stylename in STYLES[style]:
  95. path = self._get_nix_font_path(self.font_name, stylename)
  96. if path is not None:
  97. self.fonts[style] = ImageFont.truetype(path, self.font_size)
  98. break
  99. else:
  100. if style == 'BOLDITALIC':
  101. self.fonts[style] = self.fonts['BOLD']
  102. else:
  103. self.fonts[style] = self.fonts['NORMAL']
  104. def _get_mac_font_path(self, font_map, name, style):
  105. return font_map.get((name + ' ' + style).strip().lower())
  106. def _create_mac(self):
  107. font_map = {}
  108. for font_dir in (os.path.join(os.getenv("HOME"), 'Library/Fonts/'),
  109. '/Library/Fonts/', '/System/Library/Fonts/'):
  110. font_map.update(
  111. (os.path.splitext(f)[0].lower(), os.path.join(font_dir, f))
  112. for _, _, files in os.walk(font_dir)
  113. for f in files
  114. if f.lower().endswith(('ttf', 'ttc')))
  115. for name in STYLES['NORMAL']:
  116. path = self._get_mac_font_path(font_map, self.font_name, name)
  117. if path is not None:
  118. self.fonts['NORMAL'] = ImageFont.truetype(path, self.font_size)
  119. break
  120. else:
  121. raise FontNotFound(f'No usable fonts named: "{self.font_name}"')
  122. for style in ('ITALIC', 'BOLD', 'BOLDITALIC'):
  123. for stylename in STYLES[style]:
  124. path = self._get_mac_font_path(font_map, self.font_name, stylename)
  125. if path is not None:
  126. self.fonts[style] = ImageFont.truetype(path, self.font_size)
  127. break
  128. else:
  129. if style == 'BOLDITALIC':
  130. self.fonts[style] = self.fonts['BOLD']
  131. else:
  132. self.fonts[style] = self.fonts['NORMAL']
  133. def _lookup_win(self, key, basename, styles, fail=False):
  134. for suffix in ('', ' (TrueType)'):
  135. for style in styles:
  136. try:
  137. valname = '{}{}{}'.format(basename, style and ' '+style, suffix)
  138. val, _ = _winreg.QueryValueEx(key, valname)
  139. return val
  140. except OSError:
  141. continue
  142. else:
  143. if fail:
  144. raise FontNotFound(f'Font {basename} ({styles[0]}) not found in registry')
  145. return None
  146. def _create_win(self):
  147. lookuperror = None
  148. keynames = [ (_winreg.HKEY_CURRENT_USER, r'Software\Microsoft\Windows NT\CurrentVersion\Fonts'),
  149. (_winreg.HKEY_CURRENT_USER, r'Software\Microsoft\Windows\CurrentVersion\Fonts'),
  150. (_winreg.HKEY_LOCAL_MACHINE, r'Software\Microsoft\Windows NT\CurrentVersion\Fonts'),
  151. (_winreg.HKEY_LOCAL_MACHINE, r'Software\Microsoft\Windows\CurrentVersion\Fonts') ]
  152. for keyname in keynames:
  153. try:
  154. key = _winreg.OpenKey(*keyname)
  155. try:
  156. path = self._lookup_win(key, self.font_name, STYLES['NORMAL'], True)
  157. self.fonts['NORMAL'] = ImageFont.truetype(path, self.font_size)
  158. for style in ('ITALIC', 'BOLD', 'BOLDITALIC'):
  159. path = self._lookup_win(key, self.font_name, STYLES[style])
  160. if path:
  161. self.fonts[style] = ImageFont.truetype(path, self.font_size)
  162. else:
  163. if style == 'BOLDITALIC':
  164. self.fonts[style] = self.fonts['BOLD']
  165. else:
  166. self.fonts[style] = self.fonts['NORMAL']
  167. return
  168. except FontNotFound as err:
  169. lookuperror = err
  170. finally:
  171. _winreg.CloseKey(key)
  172. except OSError:
  173. pass
  174. else:
  175. # If we get here, we checked all registry keys and had no luck
  176. # We can be in one of two situations now:
  177. # * All key lookups failed. In this case lookuperror is None and we
  178. # will raise a generic error
  179. # * At least one lookup failed with a FontNotFound error. In this
  180. # case, we will raise that as a more specific error
  181. if lookuperror:
  182. raise lookuperror
  183. raise FontNotFound('Can\'t open Windows font registry key')
  184. def get_char_size(self):
  185. """
  186. Get the character size.
  187. """
  188. return self.get_text_size('M')
  189. def get_text_size(self, text):
  190. """
  191. Get the text size (width, height).
  192. """
  193. font = self.fonts['NORMAL']
  194. if hasattr(font, 'getbbox'): # Pillow >= 9.2.0
  195. return font.getbbox(text)[2:4]
  196. else:
  197. return font.getsize(text)
  198. def get_font(self, bold, oblique):
  199. """
  200. Get the font based on bold and italic flags.
  201. """
  202. if bold and oblique:
  203. if self.variable:
  204. return self.get_style('BOLDITALIC')
  205. return self.fonts['BOLDITALIC']
  206. elif bold:
  207. if self.variable:
  208. return self.get_style('BOLD')
  209. return self.fonts['BOLD']
  210. elif oblique:
  211. if self.variable:
  212. return self.get_style('ITALIC')
  213. return self.fonts['ITALIC']
  214. else:
  215. if self.variable:
  216. return self.get_style('NORMAL')
  217. return self.fonts['NORMAL']
  218. def get_style(self, style):
  219. """
  220. Get the specified style of the font if it is a variable font.
  221. If not found, return the normal font.
  222. """
  223. font = self.fonts[style]
  224. for style_name in STYLES[style]:
  225. try:
  226. font.set_variation_by_name(style_name)
  227. return font
  228. except ValueError:
  229. pass
  230. except OSError:
  231. return font
  232. return font
  233. class ImageFormatter(Formatter):
  234. """
  235. Create a PNG image from source code. This uses the Python Imaging Library to
  236. generate a pixmap from the source code.
  237. .. versionadded:: 0.10
  238. Additional options accepted:
  239. `image_format`
  240. An image format to output to that is recognised by PIL, these include:
  241. * "PNG" (default)
  242. * "JPEG"
  243. * "BMP"
  244. * "GIF"
  245. `line_pad`
  246. The extra spacing (in pixels) between each line of text.
  247. Default: 2
  248. `font_name`
  249. The font name to be used as the base font from which others, such as
  250. bold and italic fonts will be generated. This really should be a
  251. monospace font to look sane.
  252. If a filename or a file-like object is specified, the user must
  253. provide different styles of the font.
  254. Default: "Courier New" on Windows, "Menlo" on Mac OS, and
  255. "DejaVu Sans Mono" on \\*nix
  256. `font_size`
  257. The font size in points to be used.
  258. Default: 14
  259. `image_pad`
  260. The padding, in pixels to be used at each edge of the resulting image.
  261. Default: 10
  262. `line_numbers`
  263. Whether line numbers should be shown: True/False
  264. Default: True
  265. `line_number_start`
  266. The line number of the first line.
  267. Default: 1
  268. `line_number_step`
  269. The step used when printing line numbers.
  270. Default: 1
  271. `line_number_bg`
  272. The background colour (in "#123456" format) of the line number bar, or
  273. None to use the style background color.
  274. Default: "#eed"
  275. `line_number_fg`
  276. The text color of the line numbers (in "#123456"-like format).
  277. Default: "#886"
  278. `line_number_chars`
  279. The number of columns of line numbers allowable in the line number
  280. margin.
  281. Default: 2
  282. `line_number_bold`
  283. Whether line numbers will be bold: True/False
  284. Default: False
  285. `line_number_italic`
  286. Whether line numbers will be italicized: True/False
  287. Default: False
  288. `line_number_separator`
  289. Whether a line will be drawn between the line number area and the
  290. source code area: True/False
  291. Default: True
  292. `line_number_pad`
  293. The horizontal padding (in pixels) between the line number margin, and
  294. the source code area.
  295. Default: 6
  296. `hl_lines`
  297. Specify a list of lines to be highlighted.
  298. .. versionadded:: 1.2
  299. Default: empty list
  300. `hl_color`
  301. Specify the color for highlighting lines.
  302. .. versionadded:: 1.2
  303. Default: highlight color of the selected style
  304. """
  305. # Required by the pygments mapper
  306. name = 'img'
  307. aliases = ['img', 'IMG', 'png']
  308. filenames = ['*.png']
  309. unicodeoutput = False
  310. default_image_format = 'png'
  311. def __init__(self, **options):
  312. """
  313. See the class docstring for explanation of options.
  314. """
  315. if not pil_available:
  316. raise PilNotAvailable(
  317. 'Python Imaging Library is required for this formatter')
  318. Formatter.__init__(self, **options)
  319. self.encoding = 'latin1' # let pygments.format() do the right thing
  320. # Read the style
  321. self.styles = dict(self.style)
  322. if self.style.background_color is None:
  323. self.background_color = '#fff'
  324. else:
  325. self.background_color = self.style.background_color
  326. # Image options
  327. self.image_format = get_choice_opt(
  328. options, 'image_format', ['png', 'jpeg', 'gif', 'bmp'],
  329. self.default_image_format, normcase=True)
  330. self.image_pad = get_int_opt(options, 'image_pad', 10)
  331. self.line_pad = get_int_opt(options, 'line_pad', 2)
  332. # The fonts
  333. fontsize = get_int_opt(options, 'font_size', 14)
  334. self.fonts = FontManager(options.get('font_name', ''), fontsize)
  335. self.fontw, self.fonth = self.fonts.get_char_size()
  336. # Line number options
  337. self.line_number_fg = options.get('line_number_fg', '#886')
  338. self.line_number_bg = options.get('line_number_bg', '#eed')
  339. self.line_number_chars = get_int_opt(options,
  340. 'line_number_chars', 2)
  341. self.line_number_bold = get_bool_opt(options,
  342. 'line_number_bold', False)
  343. self.line_number_italic = get_bool_opt(options,
  344. 'line_number_italic', False)
  345. self.line_number_pad = get_int_opt(options, 'line_number_pad', 6)
  346. self.line_numbers = get_bool_opt(options, 'line_numbers', True)
  347. self.line_number_separator = get_bool_opt(options,
  348. 'line_number_separator', True)
  349. self.line_number_step = get_int_opt(options, 'line_number_step', 1)
  350. self.line_number_start = get_int_opt(options, 'line_number_start', 1)
  351. if self.line_numbers:
  352. self.line_number_width = (self.fontw * self.line_number_chars +
  353. self.line_number_pad * 2)
  354. else:
  355. self.line_number_width = 0
  356. self.hl_lines = []
  357. hl_lines_str = get_list_opt(options, 'hl_lines', [])
  358. for line in hl_lines_str:
  359. try:
  360. self.hl_lines.append(int(line))
  361. except ValueError:
  362. pass
  363. self.hl_color = options.get('hl_color',
  364. self.style.highlight_color) or '#f90'
  365. self.drawables = []
  366. def get_style_defs(self, arg=''):
  367. raise NotImplementedError('The -S option is meaningless for the image '
  368. 'formatter. Use -O style=<stylename> instead.')
  369. def _get_line_height(self):
  370. """
  371. Get the height of a line.
  372. """
  373. return self.fonth + self.line_pad
  374. def _get_line_y(self, lineno):
  375. """
  376. Get the Y coordinate of a line number.
  377. """
  378. return lineno * self._get_line_height() + self.image_pad
  379. def _get_char_width(self):
  380. """
  381. Get the width of a character.
  382. """
  383. return self.fontw
  384. def _get_char_x(self, linelength):
  385. """
  386. Get the X coordinate of a character position.
  387. """
  388. return linelength + self.image_pad + self.line_number_width
  389. def _get_text_pos(self, linelength, lineno):
  390. """
  391. Get the actual position for a character and line position.
  392. """
  393. return self._get_char_x(linelength), self._get_line_y(lineno)
  394. def _get_linenumber_pos(self, lineno):
  395. """
  396. Get the actual position for the start of a line number.
  397. """
  398. return (self.image_pad, self._get_line_y(lineno))
  399. def _get_text_color(self, style):
  400. """
  401. Get the correct color for the token from the style.
  402. """
  403. if style['color'] is not None:
  404. fill = '#' + style['color']
  405. else:
  406. fill = '#000'
  407. return fill
  408. def _get_text_bg_color(self, style):
  409. """
  410. Get the correct background color for the token from the style.
  411. """
  412. if style['bgcolor'] is not None:
  413. bg_color = '#' + style['bgcolor']
  414. else:
  415. bg_color = None
  416. return bg_color
  417. def _get_style_font(self, style):
  418. """
  419. Get the correct font for the style.
  420. """
  421. return self.fonts.get_font(style['bold'], style['italic'])
  422. def _get_image_size(self, maxlinelength, maxlineno):
  423. """
  424. Get the required image size.
  425. """
  426. return (self._get_char_x(maxlinelength) + self.image_pad,
  427. self._get_line_y(maxlineno + 0) + self.image_pad)
  428. def _draw_linenumber(self, posno, lineno):
  429. """
  430. Remember a line number drawable to paint later.
  431. """
  432. self._draw_text(
  433. self._get_linenumber_pos(posno),
  434. str(lineno).rjust(self.line_number_chars),
  435. font=self.fonts.get_font(self.line_number_bold,
  436. self.line_number_italic),
  437. text_fg=self.line_number_fg,
  438. text_bg=None,
  439. )
  440. def _draw_text(self, pos, text, font, text_fg, text_bg):
  441. """
  442. Remember a single drawable tuple to paint later.
  443. """
  444. self.drawables.append((pos, text, font, text_fg, text_bg))
  445. def _create_drawables(self, tokensource):
  446. """
  447. Create drawables for the token content.
  448. """
  449. lineno = charno = maxcharno = 0
  450. maxlinelength = linelength = 0
  451. for ttype, value in tokensource:
  452. while ttype not in self.styles:
  453. ttype = ttype.parent
  454. style = self.styles[ttype]
  455. # TODO: make sure tab expansion happens earlier in the chain. It
  456. # really ought to be done on the input, as to do it right here is
  457. # quite complex.
  458. value = value.expandtabs(4)
  459. lines = value.splitlines(True)
  460. # print lines
  461. for i, line in enumerate(lines):
  462. temp = line.rstrip('\n')
  463. if temp:
  464. self._draw_text(
  465. self._get_text_pos(linelength, lineno),
  466. temp,
  467. font = self._get_style_font(style),
  468. text_fg = self._get_text_color(style),
  469. text_bg = self._get_text_bg_color(style),
  470. )
  471. temp_width, _ = self.fonts.get_text_size(temp)
  472. linelength += temp_width
  473. maxlinelength = max(maxlinelength, linelength)
  474. charno += len(temp)
  475. maxcharno = max(maxcharno, charno)
  476. if line.endswith('\n'):
  477. # add a line for each extra line in the value
  478. linelength = 0
  479. charno = 0
  480. lineno += 1
  481. self.maxlinelength = maxlinelength
  482. self.maxcharno = maxcharno
  483. self.maxlineno = lineno
  484. def _draw_line_numbers(self):
  485. """
  486. Create drawables for the line numbers.
  487. """
  488. if not self.line_numbers:
  489. return
  490. for p in range(self.maxlineno):
  491. n = p + self.line_number_start
  492. if (n % self.line_number_step) == 0:
  493. self._draw_linenumber(p, n)
  494. def _paint_line_number_bg(self, im):
  495. """
  496. Paint the line number background on the image.
  497. """
  498. if not self.line_numbers:
  499. return
  500. if self.line_number_fg is None:
  501. return
  502. draw = ImageDraw.Draw(im)
  503. recth = im.size[-1]
  504. rectw = self.image_pad + self.line_number_width - self.line_number_pad
  505. draw.rectangle([(0, 0), (rectw, recth)],
  506. fill=self.line_number_bg)
  507. if self.line_number_separator:
  508. draw.line([(rectw, 0), (rectw, recth)], fill=self.line_number_fg)
  509. del draw
  510. def format(self, tokensource, outfile):
  511. """
  512. Format ``tokensource``, an iterable of ``(tokentype, tokenstring)``
  513. tuples and write it into ``outfile``.
  514. This implementation calculates where it should draw each token on the
  515. pixmap, then calculates the required pixmap size and draws the items.
  516. """
  517. self._create_drawables(tokensource)
  518. self._draw_line_numbers()
  519. im = Image.new(
  520. 'RGB',
  521. self._get_image_size(self.maxlinelength, self.maxlineno),
  522. self.background_color
  523. )
  524. self._paint_line_number_bg(im)
  525. draw = ImageDraw.Draw(im)
  526. # Highlight
  527. if self.hl_lines:
  528. x = self.image_pad + self.line_number_width - self.line_number_pad + 1
  529. recth = self._get_line_height()
  530. rectw = im.size[0] - x
  531. for linenumber in self.hl_lines:
  532. y = self._get_line_y(linenumber - 1)
  533. draw.rectangle([(x, y), (x + rectw, y + recth)],
  534. fill=self.hl_color)
  535. for pos, value, font, text_fg, text_bg in self.drawables:
  536. if text_bg:
  537. # see deprecations https://pillow.readthedocs.io/en/stable/releasenotes/9.2.0.html#font-size-and-offset-methods
  538. if hasattr(draw, 'textsize'):
  539. text_size = draw.textsize(text=value, font=font)
  540. else:
  541. text_size = font.getbbox(value)[2:]
  542. draw.rectangle([pos[0], pos[1], pos[0] + text_size[0], pos[1] + text_size[1]], fill=text_bg)
  543. draw.text(pos, value, font=font, fill=text_fg)
  544. im.save(outfile, self.image_format.upper())
  545. # Add one formatter per format, so that the "-f gif" option gives the correct result
  546. # when used in pygmentize.
  547. class GifImageFormatter(ImageFormatter):
  548. """
  549. Create a GIF image from source code. This uses the Python Imaging Library to
  550. generate a pixmap from the source code.
  551. .. versionadded:: 1.0
  552. """
  553. name = 'img_gif'
  554. aliases = ['gif']
  555. filenames = ['*.gif']
  556. default_image_format = 'gif'
  557. class JpgImageFormatter(ImageFormatter):
  558. """
  559. Create a JPEG image from source code. This uses the Python Imaging Library to
  560. generate a pixmap from the source code.
  561. .. versionadded:: 1.0
  562. """
  563. name = 'img_jpg'
  564. aliases = ['jpg', 'jpeg']
  565. filenames = ['*.jpg']
  566. default_image_format = 'jpeg'
  567. class BmpImageFormatter(ImageFormatter):
  568. """
  569. Create a bitmap image from source code. This uses the Python Imaging Library to
  570. generate a pixmap from the source code.
  571. .. versionadded:: 1.0
  572. """
  573. name = 'img_bmp'
  574. aliases = ['bmp', 'bitmap']
  575. filenames = ['*.bmp']
  576. default_image_format = 'bmp'