markdown转为pdf导出

将markdown 导出为pdf

markdown 导出为pdf,得先将markdown->html->pdf

1.maven依赖

bash 复制代码
  <!-- Markdown转pdf相关 -->
        <dependency>
            <groupId>com.vladsch.flexmark</groupId>
            <artifactId>flexmark-all</artifactId>
            <version>0.64.0</version>
        </dependency>

        <dependency>
            <groupId>com.atlassian.commonmark</groupId>
            <artifactId>commonmark</artifactId>
            <version>0.15.2</version>
        </dependency>


        <dependency>
            <groupId>org.xhtmlrenderer</groupId>
            <artifactId>flying-saucer-pdf</artifactId>
            <version>9.5.1</version>
        </dependency>

        <dependency>
            <groupId>com.openhtmltopdf</groupId>
            <artifactId>openhtmltopdf-core</artifactId>
            <version>1.0.10</version>
        </dependency>

        <dependency>
            <groupId>com.openhtmltopdf</groupId>
            <artifactId>openhtmltopdf-pdfbox</artifactId>
            <version>1.0.10</version>
        </dependency>

        <dependency>
            <groupId>org.jsoup</groupId>
            <artifactId>jsoup</artifactId>
            <version>1.15.3</version>
        </dependency>

        <!-- 如果需要其他字体支持 -->
        <dependency>
            <groupId>com.openhtmltopdf</groupId>
            <artifactId>openhtmltopdf-rtl-support</artifactId>
            <version>1.0.10</version>
        </dependency>

2.工具类

bash 复制代码
import com.gcbd.framework.common.exception.ServiceException;
import com.openhtmltopdf.pdfboxout.PdfRendererBuilder;
import com.vladsch.flexmark.html.HtmlRenderer;
import com.vladsch.flexmark.parser.Parser;
import com.vladsch.flexmark.util.ast.Node;
import lombok.extern.slf4j.Slf4j;

import java.io.ByteArrayOutputStream;
import java.io.File;
import java.io.FileInputStream;
import java.io.InputStream;
import java.util.ArrayList;
import java.util.Base64;
import java.util.List;

/**
 * @ClassName MarkdownToPdfExporterUtil
 * @Description MarkdownToPdfExporterUtil
 * @Author zxr
 * @Date 2025/10/29
 */
@Slf4j
public class MarkdownToPdfExporterUtil {

    private static final Parser parser = Parser.builder().build();
    private static final HtmlRenderer renderer = HtmlRenderer.builder().build();

    public static String convertMarkdownToHtml(String markdownTitle, String markdownContent) {
        try {

            // 预处理:将 Markdown 表格转换为 HTML 表格
            String processedContent = preprocessTables(markdownContent);

            Node document = parser.parse(processedContent);
            String htmlContent = renderer.render(document);

            // 包装成完整的 HTML 文档
            return """
                    <!DOCTYPE html>
                    <html lang="zh-CN">
                    <head>
                        <meta charset="UTF-8" />
                        <meta name="viewport" content="width=device-width, initial-scale=1.0" />
                        <style>
                            %s
                        </style>
                    </head>
                    <body>
                        <div class="container">
                            
                            <div class="main-content">
                                %s
                            </div>
                            
                        </div>
                    </body>
                    </html>
                    """.formatted(
                    getDocumentStyles(),
                    htmlContent
            );

        } catch (Exception e) {
            throw new RuntimeException("HTML 转换失败", e);
        }
    }


    /**
     * 预处理:将 Markdown 表格语法转换为 HTML 表格
     */
    private static String preprocessTables(String markdownContent) {
        if (markdownContent == null || markdownContent.trim().isEmpty()) {
            return markdownContent;
        }

        String[] lines = markdownContent.split("\n");
        StringBuilder result = new StringBuilder();
        List<String> tableLines = new ArrayList<>();
        boolean inTable = false;

        for (int i = 0; i < lines.length; i++) {
            String line = lines[i];

            // 检测表格开始(包含 | 的行,且不是代码块内的)
            if (isTableLine(line) && !isInCodeBlock(result.toString())) {
                if (!inTable) {
                    // 开始表格
                    inTable = true;
                    tableLines.clear();
                }
                tableLines.add(line);
            } else {
                if (inTable && tableLines.size() >= 2) {
                    // 结束表格,转换并添加到结果
                    result.append(convertTableToHtml(tableLines)).append("\n");
                    inTable = false;
                    tableLines.clear();
                }
                result.append(line).append("\n");
            }
        }

        // 处理文件末尾的表格
        if (inTable && tableLines.size() >= 2) {
            result.append(convertTableToHtml(tableLines)).append("\n");
        }

        return result.toString();
    }


    /**
     * 判断是否为表格行
     */
    private static boolean isTableLine(String line) {
        if (line == null || line.trim().isEmpty()) {
            return false;
        }
        // 简单的表格检测:包含 | 字符且不是标题分隔线
        return line.contains("|") &&
                !line.trim().matches("^#+.*") && // 不是标题
                !line.matches("^=+$|^-+$"); // 不是标题下划线
    }

    /**
     * 判断是否在代码块内
     */
    private static boolean isInCodeBlock(String text) {
        // 简单的代码块检测:统计 ```的数量
        int backtickCount = 0;
        for (char c : text.toCharArray()) {
            if (c == '`') {
                backtickCount++;
            }
        }
        return backtickCount % 2 != 0;
    }

    /**
     * 将 Markdown 表格转换为 HTML 表格
     */
    private static String convertTableToHtml(List<String> tableLines) {
        if (tableLines.size() < 2) {
            return String.join("\n", tableLines);
        }

        StringBuilder htmlTable = new StringBuilder();
        htmlTable.append("<div class=\"table-container\">\n");
        htmlTable.append("<table>\n");

        // 处理表头
        String headerLine = tableLines.get(0);
        htmlTable.append("<thead>\n<tr>\n");
        String[] headers = parseTableRow(headerLine);
        for (String header : headers) {
            htmlTable.append("<th>").append(escapeHtml(header.trim())).append("</th>\n");
        }
        htmlTable.append("</tr>\n</thead>\n");

        // 处理分隔线(第二行)
        String separatorLine = tableLines.get(1);
        int[] alignments = parseAlignment(separatorLine);

        // 处理数据行
        htmlTable.append("<tbody>\n");
        for (int i = 2; i < tableLines.size(); i++) {
            String[] cells = parseTableRow(tableLines.get(i));
            htmlTable.append("<tr>\n");
            for (int j = 0; j < cells.length; j++) {
                String alignment = getAlignmentClass(alignments, j);
                htmlTable.append("<td").append(alignment).append(">")
                        .append(escapeHtml(cells[j].trim()))
                        .append("</td>\n");
            }
            htmlTable.append("</tr>\n");
        }
        htmlTable.append("</tbody>\n");

        htmlTable.append("</table>\n");
        htmlTable.append("</div>");

        return htmlTable.toString();
    }
    /**
     * 解析表格行
     */
    private static String[] parseTableRow(String line) {
        // 移除行首尾的 |,然后按 | 分割
        String cleaned = line.trim();
        if (cleaned.startsWith("|")) {
            cleaned = cleaned.substring(1);
        }
        if (cleaned.endsWith("|")) {
            cleaned = cleaned.substring(0, cleaned.length() - 1);
        }
        return cleaned.split("\\|", -1); // -1 保留空字符串
    }

    /**
     * 解析对齐方式
     */
    private static int[] parseAlignment(String separatorLine) {
        String[] cells = parseTableRow(separatorLine);
        int[] alignments = new int[cells.length];

        for (int i = 0; i < cells.length; i++) {
            String cell = cells[i].trim();
            if (cell.startsWith(":") && cell.endsWith(":")) {
                alignments[i] = 1; // 居中对齐
            } else if (cell.endsWith(":")) {
                alignments[i] = 2; // 右对齐
            } else if (cell.startsWith(":")) {
                alignments[i] = 3; // 左对齐(默认)
            } else {
                alignments[i] = 0; // 默认左对齐
            }
        }
        return alignments;
    }


    /**
     * 获取对齐方式的 CSS 类
     */
    private static String getAlignmentClass(int[] alignments, int index) {
        if (index >= alignments.length) {
            return "";
        }

        switch (alignments[index]) {
            case 1: return " align=\"center\"";
            case 2: return " align=\"right\"";
            case 3: return " align=\"left\"";
            default: return "";
        }
    }

    /**
     * HTML 转义
     */
    private static String escapeHtml(String text) {
        return text.replace("&", "&amp;")
                .replace("<", "&lt;")
                .replace(">", "&gt;")
                .replace("\"", "&quot;")
                .replace("'", "&#39;");
    }

    /**
     * 获取文档样式 - 优化中文字体支持
     */
    private static String getDocumentStyles() {
        String fontFace = loadFontFace();

        return fontFace + """
                * {
                    margin: 0;
                    padding: 0;
                    box-sizing: border-box;
                }
                        
                body {
                    /* 强制使用 SimHei 字体,确保 PDF 渲染一致性 */
                    font-family: 'SimHei', 'Microsoft YaHei', 'PingFang SC', 
                               -apple-system, BlinkMacSystemFont, 
                               'Segoe UI', 'Hiragino Sans GB',
                               'WenQuanYi Micro Hei', 
                               'Source Han Sans CN', 'Noto Sans CJK SC',
                               sans-serif !important;
                    line-height: 1.8;
                    color: #2c3e50;
                    background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
                    min-height: 100vh;
                    padding: 20px;
                    -webkit-font-smoothing: antialiased;
                    -moz-osx-font-smoothing: grayscale;
                    text-rendering: optimizeLegibility;
                }

                /* 特别为代码块和预格式文本设置中文字体 */
                pre, code {
                    font-family: 'SimHei', 'SFMono-Regular', 'Consolas', 
                                'Liberation Mono', 'Menlo', 'Monaco', 
                                'Courier New', monospace !important;
                }

                /* JSON 内容通常出现在 pre 或 code 标签中 */
                pre {
                    background: #1a1a1a;
                    color: #f8f8f2;
                    padding: 1.5rem;
                    border-radius: 8px;
                    overflow-x: auto;
                    margin: 1.8rem 0;
                    border-left: 4px solid #007bff;
                    font-size: 0.95rem;
                    line-height: 1.5;
                    tab-size: 4;
                    white-space: pre-wrap; /* 确保长文本换行 */
                    word-wrap: break-word;
                }

                code {
                    background: #f1f3f4;
                    padding: 0.2rem 0.4rem;
                    border-radius: 4px;
                    font-size: 0.9em;
                    color: #e74c3c;
                }

                pre code {
                    background: none;
                    padding: 0;
                    color: inherit;
                    font-size: inherit;
                }

                /* 确保所有文本元素都使用中文字体 */
                .container, .main-content, .document-header, 
                .document-title, .document-meta,
                p, h1, h2, h3, h4, h5, h6,
                li, td, th, span, div {
                    font-family: 'SimHei', 'Microsoft YaHei', 'PingFang SC', 
                               sans-serif !important;
                }

                /* 其余样式保持不变... */
                .container {
                    max-width: 1200px;
                    margin: 0 auto;
                    background: white;
                    border-radius: 15px;
                    box-shadow: 0 20px 40px rgba(0,0,0,0.1);
                    overflow: hidden;
                }

                .document-header {
                    background: linear-gradient(135deg, #007bff, #0056b3);
                    color: white;
                    padding: 40px;
                    text-align: center;
                }

                .document-title {
                    font-size: 2.8rem;
                    font-weight: 700;
                    margin-bottom: 1rem;
                    text-shadow: 0 2px 4px rgba(0,0,0,0.3);
                    line-height: 1.3;
                    word-wrap: break-word;
                    word-break: break-word;
                }

                .document-meta {
                    display: flex;
                    justify-content: center;
                    gap: 2rem;
                    font-size: 1rem;
                    opacity: 0.9;
                    font-family: inherit;
                }

                .main-content {
                    padding: 40px;
                }

                /* 标题样式 - 优化中文排版 */
                h1, h2, h3, h4, h5, h6 {
                    margin: 2.5rem 0 1.5rem;
                    font-weight: 600;
                    line-height: 1.4;
                    color: #2c3e50;
                    font-family: inherit;
                    text-align: left;
                }

                h1 {
                    font-size: 2.2rem;
                    border-bottom: 3px solid #007bff;
                    padding-bottom: 0.8rem;
                    margin-top: 3rem;
                    letter-spacing: -0.5px;
                }

                h2 {
                    font-size: 1.8rem;
                    border-left: 5px solid #007bff;
                    padding-left: 1.2rem;
                    background: #f8f9fa;
                    padding: 1.2rem;
                    border-radius: 0 8px 8px 0;
                    margin-left: -1.2rem;
                }

                h3 {
                    font-size: 1.5rem;
                    color: #495057;
                    padding-bottom: 0.3rem;
                    border-bottom: 1px solid #e9ecef;
                }

                h4 { font-size: 1.3rem; }
                h5 { font-size: 1.1rem; }
                h6 { font-size: 1rem; color: #6c757d; }

                /* 表格样式 */
                table {
                    width: 100%;
                    border-collapse: collapse;
                    margin: 2.5rem 0;
                    box-shadow: 0 1px 3px rgba(0,0,0,0.1);
                    border-radius: 8px;
                    overflow: hidden;
                    font-family: inherit;
                }

                th, td {
                    padding: 1.2rem 1rem;
                    text-align: left;
                    border: 1px solid #dee2e6;
                    line-height: 1.6;
                    font-size: 1rem;
                }

                th {
                    background: #007bff;
                    color: white;
                    font-weight: 600;
                    font-size: 1rem;
                    font-family: inherit;
                }

                td {
                    background: white;
                    vertical-align: top;
                }

                tr:nth-child(even) td {
                    background: #f8f9fa;
                }

                tr:hover td {
                    background: #e3f2fd;
                }

                /* 代码块样式 */
                pre {
                    background: #1a1a1a;
                    color: #f8f8f2;
                    padding: 1.5rem;
                    border-radius: 8px;
                    overflow-x: auto;
                    margin: 1.8rem 0;
                    border-left: 4px solid #007bff;
                    font-family: 'SFMono-Regular', 'Consolas', 'Liberation Mono',
                                'Menlo', 'Monaco', 'Courier New', 'monospace';
                    font-size: 0.95rem;
                    line-height: 1.5;
                    tab-size: 4;
                }

                code {
                    background: #f1f3f4;
                    padding: 0.2rem 0.4rem;
                    border-radius: 4px;
                    font-family: 'SFMono-Regular', 'Consolas', 'Liberation Mono',
                                'Menlo', 'Monaco', 'Courier New', 'monospace';
                    font-size: 0.9em;
                    color: #e74c3c;
                }

                pre code {
                    background: none;
                    padding: 0;
                    color: inherit;
                    font-size: inherit;
                }

                /* 图片样式 */
                img {
                    max-width: 100%;
                    height: auto;
                    border: 1px solid #dee2e6;
                    border-radius: 8px;
                    box-shadow: 0 4px 6px rgba(0,0,0,0.1);
                    margin: 1.5rem 0;
                    display: block;
                }

                /* 段落和文本 - 优化中文阅读体验 */
                p {
                    margin-bottom: 1.5rem;
                    text-align: justify;
                    font-size: 1.1rem;
                    line-height: 1.8;
                    word-spacing: 0.05em;
                    font-family: inherit;
                }

                strong {
                    font-weight: 650;
                    color: #e74c3c;
                }

                em {
                    font-style: italic;
                    color: #6c757d;
                }

                /* 链接样式 */
                a {
                    color: #007bff;
                    text-decoration: none;
                    transition: color 0.2s ease;
                }

                a:hover {
                    color: #0056b3;
                    text-decoration: underline;
                }

                /* 列表样式 - 优化中文列表 */
                ul, ol {
                    margin: 1.8rem 0;
                    padding-left: 2.5rem;
                    font-size: 1.1rem;
                }

                li {
                    margin-bottom: 0.8rem;
                    line-height: 1.8;
                    text-align: left;
                }

                ul li {
                    list-style: none;
                    position: relative;
                }

                ul li::before {
                    content: '•';
                    color: #007bff;
                    font-weight: bold;
                    position: absolute;
                    left: -1.5rem;
                    font-size: 1.2rem;
                }

                ol {
                    counter-reset: list-counter;
                }

                ol li {
                    list-style: none;
                    position: relative;
                    counter-increment: list-counter;
                }

                ol li::before {
                    content: counter(list-counter) '.';
                    color: #007bff;
                    font-weight: 600;
                    position: absolute;
                    left: -2rem;
                    min-width: 1.5rem;
                }

                /* 引用块样式 */
                blockquote {
                    background: #f8f9fa;
                    border-left: 4px solid #007bff;
                    margin: 2rem 0;
                    padding: 1.5rem;
                    border-radius: 0 8px 8px 0;
                    font-style: italic;
                    color: #495057;
                }

                blockquote p {
                    margin-bottom: 0.5rem;
                    font-size: 1.05rem;
                }

                /* 水平线 */
                hr {
                    border: none;
                    height: 2px;
                    background: linear-gradient(90deg, transparent, #007bff, transparent);
                    margin: 3rem 0;
                }

                /* 页脚 */
                .document-footer {
                    background: #343a40;
                    color: white;
                    text-align: center;
                    padding: 2rem;
                    margin-top: 3rem;
                    font-family: inherit;
                }

                .document-footer p {
                    margin: 0;
                    font-size: 0.9rem;
                    opacity: 0.8;
                    text-align: center;
                }

                /* 特殊标记 */
                .api-section {
                    background: #e8f5e8;
                    border: 1px solid #28a745;
                    border-radius: 8px;
                    padding: 1.8rem;
                    margin: 2.5rem 0;
                }

                .parameter-table {
                    font-size: 0.95rem;
                }

                .parameter-table th {
                    background: #495057;
                    white-space: nowrap;
                }

                .example-section {
                    background: #fff3cd;
                    border: 1px solid #ffc107;
                    border-radius: 8px;
                    padding: 1.8rem;
                    margin: 2.5rem 0;
                }

                .test-case {
                    background: #d1ecf1;
                    border: 1px solid #17a2b8;
                    border-radius: 8px;
                    padding: 1.8rem;
                    margin: 2.5rem 0;
                }

                /* 响应式设计 */
                @media (max-width: 768px) {
                    .container {
                        margin: 10px;
                        border-radius: 10px;
                    }

                    .document-header {
                        padding: 2rem 1rem;
                    }

                    .document-title {
                        font-size: 2rem;
                        line-height: 1.2;
                    }

                    .main-content {
                        padding: 1.5rem;
                    }

                    table {
                        display: block;
                        overflow-x: auto;
                        font-size: 0.9rem;
                    }

                    .document-meta {
                        flex-direction: column;
                        gap: 0.5rem;
                        font-size: 0.9rem;
                    }

                    h1 { font-size: 1.8rem; }
                    h2 { font-size: 1.5rem; margin-left: 0; padding-left: 1rem; }
                    h3 { font-size: 1.3rem; }

                    p, li {
                        font-size: 1rem;
                        text-align: left;
                    }

                    pre {
                        padding: 1rem;
                        font-size: 0.85rem;
                    }
                }

                @media (max-width: 480px) {
                    body {
                        padding: 10px;
                    }

                    .document-title {
                        font-size: 1.6rem;
                    }

                    .main-content {
                        padding: 1rem;
                    }

                    th, td {
                        padding: 0.8rem 0.5rem;
                        font-size: 0.85rem;
                    }
                }

                /* 打印样式 */
                @media print {
                    body {
                        background: white !important;
                        padding: 0;
                    }

                    .container {
                        box-shadow: none;
                        border-radius: 0;
                    }

                    .document-header {
                        background: #007bff !important;
                        -webkit-print-color-adjust: exact;
                        print-color-adjust: exact;
                    }

                    a {
                        color: #0056b3;
                        text-decoration: underline;
                    }

                    .document-meta {
                        color: white !important;
                    }
                }
                """;
    }


    /**
     * 将 HTML 转换为 PDF 字节数组 - 确保中文字体支持
     */
    public static byte[] convertHtmlToPdf(String htmlContent) {
        try (ByteArrayOutputStream baos = new ByteArrayOutputStream()) {
            PdfRendererBuilder builder = new PdfRendererBuilder();

            // 强制注册中文字体
            registerChineseFonts(builder);

            builder.withHtmlContent(htmlContent, null);
            builder.toStream(baos);
            builder.run();
            return baos.toByteArray();
        } catch (Exception e) {
            throw new RuntimeException("PDF 生成失败: " + e.getMessage(), e);
        }
    }

    /**
     * 强制注册中文字体
     */
    private static void registerChineseFonts(PdfRendererBuilder builder) {
        // 定义可能的字体路径
        String[] fontPaths = {
                "font/SimHei.ttf",           // 项目根目录
                "src/main/resources/font/SimHei.ttf", // Maven 资源目录
                "resources/font/SimHei.ttf", // 资源目录
                "./font/SimHei.ttf",         // 当前目录
                "SimHei.ttf"                 // 直接文件名
        };

        boolean fontRegistered = false;

        for (String fontPath : fontPaths) {
            try {
                // 先尝试类路径
                InputStream fontStream = MarkdownToPdfExporterUtil.class.getClassLoader().getResourceAsStream(fontPath);
                if (fontStream != null) {
                    // 重要:使用 Supplier 确保每次都能获得新的流
                    builder.useFont(() -> {
                        InputStream stream = MarkdownToPdfExporterUtil.class.getClassLoader().getResourceAsStream(fontPath);
                        if (stream == null) {
                            throw new RuntimeException("无法加载字体: " + fontPath);
                        }
                        return stream;
                    }, "SimHei");

                    log.info("成功注册字体: {} (类路径)", fontPath);
                    fontRegistered = true;
                    break;
                }

                // 再尝试文件系统
                File fontFile = new File(fontPath);
                if (fontFile.exists()) {
                    builder.useFont(fontFile, "SimHei");
                    log.info("成功注册字体: {} (文件系统)", fontPath);
                    fontRegistered = true;
                    break;
                }
            } catch (Exception e) {
                throw new ServiceException(500, "字体注册失败 " + fontPath + ": " + e.getMessage());
            }
        }

        if (!fontRegistered) {
            log.error("警告: 未找到任何中文字体文件,中文将显示为 ###,请将 SimHei.ttf 字体文件放置在以下位置之一:classpath:font/SimHei.ttf,\n" +
                    "            // 项目根目录:font/SimHei.ttf,src/main/resources/font/SimHei.ttf:");
        }
    }


    /**
     * 加载字体定义 - 增强版本
     */
    private static String loadFontFace() {
        String simHeiBase64 = loadFontAsBase64("font/SimHei.ttf");

        if (!simHeiBase64.isEmpty()) {
            return """
                    @font-face {
                        font-family: 'SimHei';
                        src: url('data:font/truetype;charset=utf-8;base64,%s') format('truetype');
                        font-weight: normal;
                        font-style: normal;
                        font-display: swap;
                    }
                                    
                    @font-face {
                        font-family: 'SimHei';
                        src: url('data:font/truetype;charset=utf-8;base64,%s') format('truetype');
                        font-weight: bold;
                        font-style: normal;
                        font-display: swap;
                    }
                                    
                    @font-face {
                        font-family: 'SimHei';
                        src: url('data:font/truetype;charset=utf-8;base64,%s') format('truetype');
                        font-weight: normal;
                        font-style: italic;
                        font-display: swap;
                    }
                    """.formatted(simHeiBase64, simHeiBase64, simHeiBase64);
        }

        // 如果字体加载失败,提供回退方案
        return """
                /* 字体加载失败,使用系统字体回退 */
                """;
    }


    private static String loadFontAsBase64(String fontPath) {
        InputStream fontStream = null;
        try {
            // 1. 尝试从类路径加载
            fontStream = MarkdownToPdfExporterUtil.class.getClassLoader().getResourceAsStream(fontPath);

            // 2. 尝试绝对路径
            if (fontStream == null) {
                File fontFile = new File(fontPath);
                if (fontFile.exists()) {
                    fontStream = new FileInputStream(fontFile);
                }
            }

            // 3. 尝试常见位置
            if (fontStream == null) {
                String[] possiblePaths = {
                        "src/main/resources/" + fontPath,
                        "resources/" + fontPath,
                        "font/" + fontPath,
                        "../" + fontPath
                };

                for (String path : possiblePaths) {
                    File fontFile = new File(path);
                    if (fontFile.exists()) {
                        fontStream = new FileInputStream(fontFile);
                        break;
                    }
                }
            }

            //字体文件未找到,请将 SimHei.ttf 放置在以下位置之一:classpath:font/SimHei.ttf,
            // 项目根目录:font/SimHei.ttf,src/main/resources/font/SimHei.ttf:
            if (fontStream == null) return "";


            byte[] fontBytes = fontStream.readAllBytes();
            String base64 = Base64.getEncoder().encodeToString(fontBytes);
            log.info("字体加载成功,Base64 长度: {}", base64.length());
            return base64;

        } catch (Exception e) {
            log.error("加载字体失败:{}", fontPath);
            log.error("错误信息:{}", e.getMessage());
            return "";
        } finally {
            if (fontStream != null) {
                try {
                    fontStream.close();
                } catch (Exception e) {
                }
            }
        }
    }

}

3.controller

bash 复制代码
 	@GetMapping("/downMarkdownConvertPdf")
    @Operation(summary = "markdown转pdf下载(别带自闭和标签 比如:<br>)")
    @Parameter(name = "docId", description = "编号", required = true, example = "1024")
    @PermitAll
    @TenantIgnore
    public void exportMarkdownToPdf(@RequestParam("docId") String docId, HttpServletResponse response) throws IOException {
        try {
            CatalogueDoc catalogueDoc = catalogueDocService.getById(docId);
            if (catalogueDoc == null || StringUtils.isBlank(catalogueDoc.getContent()))
                return;

            String markdownContent = catalogueDoc.getContent();
            String filename = catalogueDoc.getTitle();

            // 生成 HTML和PDF
            String html = MarkdownToPdfExporterUtil.convertMarkdownToHtml(filename, markdownContent);
            byte[] pdfBytes = MarkdownToPdfExporterUtil.convertHtmlToPdf(html);

            // 设置响应头
            response.setContentType(MediaType.APPLICATION_PDF_VALUE);
            response.setCharacterEncoding(StandardCharsets.UTF_8.name());
            response.setContentLength(pdfBytes.length);
            response.setHeader(HttpHeaders.CONTENT_DISPOSITION,
                    "attachment; filename=\"" + URLEncoder.encode(filename, StandardCharsets.UTF_8) + ".pdf\"");

            // 添加缓存控制头
            response.setHeader(HttpHeaders.CACHE_CONTROL, "no-cache, no-store, must-revalidate");
            response.setHeader(HttpHeaders.PRAGMA, "no-cache");
            response.setDateHeader(HttpHeaders.EXPIRES, 0);

            // 直接写入二进制数据
            try (OutputStream outputStream = response.getOutputStream()) {
                outputStream.write(pdfBytes);
                outputStream.flush();
            }

        } catch (Exception e) {
            // 错误处理
            response.setStatus(HttpServletResponse.SC_INTERNAL_SERVER_ERROR);
            response.setContentType(MediaType.TEXT_PLAIN_VALUE);
            response.setCharacterEncoding(StandardCharsets.UTF_8.name());
            try (OutputStream outputStream = response.getOutputStream()) {
                outputStream.write(("PDF 生成失败: " + e.getMessage()).getBytes(StandardCharsets.UTF_8));
            }
        }
    }

4.字体截图

相关推荐
爬山算法18 分钟前
Hibernate(15)Hibernate中如何定义一个实体的主键?
java·后端·hibernate
廋到被风吹走20 分钟前
【Spring】Spring AMQP 详细介绍
java·spring·wpf
一起养小猫1 小时前
LeetCode100天Day6-回文数与加一
java·leetcode
程序员小假2 小时前
我们来说一下 MySQL 的慢查询日志
java·后端
独自破碎E2 小时前
Java是怎么实现跨平台的?
java·开发语言
To Be Clean Coder2 小时前
【Spring源码】从源码倒看Spring用法(二)
java·后端·spring
xdpcxq10292 小时前
风控场景下超高并发频次计算服务
java·服务器·网络
想用offer打牌2 小时前
你真的懂Thread.currentThread().interrupt()吗?
java·后端·架构
橘色的狸花猫3 小时前
简历与岗位要求相似度分析系统
java·nlp
独自破碎E3 小时前
Leetcode1438绝对值不超过限制的最长连续子数组
java·开发语言·算法