内容简述:预设docx文件模板,自动生成数据,可选多个不同模板合并,可区分封面追加页码,最终生成pdf
docx文件根据项目需要,从利于修改和易于扩展的角度出发,采用docxtpl包渲染的方式生成(就是把文件内预设的key替换成value,两个花括号中间就是key,网上教程很多易学)
基础功能实现:
- 根据模板文件渲染出docx文件列表
- docx合并
- docx转pdf
class PDFBuilder:
@staticmethod
def create_docx_replace_data(model_path: str, replace_dict: dict, output_path: str) -> None:
""" 替换模板数据,并创建新docx文档
:model_path: 模板文件路径
:replace_dict: 替换参数字典
:output_path: 生成文件路径(docx)
"""
from docxtpl import DocxTemplate
# ------------------------------
result_doc = DocxTemplate(model_path) # 模板对象
result_doc.render(replace_dict) # 替换数据
result_doc.save(output_path)
@staticmethod
def merge_docx(merged_path_list: list, output_path: str) -> None:
""" 按列表顺序,合并多个docx文档
:merged_path_list: 需合并的文件路径列表(docx)
:output_path: 生成文件路径(docx)
"""
from docx import Document
from docxcompose.composer import Composer
# ------------------------------
# 取列表第一个文件为主文档
merged_doc = Document(merged_path_list[0])
# 合并处理
if len(merged_path_list) > 1:
for each_docx_path in merged_path_list[1:]:
merged_doc.add_page_break() # 主文档添加分页符
cp = Composer(merged_doc) # 创建Composer对象,用于将子文档添加到主文档中
cp.append(Document(each_docx_path)) # 合并
merged_doc.save(output_path)
@staticmethod
def docx_to_pdf(docx_path, pdf_path):
""" 将docx文件转换为pdf
:docx_path: docx文件路径
:pdf_path: 生成pdf文件路径
"""
from docx2pdf import convert
# ------------------------------
convert(docx_path, pdf_path)
生成连续页码且封面无页码相对比较困难(docx的本质是zip压缩后的xml文件,和html类似,我们实际上看见的docx的每一页都是word或者wps渲染分页的结果,并不是文件存在就已经划分好了每一页)。
相对简单的处理方法是先将docx转成pdf文件(PDF相当于矢量图,完全确定了格式和样式),然后根据已经确认的pdf文件页数,生成一个页数相同且只存在页码的pdf文件,将之合并(类似ps的图层重叠)
还有其他思路,如通过代码取得docx的文件大小设置,包括且不仅限于页面长宽、页边距等,然后计算长度来分割页数(这方法实现起来肯定非常恶心我没研究)、把docx的模板用word预设好自动生成页码(该方法不适用于灵活设置有无封皮的情况,而且docx的diy封皮手动不好操作)...
还有一个处理方法是docx文件通过合并时加入分节符,来实现区分封皮无页码正文自动生成页码的功能。但是该方法普适性较低,理解相对困难,放在末尾代码中
图层重叠合并追加页码方法(基于旧版本PyPDF2):
@staticmethod
def add_page_numbers(update_pdf_path, start_page_num=0) -> None:
""" 给pdf文件追加页码
:update_pdf_path: pdf文件路径
:start_page_num: 页码'1'从pdf哪个页面下标开始(默认从0开始第一页)
"""
import os
from PyPDF2 import PdfFileWriter, PdfFileReader
from reportlab.pdfgen import canvas
# ------------------------------
pdf_reader = PdfFileReader(update_pdf_path)
pdf_writer = PdfFileWriter()
# 获取待修改PDF的总页数
total_pages = len(pdf_reader.pages)
# 使用reportlab创建只带有页码的临时PDF
for page_num in range(total_pages):
page_number = page_num + 1 - start_page_num
if page_number < 1:
continue
c = canvas.Canvas("temp_page.pdf") # canvas对象,用于添加页码
c.setFont("Helvetica", 10) # 自定义字体和字体大小
c.drawCentredString(300, 20, str(page_number)) # 在页面底部中间位置绘制页码
c.save()
# 将原始页面与新添加的页码页面合并
watermark = PdfFileReader("temp_page.pdf") # 临时PDF作为水印
page = pdf_reader.pages[page_num]
page.mergePage(watermark.pages[0])
pdf_writer.addPage(page)
# 保存合并以后的PDF文件,覆盖原文件
with open(update_pdf_path, "wb") as new_file:
pdf_writer.write(new_file)
# 删除临时文件
os.remove("temp_page.pdf")
页码追加(基于新版本PyPDF2):
@staticmethod
def add_page_numbers(update_pdf_path, start_page_num=0) -> None:
""" 给pdf文件追加页码
:update_pdf_path: pdf文件路径
:start_page_num: 页码'1'从pdf哪个页面下标开始(默认从0开始第一页)
"""
import os
from PyPDF2 import PdfWriter, PdfReader
from reportlab.pdfgen import canvas
# ------------------------------
pdf_reader = PdfReader(update_pdf_path)
pdf_writer = PdfWriter()
# 获取待修改PDF的总页数
total_pages = len(pdf_reader.pages)
# 使用reportlab创建只带有页码的临时PDF
for page_num in range(total_pages):
page_number = page_num + 1 - start_page_num
if page_number < 1:
continue
c = canvas.Canvas("temp_page.pdf") # canvas对象,用于添加页码
c.setFont("Helvetica", 10) # 自定义字体和字体大小
c.drawCentredString(300, 20, str(page_number)) # 在页面底部中间位置绘制页码
c.save()
# 将原始页面与新添加的页码页面合并
watermark = PdfReader("temp_page.pdf") # 临时PDF作为水印
page = pdf_reader.pages[page_num]
page.merge_page(watermark.pages[0])
pdf_writer.add_page(page)
# 保存合并以后的PDF文件,覆盖原文件
with open(update_pdf_path, "wb") as new_file:
pdf_writer.write(new_file)
# 删除临时文件
os.remove("temp_page.pdf")
继续优化,让文件始终在内存中操作;
应项目架构要求,采用分节符的方式增加页码(个人认为没太大意义,docx分节符、分页符和标注等注释不详细写了,不如图层合并);
完整的调用和测试:
# -*- coding: utf-8 -*-
# Author: otto
# Description: 模拟docx模板处理后转pdf流程
# 标准库 Required modules: os, io, shutil, tempfile
import os
import io
from io import BytesIO
import shutil # 高级文件操作
import tempfile # 创建临时文件和目录
# pip库 Required modules: docx, docxtpl, docxcompose, docx2pdf
from docx import Document
from docx.oxml.ns import nsdecls
from docx.oxml import parse_xml
from docx.enum.text import WD_PARAGRAPH_ALIGNMENT
from docx.enum.section import WD_SECTION_START
from docxtpl import DocxTemplate
from docxcompose.composer import Composer
from docx2pdf import convert
def action_pdf(file_name_list) -> None: # TODO 参数
""" 测试功能主流程
:file_name_list: 需要合并的docx文件名,后续更改为标识
:return: None
"""
model_path_dict = get_model_path_dict() # TODO 文件标识/路径取得
merge_docx_bytes_list = [] # 生成的docx二进制文件
for index, file_name in enumerate(file_name_list):
docx_model_path = model_path_dict[file_name] # 取得模板
render_dict = get_render_dict() # TODO 渲染数据取得
docx_bytes = PDFBuilder.create_docx_replace_data( # 渲染模板
model_path=docx_model_path,
render_dict=render_dict
)
merge_docx_bytes_list.append(docx_bytes)
# 封皮判断
if True: # TODO
cover_flg = True
merged_docx_bytes = PDFBuilder.merge_docx( # 按列表顺序合并docx生成页码
merged_bytes_list=merge_docx_bytes_list,
cover_flg=cover_flg
)
PDFBuilder.docx_to_pdf( # docx转pdf
docx_bytes=merged_docx_bytes
)
def get_model_path_dict() -> dict: # TODO 文件标识/路径取得
get_model_path_dict = {
'封皮.docx': own_path + '封皮.docx',
'空正文模板.docx': own_path + '空正文模板.docx',
'123.docx': own_path + '123.docx',
}
return get_model_path_dict
def get_render_dict() -> dict: # TODO 渲染数据取得
render_dict = {
'empty': '',
'code': 'abc',
}
return render_dict
class PDFBuilder:
@staticmethod
def create_docx_replace_data(model_path: str, render_dict: dict) -> BytesIO:
""" 渲染模板数据,并创建新docx文档
:model_path: 模板文件路径
:render_dict: 替换参数字典
:return: 内存文件(docx)
"""
docx_file = io.BytesIO()
doc_template = DocxTemplate(model_path)
doc_template.render(render_dict) # 渲染模板,替换数据
doc_template.save(docx_file)
return docx_file
@staticmethod
def merge_docx(merged_bytes_list: list, cover_flg: bool) -> BytesIO:
""" 按列表顺序,合并多个docx文档,自动生成页码
:merged_path_list: 需合并的文件路径列表(docx)
:cover_flg: 是否有封面(页脚处理)
:return: 内存文件(docx)
"""
merged_doc = Document(merged_bytes_list[0]) # 取列表第一个文件为主文档
# 封皮处理
if cover_flg:
new_section = merged_doc.add_section(WD_SECTION_START.CONTINUOUS) # 添加分节符(连续)
new_section.different_first_page = True # 首页不同
new_section.starting_number = 1 # 页码起始数字
footer = new_section.footer
footer.is_linked_to_previous = False # 将页脚与前一节的页脚不关联
# 合并处理
if len(merged_bytes_list) > 1:
for each_docx_bytes in merged_bytes_list[1:]:
merged_doc.add_page_break() # 主文档添加分页符
cp = Composer(merged_doc) # 创建Composer对象,用于将子文档添加到主文档中
cp.append(Document(each_docx_bytes)) # 合并
# 页脚处理
new_section = merged_doc.add_section(WD_SECTION_START.CONTINUOUS) # 添加分节符(连续)
new_section.different_first_page = True
footer = new_section.footer
footer_paragraph = footer.paragraphs[0]
footer.is_linked_to_previous = False # 将页脚与前一节的页脚不关联
# 连续页码
field_code = 'PAGE'
field = parse_xml(f'<w:fldSimple {nsdecls("w")} w:instr="{field_code}"/>')
run = footer_paragraph.add_run()
run._r.append(field)
footer_paragraph.alignment = WD_PARAGRAPH_ALIGNMENT.CENTER # 页脚居中对齐
# 数据保存
docx_file = io.BytesIO()
merged_doc.save(docx_file)
with open(merged_docx_path, 'wb') as file: # TODO 用于测试
file.write(docx_file.getvalue())
return docx_file
@staticmethod
def docx_to_pdf(docx_bytes) -> BytesIO:
""" 将docx文件转换为pdf
:docx_path: docx文件路径
:return: 内存文件(pdf)
"""
temp_docx = tempfile.NamedTemporaryFile(suffix='.docx', delete=False)
temp_pdf = tempfile.NamedTemporaryFile(suffix='.pdf', delete=False)
temp_docx.write(docx_bytes.getvalue())
temp_docx.close()
temp_pdf.close()
convert(temp_docx.name, temp_pdf.name)
with open(temp_pdf.name, 'rb') as file:
pdf_file = io.BytesIO()
shutil.copyfileobj(file, pdf_file)
os.remove(temp_docx.name)
os.remove(temp_pdf.name)
with open(output_pdf_path, 'wb') as file: # TODO 用于测试
file.write(pdf_file.getvalue())
return pdf_file
if __name__ == '__main__':
# 测试参数
own_path = 'C:\\Users\\11379\\Desktop\\'
output_pdf_path = own_path + '测试.pdf'
merged_docx_path = own_path + '测试.docx'
file_name_list = [
'封皮.docx',
'空正文模板.docx',
'123.docx',
]
action_pdf(file_name_list)
生活不易, 承接程序和各类论文辅导, 中英, java-python-vue-react..等等, 一帮大厂在职兄弟承包各种类型语言和程序, 详情+v