转载:ReportLab生成带表格和图文的PDF

发布时间 2023-12-08 14:07:10作者: 焦糖可丽饼

转载来自于:https://zhuanlan.zhihu.com/p/456486769 龙在天涯

 

项目环境:

环境:Anaconda Python 3.10

编辑器:PyCharm 2021.2.3

Packages:Reportlab 3.6.2

ReportLab简介

“This is a software library that lets you directly create documents in Adobe's Portable Document Format (PDF)using the Python programming language. It also creates charts and data graphics in various bitmap and vectorformats as well as PDF.”

ReportLab是一个可以让你使用Python语言直接生成AdobePDF文档软件库。它还可以创建图片或者PDF格式的图表或者数据图。

ReportLab对PDF的操作分为了几个层级,从上到下,依次为:

DocTemplates:文档的最外层容器;

PageTemplates:各种页面布局容器;

Frames:页面中放置文本,图像的区块;

Flowables:可排列的文本或者图像元素,包含图片、段落、表格、分隔符等等,但不包含页脚和固定位置的页面图像。

Canvas:接收所有其它层级信息并绘制文档的最底层。

用图层表示如下:

上述内容和本文涉及的接口都可以参考ReportLab用户指南

开发思路要点:

  1. 中文字体注册及使用
  2. 创建文档并加入标题
  3. 绘制页脚
  4. 表格绘制
  5. 饼图绘制
  6. 简单段落、图片
  7. 横向多图排布

开始正式编程,先看结果:

成品效果

中文字体注册及使用

首先,下载或者在C:\Windows\Fonts或其它途径找到字体文件,比如“宋体”,然后将其复制到你的Python环境中的reportlab包里的fonts目录下,比如我的是D:\ProgramData\Anaconda3\envs\py10\Lib\site-packages\reportlab\fonts:

然后再脚本里面写入以下内容:

from reportlab.pdfbase import pdfmetrics
from reportlab.pdfbase.ttfonts import TTFont
song = "simsun"
pdfmetrics.registerFont(TTFont(song, "simsun.ttc"))

这个字体就注册好了,同理你可以注册一些其它的字体,比如这样。

msyh = "msyh"
msyhbd = "msyhbd"
song = "simsun"
pdfmetrics.registerFont(TTFont(song, "simsun.ttc"))
pdfmetrics.registerFont(TTFont(msyh, "msyh.ttc"))
pdfmetrics.registerFont(TTFont(msyhbd, "msyhbd.ttc"))

你只需要在需要使用的地方将字体设置为song就可以使用了,比如:

labelStyle = ParagraphStyle(
    name="label",
    fontName=song,
    fontSize=6,
)

canvas.setFont(song, 8)

创建文档并加入标题

首先,我们使用SimpleDocTemplate创建文档,在bulid的onFirstPage事件里,设置好字体和填充色,使用canvas.drawCentredString绘制标题。

from reportlab.lib import colors
from reportlab.lib.colors import HexColor
from reportlab.lib.pagesizes import A4
from reportlab.lib.units import inch
from reportlab.pdfbase import pdfmetrics
from reportlab.pdfbase.ttfonts import TTFont
from reportlab.pdfgen.canvas import Canvas
from reportlab.platypus import Spacer, SimpleDocTemplate

# 注册字体
msyh = "msyh"
msyhbd = "msyhbd"
song = "simsun"
pdfmetrics.registerFont(TTFont(song, "simsun.ttc"))
pdfmetrics.registerFont(TTFont(msyh, "msyh.ttc"))
pdfmetrics.registerFont(TTFont(msyhbd, "msyhbd.ttc"))

PAGE_HEIGHT = A4[1]
PAGE_WIDTH = A4[0]

def myFirstPage(c: Canvas, doc):
    c.saveState()
    # 设置填充色
    c.setFillColor(colors.orange)
    # 设置字体大小
    c.setFont(msyhbd, 30)
    # 绘制居中标题文本
    c.drawCentredString(300, PAGE_HEIGHT - 80, "新年快乐")
    c.restoreState()


def myLaterPages(c: Canvas, doc):
    c.saveState()
    c.restoreState()


# 创建文档
doc = SimpleDocTemplate("output/pdftest.pdf")
Story = [Spacer(1, 2 * inch)]
# 保存文档
doc.build(Story, onFirstPage=myFirstPage, onLaterPages=myLaterPages)

运行后,就得到一个包含标题的基本文档了,如下。

绘制页脚

接下来绘制页脚的线条和文字,因为页脚的内容是一个在每页上固定的位置的内容,所以使用Canvas的接口来绘制。这里涉及到Canvas.line和Canvas.drawString两个方法。定义一个方法如下:

def DrawPageInfo(c: Canvas, date=datetime.date.today):
    """绘制页脚"""
    # 设置边框颜色
    c.setStrokeColor(colors.dimgrey)
    # 绘制线条
    c.line(30, PAGE_HEIGHT - 790, 570, PAGE_HEIGHT - 790)
    # 绘制页脚文字
    c.setFont(song, 8)
    c.setFillColor(colors.black)
    c.drawString(30, PAGE_HEIGHT - 810, f"生成日期:{date}")

然后再每页的事件里面调用它。如下:

def myFirstPage(c: Canvas, doc):
    c.saveState()
    # 设置填充色
    c.setFillColor(colors.orange)
    # 设置字体大小
    c.setFont(msyhbd, 30)
    # 绘制居中标题文本
    c.drawCentredString(300, PAGE_HEIGHT - 80, "新年快乐")
    # 绘制页脚
    DrawPageInfo(c)
    c.restoreState()


def myLaterPages(c: Canvas, doc):
    c.saveState()
    # 绘制页脚
    DrawPageInfo(c)
    c.restoreState()

然后运行,就可以在PDF页面的底部看到如下图像:

页脚

表格绘制

同样创建一个表格绘制方法,使用的是platypus中的Table类。

# 绘制用户信息表
def drawUserInfoTable(c: Canvas, x, y):
    """绘制用户信息表"""
    data = [["姓名", "龙在天涯"],
            ["出生日期", "2000-01-01"],
            ["性别", "男"],
            ["身高", 175],
            ["体重", 65],
            ["年龄", 20]]
    t = Table(data, style={
        ("FONT", (0, 0), (-1, -1), msyhbd, 8),
        ("TEXTCOLOR", (0, 0), (-1, -1), colors.black),
        ('ALIGN', (1, 0), (1, -1), 'CENTER')
    })
    t._argW[1] = 200
    t.wrapOn(c, 0, 0)
    t.drawOn(c, x, y)

然后在首页绘制页脚前调用它,就会得到如下结果。

绘制饼图

使用graphics.charts.piecharts中的Pie类绘制一个只有两个数据的饼图,把第二块的填充色设置为透明,然后用一个圆Circle覆盖中心的部分,饼图Pie和圆Circle都需要用一个Drawing的对象来承载。再在上层写上文字,就实现了这样的效果。

方法代码如下:

def drawScorePie(canvas: Canvas, x, y, score):
    """绘制评分饼图"""
    d = Drawing(100, 100)
    # 画大饼图
    pc = Pie()
    pc.width = 100
    pc.height = 100
    # 设置数据
    pc.data = [score, 100 - score]
    pc.slices.strokeWidth = 0.1
    # 设置颜色
    color = colors.seagreen
    if (score < 60):
        color = colors.orangered
    elif (score < 85):
        color = colors.orange
    pc.slices.strokeColor = colors.transparent
    pc.slices[0].fillColor = color
    pc.slices[1].fillColor = colors.transparent
    d.add(pc)
    # 画内圈
    circle = Circle(50, 50, 40)
    circle.fillColor = colors.white
    circle.strokeColor = colors.transparent
    d.add(circle)
    # 把饼图画到Canvas上
    d.drawOn(canvas, x, y)
    # 写字
    canvas.setFont(msyh, 30)
    canvas.setFillColor(color)
    canvas.drawCentredString(x + 50, y + 40, f"2022")

然后在绘制表格后调用,就实现了如下效果。

绘制饼图后

添加顺序排布的段落和图片

这里就比较简单了,只需要先设置好段落的格式ParagraphStyle。

# 设置段落格式
titleStyle = ParagraphStyle(
    name="titleStyle",
    alignment=1,
    fontName=msyhbd,
    fontSize=10,
    textColor=colors.black,
    backColor=HexColor(0xF2EEE9),
    borderPadding=(5, 5)
)

然后创建platypus中的Paragraph对象和Image对象,让后将他们一次添加到Story里面就可以了。如下:

# 绘制段落
Story.append(Paragraph("我是龙在天涯,在这里祝大家:", titleStyle))
Story.append(Spacer(1, 0.2 * inch))
Story.append(Paragraph("新年快乐", titleStyle))
Story.append(Spacer(1, 0.2 * inch))
Story.append(Paragraph("恭喜发财", titleStyle))
Story.append(Spacer(1, 0.2 * inch))
Story.append(Paragraph("虎年大吉", titleStyle))
Story.append(Spacer(1, 0.2 * inch))
# 绘制顺序排列的图像
Story.append(Image("data/img1.png", 6 * inch, 3 * inch))

我们还可以添加各种分隔符如空行符Spacer、分页符PageBreak,或者ListFlowable,ListItem等等所有继承于Flowable的元素,包括自定义的继承于Flowable的类。一个比较有用的就是可以使用Drawing去承载多种不同种类的元素,然后混排在一起,比如将图像和图像横排,图像和表格横排等等。然后将这个Drawing顺序排列到段落之中。

添加横向排列的图片

上面已经提到了一些,要将两个图像横向排列有两种比较便捷的方法:

1,用一个Drawing将两个图像Image都放在其中,然后把这个Drawing排列到段落之中。

2, 用一个没有边框的表格Table,将图像Image作为表格的一个格子,然后将表格排列到段落之中。

注意这里得Image和前面直接拿去排序的Image不同,它是graphics.shapes.Image类,它并不继承于Flowable,而可以直接排序的是继承于Flowable的platypus.Image类,两者的构造函数也不同。

这里采用前一种方法。实现方法如下:

from reportlab.graphics.shapes import Image as DrawingImage

d = Drawing()
d.add(DrawingImage(0, 0, 200, 200, "data/img2.png"))
d.add(DrawingImage(200, 0, 200, 200, "data/img3.png"))
Story.append(d)

把这部分添加到主逻辑里面,然后运行脚本,就可以得到最开始的PDF效果了。主体部分完整版如下:

# 创建文档
doc = SimpleDocTemplate("output/pdftest.pdf")
Story = [Spacer(1, 2 * inch)]
# 绘制段落
Story.append(Paragraph("我是龙在天涯,在这里祝大家:", titleStyle))
Story.append(Spacer(1, 0.2 * inch))
Story.append(Paragraph("新年快乐", titleStyle))
Story.append(Spacer(1, 0.2 * inch))
Story.append(Paragraph("恭喜发财", titleStyle))
Story.append(Spacer(1, 0.2 * inch))
Story.append(Paragraph("虎年大吉", titleStyle))
Story.append(Spacer(1, 0.2 * inch))
# 绘制顺序排列的图像
Story.append(Image("data/img1.png", 6 * inch, 3 * inch))
# 绘制两个横向排布的图像
d = Drawing()
d.add(DrawingImage(0, 0, 200, 200, "data/img2.png"))
d.add(DrawingImage(200, 0, 200, 200, "data/img3.png"))
Story.append(d)
# 保存文档
doc.build(Story, onFirstPage=myFirstPage, onLaterPages=myLaterPages)



龙在天涯大佬写的这篇对初级者(我)比较有帮助,备份学习一下。