docker+django+vue实例开发之二:后端api实现(三)

3)使用Reportlab生成pdf报表
本项目的一个功能需求就是要生成展示考核成绩的报表,考虑到Reportlab库的成熟性、文档齐全性和示例代码丰富性,选择它作为后端创建pdf文档的库。如何使用Reportlab的例子在Django的官方文档里就有,下面代码即是官方文档的示例:
from io import BytesIO
from reportlab.pdfgen import canvas
from django.http import HttpResponse
def some_view(request):

构建响应对象为pdf文件流

response = HttpResponse(content_type='application/pdf')
response['Content-Disposition'] = 'attachment; filename="somefilename.pdf"'

实例化内存io对象为pdf内容载体

buffer = BytesIO()

创建pdf文档画布,这是类似于绘图形式的pdf文档创建方式

p = canvas.Canvas(buffer)

绘制文档内容

p.drawString(100, 100, "Hello world.")
p.showPage()
p.save()

将pdf文档转化为字节流并导入到响应对象中返回给前端

pdf = buffer.getvalue()
buffer.close()
response.write(pdf)
return response
官方文档给出的示例提供了一个返回pdf文档流的后端接口的基本逻辑,即
a)创建内存io对象为pdf内容载体;
b)创建pdf文档对象并绘制内容。示例中给出的是画布方式,还有一种就是文档方式,文档方式将pdf所有内容看成一个列表,文字段落、图片、表格等对象都可以安照既定顺序加入到列表中输出为文档内容,每一个对象都包括内容和样式两部分。还可以通过创建类似于html语句的方式构建pdf模板,但本项目没有用到那么深入的机制。
c)从内存io对象中提取字节流导入到响应对象,然后返回给前端。
按照上述逻辑,项目实现了返回成绩报表 pdf文档流的后端接口,部分代码如下:
from django.http import HttpResponse
from rest_framework.decorators import api_view
from io import BytesIO
import json, base64
@api_view(['POST'])
def planReport(request, id):
……
#### pdf全文列表
elements = []
#### 报表标题
title = '%s成绩登记汇总表' % title
styles = getSampleStyleSheet()
title_style = styles['Title']
title_style.fontName = 'heiti'
title_style.fontSize = 24
##表头内容
tb_head_1 = ['序号', '姓名', '性别', '出生日期', '年龄', ]
tb_head_2 = ['', '', '', '', '', ]
span_list = []
col_src = len(tb_head_1)
for item in items_ser.data:
tb_head_2 += ['成绩', '标准', '评定(分)']
tb_head_1 += ['%s\n(%s)' % (item['name'], item['unit']), '', '']
span_list.append(('SPAN', (col_src, 0), (col_src + 2, 0)))
span_list.append(('BACKGROUND', (col_src, 1), (col_src, -1), colors.HexColor('#eef1f6'))) ####成绩填写列设定不同背景色
col_src += 3
tb_head_1 += ['总评', '', '备注']
tb_head_2 += ['分数', '评定', '']
span_list.append(('SPAN', (col_src, 0), (col_src + 1, 0)))
col_src += 2
span_list.append(('SPAN', (col_src, 0), (col_src, 1)))
##表头样式
tb_head_style = [
####文字样式
('FONTNAME', (0, 0), (-1, 1), 'heiti'), # 字体
('FONTSIZE', (0, 0), (-1, 1), 11), # 字体大小
('ALIGN', (0, 0), (-1, 1), 'CENTER'), # 对齐
('VALIGN', (0, 0), (-1, 1), 'MIDDLE'), # 对齐
####线条
('GRID', (0, 0), (-1, -1), 1, colors.black), ####单元格线条
('BOX', (0, 0), (-1, -1), 2, colors.black), ####边框
####合并项
('SPAN', (0, 0), (0, 1)),
('SPAN', (1, 0), (1, 1)),
('SPAN', (2, 0), (2, 1)),
('SPAN', (3, 0), (3, 1)),
('SPAN', (4, 0), (4, 1)),
('SPAN', (5, 0), (5, 1)),
('SPAN', (6, 0), (6, 1)),
]
tb_head_style += span_list

####根据表头计算文档尺寸以适配不同打印纸张要求
table_width = (const_col_width + 2) * len(tb_head_1)  ####加上线条宽度
page_width, page_height, table_width_result = getPageSizeByContent(table_width, 30)
colWidth = const_col_width
if table_width > table_width_result:
    table_width = table_width_result
    colWidth = table_width_result / len(tb_head_1)
content_font_size = const_font_size

####每个部门有单独的标题和表头,填充表格内容
for department in departments:

……
####多次添加标题
elements.append(Paragraph(title, title_style))
####多次添加单位和日期,利用表格实现对齐
tb_data = [['填报单位:%s' % department, '', '', '', '填报日期:'], ]
tb_style = [
('FONTNAME', (0, 0), (-1, 0), 'heiti'), # 字体
('FONTSIZE', (0, 0), (-1, 0), 11), # 字体大小
('ALIGN', (0, 0), (-1, 0), 'CENTER'), # 对齐
('VALIGN', (0, 0), (-1, 0), 'MIDDLE'), # 对齐
]
tb = Table(tb_data, colWidths=table_width / len(tb_data[0]), style=tb_style, rowHeights=30)
elements.append(tb)
####分批添加表格内容
tb_body = []
####添加表头
tb_body.append(tb_head_1)
tb_body.append(tb_head_2)
##添加表格内容
tb_content_style = [
####文字样式
('FONTNAME', (0, 2), (-1, -1), 'kaiti'), # 字体
('FONTSIZE', (0, 2), (-1, -1), content_font_size), # 字体大小
('ALIGN', (0, 2), (-2, -1), 'CENTER'), # 除了备注都居中对齐
('ALIGN', (-1, 2), (-1, -1), 'LEFT'),
('VALIGN', (0, 2), (-1, -1), 'MIDDLE'), # 对齐
]
tb_body += person_record ####表格内容,通过数据库查询获得
tb_head_style += tb_content_style ####表格内容样式
tb = Table(tb_body, colWidths=colWidth, style=tb_head_style, rowHeights=35)
elements.append(tb)

    ####多次添加主考人
    tb_data = [['', '', '', '', '主考人:'], ]
    tb_style = [
        ('FONTNAME', (0, 0), (-1, 0), 'heiti'),  # 字体
        ('FONTSIZE', (0, 0), (-1, 0), 11),  # 字体大小
        ('ALIGN', (0, 0), (-1, 0), 'CENTER'),  # 对齐
        ('VALIGN', (0, 0), (-1, 0), 'MIDDLE'),  # 对齐
    ]
    tb = Table(tb_data, colWidths=table_width / len(tb_data[0]), style=tb_style, rowHeights=30)
    elements.append(tb)

    ####添加分页
    elements.append(PageBreak())

####构造文档对象
####将内存文件对象传入doc
buffer = BytesIO()
doc = SimpleDocTemplate(buffer, pagesize=(page_width, page_height), topMargin=15, bottomMargin=15)
####将数据和格式写入pdf
doc.build(elements)

####生成文档
file_name = '%s.pdf' % title
response = HttpResponse(content_type='application/pdf', charset='UTF-8')
response['Content-Disposition'] = 'attachment; filename=%s' % file_name

###将内存文件对象写入response,为配合前端插件显示,转化为base64方式
pdf = base64.b64encode(buffer.getvalue())
response.write(pdf)
buffer.close()
return response

在上述代码示例中,创建了一个包含多页表格的报表pdf文档数据,转换成base64字符传递给前端,概括起来有以下4点:
a)Reportlab的文档内容都串行的包含在一个列表中,每个元素即代表一块数据,例如“标题、第一段文字、第一个表格”等等,代码编制时需要将这些内容按顺序添加到列表中,再通过doc.build构建文档;
b)每个元素都有单独构建的方法,例如Paragraph()构建段落,Table()构建表格,构造函数通常包括“内容+格式”两方面;
c)要注意文档页面尺寸的选取,为报表打印做准备,最好通过计算内容项中需要涉及页面尺寸布局的元素尺寸,来决定页面尺寸;
d)表格构造过程中,形如:'FONTNAME', (0, 0), (-1, 0), 'heiti'的代码,中间两个元组参数表示单元格位置,分别是(列起始位置,行起始位置), (列终止位置,行终止位置),索引都是从0开始算起,-1表示最后一个行或列的位置。

本项目前端显示pdf文档用的pdf.js插件,由于前端对文档浏览没有要求,简单起见采用了最简单的配置方式,即将pdf.js解压放到static文件中,然后将后端api传递过来的pdf文件流转化为临时url,然后通过"static/pdf/web/viewer.html?file=" + url和window.open函数,重新打开一个窗口,显示pdf文件内容。配合vue-admin使用时需要注意以下几个方面:
a)后端异步接口传递过来的pdf文件流需要在fetch.js文件中放开response对象的拦截权限,即
if (response.headers['content-type'] === 'application/pdf'){
return response
}
b)接口返回数据使用window.URL.createObjectURL函数构造成临时url传递给static/pdf/web/viewer.html,其中,createObjectURL仅接受file或blob对象,不接受原始数据,因而需要先转换数据类型,否则会报错,即
var binaryData = [];
binaryData.push(response.data);
let url = window.URL.createObjectURL(new Blob(binaryData, {type: "application/pdf"}))
c)上述操作对于pdf.js 2.0这种直接用view.js的方式,只能支持英文,后端传过来的数据流中若包含中文会出现乱码,需要额外操作。首先,后端不能直接传回pdf文件流,需要将其转化为base64编码后传回。其次,前端解析时需要将解析后的数据转化为unit8Array数组,这样才能显示中文,即
var binaryData = [];
var bstr = window.atob(response.data) // // base64解码
var len = bstr.length
var tmparray = new Uint8Array(new ArrayBuffer(len));
for (var i = 0; i < len; i++) {
tmparray[i] = bstr.charCodeAt(i);
}
binaryData.push(tmparray)
let url = window.URL.createObjectURL(new Blob(binaryData, {type: "application/pdf"}))
let pdfurl = "static/pdf/web/viewer.html?file=" + url
window.open(pdfurl)

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 194,457评论 5 459
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 81,837评论 2 371
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 141,696评论 0 319
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 52,183评论 1 263
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 61,057评论 4 355
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 46,105评论 1 272
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 36,520评论 3 381
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 35,211评论 0 253
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 39,482评论 1 290
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 34,574评论 2 309
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 36,353评论 1 326
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 32,213评论 3 312
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 37,576评论 3 298
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 28,897评论 0 17
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 30,174评论 1 250
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 41,489评论 2 341
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 40,683评论 2 335

推荐阅读更多精彩内容