python-docx转pdf且生成连续页码(封面无页码)

内容简述:预设docx文件模板,自动生成数据,可选多个不同模板合并,可区分封面追加页码,最终生成pdf

docx文件根据项目需要,从利于修改和易于扩展的角度出发,采用docxtpl包渲染的方式生成(就是把文件内预设的key替换成value,两个花括号中间就是key,网上教程很多易学)

基础功能实现:

  1. 根据模板文件渲染出docx文件列表
  2. docx合并
  3. 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

  • 2
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值