ReportLab 导出 PDF(文档创建)

ReportLab 导出 PDF(文档创建)
ReportLab 导出 PDF(页面布局)
ReportLab 导出 PDF(图文表格)

文章目录

  • [1. ReportLab 基础](#1. ReportLab 基础)
    • [1.1. 安装 ReportLab](#1.1. 安装 ReportLab)
    • [1. 2. 创建 PDF 文件](#1. 2. 创建 PDF 文件)
  • [2. 创建文档](#2. 创建文档)
    • [2.1. DocTemplate(文档模板 )](#2.1. DocTemplate(文档模板 ))
    • [2.2. PageTemplate(页面模板 )](#2.2. PageTemplate(页面模板 ))
    • [2.3. BaseDocTemplate(文档模板基类 目录)](#2.3. BaseDocTemplate(文档模板基类 目录))
    • [2.4. SimpleDocTemplate(封面 页眉页脚)](#2.4. SimpleDocTemplate(封面 页眉页脚))
    • [2.5. SimpleDocTemplate(继承 Canvas 页码)](#2.5. SimpleDocTemplate(继承 Canvas 页码))
    • [2.6. 直接使用 Canvas(复杂场景)](#2.6. 直接使用 Canvas(复杂场景))
      • [2.6.1. Canvas 换行与分页](#2.6.1. Canvas 换行与分页)
  • [3. 实战(联合 pydantic)](#3. 实战(联合 pydantic))

官网:https://docs.reportlab.com/
英文手册:https://docs.reportlab.com/reportlab/userguide/ch1_intro/
中文手册:https://gitcode.com/Open-source-documentation-tutorial/d25f8/blob/main/reportlab中文手册.pdf

参考:

https://www.cnblogs.com/windfic/p/17157841.html

https://dev59.com/uF7Va4cB1Zd3GeqPKod0

https://www.jb51.net/article/270782.htm

https://www.osgeo.cn/python-tutorial/pdf-reportlab.html

https://blog.51cto.com/u_14940497/12374520

https://blog.csdn.net/qq_40596572/article/details/102896520

https://www.cnblogs.com/jilingxf/p/15857940.html

1. ReportLab 基础

1.1. 安装 ReportLab

可以通过pip安装:

bash 复制代码
pip install reportlab

1. 2. 创建 PDF 文件

下面是一个简单的示例,展示如何使用 ReportLab 创建一个包含文本和图像的 PDF 文件:

python 复制代码
from reportlab.pdfgen import canvas
from reportlab.lib.pagesizes import letter
from reportlab.pdfbase.ttfonts import TTFont
from reportlab.pdfbase import pdfmetrics
 
# 注册字体
song = "simsun"
pdfmetrics.registerFont(TTFont(song, "simsun.ttc"))
# pdfmetrics.registerFont(TTFont('MyFont', 'path/to/your/font.ttf'))
 
# 创建一个PDF文件
c = canvas.Canvas("example.pdf", pagesize=letter)
c.setFont('simsun', 12)
 
# 添加文本
# 默认情况下,(0,0)原点在页面的左下角。 此外,第一个坐标x往右走,第二个坐标y往上走,这是默认的。
c.drawString(10, 10, "Hello World!")
 
# # 添加图像
c.drawImage('./image.jpg', 10, 60, width=500, height=500)

# 保存PDF文件
c.save()

2. 创建文档

2.1. DocTemplate(文档模板 )

Reportlab 的基础使用方式是创建内容块(Flowable),再使用文档模板(DocTemplate)创建 Pdf 文档。

关注点:

Paragraph(段落)、

Image(图像)、

Table(表格)、

VerticalBarChart(柱形图表)

python 复制代码
from reportlab.pdfbase import pdfmetrics
from reportlab.pdfbase.ttfonts import TTFont

from reportlab.lib.styles import getSampleStyleSheet
from reportlab.platypus import Paragraph, SimpleDocTemplate, Image, Table
from reportlab.platypus import Spacer
from reportlab.graphics.shapes import Drawing
from reportlab.graphics.charts.barcharts import VerticalBarChart
from reportlab.graphics.charts.legends import Legend
from reportlab.lib import  colors
from reportlab.lib.pagesizes import A4
from reportlab.lib.units import cm

def draw_text(st, text: str):
    return Paragraph(text, st)
 
def draw_img(path):
    img = Image(path)       # 读取指定路径下的图片
    img.drawWidth = 6*cm    # 设置图片的宽度
    img.drawHeight = 5*cm   # 设置图片的高度
    return img

def draw_table(*args):
    col_width = 120
    style = [
        ('FONTNAME', (0, 0), (-1, -1), 'song'),  		# 字体
        ('FONTSIZE', (0, 0), (-1, 0), 12),  			# 第一行的字体大小
        ('FONTSIZE', (0, 1), (-1, -1), 10),  			# 第二行到最后一行的字体大小
        ('BACKGROUND', (0, 0), (-1, 0), '#d5dae6'),  	# 设置第一行背景颜色
        ('ALIGN', (0, 0), (-1, -1), 'CENTER'),  		# 第一行水平居中
        ('ALIGN', (0, 1), (-1, -1), 'LEFT'),  			# 第二行到最后一行左右左对齐
        ('VALIGN', (0, 0), (-1, -1), 'MIDDLE'),  		# 所有表格上下居中对齐
        ('TEXTCOLOR', (0, 0), (-1, -1), colors.darkslategray),  # 设置表格内文字颜色
        ('GRID', (0, 0), (-1, -1), 0.5, colors.grey),  	# 设置表格框线为grey色,线宽为0.5
        ('SPAN', (0, 1), (2, 1)),  						# 合并第二行一二三列
    ]
    table = Table(args, colWidths=col_width, style=style)
    return table
 
def draw_bar(bar_data: list, ax: list, items: list):
    drawing = Drawing(500, 200)
    bc = VerticalBarChart()
    bc.x = 45       	# 整个图表的x坐标
    bc.y = 45      		# 整个图表的y坐标
    bc.height = 150     # 图表的高度
    bc.width = 350      # 图表的宽度
    bc.data = bar_data
    bc.strokeColor = colors.black      # 顶部和右边轴线的颜色
    bc.valueAxis.valueMin = 0          # 设置y坐标的最小值
    bc.valueAxis.valueMax = 20         # 设置y坐标的最大值
    bc.valueAxis.valueStep = 5         # 设置y坐标的步长
    bc.categoryAxis.labels.dx = 2
    bc.categoryAxis.labels.dy = -8
    bc.categoryAxis.labels.angle = 20
    bc.categoryAxis.labels.fontName = 'song'
    bc.categoryAxis.categoryNames = ax
    
    # 图示
    leg = Legend()
    leg.fontName = 'song'
    leg.alignment = 'right'
    leg.boxAnchor = 'ne'
    leg.x = 475         # 图例的x坐标
    leg.y = 140
    leg.dxTextSpace = 10
    leg.columnMaximum = 3
    leg.colorNamePairs = items
    drawing.add(leg)
    drawing.add(bc)
    return drawing

def main(filename):
    pdfmetrics.registerFont(TTFont('song', 'STSONG.ttf'))
    
    style = getSampleStyleSheet()
    
    ts = style['Heading1']
    ts.fontName = 'song'    # 字体名
    ts.fontSize = 18        # 字体大小
    ts.leading = 30         # 行间距
    ts.alignment = 1        # 居中
    ts.bold = True
    
    hs = style['Heading2']
    hs.fontName = 'song'    # 字体名
    hs.fontSize = 15        # 字体大小
    hs.leading = 20         # 行间距
    hs.textColor = colors.red  # 字体颜色
    
    ns = style['Normal']
    ns.fontName = 'song'
    ns.fontSize = 12
    ns.wordWrap = 'CJK'     # 设置自动换行
    ns.alignment = 0        # 左对齐
    ns.firstLineIndent = 32 # 第一行开头空格
    ns.leading = 20
    
    content = []
    content.append(draw_text(ts, '经典游戏盘点'))
    content.append(draw_img('./image.jpg'))
    content.append(Spacer(1, 1*cm))
    content.append(draw_text(ns, ' 《超级马里奥兄弟》于1985年9月13日发售,这是一款任天堂针对FC主机全力度身订造的游戏,被称为TV游戏奠基之作。这个游戏被赞誉为电子游戏的原始范本,确立了角色、游戏目的、流程分布、操作性、隐藏要素、BOSS、杂兵等以后通用至今的制作概念。《超级马里奥兄弟》成为游戏史首部真正意义上的超大作游戏,游戏日本本土销量总计681万份,海外累计更是达到了3342万份的天文数字。'))
    content.append(draw_text(hs, '经典游戏列表'))
    
    # 添加表格
    data = [
        ('经典游戏', '发布年代', '发行商'),
        ('TOP100',),
        ('超级马里奥兄弟', '1985年', '任天堂'),
        ('坦克大战', '1985年', '南梦宫'),
        ('魂斗罗', '1987年', '科乐美'),
        ('松鼠大战', '1990年', '卡普空'),
    ]
    content.append(draw_table(*data))
 
    # 生成图表
    content.append(draw_text(hs, '游戏厂商统计'))
    b_data = [(2, 4, 6, 12, 8, 16), (12, 14, 17, 9, 12, 7)]
    ax_data = ['任天堂', '南梦宫', '科乐美', '卡普空', '世嘉', 'SNK']
    leg_items = [(colors.red, '街机'), (colors.green, '家用机')]
    content.append(draw_bar(b_data, ax_data, leg_items))
    
    # 生成pdf文件
    doc = SimpleDocTemplate(filename, pagesize=A4, topMargin=35)
    doc.build(content)

if __name__ == '__main__':
    main(filename='example1.pdf')

2.2. PageTemplate(页面模板 )

上述的排版都是线性的,如果要有一些混排,比如列式排版,可以使用BalancedColumns ,有一些页面排版比较复杂,那可以使用页面模板(PageTemplate )。

其实还可以用传统Web艺能------Table来做排版,我试了一下,只需要指定BOX,GRID为白色即可,线宽为0不行。

关注点:

PageTemplate(页面模板)

Frame(框架)

python 复制代码
from reportlab.lib.colors import Color
from reportlab.lib.pagesizes import A4
from reportlab.lib.styles import getSampleStyleSheet
from reportlab.lib.units import cm
from reportlab.pdfbase import pdfmetrics
from reportlab.pdfbase.ttfonts import TTFont
from reportlab.pdfgen import canvas
from reportlab.lib import  colors
from reportlab.platypus import BaseDocTemplate, Frame, Paragraph, NextPageTemplate, PageBreak, PageTemplate, Image
from reportlab.lib.units import inch

def draw_text(st, text: str):
    return Paragraph(text, st)
 
def draw_img(path):
    img = Image(path)       # 读取指定路径下的图片
    img.drawWidth = 5*cm    # 设置图片的宽度
    img.drawHeight = 4*cm   # 设置图片的高度
    return img

def main(filename):
    # pdfmetrics.registerFont(TTFont('微软雅黑', 'msyh.ttf'))
    pdfmetrics.registerFont(TTFont('simsun', "simsun.ttc"))
    
    style = getSampleStyleSheet()
    
    ts = style['Heading1']
    ts.fontName = 'simsun'      # 字体名
    ts.fontSize = 18            # 字体大小
    ts.leading = 30             # 行间距
    ts.alignment = 1            # 居中
    ts.bold = True
    
    hs = style['Heading2']
    hs.fontName = 'simsun'      # 字体名
    hs.fontSize = 15            # 字体大小
    hs.leading = 20             # 行间距
    hs.textColor = colors.red   # 字体颜色
    
    ns = style['Normal']
    ns.fontName = 'simsun'
    ns.fontSize = 12
    ns.wordWrap = 'CJK'     # 设置自动换行
    ns.alignment = 0        # 左对齐
    ns.firstLineIndent = 32 # 第一行开头空格
    ns.leading = 20
    
    doc = BaseDocTemplate(filename, showBoundary=0, pagesize=A4)


    frameT = Frame(doc.leftMargin, doc.bottomMargin, doc.width, doc.height, id='normal')
    
    # 一分为三
    w = doc.width / 3
    # 高度等于宽度
    h = w
    # 上面一行 底部
    bm = doc.height - h
    # 上面一行 左列
    frame1 = Frame(doc.leftMargin,      bm, w,           h, id='col1')
    # 上面一行 右列
    frame2 = Frame(doc.leftMargin + w,  bm, doc.width-w, h, id='col2')
    # 下面一行
    frame3 = Frame(doc.leftMargin, doc.bottomMargin, doc.width , bm-doc.topMargin, id='col3')
    
    doc.addPageTemplates([
        PageTemplate(id='TwoCol', frames=[frame1, frame2, frame3]),
        PageTemplate(id='OneCol', frames=frameT),
    ])
    
    
    elements = []
    #### 适配 PageTemplate TwoCol
    # 上面一行 左列
    elements.append(draw_img("./image.jpg"))
    # 上面一行 右列
    elements.append(draw_text(ns, '11111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111 。'))
    elements.append(draw_text(ns, '22222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222 。'))
    elements.append(draw_text(ns, '33333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333 。'))

    #### 适配 PageTemplate OneCol
    elements.append(NextPageTemplate('OneCol'))
    # 强制换页
    elements.append(PageBreak())
    elements.append(draw_text(ns, "Frame one column, 44444444444444444444444444444444444444444444444444444444444444444444444444444444444444444444444444444444444444444444444444444444444444444444444444444444444444444444444444444444444444444444444444444444444444444444444444444444444444444444444444444444444444444444444444444444444444444444444444444444444444444 。"))

    #### 适配 PageTemplate TwoCol
    elements.append(NextPageTemplate('TwoCol'))
    elements.append(PageBreak())
    elements.append(draw_img("./image.jpg"))
    elements.append(draw_text(ns, '55555555555555555555555555555555555555555555555555555555555555555555555555555555555555555555555555555555555555555555555555555555555555555555555555555555555555555555555555555555555555555555555555555555555555555555555555555555555555555555555555555555555555555555555555555555555555555555555555555555555555555555555555555555555 。'))
    elements.append(draw_img("./image.jpg"))
    elements.append(draw_text(ns, '55555555555555555555555555555555555555555555555555555555555555555555555555555555555555555555555555555555555555555555555555555555555555555555555555555555555555555555555555555555555555555555555555555555555555555555555555555555555555555555555555555555555555555555555555555555555555555555555555555555555555555555555555555555555 。'))
    
    doc.build(elements)

if __name__ == '__main__':
    main(filename='example2.pdf')

2.3. BaseDocTemplate(文档模板基类 目录)

前两种方式都不能精确输出,依赖于模板的排版,精确输出需要Canvas接口。

如果你要在每一页上显示页眉和页脚,那么你可以继承文档模板(BaseDocTemplate)。

如果你要添加目录索引,这就是最方便的方式。

覆盖接口:

handle_documentBegin

handle_pageBegin

handle_pageEnd

handle_frameBegin

handle_frameEnd

handle_flowable

handle_nextPageTemplate

handle_currentFrame

handle_nextFrame

或者实现回调函数:

afterInit

beforeDocument

beforePage

afterPage

filterFlowables

afterFlowable

关注点:

BaseDocTemplate(文档模板)

bookmarkPage(书签)

addOutlineEntry(大纲)

python 复制代码
from reportlab.lib.styles import ParagraphStyle
from reportlab.platypus import PageBreak
from reportlab.platypus.paragraph import Paragraph
from reportlab.platypus.doctemplate import PageTemplate, BaseDocTemplate
from reportlab.pdfbase import pdfmetrics
from reportlab.pdfbase.ttfonts import TTFont
from reportlab.platypus.frames import Frame
from reportlab.lib.units import cm

class MyDocTemplate(BaseDocTemplate):
    
    def __init__(self, filename, **kw):
        self.allowSplitting = 0
        BaseDocTemplate.__init__(self, filename, **kw)
        template = PageTemplate('normal', [Frame(2.5*cm, 2.5*cm, 15*cm, 25*cm, id='F1')])
        self.addPageTemplates(template)
        self.chapter = 0
        self.section = 0

    def afterFlowable(self, flowable):
    	# 针对段落 添加标签和索引
        if isinstance(flowable, Paragraph):
            text = flowable.getPlainText()
            style = flowable.style.name
            if style == 'Title':
                self.chapter += 1
                # # 书签
                # self.canv.bookmarkPage(f"chapter{self.chapter}")
                # # 大纲
                # self.canv.addOutlineEntry(f"Chapter {self.chapter}", f"chapter{self.chapter}", level=0)

                # 书签 参数为索引(个人理解)
                self.canv.bookmarkPage(f"chapter{self.chapter}")
                # self.canv.bookmarkPage(key=)
                # 目录 参数为 标题、索引、层级
                self.canv.addOutlineEntry(text, f"chapter{self.chapter}", level=0)
                # self.canv.addOutlineEntry(title=, key=, level=)
            elif style == 'Heading1':
                self.section += 1
                self.canv.bookmarkPage(f"section{self.section}")
                self.canv.addOutlineEntry(f"Section {self.section}", f"section{self.section}", level=1)

def main(filename):
    # pdfmetrics.registerFont(TTFont('微软雅黑', 'msyh.ttf'))
    pdfmetrics.registerFont(TTFont('simsun', "simsun.ttc"))
    
    ts = ParagraphStyle(name = 'Title',
        fontName = 'simsun',
        fontSize = 22,
        leading = 16,
        alignment = 1,
        spaceAfter = 20)

    h1 = ParagraphStyle(
        name = 'Heading1',
        fontSize = 14,
        leading = 16)

    story = []
    
    story.append(Paragraph('继承BaseDocTemplate', ts))
    story.append(Paragraph('Section 1', h1))
    story.append(Paragraph('Text in Section 1.1'))
    # 分页
    story.append(PageBreak())
    story.append(Paragraph('Section 2', h1))
    story.append(Paragraph('Text in Section 1.2'))
    # 分页
    story.append(PageBreak())
    story.append(Paragraph('Chapter 2', ts))
    story.append(Paragraph('Section 1', h1))
    story.append(Paragraph('Text in Section 2.1'))

    doc = MyDocTemplate(filename)
    doc.build(story)

if __name__ == '__main__':
    main(filename='example3.pdf')

2.4. SimpleDocTemplate(封面 页眉页脚)

SimpleDocTemplate就是继承BaseDocTemplate的一种简单实现,它覆盖了接口handle_pageBegin,重载了build接口。

它把页面分成两种:首页和后续页,对应回调两个过程onFirstPage=, onLaterPages=,只需要实现这两个回调过程即可。

适用显示页眉和页脚,其它的功能就有限了。

关注点:

SimpleDocTemplate(文档模板)

QrCode(二维码)

drawOn(显示Flowable)

python 复制代码
from reportlab.platypus import SimpleDocTemplate, Paragraph
from reportlab.platypus import PageBreak
from reportlab.lib.styles import ParagraphStyle
from reportlab.lib.colors import Color
from reportlab.lib.pagesizes import A4
from reportlab.lib.units import mm
from reportlab.pdfbase import pdfmetrics
from reportlab.pdfbase.ttfonts import TTFont
from reportlab.graphics.barcode import qr

#首页
def myFirstPage(canvas, doc):
    canvas.saveState()
    canvas.setFillColorRGB(0, 0, 0)
    canvas.setFont('simsun',12)
    str="(内部资料)"
    canvas.drawCentredString(doc.width/2, 25*mm, str)
    myLaterPages(canvas, doc)
    canvas.restoreState()
    
#页眉页脚
def myLaterPages(canvas, doc):
    canvas.saveState()
    canvas.setStrokeColorRGB(0.8, 0.8, 0.8)
    canvas.line(0, 32, doc.width, 32)
    canvas.line(0, A4[1]-45, doc.width, A4[1]-45)
    canvas.setFillColorRGB(0, 0, 0)
    canvas.setFont('simsun',10)
    str=f"Page {doc.page}"
    canvas.drawCentredString(doc.width/2, 5*mm, str)
    canvas.setFillColorRGB(1, 0, 0)
    canvas.drawCentredString(doc.width/2, A4[1]-9*mm, "XX有限公司版权所有")
    qr_code = qr.QrCode('hello', width=45, height=45)
    canvas.setFillColorRGB(0, 0, 0)
    qr_code.drawOn(canvas, 0, A4[1]-45)
    canvas.restoreState()


def main(filename):
    # pdfmetrics.registerFont(TTFont('微软雅黑', 'msyh.ttf'))
    pdfmetrics.registerFont(TTFont('simsun', "simsun.ttc"))
    
    doc = SimpleDocTemplate(filename, pagesize=A4, leftMargin=10, rightMargin=10)
    
    title = ParagraphStyle(name = 'Title',
        fontName = 'simsun',
        fontSize = 22,
        leading = 16,
        alignment = 1,
        spaceAfter = 20)

    contents = []
    contents.append(Paragraph('封面', title))
    contents.append(Paragraph('使用SimpleDocTemplate', title))
    contents.append(PageBreak())
    contents.append(Paragraph('Hello'))
    contents.append(PageBreak())
    contents.append(Paragraph('World'))
    
    doc.build(contents, onFirstPage=myFirstPage, onLaterPages=myLaterPages)

if __name__ == '__main__':
    main(filename='example4.pdf')

2.5. SimpleDocTemplate(继承 Canvas 页码)

控制Canvas的另一种方法是继承Canvas。

与继承文档模板(DocTemplate)类似,不过网上能找到的例子也就是显示页码,不是很实用。

python 复制代码
from reportlab.platypus import SimpleDocTemplate, Image, Paragraph, PageBreak
from reportlab.pdfgen import canvas
from reportlab.lib.units import mm
from reportlab.lib.colors import Color
from reportlab.lib.pagesizes import A4
from reportlab.pdfbase import pdfmetrics
from reportlab.pdfbase.ttfonts import TTFont
from reportlab.lib.styles import ParagraphStyle

class NumberedCanvas(canvas.Canvas):
    def __init__(self, *args, **kwargs):
        canvas.Canvas.__init__(self, *args, **kwargs)
        self._saved_page_states = []

    def showPage(self):
        self._saved_page_states.append(dict(self.__dict__))
        self._startPage()

    def save(self):
        """add page info to each page (page x of y)"""
        num_pages = len(self._saved_page_states)
        for state in self._saved_page_states:
            self.__dict__.update(state)
            self.draw_page_number(num_pages)
            canvas.Canvas.showPage(self)
        canvas.Canvas.save(self)

    def draw_page_number(self, page_count):
        self.setFont("Helvetica", 9)
        self.setStrokeColor(Color(0, 0, 0, alpha=0.5))
        self.line(10*mm, 15*mm, A4[0] - 10*mm, 15*mm)
        self.setFillColor(Color(0, 0, 0, alpha=0.5))
        self.drawCentredString(A4[0]/2, 10*mm, "Page %d of %d" % (self._pageNumber, page_count))
 
def main(filename):
    # pdfmetrics.registerFont(TTFont('微软雅黑', 'msyh.ttf'))
    pdfmetrics.registerFont(TTFont('simsun', "simsun.ttc"))
    
    title = ParagraphStyle(name = 'Title',
        fontName = 'simsun',
        fontSize = 22,
        leading = 16,
        alignment = 1,
        spaceAfter = 20)

    image = Image("image.jpg")
    image.drawWidth = 160
    # image.drawHeight = 160*(image.imageHeight/image.imageWidth)
    # image.drawHeight = 160*(image.imageWidth/image.imageHeight)
    image.drawHeight = 160
    elements = [
        Paragraph('继承Canvas', title),
        Paragraph("Hello"),
        image,
        PageBreak(),
        Paragraph("world"),
        PageBreak(),
        image,
    ]
    doc = SimpleDocTemplate(filename)
    doc.build(elements, canvasmaker=NumberedCanvas)
    
if __name__ == "__main__":
    main(filename='example5.pdf')

2.6. 直接使用 Canvas(复杂场景)

当PDF内容非常复杂,难以用以上的方法实现,可以直接使用Canvas创建PDF

直接使用Canvas类,可以精确输出,但需要自己排版,而且它的坐标原点在左下角

其中也可以放置Flowable,需要排版的Flowable,如Table等,调用warp函数即可自动排版。

如果是内容已经排版的格式转换程序,非常推荐使用这种方式。

python 复制代码
from reportlab.pdfgen import canvas
from reportlab.platypus import Image, Table
from reportlab.lib.pagesizes import A4
from reportlab.lib.units import mm
from reportlab.lib.colors import Color
from reportlab.pdfbase import pdfmetrics
from reportlab.pdfbase.ttfonts import TTFont
from reportlab.lib import  colors
from reportlab.graphics.shapes import Drawing
from reportlab.graphics.charts.barcharts import VerticalBarChart
from reportlab.graphics.charts.legends import Legend
from reportlab.lib.styles import getSampleStyleSheet
from reportlab.platypus import Paragraph
from reportlab.graphics.barcode import qr

def draw_bar(bar_data: list, ax: list, items: list):
    drawing = Drawing(500, 200)
    bc = VerticalBarChart()
    bc.x = 45           # 整个图表的x坐标
    bc.y = 45           # 整个图表的y坐标
    bc.height = 150     # 图表的高度
    bc.width = 350      # 图表的宽度
    bc.data = bar_data
    bc.strokeColor = colors.black       # 顶部和右边轴线的颜色
    bc.valueAxis.valueMin = 0           # 设置y坐标的最小值
    bc.valueAxis.valueMax = 20          # 设置y坐标的最大值
    bc.valueAxis.valueStep = 5          # 设置y坐标的步长
    bc.categoryAxis.labels.dx = 2
    bc.categoryAxis.labels.dy = -8
    bc.categoryAxis.labels.angle = 20
    bc.categoryAxis.labels.fontName = 'simsun'
    bc.categoryAxis.categoryNames = ax
    
    # 图示
    leg = Legend()
    leg.fontName = 'simsun'
    leg.alignment = 'right'
    leg.boxAnchor = 'ne'
    leg.x = 475         # 图例的x坐标
    leg.y = 140
    leg.dxTextSpace = 10
    leg.columnMaximum = 3
    leg.colorNamePairs = items
    drawing.add(leg)
    drawing.add(bc)
    return drawing

def draw_table(*args):
    # col_width = 120
    col_width = (A4[0]-100)/3
    style = [
        ('FONTNAME', (0, 0), (-1, -1), 'simsun'),       # 字体
        ('FONTSIZE', (0, 0), (-1, 0), 12),              # 第一行的字体大小
        ('FONTSIZE', (0, 1), (-1, -1), 10),             # 第二行到最后一行的字体大小
        ('BACKGROUND', (0, 0), (-1, 0), '#d5dae6'),     # 设置第一行背景颜色
        ('ALIGN', (0, 0), (-1, -1), 'CENTER'),          # 第一行水平居中
        ('ALIGN', (0, 1), (-1, -1), 'LEFT'),            # 第二行到最后一行左右左对齐
        ('VALIGN', (0, 0), (-1, -1), 'MIDDLE'),         # 所有表格上下居中对齐
        # ('TEXTCOLOR', (0, 0), (-1, -1), colors.darkslategray),  # 设置表格内文字颜色
        ('TEXTCOLOR', (0, 0), (-1, -1), Color(0, 255, 10, 0.7)),  # 设置表格内文字颜色
        ('GRID', (0, 0), (-1, -1), 0.5, colors.grey),   # 设置表格框线为grey色,线宽为0.5
        ('SPAN', (0, 1), (2, 1)),                       # 合并第二行一二三列
    ]
    table = Table(args, colWidths=col_width, style=style)
    return table
 
def draw_page_number(c, page, count):
    c.setFillColorRGB(1, 0, 0)      # 红色
    c.setFont("simsun", 9)
    c.drawCentredString(A4[0]/2, A4[1]-9*mm, "XX有限公司版权所有")
    # c.drawCentredString(A4[0]/2, A4[1]-1*mm, "XX有限公司版权所有")
    qr_code = qr.QrCode('https://www.cnblogs.com/windfic', width=45, height=45)
    # c.setFillColorRGB(0, 0, 0)
    c.setFillColorRGB(0, 1, 0)      # 绿色
    qr_code.drawOn(c, 0, A4[1]-45)
    # 线段 起点(x,y)和终点(x,y)
    # c.line(10*mm, A4[1]-45, A4[0], A4[1]-45)
    c.line(10*mm, A4[1]-45, A4[0] - 10*mm, A4[1]-45)    # 上横线
    
    c.setFont("simsun", 9)
    c.setStrokeColor(Color(0, 0, 0, alpha=0.5))         # 下横线 白色 透明度0.5
    c.line(10*mm, 15*mm, A4[0] - 10*mm, 15*mm)
    c.setFillColor(Color(0, 0, 0, alpha=0.5))           # 黑色 页码
    c.drawCentredString(A4[0]/2, 10*mm, "Page %d of %d" % (page, count))
 
def main(filename):
    # pdfmetrics.registerFont(TTFont('微软雅黑', 'msyh.ttf'))
    pdfmetrics.registerFont(TTFont('simsun', "simsun.ttc"))
    
    c = canvas.Canvas(filename)
    #### 标题页
    c.bookmarkPage("title")
    c.addOutlineEntry("my book", "title", level=0)

    #### 第一页
    c.setFont("simsun", 16)
    c.setFillColor(Color(0, 0, 1, alpha=0.9))
    c.drawString(320, A4[1] - 95, "超级马里奥兄弟")
    c.setFont("simsun", 12)
    c.setFillColor(Color(0, 0, 0, alpha=0.7))
    c.drawString(320, A4[1] - 125, "SUPER MARIO BROS.")
    c.drawString(320, A4[1] - 195, "1985年9月13日发售")
    
    img = Image("./image.jpg")
    img.drawWidth = 160
    # img.drawHeight = 160*(img.imageHeight/img.imageWidth)
    # img.drawHeight = int(160*(img.imageHeight/img.imageWidth))
    img.drawHeight = 160
    # 不能打印,图片被拉伸(为什么?)
    # print("image", img.imageHeight, img.imageWidth, (img.imageHeight/img.imageWidth), 160*(img.imageHeight/img.imageWidth), img.drawHeight)
    img.drawOn(c, 150, A4[1]-210)
    # img.drawOn(c, 160, A4[1]-200)
    
    data = [
        ('经典游戏', '发布年代', '发行商'),
        ('TOP100',),
        ('超级马里奥兄弟', '1985年', '任天堂'),
        ('坦克大战', '1985年', '南梦宫'),
        ('魂斗罗', '1987年', '科乐美'),
        ('松鼠大战', '1990年', '卡普空'),
    ]
    t = draw_table(*data)
    # t.wrap(800, 600)
    t.wrap(2000, 600)
    t.drawOn(c, 50, A4[1] - 400)
    
    styleSheet = getSampleStyleSheet()
    style = styleSheet['BodyText']
    style.fontName = "simsun"
    p=Paragraph(' 《超级马里奥兄弟》于1985年9月13日发售,这是一款任天堂针对FC主机全力度身订造的游戏,被称为TV游戏奠基之作。这个游戏被赞誉为电子游戏的原始范本,确立了角色、游戏目的、流程分布、操作性、隐藏要素、BOSS、杂兵等以后通用至今的制作概念。《超级马里奥兄弟》成为游戏史首部真正意义上的超大作游戏,游戏日本本土销量总计681万份,海外累计更是达到了3342万份的天文数字。',style)
    # 左右margen 各为50
    p.wrap(A4[0]-100, 100)
    p.drawOn(c, 50, A4[1] - 280)
    
    b_data = [(2, 4, 6, 12, 8, 16), (12, 14, 17, 9, 12, 7)]
    ax_data = ['任天堂', '南梦宫', '科乐美', '卡普空', '世嘉', 'SNK']
    leg_items = [(colors.red, '街机'), (colors.green, '家用机')]
    d = draw_bar(b_data, ax_data, leg_items)
    d.drawOn(c, 50, A4[1] - 620)
    
    draw_page_number(c, 1, 2)
    c.bookmarkPage("section1")
    c.addOutlineEntry("first section", "section1", level=1)
    c.showPage()    # 手动开始新的一页
    
    #### 第二页
    c.drawString(50, A4[1] - 70, ("This is Paragraph number. ") * 5)

    draw_page_number(c, 2, 2)
    c.bookmarkPage("section2")
    c.addOutlineEntry("second section", "section2", level=1)
    c.showPage()    # 手动开始新的一页
    
    # c.showOutline()
    c.save()
    
if __name__ == "__main__":
    main(filename='example6.pdf')

效果很好,问题也很严重。第二页不会自动分页!!!

2.6.1. Canvas 换行与分页

python 复制代码
from reportlab.pdfgen import canvas
from reportlab.lib.pagesizes import letter

def main(filename):
    c = canvas.Canvas(filename, pagesize=letter)
    text = "This is a very long paragraph that might need to be split across multiple pages.\n" * 50
    lines = text.split('\n')

    # y_position = 750  # 初始Y位置
    y_position = letter[1]-105
    for line in lines:
        if y_position < 100:     # 假设当Y位置小于100时需要分页
            c.showPage()
            # y_position = 750  # 重置Y位置到页面顶部
            y_position = letter[1]-105
        # print(y_position)
        
        c.drawString(100, y_position, line)
        y_position -= 15  # 每次绘制后减少Y位置以模拟换行
    c.save()

if __name__ == "__main__":
    main(filename='example.pdf')

挺不友好的。

3. 实战(联合 pydantic)

python 复制代码
from reportlab.platypus import SimpleDocTemplate, Spacer, Paragraph, Table, TableStyle, PageBreak
from reportlab.pdfbase import pdfmetrics, ttfonts
from reportlab.lib.styles import ParagraphStyle, getSampleStyleSheet
from reportlab.lib.enums import TA_JUSTIFY, TA_LEFT, TA_CENTER
from reportlab.lib.pagesizes import A4
from reportlab.lib import colors
# from reportlab.lib.colors import HexColor
from reportlab.lib.units import inch, cm, mm

from reportlab.graphics.charts.barcharts import VerticalBarChart    # 图表类
from reportlab.graphics.charts.legends import Legend                # 图例类
from reportlab.graphics.shapes import Drawing                       # 绘图工具
from reportlab.graphics.shapes import String
from reportlab.pdfgen import canvas


from pydantic import BaseModel, Field
from typing import Union, Literal

import uuid
import random

class TaskBase(BaseModel):
    task_name: str                  = Field(..., title="任务名称", proportion=2)
    description: str                = Field(default="", title="备注信息", proportion=2)
    times: int                      = Field(default=4, title="生成次数", proportion=1)
    task_type: Union[str, int]      = Field(default=0, title="任务类型", proportion=1)
    score_type: Union[str, int]     = Field(default=0, title="打分类型", proportion=1)
    question_count: int             = Field(default=0, title="总问题数", proportion=1)

class TaskInDB(TaskBase):
    task_id: str                    = Field(..., title="任务id", proportion=2)
    user_id: str                    = Field(..., title="创建用户", proportion=2)

class ObjectBase(BaseModel):
    object_name: str                = Field(..., title="对象名称", proportion=5)
    object_param: str               = Field(..., title="对象参数", proportion=10)
    object_type: Literal["server", "model"] = Field(default="model", title="对象类型", proportion=3)

class ObjectInDB(ObjectBase):
    object_id: str                  = Field(..., title="对象id", proportion=2)
    task_id: str                    = Field(..., title="任务id", proportion=2)
    question_count: int             = Field(default=0, title="总问题数", proportion=2)
    generate_count: int             = Field(default=0, title="生成问题数", proportion=2)
    generate_rate: float            = Field(default=0.0, title="生成成功率", proportion=2)
    evaluate_count: int             = Field(default=0, title="打分问题数", proportion=2)
    evaluate_rate: float            = Field(default=0.0, title="打分成功率", proportion=2)
    token: int                      = Field(default=101, title="tokens", proportion=2)
    TPS: float                      = Field(default=0.01, title="TPS", proportion=2)
    TTFT: int                       = Field(default=0, title="TTFT", proportion=2)
    total_time: int                 = Field(default=0, title="latency", proportion=2)
    score: int                      = Field(default=0, title="得分", proportion=2)

class QuestionBase(BaseModel):
    uuid: str                   = Field(..., title="uuid", proportion=2)
    question: str               = Field(default="", title="问题", proportion=5)
    answer: str                 = Field(default="", title="标准答案", proportion=5)
    ablity: str                 = Field(default="ablity", title="ablity", proportion=5)
    category: str               = Field(default="category", title="category", proportion=5)
    subject: str                = Field(default="subject", title="subject", proportion=5)

class QuestionInDB(QuestionBase):
    question_id: str            = Field(..., title="问题id", proportion=2)
    # question_id: str
    object_id: str              = Field(..., title="评测对象", proportion=2)
    times: int                  = Field(default=0, title="生成次数", proportion=2)
    response: str               = Field(default="", title="模型回答", proportion=5)
    token: int                  = Field(default=101, title="tokens", proportion=2)
    TPS: float                  = Field(default=0.01, title="TPS", proportion=2)
    TTFT: int                   = Field(default=0, title="TTFT", proportion=2)
    total_time: int             = Field(default=0, title="latency", proportion=2)
    score: int                  = Field(default=-1, title="得分", proportion=1)
    choice: Union[str, int]     = Field(default=-1, title="是否选中", proportion=1)
    reason: str                 = Field(default="", title="打分原因", proportion=5)


PAGE_WIDTH  = A4[0]
PAGE_HEIGHT = A4[1]
PAGE_SPACEH = 60                        # 左右空白
PAGE_SPACEV = 60                        # 上下空白
PAGE_LEFT   = PAGE_SPACEH               # 页面左边
PAGE_RIGHT  = PAGE_WIDTH-PAGE_SPACEH    # 页面左边
PAGE_TOP    = PAGE_HEIGHT-PAGE_SPACEV   # 页面顶部
PAGE_DOWN   = PAGE_SPACEV               # 页面底部

def drawTable(title: str, datas: list, widths: list):
    # 创建字体样式对象
    # 表格行列的表达形式为(x, y):左上方第一个单元格为(0, 0), 右下角单元格为(-1, -1)
    tableStyle = TableStyle([
        ("FONT", (0, 0), (-1, 0), "simsun", 10),                # 第一行:
        # ("FONT", (1, -1), (-1, -1), "simsun", 8),                # 第二行 到 最后一行
        ("FONT", (0, 1), (-1, -1), "simsun", 8),                # 第二行 到 最后一行
        ("ALIGN", (0, 0), (-1, -1), "CENTER"),                  # 水平居中
        ("VALIGN", (0, 0), (-1, -1), "MIDDLE"),                 # 垂直居中
        ("INNERGRID", (0, 0), (-1, -1), 0.25, colors.black),    # 单元格分割线
        ("BOX", (0, 0), (-1, -1), 0.25, colors.black),          # 边框
        ("wordWrap", (0, 0), (-1, -1), "CJK"),                  # 自动换行
        # ("BACKGROUND", (0, 0), (-1, -1), colors.lightgrey),     # 背景色(默认为白色)
        # ("TEXTCOLOR", (0, 0), (-1, 0), colors.red),             # 第一行: 区域字体颜色
        # ("LINEBELOW", (0,-1), (-1,-1), 0, colors.white),        # 移除最后一行的下框线,延申LINEABOVE(上框线)、LINEBEFORE(左框线)、LINEAFTER(右框线)
        # ("GRID", (0, 0), (-1, -1), 0.5, colors.black),        # 表格框线为灰色,线宽为0.5
        # ("SPAN", (0, 3), (-1, 3)),                              # 合并单元格
    ])

    cellStyle = ParagraphStyle(name = "cellStyle", 
                                fontName = "simsun",
                                fontSize = 8,
                                wordWrap = 'CJK')       # 自动换行

    cells = []
    for vali in datas:
        lines = []
        for valj in vali:
            lines.append(Paragraph(valj, cellStyle))
        cells.append(lines)

    # return Table(datas, colWidths=col_widths, rowHeights=row_heights, style=tableStyle)
    tabletitle = f"""<para alignment=center fontName="simsun" fontSize=10 spaceBefore=5 spaceAfter=0>{title}</para>"""
    return [Table(cells, colWidths=widths, style=tableStyle), Paragraph(tabletitle)]

'''
title 标题
categorys 分组名称
datas 数据
legends 图示
'''
def drawBarData(title: str, categorys: list, formats: str, datas: list, legends: list=[]):
    def func():
        minv, maxv = 0, 0
        for idx,val in enumerate(datas[0]):
            if val < minv:
                minv = val
            if val > maxv:
                maxv = val
        return minv, maxv
    
    ### 调整step
    minv, maxv = func()
    # maxAxis                         = int(height/20)
    # step                            = int((maxv-minv+maxAxis-1)/maxAxis)    # 向上取整
    step                            = int((maxv-minv)/10)    # 向上取整

    drawing = Drawing(500, 265)
    # drawing = Drawing()
    # drawing.background = colors.pink
    # drawing.setFillColorRGB(0, 0, 1)
    drawing.add(String(45, 255, title, fontSize=13, fontName='simsun', fillColor=colors.black))

    bc = VerticalBarChart()
    # bc.background           = colors.pink
    bc.x                            = 45            # 整个图表的x坐标
    bc.y                            = 45            # 整个图表的y坐标
    bc.height                       = 200           # 图表的高度
    bc.width                        = 350           # 图表的宽度
    bc.data                         = datas
    bc.strokeColor                  = colors.black  # 顶部和右边轴线的颜色
    bc.valueAxis.valueMin           = minv          # 设置y坐标的最小值
    bc.valueAxis.valueMax           = maxv*1.2      # 设置y坐标的最大值
    bc.valueAxis.valueStep          = step          # 设置y坐标的步长
    # bc.groupSpacing                 = 50            # 每组柱状图之间的间隔
    # bc.barSpacing                   = 50            # 每个柱状图之间的间隔
    bc.categoryAxis.labels.dx       = 2
    bc.categoryAxis.labels.dy       = -8
    bc.categoryAxis.labels.angle    = 20            # 分组名称旋转角度
    bc.categoryAxis.categoryNames   = categorys     # 分组名称
    bc.groupSpacing                 = 2             # 每组柱状图之间的间隔
    # bc.barSpacing                   = 1           # 每个柱状图之间的间隔

    # 图形柱上标注文字
    # bc.barLabels.nudge              = -10           # 文字在图形柱的上下位置
    bc.barLabels.nudge              = 10            # 文字在图形柱的上下位置
    bc.barLabelFormat               = formats       # "%9.2f" 9位数字(含小数)两位小数

    # # 每组0-3条的颜色
    # bc.bars[0].fillColor = colors.coral
    # bc.bars[1].fillColor = colors.pink
    # bc.bars[2].fillColor = colors.chartreuse
    # bc.bars[3].fillColor = colors.violet

    # 每组第0条 也就是数组中 第一个元组的颜色
    bc.bars[(0,0)].fillColor = colors.coral
    bc.bars[(0,1)].fillColor = colors.pink
    bc.bars[(0,2)].fillColor = colors.chartreuse
    bc.bars[(0,3)].fillColor = colors.violet

    # # 图示
    # leg = Legend()
    # leg.fontName        = 'simsun'
    # leg.alignment       = 'right'
    # leg.boxAnchor       = 'ne'
    # leg.x               = 475                         # 图例的x坐标
    # leg.y               = 240
    # leg.dxTextSpace     = 10
    # leg.columnMaximum   = len(legends)
    # leg.colorNamePairs  = legends
    # drawing.add(leg)
    drawing.add(bc)
    return drawing

def drawBarRate(title: str, categorys: list, formats: str, datas: list, legends: list=[]):
    drawing = Drawing(500, 265)
    drawing.add(String(45, 255, title, fontSize=13, fontName='simsun', fillColor=colors.black))

    bc = VerticalBarChart()
    bc.x                            = 45            # 整个图表的x坐标
    bc.y                            = 45            # 整个图表的y坐标
    bc.height                       = 200           # 图表的高度
    bc.width                        = 350           # 图表的宽度
    bc.data                         = datas
    bc.strokeColor                  = colors.black  # 顶部和右边轴线的颜色
    bc.valueAxis.valueMin           = 0             # 设置y坐标的最小值
    bc.valueAxis.valueMax           = 1             # 设置y坐标的最大值
    bc.valueAxis.valueStep          = 0.1          # 设置y坐标的步长
    # bc.groupSpacing                 = 50            # 每组柱状图之间的间隔
    # bc.barSpacing                   = 50            # 每个柱状图之间的间隔
    bc.categoryAxis.labels.dx       = 2
    bc.categoryAxis.labels.dy       = -8
    bc.categoryAxis.labels.angle    = 20            # 分组名称旋转角度
    bc.categoryAxis.categoryNames   = categorys     # 分组名称
    bc.groupSpacing                 = 2             # 每组柱状图之间的间隔
    # bc.barSpacing                   = 1           # 每个柱状图之间的间隔

    # 图形柱上标注文字
    # bc.barLabels.nudge              = -10           # 文字在图形柱的上下位置
    bc.barLabels.nudge              = 10            # 文字在图形柱的上下位置
    bc.barLabelFormat               = formats       # "%9.2f" 9位数字(含小数)两位小数

    # 每组0-3条的颜色
    bc.bars[0].fillColor = colors.coral
    bc.bars[1].fillColor = colors.pink
    bc.bars[2].fillColor = colors.chartreuse
    bc.bars[3].fillColor = colors.violet

    # # 每组第0条 也就是数组中 第一个元组的颜色
    # bc.bars[(0,0)].fillColor = colors.coral
    # bc.bars[(0,1)].fillColor = colors.pink
    # bc.bars[(0,2)].fillColor = colors.chartreuse
    # bc.bars[(0,3)].fillColor = colors.violet

    # 图示
    leg = Legend()
    leg.fontName        = 'simsun'
    leg.alignment       = 'right'
    leg.boxAnchor       = 'ne'
    leg.x               = 475                         # 图例的x坐标
    leg.y               = 240
    leg.dxTextSpace     = 10
    leg.columnMaximum   = 4
    leg.colorNamePairs  = legends
    drawing.add(leg)
    drawing.add(bc)
    return drawing

def createParagraph(text: str, level):
    # 基类
    titleStyle = ParagraphStyle(
        name="titleStyle",
        alignment=TA_LEFT,
        textColor=colors.black,
        fontName="msyhbd",
        # backColor=colors.HexColor(0xF2EEE9),
        borderPadding=(5, 5),
    )
    # 封面
    title0Style = ParagraphStyle(
        name="title0Style",
        parent=titleStyle,
        fontSize=40,
        spaceBefore=0,
        spaceAfter=40,
        alignment=TA_CENTER
    )
    # 一级目录格式
    title1Style = ParagraphStyle(
        name="title1Style",
        parent=titleStyle,
        fontSize=22,
        spaceBefore=22,
        spaceAfter=22
    )
    # 二级目录格式
    title2Style = ParagraphStyle(
        name="title2Style",
        parent=titleStyle,
        fontSize=18,
        spaceBefore=18,
        spaceAfter=18
    )
    # 三级目录格式
    title3Style = ParagraphStyle(
        name="title3Style",
        parent=titleStyle,
        fontSize=15,
        spaceBefore=15,
        spaceAfter=15
    )
    # 正文文本格式
    normalStyle = ParagraphStyle(
        name="normalStyle",
        alignment=0,
        fontName="simsun",
        fontSize=12,
        textColor=colors.black,
        firstLineIndent=24,     # 首行缩进	
        leading=18,             # 行距
        spaceBefore=6,          # 段前间隔
        spaceAfter=6,            # 段后间隔
        wordWrap='CJK'
    )
    if level == 0:
        return Paragraph(text, title0Style)
    elif level == 1:
        return Paragraph(text, title1Style)
    elif level == 2:
        return Paragraph(text, title2Style)
    elif level == 3:
        return Paragraph(text, title3Style)
    return Paragraph(text, normalStyle)


# fields 表头所有字段
# except 排除
def createTableHeader(fields: dict, excepts: list=[]):
    proportions = 0
    headers = []
    widths = []
    # 1.获取总占比 和 表头
    for key, val in fields:
        if key not in excepts:
            # 1.1. 计算总站比
            proportions += 1 if not val.json_schema_extra else val.json_schema_extra.get("proportion", 1)

            # 1.2. 获取表头
            title = val.title if val.title else key
            # headers.append(Paragraph(title, cellStyle))
            headers.append(title)

    # 2.计算每个选项的宽度
    # 2.1. 计算占比单元
    delta = (PAGE_WIDTH-PAGE_SPACEH*2)/proportions
    for key, val in fields:
        if key not in excepts:
            proportion = 1 if not val.json_schema_extra else val.json_schema_extra.get("proportion", 1)
            # 2.2. 计算每列的宽度
            widths.append(proportion*delta)
    return headers, widths

def createTaskTable(task_indb: TaskInDB):
    excepts = ["task_id"]
    # 1. 创建任务表表头
    headers, widths = createTableHeader(fields=TaskInDB.model_fields.items(), excepts=excepts)

    # 2. 解析所有数据
    datas = [headers]
    lines = []
    for key, val in task_indb.model_dump().items():
        # 2.1. 逐一展示字段数据
        if key not in excepts:
            # 2.2. 字段名和数据转换
            if key == "task_type":
                val = "进对话" if val==0 else "人工评测"
            elif key == "score_type":
                val = "人工打分" if val==0 else "辅助打分"
            elif key == "user_id":
                val = "test"
            # lines.append(Paragraph(str(val), cellStyle))
            lines.append(str(val))
    datas.append(lines)

    # 3. 渲染
    return drawTable(title="任务信息表", datas=datas, widths=widths)

def createObjectTable(task_indb: TaskInDB, objects: list):
    excepts = ["task_id", "object_id"]
    # 1. 创建任务表表头
    headers, widths = createTableHeader(fields=ObjectInDB.model_fields.items(), excepts=excepts)

    # 2. 解析所有数据
    datas = [headers]
    for obj in objects:
        lines = []
        for key, val in obj.model_dump().items():
            if key not in excepts:
                # lines.append(Paragraph(str(val), cellStyle))
                lines.append(str(val))
        datas.append(lines)
    
    # 3. 渲染
    return drawTable(title="评测对象信息表", datas=datas, widths=widths)

def createObjectBars(task_indb: TaskInDB, objects: list):
    names = []
    labels = ["token", "TPS", "TTFT", "total_time", "score", "generate_rate", "evaluate_rate"]

    categorys = []
    tokens = ()
    TPSs = ()
    TTFTs = ()
    total_times = ()
    generate_rates = ()
    evaluate_rates = ()
    
    for obj in objects:
        categorys.append(obj.object_name)

        tokens          += (obj.token,)
        TPSs            += (obj.TPS,)
        TTFTs           += (obj.TTFT,)
        total_times     += (obj.total_time,)
        generate_rates  += (obj.generate_rate,)
        evaluate_rates  += (obj.evaluate_rate,)
        # tokens.append((obj.token, ))
    
    return [drawBarData("token 对比图", categorys, "%d", [tokens]),
            drawBarData("TPS 对比图(tokens/s)", categorys, "%9.2f", [TPSs]),
            drawBarData("TTFT 对比图(ms)", categorys, "%d", [TTFTs]),
            drawBarData("total_time 对比图(ms)", categorys, "%d", [total_times]),
            drawBarRate("total_time 对比图(ms)", categorys, "%3.2f", [generate_rates, evaluate_rates]),
            ]

def createQuestionTable(task_indb: TaskInDB, objects: list, questions: list):
    excepts = ["ablity", "category", "subject", "uuid", "question_id"]
    # 1. 创建任务表表头
    headers, widths = createTableHeader(fields=QuestionInDB.model_fields.items(), excepts=excepts)

    # 2. 解析所有数据
    datas = [headers]
    object_dict = {item.object_id:item for item in objects}
    for question in questions:
        lines = []
        for key, val in question.model_dump().items():
            if key not in excepts:
                if key == "choice":
                    val = "是" if val>0 else "否"
                elif key == "score":
                    val = "-" if val<0 else val
                elif key == "object_id":
                    val = val if not object_dict.get(val, None) else object_dict.get(val, None).object_name
                # lines.append(Paragraph(str(val), cellStyle))
                lines.append(str(val))
        datas.append(lines)
    
    # 3. 渲染
    return drawTable(title="问答详情", datas=datas, widths=widths)

### 首页
def myFirstPage(canvas, doc):
    canvas.saveState()
    canvas.setFillColorRGB(0, 0, 0)
    canvas.setFont('simsun', 14)
    str="(内部资料)"
    canvas.drawCentredString(PAGE_WIDTH/2, 35*mm, str)
    myLaterPages(canvas, doc)
    canvas.restoreState()

### 页眉和页脚的绘制 
def myLaterPages(canvas, doc):
    canvas.saveState()
    # 页眉
    canvas.setFillColorRGB(0, 0, 1)
    canvas.setFont('simsun', 12)
    canvas.drawString(PAGE_LEFT, PAGE_HEIGHT-18*mm, "2025-04-17")
    canvas.drawRightString(PAGE_RIGHT, PAGE_HEIGHT-18*mm, "统计报告")
    # qr_code = qr.QrCode('https://www.cnblogs.com/windfic', width=45, height=45)
    # canvas.setFillColorRGB(0, 0, 0)
    # qr_code.drawOn(canvas, 0, A4[1]-45)

    canvas.setStrokeColorRGB(0, 0, 0)
    canvas.line(PAGE_LEFT, PAGE_TOP, PAGE_RIGHT, PAGE_TOP)
    
    # 页脚
    canvas.setStrokeColorRGB(0.8, 0.8, 0.8)
    # canvas.line(50, 45, PAGE_WIDTH-50, 45)
    canvas.line(PAGE_LEFT, PAGE_DOWN, PAGE_RIGHT, PAGE_DOWN)

    canvas.setFillColorRGB(0.8, 0.8, 0.8)
    canvas.setFont('simsun', 12)
    canvas.drawCentredString(PAGE_WIDTH/2, 14*mm, f"page {doc.page}")
    canvas.restoreState()

def exportPdf(task_indb, objects, question_list):
    # 2. 初始化配置
    # 注册字体
    pdfmetrics.registerFont(ttfonts.TTFont("simsun", "simsun.ttc"))
    # pdfmetrics.registerFont(TTFont("msyh", "msyh.ttc"))
    pdfmetrics.registerFont(ttfonts.TTFont("msyhbd", "msyhbd.ttc"))
    
    # 3. 创建pdf
    # 此处不再使用canvas创建pdf对象,改为文档模板doctemplate模块的SimpleDocTemplate类,页面由点构成。设置下边距72点,即1英寸的高度
    doc = SimpleDocTemplate(f"{task_indb.task_name}.pdf", pagesize=A4)
    # doc = SimpleDocTemplate(f"{task_indb.task_name}.pdf", pagesize=A4, rightMargin=72,leftMargin=72, topMargin=72, bottomMargin=60)
    Story = [Spacer(0, 168)]
    # Story = []
    Story.append(createParagraph(f"任务执行统计报告", 0))
    Story.append(PageBreak())
    Story.append(createParagraph(f"1.任务基本信息", 1))

    task_text = "任务基本信息:任务创建用户名、任务名称、备注信息、生成次数、任务类型、打分类型、评测集名称(人工评测显示为空)、评测问题数、任务开始时间、任务结束时间、任务持续时长。"
    Story.append(createParagraph(task_text, -1))
    Story.extend(createTaskTable(task_indb=task_indb))

    Story.append(createParagraph(f"2.评测对象信息", 1))
    object_text = "评测对象信息:模型名|服务地址、评测类型、得分、问题数、打分类型、错误率、总token、TPS、TTFT、Total time、生成耗时、打分耗时。"
    Story.append(createParagraph(object_text, -1))
    Story.append(createParagraph(f"2.1.评测对象数据", 2))
    Story.extend(createObjectTable(task_indb=task_indb, objects=objects))

    Story.append(createParagraph(f"2.2.评测对比图", 2))
    Story.extend(createObjectBars(task_indb=task_indb, objects=objects))

    Story.append(createParagraph(f"3.问答打分详情", 1))
    question_text = "ablity、category、subject、主观|客观、问题UUID、问题内容、答案,模型|服务回答次数、生成回答、总token、TPS、TTFT、Total time、得分、是否为最佳、打分理由。"
    Story.append(createParagraph(question_text, -1))
    Story.extend(createQuestionTable(task_indb=task_indb, objects=objects, questions=question_list))

    doc.build(Story, onFirstPage=myFirstPage, onLaterPages=myLaterPages)

if __name__ == '__main__':
    # 1. 模拟数据
    # 1.1. 模拟问题数据
    questions = []
    question_uuid = uuid.uuid4().hex
    question_base = QuestionBase(
            uuid=question_uuid, question="请介绍一下北京",
            answer="当需要将数据在一个单元格分两列的话,可以用下面的语法当需要将数据在一个单元格分两列的话,可以用下面的语法当需要将数据在一个单元格分两列的话,可以用下面的语法当需要将数据在一个单元格分两列的话,可以用下面的语法")
    questions.append(question_base)
    question_base = QuestionBase(
            uuid=question_uuid, question="请介绍一下苏轼",
            answer="可以用下面的语法当需要将数据在一个单元格分两列的话,可以用下面的语法当需要将数据在一个单元格分两列的话,可以用下面的语法当需要将数据在一个单元格分两列的话,可以用下面的语法")
    questions.append(question_base)

    # 1.2. 模拟任务数据
    task_id = uuid.uuid4().hex
    user_id = uuid.uuid4().hex
    task_indb = TaskInDB(task_name="测试任务", description="", question_count=len(questions), task_id=task_id, user_id=user_id)

    # 1.3. 模拟对象数据
    def createObject(task_indb: TaskInDB, object_name: str, object_type: str):
        object_id = uuid.uuid4().hex
        generate_rate = round(random.uniform(0.0, 1.0), 2)
        evaluate_rate = round(random.uniform(0.0, 1.0), 2)

        object_indb = ObjectInDB(object_name=object_name, object_param="", object_type=object_type,
                                object_id=object_id, task_id=task_indb.task_id, question_count=task_indb.question_count,
                                generate_count=task_indb.question_count, generate_rate=generate_rate,
                                evaluate_count=task_indb.question_count, evaluate_rate=evaluate_rate
                                 )
        return object_indb
    objects = []
    for index in range(4):
        object_name = f"other-{index}"
        object_type = "model"
        if index == 0:
            object_name = "qwen-7b"
            object_type = "server"
        elif index == 1:
            object_name = "commandr"

        object_indb = createObject(task_indb=task_indb, object_name=object_name, object_type=object_type)
        objects.append(object_indb)

    # 1.4. 模拟问题数据
    def createQuestion(object_indb: ObjectInDB, question: QuestionBase, index: int):
        question_id = uuid.uuid4().hex
        token       = random.randint(1, 100)
        toltal_time = random.randint(50, 1000)
        TTFT        = random.randint(10, 100)
        TPS         = round((token*1000)/toltal_time, 2)
        score       = random.randint(0, 2)

        question_indb = QuestionInDB(**question.model_dump(),
            object_id=object_indb.object_id, question_id=question_id, times=index,
            response="当需要将数据在一个单元格分两列的话,可以用下面的语法当需要将数据在一个单元格分两列的话,可以用下面的语法当需要将数据在一个单元格分两列的话,可以用下面的语法当需要将数据在一个单元格分两列的话,可以用下面的语法",
            token=token, total_time=toltal_time, TTFT=TTFT, TPS=TPS, score=score,
            reason="当需要将数据在一个单元格分两列的话,可以用下面的语法当需要将数据在一个")
        
        object_indb.token       += token
        object_indb.total_time  += toltal_time
        object_indb.TTFT        += TTFT
        object_indb.score       += score
        object_indb.TPS         = round((object_indb.token*1000)/object_indb.total_time, 2)
        return question_indb
    question_list = []
    for question in questions:
        # 同步更新了对象的 数据
        for obj in objects:
            # 处理 一个问题的多次回答
            for index in range(task_indb.times):
                question_indb = createQuestion(object_indb=obj, question=question, index=index)
                question_list.append(question_indb)

    exportPdf(task_indb, objects, question_list)

不好用,实在是不好用!!!!!!!!!!!!!!!!!!!!

相关推荐
航Hang*15 分钟前
C PRIMER PLUS——第6-2节:二维数组与多维数组
c语言·开发语言·经验分享·程序人生·算法·学习方法·visual studio
带刺的坐椅19 分钟前
FastMCP(python)和 SolonMCP(java)的体验比较(不能说一样,但真的很像)
java·python·solon·mcp·fastmcp
易只轻松熊31 分钟前
C++(1):整数常量
开发语言·c++
努力的搬砖人.39 分钟前
Java 线程池原理
java·开发语言
Dovis(誓平步青云)1 小时前
精讲C++四大核心特性:内联函数加速原理、auto智能推导、范围for循环与空指针进阶
c语言·开发语言·c++·笔记·算法·学习方法
passionSnail1 小时前
《用MATLAB玩转游戏开发》Flappy Bird:小鸟飞行大战MATLAB趣味实现
开发语言·matlab
jz_ddk1 小时前
[学习]RTKLib详解:convkml.c、convrnx.c与geoid.c
c语言·开发语言·学习
stevenzqzq1 小时前
kotlin flow防抖
开发语言·kotlin·flow
极小狐1 小时前
如何从极狐GitLab 容器镜像库中删除容器镜像?
java·linux·开发语言·数据库·python·elasticsearch·gitlab