使用Freemarker来生成pdf文件

news2025/7/5 3:15:30

2022-09-02

        今天接到一个生成pdf的任务,并且web端要能下载;在网上也找了许多的工具如:itext等,感觉挺复杂的没那么好用,然后想起了之前使用Freemarker来生成world文档,挺好用的,然后调查发现也能生成pdf,就是有一点区别如果Freemarker来生成world是使用world文档来当模板,而pdf相对于简单,直接使用html文件来制作模板,只不过最后要将文件后缀改成ftl的文件。

这个博主写的挺好的,可以直接去看这个博主的文章,我只是当笔记记录一下,参考的文章链接

本文链接:Java使用Freemarker通过模板文件导出PDF文件、横向显示_虚心若愚求知若渴的博客-CSDN博客_freemarker ftl生成pdf前言:​尝试了不少方法通过模板文件导出pdf文件,要么实现起来负责,要么实现效果不理想,经过反复查找资料发现此方法是最理想的。一,依赖jar包<!-- freemarker 读取html模板文件 --><dependency> <groupId>org.freemarker</groupId> &https://blog.csdn.net/weixin_39806100/article/details/86616041

代码如下:

  • maven依赖:

<dependency>
    <groupId>org.freemarker</groupId>
    <artifactId>freemarker</artifactId>
    <version>2.3.29</version>
</dependency>

<dependency>
    <groupId>org.xhtmlrenderer</groupId>
    <artifactId>flying-saucer-pdf</artifactId>
    <version>9.1.18</version>
</dependency>
  • service层:

public void exportPdf(HttpServletResponse response, Integer id, Integer type) throws Exception {
        ByteArrayOutputStream baos = null;
        OutputStream out = null;
        FileOutputStream fileOutputStream = null;
        try {
            //获取提货单数据,根据提货单id
            TakeOrder takeOrder = this.getTakeById(id);
            //翻译提货单状态
            String[] stateName = {"待备货","备货中","已备货","已出库","装车中","已装车","已进厂","已出厂"};
            takeOrder.setStateName(takeOrder.getState() == null ? "" : stateName[takeOrder.getState() - 1]);
            //翻译提货单提货状态
            String[] orderStateName = {"待提货","已提货","作废"};
            takeOrder.setOrderStateName(orderStateName[takeOrder.getOrderState() - 1]);
            
            // 模板中的数据,实际运用从数据库中查询
            Map<String,Object> data = new HashMap<>();
            data.put("takeOrder", takeOrder);
            data.put("fileName", type == 1 ? "备货联" : "承运联");

            //因为我自己的需求有两套模板,所以我让模板名称动态化了,如果不用直接删除这个type参数,正常填文件名称就可以,记得带上后缀
            baos = PDFTemplateUtil.createPDF(data, "modezs"+type+".ftl");
            // 设置响应消息头,告诉浏览器当前响应是一个下载文件
            response.setContentType( "application/x-msdownload");
            // 告诉浏览器,当前响应数据要求用户干预保存到文件中,以及文件名是什么 如果文件名有中文,必须URL编码 
            String fileName = URLEncoder.encode("月度报告.pdf", "UTF-8");
            response.setHeader( "Content-Disposition", "attachment;filename=" + fileName);
            out = response.getOutputStream();
            baos.writeTo(out);
            baos.close();
            //下载到本地位置
//            fileOutputStream = new FileOutputStream("D:\\zscProject\\zsc.pdf");
            //生成pdf完成记录行为记录
            this.addActionLog(takeOrder.getTakeOrderNo(),1);
        } catch (Exception e) {
            e.printStackTrace();
            throw new Exception("导出失败:" + e.getMessage());
        } finally{
            if(baos != null){
                baos.close();
            }
            if(out != null){
                out.close();
            }
            if (fileOutputStream != null){
                fileOutputStream.close();
            }
        }
    }

 ps:

1. 在使用工具类时,传文件名称的参数

 2. 在如果不需要web端的方式下载pdf,可以使用文件输出流直接下载到本地

 

  • 工具类:

可以直接拿来使用

public class PDFTemplateUtil {

	/**
	 * 通过模板导出pdf文件
	 * @param data 数据
	 * @param templateFileName 模板文件名
	 * @throws Exception
	 */
    public static ByteArrayOutputStream createPDF(Map<String,Object> data, String templateFileName) throws Exception {
        // 创建一个FreeMarker实例, 负责管理FreeMarker模板的Configuration实例
        Configuration cfg = new Configuration(Configuration.DEFAULT_INCOMPATIBLE_IMPROVEMENTS);
        // 指定FreeMarker模板文件的位置 
        cfg.setClassForTemplateLoading(PDFTemplateUtil.class,"/templates");
        ITextRenderer renderer = new ITextRenderer();
        OutputStream out = new ByteArrayOutputStream();
        try {
            // 设置 css中 的字体样式(暂时仅支持宋体和黑体) 必须,不然中文不显示
            renderer.getFontResolver().addFont("/templates/font/simsun.ttc", BaseFont.IDENTITY_H, BaseFont.NOT_EMBEDDED);
            // 设置模板的编码格式
            cfg.setEncoding(Locale.CHINA, "UTF-8");
            // 获取模板文件 
            Template template = cfg.getTemplate(templateFileName, "UTF-8");
            StringWriter writer = new StringWriter();
            
            // 将数据输出到html中
            template.process(data, writer);
            writer.flush();

            String html = writer.toString();
            // 把html代码传入渲染器中
            renderer.setDocumentFromString(html);

             // 设置模板中的图片路径 (这里的images在resources目录下) 模板中img标签src路径需要相对路径加图片名 如<img src="images/xh.jpg"/>
//            URI images = PDFTemplateUtil.class.getClassLoader().getResource("images").toURI();
//            if (images != null) {
//                String url = images.toString();
//                renderer.getSharedContext().setBaseURL(url);
//            }
            renderer.layout();
            
            renderer.createPDF(out, false);
            renderer.finishPDF();
            out.flush();
            return (ByteArrayOutputStream)out;
        } finally {
        	if(out != null){
        		 out.close();
        	}
        }
    }
}

ps:

我导出pdf里面是含有图片的,但是我的图片是base64的字节码(建议使用这种方式),不是本地的方式填充数据的,如果只能使用本地的图片,将工具类中的这段代码解开,然后自己改进一下

 

  • 模板文件:

<!DOCTYPE html>
<html>

<head>
	<meta charset="utf-8" />
	<title></title>
	<style>
		* {
			margin: 0;
			padding: 0;
			box-sizing: border-box;
		}

		body {
			font-family: SimSun;
			padding: 30px 20px 0;
		}

		section {
			display: block;
			/* margin: 20px 10px; */
		}

		.title {
			text-align: center;
			margin-bottom: 20px;
		}

		.preface p {
			line-height: 30px;
			display: inline-block;
		}

		.preface p.content {
			text-indent: 2em;
		}

		section>table {
			border-collapse: collapse;
			table-layout: fixed;
			width: 100%;
			font-size: 13px;
			/* margin: 20px 0px; */
			text-align: center;
			word-wrap: break-word;
		}

		section table td {
			padding: 5px 0px;
		}
		.topTitle section{
			width: 30%;
			font-size: 13px;
			display: inline-block;
			margin-top: 20px;
		}
		.topTitle{
		}
		.outTitle{
		}
		.outTitle section{
			font-size: 13px;
			display: inline-block;
		}
		.detail{
			margin-top: 20px;
		}
		.outTable{
			margin-bottom: 20px;
		}
		.box1{
		}
		.box2{
			width: 80%;
			display: inline-block;
		}
		.box3{
			display: inline-block;
			width: 18%;
			/* min-width: 180px; */
		}
		.box3 img{
			width: 100%;
		}
		.box3 p{
			font-size: 12px;
		}
	</style>
</head>

<body>
<h3>${(fileName)!''}</h3>
<div class="box1">
	<section class="title">
		<h2>XXXXXXXXXXXXXX有限公司</h2>
		<h2>提货单</h2>
	</section>
	<div class="box2">
		<!-- 标题 start -->
		<!-- 标题 end -->

		<!-- 前言 start -->
		<div class="topTitle">
			<section class="preface">
				<p>提货单号:</p>
				<p>${(takeOrder.takeOrderNo)!''}</p>
			</section>
			<section class="preface">
				<p>提货日期:</p>
				<p>${(takeOrder.takeDate)!''}</p>
			</section>
			<section class="preface">
				<p>提货状态:</p>
				<p>${(takeOrder.orderStateName)!''}</p>
			</section>
			<section class="preface">
				<p>状态:</p>
				<p>${(takeOrder.stateName)!''}</p>
			</section>
<#--			<section class="preface">-->
<#--				<p>承运商:</p>-->
<#--				<p>${(takeOrder.takeOrderNo)!''}</p>-->
<#--			</section>-->
<#--			<section class="preface">-->
<#--				<p>车辆:</p>-->
<#--				<p>${(takeOrder.takeOrderNo)!''}</p>-->
<#--			</section>-->
			<section class="preface">
				<p>司机:</p>
				<p>${(takeOrder.driver)!''}</p>
			</section>
			<section class="preface">
				<p>发运方式:</p>
				<p>${(takeOrder.shippingMethod)!''}</p>
			</section>
		</div>
	</div>
	<div class="box3">
		<img src="${(takeOrder.qrCode)!''}"></img>
		<p>凭此二维码进出厂区</p>

	</div>
</div>
<!-- 前言 end -->


<!-- 产品列表 start -->
<#if takeOrder.outOrderProducts ??>
<section class="detail">
	<table border="1" cellspacing="0" cellpadding="0">
		<tr>
			<td width="15%">品名编号</td>
			<td width="12%">品名</td>
			<td width="12%">规格型号</td>
			<td width="12%">销售型号</td>
			<td width="12%">包装规格</td>
			<td width="12%">批号</td>
			<td width="12%">数量</td>
			<td width="12%">单位</td>
			<td width="12%">仓库编号</td>
			<td width="12%">仓库名称</td>
		</tr>
		<#list takeOrder.outOrderProducts as ad>
			<tr>
				<td>${(ad.productCode)!''}</td>
				<td>${(ad.productName)!''}</td>
				<td>${(ad.typeNum)!''}</td>
				<td>${(ad.saleType)!''}</td>
				<td>${(ad.packSize)!''}</td>
				<td>${(ad.batchNumber)!''}</td>
				<td>${(ad.num)!''}</td>
				<td>${(ad.uint)!''}</td>
				<td>${(ad.stockNo)!''}</td>
				<td>${(ad.stockName)!''}</td>
			</tr>
		</#list>
	</table>
</section>
</#if>
<!-- 产品列表 end -->

<!-- 出库单 start -->
<#if takeOrder.outOrders ??>
<section class="detail">
	<h3>出库单信息:</h3>
	<#list takeOrder.outOrders as add>
	<div class="outTitle" >
		<section class="preface">
			<p>出库单号:</p>
			<p>${(add.outOrderNo)!''}</p>
		</section>
		<section class="preface">
			<p>发货单号:</p>
			<p>${(add.sendOrderNo)!''}</p>
		</section>
		<section class="preface">
			<p>出库日期:</p>
			<p>${(add.outDate)!''}</p>
		</section>
		<section class="preface">
			<p>装车号:</p>
			<p>${(add.loadingNumber)!''}</p>
		</section>
		<section class="preface">
			<p>客户名称:</p>
			<p>${(add.customerName)!''}</p>
		</section>
	</div>
	<!--出库的单产品列表-->
	<#if add.outOrderProducts ??>
	<table class="outTable" border="1" cellspacing="0" cellpadding="0">
		<tr>
			<td width="15%">品名编号</td>
			<td width="12%">品名</td>
			<td width="12%">规格型号</td>
			<td width="12%">客户销售型号</td>
			<td width="12%">包装规格</td>
			<td width="12%">批号</td>
			<td width="12%">数量</td>
			<td width="12%">内部备注</td>
			<td width="12%">备注</td>
		</tr>
		<#list add.outOrderProducts as ad>
			<tr>
				<td>${(ad.productCode)!''}</td>
				<td>${(ad.productName)!''}</td>
				<td>${(ad.typeNum)!''}</td>
				<td>${(ad.saleType)!''}</td>
				<td>${(ad.packSize)!''}</td>
				<td>${(ad.batchNumber)!''}</td>
				<td>${(ad.num)!''}</td>
				<td>${(ad.innerRemark)!''}</td>
				<td>${(ad.remark)!''}</td>
			</tr>
		</#list>
	</table>
	</#if>
	</#list>
</section>
</#if>
<!-- 出库单 end -->
</body>

</html>

ps:

1.这里面的样式是按照html的样式来的,自己设计的要自己调整样式,只支持定位和浮动,不支持自适应,建议在class里写样式,而不是style里写样式

2.像<#if takeOrder.outOrderProducts ??>,<#list takeOrder.outOrderProducts as ad>和${(fileName)!''}这个都是ftl文件的语法,不懂的可以搜一下

占位符可以看下图

来自本文链接:简单的Freemarker判断对象是否为空方法_OxYGC的博客-CSDN博客_freemarker if判断为空Freemarker判断对象是否为空1. freemarker中显示某对象使用${name}.但如果name为null,freemarker就会报错。如果需要判断对象是否为空:2. 当然也可以通过设置默认值${name!’’}来避免对象为空的错误。如果name为空,就以默认值(“!”后的字符)显示。3. 对象user,name为user的属性的情况,user,name都有可能为空,那么......https://blog.csdn.net/YangCheney/article/details/1058324443.放置位置如图

 

 

  • 字体文件:

在windows10系统中的 C:\Windows\Fonts 这个路径中,进入后搜索宋体(ps:一定要搜索宋体,不要按照simsun这个名字去搜索,反正就是注意文件的后缀是.ttc的,而不是.ttf的

  • 前端:

1.请求方法js:(ps:注意里面的responseType: 'arraybuffer'这个参数,因为后台使用的字节数组流的方式写入的,所以如果直接使用responseType: 'blob',会导致封装的blob对象是有问题的,下载出来的pdf文件损坏,本文链接:

使用FreeMarker生成pdf时,代码没异常产生,但是web端下载下来的文件损坏_A-Superman的博客-CSDN博客使用FreeMarker生成pdf时,代码没异常产生,但是web端下载下来的文件损坏https://blog.csdn.net/Jackbillzsc/article/details/126662319

export function exportPdf(parameter) {
  return request({
    url: 'XXXXXXXXXXXXXXX/export/pdf',
    method: 'get',
    params:parameter,
    responseType: 'arraybuffer',
    
  })
}

2.封装blob对象,下载pdf的js方法:

exportPdf(type) {
      this['loading'+type] = true
      exportPdf({ id: this.pageList.id, type: type }).then((res) => {
        if (!res) {
          alert('数据为空')
          return
        }
        const content = res
        const blob = new Blob([content], { type: 'application/pdf' })
        // const fileName = titName?titName: ''
        let fileName = this.pageList.takeOrderNo
        if ('download' in document.createElement('a')) {
          // 非IE下载
          const elink = document.createElement('a')
          elink.download = fileName
          elink.style.display = 'none'
          elink.href = URL.createObjectURL(blob)
          document.body.appendChild(elink)
          elink.click()
          URL.revokeObjectURL(elink.href) // 释放URL 对象
          document.body.removeChild(elink)
          this['loading'+type] = false
        } else {
          // IE10+下载
          navigator.msSaveBlob(blob, fileName)
        }
      })
    },

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

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

相关文章

【JavaWeb】重新认识 Servlet 的初始化 [ 回顾 Servlet ]

&#x1f947;作者 .29. 的✔博客主页✔ &#x1f947;记录JavaWeb学习的专栏&#xff1a;Web专栏 &#x1f947;向前走&#xff0c;不要回头。 您的点赞&#xff0c;收藏以及关注是对作者最大的鼓励喔 ~~ 重新认识Servlet的初始化一、回顾Servlet1.什么是Servlet2.Servlet规范…

设置背景图片大小的方法

背景图片大小设置 语法&#xff1a;background-size:宽度 高度&#xff1b;作用&#xff1a;设置背景图片大小取值&#xff1a; 取值场景数字px简单方便&#xff0c;常用百分比相当于当前盒子自身的宽高百分比contain包含&#xff0c;将背景图片等比例缩放&#xff0c;直到不…

Vue 之 echarts 图表数据可视化的基础使用(简单绘制各种图表、地图)

Vue 之 echarts 图表数据可视化的基础使用&#xff08;简单绘制各种图表、地图&#xff09; 目录 Vue 之 echarts 图表数据可视化的基础使用&#xff08;简单绘制各种图表、地图&#xff09; 一、简单介绍 二、环境搭建 三、使用 echarts 四、自动缩放 echarts 五、数据更…

【网络通信】websocket如何断线重连

Vue <template><div><button click"sendDevName(xxxxxxxx)">发送</button>{{data}}</div> </template><script> export default {name: HelloWorld,data () {return {data: null}},// html加载完成后执行initWebSocket()…

Vue3-路由跳转专题详细总结

一、基本路由 点击事件似乎可以使用模板更改视图中的内容&#xff0c;个人认为与路由的区别是路由能使网页中的地址栏发生变化 请先阅读基础第二篇 1.创建一个组件,并引入 2.js文件中配置路径 //name相当于别名{path:/tabView,component:TabView},{name:myComPany,path:compan…

Vue3项目搭建全过程

目录 一、前言 二、搭建准备 三、搭建项目 四、启动项目 一、前言 在2020年的9月19日&#xff0c;万众期待的Vue3终于发布了正式版&#xff0c;命名为“One Piece”。 它也带来了很多新的特性&#xff1a;更好的性能、更小的包体积、更好的TypeScript集成、更优秀的API设…

前端面试真题宝典(一)

面试题真题 闭包和柯里化 闭包是什么&#xff1f;闭包是能够读取其他函数内部变量的函数 柯里化是什么&#xff1f;柯里化是把一个多个参数的函数转化为单参数函数的方法 闭包的用途&#xff1a;闭包的主要用途是为了不污染全局变量&#xff0c;用闭包的局部变量来做一些库…

30个题型+代码(冲刺2023蓝桥杯)(中)

2023.3.13~4.13持续更新 目录 &#x1f34e;注意 &#x1f33c;前言 &#x1f33c;十&#xff0c;KMP&#xff08;留坑&#xff09; &#x1f33c;十一&#xff0c;Trie&#xff08;留坑&#xff09; &#x1f33c;十二&#xff0c;BFS &#x1f44a;(一)1562. 微博转发…

获取街道、镇级的地图geoJson数据方法,使用echarts绘制出街道、镇级的地图区域画面(中山市为例)

一、需求 1、在echarts上绘制市级以下的区、县的区域地图。 2、在市级下很多都是有区、县的区域&#xff0c;而少部分是不存在区、县的&#xff0c;是直接市下面一级就是街道、镇级别的区域。 3、统一管理区域数据&#xff0c;有区县的市直接拿区县的geoJson数据&#xff0c;没…

炸弹人小游戏代码开源(python)

♥️作者&#xff1a;小刘在C站 ♥️个人主页&#xff1a;小刘主页 ♥️每天分享云计算网络运维课堂笔记&#xff0c;努力不一定有收获&#xff0c;但一定会有收获加油&#xff01;一起努力&#xff0c;共赴美好人生&#xff01; ♥️夕阳下&#xff0c;是最美的绽放&#xff0…

Ajax--》请求操作以及跨域相关讲解

目录 jQuery中的Ajax 请求超时与网络异常处理 取消请求 Ajax请求—fetch() 跨域 jQuery中的Ajax 在jQuery中应该如何发送Ajax请求呢&#xff1f;看到这篇文章你就能大概了解到如何在jQuery中发送Ajax。 要想使用jQuery框架&#xff0c;肯定是需要引进jQuery资源的&#…

CSS实现单行、多行文本溢出显示省略号(…)

提示&#xff1a;文章写完后&#xff0c;目录可以自动生成&#xff0c;如何生成可参考右边的帮助文档 文章目录一、单行超出显示省略号二、多行超出显示省略号一、单行超出显示省略号 描述&#xff1a;如果文字超出父元素指定宽度&#xff0c;文字会自动换行&#xff0c;而连续…

Node.js——文件模块和路径模块(读写文件,处理路径)

个人简介 &#x1f440;个人主页&#xff1a; 前端杂货铺 &#x1f64b;‍♂️学习方向&#xff1a; 主攻前端方向&#xff0c;也会涉及到服务端 &#x1f4c3;个人状态&#xff1a; 在校大学生一枚&#xff0c;已拿 offer&#xff08;秋招&#xff09; &#x1f947;推荐学习&…

MySQL - 死锁的产生及解决方案

MySQL - 死锁的产生及解决方案1. 死锁与产生死锁的四个必要条件1.1 什么是死锁1.2 死锁产生的4个必要条件2. 死锁案例2.1 表锁死锁2.2 行锁死锁2.3 共享锁转换为排他锁3. 死锁排查4. 实例分析4.1 案例描述4.2 案例死锁问题复现4.3 死锁排查4.4 解决死锁5. 如何避免死锁1. 死锁与…

【保姆级】JMeter Mqtt 压测配置

忽然有个紧急任务要对某个服务做MQTT做压测&#xff0c;紧急实操下JMeter&#xff0c;这里记录下非专业测试员的测试过程、(▽&#xff40;)&#xff0c;欢迎&#x1f44f;大家检查指点(&#xffe3;∇&#xffe3;)/下载⏬工具JMeter官方下载地址https://jmeter.apache.org/do…

【前端】1.学习了一段时间的vue,总结一下Vue书写规范

学习了一段时间的vue&#xff0c;总结一下Vue书写规范命名规范普通变量命名规范常量命名规范组件命名规范method 方法命名命名规范views 下的文件命名props 命名规范结构化规范命名规范 在团体开发项目中&#xff0c;为了团队所有成员书写可维护的代码&#xff0c;而不是一次性…

JS入门到精通完整版

前言 JavaScript&#xff08;简称“JS”&#xff09; 是一种具有函数优先的轻量级&#xff0c;解释型或即时编译型的编程语言。它是作为开发Web页面的脚本语言而出名&#xff0c;JavaScript 基于原型编程、多范式的动态脚本语言&#xff0c;并且支持面向对象、命令式、声明式、…

【前端灵魂脚本语言JavaScript①】——JS引入方式

&#x1f41a; 作者: 阿伟 &#x1f482; 个人主页: Flyme awei &#x1f40b; 希望大家多多支持&#x1f618;一起进步呀&#xff01; &#x1f4ac; 文章对你有帮助&#x1f449;关注✨点赞&#x1f44d;收藏&#x1f4c2; JavaScript引入 文章目录JavaScript引入一、JS介绍二…

PTA第六章作业详解

&#x1f680;write in front&#x1f680; &#x1f4dd;个人主页&#xff1a;认真写博客的夏目浅石. &#x1f381;欢迎各位→点赞&#x1f44d; 收藏⭐️ 留言&#x1f4dd; &#x1f4e3;系列专栏&#xff1a;夏目的作业 &#x1f4ac;总结&#xff1a;希望你看完之后&am…

vue+uniapp瀑布流布局多种实现方式

文章目录前言一、实现原理二、代码实现1.方式1&#xff08;图片高度累加比较法&#xff09;2.方式2&#xff08;父元素高度比较法&#xff09;三.uniapp实现代码实现四、多列实现代码实现前言 瀑布流布局是网页设计常见的一种布局&#xff0c;一般用于图片多列展示。列宽固定&…