Team Ai
Datasetpublic

codekingpro/portable-devtools

sourceHugging Faceupdated 5mo agoView on Hugging Face
1likes14kdownloads
img.py687 linesDownload Raw Back to formatters
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 
codekingpro/portable-devtools · Team Ai