一、需求背景
客户要求一份文书,文书内容有一些表单项,例如:
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