Team Ai
Datasetpublic

codekingpro/portable-devtools

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