Python: 生成带用户昵称的头像
需求
新建用户后,根据用户输入的昵称生成图片。(例:注册"钉钉"用户后,头像根据输入的名字生成)
开发环境
- Windows 10
- Python 3.8
- Pillow 8.1.2
实现
蛇皮皮蛋:Python创建文字图片(居中)/多图片合并(PIL),参考这篇文章,实现了新建一个图片,并把文字渲染到图片上。代码如下:
import cv2
import numpy as np
from PIL import ImageFont, ImageDraw, Image
def new_image(size, color, name):
img = Image.new('RGB', size, color) # 生成图片
img.show() # 展示图片
img.save(name) # 保存图片
def create_font_img(value, file_name, path):
img = cv2.imread(path) # 打开底片
font_path = "simsun/simsun.ttc" # 设置需要显示的字体
font = ImageFont.truetype(font_path, size=100) # 字体大小为 100
img_pil = Image.fromarray(img)
draw = ImageDraw.Draw(img_pil)
text_width, text_height = draw.textsize(value, font) # 获取字体宽高
# 绘制文字信息,img.shape 求图片的宽和高,字体绘制的开始位置(x, y),(255,255,255) 为白色字体
draw.text(((img.shape[1] - text_width) / 2, (img.shape[0] - text_height) / 2), value, fill=(255, 255, 255), font=font)
bk_img = np.array(img_pil)
# cv2.imshow("add_text", bk_img)
# cv2.waitKey()
cv2.imwrite(file_name + "_font.png", bk_img)
new_image((128, 128), (192, 202, 208), "new.jpg")
create_font_img("张", "name", "new.jpg")
代码执行后得到两张图片,如下:
底图
成果图1
根据上文提到的文章,简化后得到了以上代码,第一个函数 new_image() 新建一个图片,没有异议。
第二个函数 create_font_img() 将文字渲染到图片上。首先是 cv2.imread()
读取图片,之后设置文字的字体与大小,draw.text()
将文字画到图片上。注意到此函数的最后三行代码,cv2.imshow()
的意思应该非常明显,即直接显示图片,cv2.imwrite()
保存图片,那么 cv2.waitKey()
是什么意思呢?
这不是此文章重点,可以看看下面这篇文章:山上有强强:cv2.waitKey的入门级理解
注意 cv2 需要安装 opencv-python 使用,命令是 pip install opencv-python
。
事情到这里并没有结束,我对以上代码并不满意。主要问题如下:
- 需要新建一张图片为底片,再读取此图片,渲染文字。如果能不保存,直接使用此图片就更好了。
- 需要读字体文件,一个字体文件非常大,至少此处用到的 simsun.ttc 字体文件就有 10 MB 之大。如果能从系统读取,或者不使用字体文件会更好。
- 所用第三方库太多,达到三个。三个库看起来不多,但是如非必要勿增实体(包括上面的字体文件),特别是部分库版本不一,用在项目中,越少越好,以免出现什么问题。
基于这三个问题,必需简化代码。
进阶
经过搜索,发现了下面这篇文章:amigo1226:自动生成带昵称的头像(仿照钉钉头像)。此文章,实现了不用底片直接生成图片。不过这篇文章是用 Java 写的,为了验证是否成功,我特意用 IDEA 运行了代码,确实可以成功,得到的图与上文的成果图1一样。那么,Python 理论上也是可以的。
仔细阅读此文章中的代码,发现一句代码:BufferedImage bi = new BufferedImage(width, height, BufferedImage.TYPE_INT_RGB);
, BufferedImage 这意思似乎是说图片存在缓存中?那么就看看 PIL 库有没有类似的方法把。果然,Image 类中还真有一个方法,是 frombuffer()
,不过在多次尝试后,此方法似乎并不行。仔细思考我们的代码,第一个函数中用了 img.save()
来保存图片。
那么我们不能立即保存图片,其次 create_font_img()
中使用了 cv2.imread()
读取图片,既然不用读取图片,这句代码也就可以删除了。此时 cv2 的用法只剩 cv2.imwrite()
保存图片了,既然打算不使用那么多库,PIL 也是有保存方法的(img.save()
),如此一来便可以将 cv2 库的用法从代码中删除。
def create_font_img(value, file_name, path):
# img = cv2.imread(path) # 打开底片
img = Image.new('RGB', (128, 128), (192, 202, 208)) # 生成图片
font_path = "simsun/simsun.ttc" # 设置需要显示的字体
font = ImageFont.truetype(font_path, size=100) # 字体大小为 100
img_pil = Image.fromarray(img)
draw = ImageDraw.Draw(img_pil)
text_width, text_height = draw.textsize(value, font) # 获取字体宽高
# 绘制文字信息,img.shape 求图片的宽和高,字体绘制的开始位置(x, y),(255,255,255) 为白色字体
draw.text(((img.shape[1] - text_width) / 2, (img.shape[0] - text_height) / 2), value, fill=(255, 255, 255), font=font)
bk_img = np.array(img_pil)
# cv2.imwrite(file_name + "_font.png", bk_img)
img.save(file_name + "_font.png") # 保存图片
初步简化代码如上,这样的代码并不能正确执行,问题很多,首先是,cv2.imread()
读取图片文件得到的是 <class 'numpy.ndarray'>
类型。这个类型下面 Image.fromarray()
创建图像,和 img.shape
要读取图片像素大小。而 Image.new()
创建的图片并不是此种类型,而是 <class 'PIL.Image.Image'>
。本来打算看看是否可以将 <class 'PIL.Image.Image'>
转为 <class 'numpy.ndarray'>
(其实就是 np.array()
这个方法),但转念一想,不打算用 numpy 库,也就没有转类型的必要,直接看能否只使用 PIL 库作图。
整理一下,那么整个作图流程如下:新建一张图,求出图的像素大小(其实大小在新建图时已给出),设置需要画上去的文字的字体与大小,求出文字的像素大小(设置字体时已给出),将文字画在图片上;保存图片。
那么由此得出的代码如下:
def create_font_img(name):
img = Image.new('RGB', (128, 128), (192, 202, 208)) # 生成一张像素 128*128 图片
font_path = "simsun/simsun.ttc" # 设置需要显示的字体
font = ImageFont.truetype(font_path, size=100) # 字体大小为 100
draw = ImageDraw.Draw(img) # 绘图
# 绘制文字信息,字体绘制的开始位置(x, y),(255,255,255) 为白色字体
draw.text(((128 - 100) / 2, (128 - 100) / 2), name, fill=(255, 255, 255), font=font)
# img.show() # 展示图片
img.save("name.png") # 保存图片
执行以上的代码,得到的图与成果图1一样。
那么图片的像素大小与文字的像素大小是否有相应的方法获取呢?既然 cv2.imread()
(img.shape()
) 和 ImageDraw.Draw()
(draw.textsize()
)可以读取图片和文字像素大小,理论上 Image.new()
和 ImageFont.truetype()
应该有对应的方法。通过查找该对象所拥有的函数,发现了两个方法 size 和 getsize,分别使用 img.size
和 font.getsize()
可以得到图片像素和文字像素。
修改后的代码如下:
def create_font_img(name):
img = Image.new('RGB', (128, 128), (192, 202, 208)) # 生成图片
img_width, img_height = img.size
font_path = "simsun/simsun.ttc" # 设置需要显示的字体
font = ImageFont.truetype(font_path, size=100) # 字体大小为 100
text_width, text_height = font.getsize(name) # 获取字体宽高
draw = ImageDraw.Draw(img)
# 绘制文字信息,img.shape 求图片的宽和高,字体绘制的开始位置(x, y),(255,255,255) 为白色字体
draw.text(((img_width - text_width) / 2, (img_height - text_height) / 2), name, fill=(255, 255, 255), font=font)
# img.show()
img.save("name.png") # 保存图片
用户昵称可能是中文也可能是英文,这样字体的大小为一个不变的值不太好,同时将需要更改的数值提取出来,优化代码后如下:
from PIL import Image, ImageFont, ImageDraw
def create_font_img(username, img_size, img_bg_color, file_name):
img = Image.new('RGB', img_size, img_bg_color) # 生成图片
img_width, img_height = img.size
font_path = "simsun/simsun.ttc" # 设置需要显示的字体
if '\u4e00' <= username <= '\u9fff':
font = ImageFont.truetype(font_path, size=100) # 中文字符字体大小为 100
elif 'a' <= username <= 'z' or 'A' <= username <= 'Z':
username = username.upper() # 将英文字母转为大写
font = ImageFont.truetype(font_path, size=60) # 英文字符字体大小为 60
else:
font = ImageFont.truetype(font_path, size=32) # 其他字符字体大小为 32
text_width, text_height = font.getsize(username) # 获取字体宽高
draw = ImageDraw.Draw(img)
# 绘制文字信息,字体绘制的开始位置(x, y),(255,255,255) 为白色字体
draw.text(((img_width - text_width) / 2, (img_height - text_height) / 2), username, fill=(255, 255, 255), font=font)
img.save(file_name + ".png") # 保存图片
create_font_img('张', (128, 128), (192, 202, 208), 'zhang')
到此,解决了需要中间图片和第三方库多的问题,只剩下一个字体文件的问题?能否不使用字体文件呢?或者使用系统中的文件呢?
经过尝试,如果使用默认的字体文件,无法获取字体的像素大小,因为设置默认字体的方法是 ImageFont.load_default().font
,并没有指定像素大小,将 draw.text()
中的 char_width_height 设置定值,但是 draw.text()
中的 font 出现了中文字符的编解码问题,英文倒是可以正确执行。不过没有设置字体大小,导致一张 128*128 像素的图片中,字符的大小非常小,几乎不可见,如图:
成果图2
经过搜索,在这个问答中 如何使用Python ImageDraw库更改字体大小 发现了一句话,如下:
每个PIL’s docs,ImageDraw的默认字体是位图字体,因此无法缩放。要进行缩放,需要选择真正的字体类型。我希望不难找到一个不错的truetype字体“看起来有点像”默认字体在您想要的字体大小!
意思应该非常明显了,如果想要缩放字体的大小,那么就必需指定字体文件了。在搜索中了解到,也可以从系统中,加载字体文件,windows 系统中有许多字体文件,目录为 C:\Windows\Fonts
,可以从这里获取字体文件。在项目中没有 simsun.ttc 时,需要先下载一个,或者先使用 Fonts 下的文件,Fonts 中的文件如下图:
Fonts 中的字体文件
将上文代码中 font_path = "simsun/simsun.ttc"
修改为 font_path = "C:\\Windows\\Fonts\\STXINGKA.TTF"
,字体为"华文行楷 常规",得到的图片如下:
成果图3
总结
本文介绍了如何生成一张图片,并加以文字(用户昵称),探索了一些 PIL 的用法。
生成的图片可能并不完美,比如色彩的搭配,图片的圆角设置,以及文字字符过多如何割舍等问题,留待读者自行探索。