【Java】PDF模板生成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;
}
} }
四、下载与预览
事情到这还没走完业务流程,生成后还需要提供预览下载功能
最开始想到的办法是直接写下载接口,但是下载接口不一定对参数和客户端友好
1-1、存在的问题
存在我每次都头疼的问题:
1、一般下载都是Get请求,令牌,参数信息就直接暴露,而且拼接param也很麻烦,还要考虑编解码和特殊字符
get 请求对应到浏览器的处理是直接window.open(下载地址,’_blank‘)
2、如果参数不够传,就要考虑使用Post请求了,而Post请求在现在的前端工程里面基本是被axios接管的
响应的附件数据会被axios的拦截器拦截,取不到标准的响应code自动视为接口异常
当然,这个可以重写axios的拦截器,但是很变扭
1-2、更好的方案
基于当前的项目工程存在一个文件服务和API,同事有个更好的办法,就是不直接提供文件资源
先把生成的文件上传到文件服务,再经由文件服务返回的信息提供下载和预览的位置
文件服务有特定的上传规则,先得写一份临时文件到 /tmp上
再通过临时文件上传到文件服务指定的位置,完成后再删除临时文件
1-3、预览样例:
暂时没有做太复杂的逻辑,只要能打开就算赢
PC端就简单的多:
async openPdfView() {
let _data = this.dataList()
const res = await getCheckPdfWithData(_data)
window.open(res.data.viewUrl,'_blank')
},
H5 + 小程序端:
async openPdfPreview() {
const df = this.dataFormPack()
const data = await createPdfInfo(df)
console.log(data) const fileId = new Date().getTime() /* #ifdef H5 */
// window.open(data.viewUrl)
const downloadLink = document.createElement('a')
downloadLink.href = data.viewUrl
downloadLink.download = `demo-${fileId}.pdf` // 设置下载的文件名
downloadLink.target = '_blank'
downloadLink.style.display = 'none'
document.body.appendChild(downloadLink);
downloadLink.click();
downloadLink.remove(); // 下载之后把创建的元素删除
/* #endif */ /* #ifdef MP */
uni.openDocument({
filePath: data.viewUrl,
success: function(res) {
// console.log('打开文档成功');
}
});
/* #endif */
},
五、解决签名图片透明化问题:
在上面2-2改良封装的测试中,写入的图片是100%颜色的,但是实际上签名图片只需要签字即可
也就是无背景色,在HTML中默认是无色的,但是图片也没法查看
所以是这样,H5端和PC端同一颜色为白色:
PC端追加颜色配置
// 设置背景色为白色
ctx.fillStyle = 'white'
// 使用fillRect方法填充整个画布
ctx.fillRect(0, 0, canvas.width, canvas.height)
H5端用的uni的api默认就是白色,所以不用设置了
这样H5端和PC端预览可以看见明显的字迹:
而在生成PDF的时候,再对图像进行透明化转换处理,兼顾两边的需求了
Java 转换透明化的处理,这个方法追加到上面的工具类中
因为图片文件在内存的交互统一使用字节数组,不需要浪费资源写到磁盘上操作
正好看到有ByteArray的IO流,挺方便的
注意,是根据背景色匹配进行替换的,对图片的每一个像素点进行判断
/**
* @description 获取透明化背景的图片
* @author OnCloud9
* @date 2024/4/8 14:39
* @params
* @return
*/
@SneakyThrows
public static byte[] getTransparencyBackgroundImage(byte[] sourceByte) {
InputStream baIs = new ByteArrayInputStream(sourceByte);
BufferedImage buffImg = ImageIO.read(baIs);
int height = buffImg.getHeight();
int width = buffImg.getWidth();
/* 1 创建一个带有透明度的BufferedImage */
BufferedImage newBuffImg = new BufferedImage(width, height, BufferedImage.TYPE_INT_ARGB);
Graphics2D g2d = newBuffImg.createGraphics();
/* 2 设置渲染提示以改善图像质量 */
g2d.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON);
g2d.setRenderingHint(RenderingHints.KEY_INTERPOLATION, RenderingHints.VALUE_INTERPOLATION_BILINEAR);
g2d.setRenderingHint(RenderingHints.KEY_RENDERING, RenderingHints.VALUE_RENDER_QUALITY);
g2d.setRenderingHint(RenderingHints.KEY_ALPHA_INTERPOLATION, RenderingHints.VALUE_ALPHA_INTERPOLATION_QUALITY);
/* 3 将原始图像画到带有透明度的BufferedImage上 */
g2d.drawImage(buffImg, 0, 0, null);
g2d.dispose();
/* 4 遍历图片像素,将白色背景设置为透明 */
for (int x = 0; x < width; x++) {
for (int y = 0; y < height; y++) {
int rgb = newBuffImg.getRGB(x, y);
/* 假设背景色是白色 设置透明度 */
if (rgb == Color.WHITE.getRGB()) newBuffImg.setRGB(x, y, new Color(0, 0, 0, 0).getRGB());
}
}
/* 5 将新的透明化图片转换输出字节数组 */
ByteArrayOutputStream baOs = new ByteArrayOutputStream();
ImageIO.write(newBuffImg, "png", baOs);
byte[] imageBytes = baOs.toByteArray();
baOs.close();
baIs.close();
return imageBytes;
}
实现签字效果:
【Java】PDF模板生成PDF文档的更多相关文章
- Java根据模板生成Word文档
一,首先制作模板 1.先做一个Word文档, 2.打开Word,然后另存为*.xml文件 3.最后修改*.xml文件的后缀名为*.ftl 二,打开项目编辑器Idea,在pom文件中引入相关架包依赖(我 ...
- 根据PDF模板生成PDF文件(基于iTextSharp)
根据PDF模板生成PDF文件,这里主要借助iTextSharp工具来完成.场景是这样的,假如要做一个电子协议,用过通过在线填写表单数据,然后系统根据用户填写的数据,生成电子档的协议.原理很简单,但是每 ...
- JAVA Asponse.Word Office 操作神器,借助 word 模板生成 word 文档,并转化为 pdf,png 等多种格式的文件
一,由于该 jar 包不是免费的, maven 仓库一般不会有,需要我们去官网下载并安装到本地 maven 仓库 1,用地址 https://www-evget-com/product/564 ...
- java根据模板生成pdf
原文链接:https://www.cnblogs.com/wangpeng00700/p/8418594.html 在网上看了一些Java生成pdf文件的,写的有点乱,有的不支持写入中文字体,有的不支 ...
- Java利用模板生成pdf并导出
1.准备工作 (1)Adobe Acrobat pro软件:用来制作导出模板 (2)itext的jar包 2.开始制作pdf模板 (1)先用word做出模板界面 (2)文件另存为pdf格式文件 (3) ...
- java通过word模板生成word文档
介绍 上次公司项目需要一个生成word文档的功能,有固定的模板根据业务填充数据即可,由于从来没做过,项目也比较着急于是去网上找有没有合适的工具类,找了好几种,看到其中有freeMark模板生成比较靠谱 ...
- java使用freemarker 生成word文档
java 生成word文档 最近需要做一个导出word的功能, 在网上搜了下, 有用POI,JXL,iText等jar生成一个word文件然后将数据写到该文件中,API非常繁琐而且拼出来的 ...
- JAVAWEB使用FreeMarker利用ftl把含有图片的word模板生成word文档,然后打包成压缩包进行下载
这是写的另一个导出word方法:https://www.cnblogs.com/pxblog/p/13072711.html 引入jar包,freemarker.jar.apache-ant-zip- ...
- 使用freemarker模板生成word文档
项目中最近用到这个东西,做下记录. 如下图,先准备好一个(office2003)word文档当做模板.文档中图片.姓名.性别和生日已经使用占位符代替,生成过程中将会根据实际情况进行替换. 然后将wor ...
- JAVA Freemarker + Word 模板 生成 Word 文档 (普通的变量替换,数据的循环,表格数据的循环,以及图片的东替换)
1,最近有个需求,动态生成 Word 文当并供前端下载,网上找了一下,发现基本都是用 word 生成 xml 然后用模板替换变量的方式 1.1,这种方式虽然可行,但是生成的 xml 是在是太乱了,整理 ...
随机推荐
- ABC340
E 我们可以知道每一个点在每一轮加多少,具体如下: 假如现在操作的点的为 \(k\).那么所有的数都至少会加 \(\dfrac{A_k}{n}\).但是肯定有剩的,剩了 \(A_k \mod n\). ...
- n. Elasticsearch JAVA API操作
引言 Elasticsearch所支持的客户端连接方式有两种 Transport 连接 底层使用socket连接,用官方提供的TransPort客户端,网络IO框架使用的是netty Http连接(R ...
- 牛客网在线编程-语法篇-基础语法——C 语言解题集
前言 牛客网在线编程-语法篇-基础语法--C 语言解题集. 点击下方超链接跳转至对应编程题目,文章包含解析及源码. 01-基础语法 简单输出 BC1-Hello Nowcoder BC2-小飞机 基本 ...
- 在线XML格式化工具
在线XML格式化工具可以帮助您轻松格式化混乱的XML代码.只需将您的XML代码复制并粘贴到文本框中,工具会自动对代码进行缩进.对齐和添加空格等操作,使代码看起来更加整洁和专业. 在线XML格式化工具
- cerebro安装部署,es客户端优化界面
1.下载地址 https://github.com/lmenezes/cerebro/releases 2.下载cerebro-0.9.3.zip 运行bin/cerebro ,启动没有报错,并且命令 ...
- 浅拷贝、深拷贝与序列化【初级Java必需理解的概念】
浅拷贝 首先创建两个类,方便理解浅拷贝 @Data class Student implements Cloneable{ //年龄和名字是基本属性 private int age; private ...
- springboot3使用validation进行参数验证
前言 今天学习了使用validation整合springboot进行字段的校验,体验下来感觉很不错,有了validation可以省下一大堆控制器里面的数据校验,例如前端发送了一个请求到我们后端,请 ...
- Jenkins项目构建成功后,配置邮件
6.1 配置信息 6.1.1 发给多个收件人 邮件要发送给多个人,要使用[,]分割 6.1.2. 项目构建引用Editable Email Notification,设置tigger 在项目构建后,引 ...
- 《Programming from the Ground Up》阅读笔记:p1-p18
<Programming from the Ground Up>学习第1天,p1-18总结,总计18页. 一.技术总结 1.fetch-execute cycle p9, The CPU ...
- 脚本与数据的解耦 + Page Object模型
标签(空格分隔): 脚本与数据的解耦 + Page Object模型 测试脚本和数据的解耦 你现在已经掌握了一些基本的 GUI 自动化测试用例的实现方法,是不是正摩拳擦掌准备批量开发 GUI 自动化脚 ...