目录
- 前言
- reportlab是什么
- 安装和导入库
- 将画图、画表格、编辑文字抽象为类
- pdf插入图片
- 以文件路径写入pdf
- 以流文件写入pdf
- pdf分页
- 以生成pdf流文件为例
前言
本博客重点内容:reportlab生成流文件格式、reportlab分页和图片流文件写入reportlab等。
我讲一下我这个需求的来源,做的项目是一个地理空间查询和使用的系统,通过在前端调用高德地图api创建了一个查询区域,获取区域内的地理数据(数据库)。具体的需求就是,将查询区域和地理数据制作成一个覆盖率分析报告,报告中的其他内容都已完成,但报告中需要展示高德地图、查询区域、地理数据的完整图片这个功能卡了2个星期,主要原因是我对地理空间数据不熟悉,很多python相关库也不清楚,在构建图形的过程中走了很多弯路。
现在将整个实现过程梳理完成,希望对各位同道有帮助,跟其他文章和官网不同,本博客是以使用的优先级来讲解这个库。<我们靠所得来谋生,但靠给予来创造生活>
reportlab是什么
reportlab是Python的一个标准库,可以画图、画表格、编辑文字,最后可以输出PDF格式。它的逻辑和编辑一个word文档或者PPT很像。有两种方法:
1)建立一个空白文档,然后在上面写文字、画图等;
2)建立一个空白list,以填充表格的形式插入各种文本框、图片等,最后生成PDF文档。
因为需要产生一份给用户看的报告,里面需要插入图片、表格等,所以采用的是第二种方法。
**由于我们这个功能是django网站项目上,我们并不想生成一个文件,第二种方法还可以生成io流文件,进而传递给前端生成pdf,这样后端就不存在文件的存写读的操作了。**本篇文章在方法2的基础上,加入了reportlab生成流文件格式、reportlab分页和图片流文件写入reportlab等功能。
安装和导入库
官网链接:https://pypi.org/project/reportlab/
官方文档:https://docs.reportlab.com/reportlab/userguide/ch1_intro/
pip install reportlab
代码如下(示例):
from reportlab.pdfbase import pdfmetrics # 注册字体 | |
from reportlab.pdfbase.ttfonts import TTFont # 字体类 | |
from reportlab.platypus import Table, SimpleDocTemplate, Paragraph, Image # 报告内容相关类 | |
from reportlab.lib.pagesizes import letter # 页面的标志尺寸(.5*inch, 11*inch) | |
from reportlab.lib.styles import getSampleStyleSheet # 文本样式 | |
from reportlab.lib import colors # 颜色模块 | |
from reportlab.graphics.charts.barcharts import VerticalBarChart # 图表类 | |
from reportlab.graphics.charts.legends import Legend # 图例类 | |
from reportlab.graphics.shapes import Drawing # 绘图工具 | |
from reportlab.lib.units import cm # 单位:cm | |
# 注册字体(提前准备好字体文件, 如果同一个文件需要多种字体可以注册多个) | |
pdfmetrics.registerFont(TTFont('SimSun', 'SimSun.ttf')) |
将画图、画表格、编辑文字抽象为类
抽象为类后,可实现多次调用,方便处理。这里我将页眉和页脚的方法也放在了抽象类中。在生成文件时,实现pdf报告的自动分页。
class Graphs(object): | |
def __init__(self): | |
# 获取所有样式表 | |
self.style = getSampleStyleSheet() | |
# 绘制标题 | |
def draw__title(self, title1: str): | |
# 拿到标题样式 | |
ct = self.style['Heading'] | |
# 单独设置样式相关属性 | |
ct.fontName = 'hei' # 字体名 | |
ct.fontSize = # 字体大小 | |
ct.leading =.5 * ct.fontSize # 行间距 | |
ct.textColor = colors.black # 字体颜色 | |
ct.alignment = # 居中 | |
return Paragraph(title, ct) | |
# 绘制小标题(黑色) | |
def draw__title(self, title: str, content: str): | |
# 拿到标题样式 | |
ct = self.style['Normal'] | |
# 单独设置样式相关属性 | |
ct.fontName = 'song' # 字体名 | |
ct.fontSize = # 字体大小 | |
ct.leading =.5 * ct.fontSize # 行间距 | |
ct.textColor = colors.black # 字体颜色 | |
return Paragraph(("<font name='Dengb'>%s</font>" % title) + content, ct) | |
# 绘制普通段落内容 | |
def draw_text(self, text: str): | |
# 获取普通样式 | |
ct = self.style['Normal'] | |
ct.fontName = 'song' | |
ct.fontSize = | |
ct.leading =.5 * ct.fontSize # 行间距 | |
ct.wordWrap = 'CJK' # 设置自动换行 | |
ct.alignment = # 左对齐 | |
ct.firstLineIndent = # 第一行开头空格 | |
ct.leading = | |
return Paragraph(text, ct) | |
# 绘制表格标题 | |
def draw_table_title(self, title: str): | |
# 拿到标题样式 | |
ct = self.style['Heading'] | |
# 单独设置样式相关属性 | |
ct.fontName = 'Dengb' # 字体名 | |
ct.fontSize = # 字体大小 | |
ct.leading = # 行间距 | |
ct.textColor = colors.black # 字体颜色 | |
ct.alignment = # 居中 | |
ct.bold = True | |
# 创建标题对应的段落,并且返回 | |
return Paragraph(title, ct) | |
# 绘制表格 | |
def draw_table(*args): | |
# 列宽度 | |
col_width = [, 300, 50, 50] | |
style = TableStyle( | |
[('FONTNAME', (, 0), (-1, -1), 'song'), # 全表字体 | |
('FONTNAME', (, 0), (-1, 0), 'Dengb'), # 首行字体 | |
('FONTSIZE', (, 0), (-1, 0), 12), # 第一行的字体大小 | |
('FONTSIZE', (, 1), (-1, -1), 10), # 第二行到最后一行的字体大小 | |
('ALIGN', (, 0), (-1, -1), 'CENTER'), # 第一行水平居中 | |
('VALIGN', (, 0), (-1, -1), 'MIDDLE'), # 所有表格上下居中对齐 | |
('TEXTCOLOR', (, 0), (-1, -1), colors.black), # 设置表格内文字颜色(不包含第一行) | |
('GRID', (, 0), (-1, -1), 0.5, colors.black), # 设置表格框线为grey色,线宽为0.5 | |
# ('SPAN', (, 1), (0, 2)), # 合并第一列二三行 | |
# ('SPAN', (, 3), (0, 4)), # 合并第一列三四行 | |
# ('SPAN', (, 5), (0, 6)), # 合并第一列五六行 | |
# ('SPAN', (, 7), (0, 8)), # 合并第一列五六行 | |
]) | |
table = Table(args, colWidths=col_width, style=style) | |
return table | |
# 创建图表 | |
def draw_bar(bar_data: list, ax: list, items: list): | |
drawing = Drawing(, 250) | |
bc = VerticalBarChart() | |
bc.x = # 整个图表的x坐标 | |
bc.y = # 整个图表的y坐标 | |
bc.height = # 图表的高度 | |
bc.width = # 图表的宽度 | |
bc.data = bar_data | |
bc.strokeColor = colors.black # 顶部和右边轴线的颜色 | |
bc.valueAxis.valueMin = # 设置y坐标的最小值 | |
bc.valueAxis.valueMax = # 设置y坐标的最大值 | |
bc.valueAxis.valueStep = # 设置y坐标的步长 | |
bc.categoryAxis.labels.dx = | |
bc.categoryAxis.labels.dy = - | |
bc.categoryAxis.labels.angle = | |
bc.categoryAxis.categoryNames = ax | |
# 图示 | |
leg = Legend() | |
leg.fontName = 'song' | |
leg.alignment = 'right' | |
leg.boxAnchor = 'ne' | |
leg.x = # 图例的x坐标 | |
leg.y = | |
leg.dxTextSpace = | |
leg.columnMaximum = | |
leg.colorNamePairs = items | |
drawing.add(leg) | |
drawing.add(bc) | |
return drawing | |
# 绘制图片 | |
def draw_img(path): | |
img = Image(path, width=, height=500, kind='bound') # 读取指定路径下的图片 | |
return img | |
# 设置页脚 | |
def footer(self, canvas, doc): | |
""" | |
设置页脚--页码 | |
:param canvas:Canvas类型 pdf画布 | |
:param doc:doc类型 整个pdf文件 | |
""" # 拿到标题样式 | |
ct = self.style['Heading'] | |
# 单独设置样式相关属性 | |
ct.fontName = 'song' # 字体名 | |
ct.fontSize = # 字体大小 | |
ct.leading = # 行间距 | |
ct.textColor = colors.black # 字体颜色 | |
ct.alignment = # 居中 | |
canvas.saveState() # 先保存当前的画布状态 | |
pageNumber = ("%s" % canvas.getPageNumber()) # 获取当前的页码 | |
p = Paragraph(pageNumber, ct) | |
p.wrap( * cm, 1 * cm) # 申请一块1cm大小的空间,返回值是实际使用的空间 | |
p.drawOn(canvas,, 50) # 将页码放在指示坐标处 | |
canvas.restoreState() | |
# 设置页眉 | |
def header(self, canvas, doc): | |
""" | |
设置页眉 | |
:param canvas:Canvas类型 pdf画布 | |
:param doc:doc类型 整个pdf文件 | |
https://cxybb.com/article/liyadian/ | |
""" | |
canvas.saveState() | |
im = Image("utils/map_png/image/logo.png", height=, width=98) | |
im.drawOn(canvas,, 780) | |
# canvas.setStrokeColorRGB(.8, 0.8, 0.8) | |
# canvas.setFillColorRGB(, 0, 0) | |
# canvas.line(, 28, doc.width - 15, 28) | |
# canvas.setFont('regular',) | |
# str = f"XXX银行 | Page {doc.page}" | |
# textLen = stringWidth(str, 'regular',) | |
# canvas.drawCentredString(int((doc.width - textLen) /), 0.15 * inch, str) | |
canvas.restoreState() |
pdf插入图片
pdf中插入图片会有2中方式,网络上常用的方式都是文件路径,本片文章的图片是通过Html2Image生成的,无法保存成流文件格式,只能保存至本地文件。但是如果图片是通过其他途径生成的,可以直接以流文件的方式写入pdf,这样可以节省文件的创建、写入、读取和删除等操作,节省内存和磁盘空间。
以文件路径写入pdf
base = Path(__file__).resolve().parent | |
content.append(graphs.draw_table_title('图 影像覆盖情况')) | |
img_path = os.path.join(base, 'map_png', out_file) | |
content.append(graphs.draw_img(img_path)) | |
content.append(Spacer(, 10)) |
以流文件写入pdf
from pillow import Image as pilImage | |
with open('test.png',mode='rb') as f: | |
# 读取到图片 | |
content.append(graphs.draw_img(f)) | |
content.append(Spacer(, 10)) |
pdf分页
在第二部分我们已经写入了pdf页眉和页脚的方法和参数,具体如何加载到pdf中可以参考下列方法。
doc = SimpleDocTemplate('report.pdf', pagesize=letter) | |
# 声明一块Frame,存放页码 | |
frame_footer = Frame(doc.leftMargin, doc.bottomMargin, doc.width, doc.height, id='normal') | |
# 设置页面模板,在加载页面时先运行herder函数,在加载完页面后运行footer函数 | |
template = PageTemplate(id='test', frames=frame_footer, onPage=graphs.header, onPageEnd=graphs.footer) | |
doc.addPageTemplates([template]) | |
doc.build(content) |
以生成pdf流文件为例
def pics__pdf(info, table, out_file): | |
""" | |
info:基本信息 | |
table:表格数据 | |
""" | |
# 获取Graphs类 | |
graphs = Graphs() | |
# 创建内容对应的空列表 | |
content = list() | |
#.添加标题 | |
content.append(graphs.draw__title('数据中心')) | |
# 隔行的高度 | |
content.append(Spacer(, 10)) | |
#.添加小标题 | |
content.append(graphs.draw__title("报告形成日期: ", str(datetime.date.today().strftime("%Y/%m/%d")))) | |
content.append(Spacer(, 10)) | |
#.添加图片 | |
base = Path(__file__).resolve().parent | |
content.append(graphs.draw_table_title('图 影像覆盖情况')) | |
img_path = os.path.join(base, 'map_png', out_file) | |
content.append(graphs.draw_img(img_path)) | |
content.append(Spacer(, 10)) | |
#.添加表格标题 | |
content.append(graphs.draw_table_title('表 框选区域覆盖数据清单')) | |
#.添加表格数据 | |
# data = [ | |
# ['职位名称', '平均薪资', '较上年增长率'], | |
# ['数据分析师', '.5K', '25%'], | |
# ('高级数据分析师', '.5K', '14%'), | |
# ('资深数据分析师', '.3K', '10%') | |
# ] | |
content.append(graphs.draw_table(*table)) | |
# 生成图表 | |
# content.append(Graphs.draw_title('')) | |
# content.append(Graphs.draw_little_title('热门城市的就业情况')) | |
# b_data = [(, 12900, 20100, 20300, 20300, 17400), (15800, 9700, 12982, 9283, 13900, 7623)] | |
# ax_data = ['BeiJing', 'ChengDu', 'ShenZhen', 'ShangHai', 'HangZhou', 'NanJing'] | |
# leg_items = [(colors.red, '平均薪资'), (colors.green, '招聘量')] | |
# content.append(Graphs.draw_bar(b_data, ax_data, leg_items)) | |
# 添加文字 | |
# content.append(Graphs.draw_text( | |
# '众所周知,大数据分析师岗位是香饽饽,近几年数据分析热席卷了整个互联网行业,与数据分析的相关的岗位招聘、培训数不胜数。很多人前赴后继,想要参与到这波红利当中。那么数据分析师就业前景到底怎么样呢?')) | |
# 生成pdf文件 | |
# 方式一: | |
# doc = SimpleDocTemplate('report.pdf', pagesize=letter) | |
# 方式二-存储文件: | |
# doc = BaseDocTemplate('report.pdf') | |
# 方式二-流文件格式 | |
buffer = io.BytesIO() # 重点 起一个 io | |
doc = BaseDocTemplate(buffer) | |
# 声明一块Frame,存放页码 | |
frame_footer = Frame(doc.leftMargin, doc.bottomMargin, doc.width, doc.height, id='normal') | |
# 设置页面模板,在加载页面时先运行herder函数,在加载完页面后运行footer函数 | |
template = PageTemplate(id='test', frames=frame_footer, onPage=graphs.header, onPageEnd=graphs.footer) | |
doc.addPageTemplates([template]) | |
doc.build(content) | |
return buffer |