Last active
April 6, 2024 11:17
-
-
Save oldj/9c4d012d6fff059ccea7 to your computer and use it in GitHub Desktop.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
# -*- coding: utf-8 -*- | |
# | |
# Author: oldj | |
# Email: [email protected] | |
# Blog: http://oldj.net | |
# | |
import os | |
import re | |
import StringIO | |
from PIL import Image | |
from PIL import ImageDraw | |
import pygame | |
g_script_folder = os.path.dirname(os.path.abspath(__file__)) | |
g_fonts_folder = os.path.join(g_script_folder, "fonts") | |
g_re_first_word = re.compile((u"" | |
+ u"(%(prefix)s+\S%(postfix)s+)" # 标点 | |
+ u"|(%(prefix)s*\w+%(postfix)s*)" # 单词 | |
+ u"|(%(prefix)s+\S)|(\S%(postfix)s+)" # 标点 | |
+ u"|(\d+%%)" # 百分数 | |
) % { | |
"prefix": u"['\"\(<\[\{‘“(《「『]", | |
"postfix": u"[:'\"\)>\]\}:’”)》」』,;\.\?!,、;。?!]", | |
}) | |
pygame.init() | |
def getFontForPyGame(font_name="wqy-zenhei.ttc", font_size=14): | |
return pygame.font.Font(os.path.join(g_fonts_folder, font_name), font_size) | |
def makeConfig(cfg=None): | |
if not cfg or type(cfg) != dict: | |
cfg = {} | |
default_cfg = { | |
"width": 440, # px | |
"padding": (15, 18, 20, 18), | |
"line-height": 20, #px | |
"title-line-height": 32, #px | |
"font-size": 14, # px | |
"title-font-size": 24, # px | |
"font-family": "wqy-zenhei.ttc", | |
# "font-family": "msyh.ttf", | |
"font-color": (0, 0, 0), | |
"font-antialiasing": True, # 字体是否反锯齿 | |
"background-color": (255, 255, 255), | |
"border-size": 1, | |
"border-color": (192, 192, 192), | |
"copyright": u"本图文由 txt2.im 自动生成,但不代表 txt2.im 赞同其内容或立场。", | |
"copyright-center": False, # 版权信息居中显示,如为 False 则居左显示 | |
"first-line-as-title": True, | |
"break-word": False, | |
} | |
default_cfg.update(cfg) | |
return default_cfg | |
def makeLineToWordsList(line, break_word=False): | |
u"""将一行文本转为单词列表""" | |
if break_word: | |
return [c for c in line] | |
lst = [] | |
while line: | |
ro = g_re_first_word.match(line) | |
end = 1 if not ro else ro.end() | |
lst.append(line[:end]) | |
line = line[end:] | |
return lst | |
def makeLongLineToLines(long_line, start_x, start_y, width, line_height, font, cn_char_width=0): | |
u"""将一个长行分成多个可显示的短行""" | |
txt = long_line | |
# txt = u"测试汉字abc123" | |
# txt = txt.decode("utf-8") | |
if not txt: | |
return [None] | |
words = makeLineToWordsList(txt) | |
lines = [] | |
if not cn_char_width: | |
cn_char_width, h = font.size(u"汉") | |
avg_char_per_line = width / cn_char_width | |
if avg_char_per_line <= 1: | |
avg_char_per_line = 1 | |
line_x = start_x | |
line_y = start_y | |
while words: | |
tmp_words = words[:avg_char_per_line] | |
tmp_ln = "".join(tmp_words) | |
w, h = font.size(tmp_ln) | |
wc = len(tmp_words) | |
while w < width and wc < len(words): | |
wc += 1 | |
tmp_words = words[:wc] | |
tmp_ln = "".join(tmp_words) | |
w, h = font.size(tmp_ln) | |
while w > width and len(tmp_words) > 1: | |
tmp_words = tmp_words[:-1] | |
tmp_ln = "".join(tmp_words) | |
w, h = font.size(tmp_ln) | |
if w > width and len(tmp_words) == 1: | |
# 处理一个长单词或长数字 | |
line_y = makeLongWordToLines( | |
tmp_words[0], line_x, line_y, width, line_height, font, lines | |
) | |
words = words[len(tmp_words):] | |
continue | |
line = { | |
"x": line_x, | |
"y": line_y, | |
"text": tmp_ln, | |
"font": font, | |
} | |
line_y += line_height | |
words = words[len(tmp_words):] | |
lines.append(line) | |
if len(lines) >= 1: | |
# 去掉长行的第二行开始的行首的空白字符 | |
while len(words) > 0 and not words[0].strip(): | |
words = words[1:] | |
return lines | |
def makeLongWordToLines(long_word, line_x, line_y, width, line_height, font, lines): | |
if not long_word: | |
return line_y | |
c = long_word[0] | |
char_width, char_height = font.size(c) | |
default_char_num_per_line = width / char_width | |
while long_word: | |
tmp_ln = long_word[:default_char_num_per_line] | |
w, h = font.size(tmp_ln) | |
l = len(tmp_ln) | |
while w < width and l < len(long_word): | |
l += 1 | |
tmp_ln = long_word[:l] | |
w, h = font.size(tmp_ln) | |
while w > width and len(tmp_ln) > 1: | |
tmp_ln = tmp_ln[:-1] | |
w, h = font.size(tmp_ln) | |
l = len(tmp_ln) | |
long_word = long_word[l:] | |
line = { | |
"x": line_x, | |
"y": line_y, | |
"text": tmp_ln, | |
"font": font, | |
} | |
line_y += line_height | |
lines.append(line) | |
return line_y | |
def makeMatrix(txt, font, title_font, cfg): | |
width = cfg["width"] | |
data = { | |
"width": width, | |
"height": 0, | |
"lines": [], | |
} | |
a = txt.split("\n") | |
cur_x = cfg["padding"][3] | |
cur_y = cfg["padding"][0] | |
cn_char_width, h = font.size(u"汉") | |
for ln_idx, ln in enumerate(a): | |
ln = ln.rstrip() | |
if ln_idx == 0 and cfg["first-line-as-title"]: | |
f = title_font | |
line_height = cfg["title-line-height"] | |
else: | |
f = font | |
line_height = cfg["line-height"] | |
current_width = width - cur_x - cfg["padding"][1] | |
lines = makeLongLineToLines(ln, cur_x, cur_y, current_width, line_height, f, cn_char_width=cn_char_width) | |
cur_y += line_height * len(lines) | |
data["lines"].extend(lines) | |
data["height"] = cur_y + cfg["padding"][2] | |
return data | |
def makeImage(data, cfg): | |
u""" | |
""" | |
width, height = data["width"], data["height"] | |
if cfg["copyright"]: | |
height += 48 | |
im = Image.new("RGB", (width, height), cfg["background-color"]) | |
dr = ImageDraw.Draw(im) | |
for ln_idx, line in enumerate(data["lines"]): | |
__makeLine(im, line, cfg) | |
# dr.text((line["x"], line["y"]), line["text"], font=font, fill=cfg["font-color"]) | |
# 缩放 | |
# im = im.resize((width / 2, height / 2), Image.ANTIALIAS) | |
drawBorder(im, dr, cfg) | |
drawCopyright(im, dr, cfg) | |
return im | |
def drawCopyright(im, dr, cfg): | |
u"""绘制版权信息""" | |
if not cfg["copyright"]: | |
return | |
font = getFontForPyGame(font_name=cfg["font-family"], font_size=12) | |
rtext = font.render(cfg["copyright"], | |
cfg["font-antialiasing"], (128, 128, 128), cfg["background-color"] | |
) | |
sio = StringIO.StringIO() | |
pygame.image.save(rtext, sio) | |
sio.seek(0) | |
copyright_im = Image.open(sio) | |
iw, ih = im.size | |
cw, ch = rtext.get_size() | |
padding = cfg["padding"] | |
offset_y = ih - 32 - padding[2] | |
if cfg["copyright-center"]: | |
cx = (iw - cw) / 2 | |
else: | |
cx = cfg["padding"][3] | |
cy = offset_y + 12 | |
dr.line([(padding[3], offset_y), (iw - padding[1], offset_y)], width=1, fill=(192, 192, 192)) | |
im.paste(copyright_im, (cx, cy)) | |
def drawBorder(im, dr, cfg): | |
u"""绘制边框""" | |
if not cfg["border-size"]: | |
return | |
w, h = im.size | |
x, y = w - 1, h - 1 | |
dr.line( | |
[(0, 0), (x, 0), (x, y), (0, y), (0, 0)], | |
width=cfg["border-size"], | |
fill=cfg["border-color"], | |
) | |
def __makeLine(im, line, cfg): | |
if not line: | |
return | |
sio = StringIO.StringIO() | |
x, y = line["x"], line["y"] | |
text = line["text"] | |
font = line["font"] | |
rtext = font.render(text, cfg["font-antialiasing"], cfg["font-color"], cfg["background-color"]) | |
pygame.image.save(rtext, sio) | |
sio.seek(0) | |
ln_im = Image.open(sio) | |
im.paste(ln_im, (x, y)) | |
def txt2im(txt, outfn, cfg=None, show=False): | |
# print(cfg) | |
cfg = makeConfig(cfg) | |
# print(cfg) | |
font = getFontForPyGame(cfg["font-family"], cfg["font-size"]) | |
title_font = getFontForPyGame(cfg["font-family"], cfg["title-font-size"]) | |
data = makeMatrix(txt, font, title_font, cfg) | |
im = makeImage(data, cfg) | |
im.save(outfn) | |
if os.name == "nt" and show: | |
im.show() | |
def test(): | |
c = open("test.txt", "rb").read().decode("utf-8") | |
txt2im(c, "test.png", show=True) | |
if __name__ == "__main__": | |
test() | |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Adapt for Python3