Reportlab生成PDF文件

这里定义一个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,
             ))