Reportlab生成PDF文件

这里做一个flowable类RotatedImage,除了将图片旋转45度外,还会显示画布边界。自己定义flowable类一定要有方法函数draw(),最好有wrap()函数。方法函数wrap()一般起两个作用,一个是判断当前页剩下的可用空间够不够放类内使用的flowable对象,另一个是声明当前flowable类生成的flowable的宽和高。

方法函数wrap()的函数头是固定的:

    def wrap(self, availWidth, availHeight)

availWidth, availHeight当前frame可用的宽高。

当类内添加其它类别的flowable时,需要在方法函数draw()里把这个flowable画到画布上,而画之前需要评估当前页能不能放得下这个flowable。如果类里没有定义方法函数wrap(),那么在方法函数draw()里调用这个flowable的drawOn(self, x坐标, y坐标)前必须使用该flowable的方法函数wrapOn(self,可用宽,可用高)评估是否有足够的空间放这个flowable。wrapOn()需要手动设置可用的宽高,而在类内定义方法函数wrap(self, availWidth, availHeight)可以直接使用自带的availWidth和awailHeight来得方便。如果在方法wrap()函数中调用flowable自己的wrap()函数评估过是否放得下该flowable,则在方法函数draw()中类内添加的flowable的drawOn()函数时,无需先使用wrapOn()评估是否放得下了。

生成文档对象最简便的方法就是使用SimpleDocTemplat():

from reportlab.platypus import SimpleDocTemplate
doc = SimpleDocTemplate("imageFlowable.pdf")

而后讲生成的各个flowable都放入一个列表,最后会用文档对象doc的方法函数build()将flowable列表统一生成pdf文档。SimpleDocTemplate类是做好的生成文档对象模版,使用时不用象使用BaseDocTemplate类那样要手工添加页面模版以及往页面模版里手工添加frame。前文说的"availWidth, availHeight是添加flowable对象时所在的当前frame可用的宽高"在这种情况下可以理解成所在页的可用宽高。在RotatedImage类里面,会特意显示availWidth和availHeight以观察可用空间高度的变化。

flowable类RotatedImage的实现如下:

class RotatedImage(Image):  # 父类Image
    """
    把图片旋转45度的flowable
    """
    def wrap(self, availWidth, availHeight):
        width, height = Image.wrap(self, availWidth, availHeight)
        # availWidth, availHeight当前页可用宽高
        # width, height是获得的调用时Image图片对象时给的宽高
        print('(availWidth, availHeight)-->', availWidth, availHeight,
              '\n(width, height)-->',width, height, '\n')
        # 打印可用宽高和实际宽高
        return width, height    # flowable对象声称的宽和高
    def draw(self):
        self.canv.rotate(45)    # 画布旋转45度
        self.canv.rect(0, 0, *self.canv._pagesize)  # 画布边界框
        Image.draw(self)        # 在旋转了45度的画布上画图片

RotatedImage类的父类是Image类,所以它继承了Image类所有的属性和方法函数,自己定义的方法函数wrap()和draw()覆盖了父类Image内的同名函数。

在主函数中生成4个RotatedImage类的flowable,依次添加到flowable列表story中,然后build进pdf文件imageFlowable.pdf中:

doc = SimpleDocTemplate("imageFlowable.pdf")  # 生成文档对象
story = []
img = RotatedImage('pythonabc.jpg', width=300, height=300)
# 生成RotatedImage类flowable,
story.append(img)    # 添加进flowable列表story
img = RotatedImage('pythonabc.jpg', width=200, height=200)
story.append(img)
img = RotatedImage('pythonabc.jpg', width=200, height=200)
story.append(img)
img = RotatedImage('pythonabc.jpg', width=200, height=200)
story.append(img)
doc.build(story)

RotatedImage('pythonabc.jpg', width=XXX, height=XXX)生成对象时硬性规定了图片的尺寸。运行结果输出到屏幕上的:

(availWidth, availHeight)--> 439.27559055118115 685.8897637795277 

(width, height)--> 300 300 

(availWidth, availHeight)--> 439.27559055118115 385.8897637795277 

(width, height)--> 200 200 

(availWidth, availHeight)--> 439.27559055118115 185.8897637795277 

(width, height)--> 200 200 

(availWidth, availHeight)--> 439.27559055118115 685.8897637795277 

(width, height)--> 200 200 

(availWidth, availHeight)--> 439.27559055118115 485.8897637795277 

(width, height)--> 200 200 

注意看可用的高度availHeight是不断递减的,递减的幅度由当前添加的flowable的wrap()返回值决定,规定的返回高度是调用时图片的高(return width, height):第一张300,其余三张均为200。

生成的pdf文件ImageFlowable.pdf:

 

可用高度是关键,最初可用高度为685.8897637795277 ,程序把这个值跟RotatedImage类生成的flowable自己声称的(我们定义的)的高300做比较,比300大,往上放。放第二个RotatedImage类生成的对象时,可用高度由原来的685.8897637795277 变成了385.8897637795277(减去了声称的高度300),还是够放,继续往上放。接下来可用的高度还剩185.8897637795277(385.8897637795277-200),要放的图片声称的高度为200,不够放,图片不能分割成两块,所以放弃这一页,试下一页(空白页)。可用空间又变成了685.8897637795277,够放,放上去……

可能有朋友注意到每个图片所占的位置并不是flowable对象所声称的尺寸。为了看得更清楚,在每个生成的RotatedImage类的flowable对象下面画条线,画线的操作由一个画线的类MCLine生成对象,紧随RotatedImage类对象之后添加到flowable列表story中。定制的flowable类MCLine的实现如下:

class MCLine(Flowable):
    """
    画条直线的flowable
    """
    def __init__(self, width):
        Flowable.__init__(self)     # 父类的初始化函数拿过来用
        self.width = width          # 自己添加的属性
    def draw(self):     # flowable类这个方法函数必须有
        """
        画直线
        """
        self.canv.line(0, 0, self.width, 0)     
        # 直接调用画布的方法函数line(),起始终了坐标做参数,直接往画布上画直线

现在主函数中每一个RotatedImage类生成的flowable后都跟一个画线MCLine类的flowable:

doc = SimpleDocTemplate("imageFlowable.pdf")
story = []

img = RotatedImage('pythonabcTransparentBackground.jpg', width=300, height=300)
story.append(img)

line = MCLine(450)
story.append(line)

img = RotatedImage('pythonabcTransparentBackground.jpg', width=200, height=200)
story.append(img)

line = MCLine(450)
story.append(line)

img = RotatedImage('pythonabcTransparentBackground.jpg', width=200, height=200)
story.append(img)

line = MCLine(450)
story.append(line)

img = RotatedImage('pythonabcTransparentBackground.jpg', width=200, height=200)
story.append(img)

line = MCLine(450)
story.append(line)

doc.build(story)

出来的效果:

 

每次往pdf里build flowable时都会判断当前页可用空间够不够放这个flowable:

  1. 可用高度大于声称高度,则在可用空间内分配出flowable声称的高度放flowable。声称的宽度高度可能并不准确,比如这里给的例子,flowable的方法函数wrap()规定返回的宽高(外界看来就是声称的宽高)为图片调用时的尺寸,可这个flowable除了有图片外,还有画布边界线(矩形),边界线无论宽高都远远大于声称的宽高。对此的处理是:可用宽度随便用,实在宽太多了,甚至多出了空白页的可用宽度,那就把多出的部分截掉;分配的高度以下部分不能用,以上部分随便用。所以每一个flowable都不会超过它之后画的那条直线,却可以向上向左右任意延伸,直到……向上向左右方向到达了可用的空间的“尽头”,“尽头”之外的部分被截掉。

  2. 若可用高度小于声称高度,则要看这个flowable是不是可以分离,比如Paragraph类的flowable可以分离,那就当前页剩下的空间放一部分,接下去的几页放剩下的部分。而象Image类的flowable不能被分离,则会越过当前页,尝试把它往下一页(空白页)放。若空白页的可用高度685.8897637795277大于flowable声称的高度,则皆大欢喜,该flowable被放进该空白页;若空白页可用高度小于flowable声称高度,则程序就会报错,错误提示是当前页可用高度不够放,尝试下一页还是不够放。