codekingpro/portable-devtools
114k
1"""2 pygments.formatters.img3 ~~~~~~~~~~~~~~~~~~~~~~~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 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 _, _, files in os.walk(font_dir)136 for f in files137 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 break144 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 break152 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 val165 except OSError:166 continue167 else:168 if fail:169 raise FontNotFound(f'Font {basename} ({styles[0]}) not found in registry')170 return None171 172 def _create_win(self):173 lookuperror = None174 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 return194 except FontNotFound as err:195 lookuperror = err196 finally:197 _winreg.CloseKey(key)198 except OSError:199 pass200 else:201 # If we get here, we checked all registry keys and had no luck202 # We can be in one of two situations now:203 # * All key lookups failed. In this case lookuperror is None and we204 # will raise a generic error205 # * At least one lookup failed with a FontNotFound error. In this206 # case, we will raise that as a more specific error207 if lookuperror:208 raise lookuperror209 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.0223 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 font262 except ValueError:263 pass264 except OSError:265 return font266 267 return font268 269 270class ImageFormatter(Formatter):271 """272 Create a PNG image from source code. This uses the Python Imaging Library to273 generate a pixmap from the source code.274 275 .. versionadded:: 0.10276 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: 2291 292 `font_name`293 The font name to be used as the base font from which others, such as294 bold and italic fonts will be generated. This really should be a295 monospace font to look sane.296 If a filename or a file-like object is specified, the user must297 provide different styles of the font.298 299 Default: "Courier New" on Windows, "Menlo" on Mac OS, and300 "DejaVu Sans Mono" on \\*nix301 302 `font_size`303 The font size in points to be used.304 305 Default: 14306 307 `image_pad`308 The padding, in pixels to be used at each edge of the resulting image.309 310 Default: 10311 312 `line_numbers`313 Whether line numbers should be shown: True/False314 315 Default: True316 317 `line_number_start`318 The line number of the first line.319 320 Default: 1321 322 `line_number_step`323 The step used when printing line numbers.324 325 Default: 1326 327 `line_number_bg`328 The background colour (in "#123456" format) of the line number bar, or329 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 number340 margin.341 342 Default: 2343 344 `line_number_bold`345 Whether line numbers will be bold: True/False346 347 Default: False348 349 `line_number_italic`350 Whether line numbers will be italicized: True/False351 352 Default: False353 354 `line_number_separator`355 Whether a line will be drawn between the line number area and the356 source code area: True/False357 358 Default: True359 360 `line_number_pad`361 The horizontal padding (in pixels) between the line number margin, and362 the source code area.363 364 Default: 6365 366 `hl_lines`367 Specify a list of lines to be highlighted.368 369 .. versionadded:: 1.2370 371 Default: empty list372 373 `hl_color`374 Specify the color for highlighting lines.375 376 .. versionadded:: 1.2377 378 Default: highlight color of the selected style379 """380 381 # Required by the pygments mapper382 name = 'img'383 aliases = ['img', 'IMG', 'png']384 filenames = ['*.png']385 386 unicodeoutput = False387 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 thing399 # Read the style400 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_color405 # Image options406 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 fonts412 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 options416 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 = 0435 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 pass442 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_pad455 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_pad461 462 def _get_char_width(self):463 """464 Get the width of a character.465 """466 return self.fontw467 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_width473 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 fill495 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 = None504 return bg_color505 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 = 0543 maxlinelength = linelength = 0544 for ttype, value in tokensource:545 while ttype not in self.styles:546 ttype = ttype.parent547 style = self.styles[ttype]548 # TODO: make sure tab expansion happens earlier in the chain. It549 # really ought to be done on the input, as to do it right here is550 # quite complex.551 value = value.expandtabs(4)552 lines = value.splitlines(True)553 # print lines554 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_width566 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 value571 linelength = 0572 charno = 0573 lineno += 1574 self.maxlinelength = maxlinelength575 self.maxcharno = maxcharno576 self.maxlineno = lineno577 578 def _draw_line_numbers(self):579 """580 Create drawables for the line numbers.581 """582 if not self.line_numbers:583 return584 for p in range(self.maxlineno):585 n = p + self.line_number_start586 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 return595 if self.line_number_fg is None:596 return597 draw = ImageDraw.Draw(im)598 recth = im.size[-1]599 rectw = self.image_pad + self.line_number_width - self.line_number_pad600 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 draw605 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 the612 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_color620 )621 self._paint_line_number_bg(im)622 draw = ImageDraw.Draw(im)623 # Highlight624 if self.hl_lines:625 x = self.image_pad + self.line_number_width - self.line_number_pad + 1626 recth = self._get_line_height()627 rectw = im.size[0] - x628 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-methods635 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 result645# when used in pygmentize.646 647class GifImageFormatter(ImageFormatter):648 """649 Create a GIF image from source code. This uses the Python Imaging Library to650 generate a pixmap from the source code.651 652 .. versionadded:: 1.0653 """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 to664 generate a pixmap from the source code.665 666 .. versionadded:: 1.0667 """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 to678 generate a pixmap from the source code.679 680 .. versionadded:: 1.0681 """682 683 name = 'img_bmp'684 aliases = ['bmp', 'bitmap']685 filenames = ['*.bmp']686 default_image_format = 'bmp'687 