img.py 20 KB

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