codekingpro/portable-devtools
114k
1"""2 pygments.formatters.img3 ~~~~~~~~~~~~~~~~~~~~~~~4 5 Formatter for Pixmap output.6 7 :copyright: Copyright 2006-2024 by the Pygments team, see AUTHORS.8 :license: BSD, see LICENSE for details.9"""10import os11import sys12 13from pygments.formatter import Formatter14from pygments.util import get_bool_opt, get_int_opt, get_list_opt, \15 get_choice_opt16 17import subprocess18 19# Import this carefully20try:21 from PIL import Image, ImageDraw, ImageFont22 pil_available = True23except ImportError:24 pil_available = False25 26try:27 import _winreg28except ImportError:29 try:30 import winreg as _winreg31 except ImportError:32 _winreg = None33 34__all__ = ['ImageFormatter', 'GifImageFormatter', 'JpgImageFormatter',35 'BmpImageFormatter']36 37 38# For some unknown reason every font calls it something different39STYLES = {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 systems47DEFAULT_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_name67 self.font_size = font_size68 self.fonts = {}69 self.encoding = None70 self.variable = False71 if hasattr(font_name, 'read') or os.path.isfile(font_name):72 font = ImageFont.truetype(font_name, self.font_size)73 self.variable = True74 for style in STYLES:75 self.fonts[style] = font76 77 return78 79 if sys.platform.startswith('win'):80 if not font_name:81 self.font_name = DEFAULT_FONT_NAME_WIN82 self._create_win()83 elif sys.platform.startswith('darwin'):84 if not font_name:85 self.font_name = DEFAULT_FONT_NAME_MAC86 self._create_mac()87 else:88 if not font_name:89 self.font_name = DEFAULT_FONT_NAME_NIX90 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 continue101 path = line.decode().strip().strip(':')102 if path:103 return path104 return None105 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 break112 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 break120 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 f in os.listdir(font_dir)136 if f.lower().endswith(('ttf', 'ttc')))137 138 for name in STYLES['NORMAL']:139 path = self._get_mac_font_path(font_map, self.font_name, name)140 if path is not None:141 self.fonts['NORMAL'] = ImageFont.truetype(path, self.font_size)142 break143 else:144 raise FontNotFound(f'No usable fonts named: "{self.font_name}"')145 for style in ('ITALIC', 'BOLD', 'BOLDITALIC'):146 for stylename in STYLES[style]:147 path = self._get_mac_font_path(font_map, self.font_name, stylename)148 if path is not None:149 self.fonts[style] = ImageFont.truetype(path, self.font_size)150 break151 else:152 if style == 'BOLDITALIC':153 self.fonts[style] = self.fonts['BOLD']154 else:155 self.fonts[style] = self.fonts['NORMAL']156 157 def _lookup_win(self, key, basename, styles, fail=False):158 for suffix in ('', ' (TrueType)'):159 for style in styles:160 try:161 valname = '{}{}{}'.format(basename, style and ' '+style, suffix)162 val, _ = _winreg.QueryValueEx(key, valname)163 return val164 except OSError:165 continue166 else:167 if fail:168 raise FontNotFound(f'Font {basename} ({styles[0]}) not found in registry')169 return None170 171 def _create_win(self):172 lookuperror = None173 keynames = [ (_winreg.HKEY_CURRENT_USER, r'Software\Microsoft\Windows NT\CurrentVersion\Fonts'),174 (_winreg.HKEY_CURRENT_USER, r'Software\Microsoft\Windows\CurrentVersion\Fonts'),175 (_winreg.HKEY_LOCAL_MACHINE, r'Software\Microsoft\Windows NT\CurrentVersion\Fonts'),176 (_winreg.HKEY_LOCAL_MACHINE, r'Software\Microsoft\Windows\CurrentVersion\Fonts') ]177 for keyname in keynames:178 try:179 key = _winreg.OpenKey(*keyname)180 try:181 path = self._lookup_win(key, self.font_name, STYLES['NORMAL'], True)182 self.fonts['NORMAL'] = ImageFont.truetype(path, self.font_size)183 for style in ('ITALIC', 'BOLD', 'BOLDITALIC'):184 path = self._lookup_win(key, self.font_name, STYLES[style])185 if path:186 self.fonts[style] = ImageFont.truetype(path, self.font_size)187 else:188 if style == 'BOLDITALIC':189 self.fonts[style] = self.fonts['BOLD']190 else:191 self.fonts[style] = self.fonts['NORMAL']192 return193 except FontNotFound as err:194 lookuperror = err195 finally:196 _winreg.CloseKey(key)197 except OSError:198 pass199 else:200 # If we get here, we checked all registry keys and had no luck201 # We can be in one of two situations now:202 # * All key lookups failed. In this case lookuperror is None and we203 # will raise a generic error204 # * At least one lookup failed with a FontNotFound error. In this205 # case, we will raise that as a more specific error206 if lookuperror:207 raise lookuperror208 raise FontNotFound('Can\'t open Windows font registry key')209 210 def get_char_size(self):211 """212 Get the character size.213 """214 return self.get_text_size('M')215 216 def get_text_size(self, text):217 """218 Get the text size (width, height).219 """220 font = self.fonts['NORMAL']221 if hasattr(font, 'getbbox'): # Pillow >= 9.2.0222 return font.getbbox(text)[2:4]223 else:224 return font.getsize(text)225 226 def get_font(self, bold, oblique):227 """228 Get the font based on bold and italic flags.229 """230 if bold and oblique:231 if self.variable:232 return self.get_style('BOLDITALIC')233 234 return self.fonts['BOLDITALIC']235 elif bold:236 if self.variable:237 return self.get_style('BOLD')238 239 return self.fonts['BOLD']240 elif oblique:241 if self.variable:242 return self.get_style('ITALIC')243 244 return self.fonts['ITALIC']245 else:246 if self.variable:247 return self.get_style('NORMAL')248 249 return self.fonts['NORMAL']250 251 def get_style(self, style):252 """253 Get the specified style of the font if it is a variable font.254 If not found, return the normal font.255 """256 font = self.fonts[style]257 for style_name in STYLES[style]:258 try:259 font.set_variation_by_name(style_name)260 return font261 except ValueError:262 pass263 except OSError:264 return font265 266 return font267 268 269class ImageFormatter(Formatter):270 """271 Create a PNG image from source code. This uses the Python Imaging Library to272 generate a pixmap from the source code.273 274 .. versionadded:: 0.10275 276 Additional options accepted:277 278 `image_format`279 An image format to output to that is recognised by PIL, these include:280 281 * "PNG" (default)282 * "JPEG"283 * "BMP"284 * "GIF"285 286 `line_pad`287 The extra spacing (in pixels) between each line of text.288 289 Default: 2290 291 `font_name`292 The font name to be used as the base font from which others, such as293 bold and italic fonts will be generated. This really should be a294 monospace font to look sane.295 If a filename or a file-like object is specified, the user must296 provide different styles of the font.297 298 Default: "Courier New" on Windows, "Menlo" on Mac OS, and299 "DejaVu Sans Mono" on \\*nix300 301 `font_size`302 The font size in points to be used.303 304 Default: 14305 306 `image_pad`307 The padding, in pixels to be used at each edge of the resulting image.308 309 Default: 10310 311 `line_numbers`312 Whether line numbers should be shown: True/False313 314 Default: True315 316 `line_number_start`317 The line number of the first line.318 319 Default: 1320 321 `line_number_step`322 The step used when printing line numbers.323 324 Default: 1325 326 `line_number_bg`327 The background colour (in "#123456" format) of the line number bar, or328 None to use the style background color.329 330 Default: "#eed"331 332 `line_number_fg`333 The text color of the line numbers (in "#123456"-like format).334 335 Default: "#886"336 337 `line_number_chars`338 The number of columns of line numbers allowable in the line number339 margin.340 341 Default: 2342 343 `line_number_bold`344 Whether line numbers will be bold: True/False345 346 Default: False347 348 `line_number_italic`349 Whether line numbers will be italicized: True/False350 351 Default: False352 353 `line_number_separator`354 Whether a line will be drawn between the line number area and the355 source code area: True/False356 357 Default: True358 359 `line_number_pad`360 The horizontal padding (in pixels) between the line number margin, and361 the source code area.362 363 Default: 6364 365 `hl_lines`366 Specify a list of lines to be highlighted.367 368 .. versionadded:: 1.2369 370 Default: empty list371 372 `hl_color`373 Specify the color for highlighting lines.374 375 .. versionadded:: 1.2376 377 Default: highlight color of the selected style378 """379 380 # Required by the pygments mapper381 name = 'img'382 aliases = ['img', 'IMG', 'png']383 filenames = ['*.png']384 385 unicodeoutput = False386 387 default_image_format = 'png'388 389 def __init__(self, **options):390 """391 See the class docstring for explanation of options.392 """393 if not pil_available:394 raise PilNotAvailable(395 'Python Imaging Library is required for this formatter')396 Formatter.__init__(self, **options)397 self.encoding = 'latin1' # let pygments.format() do the right thing398 # Read the style399 self.styles = dict(self.style)400 if self.style.background_color is None:401 self.background_color = '#fff'402 else:403 self.background_color = self.style.background_color404 # Image options405 self.image_format = get_choice_opt(406 options, 'image_format', ['png', 'jpeg', 'gif', 'bmp'],407 self.default_image_format, normcase=True)408 self.image_pad = get_int_opt(options, 'image_pad', 10)409 self.line_pad = get_int_opt(options, 'line_pad', 2)410 # The fonts411 fontsize = get_int_opt(options, 'font_size', 14)412 self.fonts = FontManager(options.get('font_name', ''), fontsize)413 self.fontw, self.fonth = self.fonts.get_char_size()414 # Line number options415 self.line_number_fg = options.get('line_number_fg', '#886')416 self.line_number_bg = options.get('line_number_bg', '#eed')417 self.line_number_chars = get_int_opt(options,418 'line_number_chars', 2)419 self.line_number_bold = get_bool_opt(options,420 'line_number_bold', False)421 self.line_number_italic = get_bool_opt(options,422 'line_number_italic', False)423 self.line_number_pad = get_int_opt(options, 'line_number_pad', 6)424 self.line_numbers = get_bool_opt(options, 'line_numbers', True)425 self.line_number_separator = get_bool_opt(options,426 'line_number_separator', True)427 self.line_number_step = get_int_opt(options, 'line_number_step', 1)428 self.line_number_start = get_int_opt(options, 'line_number_start', 1)429 if self.line_numbers:430 self.line_number_width = (self.fontw * self.line_number_chars +431 self.line_number_pad * 2)432 else:433 self.line_number_width = 0434 self.hl_lines = []435 hl_lines_str = get_list_opt(options, 'hl_lines', [])436 for line in hl_lines_str:437 try:438 self.hl_lines.append(int(line))439 except ValueError:440 pass441 self.hl_color = options.get('hl_color',442 self.style.highlight_color) or '#f90'443 self.drawables = []444 445 def get_style_defs(self, arg=''):446 raise NotImplementedError('The -S option is meaningless for the image '447 'formatter. Use -O style=<stylename> instead.')448 449 def _get_line_height(self):450 """451 Get the height of a line.452 """453 return self.fonth + self.line_pad454 455 def _get_line_y(self, lineno):456 """457 Get the Y coordinate of a line number.458 """459 return lineno * self._get_line_height() + self.image_pad460 461 def _get_char_width(self):462 """463 Get the width of a character.464 """465 return self.fontw466 467 def _get_char_x(self, linelength):468 """469 Get the X coordinate of a character position.470 """471 return linelength + self.image_pad + self.line_number_width472 473 def _get_text_pos(self, linelength, lineno):474 """475 Get the actual position for a character and line position.476 """477 return self._get_char_x(linelength), self._get_line_y(lineno)478 479 def _get_linenumber_pos(self, lineno):480 """481 Get the actual position for the start of a line number.482 """483 return (self.image_pad, self._get_line_y(lineno))484 485 def _get_text_color(self, style):486 """487 Get the correct color for the token from the style.488 """489 if style['color'] is not None:490 fill = '#' + style['color']491 else:492 fill = '#000'493 return fill494 495 def _get_text_bg_color(self, style):496 """497 Get the correct background color for the token from the style.498 """499 if style['bgcolor'] is not None:500 bg_color = '#' + style['bgcolor']501 else:502 bg_color = None503 return bg_color504 505 def _get_style_font(self, style):506 """507 Get the correct font for the style.508 """509 return self.fonts.get_font(style['bold'], style['italic'])510 511 def _get_image_size(self, maxlinelength, maxlineno):512 """513 Get the required image size.514 """515 return (self._get_char_x(maxlinelength) + self.image_pad,516 self._get_line_y(maxlineno + 0) + self.image_pad)517 518 def _draw_linenumber(self, posno, lineno):519 """520 Remember a line number drawable to paint later.521 """522 self._draw_text(523 self._get_linenumber_pos(posno),524 str(lineno).rjust(self.line_number_chars),525 font=self.fonts.get_font(self.line_number_bold,526 self.line_number_italic),527 text_fg=self.line_number_fg,528 text_bg=None,529 )530 531 def _draw_text(self, pos, text, font, text_fg, text_bg):532 """533 Remember a single drawable tuple to paint later.534 """535 self.drawables.append((pos, text, font, text_fg, text_bg))536 537 def _create_drawables(self, tokensource):538 """539 Create drawables for the token content.540 """541 lineno = charno = maxcharno = 0542 maxlinelength = linelength = 0543 for ttype, value in tokensource:544 while ttype not in self.styles:545 ttype = ttype.parent546 style = self.styles[ttype]547 # TODO: make sure tab expansion happens earlier in the chain. It548 # really ought to be done on the input, as to do it right here is549 # quite complex.550 value = value.expandtabs(4)551 lines = value.splitlines(True)552 # print lines553 for i, line in enumerate(lines):554 temp = line.rstrip('\n')555 if temp:556 self._draw_text(557 self._get_text_pos(linelength, lineno),558 temp,559 font = self._get_style_font(style),560 text_fg = self._get_text_color(style),561 text_bg = self._get_text_bg_color(style),562 )563 temp_width, _ = self.fonts.get_text_size(temp)564 linelength += temp_width565 maxlinelength = max(maxlinelength, linelength)566 charno += len(temp)567 maxcharno = max(maxcharno, charno)568 if line.endswith('\n'):569 # add a line for each extra line in the value570 linelength = 0571 charno = 0572 lineno += 1573 self.maxlinelength = maxlinelength574 self.maxcharno = maxcharno575 self.maxlineno = lineno576 577 def _draw_line_numbers(self):578 """579 Create drawables for the line numbers.580 """581 if not self.line_numbers:582 return583 for p in range(self.maxlineno):584 n = p + self.line_number_start585 if (n % self.line_number_step) == 0:586 self._draw_linenumber(p, n)587 588 def _paint_line_number_bg(self, im):589 """590 Paint the line number background on the image.591 """592 if not self.line_numbers:593 return594 if self.line_number_fg is None:595 return596 draw = ImageDraw.Draw(im)597 recth = im.size[-1]598 rectw = self.image_pad + self.line_number_width - self.line_number_pad599 draw.rectangle([(0, 0), (rectw, recth)],600 fill=self.line_number_bg)601 if self.line_number_separator:602 draw.line([(rectw, 0), (rectw, recth)], fill=self.line_number_fg)603 del draw604 605 def format(self, tokensource, outfile):606 """607 Format ``tokensource``, an iterable of ``(tokentype, tokenstring)``608 tuples and write it into ``outfile``.609 610 This implementation calculates where it should draw each token on the611 pixmap, then calculates the required pixmap size and draws the items.612 """613 self._create_drawables(tokensource)614 self._draw_line_numbers()615 im = Image.new(616 'RGB',617 self._get_image_size(self.maxlinelength, self.maxlineno),618 self.background_color619 )620 self._paint_line_number_bg(im)621 draw = ImageDraw.Draw(im)622 # Highlight623 if self.hl_lines:624 x = self.image_pad + self.line_number_width - self.line_number_pad + 1625 recth = self._get_line_height()626 rectw = im.size[0] - x627 for linenumber in self.hl_lines:628 y = self._get_line_y(linenumber - 1)629 draw.rectangle([(x, y), (x + rectw, y + recth)],630 fill=self.hl_color)631 for pos, value, font, text_fg, text_bg in self.drawables:632 if text_bg:633 # see deprecations https://pillow.readthedocs.io/en/stable/releasenotes/9.2.0.html#font-size-and-offset-methods634 if hasattr(draw, 'textsize'):635 text_size = draw.textsize(text=value, font=font)636 else:637 text_size = font.getbbox(value)[2:]638 draw.rectangle([pos[0], pos[1], pos[0] + text_size[0], pos[1] + text_size[1]], fill=text_bg)639 draw.text(pos, value, font=font, fill=text_fg)640 im.save(outfile, self.image_format.upper())641 642 643# Add one formatter per format, so that the "-f gif" option gives the correct result644# when used in pygmentize.645 646class GifImageFormatter(ImageFormatter):647 """648 Create a GIF image from source code. This uses the Python Imaging Library to649 generate a pixmap from the source code.650 651 .. versionadded:: 1.0652 """653 654 name = 'img_gif'655 aliases = ['gif']656 filenames = ['*.gif']657 default_image_format = 'gif'658 659 660class JpgImageFormatter(ImageFormatter):661 """662 Create a JPEG image from source code. This uses the Python Imaging Library to663 generate a pixmap from the source code.664 665 .. versionadded:: 1.0666 """667 668 name = 'img_jpg'669 aliases = ['jpg', 'jpeg']670 filenames = ['*.jpg']671 default_image_format = 'jpeg'672 673 674class BmpImageFormatter(ImageFormatter):675 """676 Create a bitmap image from source code. This uses the Python Imaging Library to677 generate a pixmap from the source code.678 679 .. versionadded:: 1.0680 """681 682 name = 'img_bmp'683 aliases = ['bmp', 'bitmap']684 filenames = ['*.bmp']685 default_image_format = 'bmp'686 