通过freemarker与itext实现html转pdf

发布时间 2023-10-07 11:40:29作者: changlinlo

因果

因一些合同、发票等单据需要生成pdf文档,并且审核通过后需给该pdf文档盖章,所以需要生成pdf的解决方案。

实践中遇到的问题

  1. 其实单独使用itext也可以实现pdf的生成,但通过文本域进行实现数据的动态替换存在局限性,如动态表格就不能友好的进行动态生成,生成了也是个绝对定位,假如文档后面还存在其它内容就会出现文本重合问题。

单独通过itext实现生成pdf遇到的动态表格问题

2.参考文章https://blog.csdn.net/u014331138/article/details/108361728生成的pdf文档,中文不显示,这个问题我找了很多文档,很多方法,最后只有微软雅黑'Microsoft YaHei'才生效;因需求必须用宋体'SimSum',所以只能另找方法解决;

生成的pdf文档中文不显示问题

经过一系列的bug与困难,最后终于通过freemarker+html2pdf+font-asian实现了pdf文档的生成,也满足了业务的需求,让我们继续往下看吧。

成功结果

思路

itext通过文本域进行替换值的局限性,所以通过freemarker进行值的替换。

  1. 设计好ftl模板,然后通过freemarker进行数据的写入,生成一个替换好值的html文档,并保存到指定位置;
  2. 再通过读取已经写好数据的html文档进行转换成pdf;
  3. 没了;

实现

  1. 准备工作 - 模版设计,先设计html模板,通过[freemarker](http://freemarker.foofun.cn/ref_directives.html)的指令语法进行配置,配置好后将html改为ftl模板(与el表达式类似),在resources目录下建一个templates将你所设计的ftl模板放入进去;
freemarker模板
  1. 准备工作 - 语言存放,在resources目录下建一个fonts将你所需的字体放入进去;
freemarker模板
  1. 引入依赖
 <!-- itext7html转pdf  -->
        <dependency>
            <groupId>com.itextpdf</groupId>
            <artifactId>html2pdf</artifactId>
            <version>3.0.2</version>
        </dependency>
        
        <!-- 中文字体支持 -->
        <dependency>
            <groupId>com.itextpdf</groupId>
            <artifactId>font-asian</artifactId>
            <version>7.1.13</version>
        </dependency>

        <!--freemarker模板-->
        <dependency>
            <groupId>org.freemarker</groupId>
            <artifactId>freemarker</artifactId>
        </dependency>
  1. 水印
import com.itextpdf.kernel.colors.WebColors;
import com.itextpdf.kernel.events.Event;
import com.itextpdf.kernel.events.IEventHandler;
import com.itextpdf.kernel.events.PdfDocumentEvent;
import com.itextpdf.kernel.font.PdfFont;
import com.itextpdf.kernel.font.PdfFontFactory;
import com.itextpdf.kernel.geom.Rectangle;
import com.itextpdf.kernel.pdf.PdfDocument;
import com.itextpdf.kernel.pdf.PdfPage;
import com.itextpdf.kernel.pdf.canvas.PdfCanvas;
import com.itextpdf.layout.Canvas;
import com.itextpdf.layout.element.Paragraph;
import com.itextpdf.layout.property.TextAlignment;
import com.itextpdf.layout.property.VerticalAlignment;

import java.io.IOException;

/**
 * 水印
 */
public class WaterMarkEventHandler implements IEventHandler
{

    /**
     * 水印内容
     */
    private String waterMarkContent;

    /**
     * 一页中有几列水印
     */
    private int waterMarkX;

    /**
     * 一页中每列有多少水印
     */
    private int waterMarkY;

    public WaterMarkEventHandler(String waterMarkContent)
    {
        this(waterMarkContent, 5, 5);
    }

    public WaterMarkEventHandler(String waterMarkContent, int waterMarkX, int waterMarkY)
    {
        this.waterMarkContent = waterMarkContent;
        this.waterMarkX = waterMarkX;
        this.waterMarkY = waterMarkY;
    }

    @Override
    public void handleEvent(Event event)
    {

        PdfDocumentEvent documentEvent = (PdfDocumentEvent) event;
        PdfDocument document = documentEvent.getDocument();
        PdfPage page = documentEvent.getPage();
        Rectangle pageSize = page.getPageSize();

        PdfFont pdfFont = null;
        try
        {
            pdfFont = PdfFontFactory.createFont("STSongStd-Light", "UniGB-UCS2-H", false);
        }
        catch (IOException e)
        {
            e.printStackTrace();
        }

        PdfCanvas pdfCanvas = new PdfCanvas(page.newContentStreamAfter(), page.getResources(), document);

        Paragraph waterMark = new Paragraph(waterMarkContent).setOpacity(0.5f);
        Canvas canvas = new Canvas(pdfCanvas, pageSize)
                .setFontColor(WebColors.getRGBColor("lightgray"))
                .setFontSize(16)
                .setFont(pdfFont);

        for (int i = 0; i < waterMarkX; i++)
        {
            for (int j = 0; j < waterMarkY; j++)
            {
                canvas.showTextAligned(waterMark, (150 + i * 300), (160 + j * 150), document.getNumberOfPages(), TextAlignment.CENTER, VerticalAlignment.BOTTOM, 120);
            }
        }
        canvas.close();
    }
}
  1. 页码
import com.itextpdf.kernel.events.Event;
import com.itextpdf.kernel.events.IEventHandler;
import com.itextpdf.kernel.events.PdfDocumentEvent;
import com.itextpdf.kernel.font.PdfFont;
import com.itextpdf.kernel.font.PdfFontFactory;
import com.itextpdf.kernel.geom.Rectangle;
import com.itextpdf.kernel.pdf.PdfDocument;
import com.itextpdf.kernel.pdf.PdfPage;
import com.itextpdf.kernel.pdf.canvas.PdfCanvas;
import com.itextpdf.layout.Canvas;
import com.itextpdf.layout.element.Paragraph;
import com.itextpdf.layout.property.TextAlignment;

import java.io.IOException;

/**
 * 页码
 */
public class PageEventHandler implements IEventHandler
{

    @Override
    public void handleEvent(Event event)
    {
        PdfDocumentEvent documentEvent = (PdfDocumentEvent) event;
        PdfDocument document = documentEvent.getDocument();
        PdfPage page = documentEvent.getPage();
        Rectangle pageSize = page.getPageSize();

        PdfFont pdfFont = null;
        try
        {
            pdfFont = PdfFontFactory.createFont("STSongStd-Light", "UniGB-UCS2-H", false);
        }
        catch (IOException e)
        {
            e.printStackTrace();
        }

        PdfCanvas pdfCanvas = new PdfCanvas(page.getLastContentStream(), page.getResources(), document);
        Canvas canvas = new Canvas(pdfCanvas, pageSize);
        float  x = (pageSize.getLeft() + pageSize.getRight()) / 2;
        float  y = pageSize.getBottom() + 15;
        Paragraph paragraph = new Paragraph("第" + document.getPageNumber(page) + "页/共" + document.getNumberOfPages() + "页")
                .setFontSize(10)
                .setFont(pdfFont);
        canvas.showTextAligned(paragraph, x, y, TextAlignment.CENTER);
        canvas.close();
    }
}
  1. FreeMarkerUtils工具类
    用来填充ftl的数据并生成html文件;

import freemarker.template.Configuration;
import freemarker.template.DefaultObjectWrapper;
import freemarker.template.Template;

import java.io.*;
import java.util.Map;

public class FreeMarkerUtils
{

    private static Template getTemplate(String template_path, String templateFileName)
    {
        Configuration configuration = new Configuration();
        Template template = null;
        try {
            configuration.setDirectoryForTemplateLoading(new File(template_path));
            configuration.setObjectWrapper(new DefaultObjectWrapper());
            configuration.setDefaultEncoding("UTF-8");   //设置编码格式
            //模板文件
            template = configuration.getTemplate(templateFileName + ".ftl");
        } catch (IOException e) {
            e.printStackTrace();
        }
        return template;
    }

    public static void genteratorFile(Map<String, Object> input, String template_path, String templateFileName, String savePath, String fileName)
    {
        Template template = getTemplate(template_path, templateFileName);
        File filePath = new File(savePath);
        if (!filePath.exists()) {
            filePath.mkdirs();
        }
        String filename = savePath + "\\" + fileName;
        File file = new File(filename);
        if (!file.exists()) {
            file.delete();
        }
        Writer writer = null;
        try {
            writer = new OutputStreamWriter(new FileOutputStream(filename), "UTF-8");
            template.process(input, writer);
        } catch (Exception e) {
            e.printStackTrace();
        }
    }

    public static String genterator(Map<String, Object> variables, String template_path, String templateFileName)  throws Exception
    {
        Template template = getTemplate(template_path, templateFileName);
        StringWriter stringWriter = new StringWriter();
        BufferedWriter writer = new BufferedWriter(stringWriter);
        template.setEncoding("UTF-8");
        template.process(variables, writer);
        String htmlStr = stringWriter.toString();
        writer.flush();
        writer.close();
        return htmlStr;
    }
}
  1. HtmlToPdfUtils工具类
    用于将已经填充好数据的html文件转换成pdf文档;
import com.itextpdf.html2pdf.ConverterProperties;
import com.itextpdf.html2pdf.HtmlConverter;
import com.itextpdf.io.font.PdfEncodings;
import com.itextpdf.kernel.events.PdfDocumentEvent;
import com.itextpdf.kernel.font.PdfFont;
import com.itextpdf.kernel.font.PdfFontFactory;
import com.itextpdf.kernel.geom.PageSize;
import com.itextpdf.kernel.pdf.PdfDocument;
import com.itextpdf.kernel.pdf.PdfWriter;
import com.itextpdf.layout.font.FontProvider;
import com.jyxpackaging.ecp.microservices.common.core.utils.StringUtils;
import lombok.extern.slf4j.Slf4j;

import java.io.*;
import java.util.regex.Matcher;
import java.util.regex.Pattern;

/**
 * Itext7转换工具类
 */
@Slf4j
public class HtmlToPdfUtils
{

    /**
     * html转pdf
     *
     * @param inputStream  输入流
     * @param waterMark    水印
     * @param fontPath     字体路径,ttc后缀的字体需要添加<b>,0<b/>
     * @param outputStream 输出流
     * @date : 2022/11/15 14:07
     */
    public static void convertToPdf(InputStream inputStream, String waterMark, String fontPath, OutputStream outputStream) throws IOException
    {

        PdfWriter pdfWriter = new PdfWriter(outputStream);
        PdfDocument pdfDocument = new PdfDocument(pdfWriter);
        // 设置为A4大小
        pdfDocument.setDefaultPageSize(PageSize.A4);
        // 添加水印
        pdfDocument.addEventHandler(PdfDocumentEvent.END_PAGE, new WaterMarkEventHandler(waterMark));

        // 添加中文字体支持
        ConverterProperties properties = new ConverterProperties();
        FontProvider fontProvider = new FontProvider();

        //        设置字体
        /* PdfFont sysFont = PdfFontFactory.createFont("STSongStd-Light", "UniGB-UCS2-H", false);
        fontProvider.addFont(sysFont.getFontProgram(), "UniGB-UCS2-H");*/

        // 添加自定义字体,例如微软雅黑
        if (StringUtils.isNotBlank(fontPath))
        {
            PdfFont microsoft = PdfFontFactory.createFont(fontPath, PdfEncodings.IDENTITY_H, false);
            fontProvider.addFont(microsoft.getFontProgram(), PdfEncodings.IDENTITY_H);
        }

        properties.setFontProvider(fontProvider);
        // 读取Html文件流,查找出当中的&nbsp;或出现类似的符号空格字符
        inputStream = readInputStrem(inputStream);
        if (inputStream != null)
        {
            // 生成pdf文档
            HtmlConverter.convertToPdf(inputStream, pdfDocument, properties);
            pdfWriter.close();
            pdfDocument.close();
            return;
        }
        else
        {
            log.error("转换失败!");
        }
    }

    /**
     * 读取HTML 流文件,并查询当中的&nbsp;或类似符号直接替换为空格
     *
     * @param inputStream
     * @return
     */
    private static InputStream readInputStrem(InputStream inputStream)
    {
        // 定义一些特殊字符的正则表达式 如:
        String regEx_special = "\\&[a-zA-Z]{1,10};";
        try
        {
            //<1>创建字节数组输出流,用来输出读取到的内容
            ByteArrayOutputStream baos = new ByteArrayOutputStream();
            //<2>创建缓存大小
            byte[] buffer = new byte[1024]; // 1KB
            //每次读取到内容的长度
            int len = -1;
            //<3>开始读取输入流中的内容
            while ((len = inputStream.read(buffer)) != -1) { //当等于-1说明没有数据可以读取了
                baos.write(buffer, 0, len);   //把读取到的内容写到输出流中
            }
            //<4> 把字节数组转换为字符串
            String content = baos.toString();
            //<5>关闭输入流和输出流
            //            inputStream.close();
            baos.close();
            //            log.info("读取的内容:{}", content);
            //            判断HTML内容是否具有HTML的特殊字符标记
            Pattern compile = Pattern.compile(regEx_special, Pattern.CASE_INSENSITIVE);
            Matcher matcher = compile.matcher(content);
            String replaceAll = matcher.replaceAll("");
            //            log.info("替换后的内容:{}", replaceAll);
            //            将字符串转化为输入流返回
            InputStream stringStream = getStringStream(replaceAll);
            //<6>返回结果
            return stringStream;
        }
        catch (Exception e)
        {
            e.printStackTrace();
            log.error("错误信息:{}", e.getMessage());
            return null;
        }
    }

    /**
     * 将一个字符串转化为输入流
     * @param sInputString 字符串
     * @return
     */
    public static InputStream getStringStream(String sInputString)
    {
        if (sInputString != null && !sInputString.trim().equals(""))
        {
            try
            {
                ByteArrayInputStream tInputStringStream = new ByteArrayInputStream(sInputString.getBytes());
                return tInputStringStream;
            }
            catch (Exception e)
            {
                e.printStackTrace();
            }
        }
        return null;
    }

}
  1. 测试
import cn.hutool.core.date.DateUtil;
import cn.hutool.core.io.resource.ResourceUtil;
import com.jyxpackaging.ecp.microservices.file.utils.DuizhangDomain;
import com.jyxpackaging.ecp.microservices.file.utils.FreeMarkerUtils;
import lombok.extern.slf4j.Slf4j;

import java.io.*;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;

@Slf4j
public class TestHtmlToPdf
{
    // 模板路径
    public final static String TEMP = "C:\\Users\\Administrator\\Desktop\\JYX_QYZT\\MicroServices\\base\\file\\src\\main\\resources\\templates\\";

    /**
     * 模板所需的数据
     * @return 数据
     */
    public static Map<String, Object> getContent()
    {
        // 从数据库中获取数据, 出于演示目的, 这里数据不从数据库获取, 而是直接写死
        Map input = new HashMap();
        input.put("采购单号", "Fuck you!采购单号~");
        input.put("单据日期", "Fuck you!2");
        input.put("工厂编号", "Fuck you!3");
        input.put("供应商名称", "Fuck you!4");
        input.put("工厂地址", "Fuck you!5");
        input.put("供货方地址", "Fuck you!6");
        input.put("公司电话", "Fuck you!7");
        input.put("供应商电话", "Fuck you!8");
        input.put("公司传真", "Fuck you!9");
        input.put("供应商传真", "Fuck you!10");
        input.put("公司联系人", "Fuck you!11");
        input.put("供应商联系人及手机号", "Fuck you!12");
        input.put("结算方式", "Fuck you!13");
        input.put("运费", "Fuck you!14");
        input.put("开机费", "Fuck you!15");
        input.put("备注", "Fuck you!16");
        input.put("合计金额", "Fuck you!17");
        input.put("采购金额", "Fuck you!18");
        // input.put("印章", "display:none");
        input.put("印章", "display:block");
        input.put("制表人", "Fuck you!20");
        // 表格渲染数据
        List<DuizhangDomain> dzList = new ArrayList<>();
        dzList.add(new DuizhangDomain(1,"工单号", "物料料号", "物料名称","规格",1.0,"单位",16.5800,16.5800,16.5800, DateUtil.now(),"我的字体太长了怎么办"));
        dzList.add(new DuizhangDomain(2,"工单号2", "物料料号2", "物料名称2","规格2",2.0,"单位2",16.5800,16.5800,16.5800, DateUtil.now(),"怎么办怎么办怎么办怎么办"));
        dzList.add(new DuizhangDomain(3,"工单号3", "物料料号3", "物料名称3","规格3",2.0,"单位3",16.5800,16.5800,16.5800, DateUtil.now(),"怎么办怎么办怎么办怎么办~"));
        dzList.add(new DuizhangDomain(4,"工单号4", "物料料号4", "物料名称4","规格4",2.0,"单位4",16.5800,16.5800,16.5800, DateUtil.now(),"怎么办怎么办怎么办怎么办怎么办怎么办怎么办"));
        input.put("users", dzList);

        return input;
    }


    public static void main(String[] args) throws IOException
    {
        long startTime = System.currentTimeMillis();
        // 指定模板渲染值并生成html文件至指定位置
        FreeMarkerUtils.genteratorFile(getContent(),
                TEMP,
                "ProcurementContractTemplate",
                TEMP,
                "afterGeneration.html");

        // 需转换的html文件名称
        String htmlFile = "afterGeneration.html";
        // 转换好pdf存储名称
        String pdfFile = "result.pdf";
        // 自定义水印
        String waterMarkText = "JYX";
        // 读取需转换的html文件
        InputStream inputStream = new FileInputStream(TEMP + htmlFile);
        // 写出pdf存储位置
        OutputStream outputStream = new FileOutputStream(TEMP + pdfFile);
        // 微软雅黑在windows系统里的位置如下,linux系统直接拷贝该文件放在linux目录下即可
        // String fontPath = "src/main/resources/font/STHeiti Light.ttc,0";
        String FONT_TTF_PATH = ResourceUtil.getResource("").getPath().replace("target/classes/", "").concat("src/main/resources/") + "fonts/simsun.ttc,0";
        // 开始转换html生成pdf文档
        HtmlToPdfUtils.convertToPdf(inputStream, waterMarkText, FONT_TTF_PATH, outputStream);
        log.info("转换结束,耗时:{}ms",System.currentTimeMillis()-startTime);
    }
}

结果

执行

生成的html

根据html转换后的pdf