首页 > 编程语言 >【Java】PDF模板生成PDF文档

【Java】PDF模板生成PDF文档

时间:2024-04-04 19:33:06浏览次数:28  
标签:Java pdf private import PDF 文本 模板 图片

一、需求背景

客户要求一份文书,文书内容有一些表单项,例如:

1、基本的是和否 (单选框或复选框)

2、备注内容(纯文本信息)

3、单位,机构组织,人员,字典项(下拉选择)

4、用户数字签名(图片信息)

文书的模板是固定不变的,只需要把上述信息写入模板中生成即可

这个模板不是动态的,动态模板是表单数据决定文档内容,这个相反,文档内容决定表单数据

 

二、技术选型方案

同事给出的方案是自己用Java代码画一份出来,完全使用代码画模板,然后填充数据

但是就给我两天都没有的时间,我自己是否定了这个方案,第一时间不够,第二是我基本上没有用过pdf文档的API操作

我的想法是,模板固定的,那肯定只要丢参数就好了,类型就两种,一个文本,一个图片

 

三、落地实现

那么这个方案能不能行呢,网上找了找还是挺多的,看起来能行

参考这个文档,我有了一些初步了解

https://blog.csdn.net/u011628753/article/details/131377253

1-1、需要有一个可以编辑PDF,填写表单域的软件

市面上主流的编辑软件我都一个个踩坑了,免费的都会添加水印,如果对文档水印没有特别要求

可以使用 【万兴PDF】【福昕PDF】【WPS自带】,看UI感觉还是万兴的更好

但是要不夹带私货,高保真文档原貌,还是老老实实用 Adobe Acrobat DC Pro吧

1-2、关于Acrobat DC软件本身

Acrobat DC 普通版没有这个功能,一定要Pro版本才支持

我本来心想这破逼软件应该挺好找的,没想到费老大劲才找到最近2019版本的

不记得在哪个链接找到了,我自己的度盘备份了一份,分享出来

链接:https://pan.baidu.com/s/1A8TdcfkFcuh7ngQg41zBRA?pwd=ez0k 
提取码:ez0k 
--来自百度网盘超级会员V6的分享

解压后在目录中双击setup.exe进行安装即可,是已经破解好的

1-3、如何设置PDF表单

先把模板文件用Acrobat DC打开

找到更多工具 - 【准备表单】

 

 第一次进入之后Adobe会自动对空白填写的位置创建表单项

如果部分位置没有自动创建,可以右键手动设置表单项

 表单项统一使用文本域,图片也是通过文本域写入(后面细节再说)

1-4、文本类型的设置

简单摸索之后,主要的设置是这几个内容

 名称和锁定的作用

 字体信息设置

文本排版设置

1-5、图片类型的设置

对于图片的设置,只需要调整文本域的宽高即可

不要怀疑,图片也是用文本域写入的

 

 

2-1、Java 关于PDF操作的一些依赖库

在工程里面找依赖太麻烦了,刚找的一篇快速定位依赖的IDEA插件:

https://blog.csdn.net/Dream_Weave/article/details/131383822

  

我发现这俩就满足了

<dependency>
    <groupId>com.itextpdf</groupId>
    <artifactId>itextpdf</artifactId>
    <version>${itextpdf.version}</version>
</dependency>
<dependency>
    <groupId>com.itextpdf</groupId>
    <artifactId>itext-asian</artifactId>
    <version>${itext-asian.version}</version>
</dependency>

版本号:

<itextpdf.version>5.5.13.3</itextpdf.version>
<itext-asian.version>5.2.0</itext-asian.version>

 

2-2、封装改良

网上内容编写的API都没有进行简单封装,不能满足业务开发的需求,需要自己封装改良

一、首先得有个基本参数对象,一个表单项即一个对象

@Data
@AllArgsConstructor
@NoArgsConstructor
@Builder
public static final class PdfFormMap {
    /* 对应的表单域键名 */
    private String fieldKey;
    /* 对应类型, 1 文本  2 图片 */
    private PdfFieldType fieldType;
    /* 文本值 */
    private String text;
    /* 图片内容 */
    private byte[] imageCtx;
    /* 自定义宽高 */
    private Float customWidth;
    private Float customHeight;
}

二、明确参数类型

目前只有文本和图片两种类型,用枚举来准确描述类型

@Getter
public static enum PdfFieldType {
    TEXT("文本", 1),
    IMAGE("图片", 2);
    private final String name;
    private final Integer type;
    PdfFieldType(String name, Integer type) {
        this.name = name;
        this.type = type;
    }
}

  

三、方法实现:

业务逻辑只需要包装表单项的数据即可

@SneakyThrows
public static void writeFormDataToPdf(PdfReader reader, PdfStamper pdfStamper, List<PdfFormMap> formMapList) {
    AcroFields acroFields = pdfStamper.getAcroFields();
    // 需要设置字体,否则中文无法被输出到PDF上,这里就不处理这个逻辑了
    // BaseFont.NOT_EMBEDDED 不把字体文件嵌入pdf,但是系统没有该字体将无法正常查看...
    // BaseFont bf = BaseFont.createFont("C:\\Windows\\Fonts\\STFANGSO.TTF", BaseFont.IDENTITY_H, BaseFont.NOT_EMBEDDED);
    // acroFields.addSubstitutionFont(bf);
    /* 获取每页的内容字节对象, 一页对应一个内容字节对象, 因为需要把图片写入,这里先用list保存每一页的对象 */
    int totalPage = reader.getNumberOfPages();
    List<PdfContentByte> pageCttByteList = new ArrayList<>(totalPage);
    /* 定位下标从1开始计算 */
    for (int pageIdx = 1; pageIdx <= totalPage; pageIdx++) {
        PdfContentByte pageContentByte = pdfStamper.getOverContent(pageIdx);
        pageCttByteList.add(pageContentByte);
    }
    for (PdfFormMap formMap : formMapList) {
        String fieldKey = formMap.getFieldKey();
        /* 判断这个表单项在模板是否存在,不存在的表单项不设置 */
        AcroFields.Item fieldItem = acroFields.getFieldItem(fieldKey);
        if (Objects.isNull(fieldItem)) continue;
        /* 根据类型设置对应的值 */
        PdfFieldType fieldType = formMap.getFieldType();
        switch (fieldType) {
            case TEXT:
                acroFields.setField(fieldKey, formMap.getText());
                break;
            case IMAGE:
                /* 图片存在于多个位置,每一页的每一个位置都有自己的矩阵信息,定位,宽高 */
                List<AcroFields.FieldPosition> positions = acroFields.getFieldPositions(fieldKey);
                /* 读取图片字节重新转换成PDF图片对象 */
                Image image = Image.getInstance(formMap.getImageCtx());
                boolean usingCustomSetting = Objects.nonNull(formMap.customHeight) && Objects.nonNull(formMap.customWidth);
                for (AcroFields.FieldPosition position : positions) {
                    /* 获取具体要输出的那一页的内容字节对象 */
                    PdfContentByte contentByte = pageCttByteList.get(position.page - 1);
                    /* 图片域的矩阵对象信息 */
                    Rectangle rectangle = position.position;
                    float x = rectangle.getLeft();
                    float y = rectangle.getBottom();
                    /* 是否使用自定义宽高设置, 具体如何没有尝试过... 逻辑待优化 */
                    if (usingCustomSetting) contentByte.addImage(image, formMap.customWidth, 0F, 0F, formMap.customHeight, x, y);
                    /* 使用表单域设置的宽高进行填充 */
                    else contentByte.addImage(image, rectangle.getWidth(), 0F, 0F, rectangle.getHeight(), x, y);
                }
                break;
            default:
                continue;
        }
    }
}

  

四、测试Demo

@SneakyThrows
public static void main(String[] args) {
    /* 模板路径,输出路径,图片路径 */
    String templatePath = "C:\\Users\\Administrator\\Desktop\\pdf-test\\template-new.pdf";
    String outputPath = "C:\\Users\\Administrator\\Desktop\\pdf-test\\template-output.pdf";
    String orgSignPath = "C:\\Users\\Administrator\\Desktop\\pdf-test\\signature-1.png";
    
    /* 创建资源操作对象 读取,输出,表单操作 */
    PdfReader pdfReader = new PdfReader(templatePath);
    OutputStream outputStream = Files.newOutputStream(Paths.get(outputPath));
    PdfStamper pdfStamper = new PdfStamper(pdfReader, outputStream);
    
    /* 简单设置两个表单项,文本,和图片 */
    List<PdfTemplateUtil.PdfFormMap> pdfFormMaps = new ArrayList<>();
    PdfTemplateUtil.PdfFormMap orgName = PdfTemplateUtil.PdfFormMap
            .builder()
            .fieldKey("orgName")
            .fieldType(PdfTemplateUtil.PdfFieldType.TEXT)
            .text("被检查单位xxxx")
            .build();
    PdfTemplateUtil.PdfFormMap orgSignImg = PdfTemplateUtil.PdfFormMap
            .builder()
            .fieldKey("orgSignImg")
            .fieldType(PdfTemplateUtil.PdfFieldType.IMAGE)
            .imageCtx(FileUtil.readBytes(orgSignPath))
            .build();
    pdfFormMaps.add(orgName);
    pdfFormMaps.add(orgSignImg);

    /* 将数据写入pdf中 */
    PdfTemplateUtil.writeFormDataToPdf(pdfReader, pdfStamper, pdfFormMaps);
    /* 锁定表单和资源释放 */
    pdfStamper.setFormFlattening(true);
    pdfStamper.close();
    outputStream.close();
    pdfReader.close();
}

  

文本的没啥好展示的(记得追加字体配置逻辑)

主要是图片这块,图片会按照文本域的宽高渲染,本身宽高是不会改变的(之前的Excel写入图片同理)

 

 五、工具类完整代码:

package jnpf.util;

import com.itextpdf.text.Image;
import com.itextpdf.text.Rectangle;
import com.itextpdf.text.pdf.AcroFields;
import com.itextpdf.text.pdf.PdfContentByte;
import com.itextpdf.text.pdf.PdfReader;
import com.itextpdf.text.pdf.PdfStamper;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.*;
import lombok.extern.slf4j.Slf4j;

import javax.servlet.http.HttpServletResponse;
import java.io.File;
import java.util.ArrayList;
import java.util.List;
import java.util.Objects;

/**
 * @description Pdf表单域写入工具类
 * @author OnCloud9
 * @date 2024/4/3 14:17
 * @params
 * @return
 */
@Slf4j
public class PdfTemplateUtil {


    @SneakyThrows
    public static void writeFormDataToPdf(PdfReader reader, PdfStamper pdfStamper, List<PdfFormMap> formMapList) {
        AcroFields acroFields = pdfStamper.getAcroFields();
        // 需要设置字体,否则中文无法被输出到PDF上,这里就不处理这个逻辑了
        // BaseFont.NOT_EMBEDDED 不把字体文件嵌入pdf,但是系统没有该字体将无法正常查看...
        // BaseFont bf = BaseFont.createFont("C:\\Windows\\Fonts\\STFANGSO.TTF", BaseFont.IDENTITY_H, BaseFont.NOT_EMBEDDED);
        // acroFields.addSubstitutionFont(bf);

        /* 获取每页的内容字节对象, 一页对应一个内容字节对象, 因为需要把图片写入,这里先用list保存每一页的对象 */
        int totalPage = reader.getNumberOfPages();
        List<PdfContentByte> pageCttByteList = new ArrayList<>(totalPage);
        /* 定位下标从1开始计算 */
        for (int pageIdx = 1; pageIdx <= totalPage; pageIdx++) {
            PdfContentByte pageContentByte = pdfStamper.getOverContent(pageIdx);
            pageCttByteList.add(pageContentByte);
        }

        for (PdfFormMap formMap : formMapList) {
            String fieldKey = formMap.getFieldKey();
            /* 判断这个表单项在模板是否存在,不存在的表单项不设置 */
            AcroFields.Item fieldItem = acroFields.getFieldItem(fieldKey);
            if (Objects.isNull(fieldItem)) continue;

            /* 根据类型设置对应的值 */
            PdfFieldType fieldType = formMap.getFieldType();
            switch (fieldType) {
                case TEXT:
                    acroFields.setField(fieldKey, formMap.getText());
                    break;
                case IMAGE:
                    /* 图片存在于多个位置,每一页的每一个位置都有自己的矩阵信息,定位,宽高 */
                    List<AcroFields.FieldPosition> positions = acroFields.getFieldPositions(fieldKey);
                    /* 读取图片字节重新转换成PDF图片对象 */
                    Image image = Image.getInstance(formMap.getImageCtx());
                    boolean usingCustomSetting = Objects.nonNull(formMap.customHeight) && Objects.nonNull(formMap.customWidth);
                    for (AcroFields.FieldPosition position : positions) {
                        /* 获取具体要输出的那一页的内容字节对象 */
                        PdfContentByte contentByte = pageCttByteList.get(position.page - 1);
                        /* 图片域的矩阵对象信息 */
                        Rectangle rectangle = position.position;
                        float x = rectangle.getLeft();
                        float y = rectangle.getBottom();
                        /* 是否使用自定义宽高设置, 具体如何没有尝试过... 逻辑待优化 */
                        if (usingCustomSetting) contentByte.addImage(image, formMap.customWidth, 0F, 0F, formMap.customHeight, x, y);
                        /* 使用表单域设置的宽高进行填充 */
                        else contentByte.addImage(image, rectangle.getWidth(), 0F, 0F, rectangle.getHeight(), x, y);
                    }
                    break;
                default:
                    continue;
            }
        }
    }

    @Data
    @AllArgsConstructor
    @NoArgsConstructor
    @Builder
    public static final class PdfFormMap {
        /* 对应的表单域键名 */
        private String fieldKey;
        /* 对应类型, 1 文本  2 图片 */
        private PdfFieldType fieldType;
        /* 文本值 */
        private String text;
        /* 图片内容 */
        private byte[] imageCtx;
        /* 自定义宽高 */
        private Float customWidth;
        private Float customHeight;
    }

    @Getter
    public static enum PdfFieldType {
        TEXT("文本", 1),
        IMAGE("图片", 2);

        private final String name;
        private final Integer type;

        PdfFieldType(String name, Integer type) {
            this.name = name;
            this.type = type;
        }
    }

}

  

 

标签:Java,pdf,private,import,PDF,文本,模板,图片
From: https://www.cnblogs.com/mindzone/p/18114526

相关文章

  • javascript常见100问|前端基础知识|问ajax-fetch-axios-区别请用 XMLHttpRequestfetch
    00-开始前端基础知识HTMLCSSJSHTTP等基础知识是前端面试的第一步,基础知识不过关将直接被拒。本章将通过多个面试题,讲解前端常考的基础知识面试题,同时复习一些重要的知识点。为何要考察扎实的前端基础知识,是作为前端工程师的根本。基础知识能保证最基本的使用,即招聘......
  • P2613 【模板】有理数取余
    原题链接题解然后就变成了求解同余方程code#definelllonglong#include<bits/stdc++.h>constllmod=19260817;usingnamespacestd;llx,y;llc;lla,b;inlinevoidread(ll&x){x=0;llflag=1;charc=getchar();while(c<'0......
  • 《手把手教你》系列技巧篇(六十九)-java+ selenium自动化测试 - 读取csv文件(详细教程)
    1.简介 在实际测试中,我们不仅需要读取Excle,而且有时候还需要读取CSV类的文件。如何去读取CSV的文件,宏哥今天就讲解和分享一下,希望对你能够有所帮助。前面介绍了如何读取excel文件,本篇介绍如何读取vsc文件,同样需要用到第三方lib去处理读取csv文件的数据。2.什么是CSV?csv是【......
  • 前端学习<四>JavaScript基础——03-常量和变量
    常量(字面量):数字和字符串常量也称之为“字面量”,是固定值,不可改变。看见什么,它就是什么。常量有下面这几种:数字常量(数值常量)字符串常量布尔常量自定义常量数字常量数字常量非常简单,直接写数字就行,不需要任何其他的符号。既可以是整数,也可以是浮点数。例如: //不......
  • 《手把手教你》系列技巧篇(七十)-java+ selenium自动化测试-Java中如何读取properties配
     1.简介Java自动化测试开发中,需要将一些易变的配置参数放置再XML配置文件或者properties配置文件中。然而XML配置文件需要通过DOM或SAX方式解析,而读取properties配置文件就比较容易。因此今天宏哥讲解和分享如何读取properties配置文件的内容。2.properties文件......
  • Java | Leetcode Java题解之第10题正则表达式匹配
    题目:题解:classSolution{publicbooleanisMatch(Strings,Stringp){intm=s.length();intn=p.length();boolean[][]f=newboolean[m+1][n+1];f[0][0]=true;for(inti=0;i<=m;++i){......
  • Java | Leetcode Java题解之第9题回文数
    题目:题解:classSolution{publicbooleanisPalindrome(intx){//特殊情况://如上所述,当x<0时,x不是回文数。//同样地,如果数字的最后一位是0,为了使该数字为回文,//则其第一位数字也应该是0//只有0满足这一属......
  • Java | Leetcode Java题解之第8题字符串转换整数atoi
    题目:题解:classSolution{publicintmyAtoi(Stringstr){Automatonautomaton=newAutomaton();intlength=str.length();for(inti=0;i<length;++i){automaton.get(str.charAt(i));}retur......
  • Java | Leetcode Java题解之第7题整数反转
    题目:题解:classSolution{publicintreverse(intx){intrev=0;while(x!=0){if(rev<Integer.MIN_VALUE/10||rev>Integer.MAX_VALUE/10){return0;}intdigit=x......
  • 第01章 JavaWeb基础
    B/S架构是Brower/Server的缩写,即浏览器/服务器结构。在这种结构中,客户端不需要开发任何用户界面,而是通过浏览器(如IE,Firefox,Chrome,Safari等等)向服务器发送请求,由服务器进行处理后将响应结果返回浏览器,最后浏览器将结果内容展示出来。我们写的JavaWeb程序是在服务器端运行的。W......