Java 根据前端模板生成 pdf
有个需求,就是根据前端模板生成 pdf。这简单啊,作为一个前端基本的素养,一会就写完页面了。还原度可以说极度高。然后就把代码给了后端,后端把给的代码转为 ftl 文件,剩下的事情也就没啥前端的事了吧。
不一会的功夫就听到后端说不对啊,和给的样式完全不对啊,不能吧?一看果真是完全不对,在我这儿对的呢,在他那儿不对,两个人撕逼之后,后端把以前的代码拿出来让我对比,害,不屑于看,咋就能不对,一顿拉扯下,我拉下后端的代码,改了起来。
运行之后一看,果然乱七八糟的,字体啥的都是黑体,然后尝试内联样式,发现也不起作用,看来是后端解析问题,也没啥直接百度,用到了 itextpdf;
一、引入一堆相关依赖
js
<dependency>
<groupId>com.itextpdf</groupId>
<artifactId>itextpdf</artifactId>
<version>5.5.13.3</version>
</dependency>
<dependency>
<groupId>com.itextpdf</groupId>
<artifactId>itext-asian</artifactId>
<version>5.2.0</version>
</dependency>
二、freemarker 模板工具类
由于使用的 freemarker,所以先处理模板
java
package com.example.freemarker.util;
import freemarker.cache.ClassTemplateLoader;
import freemarker.template.Configuration;
import freemarker.template.Template;
import java.io.IOException;
import java.io.StringWriter;
import java.io.Writer;
import java.util.Map;
/**
* freemarker模板工具类
*
* @author wang
* @since 2023-07-05
*/
public class FreeMarkerUtil {
private static Configuration config;
static {
config = new Configuration(Configuration.VERSION_2_3_0);
try {
// freemarker的模板目录
config.setTemplateLoader(new ClassTemplateLoader(FreeMarkerUtil.class,"/templates/"));
config.setDefaultEncoding("UTF-8");
//必须约定下时间 否则使用时间报错
config.setDateTimeFormat("yyyy-MM-dd HH:mm:ss");
config.setDateFormat("yyyy-MM-dd");
config.setTimeFormat("HH:mm:ss");
config.setLogTemplateExceptions(false);
config.setWrapUncheckedExceptions(true);
} catch (Exception e) {
e.printStackTrace();
}
}
/**
* freemarker渲染html
*
* @param data 数据
* @param templateName 模板名称
* @return
*/
public static String freeMarkerRender(Map<String, Object> data, String templateName) {
Writer out = new StringWriter();
try {
// 获取模板
Template template = config.getTemplate(templateName + ".ftl");
// 合并数据模型与模板 将合并后的数据和模板写入到流中
template.process(data, out);
out.flush();
return out.toString();
} catch (Exception e) {
e.printStackTrace();
return null;
} finally {
try {
out.close();
} catch (IOException ex) {
ex.printStackTrace();
}
}
}
}
调用方法生成 pdf
java
Map<String, Object> dataMap = new HashMap<>(){
// 所需数据
.......
};
String fileName = "test";
PdfUtil.createPdf(fileName,dataMap);
这里就是主要逻辑
- 先调用刚封装好的方法获取到 ftl 模板,解析成 html
- 把 html 代码传入到渲染器中
- 就这样是不是很简单,如果格式不对可等比例转换 a4 大小
java
public static void createPdf(String fileName, Map<String, Object> templateData){
String template = null;
Map<String,Object> resultMap = new HashMap<>();
template = FreeMarkerUtil.freeMarkerRender(templateData,FRANCE_INVOICE_TEMPLATE);
// 定义pdf文件尺寸,采用A4横切
Document document = new Document(PageSize.A4, 10, 10, 40, 60);
String result = createPdf(fileName, template, document,templateData);
String fileNamePdf = fileName + ".pdf";
File sourceFile = new File(fileNamePdf);
//处理
ITextRenderer renderer = new ITextRenderer();
FileOutputStream out = null;
try {
out = new FileOutputStream(sourceFile);
} catch (FileNotFoundException e) {
throw new RuntimeException(e);
}
try {
// 把html代码传入渲染器中
renderer.setDocumentFromString(template);
renderer.layout();
renderer.createPDF(out, false);
renderer.finishPDF();
out.flush();
out.close();
} catch (Exception e) {
throw new RuntimeException(e);
}
}
经过上述操作后,打开 pdf 一看果然,大概样式有了点,就是布局乱了点,后来一查才发现对 html 要求比较严格,flex 布局啥的好像都不能用,然后,一步步改成 float,最后效果一看果然可以,然后发现不是 A4 大小,又转换了一波,经过后续的改进,应该是 css 问题才导致转换,写法严谨正确的话不需要特别的转换一次
java
private static String convert(String source,
String target,
Map<String, Object> dataMap,Document document) throws IOException{
// 处理后的pdf
// source 需要转换的文件
File targeFileToA4 = new File(target);
File sourceFile = new File(source);
try{
PdfReader pdfReader = new PdfReader(source);
PdfWriter writer =
PdfWriter.getInstance(document,
Files.newOutputStream(Paths.get(target)));
int pageNum = pdfReader.getNumberOfPages();
document.open();
PdfContentByte cb = writer.getDirectContent();
for(int i = 1; i <= pdfReader.getNumberOfPages(); i++){
float moveDown = 0;
PdfImportedPage page = writer.getImportedPage(pdfReader, i);
float width = page.getWidth();
float height = page.getHeight();
document.setPageSize(PageSize.A4);
document.newPage();
float widthScale = getWidthScale(width);
float heightScale = getHeightScale(height);
// 添加page
cb.addTemplate(page, widthScale, 0, 0, heightScale, 0, 0);
}
document.close();
// 关闭流
pdfReader.close();
}catch(Exception ex){
ex.printStackTrace();
}
return uploadFile(targeFileToA4);
}
private static float getWidthScale(float width){
float scale = PageSize.A4.getWidth() / width;
return scale;
}
private static float getHeightScale(float height){
float scale = PageSize.A4.getHeight() / height;
return scale;
}
就这样装交给后端,当然还是样式不正确,把修改的 java 代码也给他了,效果达成。又白白做了一次贡献。
他那样的写法我后来看了一下,得改一堆配置,我试了下,效果也能达成,但是用 ITextRenderer 比较简单,也符合需求,他最后也采用了这个方法。
正松口气时,需求又来了。要把顶部作为页眉,设置页眉。table 如果多页的话,如果当页有 tabody,那么表头也应该设置到当前页。
此时的我想着应该没我啥事了,然后就没管了。后来着急上线,听他的语气挺烦的,说是要一个一个定位上去,效果和 html 页面生成的不同。但是勉强上线了。
后老我也大概看了一下,就是使用 setPageEvent 方法给设置页眉和页脚,然后页眉的话第一页正常使用 html 就行,第二页的话把第一页的公共样式在 onEndPage 方法里把顶部公共样式用提供的方法一行一行写进去,这样的话效果就不同了,而且数据比较多,所以,又使用了方法进行便宜内容
java
for(int i = 1; i <= pdfReader.getNumberOfPages(); i++){
PdfImportedPage page = writer.getImportedPage(pdfReader, i);
float width = page.getWidth();
float height = page.getHeight();
//横向 比例不对 强制A4效果
doc.setPageSize(PageSize.A4);
doc.newPage();
//计算比例
float widthScale = getWidthScale(width);
float heightScale = getHeightScale(height);
//二维图像仿射变换:https://www.cnblogs.com/v2m_/archive/2013/05/09/3070187.html
if(i>1){
cb.addTemplate(page, widthScale, 0, 0, heightScale,0,-200f);
}else {
cb.addTemplate(page, widthScale, 0, 0, heightScale,0,0);
}
}
上述代码只是大概。第二页的时候偏移,使得页眉能够完全展示出来,但是其实问题就暴露出来了,偏移后的内容并不会自动换页,而是挤压出去,导致展示不全。
然后便开始了漫长的解决之路,最先想到的方法就是把 顶部公共的抽出来,然后计算高度,从第二页开始不断裁剪拼接成一个新页面然后 newPage() ,放进去,我感觉这个可行。但是太麻烦,就懒得算了。
后来看到说,iText 支持一个 CSS 属性,只需要给你需要重复表头表尾的 table 标签设置 css 属性"repeat-header:yes"或"repeat-footer:yes"即可,然后将你需要重复的表头放在 thead 标签内,表尾放在 tfoot 标签内。
html
<table style="repeat-header:yes;repeat-footer:yes;">
<thead>
<tr>
<th colspan="3">DESCRIPTION</th>
<th>QUANTITY</th>
<th>UNIT PRICE</th>
<th>AMOUNT</th>
</tr>
</thead>
<!-- 发票第四部分 -->
<tbody>
<tr class="el-descriptions-row">
<td
colspan="3"
class="el-descriptions-item__cell el-descriptions-item__content"
>
bbbbb
<br />
fsjdfjksldfkjdsklfjkl
</td>
<td
colspan="1"
class="el-descriptions-item__cell el-descriptions-item__content"
>
1 pezzi
</td>
<td
colspan="1"
class="el-descriptions-item__cell el-descriptions-item__content"
>
€ 2
</td>
<td
colspan="1"
class="el-descriptions-item__cell el-descriptions-item__content"
>
€
</td>
</tr>
.....
</tbody>
<tbody>
<table></table>
</tbody>
</table>
这样子一看,表格表头确实起效果了,超过多页后表格会重复展示,然后表头的话也试着用css 去实现,
css
@page {
margin-top: 6cm;
@top-center {
content: element(header);
}
}
@page {
@bottom-center {
content: element(footer)
}
}
这里@page里设置margin-top是因为顶部高度比较大,可以自行设置,大概效果就实现了。效果如下:
页脚的话就比较好实现了,毕竟页脚内容少,可以前端css实现,也可以后端itext 中设置。
唯一的不足就是顶部高度是固定的,用js动态修改不起效果。大佬们知道的可以提出来。
编辑于2023年,发布于2024年,记录一下,清空草稿箱。
借鉴《iText5使用HTML生成PDF设置重复表头和强制翻页》 ---- 《 css @page规则控制打印设置选项》