这里定义一个Flowable的子类IntroductionMaker,它生成的flowable,跟Paragraph、Spacer、PageBreak、Image、Table……这些reportlab帮我们定义好的类型一样使用,都可以被文档对象build进pdf文件。定义自己的Flowable类一个是指定父类是Flowable,另一个是定义的方法函数中要有draw()。 wrap()最好也有,因为它指明生成的flowable要占据多大的位置。
在自己定义的Flowable类内,可以直接往画布上画图写字,也可以使用其他的flowable,方法函数draw()负责把各个部分画上画布。如果添加的是flowable,就使用flowable自带的wrapOn()和drawOn()函数往画布上画。注意这俩函数必须成对使用,只使用drawOn()函数会报错,因为需要wrapOn()来了解添加的flowable的大小,进而影响其在画布上的位置。
IntroductionMaker类生成人物介绍模版,素材放在文件夹info内,每个人的介绍材料包括一个txt文本和一张图片:
文本有人物三方面的信息:姓名、评价、贡献,彼此之间用一个或多个空行分隔。
用这些信息生成人物介绍的pdf文档:
文档中的每一页都相同的页脚:
页脚部分用函数back()绘制,back()调用两个已经做好的Flowable:BoxyLine()和HandAnnotation()生成对象的drawOn()方法函数往画布上绘制带文本框的分割线和做指向动作的手。BoxyLine和HandAnnotation的定义放在文件myFlowable.py中,所以程序中要把它俩引入进来:
def back(canvas, document):
from myFlowable import BoxyLine, HandAnnotation # 手和带字块的线
# 页脚,每页都一样
BoxyLine(text="两弹一星元勋", width=120).drawOn(canvas, x=50, y=65)
# 直线+带字的矩形
HandAnnotation(size=30).drawOn(canvas, x=20, y=55) # 手
主函数中生成页脚部分的代码为:
# 使用BaseDocTemplate,要手动加入页面模版(addPageTemplates)
from reportlab.platypus import BaseDocTemplate
doc = BaseDocTemplate("introducation.pdf")
doc.addPageTemplates(PageTemplate(id='OneCol',
frames=Frame(doc.leftMargin, doc.bottomMargin,
doc.width, doc.height, id='normal'), onPage=back))
用SimpleDocTemplate也可以生成所有页都相同的页脚:
doc = SimpleDocTemplate("introducation.pdf")
doc.build(story, onFirstPage=back, onLaterPages=back)
每个人物介绍最开始部分都是人物评价、图片和姓名,排列和高度都相同:
这部分用自定义的IntroductionMaker生成的flowable来生成和画到画布上,贡献部分也在IntroductionMaker中生成段落类型的flowable,不画出来只是保存到属性里。
先用普通函数extractContent()把文件夹里的文本和图片整合进一个列表,列表里每个元素都是字典类型,关键字分别是姓名name、评价review、图片img和贡献contribution,对应的值是从文本文件中提取的内容和对应的图片。整合内容时会调用oneBlankLine()去除分隔姓名、评价和贡献的多余空格行。
在IntroductionMaker的方法函数createDocument()里,使用人物介绍的材料生成三个flowable:人物评价review生成Paragraph类型的flowable,图片生成Image类型的flowalbe,然后把这两个flowable装进一个表格里,生成表格类型的flowable后送入table属性属性。人物的姓名生成一个Paragraph类型的flowable送入title属性。
为了规整放进表格的图片的尺寸要调整,如果是jpg类型的图片直接调整图片对象的属性drawWidth和drawHeight就可以了,但这里的图片既有jpg又有png,所以引入第三方模块pillow的Image,在它的帮助下对图片尺寸进行调整:
from PIL import Image as IM # 与reportlab的Image重名,所以这里改名为IM
logo = IM.open(item['img'])
logo1 = logo.resize((int(150*logo.width/logo.height), 150))
保存进内存:
from io import BytesIO
fake_buf_file = BytesIO()
logo1.save(fake_buf_file, format='png')
然后才生成Image类型的flowable
logo = Image(fake_buf_file) # 添加人物图片
人物的贡献生成一个Paragraph类型的flowable也作为IntroductionMaker的对象属性(contribution)。为什么不把人物贡献也放进element呢?这跟往画布上画的方式有关系。自己建立的Flowable类必须有自己的draw()方法函数,flowable在这个draw()函数里先调用自己的wrapOn()函数确认自己所需要的宽和高,然后调用自己的drawOn()函数往画布上画,问题是drawOn()函数必须给出坐标,这个坐标是flowable(图形或段落或表格或……)的底部相对于坐标原点的x,y。无法保证个人贡献的内容多少是相同的,所以无法使用drawOn()往画布上画。用文档对象的方法函数build()进文档是顺次从上向下往文档里添加更适合这个flowable。
IntroductionMaker类初始化函数中的width和height属性是生成的flowable对象估算的宽和高:IntroductionMaker这个类内的表格包括两列,每列定义为230,那么表格宽度460,对象宽度属性就用460这个值;至于高度,表格行宽150,加上后面的空格和段落,估成200左右。在flowable类里往画布上画图形或flowable对象,图形和flowable的底端受限制,相对于画布左下角的坐标之上可以伸展,往下不行!这个部分下一个帖子会专门讨论。所以估值给200,往下无法逾越,往上只要有空间还是可以尽情发挥的。
初始化函数中部分属性和初值
def __init__(self, item):
"""item是字典类型,存放人物信息"""
……
self.width, self.height = 460, 200 # 可用宽高
self.contribution = Spacer(0,0)
self.table = Spacer(0,0)
self.title = Spacer(0,0)
self.createDocument(item)
# 这个方法函数负责把整理好的flowable赋给对象属性
接下来是flowable类里的wrap()函数,一般说来它起两个作用:一个是判断这一页剩下的地方够不够放flowable类生成的flowable对象,不够换下一页,要是空白页也不够放,报错;另外一个作用是返回flowable类生成的flowable对象的宽和高。在方法wrap()内判断该页是否有足够空间能放下flowable对象的好处是程序自动帮我们计算可用的宽高,如果放在draw()方法函数内在drawOn()用wrapOn()判断需要自己计算坐标,方法函数wrap():
def wrap(self, availWidth, availHeight):
# 两个作用:
# 1、判断当前页剩下的地方够不够放该flowable类生成的flowable对象
# 2、返回这个flowable占据的宽高,后面在添加的flowable会从这里给的高度之后开始
self.table.wrap(availWidth, availHeight)
self.title.wrap(availWidth, availHeight)
return self.width, self.height
如果flowable类里没有定义wrap()方法函数,则在draw()函数内用flowable的方法函数drawOn往画布上画前必须用wrapOn()函数判断是否有足够空间;定义的wrap()函数做了判断,则可直接使用,代码修改为:
def draw(self):
# 往画布上画,flowable必备的方法函数。
# 必须指定坐标,讨厌的事这个坐标是画上去的字或图或表格的底端的坐标
# 长度不确定的部分没办法用这种方法画进画布
self.table.drawOn(self.canv, *self.coord(-10, 150))
# 往画布上写表格
# 如果没有定义方法函数wrap()或warp()函数内没有判断这页是否有足够空间放flowable类产生的对象,
# 则drawOn()前必须有wrapOn(),配对使用
self.title.drawOn(self.canv, *self.coord(-30, 210))
# 往画布上写名字
完整代码及注释:
from reportlab.platypus import Image, Paragraph, Table, SimpleDocTemplate, \
BaseDocTemplate, PageTemplate, Frame, PageBreak, Spacer, Flowable
from myStyle import * # 自己定义的段落格式
from myFlowable import BoxyLine, HandAnnotation # 手和带字块的线
########################################################################
class IntroductionMaker(Flowable):
"""生成人物介绍的模版flowable"""
def __init__(self, item):
"""item是字典类型,存放人物信息"""
Flowable.__init__(self)
self.textStyle = styles['text'] # 主体文字格式
self.headingStyle = styles['heading'] # 标题文字格式
self.width, self.height = 460, 200 # 可用宽高
self.contribution = Spacer(0,0)
self.table = Spacer(0,0)
self.title = Spacer(0,0)
self.createDocument(item) # 这个方法函数负责把整理好的flowable赋给对象属性
def createDocument(self, item):
"""创建文档内容flowable
item是字典类型,关键字是review、img、name、contribution
review对应人物评价,img对应图片,生成段落和图片类型的flowable后放进一行两列的表格类的flowable
name对应人物姓名,contribute对应着所作贡献"""
p = Paragraph(item['review'], self.headingStyle)
# 人物评价生成段落型flowable
# 如果图片都是jpg,可以直接调整
# logo = Image(item['img']) # 添加人物图片
# logo.drawHeight = 150 # 按比例调整图片尺度,只对jpg有效,对png无效
# logo.drawWidth = 150 * logo.imageWidth/logo.imageHeight
# 否则就要用第三方模块pillow来挑战
from PIL import Image as IM
logo = IM.open(item['img'])
logo1 = logo.resize((int(150*logo.width/logo.height), 150))
from io import BytesIO
fake_buf_file = BytesIO()
logo1.save(fake_buf_file, format='png')
logo = Image(fake_buf_file) # 添加人物图片
data = [[p, logo]] # 准备表格数据
table = Table(data, colWidths = 230, rowHeights = 160)
from reportlab.lib import colors
table.setStyle([("VALIGN", (0, 0), (-1, -1), "MIDDLE"), # 上下居中
# ('GRID', (0, 0), (-1, -1), 0.5, colors.blue),
("ALIGN", (0, 0), (-1, -1), 'CENTRE')]) # 左右居中
self.table = table # 表格收进来
self.title = Paragraph(item['name'], self.headingStyle)
# 人物姓名生成段落行flowable收进来
# 人物贡献生成段落行flowable赋给对象的属性
self.contribution = Paragraph(item['contribution'], self.textStyle)
def coord(self, x, y):
"""
把针对左上角的坐标换算成以左下角为原点的坐标
"""
x, y = x, self.height - y
return x, y
def wrap(self, availWidth, availHeight):
# 两个作用:
# 1、判断当前页剩下的地方够不够放该flowable类生成的flowable对象
# 2、返回这个flowable占据的宽高,后面在添加的flowable会从这里给的高度之后开始
self.table.wrap(availWidth, availHeight)
self.title.wrap(availWidth, availHeight)
return self.width, self.height
def draw(self):
# 往画布上画,flowable必备的方法函数。
# 必须指定坐标,讨厌的事这个坐标是画上去的字或图或表格的底端的坐标
# 长度不确定的部分没办法用这种方法画进画布
self.table.drawOn(self.canv, *self.coord(-10, 150))
# 往画布上写表格
# 如果没有定义方法函数wrap()或warp()函数内没有判断这页是否有足够空间放flowable类产生的对象,
# 则drawOn()前必须有wrapOn(),配对使用
self.title.drawOn(self.canv, *self.coord(-30, 210))
# 往画布上写名字
def back(canvas, document):
# 页脚,每页都一样
BoxyLine(text="两弹一星元勋", width=120).drawOn(canvas, x=50, y=65)
# 直线+带字的矩形
HandAnnotation(size=30).drawOn(canvas, x=20, y=55) # 手
def oneBlankLine(cList, start):
"""从索引start开始遍历clist找空行,找到空行后继续扫描后续几行。
去除相邻的空行,只留下唯一空行,返回指向空行的索引
clist:
"""
indexL = start # 初始化指针
indexL = cList.index('\n', indexL) # 锁定空行的位置
while cList[indexL + 1] == '\n': # 继续扫描相邻元素,发现空行就一直删除下去,不是空行就退出循环
del cList[indexL + 1]
return indexL # 返回空行的索引
def extractContent(folder):
"""
把各个人物的信息从文件夹里提取出来,生成一个列表,列表里的每个元素是个字典,
记录一个人物信息。folder是存放资料的文件夹字符串
"""
from pathlib import Path
p = Path(folder) # 路径对象指向了存放资料的文件夹
surnameList = ['zhao', 'yao', 'deng', 'qian', 'guo'] # 定义要取谁的资料
founderList = []
for i in surnameList: # 遍历列表,开始整理每个人的资料
founder = dict() # 定义存放每个人资料的空字典变量
q = p.joinpath(i + '.txt').open(encoding='utf-8')
# 文件对象q指向文字资料
contentList = q.readlines() # 文字资料读入contentList,以回车符分隔
blankLine1 = oneBlankLine(contentList, 0)
# 调用函数找到第一个空行的位置,同时清除相邻空行
blankLine2 = oneBlankLine(contentList, blankLine1 + 1)
# 调用函数找到第二个空行的位置,同时清除相邻空行
founder['name'] = contentList[blankLine1 - 1] # 空行之前的元素是名字
# 两个空行之间的元素是评价,需要把这些元素连接起来。
# 每个元素最后一个字符都是'\n',需要换成<br/>,不然生成段落flowable不换行
founder['review'] = ''
for line in contentList[blankLine1 + 1:blankLine2]:
founder['review'] += line[:-1] + '<br/>'
# 第二个空行之后的元素都是贡献,也需要连接和替换'\n'
founder['contribution'] = ''
for line in contentList[blankLine2 + 1:]:
founder['contribution'] += line[:-1] + '<br/>'
for ext in ['.jpg', '.png']: # 图片的扩展名只列出两种,还可以列更多
img = p.joinpath(i + ext)
if img.exists(): # 哪个存在用哪个
founder['img'] = img
founderList.append(founder)
return founderList
if __name__ == "__main__":
contentList = extractContent('/Users/shiying/Documents/generatePDF/reportlab/officialTuitorial/info/')
# 把文件夹资料整理进列表contentList内,列表的元素是字典类型,记录每个人的信息
story = []
for item in contentList:
f = IntroductionMaker(item)
# 除了贡献,其他部分都用IntroductionMaker类型的flowable涵盖了
story.append(f)
story.append(Spacer(2, 60)) # 空白flowable
story.append(f.contribution) # 这个部分是人物贡献,生成段落添加
story.append(PageBreak()) # 分页符
doc = SimpleDocTemplate("introducation.pdf")
doc.build(story, onFirstPage=back, onLaterPages=back)
# 使用BaseDocTemplate,要手动加入页面模版(addPageTemplates)
# doc = BaseDocTemplate("introducation.pdf")
# doc.addPageTemplates(PageTemplate(id='OneCol',
# frames=Frame(doc.leftMargin, doc.bottomMargin,
# doc.width, doc.height, id='normal'), onPage=back))
# doc.build(story)
myFlowable.py里的内容:
from reportlab.platypus import Flowable, Paragraph
from reportlab.lib.colors import tan, green
from myStyle import *
class BoxyLine(Flowable):
"""
画一条线+一个文本框+文本
-----------------------------------------
| 文本内容 |
-----------
"""
# ----------------------------------------------------------------------
def __init__(self, x=50, y=-22, width=75, height=22, text=""):
# x,y指定位置;width,height文本框宽度;text文本内容
Flowable.__init__(self)
self.x = x
self.y = y
self.width = width
self.height = height
self.text = text
self.style = styles['plainText']
# ----------------------------------------------------------------------
def coord(self, x, y):
"""
http://stackoverflow.com/questions/4726011/wrap-text-in-a-table-reportlab
Helper class to help position flowables in Canvas objects
"""
x, y = x , self.height - y
return x, y
# ----------------------------------------------------------------------
def draw(self):
"""
Draw the shape, text, etc
"""
self.canv.rect(self.x, self.y, self.width, self.height)
self.canv.line(self.x-20, 0, 500, 0)
p = Paragraph(self.text, style=self.style)
p.wrapOn(self.canv, self.width, self.height)
p.drawOn(self.canv, *self.coord(self.x + 4, self.y+55))
class HandAnnotation(Flowable):
"""手"""
def __init__(self, angle = 0, xoffset=0, size=None, fillcolor=tan, strokecolor=green):
Flowable.__init__(self)
if size is None: size=200
self.fillcolor, self.strokecolor = fillcolor, strokecolor
self.xoffset = xoffset
self.size = size # 默认尺寸是200points
self.scale = size/(200)
self.angle = angle
def wrap(self, *args):
return (self.xoffset, self.size)
def hand(self, fill=0):
c = self.canv
(startx, starty) = (0, 0)
curves = [
(0, 2), (0, 4), (0, 8), # back of hand
(5, 8), (7, 10), (7, 14),
(10, 14), (10, 13), (7.5, 8), # thumb
(13, 8), (14, 8), (17, 8),
(19, 8), (19, 6), (17, 6),
(15, 6), (13, 6), (11, 6), # index, pointing
(12, 6), (13, 6), (14, 6),
(16, 6), (16, 4), (14, 4),
(13, 4), (12, 4), (11, 4), # middle
(11.5, 4), (12, 4), (13, 4),
(15, 4), (15, 2), (13, 2),
(12.5, 2), (11.5, 2), (11, 2), # ring
(11.5, 2), (12, 2), (12.5, 2),
(14, 2), (14, 0), (12.5, 0),
(10, 0), (8, 0), (6, 0), # pinky, then close
]
u = 10
p = c.beginPath()
p.moveTo(startx, starty)
ccopy = list(curves)
while ccopy:
[(x1, y1), (x2, y2), (x3, y3)] = ccopy[:3]
del ccopy[:3]
p.curveTo(x1 * u, y1 * u, x2 * u, y2 * u, x3 * u, y3 * u)
p.close()
c.saveState()
c.rotate(self.angle)
c.drawPath(p, fill=fill)
c.restoreState()
def draw(self):
self.canv.setLineWidth(6)
self.canv.setFillColor(self.fillcolor)
self.canv.setStrokeColor(self.strokecolor)
self.canv.translate(self.xoffset+self.size,0)
self.canv.scale(self.scale, self.scale)
self.hand(fill=1)
myStyle.py里有Paragraph用到的段落格式:
styles.add(ParagraphStyle("text", fontName='apple', fontSize=18,
leading=30, allowOrphans=0, allowWidows=0))
styles.add(ParagraphStyle("heading", fontName = 'lively',
fontSize=24, alignment=TA_CENTER,
leading=28,
))