最近有文字轉圖片的需求,可是不太想下載 APP,就使用 Python Pillow 實現了一個,效果以下:python
PIL 提供了 PIL.ImageDraw.ImageDraw.text
方法,能夠方便的把文字寫到圖片上,簡單示例以下:git
from PIL import Image, ImageDraw, ImageFont
# get an image
base = Image.open('Pillow/Tests/images/hopper.png').convert('RGBA')
# make a blank image for the text, initialized to transparent text color
txt = Image.new('RGBA', base.size, (255,255,255,0))
# get a font
fnt = ImageFont.truetype('Pillow/Tests/fonts/FreeMono.ttf', 40)
# get a drawing context
d = ImageDraw.Draw(txt)
# draw text, half opacity
d.text((10,10), "Hello", font=fnt, fill=(255,255,255,128))
# draw text, full opacity
d.text((10,60), "World", font=fnt, fill=(255,255,255,255))
out = Image.alpha_composite(base, txt)
out.show()
複製代碼
爲何要計算文字的寬高呢?把文字直接寫到背景圖不能夠麼?github
Pillow PIL.ImageDraw.ImageDraw.text
寫文字是按換行符\n
換行的,若是一句話特別長,文字就會超出背景圖的寬度,因此第一步咱們須要先把文本按固定的寬度計算出高度。服務器
像圖上寫的這樣,文字轉圖片分三步:app
這裏背景圖寬度是固定的,因此文字的寬能夠不用計算。PIL.ImageDraw.ImageDraw.text
是經過\n
來換行的,那咱們只須要在文字合適的位置加上\n
就能夠了。工具
第一個想到的是 textwrap
方法,textwrap 能夠實現經過調整換行符的位置來格式化文本。但 textwrap 還有一個問題就是它是根據字符長度來分隔的,但文本中的字符並非等寬的,經過textwrap
格式化後的文字寫到圖片上效果多是這樣的:性能
使用這種方式,若是咱們要調整字體大小,每一行的長度都還須要再從新調整。測試
爲了保證每一行寬度儘量的一致,這裏使用 PIL.ImageDraw.ImageDraw.textsize
獲取字符寬高,而後按約定寬度把長文本分隔成文本列表,而後把列表每行文字寫到圖片上。字體
def get_paragraph(text, note_width):
# 把每段文字按約定寬度分隔成幾行
txt = Image.new('RGBA', (100, 100), (255, 255, 255, 0))
# get a drawing context
draw = ImageDraw.Draw(txt)
paragraph, sum_width = '', 0
line_numbers, line_height = 1, 0
for char in text:
w, h = draw.textsize(char, font)
sum_width += w
if sum_width > note_width:
line_numbers += 1
sum_width = 0
paragraph += '\n'
paragraph += char
line_height = max(h, line_height)
if not paragraph.endswith('\n'):
paragraph += '\n'
return paragraph, line_height, line_numbers
def split_text(text):
# 將文本按規定寬度分組
max_line_height, total_lines = 0, 0
paragraphs = []
for t in text.split('\n'):
# 先按 \n 把文本分段
paragraph, line_height, line_numbers = get_paragraph(t)
max_line_height = max(line_height, max_line_height)
total_lines += line_numbers
paragraphs.append((paragraph, line_numbers))
line_height = max_line_height
total_height = total_lines * line_height
# 這裏返回分好的段,文本總高度以及行高
return paragraphs, total_height, line_height
複製代碼
這是按字符寬度分隔文本寫到圖片的效果:優化
因爲文本長度不固定,生成獲得的文本高度也不固定,背景圖咱們也須要動態生成
經過圖片咱們能夠看到,頭部和尾部是固定的,變化的是文字部分,那麼背景圖片的高度計算公式爲
背景圖片高度=頭部高度+尾部高度+文本高度
實現代碼以下:
NOTE_HEADER_IMG = path.normpath(path.join(
path.dirname(__file__), 'note_header_660.png'))
NOTE_BODY_IMG = path.normpath(path.join(
path.dirname(__file__), 'note_body_660.png'))
NOTE_FOOTER_IMG = path.normpath(path.join(
path.dirname(__file__), 'note_footer_660.png'))
NOTE_WIDTH = 660
NOTE_TEXT_WIDTH = 460
body_height = NOTE_BODY_HEIGHT = 206
header_height = NOTE_HEADER_HEIGHT = 89
footer_height = NOTE_FOOTER_HEIGHT = 145
font = ImageFont.truetype(NOTE_OTF, 24)
def get_images(note_height):
numbers = note_height // body_height + 1
images = [(NOTE_HEADER_IMG, header_height)]
images.extend([(NOTE_BODY_IMG, body_height)] * numbers)
images.append((NOTE_FOOTER_IMG, footer_height))
return images
def make_backgroud():
# 將圖片拼接到一塊兒
images = get_images()
total_height = sum([height for _, height in images])
# 最終拼接完成後的圖片
backgroud = Image.new('RGB', (body_width, total_height))
left, right = 0, 0
background_img = '/tmp/%s_backgroud.png' % total_height
# 判斷背景圖是否存在
if path.exists(background_img):
return background_img
for image_file, height in images:
image = Image.open(image_file)
# (0, left, self.body_width, right+height)
# 分別爲 左上角座標 0, left
# 右下角座標 self.body_width, right+height
backgroud.paste(image, (0, left, body_width, right+height))
left += height # 從上往下拼接,左上角的縱座標遞增
right += height # 左下角的縱座標也遞增
backgroud.save(background_img, quality=85)
return background_img
複製代碼
如今咱們獲得了背景圖以及分隔好的文本,就能夠直接將文本寫到圖片上了
def draw_text(paragraphs, height):
background_img = make_backgroud()
note_img = Image.open(background_img).convert("RGBA")
draw = ImageDraw.Draw(note_img)
# 文字開始位置座標,須要根據背景圖的大小作調整
x, y = 80, 100
for paragraph, line_numbers in paragraphs:
for line in paragraph.split('\n')[:-1]:
draw.text((x, y), line, fill=(110, 99, 87), font=font)
y += line_height
# draw.text((x, y), paragraph, fill=(110, 99, 87), font=font)
# y += self.line_height * line_numbers
note_img.save(filename, "png", quality=1, optimize=True)
return filename
複製代碼
完整版代碼請查看 [https://github.com/gusibi/momo/blob/master/momo/note.py][https://github.com/gusibi/momo/blob/master/momo/note.py]
執行後效果如圖:
爲了能方便使用,我把這個作成了公號的一個功能,而後遇到了一個嚴重問題,太慢了!
使用 line_profiler
分析能夠發現,大部分時間都消耗在了圖片保存這一步,
note_img.save(filename, "png", quality=1, optimize=True)
複製代碼
性能分析工具也會佔用時間,測試完成後須要關閉分析
解決這個問題可能的方法:
經過測試,發現把背景圖寬度從990減到660,字體大小從40px 調整到24px,生成的圖片大小體積縮小了接近1倍,生成速度也比原來快了2/5。
相同代碼,相同文本,使用 python3 只用了2.3s,而 Python2 用時倒是5.3 s,還歷來沒在其它功能上遇到過 Python2 和 Python3 有這麼大的差異。
具體差別能夠使用源碼測試一下
優化完圖片生成速度後,發如今長文本狀態下,公號仍是會超時報錯。通過檢查發現是圖片上傳到公衆平臺太慢了(服務器只有1M 帶寬,沒有辦法.)。
解決方法,把圖片上傳到騰訊雲(文件上傳使用的是內網帶寬,不受限制),返回圖片 url。