手摸手系列之 Java 通过 PDF 模板生成 PDF 功能

ops/2025/1/20 13:58:10/

集团 SaaS 平台目前需要实现导出 PDF 格式的电子委托协议功能。业务方已经提供了一个现成的 PDF 文件作为参考。针对这一需求,我们有两个可行的方案:

  1. 完全代码生成:根据 PDF 文件的外观,完全通过代码动态生成 PDF 文件。
  2. 模板填充:将现有的 PDF 文件作为模板,仅需在代码中填充真实数据即可生成最终的 PDF 文件。

从实现效率和开发速度的角度来看,方案二(模板填充)无疑是更优的选择。它不仅能够大幅减少开发工作量,还能确保生成的 PDF 文件与业务方提供的模板完全一致,避免样式偏差。接下来,我们将重点探讨如何通过模板填充的方式实现这一功能。

一、PDF 模板制作

首先通过 PDF 编辑器制作 PDF 模板,这里我选用 Adobe Acrobat Pro 编辑表单来实现,这里我主要用到了表单的文本域和复选框。
工具我放云盘,需要的自取:https://caiyun.139.com/m/i?105CqcMLSgEyR 提取码:6ais
在这里插入图片描述
文本域的 name 对应 Java 中 model 类的属性。

二、前端编码

// html
<a-button v-has="'dec:down'" type="primary" icon="printer" :loading="printBatchLoading" @click="handlePrintBatch">批量打印</a-button>// JavaScript
/*** 批量打印*/
handlePrintBatch(){if (this.selectedRowKeys.length == 0) {this.$message.error('请选择至少一票数据!')return}let params = {}params.ids = this.selectedRowKeys.join(',')this.printBatchLoading = truelet fileName = ''if (this.selectedRowKeys.length > 1) {fileName = '电子委托协议批量导出.zip'} else {fileName = '电子委托协议导出' + (this.selectionRows[0].consignNo ? this.selectionRows[0].consignNo :this.selectionRows[0].id) + '.pdf'}downloadFile(this.url.exportElecProtocolBatch, fileName,params).then((res) => {if (res.success) {} else {this.$message.warn(`导出失败!${res.message}`)}}).finally(() => {this.printBatchLoading = false})
}/*** 下载文件* @param url 文件路径* @param fileName 文件名* @param parameter* @returns {*}*/
export function downloadFile(url, fileName, parameter) {return downFile(url, parameter).then((data) => {if (!data || data.size === 0) {Vue.prototype['$message'].warning('文件下载失败')return}if (typeof window.navigator.msSaveBlob !== 'undefined') {window.navigator.msSaveBlob(new Blob([data]), fileName)} else {let url = window.URL.createObjectURL(new Blob([data]))let link = document.createElement('a')link.style.display = 'none'link.href = urllink.setAttribute('download', fileName)document.body.appendChild(link)link.click()document.body.removeChild(link) //下载完成移除元素window.URL.revokeObjectURL(url) //释放掉blob对象}})
}

三、后端编码

java">/*** 批量打印电子委托协议** @param ids* @return org.jeecg.common.api.vo.Result<?>* @author ZHANGCHAO* @date 2025/1/16 08:54*/
@Override
public void exportElecProtocolBatch(String ids, HttpServletResponse response) {try {// 获取协议列表List<ElecProtocol> elecProtocolList = fetchProtocolsByIds(ids);if (isEmpty(elecProtocolList)) {throw new RuntimeException("未获取到电子委托协议数据");}if (elecProtocolList.size() == 1) {// 单个文件导出ElecProtocol protocol = elecProtocolList.get(0);Map<String, Object> data = prepareDataMap(protocol);byte[] pdfBytes = generatePdf(data);// 设置响应头String pdfFileName = URLEncoder.encode("电子委托协议_" + (isNotBlank(protocol.getConsignNo()) ? protocol.getConsignNo() : protocol.getId()) + ".pdf","UTF-8");response.setContentType("application/pdf");response.setHeader("Content-Disposition", "attachment; filename=" + pdfFileName);try (ServletOutputStream outputStream = response.getOutputStream()) {outputStream.write(pdfBytes);outputStream.flush();}} else {// 多个文件压缩成 ZIP 导出response.setContentType("application/zip");String zipFileName = URLEncoder.encode("电子委托协议导出.zip", "UTF-8");response.setHeader("Content-Disposition", "attachment; filename=" + zipFileName);try (ZipOutputStream zipOut = new ZipOutputStream(response.getOutputStream())) {for (ElecProtocol protocol : elecProtocolList) {Map<String, Object> data = prepareDataMap(protocol);byte[] pdfBytes = generatePdf(data);String pdfFileName = "电子委托协议_" +(isNotBlank(protocol.getConsignNo()) ? protocol.getConsignNo() : protocol.getId()) + ".pdf";ZipEntry zipEntry = new ZipEntry(pdfFileName);zipOut.putNextEntry(zipEntry);zipOut.write(pdfBytes);zipOut.closeEntry();}}}} catch (Exception e) {log.error("导出电子委托协议失败: {}", e.getMessage(), e);throw new RuntimeException("导出失败,请稍后再试");}
}/*** 获取数据** @param ids* @return java.util.List<org.jeecg.modules.business.entity.ElecProtocol>* @author ZHANGCHAO* @date 2025/1/16 16:24*/
private List<ElecProtocol> fetchProtocolsByIds(String ids) {return baseMapper.selectBatchIds(Arrays.asList(ids.split(",")));
}/*** 处理数据** @param elecProtocol* @return java.util.Map<java.lang.String, java.lang.Object>* @author ZHANGCHAO* @date 2025/1/16 16:23*/
private Map<String, Object> prepareDataMap(ElecProtocol elecProtocol) {Map<String, Object> map = new HashMap<>();map.put("consignorName", elecProtocol.getConsignorName());map.put("trusteeName", elecProtocol.getTrusteeName());map.put("gName", elecProtocol.getGName());if (isNotBlank(elecProtocol.getEntryId())) {map.put("entryId", "No." + elecProtocol.getEntryId());}map.put("codeTs", elecProtocol.getCodeTs());map.put("receiveDate", elecProtocol.getReceiveDate());map.put("ieDate", elecProtocol.getIeDate());map.put("billCode", elecProtocol.getBillCode());if (isNotBlank(elecProtocol.getTradeMode())) {List<DictModelVO> jgfs = decListMapper.getDictItemByCode("JGFS");List<DictModelVO> dictModelVO1=jgfs.stream().filter(i->i.getValue().equals(elecProtocol.getTradeMode())).collect(Collectors.toList());map.put("tradeMode", isNotEmpty(dictModelVO1) ? dictModelVO1.get(0).getText() : "");}map.put("qtyOrWeight", elecProtocol.getQtyOrWeight());map.put("packingCondition", elecProtocol.getPackingCondition());map.put("paperinfo", elecProtocol.getPaperinfo());if (isNotBlank(elecProtocol.getOriCountry())) {List<DictModel> dictModels2 = sysBaseApi.getDictItems("erp_countries,name,code");Map<String, String> dictMap2 = new HashMap<>();if (isNotEmpty(dictModels2)) {dictModels2.forEach(dictModel -> {dictMap2.put(dictModel.getValue(), dictModel.getText());});}if(dictMap2.containsKey(elecProtocol.getOriCountry())) {map.put("oriCountry", dictMap2.get(elecProtocol.getOriCountry()));}}if (isNotBlank(elecProtocol.getDeclarePrice())) {if (isNotBlank(elecProtocol.getCurr())) {// 币制List<DictModel> dictModels3 = sysBaseApi.getDictItems("erp_currencies,name,code,1=1");Map<String, String> dictMap3 = new HashMap<>();if (isNotEmpty(dictModels3)) {dictModels3.forEach(dictModel -> {dictMap3.put(dictModel.getValue(), dictModel.getText());});}if(dictMap3.containsKey(elecProtocol.getCurr())) {map.put("declarePrice", dictMap3.get(elecProtocol.getCurr()) + ": " + elecProtocol.getDeclarePrice() + "元");} else {map.put("declarePrice", elecProtocol.getDeclarePrice());}} else {map.put("declarePrice", elecProtocol.getDeclarePrice());}}map.put("otherNote", elecProtocol.getOtherNote());map.put("promiseNote", elecProtocol.getPromiseNote());String dateStr = DateUtil.format(new Date(), DatePattern.CHINESE_DATE_PATTERN);map.put("dateStr", dateStr);map.put("printTime", dateStr + " " + DateUtil.format(new Date(), DatePattern.NORM_TIME_PATTERN));// 处理选框逻辑String paperinfo = elecProtocol.getPaperinfo();if (isNotBlank(paperinfo) && paperinfo.length() == 6) {for (int i = 0; i < paperinfo.length(); i++) {if (paperinfo.charAt(i) == '1') {map.put("gou" + (i + 1), "On");}}}return map;
}/*** 生成PDF** @param data* @return byte[]* @author ZHANGCHAO* @date 2025/1/16 16:24*/
private byte[] generatePdf(Map<String, Object> data) throws Exception {try (ByteArrayOutputStream bos = new ByteArrayOutputStream()) {PdfReader reader = new PdfReader(this.getClass().getResourceAsStream("/templates/pdf/电子委托协议模板.pdf"));PdfStamper stamper = new PdfStamper(reader, bos);AcroFields form = stamper.getAcroFields();BaseFont bf = BaseFont.createFont("STSong-Light", "UniGB-UCS2-H", BaseFont.EMBEDDED);ArrayList<BaseFont> fontList = new ArrayList<>();fontList.add(bf);form.setSubstitutionFonts(fontList);for (Map.Entry<String, Object> entry : data.entrySet()) {if (entry.getKey().contains("gou")) {form.setField(entry.getKey(), isNotEmpty(entry.getValue()) ? entry.getValue().toString() : "", true);} else {form.setField(entry.getKey(), isNotEmpty(entry.getValue()) ? entry.getValue().toString() : "");}}stamper.setFormFlattening(true);stamper.close();return bos.toByteArray();}
}

同时支持导出单个和批量,单个是直接生成PDF文件,批量是打成压缩包。

四、效果展示

页面数据:
[图片]
生成的 PDF:
在这里插入图片描述

总结

总得来说,Java通过itext PDF模板生成PDF文件功能很简单,主要是数据的填充而已。还可以继续丰富下,比如多行文本、自动换行功能等。


http://www.ppmy.cn/ops/151689.html

相关文章

PHP:写接口与接口的调用(完整版,封装公共方法)

说明&#xff1a;绑定的资源详细展示了两个项目的接口、接口调用的实现&#xff0c;已经数据库的连接&#xff0c;目录展示更加一目了然&#xff0c;有需要可以下载资源&#xff0c;实际文章已经描述的很详细了 一、A页面-发送请求页面 1、说明 发送请求部分&#xff0c;去调…

人脸识别【python-基于OpenCV】

1. 导入并显示图片 #导入模块 import cv2 as cv#读取图片 imgcv.imread(img/wx(1).jpg) #路径名为全英文&#xff0c;出现中文 图片加载失败,"D:\picture\wx.jpg" #显示图片 &#xff08;显示标题&#xff0c;显示图片对象&#xff09; cv.imshow(read_picture,im…

专业数据分析不止于Tableau,四款小众报表工具解析

在众多的报表工具中&#xff0c;市场上的常见报表工具如Tableau、Power BI等被广泛使用&#xff0c;但一些小众工具也提供了独特的功能和优势。以下是四款小众报表工具的介绍&#xff0c;它们各具特色&#xff0c;适合不同需求的用户&#xff0c;下面就为大家简单介绍一下。 1…

使用 Cargo 打开 Rust 世界的大门

一、什么是 Cargo&#xff1f; Cargo 是 Rust 开发者不可或缺的工具。它可以&#xff1a; 构建代码&#xff1b;下载并管理依赖库&#xff1b;简化项目初始化和配置。 对于一个简单的程序&#xff0c;比如 “Hello, world!”&#xff0c;你可能并不需要依赖库。但当你开始编…

Python 爬虫:获取网页数据的 5 种方法

&#x1f496; 欢迎来到我的博客&#xff01; 非常高兴能在这里与您相遇。在这里&#xff0c;您不仅能获得有趣的技术分享&#xff0c;还能感受到轻松愉快的氛围。无论您是编程新手&#xff0c;还是资深开发者&#xff0c;都能在这里找到属于您的知识宝藏&#xff0c;学习和成长…

鸿蒙学习构建视图的基本语法(二)

一、层叠布局 // 图片 本地图片和在线图片 Image(https://developer.huawei.com/allianceCmsResource/resource/HUAWEI_Developer_VUE/images/080662.png) Entry Component//自适应伸缩 设置layoutWeight属性的子元素与兄弟元素 会按照权重进行分配主轴的空间// Position s…

基于VSCode+CMake+debootstrap搭建Ubuntu交叉编译开发环境

基于VSCodeCMakedebootstrap搭建Ubuntu交叉编译开发环境 1 基于debootstrap搭建目标系统环境1.1 安装必要软件包1.2 创建sysroot目录1.3 运行debootstrap1.4 挂载必要的虚拟文件系统1.5 进入目标系统1.6 使用目标系统&#xff08;以安装zlog为例&#xff09;1.7 清理和退出 2 基…

ShardingSphere 注意事项

在使用 ShardingSphere 时&#xff0c;需要特别注意一些关键点和最佳实践&#xff0c;以确保系统的稳定性、可扩展性、性能和易维护性。下面列出了在使用 ShardingSphere 时需要注意的几个重要方面&#xff1a; 1. 分片规则设计 分片策略的选择&#xff1a;ShardingSphere 支持…