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

相关推荐
雯0609~5 分钟前
网页F12:缓存的使用(设值、取值、删除)
前端·缓存
℘团子এ8 分钟前
vue3中如何上传文件到腾讯云的桶(cosbrowser)
前端·javascript·腾讯云
学习前端的小z14 分钟前
【前端】深入理解 JavaScript 逻辑运算符的优先级与短路求值机制
开发语言·前端·javascript
XINGTECODE22 分钟前
海盗王集成网关和商城服务端功能golang版
开发语言·后端·golang
程序猿进阶28 分钟前
堆外内存泄露排查经历
java·jvm·后端·面试·性能优化·oom·内存泄露
FIN技术铺33 分钟前
Spring Boot框架Starter组件整理
java·spring boot·后端
彭世瑜38 分钟前
ts: TypeScript跳过检查/忽略类型检查
前端·javascript·typescript
FØund40439 分钟前
antd form.setFieldsValue问题总结
前端·react.js·typescript·html
Backstroke fish39 分钟前
Token刷新机制
前端·javascript·vue.js·typescript·vue
小五Five40 分钟前
TypeScript项目中Axios的封装
开发语言·前端·javascript