Last active
April 6, 2024 11:17
-
-
Save oldj/9c4d012d6fff059ccea7 to your computer and use it in GitHub Desktop.
This file contains 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() | |
碰到点问题,python3好像不行
Adapt for Python3
import os
import re
from io import StringIO, BytesIO
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": "sarasa-regular.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 = int(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 = int(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 = BytesIO()
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 = BytesIO()
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):
cfg = makeConfig(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
参见 https://blog.oldj.net/2012/02/19/text-to-image/