java根据html生成pdf

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规则控制打印设置选项》

相关推荐
刀法如飞7 分钟前
Go数组去重的20种实现方式,AI时代解决问题的不同思路
后端·算法·go
AI人工智能+电脑小能手34 分钟前
【大白话说Java面试题】【Java基础篇】第30题:JDK动态代理和CGLIB动态代理有什么区别
java·开发语言·后端·面试·代理模式
言萧凡_CookieBoty36 分钟前
AI 编程省 Token 实战:从 Spec、上下文工程到模型分层的降本策略
前端·ai编程
swipe1 小时前
别再把 AI 聊天做成纯文本:从 agui 这个前后端项目,拆解“可感知工具调用”的流式 AI UI
后端·langchain·llm
GetcharZp1 小时前
GitHub 爆火!纯 Go 编写的文件同步神器 Syncthing,凭什么成为程序员的标配?
后端
hERS EOUS1 小时前
SpringBoot 使用 spring.profiles.active 来区分不同环境配置
spring boot·后端·spring
DFT计算杂谈1 小时前
wannier90 参数详解大全
java·前端·css·html·css3
LucianaiB1 小时前
我用飞书多维表做了一个 AI 活动推荐智能体:每天自动催我别错过截止日期!
后端
铁皮饭盒2 小时前
第2课:5分钟!用 Trae AI 生成你的第一个后端服务(Bunjs + Elysia)
前端·后端·全栈
金銀銅鐵2 小时前
[git] 浅解 git reset 命令
git·后端