关于java:PDF技术方案wkhtmltopdf

48次阅读

共计 11855 个字符,预计需要花费 30 分钟才能阅读完成。

前端实现导出 PDF 产品报告,存在几个问题:1. 是图片版的 PDF; 2. PDF 太大,会卡;3. 可能会把文字裁剪分页;4. 无奈满足平台提供 Api 接口服务。外围就是问题 3 和问题 4,于是,思考后端服务实现导出 PDF 产品报告的计划。Java 实现 HTML 转 PDF 技术选型

举荐应用 wkhtmltopdf, Itext,但 wkhtmltopdf 开源收费,Itext 须要思考版权参考:https://blog.csdn.net/weixin_43981813/article/details/128135730 参考:https://www.cnblogs.com/IT-study/p/13706690.html 技术实现计划技术采纳模板引擎 + PDF 插件实现。开发好页面模板,Thymeleaf 模板引擎渲染动态 HTML 文件,wkhtmltopdf 将动态的 HTML 生成 PDF 文件。整体计划流程如下:

后盾应用 Thymeleaf 模板生成 Html 报告页面 PDF 接口依据 ID 查问报告数据调用 wkhtmltopdf 工具 将 Html 转为 PDF 文件对于 wkhtmltopdf 参数文档: https://wkhtmltopdf.org/usage/wkhtmltopdf.txtwkhtmltopdf 装置 Yum 装置 (可能是老版本存在 bug, 不举荐)yum -y install wkhtmltopdfrpm 包装置下载最新依照包,如 wget https://objects.githubusercontent.com/github-production-relea… 先装置依赖包 yum install -y fontconfig libX11 libXext libXrender libjpeg libpng xorg-x11-fonts-75dpi xorg-x11-fonts-Type1wkhtmltox 装置 rpm -ivh wkhtmltox-0.12.6-1.centos8.x86_64.rpm 若须要门路执行,可配置 cp /usr/local/bin/wkhtmltopdf /usr/bin/wkhtmltopdf 内网装置先下载依赖包到指定目录,例如下载 openssl 依赖包到指定目录 yum install –downloadonly –downloaddir=/usr/soft/wktooltopdf/ openssl 之后,拷贝依赖包到内网环境,执行命令 rpm -ivh –force –nodeps .rpmrpm -ivh –force –nodeps .rpm 常见问题短少依赖包手动装置依赖包 FAQ-linux 装置 wkhtmltopdf 中文乱码或者空白解决办法参考:https://www.cnblogs.com/jluo/p/17403785.html 装置中文字体,或复制已有字体关上 windows c:\Windows\fonts\simsun.ttc
拷贝到 linux 服务器 /usr/share/fonts/ 目录下, 再次生成 pdf 中文显示失常呈现谬误: wkhtmltopdf:cannot connect to X server 参考:https://www.jianshu.com/p/2cfc02961528 需再装置 xvfbyum install xorg-x11-server-Xvfb 在 /usr/bin/ 目录下生成脚本 wkhtmltopdf.sh 并写入命令 sudo vim /usr/bin/wkhtmltopdf.sh
命令:
xvfb-run -a –server-args=”-screen 0, 1024x768x24″ /usr/bin/wkhtmltopdf -q $* 更改文件权限并建设连贯 chmod a+x /usr/bin/wkhtmltopdf.sh
ln -s /usr/bin/wkhtmltopdf.sh /usr/local/bin/wkhtmltopdf 中文字体装置若呈现中文乱码,则可能是短少字体阿里巴巴普惠体 2.0,收费无版权,好用下载地址: https://done.alibabadesign.com/puhuiti2.0 介绍阐明: https://fonts.adobe.com/fonts/alibaba-puhuiti 设置字体集 font-family: alibaba-puhuiti, sans-serif;
font-style: normal;
font-weight: 300; 开发代码及配置动态资源目录位于 *-model 工程下的资源文件,包含以下目录 templates/ – 模板文件
static/ – 动态资源文件若前端有批改调整,需将更新的文件复制到 *-model 工程下对应目录,动态资源复制计划 1:maven 插件配置, 用于复制公共的资源 pom.xml 减少插件配置 <plugin> <!– 该插件的作用是用于复制 PDF 模板资源文件 –>

<artifactId>maven-resources-plugin</artifactId>
<executions>
    <execution>
        <id>copy-resources</id>
        <phase>package</phase>
        <goals>
            <goal>copy-resources</goal>
        </goals>
        <configuration>
            <resources>
                <resource>
                    <directory>../../*-model/src/main/resources</directory>  <!-- 指定相对路径, 复制 *-model 下的模板动态资源 -->
                    <includes>  <!-- 复制模板文件和动态资源文件 -->
                        <include>templates/**</include>
                        <include>static/**</include>
                    </includes>
                </resource>
            </resources>
            <outputDirectory>src/main/resources</outputDirectory>  <!-- 指定输入目录, 复制到以后工程资源模型下, 用于下一步打包 -->
            <skip>true</skip>  <!-- 跳过执行, 已配置了 package.xml, 间接复制到打包文件 -->
        </configuration>
    </execution>
</executions>

</plugin> 动态资源复制计划 2:自定义的打包配置,减少资源复制举荐应用此办法,间接复制资源并打包到指标 zip 包门路:/src/main/assemble/package.xml,减少配置 <fileSet> <!– 该插件的作用是用于复制 PDF 模板资源文件 –>

  <directory>../../*-model/src/main/resources</directory>
  <outputDirectory>\</outputDirectory>
  <includes>  <!-- 复制模板文件和动态资源文件 -->
      <include>templates/**</include>
      <include>static/**</include>
  </includes>

</fileSet>Java 工具类因为模板引擎对 JS 的反对无限,固减少 Java 工具类,用于模板中解决数据(模板引擎是在服务端执行, 可执行 Java 代码)

参考 HtmlThymeleafHelper 配置, 留神 ModelAndView 中返回工具列后端开发 pom.xml 依赖 <dependency>

<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-thymeleaf</artifactId>

</dependency>
<dependency>

<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>

</dependency> 开发模板 <!DOCTYPE html>
<html xmlns:th=”http://www.thymeleaf.org”>
<head>

<meta charset="UTF-8">
<title>title</title>

</head>
<body>
Hello Thymeleaf
<div th:text=”${name}”> 张三(离线数据)</div>
</body>
</html> 后端接口解决 ModelAndView// 接口返回 ModelAndView, 指定模板, 设置数据
@GetMapping(“/template/render”)
public ModelAndView templateReportDetail(HttpServletRequest request, @RequestParam String reportId) {

this.initJavaEnv(request);
return this.renderModelAndView("TemplateReportDetail", reportId);

}

// 通过 Session 设置 Java 对象, 用于模板中执行 Java 办法
private void initJavaEnv(HttpServletRequest request) {

HtmlThymeleafHelper helper = Singleton.get(HtmlThymeleafHelper.class);
request.getSession().setAttribute("helper", helper);

}

// 创立一个模型视图对象
ModelAndView mav = new ModelAndView();
// 获取到查问的数据
Object data = ret.getRetObject();
// 将数据搁置到 ModelAndView 对象 view 中, 第二个参数能够是任何 java 类型
mav.addObject(“sourceData”, data);
// 放入模板门路
mav.setViewName(“template”);
// 返回 ModelAndView 对象 mav
return mav;SpringBoot yml 配置 spring:
mvc:

# 增加 static 文件夹下其余文件夹可拜访
static-path-pattern: /project/static/**
# 自定义配置项, 指定模板门路
base-template-path: /project/template

thymeleaf:

cache: true
mode: HTML5
suffix: .html
prefix: classpath:/templates/
encoding: UTF-8
servlet:
  content-type: text/html 对于模板引擎 Thymeleaf 什么是 Thymeleaf?Thymeleaf 官网是这么解释的:Thymeleaf is a modern server-side Java template engine for both web and standalone environments. 译过去就是:Thymeleaf 是实用于 Web 和独立环境的古代服务器端 Java 模板引擎什么是模板引擎?模板引擎(这里特指用于 Web 开发的模板引擎)是为了使用户界面与业务数据(内容)拆散而产生的,它能够生成特定格局的文档,用于网站的模板引擎就会生成一个规范的 html 文档。从字面上了解模板引擎,最重要的就是模板二字,这个意思就是做好一个模板后套入对应地位的数据,最终以 html 的格局展现进去,这就是模板引擎的作用。不仅如此,在 Java 中模板引擎还有很多,模板引擎是动静网页倒退提高的产物,在最后并且流传度最广的 jsp 它就是一个模板引擎。jsp 是晚期官网规范的模板,然而因为 jsp 的毛病比拟多也挺重大的,所以很多人弃用 jsp 选用第三方的模板引擎,市面上开源的第三方的模板引擎也比拟多,有 Thymeleaf、FreeMaker、Velocity 等模板引擎受众较广。听完了模板引擎的介绍,置信你也很容易明确了模板引擎在 web 畛域的次要作用:让网站实现界面和数据拆散,这样大大提高了开发效率,让代码重用更加容易。

Model、ModelMap、ModelAndViewModel 一般来说,能够用 Model 来接管各种类型的数据,如果应用来接管一组数据 List,那么这个时候的 Model 实际上是 ModelMapModelMap 次要用于传递管制办法解决数据到后果页面,也就是说咱们把后果页面上须要的数据放到 ModelMap 对象中即可,他的作用相似于 request 对象的 setAttribute 办法的作用:用来在一个申请过程中传递解决的数据 ModelMap 或者 Model 通过 addAttribute 办法向页面传递参数 ModelAndView 指模型和视图的汇合,既蕴含 模型 又蕴含 视图 Model 和 ModelMap 无需用户本人创立,而且须要 return 返回指定的页面门路 Model 和 ModelMap 无需用户本人创立,而且须要 return 返回指定的页面门路

public String listCategory2(Model model) {

// 接管查问的信息
List<Category> cs2= categoryService.list();
// 封装了查问的数据
model.addAttribute("test", cs2);
// 重要!!须要给出返回 model 跳转的门路
return "listCategory2";

}

ModelAndView 的实例是须要咱们手动 new 的,这也是和 ModelMap 的一个区别。
而且,ModelAndView 能够本人寻址,只须要 return 返回其对象即可。

public ModelAndView listCategory(){
// 创立一个模型视图对象

ModelAndView mav = new ModelAndView();
// 获取到查问的数据
List<Category> cs= categoryService.list();

// // 将数据搁置到 ModelAndView 对象 view 中, 第二个参数能够是任何 java 类型
mav.addObject("cs", cs);
// 放入 jsp 门路
mav.setViewName("listCategory");
 // 返回 ModelAndView 对象 mav
return mav;

}参考:https://cloud.tencent.com/developer/article/1698750Thymeleaf 罕用标签标签作用示例 th:id 替换 id<input th:id=”${user.id}”/>th:text 文本替换 <p text:=”${user.name}”>bigsai</p>th:utext 反对 html 的文本替换 <p utext:=”${htmlcontent}”>content</p>th:object 替换对象 <div th:object=”${user}”></div>th:value 替换值 <input th:value=”${user.name}” >th:each 迭代 <tr th:each=”student:${user}” >th:href 替换超链接超链接 th:src 替换资源 <script type=”text/javascript” th:src=”@{index.js}”></script> 七大根底对象:${#ctx} 上下文对象,可用于获取其它内置对象。${#vars}: 上下文变量。${#locale}:上下文区域设置。${#request}: HttpServletRequest 对象。${#response}: HttpServletResponse 对象。${#session}: HttpSession 对象。${#servletContext}: ServletContext 对象。罕用的工具类:#strings:字符串工具类 #lists:List 工具类 #arrays:数组工具类#sets:Set 工具类 #maps:罕用 Map 办法。#objects:个别对象类,通常用来判断非空#bools:罕用的布尔方法。#execInfo:获取页面模板的解决信息。#messages:在变量表达式中获取内部音讯的办法,与应用#{…}语法获取的办法雷同。#uris:本义局部 URL / URI 的办法。#conversions:用于执行已配置的转换服务的办法。#dates:工夫操作和工夫格式化等。#calendars:用于更简单工夫的格式化。#numbers:格式化数字对象的办法。#aggregates:在数组或汇合上创立聚合的办法。#ids:解决可能反复的 id 属性的办法。引入 css(必须要在标签中加上 rel 属性)
<link rel=”stylesheet” th:href=”@{index.css}”>
<link th:href=”@{/static/css/index.css}” type=”text/css” rel=”stylesheet”>

引入 JavaScript:
<script type=”text/javascript” th:src=”@{index.js}”></script>
<script type=”text/javascript” th:src=”@{/js/jquery.js}”></script>

超链接:
超链接

变量表达式: ${…}
在 Thymeleaf 中能够通过 ${…}进行取值,这点和 ONGL 表达式语法统一

取 JavaBean 对象:
应用 ${对象名. 对象属性}或者 ${对象名 [‘ 对象属性 ’]} 来取值
如果该 JavaBean 如果写了 get 办法,也能够通过 get 办法取值例如 ${对象.get 办法名}

<td th:text=”${user.name}”></td>
<td th:text=”${user[‘age’]}”></td>
<td th:text=”${user.getDetail()}”></td>

取 List 汇合 (each):
因为 List 汇合是个有序列表,要遍历 List 对其中对象取值,而遍历须要用到标签:th:each,
具体应用为 <tr th:each=”item:${userlist}”>, 其中 item 就相当于遍历每一次的对象名

间接取 Map:
很多时候咱们不存 JavaBean 而是将一些值放入 Map 中,再将 Map 存在 Model 中,咱们就须要对 Map 取值,
能够 ${Map 名 [‘key’]} 取值。也能够 ${Map 名.key} 取值,当然也能够 ${map.get(‘key’)}(java 语法) 取值

place:
feeling:

参考:https://developer.aliyun.com/article/769977Thymeleaf 管制解决 <input type=”text” name=”menuName” disabled th:value=”${result?.data?.menuName}” class=”layui-input”>

? 会判断对象是否为空,如果为空就不会持续取值

SPEL 解决 null 值

变量为 null 时,显示默认值
name?:’Unknown’
当 name 变量为 null 时,显示值 Unknown。等价于 name?name:’Unknown’。

对象为 null 时,防止调用办法或属性出错
placeOfBirth?.city
当 placeOfBirth 为 null 时,不再持续调用属性 city。

code?.toUpperCase()
当 code 为 null 时,不再持续调用办法 toUpperCase。

Map 获取的元素为 null
当 map 中没有名为 name 的元素时,这样写会报错 map.name。
平安的写法是这样:map[‘name’]。

如果 map 中的元素为对象时,能够这样写:map[‘user’]?.name。

List 类型数组越界
数组越界时,谬误是这样的:

Caused by: org.thymeleaf.exceptions.TemplateProcessingException: Exception evaluating SpringEL expression: “slist[2].score” (template: “exam/papers/edit” – line 117, col 92)
Caused by: org.springframework.expression.spel.SpelEvaluationException: EL1025E: The collection has ‘1’ elements, index ‘2’ is invalid

SPEL 是这样的 slist[2].score,但 slist 不够 3 个元素(EL1025E: The collection has ‘1’ elements, index ‘2’ is invalid),因而数组越界了。

解决办法:增加数组大小的判断。下面的状况下,用 #lists.size(slist)>=3?slist[2]?.score:0 替换 slist[2].score 参考:https://blog.csdn.net/sayyy/article/details/109385456 参考:https://zhuanlan.zhihu.com/p/90642654Thymeleaf 调用 Java 办法 1. Java 对象示例存入 Thymeleaf Context 域中,代码层面即为:将实例对象存入 Request 对象中

MethodService md = new MethodService();
mmap.put(“methodService”,md);
mmap.put(“proofsList”,proofsList);

<label class=”checkbox-inline i-checks” th:each=”data : ${list}”>
<input th:attr=”checked=${methodService.contains(data.id,proofsList)?true:false}” type=”checkbox” name=”proofs[]” th:value=”${data.id}” id=”inlineCheckbox1″ />
</lable>
Thymeleaf 动静增加款式 <li class=”treeview” th:classappend=”${tree == ‘index’}?’active'”></li>
或者
<li class=”treeview” th:classappend=”${tree == ‘index’?’active’:”}”></li>

动静绑定款式
<li th:class=”${tree == ‘index’?’active’:”}”></li>Thymeleaf 数组解决 // 数组长度
<p>The greatest <span th:text=”${#arrays.length(continents)}”></span> continents.</p>
// 数组蕴含
<p>Europe is a continent: <span th:text=”${#arrays.contains(continents, ‘Europe’)}”></span>.</p>
// 数组判空
<p>Array of continents is empty <span th:text=”${#arrays.isEmpty(continents)}”></span>.</p>
Thymeleaf 遍历生成简单的表格

<div th:remove=”tag” th:if=”*{#lists.isEmpty(institution)}”>

</div>
<div th:remove=”tag” th:if=”*{not #lists.isEmpty(institution)}” th:each=”institution:${institution}”>

</div>

机构 年份 得分 全球排名
无排名信息

th:remove=”tag”
它在这的作用是生成表格后把 div 删除,但不删除子元素

th:if=”*{#lists.isEmpty(institution)}”
判断从后盾获取的数据为空,空则不渲染 tr 标签

th:if=”*{not #lists.isEmpty(institution)}”
判断从后盾获取的数据不为空,不为空则渲染 tr 标签 <div th:remove=”tag” th:each=”downPriceEntry,stats:${appPriceInfoVO.downPriceMap}”

 th:with="appName = ${downPriceEntry.key}, appChangeNum = ${downPriceEntry.value.size()},
      appInfo0 = ${downPriceEntry.value.get(0)}, downPriceList = ${downPriceEntry.value}">
<tr th:if="${appPriceInfoVO.downNum}>0">
    <td class="btbg1" th:text="价格下降" th:rowspan="${appPriceInfoVO.downNum}" th:if="${stats.first}"></td>
    <td th:text="${appName}" th:rowspan="${appChangeNum}"
        th:class="${stats.index % 2 == 0} ?'btbg4':'btbg3'"></td>
    <td th:class="${stats.index % 2 == 0} ?'btbg4':'btbg3'"th:text="${appInfo0.price}"></td>
    <td th:class="${stats.index % 2 == 0} ?'btbg4':'btbg3'"th:text="${appInfo0.version}"></td>
    <td th:class="${stats.index % 2 == 0} ?'btbg4':'btbg3'"th:text="${appInfo0.createTime}"></td>
    <td th:class="${stats.index % 2 == 0} ?'btbg4':'btbg3'"th:text="${appInfo0.language}"></td>
    <td th:class="${stats.index % 2 == 0} ?'btbg4':'btbg3'">
        <a th:href="${appInfo0.url}" target="_blank" th:text="${appInfo0.name}"></a>
    </td>
</tr>

<tr th:each="downPriceAppInfo,stat : ${downPriceList}" th:if="${!stat.first}">
    <td th:class="${stats.index % 2 == 0} ?'btbg4':'btbg3'"th:text="${downPriceAppInfo.price}"></td>
    <td th:class="${stats.index % 2 == 0} ?'btbg4':'btbg3'"th:text="${downPriceAppInfo.version}"></td>
    <td th:class="${stats.index % 2 == 0} ?'btbg4':'btbg3'"th:text="${downPriceAppInfo.createTime}"></td>
    <td th:class="${stats.index % 2 == 0} ?'btbg4':'btbg3'"th:text="${downPriceAppInfo.language}"></td>
    <td th:class="${stats.index % 2 == 0} ?'btbg4':'btbg3'">
        <a th:href="${downPriceAppInfo.url}" target="_blank" th:text="${downPriceAppInfo.name}"></a>
    </td>
</tr>

</div>

th:remove:会移除该标签行,不会移除其子标签

th:each:迭代汇合或者数组

th:with:长期变量的申明

colspan 合并单元格 列
rowspan 合并单元格 行常见问题 wkhtmltopdf 生成 PDF 的表格行内呈现分页符、表头反复、截断等减少表格款式 thead {

display: table-row-group;

}
tr {

page-break-before: always;
page-break-after: always;
page-break-inside: avoid;

}
table {

word-wrap: break-word;

}
table td {

word-break: break-all;

}阐明:wkhtmltopdf 对表格的反对很差,会导致文件很大,长表格兼容性等问题参考:https://blog.csdn.net/yellowatumn/article/details/87864601

正文完
 0