java导出pdf(使用html)

发布于:2025-07-30 ⋅ 阅读:(20) ⋅ 点赞:(0)

引入maven

<dependencies>
    <!-- Thymeleaf -->
    <dependency>
        <groupId>org.thymeleaf</groupId>
        <artifactId>thymeleaf</artifactId>
        <version>3.1.1.RELEASE</version> <!-- 或与 Spring Boot 匹配的版本 -->
    </dependency>

    <!-- Flying Saucer PDF 渲染(使用 OpenPDFXHTMLRenderer-->
    <dependency>
        <groupId>org.xhtmlrenderer</groupId>
        <artifactId>flying-saucer-pdf-openpdf</artifactId>
        <version>9.1.22</version>
    </dependency>

    <!-- iText 2.x (lowagie) 用于合并 PDF 页等操作 -->
    <dependency>
        <groupId>com.lowagie</groupId>
        <artifactId>itext</artifactId>
        <version>2.1.7</version>
    </dependency>

    <!-- Servlet API(如果你在非 Web 项目中操作 HttpServletResponse-->
    <dependency>
        <groupId>javax.servlet</groupId>
        <artifactId>javax.servlet-api</artifactId>
        <version>4.0.1</version>
        <scope>provided</scope>
    </dependency>

    <!-- Lombok(用于注解 @Slf4j @SneakyThrows-->
    <dependency>
        <groupId>org.projectlombok</groupId>
        <artifactId>lombok</artifactId>
        <version>1.18.30</version>
        <scope>provided</scope>
    </dependency>
</dependencies>

工具类

package com.tlzf.util;

import com.lowagie.text.pdf.BaseFont;
import com.lowagie.text.pdf.PdfCopy;
import com.lowagie.text.pdf.PdfImportedPage;
import com.lowagie.text.pdf.PdfReader;
import lombok.SneakyThrows;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Component;
import org.springframework.util.CollectionUtils;
import org.thymeleaf.TemplateEngine;
import org.thymeleaf.context.Context;
import org.w3c.dom.Document;
import org.xhtmlrenderer.pdf.ITextFontResolver;
import org.xhtmlrenderer.pdf.ITextRenderer;

import javax.servlet.ServletOutputStream;
import javax.servlet.http.HttpServletResponse;
import javax.xml.parsers.DocumentBuilder;
import javax.xml.parsers.DocumentBuilderFactory;
import java.io.*;
import java.util.List;
import java.util.Map;

/**
 * pdf处理工具类
 *
 * @author gourd.hu
 * @version 1.0findInstallationElevatorBlockVOById
 */
@Slf4j
@Component
public class PdfUtil {

    private PdfUtil() {
    }


    /**
     * 按模板和参数生成html字符串,再转换为flying-saucer识别的Document
     *
     * @param templateName freemarker模板名称x`
     * @param variables    freemarker模板参数
     * @return Document
     */
    private static Document generateDoc(TemplateEngine templateEngine, String templateName, Map<String, Object> variables) {
        // 声明一个上下文对象,里面放入要存到模板里面的数据
        final Context context = new Context();
        context.setVariables(variables);
        StringWriter stringWriter = new StringWriter();
        try (BufferedWriter writer = new BufferedWriter(stringWriter)) {
            templateEngine.process(templateName, context, writer);
            writer.flush();
            DocumentBuilder builder = DocumentBuilderFactory.newInstance().newDocumentBuilder();
            return builder.parse(new ByteArrayInputStream(stringWriter.toString().getBytes("UTF-8")));
        } catch (Exception e) {
            log.error(e.getMessage(), e);
            return null;
        }
    }

    /**
     * 核心: 根据freemarker模板生成pdf文档
     *
     * @param templateEngine 配置
     * @param templateName   模板名称
     * @param out            输出流
     * @param listVars       模板参数
     * @throws Exception 模板无法找到、模板语法错误、IO异常
     */
    private static void generateAll(TemplateEngine templateEngine, String templateName, OutputStream out, List<Map<String, Object>> listVars) throws Exception {
        if (CollectionUtils.isEmpty(listVars)) {
            log.warn("警告:模板参数为空!");
            return;
        }

        ITextRenderer renderer = new ITextRenderer();
        //设置字符集(宋体),此处必须与模板中的<body style="font-family: SimSun">一致,区分大小写,不能写成汉字"宋体"
        ITextFontResolver fontResolver = renderer.getFontResolver();
        //避免中文为空设置系统字体
        fontResolver.addFont("static/fonts/simsun.ttf", BaseFont.IDENTITY_H, BaseFont.EMBEDDED);
        fontResolver.addFont("static/fonts/simsun.ttc", BaseFont.IDENTITY_H, BaseFont.EMBEDDED);
        //根据参数集个数循环调用模板,追加到同一个pdf文档中
        //(注意:此处从1开始,因为第0是创建pdf,从1往后则向pdf中追加内容)
        for (int i = 0; i < listVars.size(); i++) {
            Document docAppend = generateDoc(templateEngine, templateName, listVars.get(i));
            renderer.setDocument(docAppend, null);
            renderer.getSharedContext().setBaseURL(null);
            //展现和输出pdf
            renderer.layout();
            if (i == 0) {
                renderer.createPDF(out, false);
            } else {
                //写下一个pdf页面
                renderer.writeNextDocument();
            }
        }
        renderer.finishPDF(); //完成pdf写入
    }

    private static void generateAll(TemplateEngine templateEngine, String templateName, OutputStream out, List<Map<String, Object>> listVars, String path) throws Exception {
        if (CollectionUtils.isEmpty(listVars)) {
            log.warn("警告:模板参数为空!");
            return;
        }

        ITextRenderer renderer = new ITextRenderer();
        //设置字符集(宋体),此处必须与模板中的<body style="font-family: SimSun">一致,区分大小写,不能写成汉字"宋体"
        ITextFontResolver fontResolver = renderer.getFontResolver();
        //避免中文为空设置系统字体
        fontResolver.addFont("static/fonts/simsun.ttf", BaseFont.IDENTITY_H, BaseFont.NOT_EMBEDDED);
        fontResolver.addFont("static/fonts/simsun.ttc", BaseFont.IDENTITY_H, BaseFont.NOT_EMBEDDED);
        String url = "File:///" + path + "/";
        log.info("导出 文件地址  = {}", url);
        //根据参数集个数循环调用模板,追加到同一个pdf文档中
        //(注意:此处从1开始,因为第0是创建pdf,从1往后则向pdf中追加内容)
        for (int i = 0; i < listVars.size(); i++) {
            Document docAppend = generateDoc(templateEngine, templateName, listVars.get(i));
            renderer.setDocument(docAppend, null);
            renderer.getSharedContext().setBaseURL(url);
            //展现和输出pdf
            renderer.layout();
            if (i == 0) {
                renderer.createPDF(out, false);
            } else {
                //写下一个pdf页面
                renderer.writeNextDocument();
            }

        }
        renderer.finishPDF(); //完成pdf写入
    }

    /**
     * pdf下载
     *
     * @param templateEngine 配置
     * @param templateName   模板名称(带后缀.ftl)
     * @param listVars       模板参数集
     * @param response       HttpServletResponse
     * @param fileName       下载文件名称(带文件扩展名后缀)
     */
    public static void download(TemplateEngine templateEngine, String templateName, List<Map<String, Object>> listVars, HttpServletResponse response, String fileName) {
        // 设置编码、文件ContentType类型、文件头、下载文件名
        response.setCharacterEncoding("utf-8");
        response.setContentType("multipart/form-data");
        try {
            response.setHeader("Content-Disposition", "attachment;fileName=" +
                    new String(fileName.getBytes("gb2312"), "ISO8859-1"));
        } catch (UnsupportedEncodingException e) {
            log.error(e.getMessage(), e);
        }
        try (ServletOutputStream out = response.getOutputStream()) {
            generateAll(templateEngine, templateName, out, listVars);
            out.flush();
        } catch (Exception e) {
            log.error(e.getMessage(), e);
        }
    }

    public static void downloadWithImg(TemplateEngine templateEngine, String templateName, List<Map<String, Object>> listVars, HttpServletResponse response, String fileName, String path) {
        // 设置编码、文件ContentType类型、文件头、下载文件名
        response.setCharacterEncoding("utf-8");
        response.setContentType("multipart/form-data");
        try {
            response.setHeader("Content-Disposition", "attachment;fileName=" +
                    new String(fileName.getBytes("gb2312"), "ISO8859-1"));
        } catch (UnsupportedEncodingException e) {
            log.error(e.getMessage(), e);
        }
        try (ServletOutputStream out = response.getOutputStream()) {
            generateAll(templateEngine, templateName, out, listVars, path);
            out.flush();
        } catch (Exception e) {
            log.error(e.getMessage(), e);
        }
    }

    /**
     * pdf预览
     *
     * @param templateEngine 配置
     * @param templateName   模板名称(带后缀.ftl)
     * @param listVars       模板参数集
     * @param response       HttpServletResponse
     */
    public static void preview(TemplateEngine templateEngine, String templateName, List<Map<String, Object>> listVars, HttpServletResponse response) {
        try (ServletOutputStream out = response.getOutputStream()) {
            generateAll(templateEngine, templateName, out, listVars);
            out.flush();
        } catch (Exception e) {
            log.error(e.getMessage(), e);
        }
    }

    public static void previewWithImg(TemplateEngine templateEngine, String templateName, List<Map<String, Object>> listVars, HttpServletResponse response, String path) {
        try (ServletOutputStream out = response.getOutputStream()) {
            generateAll(templateEngine, templateName, out, listVars, path);
            out.flush();
        } catch (Exception e) {
            log.error(e.getMessage(), e);
        }
    }


    /**
     * 生成pdf 指定文件绝对路径
     *
     * @param templateEngine 配置
     * @param templateName   模板名称(带后缀.ftl)
     * @param listVars       模板参数集
     * @param filePath       文件路径
     */
    public static void creatPdfFile(TemplateEngine templateEngine, String templateName, List<Map<String, Object>> listVars, String filePath) {
        FileOutputStream out = null;
        try {
            //新建文件
            File file = new File(filePath);
            // 创建复制路径
            File parent = file.getParentFile();
            if (!parent.exists()) {
                parent.mkdirs();
            }
            // 复制文件
            if (file.exists()) {
                file.createNewFile();
            }
            out = new FileOutputStream(file);
            generateAll(templateEngine, templateName, out, listVars);
            out.flush();
        } catch (Exception e) {
            log.error(e.getMessage(), e);
        } finally {
            if (null != out) {
                try {
                    out.close();
                } catch (IOException e) {
                    e.printStackTrace();
                }
            }
        }
    }

    /**
     * 多个PDF合并功能 要捕获错误信息,所以不用try-catch
     *
     * @param files    多个PDF的路径
     * @param savePath 生成的新PDF绝对路径
     */
    @SneakyThrows
    public static void mergePdfFiles(String[] files, String savePath) {
        if (files.length > 0) {
//            try {
            com.lowagie.text.Document document = new com.lowagie.text.Document(new PdfReader(files[0]).getPageSize(1));
            PdfCopy copy = new PdfCopy(document, new FileOutputStream(savePath));
            document.open();
            for (String file : files) {
                PdfReader reader = new PdfReader(file);
                int n = reader.getNumberOfPages();
                for (int j = 1; j <= n; j++) {
                    document.newPage();
                    PdfImportedPage page = copy.getImportedPage(reader, j);
                    copy.addPage(page);
                }
            }
            document.close();
//            } catch (IOException | DocumentException e) {
//                e.printStackTrace();
//            }
        }
    }


}

pdf.html 模版

多页模版

<!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org" xmlns:layout="http://www.w3.org/1999/xhtml" layout:decorator="layout">
<head lang="en">
    <title></title>
    <style>
        @media print {
            div.footer-content {
                display: block;
                font-family: SimSun;
                font-size: 16px;
                color: #000;
                text-align: left;
                margin-left: -490px;
                position: running(footer-content);
            }
        }

        @page {
            size: A4; /*设置纸张大小:A4(210mm 297mm)、A3(297mm 420mm) 横向则反过来*/
            margin: 1in;
            padding: 1em;
        }

        .page {
            page-break-after: always;
            font-size: 16px;
            font-family: SimSun;
        }

        /* 防止最后一页多空页 */
        .page:last-child {
            page-break-after: auto;
        }

        body {
            font-family: 'SimSun'
        }

        h1 {
            text-align: center;
            line-height: 60px;
        }


        table, th, td {
            border: 1px solid black;
            border-collapse: collapse;
            padding: 3px;
            font-size: 16px;
            height: 10px;
        }

        .half-cell {
            position: relative;
        }

        .footer {
            position: fixed;
            bottom: 10px;
            left: 0;
            width: 100%;
            font-size: 12px;
            margin-left: 10px;
            margin-right: 10px; /* 右边也留 */
        }

        .footer .right {
            text-align: right;
            margin-top: 30px;
        }

    </style>
</head>
<!--这样配置不中文不会显示-->
<!--<body style="font-family: 宋体">-->
<body style="font-family: 'SimSun'">
<!--第一页-->
<div class="page">
    <div style="width: 100%; overflow: hidden;">
        <!---->
        <div style="float: left; width: 40%;">
            <table style="width: 100%; border-collapse: collapse;text-align: center;">
                <colgroup>
                    <col style="width: 50%;" />
                    <col style="width: 50%;" />
                </colgroup>
                <tr class="expertTitle">
                    <td>**</td>
                    <td th:text="${bjbh}"></td>
                </tr>
                <tr class="expertTitle">
                    <td>**</td>
                    <td th:text="${bdh}"></td>
                </tr>
                <tr>
                    <td>**</td>
                    <td th:text="${fbType}"></td>
                </tr>
            </table>
        </div>

        <!---->
        <div style="float: right; width: 30%; text-align: right;">
            <img th:src="${photo}" alt="二维码" style="width: 100px; height: 100px;"/>

        </div>
    </div>


    <h2 style="text-align: center;" data-pdf-bookmark-name="***">***</h2>
    <div style="line-height: 20px;">
        <!-- 继续下面内容 -->
        <div style="margin-top: 20px; font-size: 16px; margin-left: 10px; line-height: 1.8;">
            <span style="border-bottom: 1px solid #000; padding: 0 0px" th:text="${name}"></span> <span></span>

            <p style="text-indent: 2em;">
                    ***
            </p>
        </div>

        <div>
            <table style="text-align: center; width: 100%; border-collapse: collapse;" border="1">
                <colgroup>
                <!-- 设置表格宽度 -->
                    <col style="width: 16.66%;" />
                    <col style="width: 16.66%;" />
                    <col style="width: 16.66%;" />
                    <col style="width: 16.66%;" />
                    <col style="width: 16.66%;" />
                    <col style="width: 16.66%;" />
                </colgroup>
                <tr>
                    <td>**</td>
                      <!-- 设置表格合并 -->
                    <td colspan="5" th:text="${jsdd}"></td>
                </tr>
                <tr style="height: 150px;">
                <td colspan="1">**</td>
                <!-- 设置表格自动换行 -->
                <td colspan="3" style="text-align: left; vertical-align: top; padding: 5px;">
                    <span th:text="${zbdljg}"></span>
                </td>
            </tr>

            </table>

        </div>

        
    </div>

    <div class="footer" >
        <div>
            附注:
        </div>
        <div>
            &#160;1. ****</div>

        <div class="right">
            <div>******&#160;&#160;</div>
            <div>2020&#160;&#160;</div>
        </div>
    </div>

</div>

<div class="page">
    <div style="width: 100%; overflow: hidden;">
        <!---->
        <div style="float: left; width: 40%;text-align: center;">
            <table style="width: 100%; border-collapse: collapse;text-align: center;">
                <colgroup>
                    <col style="width: 50%;" />
                    <col style="width: 50%;" />
                </colgroup>
                <tr class="expertTitle">
                    <td>**</td>
                    <td th:text="${bjbh}"></td>
                </tr>
                <tr class="expertTitle">
                    <td>**</td>
                    <td th:text="${bdh}"></td>
                </tr>
                <tr>
                    <td>**</td>
                    <td th:text="${fbType}"></td>
                </tr>
            </table>
        </div>

        <!---->
        <div style="float: right; width: 30%; text-align: right;">
            <img th:src="${photo}" alt="二维码" style="width: 100px; height: 100px;"/>
        </div>
    </div>
    <h2 style="text-align: center;" data-pdf-bookmark-name="专业工程暂估价明细表">专业工程暂估价明细表</h2>

    <table style="width: 100%; border-collapse: collapse; font-weight: normal;text-align: center;" >
        <colgroup>
            <col style="width: 20%;" />
            <col style="width: 20%;" />
            <col style="width: 20%;" />
            <col style="width: 20%;" />
            <col style="width: 20%;" />
        </colgroup>
        <tr>
            <td>序号</td>
            <td>专业工程名称</td>
            <td>专业类别</td>
            <td>招标主体</td>
            <td>金额(万元)</td>
        </tr>


        <!-- 循环 -->
        <tr th:each="item, iterStat : ${list}" >
            <td th:text="${iterStat.count}"></td>
            <td th:text="${item.projectName}"></td>
            <td th:text="${item.projectType}"></td>
            <td th:text="${item.bidEntity}"></td>
            <td th:text="${item.amount}"></td>
        </tr>

    </table>


    <div class="footer">
        <div>
            附注:
        </div>
        <div>
            1. 本中标可通过二维码在上海市建筑业官方微信验证。
        </div>

        <div class="right" style=" margin-right: 30px;">
            <div> ***&#160;&#160;</div>
            <div>2020&#160;&#160;</div>
        </div>
    </div>


</div>

</body>
</html>


网站公告

今日签到

点亮在社区的每一天
去签到