Java使用PDFBox渲染生成pdf文档的代码详解

 更新时间:2025年04月07日 10:23:45   作者:SYKMI  
使用PDFBox可以渲染生成pdf文档,并且自定义程度高,只是比较麻烦,这篇文章将为大家详细介绍一下具体的实现方法,感兴趣的小伙伴可以参考一下

使用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,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文件本质是「坐标画布」‌

  • 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();

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

到此这篇关于Java使用PDFBox渲染生成pdf文档的代码详解的文章就介绍到这了,更多相关Java PDFBox渲染pdf内容请搜索脚本之家以前的文章或继续浏览下面的相关文章希望大家以后多多支持脚本之家!

相关文章

  • Java SSM框架如何配置静态资源加载

    Java SSM框架如何配置静态资源加载

    这篇文章主要介绍了Java SSM框架如何配置静态资源加载,文中通过示例代码介绍的非常详细,对大家的学习或者工作具有一定的参考学习价值,需要的朋友可以参考下
    2020-04-04
  • 十五道tomcat面试题,为数不多的机会!

    十五道tomcat面试题,为数不多的机会!

    这篇文章主要介绍了十五道tomcat面试题,Tomcat的本质是一个Servlet容器。一个Servlet能做的事情是:处理请求资源,并为客户端填充response对象,需要的朋友可以参考下
    2021-08-08
  • 基于Java编写一个实用的ExcelUtil工具类

    基于Java编写一个实用的ExcelUtil工具类

    在项目中经常遇到excel表格导入导出功能,每次都要重复写有关excel 的逻辑,所以本文直接使用Java编写一个实用的ExcelUtil工具类,希望对大家有所帮助
    2024-04-04
  • Java中Date、LocalDate、LocalDateTime、LocalTime、时间戳之间的相互转换代码

    Java中Date、LocalDate、LocalDateTime、LocalTime、时间戳之间的相互转换代码

    这篇文章主要介绍了Java中日期时间转换的多种方法,包括将Date转换为LocalDateTime、LocalDate等,以及将时间戳转换为LocalDateTime,文中通过代码介绍的非常详细,需要的朋友可以参考下
    2025-04-04
  • SpringBoot 使用 OpenAPI3 规范整合 knife4j的详细过程

    SpringBoot 使用 OpenAPI3 规范整合 knife4j的详细过程

    Swagger工具集使用OpenAPI规范,可以生成、展示和测试基于OpenAPI规范的API文档,并提供了生成客户端代码的功能,本文给大家介绍SpringBoot使用OpenAPI3规范整合knife4j的详细过程,感兴趣的朋友跟随小编一起看看吧
    2023-12-12
  • Spring Boot优雅使用RocketMQ的方法实例

    Spring Boot优雅使用RocketMQ的方法实例

    这篇文章主要给大家介绍了关于Spring Boot优雅使用RocketMQ的相关资料,文中通过示例代码介绍的非常详细,对大家学习或者使用Spring Boot具有一定的参考学习价值,需要的朋友们下面来一起学习学习吧
    2019-12-12
  • 详解Vue响应式的部分实现

    详解Vue响应式的部分实现

    响应式,简单来说当数据发生变化时,对数据有依赖的代码会重新执行。这篇文章主要为大家介绍了Vue中响应式的部分实现,感兴趣的可以了解一下
    2022-12-12
  • 教你用Java GUI实现文本文件的读写

    教你用Java GUI实现文本文件的读写

    今天带大家来学习怎么用JavaSwing实现实现文本文件读写,文中有非常详细的代码示例,对正在学习java的小伙伴们有很好的帮助,需要的朋友可以参考下
    2021-05-05
  • java键盘录入的方法举例详解

    java键盘录入的方法举例详解

    这篇文章主要给大家介绍了关于java键盘录入的相关资料,我们在写程序的时候,数据值都是固定的,但是实际开发中,数据值肯定是变化的,所以,把数据改进为键盘录入,提高程序的灵活性,需要的朋友可以参考下
    2023-10-10
  • springboot 实现动态刷新配置的详细过程

    springboot 实现动态刷新配置的详细过程

    这篇文章主要介绍了springboot实现动态刷新配置,本文通过实例代码给大家介绍的非常详细,对大家的学习或工作具有一定的参考借鉴价值,需要的朋友可以参考下
    2023-05-05

最新评论