PDFBox渲染生成pdf文档

news2025/4/9 23:33:11

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

1. 实际效果

在这里插入图片描述
填充数据后效果
在这里插入图片描述
实现代码:
以下代码基于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,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();
                    // 操作时间位置
                    contentStream.beginText();
                    contentStream.newLineAtOffset(80, 650); // 80,650
                    contentStream.showText("操作时间:");
                    contentStream.endText();

                    // ----------------实际内容-----------------------
                    // 表头
                    contentStream.beginText();
                    contentStream.newLineAtOffset(80, 625); //80,625
                    contentStream.showText("序号");
                    contentStream.newLineAtOffset(40, 0); //120,625
                    contentStream.showText("商品编号");
                    contentStream.newLineAtOffset(80, 0); //200,625
                    contentStream.showText("商品名称");
                    contentStream.newLineAtOffset(70, 0); //270,625
                    contentStream.showText("单位");
                    contentStream.newLineAtOffset(40, 0); //310,625
                    contentStream.showText("借出数量");
                    contentStream.newLineAtOffset(70, 0); //380,625
                    contentStream.showText("备注");
                    contentStream.newLineAtOffset(100, 0); //480,625
                    contentStream.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,150
                    contentStream.showText("合计");
                    contentStream.endText();
                    // 出库数量和总金额位置
                    contentStream.beginText();
                    contentStream.newLineAtOffset(110, 125); // 110,125
                    contentStream.showText("出库数量:");
                    contentStream.newLineAtOffset(270, 0); // 380,125
                    contentStream.showText("总金额:");
                    contentStream.endText();
                    // 签名位置
                    contentStream.beginText();
                    contentStream.newLineAtOffset(80, 50); // 110,125
                    contentStream.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文件本质是「坐标画布」‌:

  1. PDF的渲染模型基于‌绝对坐标系‌(原点在页面左下角),所有元素(文字、图形)必须明确指定位置(x,y)。
  2. 无布局引擎‌: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();

这个方法更适合大段连贯的文字渲染,你只要设定好固定行距之后就可以直接开启新行,新行的位置会成功进入到下一行的开头并且行距就是你设定的值,这样你就不用每次都自行定位了,效果如下
在这里插入图片描述

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/2329823.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

Batch Normalization:深度学习训练的加速引擎

引言 在深度学习的发展历程中&#xff0c;训练深度神经网络一直是一项极具挑战性的任务。随着网络层数的增加&#xff0c;梯度消失、梯度爆炸以及训练过程中的内部协变量偏移&#xff08;Internal Covariate Shift&#xff09;问题愈发严重&#xff0c;极大地影响了模型的收敛…

低空经济基础设施建设方向与展望

随着科技的不断进步&#xff0c;低空经济正逐渐成为推动国家经济发展的新引擎。低空经济&#xff0c;指的是在低空范围内进行的各种经济活动&#xff0c;包括但不限于无人机物流、空中交通管理、低空旅游、农业监测等。本文将探讨低空经济基础设施建设的方向与未来展望。 1. 低…

如何保证RabbitMQ消息的可靠传输?

在这个图中&#xff0c;消息可能丢失的场景是1&#xff0c;2&#xff0c;3 1.在生产者将消息发送给RabbitMQ的时候&#xff0c;消息到底有没有正确的到达服务器呢&#xff0c;RabbitMQ提供了两种解决方案&#xff1a; a. 通过事务机制实现&#xff08;比较消耗性能&#xff0…

Sentinel核心源码分析(上)

文章目录 前言一、客户端与Spring Boot整合二、SphU.entry2.1、构建责任链2.2、调用责任链2.2.1、NodeSelectorSlot2.2.2、ClusterBuilderSlot2.2.3、LogSlot2.2.4、StatisticSlot2.2.5、AuthoritySlot2.2.6、SystemSlot2.2.7、FlowSlot2.2.7.1、selectNodeByRequesterAndStrat…

Systemd安全加密备份系统与智能通知

实训背景 你是一家金融科技公司的系统架构师&#xff0c;需为敏感数据设计一套安全备份系统&#xff0c;满足以下需求&#xff1a; 加密存储&#xff1a;自动解密插入的LUKS加密USB设备&#xff0c;挂载到安全目录。备份验证&#xff1a;备份完成后校验文件完整性&#xff0c…

6.0 使用Qt+ OpenCV+Python加载图片

本例作为python图像处理的入门课程1,使用Qt+ OpenCV+Python加载图片。 主要有如下几个地方需要注意: 1. OpenCV 默认使用 BGR 格式,而 Qt 使用 RGB。显示前需要转换:cv2.cvtColor(img, cv2.COLOR_BGR2RGB),一般使用某个QLabel控件进行显示。 pic = cv2.cvtColor(pic, cv2.C…

【Mac 从 0 到 1 保姆级配置教程 11】- Mac 基础配置 Finder、触控板、常用快捷键等

文章目录 前言配置 Finder1. 把我们的家目录请出来2. 显示文件扩展名3. 展示隐藏文件4. 显示路径栏和状态栏5. 固定文件夹到工具栏 基础快捷键1. Finder 导航快捷键2. 文件操作快捷键3. 视图和显示快捷键4. 搜索和选择快捷键5. 实用技巧6. 关于文件创建 配置触控板1. 右键设置2…

C++Primer - 动态内存管理

欢迎阅读我的 【CPrimer】专栏 专栏简介&#xff1a;本专栏主要面向C初学者&#xff0c;解释C的一些基本概念和基础语言特性&#xff0c;涉及C标准库的用法&#xff0c;面向对象特性&#xff0c;泛型特性高级用法。通过使用标准库中定义的抽象设施&#xff0c;使你更加适应高级…

DeepSeek本地部署(Ollama)

1. Ollama 安装 Ollama 官网地址&#xff1a; https://ollama.com/安装包网盘地址: https://pan.baidu.com 2. Deepseek 部署 根据自己电脑配置和应用需求选择不同模型&#xff0c;配置不足会导致运行时候卡顿。 版本安装指令模型大小硬盘&#xff08;存储&#xff09;显卡…

第二期:深入理解 Spring Web MVC [特殊字符](核心注解 + 进阶开发)

前言&#xff1a; 欢迎来到 Spring Web MVC 深入学习 的第二期&#xff01;在第一期中&#xff0c;我们介绍了 Spring Web MVC 的基础知识&#xff0c;学习了如何 搭建开发环境、配置 Spring MVC、编写第一个应用&#xff0c;并初步了解了 控制器、视图解析、请求处理流程 等核…

论伺服电机在轨道式巡检机器人中的优势及应用实践​

一、引言​ 1.1 研究背景与意义​ 在现代工业生产、电力系统、轨道交通等诸多领域&#xff0c;保障设施设备的安全稳定运行至关重要。轨道式巡检机器人作为一种高效、智能的巡检工具&#xff0c;正逐渐在这些领域崭露头角。它能够沿着预设轨道&#xff0c;对目标区域进行全方位…

(51单片机)独立按键控制流水灯LED流向(独立按键教程)(LED使用教程)

源代码 如上图将7个文放在Keli5 中即可&#xff0c;然后烧录在单片机中就行了 烧录软件用的是STC-ISP&#xff0c;不知道怎么安装的可以去看江科大的视频&#xff1a; 【51单片机入门教程-2020版 程序全程纯手打 从零开始入门】https://www.bilibili.com/video/BV1Mb411e7re?…

react-router children路由报错

项目场景&#xff1a; 写个路由页面&#xff0c;引发的问题 问题描述 报错&#xff1a; An absolute child route path must start with the combined path of all its parent routes. 代码&#xff1a; import { createBrowserRouter } from "react-router-dom";…

Socket编程TCP

Socket编程TCP 1、V1——EchoServer单进程版2、V2——EchoServer多进程版3、V3——EchoServer多线程版4、V4——EchoServer线程池版5、V5——多线程远程命令执行6、验证TCP——Windows作为client访问Linux7、connect的断线重连 1、V1——EchoServer单进程版 在TcpServer.hpp中实…

文件映射mmap与管道文件

在用户态申请内存&#xff0c;内存内容和磁盘内容建立一一映射 读写内存等价于读写磁盘 支持随机访问 简单来说&#xff0c;把磁盘里的数据与内存的用户态建立一一映射关系&#xff0c;让读写内存等价于读写磁盘&#xff0c;支持随机访问。 管道文件&#xff1a;进程间通信机…

代码随想录回溯算法03

93.复原IP地址 本期本来是很有难度的&#xff0c;不过 大家做完 分割回文串 之后&#xff0c;本题就容易很多了 题目链接/文章讲解&#xff1a;代码随想录 视频讲解&#xff1a;回溯算法如何分割字符串并判断是合法IP&#xff1f;| LeetCode&#xff1a;93.复原IP地址_哔哩哔…

批量改CAD图层颜色——CAD c#二次开发

一个文件夹下大量图纸&#xff08;几百甚至几千个文件&#xff09;需要改图层颜色时&#xff0c;可采用插件实现&#xff0c;效果如下&#xff1a; 转换前&#xff1a; 转换后&#xff1a; 使用方式如下&#xff1a;netload加载此dll插件&#xff0c;输入xx运行。 附部分代码如…

【内网安全】DHCP 饿死攻击和防护

正常情况&#xff1a;PC2可以正常获取到DHCP SERVER分别的IP地址查看DHCP SERCER 的ip pool地址池可以看到分配了一个地址、Total 253个 Used 1个 使用kali工具进行模拟攻击 进行DHCP DISCOVER攻击 此时查看DHCP SERVER d大量的抓包&#xff1a;大量的DHCP Discover包 此时模…

10种电阻综合对比——《器件手册--电阻》

二、电阻 前言 10种电阻对比数据表 电阻类型 原理 特点 应用 贴片电阻 贴片电阻是表面贴装元件&#xff0c;通过将电阻体直接贴在电路板上实现电路连接 体积小、重量轻&#xff0c;适合高密度电路板&#xff1b;精度高、稳定性好&#xff0c;便于自动化生产 广泛应用于…

剑指Offer(数据结构与算法面试题精讲)C++版——day6

剑指Offer&#xff08;数据结构与算法面试题精讲&#xff09;C版——day6 题目一&#xff1a;不含重复字符的最长子字符串题目二&#xff1a;包含所有字符的最短字符串题目三&#xff1a;有效的回文 题目一&#xff1a;不含重复字符的最长子字符串 这里还是可以使用前面&#x…