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'