生成截图并写入文档
遇到了一个很抽象的需求,需要把测试的截图放到文档里, 但是截图可能有快上万张,太逆天了,截图截一辈子. 好在截图的内容比较固定, 都是纯文字,并且背景也是固定的,因此可以直接根据文本内容生成截图,再把生成的图片流式放到文档里,这样就不用截图了,而且也不用担心截图的数量了。
因此我们通过以下几步来实现这个需求:
- 生成截图的文本(这个与本次无关)
- 生成图片
- 把图片放到文档里
环境
python-docx=1.1.0
pillow=10.2.0
1. 生成图片
图片分成两部分: 文字和背景。先将思路,后再给出所有代码
1.1 生成背景
from PIL import Image, ImageDraw
# 因为本来背景颜色就是纯色的
# 所以zhijietobfgu
image = Image.new('RGB', (image_width, image_height), background_color)
1.2 生成文字
首先需要字体, 因为是对控制台的截图, VSCODE的默认字体是Consolas
, 因此我们也使用这个字体
font_path = r'Consolas.ttf' # 替换为实际的字体文件路径
# 这个 font_size 对应了中文的宽度
# 这里之所以设置成50这么大, 是为了之后保证图片的分辨率不会太小.
font_size = 50
font = ImageFont.truetype(font_path, font_size)
有了字体我们要设计图片的大小,之前真实的截图每次不超过7行,基本最多是6行或者7行, 因此
# 基础配置
text_color = (51, 51, 51)
# 背景颜色
background_color = (253, 246, 227)
# 设置图片大小
# 这里是根据一个中文, 因为我的场景是中英文混合,中文的宽度比英文大,并且中文的宽度是固定的
# 因此这里我以中文的宽度为基准,设置图片的宽度,其中第一个乘的40是预设一行中文最多40个字
# 加40是一个冗余,我们要模拟真实截图,因此起点不可能是0,而是有一定的间距, 并且是随机的,
# 末尾也是一样,因此这里加40
image_width = font.getbbox("中")[2] * 40 + 40
image_width = int(image_width)
# -40 属于超参了,这里是对应上面的加40,指的是文本的最大长度,更长就会要换行了
max_width = image_width-40
有了图片大小,我们就可以开始生成文本了,因为生成的文本长度是随机的,很有可能我们的文本长度过长,7行放不下,因此就要生成多个截图,第一步是处理字符串
# 1. 切分出每一行的字符串
# 这个是我的需求里的一个函数,这里只是简单的模拟一下
image_text.extend("{}".format(response.text).split("\n"))
# 两步切分,第一步是根据 \n 切分,第二步是根据 max_width 切分
def text_wrap(font, text, max_width, line_space=3):
lines = []
length=0
str_index=0
temp = ''
# 如果字符串长度小于最大宽度,直接返回
while str_index < len(text):
# 如果当前长度小于最大宽度,继续添加
while length < max_width and str_index < len(text):
temp += text[str_index]
length += font.getbbox(text[str_index])[2]
str_index += 1
# 如果当前长度大于最大宽度,回退一个字符,然后添加到数组里
if length >= max_width:
str_index -= 1
temp = temp[:-1]
lines.append(temp)
length = 0
temp = ''
# 这个是这一行的字符中最高的高度
max_height = font.getbbox(text)[3]
// 返回切分好的每一行的字符串和行高
return lines, max_height + line_space
# 相当于是简单的贪心算法
from PIL import Image, ImageDraw
draw = ImageDraw.Draw(image)
# 主要是处理字符串
def process_one_request(str_list, font, max_width, doc, image_width):
# print("开始处理一个请求")
lines_list = []
line_height_max = 0
for text in str_list:
# 处理字符串
lines, line_height = text_wrap(font, text, max_width)
lines_list.extend(lines)
line_height_max = max(line_height_max, line_height)
# 一直进行处理
print(lines_list)
print(len(lines_list))
while lines_list != None and len(lines_list) != 0:
num = random.randint(6, 7)
# 如果 len(lines_list) <= num 则全部处理
if len(lines_list) <= num:
# 生成图片, 并写入
doc = generate_and_write_pic_to_doc(lines_list, doc, font, line_height_max, image_width)
lines_list = []
else:
# 移出前 num 个元素
temp_list = lines_list[:num]
lines_list = lines_list[num:]
# 生成图片, 并写入
doc = generate_and_write_pic_to_doc(temp_list, doc, font, line_height_max, image_width)
return doc
def generate_and_write_pic_to_doc(lines, doc, font, line_height, image_width):
# 生成图片
image = trans_text_to_image(font, lines, line_height, image_width)
# 将图像添加到文档
doc = write_one_pic_to_doc(doc, image)
return doc
# 生成图片
def trans_text_to_image(font, lines, line_height, image_width, background_color=(253, 246, 227), text_color=(51, 51, 51)):
# 处理字符串
# lines, line_height = text_wrap(font, text, image_width-40)
# 设置图片高度
image_height = line_height * len(lines) + 40
# 设置随机开始位置
text_position = (random.randint(10,30), random.randint(10,30))
# PIL 生成图片
image = Image.new('RGB', (image_width, image_height), background_color)
draw = ImageDraw.Draw(image)
# 绘制文本
y_text = text_position[1]
for line in lines:
draw.text((text_position[0], y_text), line, fill=text_color, font=font)
y_text += line_height
return image
到此我们就把图片生成了
2. 把图片放到文档里
import io
from docx import Document
doc = Document()
# 只是单独封装了以下
def write_one_paragraph_to_doc(doc, text):
doc.add_paragraph(text)
return doc
doc = write_one_paragraph_to_doc(doc, "{}".format(old_query))
# 添加图片到文档里主要是下面这个函数
doc = write_one_pic_to_doc(doc, image)
# 写入图片到文档里
def write_one_pic_to_doc(doc, image):
# 创建一个流对象
image_stream = io.BytesIO()
image.save(image_stream, format='PNG')
image_stream.seek(0)
# 获取可打印区域的宽度
# 这里主要是为了保证图片的宽度不会超过可打印区域的宽度, 把图片调整到统一大小
# 这个还是对我挺重要的
page_width_inches = get_printable_area_width_inches(doc)
# 将图像添加到文档
doc.add_picture(image_stream, width=Inches(page_width_inches))
# 换行
# doc.add_paragraph()
# print("写入图片成功")
return doc
# 加入空行
doc.add_paragraph()
doc.save("test_query_{}.docx".format(query_index+1))
到此我们就把图片放到文档里了
下面是整个工具的完整文档
import random
from PIL import Image, ImageFont, ImageDraw
import io
from docx.shared import Inches, Pt
#
# 获得页面的可打印区域的宽度, 单位 inches
def get_printable_area_width_inches(doc):
section = doc.sections[0] # 获取第一个节(第一页)的属性
page_width = section.page_width.inches
left_margin = section.left_margin.inches
right_margin = section.right_margin.inches
# 计算可打印区域的宽度(纸张宽度减去左右页边距)
printable_area_width = page_width - left_margin - right_margin
return printable_area_width
# 将一个文本转化成数组, 每个元素是一行, 并且返回最大高度
# 正常传入 font, 需要转换的文本即可
def text_wrap(font, text, max_width, line_space=3):
lines = []
length=0
str_index=0
temp = ''
while str_index < len(text):
while length < max_width and str_index < len(text):
temp += text[str_index]
length += font.getbbox(text[str_index])[2]
str_index += 1
if length >= max_width:
str_index -= 1
temp = temp[:-1]
lines.append(temp)
length = 0
temp = ''
max_height = font.getbbox(text)[3]
return lines, max_height + line_space
def trans_text_to_image(font, lines, line_height, image_width, background_color=(253, 246, 227), text_color=(51, 51, 51)):
# 处理字符串
# lines, line_height = text_wrap(font, text, image_width-40)
# 设置图片高度
image_height = line_height * len(lines) + 40
# 设置随机开始位置
text_position = (random.randint(10,30), random.randint(10,30))
# PIL 生成图片
image = Image.new('RGB', (image_width, image_height), background_color)
draw = ImageDraw.Draw(image)
# 绘制文本
y_text = text_position[1]
for line in lines:
draw.text((text_position[0], y_text), line, fill=text_color, font=font)
y_text += line_height
return image
def write_one_pic_to_doc(doc, image):
# 创建一个流对象
image_stream = io.BytesIO()
image.save(image_stream, format='PNG')
image_stream.seek(0)
# 获取可打印区域的宽度
page_width_inches = get_printable_area_width_inches(doc)
# 将图像添加到文档
doc.add_picture(image_stream, width=Inches(page_width_inches))
# 换行
# doc.add_paragraph()
# print("写入图片成功")
return doc
def generate_and_write_pic_to_doc(lines, doc, font, line_height, image_width):
# 生成图片
image = trans_text_to_image(font, lines, line_height, image_width)
# 将图像添加到文档
doc = write_one_pic_to_doc(doc, image)
return doc
def process_one_request(str_list, font, max_width, doc, image_width):
# print("开始处理一个请求")
lines_list = []
line_height_max = 0
for text in str_list:
# 处理字符串
lines, line_height = text_wrap(font, text, max_width)
lines_list.extend(lines)
line_height_max = max(line_height_max, line_height)
# 一直进行处理
print(lines_list)
print(len(lines_list))
while lines_list != None and len(lines_list) != 0:
num = random.randint(6, 7)
# 如果 len(lines_list) <= num 则全部处理
if len(lines_list) <= num:
# 生成图片, 并写入
doc = generate_and_write_pic_to_doc(lines_list, doc, font, line_height_max, image_width)
lines_list = []
else:
# 移出前 num 个元素
temp_list = lines_list[:num]
lines_list = lines_list[num:]
# 生成图片, 并写入
doc = generate_and_write_pic_to_doc(temp_list, doc, font, line_height_max, image_width)
return doc
# 写入一段话
def write_one_paragraph_to_doc(doc, text):
doc.add_paragraph(text)
return doc
测试代码
font_path = r'Consolas.ttf' # 替换为实际的字体文件路径
# 这个 font_size 对应了中文的宽度
font_size = 50
font = ImageFont.truetype(font_path, font_size)
# 基础配置
text_color = (51, 51, 51)
# 背景颜色
background_color = (253, 246, 227)
# 设置图片大小
image_width = font.getbbox("中")[2] * 40 + 40
image_width = int(image_width)
# -40 属于超参了
max_width = image_width-40
doc = Document()
doc = write_one_paragraph_to_doc(doc, "第1次测试")
doc = process_one_request(image_text, font, max_width, doc, image_width)
doc.save("query.docx")