遇到了一个很抽象的需求,需要把测试的截图放到文档里, 但是截图可能有快上万张,太逆天了,截图截一辈子. 好在截图的内容比较固定, 都是纯文字,并且背景也是固定的,因此可以直接根据文本内容生成截图,再把生成的图片流式放到文档里,这样就不用截图了,而且也不用担心截图的数量了。

因此我们通过以下几步来实现这个需求:

  • 生成截图的文本(这个与本次无关)
  • 生成图片
  • 把图片放到文档里

环境

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