|
""" |
|
pygments.formatters.img |
|
~~~~~~~~~~~~~~~~~~~~~~~ |
|
|
|
Formatter for Pixmap output. |
|
|
|
:copyright: Copyright 2006-2025 by the Pygments team, see AUTHORS. |
|
:license: BSD, see LICENSE for details. |
|
""" |
|
import os |
|
import sys |
|
|
|
from pygments.formatter import Formatter |
|
from pygments.util import get_bool_opt, get_int_opt, get_list_opt, \ |
|
get_choice_opt |
|
|
|
import subprocess |
|
|
|
|
|
try: |
|
from PIL import Image, ImageDraw, ImageFont |
|
pil_available = True |
|
except ImportError: |
|
pil_available = False |
|
|
|
try: |
|
import _winreg |
|
except ImportError: |
|
try: |
|
import winreg as _winreg |
|
except ImportError: |
|
_winreg = None |
|
|
|
__all__ = ['ImageFormatter', 'GifImageFormatter', 'JpgImageFormatter', |
|
'BmpImageFormatter'] |
|
|
|
|
|
|
|
STYLES = { |
|
'NORMAL': ['', 'Roman', 'Book', 'Normal', 'Regular', 'Medium'], |
|
'ITALIC': ['Oblique', 'Italic'], |
|
'BOLD': ['Bold'], |
|
'BOLDITALIC': ['Bold Oblique', 'Bold Italic'], |
|
} |
|
|
|
|
|
DEFAULT_FONT_NAME_NIX = 'DejaVu Sans Mono' |
|
DEFAULT_FONT_NAME_WIN = 'Courier New' |
|
DEFAULT_FONT_NAME_MAC = 'Menlo' |
|
|
|
|
|
class PilNotAvailable(ImportError): |
|
"""When Python imaging library is not available""" |
|
|
|
|
|
class FontNotFound(Exception): |
|
"""When there are no usable fonts specified""" |
|
|
|
|
|
class FontManager: |
|
""" |
|
Manages a set of fonts: normal, italic, bold, etc... |
|
""" |
|
|
|
def __init__(self, font_name, font_size=14): |
|
self.font_name = font_name |
|
self.font_size = font_size |
|
self.fonts = {} |
|
self.encoding = None |
|
self.variable = False |
|
if hasattr(font_name, 'read') or os.path.isfile(font_name): |
|
font = ImageFont.truetype(font_name, self.font_size) |
|
self.variable = True |
|
for style in STYLES: |
|
self.fonts[style] = font |
|
|
|
return |
|
|
|
if sys.platform.startswith('win'): |
|
if not font_name: |
|
self.font_name = DEFAULT_FONT_NAME_WIN |
|
self._create_win() |
|
elif sys.platform.startswith('darwin'): |
|
if not font_name: |
|
self.font_name = DEFAULT_FONT_NAME_MAC |
|
self._create_mac() |
|
else: |
|
if not font_name: |
|
self.font_name = DEFAULT_FONT_NAME_NIX |
|
self._create_nix() |
|
|
|
def _get_nix_font_path(self, name, style): |
|
proc = subprocess.Popen(['fc-list', f"{name}:style={style}", 'file'], |
|
stdout=subprocess.PIPE, stderr=None) |
|
stdout, _ = proc.communicate() |
|
if proc.returncode == 0: |
|
lines = stdout.splitlines() |
|
for line in lines: |
|
if line.startswith(b'Fontconfig warning:'): |
|
continue |
|
path = line.decode().strip().strip(':') |
|
if path: |
|
return path |
|
return None |
|
|
|
def _create_nix(self): |
|
for name in STYLES['NORMAL']: |
|
path = self._get_nix_font_path(self.font_name, name) |
|
if path is not None: |
|
self.fonts['NORMAL'] = ImageFont.truetype(path, self.font_size) |
|
break |
|
else: |
|
raise FontNotFound(f'No usable fonts named: "{self.font_name}"') |
|
for style in ('ITALIC', 'BOLD', 'BOLDITALIC'): |
|
for stylename in STYLES[style]: |
|
path = self._get_nix_font_path(self.font_name, stylename) |
|
if path is not None: |
|
self.fonts[style] = ImageFont.truetype(path, self.font_size) |
|
break |
|
else: |
|
if style == 'BOLDITALIC': |
|
self.fonts[style] = self.fonts['BOLD'] |
|
else: |
|
self.fonts[style] = self.fonts['NORMAL'] |
|
|
|
def _get_mac_font_path(self, font_map, name, style): |
|
return font_map.get((name + ' ' + style).strip().lower()) |
|
|
|
def _create_mac(self): |
|
font_map = {} |
|
for font_dir in (os.path.join(os.getenv("HOME"), 'Library/Fonts/'), |
|
'/Library/Fonts/', '/System/Library/Fonts/'): |
|
font_map.update( |
|
(os.path.splitext(f)[0].lower(), os.path.join(font_dir, f)) |
|
for _, _, files in os.walk(font_dir) |
|
for f in files |
|
if f.lower().endswith(('ttf', 'ttc'))) |
|
|
|
for name in STYLES['NORMAL']: |
|
path = self._get_mac_font_path(font_map, self.font_name, name) |
|
if path is not None: |
|
self.fonts['NORMAL'] = ImageFont.truetype(path, self.font_size) |
|
break |
|
else: |
|
raise FontNotFound(f'No usable fonts named: "{self.font_name}"') |
|
for style in ('ITALIC', 'BOLD', 'BOLDITALIC'): |
|
for stylename in STYLES[style]: |
|
path = self._get_mac_font_path(font_map, self.font_name, stylename) |
|
if path is not None: |
|
self.fonts[style] = ImageFont.truetype(path, self.font_size) |
|
break |
|
else: |
|
if style == 'BOLDITALIC': |
|
self.fonts[style] = self.fonts['BOLD'] |
|
else: |
|
self.fonts[style] = self.fonts['NORMAL'] |
|
|
|
def _lookup_win(self, key, basename, styles, fail=False): |
|
for suffix in ('', ' (TrueType)'): |
|
for style in styles: |
|
try: |
|
valname = '{}{}{}'.format(basename, style and ' '+style, suffix) |
|
val, _ = _winreg.QueryValueEx(key, valname) |
|
return val |
|
except OSError: |
|
continue |
|
else: |
|
if fail: |
|
raise FontNotFound(f'Font {basename} ({styles[0]}) not found in registry') |
|
return None |
|
|
|
def _create_win(self): |
|
lookuperror = None |
|
keynames = [ (_winreg.HKEY_CURRENT_USER, r'Software\Microsoft\Windows NT\CurrentVersion\Fonts'), |
|
(_winreg.HKEY_CURRENT_USER, r'Software\Microsoft\Windows\CurrentVersion\Fonts'), |
|
(_winreg.HKEY_LOCAL_MACHINE, r'Software\Microsoft\Windows NT\CurrentVersion\Fonts'), |
|
(_winreg.HKEY_LOCAL_MACHINE, r'Software\Microsoft\Windows\CurrentVersion\Fonts') ] |
|
for keyname in keynames: |
|
try: |
|
key = _winreg.OpenKey(*keyname) |
|
try: |
|
path = self._lookup_win(key, self.font_name, STYLES['NORMAL'], True) |
|
self.fonts['NORMAL'] = ImageFont.truetype(path, self.font_size) |
|
for style in ('ITALIC', 'BOLD', 'BOLDITALIC'): |
|
path = self._lookup_win(key, self.font_name, STYLES[style]) |
|
if path: |
|
self.fonts[style] = ImageFont.truetype(path, self.font_size) |
|
else: |
|
if style == 'BOLDITALIC': |
|
self.fonts[style] = self.fonts['BOLD'] |
|
else: |
|
self.fonts[style] = self.fonts['NORMAL'] |
|
return |
|
except FontNotFound as err: |
|
lookuperror = err |
|
finally: |
|
_winreg.CloseKey(key) |
|
except OSError: |
|
pass |
|
else: |
|
|
|
|
|
|
|
|
|
|
|
|
|
if lookuperror: |
|
raise lookuperror |
|
raise FontNotFound('Can\'t open Windows font registry key') |
|
|
|
def get_char_size(self): |
|
""" |
|
Get the character size. |
|
""" |
|
return self.get_text_size('M') |
|
|
|
def get_text_size(self, text): |
|
""" |
|
Get the text size (width, height). |
|
""" |
|
font = self.fonts['NORMAL'] |
|
if hasattr(font, 'getbbox'): |
|
return font.getbbox(text)[2:4] |
|
else: |
|
return font.getsize(text) |
|
|
|
def get_font(self, bold, oblique): |
|
""" |
|
Get the font based on bold and italic flags. |
|
""" |
|
if bold and oblique: |
|
if self.variable: |
|
return self.get_style('BOLDITALIC') |
|
|
|
return self.fonts['BOLDITALIC'] |
|
elif bold: |
|
if self.variable: |
|
return self.get_style('BOLD') |
|
|
|
return self.fonts['BOLD'] |
|
elif oblique: |
|
if self.variable: |
|
return self.get_style('ITALIC') |
|
|
|
return self.fonts['ITALIC'] |
|
else: |
|
if self.variable: |
|
return self.get_style('NORMAL') |
|
|
|
return self.fonts['NORMAL'] |
|
|
|
def get_style(self, style): |
|
""" |
|
Get the specified style of the font if it is a variable font. |
|
If not found, return the normal font. |
|
""" |
|
font = self.fonts[style] |
|
for style_name in STYLES[style]: |
|
try: |
|
font.set_variation_by_name(style_name) |
|
return font |
|
except ValueError: |
|
pass |
|
except OSError: |
|
return font |
|
|
|
return font |
|
|
|
|
|
class ImageFormatter(Formatter): |
|
""" |
|
Create a PNG image from source code. This uses the Python Imaging Library to |
|
generate a pixmap from the source code. |
|
|
|
.. versionadded:: 0.10 |
|
|
|
Additional options accepted: |
|
|
|
`image_format` |
|
An image format to output to that is recognised by PIL, these include: |
|
|
|
* "PNG" (default) |
|
* "JPEG" |
|
* "BMP" |
|
* "GIF" |
|
|
|
`line_pad` |
|
The extra spacing (in pixels) between each line of text. |
|
|
|
Default: 2 |
|
|
|
`font_name` |
|
The font name to be used as the base font from which others, such as |
|
bold and italic fonts will be generated. This really should be a |
|
monospace font to look sane. |
|
If a filename or a file-like object is specified, the user must |
|
provide different styles of the font. |
|
|
|
Default: "Courier New" on Windows, "Menlo" on Mac OS, and |
|
"DejaVu Sans Mono" on \\*nix |
|
|
|
`font_size` |
|
The font size in points to be used. |
|
|
|
Default: 14 |
|
|
|
`image_pad` |
|
The padding, in pixels to be used at each edge of the resulting image. |
|
|
|
Default: 10 |
|
|
|
`line_numbers` |
|
Whether line numbers should be shown: True/False |
|
|
|
Default: True |
|
|
|
`line_number_start` |
|
The line number of the first line. |
|
|
|
Default: 1 |
|
|
|
`line_number_step` |
|
The step used when printing line numbers. |
|
|
|
Default: 1 |
|
|
|
`line_number_bg` |
|
The background colour (in "#123456" format) of the line number bar, or |
|
None to use the style background color. |
|
|
|
Default: "#eed" |
|
|
|
`line_number_fg` |
|
The text color of the line numbers (in "#123456"-like format). |
|
|
|
Default: "#886" |
|
|
|
`line_number_chars` |
|
The number of columns of line numbers allowable in the line number |
|
margin. |
|
|
|
Default: 2 |
|
|
|
`line_number_bold` |
|
Whether line numbers will be bold: True/False |
|
|
|
Default: False |
|
|
|
`line_number_italic` |
|
Whether line numbers will be italicized: True/False |
|
|
|
Default: False |
|
|
|
`line_number_separator` |
|
Whether a line will be drawn between the line number area and the |
|
source code area: True/False |
|
|
|
Default: True |
|
|
|
`line_number_pad` |
|
The horizontal padding (in pixels) between the line number margin, and |
|
the source code area. |
|
|
|
Default: 6 |
|
|
|
`hl_lines` |
|
Specify a list of lines to be highlighted. |
|
|
|
.. versionadded:: 1.2 |
|
|
|
Default: empty list |
|
|
|
`hl_color` |
|
Specify the color for highlighting lines. |
|
|
|
.. versionadded:: 1.2 |
|
|
|
Default: highlight color of the selected style |
|
""" |
|
|
|
|
|
name = 'img' |
|
aliases = ['img', 'IMG', 'png'] |
|
filenames = ['*.png'] |
|
|
|
unicodeoutput = False |
|
|
|
default_image_format = 'png' |
|
|
|
def __init__(self, **options): |
|
""" |
|
See the class docstring for explanation of options. |
|
""" |
|
if not pil_available: |
|
raise PilNotAvailable( |
|
'Python Imaging Library is required for this formatter') |
|
Formatter.__init__(self, **options) |
|
self.encoding = 'latin1' |
|
|
|
self.styles = dict(self.style) |
|
if self.style.background_color is None: |
|
self.background_color = '#fff' |
|
else: |
|
self.background_color = self.style.background_color |
|
|
|
self.image_format = get_choice_opt( |
|
options, 'image_format', ['png', 'jpeg', 'gif', 'bmp'], |
|
self.default_image_format, normcase=True) |
|
self.image_pad = get_int_opt(options, 'image_pad', 10) |
|
self.line_pad = get_int_opt(options, 'line_pad', 2) |
|
|
|
fontsize = get_int_opt(options, 'font_size', 14) |
|
self.fonts = FontManager(options.get('font_name', ''), fontsize) |
|
self.fontw, self.fonth = self.fonts.get_char_size() |
|
|
|
self.line_number_fg = options.get('line_number_fg', '#886') |
|
self.line_number_bg = options.get('line_number_bg', '#eed') |
|
self.line_number_chars = get_int_opt(options, |
|
'line_number_chars', 2) |
|
self.line_number_bold = get_bool_opt(options, |
|
'line_number_bold', False) |
|
self.line_number_italic = get_bool_opt(options, |
|
'line_number_italic', False) |
|
self.line_number_pad = get_int_opt(options, 'line_number_pad', 6) |
|
self.line_numbers = get_bool_opt(options, 'line_numbers', True) |
|
self.line_number_separator = get_bool_opt(options, |
|
'line_number_separator', True) |
|
self.line_number_step = get_int_opt(options, 'line_number_step', 1) |
|
self.line_number_start = get_int_opt(options, 'line_number_start', 1) |
|
if self.line_numbers: |
|
self.line_number_width = (self.fontw * self.line_number_chars + |
|
self.line_number_pad * 2) |
|
else: |
|
self.line_number_width = 0 |
|
self.hl_lines = [] |
|
hl_lines_str = get_list_opt(options, 'hl_lines', []) |
|
for line in hl_lines_str: |
|
try: |
|
self.hl_lines.append(int(line)) |
|
except ValueError: |
|
pass |
|
self.hl_color = options.get('hl_color', |
|
self.style.highlight_color) or '#f90' |
|
self.drawables = [] |
|
|
|
def get_style_defs(self, arg=''): |
|
raise NotImplementedError('The -S option is meaningless for the image ' |
|
'formatter. Use -O style=<stylename> instead.') |
|
|
|
def _get_line_height(self): |
|
""" |
|
Get the height of a line. |
|
""" |
|
return self.fonth + self.line_pad |
|
|
|
def _get_line_y(self, lineno): |
|
""" |
|
Get the Y coordinate of a line number. |
|
""" |
|
return lineno * self._get_line_height() + self.image_pad |
|
|
|
def _get_char_width(self): |
|
""" |
|
Get the width of a character. |
|
""" |
|
return self.fontw |
|
|
|
def _get_char_x(self, linelength): |
|
""" |
|
Get the X coordinate of a character position. |
|
""" |
|
return linelength + self.image_pad + self.line_number_width |
|
|
|
def _get_text_pos(self, linelength, lineno): |
|
""" |
|
Get the actual position for a character and line position. |
|
""" |
|
return self._get_char_x(linelength), self._get_line_y(lineno) |
|
|
|
def _get_linenumber_pos(self, lineno): |
|
""" |
|
Get the actual position for the start of a line number. |
|
""" |
|
return (self.image_pad, self._get_line_y(lineno)) |
|
|
|
def _get_text_color(self, style): |
|
""" |
|
Get the correct color for the token from the style. |
|
""" |
|
if style['color'] is not None: |
|
fill = '#' + style['color'] |
|
else: |
|
fill = '#000' |
|
return fill |
|
|
|
def _get_text_bg_color(self, style): |
|
""" |
|
Get the correct background color for the token from the style. |
|
""" |
|
if style['bgcolor'] is not None: |
|
bg_color = '#' + style['bgcolor'] |
|
else: |
|
bg_color = None |
|
return bg_color |
|
|
|
def _get_style_font(self, style): |
|
""" |
|
Get the correct font for the style. |
|
""" |
|
return self.fonts.get_font(style['bold'], style['italic']) |
|
|
|
def _get_image_size(self, maxlinelength, maxlineno): |
|
""" |
|
Get the required image size. |
|
""" |
|
return (self._get_char_x(maxlinelength) + self.image_pad, |
|
self._get_line_y(maxlineno + 0) + self.image_pad) |
|
|
|
def _draw_linenumber(self, posno, lineno): |
|
""" |
|
Remember a line number drawable to paint later. |
|
""" |
|
self._draw_text( |
|
self._get_linenumber_pos(posno), |
|
str(lineno).rjust(self.line_number_chars), |
|
font=self.fonts.get_font(self.line_number_bold, |
|
self.line_number_italic), |
|
text_fg=self.line_number_fg, |
|
text_bg=None, |
|
) |
|
|
|
def _draw_text(self, pos, text, font, text_fg, text_bg): |
|
""" |
|
Remember a single drawable tuple to paint later. |
|
""" |
|
self.drawables.append((pos, text, font, text_fg, text_bg)) |
|
|
|
def _create_drawables(self, tokensource): |
|
""" |
|
Create drawables for the token content. |
|
""" |
|
lineno = charno = maxcharno = 0 |
|
maxlinelength = linelength = 0 |
|
for ttype, value in tokensource: |
|
while ttype not in self.styles: |
|
ttype = ttype.parent |
|
style = self.styles[ttype] |
|
|
|
|
|
|
|
value = value.expandtabs(4) |
|
lines = value.splitlines(True) |
|
|
|
for i, line in enumerate(lines): |
|
temp = line.rstrip('\n') |
|
if temp: |
|
self._draw_text( |
|
self._get_text_pos(linelength, lineno), |
|
temp, |
|
font = self._get_style_font(style), |
|
text_fg = self._get_text_color(style), |
|
text_bg = self._get_text_bg_color(style), |
|
) |
|
temp_width, _ = self.fonts.get_text_size(temp) |
|
linelength += temp_width |
|
maxlinelength = max(maxlinelength, linelength) |
|
charno += len(temp) |
|
maxcharno = max(maxcharno, charno) |
|
if line.endswith('\n'): |
|
|
|
linelength = 0 |
|
charno = 0 |
|
lineno += 1 |
|
self.maxlinelength = maxlinelength |
|
self.maxcharno = maxcharno |
|
self.maxlineno = lineno |
|
|
|
def _draw_line_numbers(self): |
|
""" |
|
Create drawables for the line numbers. |
|
""" |
|
if not self.line_numbers: |
|
return |
|
for p in range(self.maxlineno): |
|
n = p + self.line_number_start |
|
if (n % self.line_number_step) == 0: |
|
self._draw_linenumber(p, n) |
|
|
|
def _paint_line_number_bg(self, im): |
|
""" |
|
Paint the line number background on the image. |
|
""" |
|
if not self.line_numbers: |
|
return |
|
if self.line_number_fg is None: |
|
return |
|
draw = ImageDraw.Draw(im) |
|
recth = im.size[-1] |
|
rectw = self.image_pad + self.line_number_width - self.line_number_pad |
|
draw.rectangle([(0, 0), (rectw, recth)], |
|
fill=self.line_number_bg) |
|
if self.line_number_separator: |
|
draw.line([(rectw, 0), (rectw, recth)], fill=self.line_number_fg) |
|
del draw |
|
|
|
def format(self, tokensource, outfile): |
|
""" |
|
Format ``tokensource``, an iterable of ``(tokentype, tokenstring)`` |
|
tuples and write it into ``outfile``. |
|
|
|
This implementation calculates where it should draw each token on the |
|
pixmap, then calculates the required pixmap size and draws the items. |
|
""" |
|
self._create_drawables(tokensource) |
|
self._draw_line_numbers() |
|
im = Image.new( |
|
'RGB', |
|
self._get_image_size(self.maxlinelength, self.maxlineno), |
|
self.background_color |
|
) |
|
self._paint_line_number_bg(im) |
|
draw = ImageDraw.Draw(im) |
|
|
|
if self.hl_lines: |
|
x = self.image_pad + self.line_number_width - self.line_number_pad + 1 |
|
recth = self._get_line_height() |
|
rectw = im.size[0] - x |
|
for linenumber in self.hl_lines: |
|
y = self._get_line_y(linenumber - 1) |
|
draw.rectangle([(x, y), (x + rectw, y + recth)], |
|
fill=self.hl_color) |
|
for pos, value, font, text_fg, text_bg in self.drawables: |
|
if text_bg: |
|
|
|
if hasattr(draw, 'textsize'): |
|
text_size = draw.textsize(text=value, font=font) |
|
else: |
|
text_size = font.getbbox(value)[2:] |
|
draw.rectangle([pos[0], pos[1], pos[0] + text_size[0], pos[1] + text_size[1]], fill=text_bg) |
|
draw.text(pos, value, font=font, fill=text_fg) |
|
im.save(outfile, self.image_format.upper()) |
|
|
|
|
|
|
|
|
|
|
|
class GifImageFormatter(ImageFormatter): |
|
""" |
|
Create a GIF image from source code. This uses the Python Imaging Library to |
|
generate a pixmap from the source code. |
|
|
|
.. versionadded:: 1.0 |
|
""" |
|
|
|
name = 'img_gif' |
|
aliases = ['gif'] |
|
filenames = ['*.gif'] |
|
default_image_format = 'gif' |
|
|
|
|
|
class JpgImageFormatter(ImageFormatter): |
|
""" |
|
Create a JPEG image from source code. This uses the Python Imaging Library to |
|
generate a pixmap from the source code. |
|
|
|
.. versionadded:: 1.0 |
|
""" |
|
|
|
name = 'img_jpg' |
|
aliases = ['jpg', 'jpeg'] |
|
filenames = ['*.jpg'] |
|
default_image_format = 'jpeg' |
|
|
|
|
|
class BmpImageFormatter(ImageFormatter): |
|
""" |
|
Create a bitmap image from source code. This uses the Python Imaging Library to |
|
generate a pixmap from the source code. |
|
|
|
.. versionadded:: 1.0 |
|
""" |
|
|
|
name = 'img_bmp' |
|
aliases = ['bmp', 'bitmap'] |
|
filenames = ['*.bmp'] |
|
default_image_format = 'bmp' |
|
|