利用Python如何生成便签图片详解


Posted in Python onJuly 09, 2018

前言

最近有文字转图片的需求,但是不太想下载 APP,就使用 Python Pillow 实现了一个,效果如下:

利用Python如何生成便签图片详解

PIL 提供了 PIL.ImageDraw.ImageDraw.text 方法,可以方便的把文字写到图片上,简单示例如下:

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()

为什么要计算文字的宽高呢?把文字直接写到背景图不可以么?

Pillow PIL.ImageDraw.ImageDraw.text 写文字是按换行符 \n 换行的,如果个字符串特别长,文字部分就会超出背景图的宽度,所以第一步我们需要先把文本按固定的宽度计算出高度。

像图上写的这样,文字转图片分三步:

  • 计算文字宽高
  • 生成响应尺寸背景图
  • 把文字写到图片上

计算文字宽高

这里背景图宽度是固定的,所以文字的宽可以不用计算。 PIL.ImageDraw.ImageDraw.text 是通过 \n 来换行的,那我们只需要在文字合适的位置加上 \n 就可以了。

第一个想到的是 textwrap 方法,textwrap 可以实现通过调整换行符的位置来格式化文本。但 textwrap 还有一个问题就是它是根据字符长度来分隔的,但文本中的字符并不是等宽的,通过 textwrap 格式化后的文字写到图片上效果可能是这样的:

利用Python如何生成便签图片详解

使用这种方式,如果我们要调整字体大小,每一行的长度都还需要再重新调整。

为了保证每一行宽度尽可能的一致,这里使用 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

这是按字符宽度分隔文本写到图片的效果:

利用Python如何生成便签图片详解

由于文本长度不固定,生成得到的文本高度也不固定,背景图我们也需要动态生成

根据文本高度生成背景图

利用Python如何生成便签图片详解

通过图片我们可以看到,头部和尾部是固定的,变化的是文字部分,那么背景图片的高度计算公式为

背景图片高度=头部高度+尾部高度+文本高度

实现代码如下:

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 ]

执行后效果如图:

利用Python如何生成便签图片详解

遇到的问题

为了能方便使用,我把这个做成了公号的一个功能,然后遇到了一个严重问题, 太慢了!

使用 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。

利用Python如何生成便签图片详解

总结

以上就是这篇文章的全部内容了,希望本文的内容对大家的学习或者工作具有一定的参考学习价值,如果有疑问大家可以留言交流,谢谢大家对三水点靠木的支持。

Python 相关文章推荐
Python实现向QQ群成员自动发邮件的方法
Nov 19 Python
详细介绍Python函数中的默认参数
Mar 30 Python
python并发和异步编程实例
Nov 15 Python
Python发送邮件测试报告操作实例详解
Dec 08 Python
对Python+opencv将图片生成视频的实例详解
Jan 08 Python
python:按行读入,排序然后输出的方法
Jul 20 Python
Django实现发送邮件找回密码功能
Aug 12 Python
Django 设置多环境配置文件载入问题
Feb 25 Python
简单了解Python变量作用域正确使用方法
Jun 12 Python
Pytorch 高效使用GPU的操作
Jun 27 Python
python中random.randint和random.randrange的区别详解
Sep 20 Python
scrapy redis配置文件setting参数详解
Nov 18 Python
Caffe均值文件mean.binaryproto转mean.npy的方法
Jul 09 #Python
使用Python更换外网IP的方法
Jul 09 #Python
使用Python AIML搭建聊天机器人的方法示例
Jul 09 #Python
Python迭代器与生成器用法实例分析
Jul 09 #Python
numpy的文件存储.npy .npz 文件详解
Jul 09 #Python
Python实现聊天机器人的示例代码
Jul 09 #Python
Atom的python插件和常用插件说明
Jul 08 #Python
You might like
PHP使用内置dir类实现目录遍历删除
2015/03/31 PHP
YII使用url组件美化管理的方法
2015/12/28 PHP
PHP使用PDO操作数据库的乱码问题解决方法
2016/04/08 PHP
PHP开发制作一个简单的活动日程表Calendar
2016/06/20 PHP
在JavaScript中,为什么要尽可能使用局部变量?
2009/04/06 Javascript
JavaScript中SQL语句的应用实现
2010/05/04 Javascript
基于jQuery的history历史记录插件
2010/12/11 Javascript
JQuery 返回布尔值Is()条件判断方法代码
2012/05/14 Javascript
cument.execCommand()用法深入理解
2012/12/04 Javascript
鼠标滚轮控制网页横向移动实现思路
2013/03/22 Javascript
JQuery的ready函数与JS的onload的区别详解
2013/11/21 Javascript
JavaScript实现在标题栏上显示当前日期的方法
2015/03/19 Javascript
解读ES6中class关键字
2017/11/20 Javascript
webpack打包node.js后端项目的方法
2018/03/10 Javascript
vue路由事件beforeRouteLeave及组件内定时器的清除方法
2018/09/29 Javascript
《javascript设计模式》学习笔记五:Javascript面向对象程序设计工厂模式实例分析
2020/04/08 Javascript
[01:17:47]TNC vs VGJ.S 2018国际邀请赛小组赛BO2 第一场 8.18
2018/08/19 DOTA
[02:16]完美世界DOTA2联赛PWL S3 集锦第三期
2020/12/21 DOTA
Python闭包实现计数器的方法
2015/05/05 Python
Python自动化部署工具Fabric的简单上手指南
2016/04/19 Python
浅谈Python处理PDF的方法
2017/11/10 Python
python selenium执行所有测试用例并生成报告的方法
2019/02/13 Python
Scrapy-Redis结合POST请求获取数据的方法示例
2019/05/07 Python
Python-Seaborn热图绘制的实现方法
2019/07/15 Python
使用python的turtle函数绘制一个滑稽表情
2020/02/28 Python
Python实现封装打包自己写的代码,被python import
2020/07/12 Python
使用CSS3实现圆角,阴影,透明
2014/12/23 HTML / CSS
美国花园雕像和家居装饰网上商店:Design Toscano
2019/03/09 全球购物
.NET面试题:什么是值类型和引用类型
2016/01/12 面试题
大二学生职业生涯规划书
2014/02/05 职场文书
法学求职信
2014/06/22 职场文书
大学生学习面向未来的赶考思想汇报
2014/09/12 职场文书
工作期间打牌检讨书范文
2014/11/20 职场文书
2014年乡镇团委工作总结
2014/12/18 职场文书
2015年妇委会工作总结
2015/05/22 职场文书
如何写好活动总结
2019/06/21 职场文书