用于通过模板生成PDF,在项目中生成个人授权协议函、个人电子保单、流水报表,数据报表等,将HTML静态模板写出来后,将数据替换成动态数据即可。

<!-- html2pdf -->
<dependency>
    <groupId>org.xhtmlrenderer</groupId>
    <artifactId>flying-saucer-pdf</artifactId>
    <version>9.1.16</version>
</dependency>

<dependency>
    <groupId>com.itextpdf</groupId>
    <artifactId>itextpdf</artifactId>
    <version>5.5.13</version>
</dependency>

<!-- freemarker -->
<dependency>
    <groupId>org.freemarker</groupId>
    <artifactId>freemarker</artifactId>
    <version>2.3.31</version>
</dependency>

<!-- docx4j docx2pdf -->
<dependency>
   <groupId>org.docx4j</groupId>
   <artifactId>docx4j</artifactId>
   <version>6.1.2</version>
   <exclusions>
      <exclusion>
      <groupId>org.slf4j</groupId>
      <artifactId>slf4j-log4j12</artifactId>
      </exclusion>
   </exclusions>
</dependency>
<dependency>
   <groupId>org.docx4j</groupId>
   <artifactId>docx4j-export-fo</artifactId>
   <version>6.0.0</version>
</dependency>

 templates.ftl 放入resource/templates文件目录中

<!DOCTYPE html>
<html>
<head>
    <meta http-equiv="Content-Type" content="text/html; charset=utf-8"/>
    <title>Title</title>
    <style>
        @page {
            size:  210mm 290mm; /* size: 210mm 297mm;  设置纸张大小:A4(210mm 297mm)、A3(297mm 420mm) 横向则反过来 */
            /*margin-bottom: 1cm;*/
            /*padding: 1em;*/

            /*页眉*/
            /*@top-center {*/
            /*    content: counter(page);*/
            /*    font-family: SimSun;*/
            /*    font-size: 15px;*/
            /*    color:#000;*/
            /*};*/
            /*@top-right{*/
            /*    background: url("https://dpxdata.oss-cn-beijing.aliyuncs.com/upload/kbt/null_1660715685801.png") top right no-repeat;*/
            /*    width: 195px;height: 50px;*/
            /*    content: "logo";*/
            /*    font-family: SimSun;*/
            /*    font-size: 15px;*/
            /*    !*color:#000;*!*/
            /*};*/

            /*页脚*/
            @bottom-center{
                content: counter(page);
                font-family: SimSun;
                font-size: 15px;
                color:#000;
            };

            /*@bottom-right{*/
            /*    content: "第" counter(page) "页 共" counter(pages) "页";*/
            /*    font-family: SimSun;*/
            /*    font-size: 15px;*/
            /*    color:#000;*/
            /*};*/
        }
        * {
            page-break-inside: always;
        }
    </style>
</head>

<body style="font-family: SimSun; font-size: 14px">
<div style="">
    ${text!''}
</div>

</body>
</html>
simsun.ttc 为字体文件

1.HTML 生成 PDF

HttpHeaders 写法

@GetMapping("/pdf")
    public ResponseEntity<?> export(HttpServletResponse response) {
        try {
            HttpHeaders headers = new HttpHeaders();
            Map<String, Object> dataMap = new HashMap<>(16);
            dataMap.put("text", "张三");
            String htmlStr = FreeMarkUtils.freemarkerRender(dataMap, "templates.ftl");
            byte[] pdfBytes = FreeMarkUtils.createPDF(htmlStr, "simsun.ttc");
            if (pdfBytes != null && pdfBytes.length > 0) {
                String fileName = System.currentTimeMillis() + (int) (Math.random() * 90000 + 10000) + ".pdf";
                headers.setContentDispositionFormData("attachment", fileName);
                headers.setContentType(MediaType.APPLICATION_OCTET_STREAM);
                return new ResponseEntity<byte[]>(pdfBytes, headers, HttpStatus.OK);
            }
        } catch (Exception e) {
            e.printStackTrace();
        }
        HttpHeaders headers = new HttpHeaders();
        headers.setContentType(MediaType.APPLICATION_JSON_UTF8);
        return new ResponseEntity<>("{ \"code\" : \"404\", \"message\" : \"not found\" }", headers, HttpStatus.NOT_FOUND);
    }

 Response 写法

    @GetMapping("/pdf")
    public void export(HttpServletResponse response) {
        InputStream inputStream = null;
        try {
            // 设置参数
            Map<String, Object> dataMap = new HashMap<>(16);
            dataMap.put("text", "张三");
            String htmlStr = FreeMarkUtils.freemarkerRender(dataMap, "template.ftl");
            byte[] data = FreeMarkUtils.createHtml2Pdf(htmlStr, "simsun.ttc");
            // 设置文件名
            String fileName = System.currentTimeMillis() + (int) (Math.random() * 90000 + 10000) + ".pdf";
            // 浏览器直接下载文件
            generateFile(response, data, fileName);
            // 获取输入流
            inputStream = new ByteArrayInputStream(data);
            // 输入流保存阿里云
            final String url = OssBootUtil.upload(inputStream, "upload/agree/" + fileName);
            log.info(url);
        } catch (Exception e) {
            e.printStackTrace();
        } finally {
            try {
                if (inputStream != null) {
                    inputStream.close();
                }
            } catch (IOException ex) {
                ex.printStackTrace();
            }
        }
    }

    @GetMapping("/doc")
    public void doc(HttpServletResponse response) {
        Map<String, Object> dataMap = new HashMap<>();
        dataMap.put("name", "zhangsan");
        String fileName = System.currentTimeMillis() + (int) (Math.random() * 90000 + 10000) + ".doc";
        generateFile(response, FreeMarkUtils.freemarkerRender(dataMap, "test.ftl").getBytes(StandardCharsets.UTF_8), fileName);
    }

    @GetMapping("/doc2pdf")
    public void doc2pdf(HttpServletResponse response) {
        Map<String, Object> dataMap = new HashMap<>();
        dataMap.put("name", "张三");
        final byte[] data = FreeMarkUtils.createDocx2Pdf(dataMap, "document.ftl", "templates/test3.docx");
        String fileName = System.currentTimeMillis() + (int) (Math.random() * 90000 + 10000) + ".pdf";
        generateFile(response, data, fileName);
    }

     /**
     * 下载文件
     * @param response 相应
     * @param data 数据
     * @param fileName 文件名
     */
    private void generateFile(HttpServletResponse response, byte[] data, String fileName) {
        response.setHeader("content-Type", "application/octet-stream");
        response.setCharacterEncoding("utf-8");
        response.setHeader("Content-disposition", "attachment;filename=" + fileName);
        try {
            response.getOutputStream().write(data);
        } catch (IOException e) {
            e.printStackTrace();
        } finally {
            try {
                response.getOutputStream().close();
            } catch (IOException e) {
                e.printStackTrace();
            }
        }
    }

2. docx 文件生成PDF 

新建word 2007 test3.docx

复制文件,重命名为test3.zip,解压后

复制word/document.xml文件到项目中 document.ftl,更改文件内容变量 

导出PDF工具类

package com.dpxdata.backend.report;

import cn.hutool.core.io.file.FileMode;
import com.itextpdf.text.*;
import com.itextpdf.text.Image;
import com.itextpdf.text.Rectangle;
import com.itextpdf.text.pdf.*;
import freemarker.template.Configuration;
import freemarker.template.Template;
import freemarker.template.TemplateExceptionHandler;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.io.FileUtils;
import org.apache.commons.lang.WordUtils;
import org.docx4j.Docx4J;
import org.docx4j.convert.out.FOSettings;
import org.docx4j.fonts.IdentityPlusMapper;
import org.docx4j.fonts.Mapper;
import org.docx4j.fonts.PhysicalFonts;
import org.docx4j.openpackaging.packages.WordprocessingMLPackage;
import org.springframework.core.io.DefaultResourceLoader;
import org.springframework.stereotype.Component;
import org.springframework.ui.freemarker.FreeMarkerTemplateUtils;
import org.springframework.ui.freemarker.SpringTemplateLoader;
import org.xhtmlrenderer.pdf.ITextFontResolver;
import org.xhtmlrenderer.pdf.ITextRenderer;

import javax.swing.*;
import java.awt.*;
import java.io.*;
import java.nio.charset.StandardCharsets;
import java.nio.file.Path;
import java.util.Enumeration;
import java.util.Map;
import java.util.zip.ZipEntry;
import java.util.zip.ZipFile;
import java.util.zip.ZipOutputStream;

/**
 * @Description: pdf 导出工具类
 * @Author: LiSaiHang
 * @Date: 2022/1/7 11:30 上午
 */
@Component
@Slf4j
public class FreeMarkUtils {
    private FreeMarkUtils() {
    }

    private volatile static Configuration configuration;

    static {
        if (configuration == null) {
            synchronized (FreeMarkUtils.class) {
                if (configuration == null) {
                    configuration = new Configuration(Configuration.VERSION_2_3_28);
                }
            }
        }
    }

    /**
     * freemarker 引擎渲染 html
     *
     * @param dataMap  传入 html 模板的 Map 数据
     * @param fileName 模板文件名称
     *                 eg: "templates/template.ftl"
     * @return
     */
    public static String freemarkerRender(Map<String, Object> dataMap, String fileName) {
        configuration.setDefaultEncoding("UTF-8");
        configuration.setTemplateExceptionHandler(TemplateExceptionHandler.RETHROW_HANDLER);
        try {
            // TODO Jar包运行时,是无法读取classpath下资源文件,可以通过 SpringTemplateLoader 读取资源文件目录
            final SpringTemplateLoader templateLoader = new SpringTemplateLoader(new DefaultResourceLoader(), "classpath:templates");
            configuration.setTemplateLoader(templateLoader);
            configuration.setLogTemplateExceptions(false);
            configuration.setWrapUncheckedExceptions(true);
            Template template = configuration.getTemplate(fileName);
            return FreeMarkerTemplateUtils.processTemplateIntoString(template, dataMap);
        } catch (IOException e) {
            e.printStackTrace();
        } catch (Exception e) {
            e.printStackTrace();
        }
        return null;
    }

    /**
     * 使用 iText 生成 PDF 文档
     *
     * @param htmlTmpStr html 模板文件字符串
     * @param fontFile   所需字体文件(相对路径+文件名)
     */
    public static byte[] createHtml2Pdf(String htmlTmpStr, String fontFile) {
        ByteArrayOutputStream outputStream = null;
        byte[] result = null;
        // 生成 字体临时文件
        // TODO jar包运行后,是无法直接读取 classpath 下的资源文件,需要通过文件流读取,生成临时文件
        final File tempFile = getTempFile(fontFile);
        // TODO jar包运行后,是无法直接读取 classpath 下的资源文件,需要通过文件流读取,生成临时文件
        try {
            outputStream = new ByteArrayOutputStream();
            ITextRenderer renderer = new ITextRenderer();
            renderer.setDocumentFromString(htmlTmpStr);
            ITextFontResolver fontResolver = renderer.getFontResolver();
            // 解决中文支持问题,需要所需字体(ttc)文件
            fontResolver.addFont(tempFile.getAbsolutePath(), BaseFont.IDENTITY_H, BaseFont.NOT_EMBEDDED);
            renderer.layout();
            renderer.createPDF(outputStream);
            // 对Html2Pdf 的文件插入水印图片
            final String fileName = System.currentTimeMillis() + (int) (Math.random() * 90000 + 10000) + ".pdf";
            addWaterMark(outputStream.toByteArray(), fileName);
            final ByteArrayOutputStream fileOutputStream = getFileOutputStream(new File(fileName));
            result = fileOutputStream.toByteArray();
        } catch (FileNotFoundException e) {
            e.printStackTrace();
        } catch (IOException e) {
            e.printStackTrace();
        } catch (com.lowagie.text.DocumentException e) {
            e.printStackTrace();
        } finally {
            try {
                if (outputStream != null) {
                    outputStream.flush();
                    outputStream.close();
                }
                if (tempFile.exists()) {
                    tempFile.delete();
                }
            } catch (IOException e) {
                e.printStackTrace();
            }
        }
        return result;
    }

    /**
     * freemark生成word----docx格式
     *
     * @param dataMap      数据源
     * @param templateFile document.xml模板的文件名
     * @param docxFile     docx模板的文件名
     * @return 生成的文件路径
     */
    public static byte[] createDocx2Pdf(Map<String, Object> dataMap, String templateFile, String docxFile) {
        //word输出流
        ZipOutputStream zipout = null;
        //docx格式的word文件路径
        //输出word文件路径和名称
        String fileName = "applyWord_" + System.currentTimeMillis() + ".docx";
        // 生成docx临时文件
        final File tempPath = new File(fileName);
        // 获取docx模板
        final File docxTempFile = getTempFile(docxFile);
        try {
            //freeMark根据模板生成内容xml
            // 获取 document.ftl 输入流
            final String str = freemarkerRender(dataMap, templateFile);
            ByteArrayInputStream documentInput = new ByteArrayInputStream(str.getBytes(StandardCharsets.UTF_8));
            final ZipFile zipFile = new ZipFile(docxTempFile);
            System.out.println(zipFile.getName());
            Enumeration<? extends ZipEntry> zipEntrys = zipFile.entries();
            // docx文件输出流
            zipout = new ZipOutputStream(new FileOutputStream(tempPath));

            //循环遍历主模板docx文件,替换掉主内容区,也就是上面获取的document.xml的内容
            //------------------覆盖文档------------------
            int len = -1;
            byte[] buffer = new byte[1024];
            while (zipEntrys.hasMoreElements()) {
                ZipEntry next = zipEntrys.nextElement();
                final InputStream is = zipFile.getInputStream(next);
                if (next.toString().indexOf("media") < 0) {
                    zipout.putNextEntry(new ZipEntry(next.getName()));
                    if ("word/document.xml".equals(next.getName())) {
                        //写入填充数据后的主数据信息
                        if (documentInput != null) {
                            while ((len = documentInput.read(buffer)) != -1) {
                                zipout.write(buffer, 0, len);
                            }
                        }
                    } else {//不是主数据区的都用主模板的
                        while ((len = is.read(buffer)) != -1) {
                            zipout.write(buffer, 0, len);
                        }
                        is.close();
                    }
                }
            }
        } catch (Exception e) {
            e.printStackTrace();
        } finally {
            try {
                if (zipout != null) {
                    zipout.close();
                }
                if (docxTempFile.exists()) {
                    docxTempFile.delete();
                }
            } catch (Exception ex) {
                ex.printStackTrace();
            }
        }
        // word转pdf
        final String pdfFile = convertDocx2Pdf(fileName);
        return getFileOutputStream(new File(pdfFile)).toByteArray();
    }

    /**
     * word(docx)转pdf
     *
     * @param wordPath docx文件路径
     * @return 生成的带水印的pdf路径
     */
    public static String convertDocx2Pdf(String wordPath) {
        OutputStream os = null;
        InputStream is = null;
        //输出pdf文件路径和名称
        final String fileName = "pdfNoMark_" + System.currentTimeMillis() + ".pdf";
        try {
            is = new FileInputStream(wordPath);
            WordprocessingMLPackage mlPackage = WordprocessingMLPackage.load(is);
            Mapper fontMapper = new IdentityPlusMapper();
            fontMapper.put("隶书", PhysicalFonts.get("LiSu"));
            fontMapper.put("宋体", PhysicalFonts.get("SimSun"));
            fontMapper.put("微软雅黑", PhysicalFonts.get("Microsoft Yahei"));
            fontMapper.put("黑体", PhysicalFonts.get("SimHei"));
            fontMapper.put("楷体", PhysicalFonts.get("KaiTi"));
            fontMapper.put("新宋体", PhysicalFonts.get("NSimSun"));
            fontMapper.put("华文行楷", PhysicalFonts.get("STXingkai"));
            fontMapper.put("华文仿宋", PhysicalFonts.get("STFangsong"));
            fontMapper.put("宋体扩展", PhysicalFonts.get("simsun-extB"));
            fontMapper.put("仿宋", PhysicalFonts.get("FangSong"));
            fontMapper.put("仿宋_GB2312", PhysicalFonts.get("FangSong_GB2312"));
            fontMapper.put("幼圆", PhysicalFonts.get("YouYuan"));
            fontMapper.put("华文宋体", PhysicalFonts.get("STSong"));
            fontMapper.put("华文中宋", PhysicalFonts.get("STZhongsong"));
            //解决宋体(正文)和宋体(标题)的乱码问题
            PhysicalFonts.put("PMingLiU", PhysicalFonts.get("SimSun"));
            PhysicalFonts.put("新細明體", PhysicalFonts.get("SimSun"));
            PhysicalFonts.addPhysicalFonts("SimSun", WordUtils.class.getResource("/simsun1.ttc"));

            mlPackage.setFontMapper(fontMapper);
            os = new FileOutputStream(fileName);

            //docx4j  docx转pdf
            FOSettings foSettings = Docx4J.createFOSettings();
            foSettings.setWmlPackage(mlPackage);
            Docx4J.toFO(foSettings, os, Docx4J.FLAG_EXPORT_PREFER_XSL);
            is.close();//关闭输入流
            os.close();//关闭输出流
        } catch (Exception e) {
            e.printStackTrace();
        } finally {
            // 删除docx 临时文件
            File file = new File(wordPath);
            if (file != null && file.isFile() && file.exists()) {
                file.delete();
            }
            try {
                if (is != null) {
                    is.close();
                }
                if (os != null) {
                    os.close();
                }
            } catch (Exception ex) {
                ex.printStackTrace();
            }
        }
        return fileName;
    }

    /**
     * 文件转字节输出流
     *
     * @param outFile 文件
     * @return
     */
    public static ByteArrayOutputStream getFileOutputStream(File outFile) {
        // 获取生成临时文件的输出流
        InputStream input = null;
        ByteArrayOutputStream bytestream = null;
        try {
            input = new FileInputStream(outFile);
            bytestream = new ByteArrayOutputStream();
            int ch;
            while ((ch = input.read()) != -1) {
                bytestream.write(ch);
            }
        } catch (FileNotFoundException e) {
            e.printStackTrace();
        } catch (IOException e) {
            e.printStackTrace();
        } finally {
            try {
                bytestream.close();
                input.close();
                log.info("删除临时文件");
                if (outFile.exists()) {
                    outFile.delete();
                }
            } catch (IOException e) {
                e.printStackTrace();
            }
        }
        return bytestream;
    }

    /**
     * 获取资源文件的临时文件
     * 资源文件打jar包后,不能直接获取,需要通过流获取生成临时文件
     *
     * @param fileName 文件路径 temp/temp.ftl
     * @return
     */
    public static File getTempFile(String fileName) {
        final File tempFile = new File(fileName);
        InputStream fontTempStream = null;
        try {
            fontTempStream = FreeMarkUtils.class.getClassLoader().getResourceAsStream(fileName);
            FileUtils.copyInputStreamToFile(fontTempStream, tempFile);
        } catch (Exception e) {
            e.printStackTrace();
        } finally {
            try {
                if (fontTempStream != null) {
                    fontTempStream.close();
                }
            } catch (IOException e) {
                e.printStackTrace();
            }
        }
        return tempFile;
    }


    /**
     * 插入图片水印
     * @param srcByte 已生成PDF的字节数组(流转字节)
     * @param destFile 生成有水印的临时文件 temp.pdf
     * @return
     */
    public static FileOutputStream addWaterMark(byte[] srcByte, String destFile) {
        // 待加水印的文件
        PdfReader reader = null;
        // 加完水印的文件
        PdfStamper stamper = null;
        FileOutputStream fileOutputStream = null;
        try {
            reader = new PdfReader(srcByte);
            fileOutputStream = new FileOutputStream(destFile);
            stamper = new PdfStamper(reader, fileOutputStream);
            int total = reader.getNumberOfPages() + 1;
            PdfContentByte content;
            // 设置字体
            //BaseFont font = BaseFont.createFont();
            // 循环对每页插入水印
            for (int i = 1; i < total; i++) {
                final PdfGState gs = new PdfGState();
                // 水印的起始
                content = stamper.getUnderContent(i);
                // 开始
                content.beginText();
                // 设置颜色 默认为蓝色
                //content.setColorFill(BaseColor.BLUE);
                // content.setColorFill(Color.GRAY);
                // 设置字体及字号
                //content.setFontAndSize(font, 38);
                // 设置起始位置
                // content.setTextMatrix(400, 880);
                //content.setTextMatrix(textWidth, textHeight);
                // 开始写入水印
                //content.showTextAligned(Element.ALIGN_LEFT, text, textWidth, textHeight, 45);

                // 设置水印透明度
                // 设置笔触字体不透明度为0.4f
                gs.setStrokeOpacity(0f);
                Image image = null;
                image = Image.getInstance("url");
                // 设置坐标 绝对位置 X Y 这个位置大约在 A4纸 右上角展示LOGO
                image.setAbsolutePosition(472, 785);
                // 设置旋转弧度
                image.setRotation(0);// 旋转 弧度
                // 设置旋转角度
                image.setRotationDegrees(0);// 旋转 角度
                // 设置等比缩放 图片大小
                image.scalePercent(4);// 依照比例缩放
                // image.scaleAbsolute(200,100);//自定义大小
                // 设置透明度
                content.setGState(gs);
                // 添加水印图片
                content.addImage(image);
                // 设置透明度
                content.setGState(gs);
                //结束设置
                content.endText();
                content.stroke();
            }
        } catch (IOException e) {
            e.printStackTrace();
        } catch (DocumentException e) {
            e.printStackTrace();
        } finally {
            try {
                stamper.close();
                fileOutputStream.close();
                reader.close();
            } catch (DocumentException e) {
                e.printStackTrace();
            } catch (IOException e) {
                e.printStackTrace();
            }
        }
        return fileOutputStream;
    }
}

Logo

为开发者提供学习成长、分享交流、生态实践、资源工具等服务,帮助开发者快速成长。

更多推荐