PDFBox/Itext5渲染生成pdf文档
目录
- PDFBox
- 最终效果
- 实现代码
- Itext5
- 最终效果
- 实现代码
PDFBox
使用PDFBox可以渲染生成pdf文档,并且自定义程度高,只是比较麻烦,pdf的内容位置都需要手动设置x(横向)和y(纵向)绝对位置,但是每个企业的单据都是不一样的,一般来说都会设置一个模板,然后内容再填充到适当位置,所以这个功能还是有用的
最终效果

填充数据后效果

实现代码
以下代码基于PDFBox依赖版本-2.0.23
public class Demo01 {public static void main(String[] args) throws Exception{// 设定中文字体File fontFile = new File("C:\\Windows\\Fonts\\simHei.ttf");try (PDDocument document = new PDDocument()) {PDType0Font load = PDType0Font.load(document, fontFile);PDPage page;for (int i = 0; i < 1; i++) {page = new PDPage();document.addPage(page);// 对具体PDPage设定内容try(PDPageContentStream contentStream = new PDPageContentStream(document, page)) {contentStream.setFont(load, 25);contentStream.beginText();// newLineAtOffset方法contentStream.newLineAtOffset(220, 750);contentStream.showText("借用出库打印单");contentStream.setFont(load, 12);contentStream.endText();// 仓库和会员渲染位置contentStream.beginText();contentStream.newLineAtOffset(80, 700); // 80,700contentStream.showText("仓库:");contentStream.newLineAtOffset(300, 0); //380,700contentStream.showText("会员:");contentStream.endText();// 销售员和操作人渲染位置contentStream.beginText();contentStream.newLineAtOffset(80, 675); // 80,675contentStream.showText("销售员:");contentStream.newLineAtOffset(300, 0); //380,675contentStream.showText("操作人:");contentStream.endText();// 操作时间位置contentStream.beginText();contentStream.newLineAtOffset(80, 650); // 80,650contentStream.showText("操作时间:");contentStream.endText();// ----------------实际内容-----------------------// 表头contentStream.beginText();contentStream.newLineAtOffset(80, 625); //80,625contentStream.showText("序号");contentStream.newLineAtOffset(40, 0); //120,625contentStream.showText("商品编号");contentStream.newLineAtOffset(80, 0); //200,625contentStream.showText("商品名称");contentStream.newLineAtOffset(70, 0); //270,625contentStream.showText("单位");contentStream.newLineAtOffset(40, 0); //310,625contentStream.showText("借出数量");contentStream.newLineAtOffset(70, 0); //380,625contentStream.showText("备注");contentStream.newLineAtOffset(100, 0); //480,625contentStream.showText("零售价");contentStream.endText();Map<String, String> contentMap = new HashMap<>();contentMap.put("序号", "1");contentMap.put("商品编号", "000212130023");contentMap.put("商品名称", "洗地机124123");contentMap.put("单位", "个");contentMap.put("借出数量", "13");contentMap.put("备注", "我是备注我是备注");contentMap.put("零售价", "1123300.34");fillContent(contentStream, contentMap, load);// 结尾结构渲染// 合计位置contentStream.beginText();contentStream.newLineAtOffset(80, 150); // 80,150contentStream.showText("合计");contentStream.endText();// 出库数量和总金额位置contentStream.beginText();contentStream.newLineAtOffset(110, 125); // 110,125contentStream.showText("出库数量:");contentStream.newLineAtOffset(270, 0); // 380,125contentStream.showText("总金额:");contentStream.endText();// 签名位置contentStream.beginText();contentStream.newLineAtOffset(80, 50); // 110,125contentStream.showText("签名:_______");contentStream.endText();// 模拟填充模板Map<String, String> map = new HashMap<>();map.put("仓库", "上海仓");map.put("会员", "小明");map.put("销售员", "销售员01");map.put("操作人", "系统管理员");map.put("操作时间", "2025年4月1日23点07分");map.put("出库数量", "1455");map.put("总金额", "285743835.45");fillTemplate(contentStream, map);}}document.save("demo01.pdf");System.out.println("PDF created successfully!");} catch (IOException e) {throw new RuntimeException(e);}}// 填充固定模板方法 该方法不填充中间详细内容public static void fillTemplate(PDPageContentStream contentStream, Map<String, String> map) {try {contentStream.beginText();contentStream.newLineAtOffset(130, 700);contentStream.showText(map.get("仓库"));contentStream.endText();contentStream.beginText();contentStream.newLineAtOffset(430, 700);contentStream.showText(map.get("会员"));contentStream.endText();contentStream.beginText();contentStream.newLineAtOffset(130, 675);contentStream.showText(map.get("销售员"));contentStream.endText();contentStream.beginText();contentStream.newLineAtOffset(430, 675);contentStream.showText(map.get("操作人"));contentStream.endText();contentStream.beginText();contentStream.newLineAtOffset(150, 650);contentStream.showText(map.get("操作时间"));contentStream.endText();contentStream.beginText();contentStream.newLineAtOffset(180, 125);contentStream.showText(map.get("出库数量"));contentStream.endText();contentStream.beginText();contentStream.newLineAtOffset(430, 125);contentStream.showText(map.get("总金额"));contentStream.endText();} catch (IOException e) {throw new RuntimeException(e);}}public static void fillContent(PDPageContentStream contentStream, Map<String, String> map, PDType0Font font) {try {contentStream.setFont(font, 10);contentStream.beginText();contentStream.newLineAtOffset(80, 600);contentStream.showText(map.get("序号"));contentStream.endText();contentStream.beginText();contentStream.newLineAtOffset(120, 600);contentStream.showText(map.get("商品编号"));contentStream.endText();contentStream.beginText();contentStream.newLineAtOffset(200, 600);contentStream.showText(map.get("商品名称"));contentStream.endText();contentStream.beginText();contentStream.newLineAtOffset(270, 600);contentStream.showText(map.get("单位"));contentStream.endText();contentStream.beginText();contentStream.newLineAtOffset(310, 600);contentStream.showText(map.get("借出数量"));contentStream.endText();contentStream.beginText();contentStream.newLineAtOffset(380, 600);contentStream.showText(map.get("备注"));contentStream.endText();contentStream.beginText();contentStream.newLineAtOffset(480, 600);contentStream.showText(map.get("零售价"));contentStream.endText();contentStream.setFont(font, 12);} catch (IOException e) {throw new RuntimeException(e);}}
}
上述代码看着确实是挺繁琐,每个内容的位置都需要设置x和y值,但是没办法
PDF文件本质是「坐标画布」:
- PDF的渲染模型基于绝对坐标系(原点在页面左下角),所有元素(文字、图形)必须明确指定位置(x,y)。
- 无布局引擎:PDF规范未定义“自动换行”或“文档流”等高级排版概念,开发者需自行计算坐标。
但是这样设计的好处就是自定义程度高,你可以任意设计一个PDF文档的模板应该是什么样子,内容该如何填充全部由你自由设定,就像低代码平台一样,市面上成熟开源的低代码平台有许多,但是逻辑都是一开始就定好的,如果你想加上许多符合自己公司需求的功能但是平台没有那么都得自行开发,并且自行开发的代码融合进已有的系统不是一件容易的事情,甚至比自行开发一套系统都麻烦。
所以如果你有这样的需求可以看下上述代码实现,上述代码只是一个简单的demo,我只是进行记录方便自己以后用到。
tips:关于一些方法的解释
contentStream.beginText();contentStream.newLineAtOffset(80, 700); // 80,700 --绝对定位contentStream.showText("仓库:");contentStream.newLineAtOffset(300, 0); //380,700 --相对定位(以'仓库:'的位置为准)contentStream.showText("会员:");contentStream.endText();contentStream.beginText();contentStream.newLineAtOffset(80, 675); // 80,675 --绝对定位contentStream.showText("销售员:");contentStream.newLineAtOffset(300, 0); //380,675 --相对定位(以'销售员'的位置为准)contentStream.showText("操作人:");contentStream.endText();
上述代码可以看到在渲染内容时是被包裹在beginText()和endText()方法中间的,这样当你调用newLineAtOffset(x, y)方法时参数中的x和y才从坐标系的绝对位置(绝对位置为画布的左下角0,0)进行定位。如果你在定位时没有重新开启beginText()和endText()时,调用newLineAtOffset(x, y)方法则是参照上一个文本的位置进行相对定位的,相对定位对于需要在同一行的不同位置渲染内容会比较方便。
newLineAtOffset(x, y)方法的官方注释有问题,官方说法是移动到下一行的开头,从当前行的开头进行偏移 (x, y),实测不对,并不会移动到下一行的开头,并且在相对定位时参考的位置也是你上一次的位置的起始点。
如果你需要像写文章那样一段一段的文字进行渲染,那么可以考虑使用另外一个方法
contentStream.beginText();contentStream.newLineAtOffset(80, 500); // 设定绝对位置的起点contentStream.setLeading(20); // 文本行距contentStream.showText("XXXXX"); //渲染内容contentStream.newLine(); //开启新行contentStream.showText("XXXXX"); //渲染内容contentStream.newLine(); //开启新行contentStream.showText("XXXXX"); //渲染内容contentStream.newLine(); //开启新行contentStream.endText();
这个方法更适合大段连贯的文字渲染,你只要设定好固定行距之后就可以直接开启新行,新行的位置会成功进入到下一行的开头并且行距就是你设定的值,这样你就不用每次都自行定位了,效果如下

Itext5
Itext感觉绘制pdf文档确实要比pdfBox强大一些,绘制表格啥的pdfBox都需要自行画线,Itext可以直接生成并填充数据,这方面来说就省下很多功夫了,下面直接贴效果和代码了
最终效果

实现代码
依赖
<dependency><groupId>org.apache.commons</groupId><artifactId>commons-lang3</artifactId><version>3.10</version></dependency><!-- https://mvnrepository.com/artifact/com.itextpdf/itextpdf --><dependency><groupId>com.itextpdf</groupId><artifactId>itextpdf</artifactId><version>5.5.13.3</version></dependency>
public class ItextDemo {public static void main(String[] args) throws Exception {// 输出PDF文件String outputPath = "demo02.pdf";// 创建文档Document document = new Document();try {PdfWriter writer = PdfWriter.getInstance(document, Files.newOutputStream(Paths.get(outputPath)));document.open();// 设置中文字体// 以输入流的形式获取字体,这种方式对于jar包中的字体读取非常有用,路径是无法达到jar包中的字体文件的File file = new File("C:\\Windows\\Fonts\\simHei.ttf");InputStream inputStream = Files.newInputStream(file.toPath());BaseFont bfChinese = BaseFont.createFont("simHei.ttf", BaseFont.IDENTITY_H,BaseFont.EMBEDDED, false, IOUtils.toByteArray(inputStream), null);// 直接使用路径获取
// BaseFont bfChinese = BaseFont.createFont("C:\\Windows\\Fonts\\simHei.ttf", BaseFont.IDENTITY_H, BaseFont.EMBEDDED);// 添加标题Paragraph title = new Paragraph("我是标题1", new Font(bfChinese, 30, Font.BOLD));title.setAlignment(Element.ALIGN_CENTER);document.add(title);PdfContentByte canvas;canvas = writer.getDirectContent();canvas.setFontAndSize(bfChinese, 18);canvas.beginText();// 上方模板canvas.setTextMatrix(20, 700);// 绝对位置canvas.showText("我是内容");canvas.moveText(140, 0); // 相对位置canvas.showText("我是第二个内容");List<Map<String, String>> sampleData = IText5TableMultiRowExample.getSampleData();fillContent(sampleData, bfChinese, canvas);// 下方模板canvas.setTextMatrix(20, 200);canvas.showText("合计");canvas.endText();// 模拟手动分页 (一般实际使用循环完成)document.newPage();// 标题Paragraph title2 = new Paragraph("我是标题2", new Font(bfChinese, 30, Font.BOLD));title2.setAlignment(Element.ALIGN_CENTER);document.add(title2);canvas.setFontAndSize(bfChinese, 10);canvas.beginText();// 上方模板canvas.setTextMatrix(20, 700);canvas.showText("我是内容");canvas.moveText(40, 0);canvas.showText("我是第二个内容");fillContent(sampleData, bfChinese, canvas);// 下方模板canvas.setTextMatrix(20, 200);canvas.showText("合计");canvas.endText();} catch (Exception e) {e.printStackTrace();} finally {document.close();}}public static void fillContent(List<Map<String, String>> dataList, BaseFont bfChinese, PdfContentByte canvas) throws DocumentException {// 创建表格(7列)PdfPTable table = new PdfPTable(7);table.setTotalWidth(500);table.setLockedWidth(true);// 设置列宽(相对宽度)float[] columnWidths = {0.5f, 2f, 3f, 1f, 1f, 1.5f, 3f}; // 根据需要调整table.setWidths(columnWidths);// 设置表头String[] headers = {"序号", "商品编号", "商品名称", "单位", "借出数量", "零售价", "备注"};for (String header : headers) {PdfPCell cell = new PdfPCell(new Phrase(header, new Font(bfChinese, 14, Font.BOLD)));cell.setHorizontalAlignment(Element.ALIGN_CENTER); // 表头居中cell.setPadding(5f); // 内边距table.addCell(cell);}// 填充表格内容for (Map<String, String> map : dataList) {table.addCell(new Phrase(map.get("序号"), new Font(bfChinese, 12)));table.addCell(new Phrase(map.get("商品编号"), new Font(bfChinese, 12)));table.addCell(new Phrase(map.get("商品名称"), new Font(bfChinese, 12)));table.addCell(new Phrase(map.get("单位"), new Font(bfChinese, 12)));table.addCell(new Phrase(map.get("借出数量"), new Font(bfChinese, 12)));table.addCell(new Phrase(map.get("零售价"), new Font(bfChinese, 12)));
// table.addCell(new Phrase(map.get("备注"), new Font(bfChinese, 12)));// 备注字段支持自动换行 通过setPadding设置内边距,增加单元格内容与边框之间的距离,从而间接影响单元格的视觉高度。PdfPCell remarkCell = new PdfPCell(new Phrase(map.get("备注"), new Font(bfChinese, 12)));remarkCell.setPadding(5f);table.addCell(remarkCell);}// 将表格添加到文档table.writeSelectedRows(0, -1, 40, 600, canvas);}
}
PDFBox/Itext5可以各取所需
相关文章:
PDFBox/Itext5渲染生成pdf文档
目录 PDFBox最终效果实现代码 Itext5最终效果实现代码 PDFBox 使用PDFBox可以渲染生成pdf文档,并且自定义程度高,只是比较麻烦,pdf的内容位置都需要手动设置x(横向)和y(纵向)绝对位置ÿ…...
前端获取不到后端新加的字段 解决方案
前端获取不到后端新加的字段 解决方案 sql 返回的是 FileInfo 对象 private String lastUpdateTimeStr;// 自定义 setLastUpdateTime 方法,确保在设置 lastUpdateTime 时自动格式化为字符串public void setLastUpdateTime(LocalDateTime lastUpdateTime) {this.las…...
【Java学习】AI时代下如何学习Java语言开发
学习 Java 语言开发时,合理借助 AI 工具可以提升效率、深化理解,以下是具体的学习策略和方法: 一、利用 AI 辅助基础学习 1. 智能文档解读与语法解析 工具:ChatGPT、Bing Chat、Google Bard用法: 直接提问基础语法问…...
联想拯救者Y9000K重装Ubuntu系统
USB刻录Ubuntu,并插入电脑。 进入官网https://rufus.ie/downloads/,安装4.0p版本,对应Ubuntu 22.04版本进入官网https://www.releases.ubuntu.com/22.04/,下载Ubuntu 22.04的iso文件插入一个空USB。运行rufus.exe,选择…...
罗技K860键盘
罗技蓝牙键盘的顶部功能键F1-F12的原本功能 单击罗技键盘的功能键时,默认响应的是键盘上面显示的快进、调节音量等功能。改变回F1~F12原本功能,同时按下 fn和esc组合键...
PyTorch Tensor维度变换实战:view/squeeze/expand/repeat全解析
本文从图像数据处理、模型输入适配等实际场景出发,系统讲解PyTorch中view、squeeze、expand和repeat四大维度变换方法。通过代码演示对比不同方法的适用性,助您掌握数据维度调整的核心技巧。 一、基础维度操作方法 1. view:内存连续的形状重…...
【NLP 面经 9、逐层分解Transformer】
目录 一、Transformer 整体结构 1.Tranformer的整体结构 2.Transformer的工作流程 二、Transformer的输入 1.单词 Embedding 2.位置 Embedding 计算公式: 三、Self-Attention 自注意力机制 1.Self-Attention 结构 编辑 2.Q、K、V的计算 代码实现 3.Self-Attenti…...
【线程有哪些状态?这些状态如何相互转换?阻塞和等待的状态有什么区别?】
线程状态及其转换与区别 线程的生命周期包含多个状态,不同状态之间的转换由线程调度和同步机制决定。以下是线程状态的详细说明、转换关系及阻塞与等待的区别: 一、线程的六种基本状态(以Java为例) 状态描述NEW(新建…...
netty中的ChannelPipeline详解
Netty中的ChannelPipeline是事件处理链的核心组件,负责将多个ChannelHandler组织成有序的责任链,实现网络事件(如数据读写、连接状态变化)的动态编排和传播。以下从核心机制、执行逻辑到应用场景进行详细解析: 1. 核心结构与组成 双向链表结构 组成单元:ChannelPipeline…...
Ubuntu 24.04 中文输入法安装
搜狗输入法,在Ubuntu 24.04上使用失败,安装教程如下 https://shurufa.sogou.com/linux/guide 出现问题的情况,是这个帖子里描述的: https://forum.ubuntu.org.cn/viewtopic.php?t493893 后面通过google拼音输入法解决了&#x…...
踩雷,前端一直卡在获取token中
问题:一直卡在var token SecureStorage.Default.GetAsync("auth_token").Result; public VideoService(){_httpClient new HttpClient();var token SecureStorage.Default.GetAsync("auth_token");} 这是一个典型的同步等待异步操作导致的死…...
这是一个文章标题
# Markdown 全语法示例手册本文档将全面演示 Markdown 的语法元素,包含 **标题**、**列表**、**代码块**、**表格**、**数学公式** 等 18 种核心功能。所有示例均附带实际应用场景说明。---## 一、基础文本格式### 1.1 标题层级 markdown # H1 (使用 #) ## H2 (使用…...
xtrabackup备份
安装: https://downloads.percona.com/downloads/Percona-XtraBackup-8.0/Percona-XtraBackup-8.0.35-30/binary/tarball/percona-xtrabackup-8.0.35-30-Linux-x86_64.glibc2.17.tar.gz?_gl1*1ud2oby*_gcl_au*MTMyODM4NTk1NS4xNzM3MjUwNjQ2https://downloads.perc…...
(51单片机)串口通讯(串口通讯教程)(串口接收发送教程)
前言: 今天有两个项目,分别为: 串口接收: 串口发送: 如上图将文件放在Keli5 中即可,然后烧录在单片机中就行了 烧录软件用的是STC-ISP,不知道怎么安装的可以去看江科大的视频: 【51单片机入门…...
redis 延迟双删
Redis延迟双删是一种用于解决缓存与数据库数据一致性问题的策略,通常在高并发场景下使用。以下是其核心内容: 1. 问题背景 当更新数据库时,如果未及时删除或更新缓存,可能导致后续读请求仍从缓存中读取旧数据,造成数…...
大语言模型中的幻觉现象深度解析
一、幻觉的定义及出现的原因 1. 基本定义 幻觉(Hallucination) 指大语言模型在自然语言处理过程中产生的与客观事实或既定输入相悖的响应,主要表现为信息失准与逻辑矛盾。 2. 幻觉类型与机制 2.1 事实性幻觉 定义:生成内容与可验证…...
App的欢迎页,以及启动黑屏的问题
1、在styles.xml文件中配置:<style name"WelcomePageStyle" parent"style/Theme.AppCompat.Light.NoActionBar"><item name"android:windowBackground">mipmap/icon_welcome_bg</item><item name"android:…...
各种颜色空间的相互转换方法(RGB,HSV,CMYK,灰度)
各个颜色空间原始值的取值范围: RGB:[0,255] H:[0,360],S:[0,1],V:[0,1] CMYK:[0,1] 灰度:[0,255] 以下给出各个颜色空间转换的伪代码。 RGB转HSV rR/255 gG/255 b…...
详解如何从零用 Python复现类似 GPT-4o 的多模态模型
🧠 向所有学习者致敬! “学习不是装满一桶水,而是点燃一把火。” —— 叶芝 我的博客主页: https://lizheng.blog.csdn.net 🌐 欢迎点击加入AI人工智能社区! 🚀 让我们一起努力,共创…...
大模型训练关键两步
大模型的核心原理是基于深度学习,通过多层神经网络进行数据建模和特征提取。目前大部分的大模型采用的是Transformer架构,它采用了自注意力机制,能够处理长距离依赖关系,从而更好地捕捉文本的语义和上下文信息。大模型还结合了预训…...
当算力遇上脑科学:破解意识上传的算力密码
目录 一、人脑复刻面临的三座大山 二、自然科学之外的三大麻烦 三、未来发展的三种可能结局 没有人的文明还是文明吗? 最近,全球首例"数字永生"官司闹得沸沸扬扬——美国富豪家属指控科技公司造假,而马斯克却宣布脑机接口芯片升级到第9代。科学家们算了一…...
前端面试宝典---创建对象的配置
Object.create 对整个对象的多个属性值进行配置 创建对象 不可更改属性值 // 创建对象 不可更改属性值 let obj Object.create({}, {name: {value: lisi,writable: false,},age: {value: 20,writable: true,} })console.log(初始化obj, obj) obj.name wangwu console.log(…...
【设计模式】创建型 -- 单例模式 (c++实现)
文章目录 单例模式使用场景c实现静态局部变量饿汉式(线程安全)懒汉式(线程安全)懒汉式(线程安全) 智能指针懒汉式(线程安全)智能指针call_once懒汉式(线程安全)智能指针call_onceCRTP 单例模式 单例模式是…...
共享内存(与消息队列相似)
目录 共享内存概述 共享内存函数 (1)shmget函数 功能概述 函数原型 参数解释 返回值 示例 结果 (2)shmat函数 功能概述 函数原型 参数解释 返回值 (3)shmdt函数 功能概述 函数原型 参数解释…...
2025年常见渗透测试面试题- PHP考察(题目+回答)
网络安全领域各种资源,学习文档,以及工具分享、前沿信息分享、POC、EXP分享。不定期分享各种好玩的项目及好用的工具,欢迎关注。 PHP考察 php的LFI,本地包含漏洞原理是什么?写一段带有漏洞的代码。手工的话如何发掘&am…...
【C++进阶】关联容器:multimap类型
目录 一、multimap 基础概念与底层实现 1.1 定义与核心特性 1.2 底层数据结构 1.3 类模板定义 1.4 与其他容器的对比 二、multimap 核心操作详解 2.1 定义与初始化 2.2 插入元素 2.3 查找元素 2.4 删除元素 2.5 遍历元素 三、性能分析与适用场景 3.1 时间复杂度分…...
远程管理命令:关机和重启
关机/重启 序号命令对应英文作用01shutdown 选项 时间shutdown关机 / 重新启动 一、shutdown shutdown 命令可以安全关闭 或者 重新启动系统。 选项含义-r重新启动 提示: 不指定选项和参数,默认表示 1 分钟之后 关闭电脑远程维护服务器时࿰…...
塑造现代互联网的力量:Berkeley在网络领域的影响与贡献
引言 “Berkeley” 这个名字在计算机网络和互联网领域中具有举足轻重的地位,许多关键的技术、协议和工具都与其紧密相关。它与 加利福尼亚大学伯克利分校(UC Berkeley) 密切相关,该校在计算机科学与网络研究中做出了许多开创性的…...
【MySQL】001.MySQL安装
文章目录 一. MySQL在Ubuntu 20.04 环境安装1.1 更新软件包列表1.2 安装MySQL服务器1.3 配置安全设置1.4 检查mysql server是否正在运行1.5 进行连接1.6 查询自带的数据库 二. 配置文件的修改三. MySQL连接TCP/IP时的登陆问题四. MySQL中的命令 一. MySQL在Ubuntu 20.04 环境安…...
vue 入门:组件事件
文章目录 vue介绍vue 入门简单示例自定义组件事件 vue介绍 vue2 官网 Vue (读音 /vjuː/,类似于 view) 是一套用于构建用户界面的渐进式框架。Vue 被设计为可以自底向上逐层应用。Vue 的核心库只关注视图层。 vue 入门 Vue.js 的核心是一个允许采用简洁的模板语…...
