最近碰见了这样一个需求,需要使用pyments对代码进行高亮并生成图片,但是存在生成的代码图片没有行数限制的问题,于是我自己重新改写了image formatter规则。

重写的类

以下带代码可能在pyright出现报错,但不影响运行

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
class LineBreakFormatter(ImageFormatter):
def __init__(self, **options):
super().__init__(**options)
# 每行最大字符数(0 或未设置表示不启用字符数换行)
self.max_chars_per_line = int(options.get("max_chars_per_line", 0) or 0)

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.
"""
line_break_map = self._create_drawables(tokensource)
logger.debug(f"Line break map: {line_break_map}")
self._draw_line_numbers(line_break_map)
sizex, sizey = self._get_image_size(self.maxlinelength, self.maxlineno)
im = Image.new(
"RGB",
(sizex + 200, sizey),
self.background_color,
)
self._paint_line_number_bg(im)
draw = ImageDraw.Draw(im)
# Highlight
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:
# see deprecations https://pillow.readthedocs.io/en/stable/releasenotes/9.2.0.html#font-size-and-offset-methods
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())

def _draw_line_numbers(self, is_line_break: List[bool]):
"""
Create drawables for the line numbers.
"""
if not self.line_numbers:
return
if not is_line_break:
is_line_break = [False] * (self.maxlineno + 1)
n = 0
for p, t in zip(range(self.maxlineno), is_line_break):
if t:
n += 1
if (n % self.line_number_step) == 0:
self._draw_linenumber(p, n)
else:
self._draw_linenumber(p, " ")

def _create_drawables(self, tokensource) -> list[bool]:
"""
Create drawables for the token content.
"""
lineno = 0
charno = 0
maxcharno = 0
maxlinelength = 0
linelength = 0
is_line_break = [True]
for ttype, value in tokensource:
while ttype not in self.styles:
ttype = ttype.parent
style = self.styles[ttype]
# TODO: make sure tab expansion happens earlier in the chain. It
# really ought to be done on the input, as to do it right here is
# quite complex.
value = value.expandtabs(4)
lines = value.splitlines(True)
for line in lines:
temp = line.rstrip("\n")
while temp:
if not self.max_chars_per_line:
chunk = temp
temp = ""
else:
remaining = self.max_chars_per_line - charno
if remaining <= 0:
# 软换行:进入新行
linelength = 0
charno = 0
lineno += 1
is_line_break.append(False)
remaining = self.max_chars_per_line
if len(temp) > remaining:
chunk, temp = temp[:remaining], temp[remaining:]
else:
chunk, temp = temp, ""

if chunk:
self._draw_text(
self._get_text_pos(linelength, lineno),
chunk,
font=self._get_style_font(style),
text_fg=self._get_text_color(style),
text_bg=self._get_text_bg_color(style),
)
chunk_width, _ = self.fonts.get_text_size(chunk)
linelength += chunk_width
maxlinelength = max(maxlinelength, linelength)
charno += len(chunk)
maxcharno = max(maxcharno, charno)

if line.endswith("\n"):
# 源文本的硬换行
linelength = 0
charno = 0
lineno += 1
is_line_break.append(True)
self.maxlinelength = maxlinelength
self.maxcharno = maxcharno
self.maxlineno = lineno
return is_line_break

使用方法

如果你之前的代码是这这样的

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
DEFAULT_FMT_CONFIG = {
"font_name": "DejaVu Sans Mono",
"style": "sas",
"line_pad": 40,
"font_size": 50,
"line_number_pad": 10,
}

fmt_config = DEFAULT_FMT_CONFIG.copy()
code = /*代码*/
code_type = /*代码语言*/
if extra_fmt:
fmt_config.update(extra_fmt)
lexer = get_lexer_by_name(code_type)
formatter = ImageFormatter(**fmt_config)
highlighted_code = highlight(code, lexer, formatter)
image_path = f"code_{i}.png"
with open(image_path, "wb") as fw:
fw.write(highlighted_code)

只需要修改为以下格式即可

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
DEFAULT_FMT_CONFIG = {
"font_name": "DejaVu Sans Mono",
"style": "sas",
"line_pad": 40,
"font_size": 50,
"line_number_pad": 10,
"max_chars_per_line": 80, #添加最大单行代码限制
}

fmt_config = DEFAULT_FMT_CONFIG.copy()
code =/*代码*/
code_type = /*代码语言*/
if extra_fmt:
fmt_config.update(extra_fmt)
lexer = get_lexer_by_name(code_type)
formatter = LineBreakFormatter(**fmt_config) #替换为新加的formatter
highlighted_code = highlight(code, lexer, formatter)
image_path = f"code_{i}.png"
with open(image_path, "wb") as fw:
fw.write(highlighted_code)