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.字体截图

相关推荐
一缕茶香思绪万堵4 小时前
028.爬虫专用浏览器-抓取#shadowRoot(closed)下
java·后端
Deamon Tree4 小时前
如何保证缓存与数据库更新时候的一致性
java·数据库·缓存
9号达人4 小时前
认证方案的设计与思考
java·后端·面试
大G的笔记本4 小时前
MySQL 中的 行锁(Record Lock) 和 间隙锁(Gap Lock)
java·数据库·mysql
R.lin4 小时前
Java支付对接策略模式详细设计
java·架构·策略模式
没有bug.的程序员4 小时前
Spring Boot 常见性能与配置优化
java·spring boot·后端·spring·动态代理
没有bug.的程序员4 小时前
Spring Boot Actuator 监控机制解析
java·前端·spring boot·spring·源码
三次拒绝王俊凯4 小时前
java求职学习day47
java·开发语言·学习
包饭厅咸鱼5 小时前
autojs----2025淘宝淘金币跳一跳自动化
java·javascript·自动化