使用 Flying-Saucer-Pdf + velocity 模板引擎生成 PDF(解决中文和图片问题)

使用 Flying Saucer Pdf + 模板引擎生成 PDF(解决中文和图片问题)

概述

本文原创(参考自实际项目经验)介绍如何使用Flying Saucer(flying-saucer-pdf)(xhtmlrenderer)结合多种模板引擎(Velocity、FreeMarker、XHTML)生成PDF文件,并解决中文字体显示和图片嵌入问题。

核心技术栈

  1. Flying Saucer (xhtmlrenderer) - 将XHTML转换为PDF
  2. 模板引擎 - 支持Velocity、FreeMarker、原生XHTML
  3. Base64图片编码 - 解决图片嵌入问题

完整解决方案

1. Maven依赖配置

xml 复制代码
<dependencies>
    <!-- Flying Saucer PDF生成 -->
      <dependency>
           <groupId>org.xhtmlrenderer</groupId>
           <artifactId>flying-saucer-pdf</artifactId>
           <version>9.1.22</version>
     </dependency>
    
    <!-- Velocity模板引擎 -->
    <dependency>
        <groupId>org.apache.velocity</groupId>
        <artifactId>velocity-engine-core</artifactId>
        <version>2.3</version>
    </dependency>
    
    <!-- FreeMarker模板引擎 -->
    <dependency>
        <groupId>org.freemarker</groupId>
        <artifactId>freemarker</artifactId>
        <version>2.3.32</version>
    </dependency>
    
    <!-- 工具类 -->
    <dependency>
        <groupId>cn.hutool</groupId>
        <artifactId>hutool-all</artifactId>
        <version>5.8.16</version>
    </dependency>
</dependencies>

2. 通用PDF生成工具类

java 复制代码
package com.example.pdf.core;

import com.lowagie.text.pdf.BaseFont;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.xhtmlrenderer.pdf.ITextRenderer;
import org.xhtmlrenderer.pdf.ITextUserAgent;

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

/**
 * PDF生成核心类(原创实现)
 * 解决中文显示和图片嵌入问题
 */
public class PdfGenerator {
    
    private static final Logger log = LoggerFactory.getLogger(PdfGenerator.class);
    
    /**
     * 生成PDF文件
     * @param htmlContent HTML内容
     * @param outputPath 输出文件路径
     * @param fontPath 中文字体路径
     */
    public static void generatePdf(String htmlContent, String outputPath, String fontPath) {
        try (OutputStream os = new FileOutputStream(outputPath)) {
            ITextRenderer renderer = new ITextRenderer();
            
            // 设置自定义的ResourceLoader,处理Base64图片
            renderer.getSharedContext().setUserAgentCallback(
                new CustomResourceLoader()
            );
            
            // 设置中文字体
            if (fontPath != null && !fontPath.isEmpty()) {
                setChineseFont(renderer, fontPath);
            }
            
            // 渲染PDF
            renderer.setDocumentFromString(htmlContent);
            renderer.layout();
            renderer.createPDF(os);
            
            log.info("PDF生成成功: {}", outputPath);
        } catch (Exception e) {
            log.error("PDF生成失败", e);
            throw new RuntimeException("PDF生成失败", e);
        }
    }
    
    /**
     * 设置中文字体
     */
    private static void setChineseFont(ITextRenderer renderer, String fontPath) throws Exception {
        try {
            // 尝试加载字体文件
            File fontFile = new File(fontPath);
            if (fontFile.exists()) {
                renderer.getFontResolver().addFont(
                    fontPath, 
                    BaseFont.IDENTITY_H, 
                    BaseFont.EMBEDDED
                );
                log.debug("使用字体文件: {}", fontPath);
            } else {
                // 从classpath加载
                InputStream fontStream = PdfGenerator.class
                    .getClassLoader()
                    .getResourceAsStream(fontPath);
                if (fontStream != null) {
                    // 创建临时字体文件
                    File tempFont = File.createTempFile("font_", ".ttc");
                    try (FileOutputStream fos = new FileOutputStream(tempFont)) {
                        byte[] buffer = new byte[1024];
                        int len;
                        while ((len = fontStream.read(buffer)) != -1) {
                            fos.write(buffer, 0, len);
                        }
                    }
                    renderer.getFontResolver().addFont(
                        tempFont.getAbsolutePath(),
                        BaseFont.IDENTITY_H,
                        BaseFont.EMBEDDED
                    );
                    tempFont.deleteOnExit();
                    log.debug("使用classpath字体: {}", fontPath);
                } else {
                    log.warn("字体文件未找到: {}", fontPath);
                }
            }
        } catch (Exception e) {
            log.warn("字体设置失败,使用默认字体", e);
        }
    }
    
    /**
     * 自定义资源加载器,支持Base64图片
     */
    private static class CustomResourceLoader extends ITextUserAgent {
        
        @Override
        protected InputStream resolveAndOpenStream(String uri) {
            // 处理Base64图片
            if (uri.startsWith("data:image")) {
                try {
                    // 提取Base64数据
                    String base64Data = uri.substring(uri.indexOf(",") + 1);
                    byte[] imageBytes = java.util.Base64.getDecoder().decode(base64Data);
                    return new ByteArrayInputStream(imageBytes);
                } catch (Exception e) {
                    log.error("Base64图片解析失败", e);
                    return null;
                }
            }
            return super.resolveAndOpenStream(uri);
        }
    }
}

3. 模板引擎封装类

java 复制代码
package com.example.pdf.template;

import org.apache.velocity.VelocityContext;
import org.apache.velocity.app.VelocityEngine;
import freemarker.template.Configuration;
import freemarker.template.Template;
import freemarker.template.TemplateException;

import java.io.*;
import java.nio.charset.StandardCharsets;
import java.util.Map;

/**
 * 模板引擎工厂(原创实现)
 * 支持Velocity、FreeMarker和原生XHTML
 */
public class TemplateEngine {
    
    /**
     * Velocity模板引擎
     */
    public static class VelocityRenderer {
        private final VelocityEngine velocityEngine;
        
        public VelocityRenderer() {
            velocityEngine = new VelocityEngine();
            velocityEngine.setProperty("resource.loader", "class");
            velocityEngine.setProperty("class.resource.loader.class", 
                "org.apache.velocity.runtime.resource.loader.ClasspathResourceLoader");
            velocityEngine.setProperty("input.encoding", "UTF-8");
            velocityEngine.setProperty("output.encoding", "UTF-8");
            velocityEngine.init();
        }
        
        public String render(String templatePath, Map<String, Object> data) throws Exception {
            VelocityContext context = new VelocityContext();
            if (data != null) {
                data.forEach(context::put);
            }
            
            String templateContent = loadTemplate(templatePath);
            StringWriter writer = new StringWriter();
            velocityEngine.evaluate(context, writer, "template", templateContent);
            
            return writer.toString();
        }
    }
    
    /**
     * FreeMarker模板引擎
     */
    public static class FreeMarkerRenderer {
        private final Configuration configuration;
        
        public FreeMarkerRenderer(String templateDirectory) throws IOException {
            configuration = new Configuration(Configuration.VERSION_2_3_32);
            configuration.setDirectoryForTemplateLoading(
                new File(templateDirectory)
            );
            configuration.setDefaultEncoding("UTF-8");
        }
        
        public String render(String templateName, Map<String, Object> data) 
                throws IOException, TemplateException {
            
            Template template = configuration.getTemplate(templateName);
            StringWriter writer = new StringWriter();
            template.process(data, writer);
            
            return writer.toString();
        }
    }
    
    /**
     * 原生XHTML模板(直接使用)
     */
    public static class XhtmlRenderer {
        public String render(String templatePath, Map<String, Object> data) throws Exception {
            String templateContent = loadTemplate(templatePath);
            
            // 简单替换变量(实际项目可替换为更复杂的逻辑)
            if (data != null) {
                for (Map.Entry<String, Object> entry : data.entrySet()) {
                    String placeholder = "${" + entry.getKey() + "}";
                    templateContent = templateContent.replace(
                        placeholder, 
                        String.valueOf(entry.getValue())
                    );
                }
            }
            
            return templateContent;
        }
    }
    
    /**
     * 加载模板文件
     */
    private static String loadTemplate(String templatePath) throws IOException {
        try (InputStream is = TemplateEngine.class
                .getClassLoader()
                .getResourceAsStream(templatePath)) {
            
            if (is == null) {
                throw new FileNotFoundException("模板文件未找到: " + templatePath);
            }
            
            return readInputStream(is);
        }
    }
    
    /**
     * 读取输入流
     */
    private static String readInputStream(InputStream is) throws IOException {
        StringBuilder content = new StringBuilder();
        try (BufferedReader reader = new BufferedReader(
                new InputStreamReader(is, StandardCharsets.UTF_8))) {
            String line;
            while ((line = reader.readLine()) != null) {
                content.append(line).append("\n");
            }
        }
        return content.toString();
    }
}

4. 图片处理工具类

java 复制代码
package com.example.pdf.util;

import java.io.File;
import java.io.IOException;
import java.nio.file.Files;
import java.util.Base64;

/**
 * 图片处理工具类(原创实现)
 * 解决Flying Saucer图片嵌入问题
 */
public class ImageProcessor {
    
    /**
     * 将图片文件转换为Base64编码
     * @param imageFile 图片文件
     * @return Base64编码的图片字符串
     */
    public static String imageToBase64(File imageFile) throws IOException {
        if (!imageFile.exists()) {
            throw new IllegalArgumentException("图片文件不存在: " + imageFile.getPath());
        }
        
        byte[] imageBytes = Files.readAllBytes(imageFile.toPath());
        String base64 = Base64.getEncoder().encodeToString(imageBytes);
        String mimeType = getMimeType(imageFile.getName());
        
        return String.format("data:%s;base64,%s", mimeType, base64);
    }
    
    /**
     * 将图片字节数组转换为Base64编码
     * @param imageBytes 图片字节数组
     * @param fileName 文件名(用于确定MIME类型)
     * @return Base64编码的图片字符串
     */
    public static String bytesToBase64(byte[] imageBytes, String fileName) {
        if (imageBytes == null || imageBytes.length == 0) {
            return "";
        }
        
        String base64 = Base64.getEncoder().encodeToString(imageBytes);
        String mimeType = getMimeType(fileName);
        
        return String.format("data:%s;base64,%s", mimeType, base64);
    }
    
    /**
     * 根据文件名获取MIME类型
     */
    private static String getMimeType(String fileName) {
        if (fileName == null) {
            return "image/jpeg";
        }
        
        fileName = fileName.toLowerCase();
        if (fileName.endsWith(".png")) return "image/png";
        if (fileName.endsWith(".jpg") || fileName.endsWith(".jpeg")) return "image/jpeg";
        if (fileName.endsWith(".gif")) return "image/gif";
        if (fileName.endsWith(".bmp")) return "image/bmp";
        if (fileName.endsWith(".svg")) return "image/svg+xml";
        if (fileName.endsWith(".tiff") || fileName.endsWith(".tif")) return "image/tiff";
        
        return "image/jpeg";
    }
    
    /**
     * 下载网络图片并转换为Base64
     * @param imageUrl 图片URL
     * @return Base64编码的图片字符串
     */
    public static String downloadImageToBase64(String imageUrl) {
        try {
            // 使用Hutool简化HTTP请求
            byte[] bytes = cn.hutool.http.HttpUtil.downloadBytes(imageUrl);
            return bytesToBase64(bytes, getFileNameFromUrl(imageUrl));
        } catch (Exception e) {
            throw new RuntimeException("下载图片失败: " + imageUrl, e);
        }
    }
    
    /**
     * 从URL提取文件名
     */
    private static String getFileNameFromUrl(String url) {
        if (url == null || url.isEmpty()) {
            return "image.jpg";
        }
        
        int lastSlash = url.lastIndexOf('/');
        if (lastSlash >= 0 && lastSlash < url.length() - 1) {
            return url.substring(lastSlash + 1);
        }
        
        return "image.jpg";
    }
}

5. 使用示例

5.1 使用Velocity模板

模板文件:templates/velocity/report.vm

html 复制代码
<!DOCTYPE html>
<html>
<head>
    <meta charset="UTF-8">
    <title>业务报告</title>
    <style>
        body { font-family: "SimSun", sans-serif; }
        .header { text-align: center; }
        .logo { width: 100px; height: 60px; }
        .table { border-collapse: collapse; width: 100%; }
        .table th, .table td { border: 1px solid #ddd; padding: 8px; }
    </style>
</head>
<body>
    <div class="header">
        <h1>$report.title</h1>
        <p>生成日期: $dateUtil.format($report.date, "yyyy年MM月dd日")</p>
    </div>
    
    <table class="table">
        <thead>
            <tr>
                <th>序号</th>
                <th>项目名称</th>
                <th>金额</th>
                <th>备注</th>
            </tr>
        </thead>
        <tbody>
            #foreach($item in $report.items)
            <tr>
                <td>$velocityCount</td>
                <td>$item.name</td>
                <td>¥$number.format('#,##0.00', $item.amount)</td>
                <td>$item.remark</td>
            </tr>
            #end
        </tbody>
    </table>
    
    <div style="margin-top: 30px;">
        <p>总计: ¥$number.format('#,##0.00', $report.totalAmount)</p>
        #if($report.signature)
        <p>签章:</p>
        <img src="$report.signature" alt="电子签章" style="width: 120px;"/>
        #end
    </div>
</body>
</html>

Java代码:

java 复制代码
// 使用Velocity模板生成PDF
TemplateEngine.VelocityRenderer velocityRenderer = 
    new TemplateEngine.VelocityRenderer();

Map<String, Object> data = new HashMap<>();
// 准备数据...
data.put("report", reportData);

// 将图片转换为Base64
String signatureBase64 = ImageProcessor.imageToBase64(
    new File("signature.png")
);
data.put("report.signature", signatureBase64);

// 渲染模板
String html = velocityRenderer.render(
    "templates/velocity/report.vm", 
    data
);

// 生成PDF
PdfGenerator.generatePdf(
    html, 
    "report.pdf", 
    "fonts/simsun.ttc"
);
5.2 使用FreeMarker模板

模板文件:templates/freemarker/invoice.ftl

html 复制代码
<!DOCTYPE html>
<html>
<head>
    <meta charset="UTF-8">
    <title>发票</title>
    <style>
        body { font-family: "SimSun", sans-serif; font-size: 12px; }
        .invoice-table { border-collapse: collapse; width: 100%; }
        .invoice-table th { background-color: #f2f2f2; }
    </style>
</head>
<body>
    <h2 style="text-align: center;">增值税专用发票</h2>
    
    <table class="invoice-table">
        <tr>
            <td colspan="2">购方: ${buyer.name!""}</td>
            <td colspan="2">销方: ${seller.name!""}</td>
        </tr>
        <#list items as item>
        <tr>
            <td>${item_index + 1}</td>
            <td>${item.productName!""}</td>
            <td>${item.quantity!0}</td>
            <td>¥${item.price?string("#,##0.00")}</td>
        </tr>
        </#list>
    </table>
    
    <div style="margin-top: 20px;">
        <p>合计金额: ¥${totalAmount?string("#,##0.00")}</p>
        <#if qrCode??>
        <img src="${qrCode}" alt="二维码" style="width: 80px; height: 80px;"/>
        </#if>
    </div>
</body>
</html>

Java代码:

java 复制代码
// 使用FreeMarker模板生成PDF
TemplateEngine.FreeMarkerRenderer freemarkerRenderer = 
    new TemplateEngine.FreeMarkerRenderer("templates/freemarker");

Map<String, Object> data = new HashMap<>();
// 准备数据...

// 生成二维码Base64
String qrCodeBase64 = generateQrCodeBase64("发票编号: INV001");
data.put("qrCode", qrCodeBase64);

// 渲染模板
String html = freemarkerRenderer.render("invoice.ftl", data);

// 生成PDF
PdfGenerator.generatePdf(
    html, 
    "invoice.pdf", 
    "/usr/share/fonts/chinese/SimSun.ttf"
);
5.3 使用原生XHTML模板

模板文件:templates/xhtml/certificate.xhtml

html 复制代码
<!DOCTYPE html>
<html xmlns="http://www.w3.org/1999/xhtml">
<head>
    <meta charset="UTF-8"/>
    <title>荣誉证书</title>
    <style>
        @page {
            size: A4 landscape;
            margin: 50px;
        }
        
        body {
            font-family: "SimSun", "STSong", serif;
            background-image: url('${backgroundImage}');
            background-size: cover;
            text-align: center;
        }
        
        .certificate {
            padding: 100px;
        }
        
        .title {
            font-size: 36px;
            font-weight: bold;
            color: #b22222;
            margin-bottom: 60px;
        }
        
        .content {
            font-size: 24px;
            line-height: 1.8;
            margin-bottom: 40px;
        }
        
        .signature {
            font-size: 18px;
            margin-top: 80px;
        }
        
        .stamp {
            position: absolute;
            right: 150px;
            bottom: 150px;
            width: 120px;
            height: 120px;
            opacity: 0.9;
        }
    </style>
</head>
<body>
    <div class="certificate">
        <div class="title">荣誉证书</div>
        <div class="content">
            兹授予 <strong>${recipientName}</strong> 同志<br/>
            在${year}年度${achievement}中表现突出,<br/>
            特发此证,以资鼓励。
        </div>
        <div class="signature">
            <p>${organizationName}</p>
            <p>${issueDate}</p>
        </div>
        <img src="${stampImage}" class="stamp" alt="公章"/>
    </div>
</body>
</html>

Java代码:

java 复制代码
// 使用原生XHTML模板生成PDF
TemplateEngine.XhtmlRenderer xhtmlRenderer = 
    new TemplateEngine.XhtmlRenderer();

Map<String, Object> data = new HashMap<>();
data.put("recipientName", "张三");
data.put("year", "2024");
data.put("achievement", "技术研发项目");

// 将印章图片转换为Base64
String stampBase64 = ImageProcessor.imageToBase64(
    new File("stamp.png")
);
data.put("stampImage", stampBase64);

// 背景图片(使用Base64或文件路径)
data.put("backgroundImage", 
    "data:image/jpeg;base64,/9j/4AAQSkZJRgABAQEAAAAAAAD/...");

// 渲染模板
String html = xhtmlRenderer.render(
    "templates/xhtml/certificate.xhtml", 
    data
);

// 生成PDF
PdfGenerator.generatePdf(
    html, 
    "certificate.pdf", 
    "classpath:fonts/simsun.ttc"
);

6. 高级功能:缓存和性能优化

java 复制代码
package com.example.pdf.advanced;

import com.example.pdf.core.PdfGenerator;
import com.example.pdf.template.TemplateEngine;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.Executors;
import java.util.concurrent.ScheduledExecutorService;
import java.util.concurrent.TimeUnit;

/**
 * 高级PDF生成服务(原创实现)
 * 包含模板缓存、异步生成等功能
 */
public class AdvancedPdfService {
    
    private static final Logger log = LoggerFactory.getLogger(AdvancedPdfService.class);
    
    // 模板缓存
    private final Map<String, String> templateCache = new ConcurrentHashMap<>();
    
    // 清理缓存的调度器
    private final ScheduledExecutorService scheduler = 
        Executors.newScheduledThreadPool(1);
    
    public AdvancedPdfService() {
        // 每小时清理一次缓存
        scheduler.scheduleAtFixedRate(() -> {
            log.info("清理模板缓存,当前大小: {}", templateCache.size());
            templateCache.clear();
        }, 1, 1, TimeUnit.HOURS);
    }
    
    /**
     * 带缓存的模板渲染
     */
    public String renderWithCache(String templatePath, 
                                  Map<String, Object> data,
                                  String engineType) throws Exception {
        
        String cacheKey = templatePath + "|" + engineType;
        String templateContent = templateCache.get(cacheKey);
        
        if (templateContent == null) {
            templateContent = loadTemplateContent(templatePath);
            templateCache.put(cacheKey, templateContent);
            log.debug("缓存模板: {}", cacheKey);
        }
        
        return renderTemplate(templateContent, data, engineType);
    }
    
    /**
     * 异步生成PDF
     */
    public void generatePdfAsync(String templatePath,
                                 Map<String, Object> data,
                                 String outputPath,
                                 String fontPath,
                                 String engineType) {
        
        Executors.newSingleThreadExecutor().submit(() -> {
            try {
                String html = renderWithCache(templatePath, data, engineType);
                PdfGenerator.generatePdf(html, outputPath, fontPath);
                log.info("异步PDF生成完成: {}", outputPath);
            } catch (Exception e) {
                log.error("异步PDF生成失败", e);
            }
        });
    }
    
    /**
     * 批量生成PDF
     */
    public void batchGeneratePdf(String templatePath,
                                 Iterable<Map<String, Object>> dataList,
                                 String outputPattern,
                                 String fontPath,
                                 String engineType) {
        
        int index = 0;
        for (Map<String, Object> data : dataList) {
            String outputPath = String.format(outputPattern, index++);
            generatePdfAsync(templatePath, data, outputPath, fontPath, engineType);
        }
    }
    
    /**
     * 加载模板内容
     */
    private String loadTemplateContent(String templatePath) throws Exception {
        try (java.io.InputStream is = getClass()
                .getClassLoader()
                .getResourceAsStream(templatePath)) {
            
            if (is == null) {
                throw new IllegalArgumentException("模板未找到: " + templatePath);
            }
            
            return new String(is.readAllBytes(), java.nio.charset.StandardCharsets.UTF_8);
        }
    }
    
    /**
     * 渲染模板
     */
    private String renderTemplate(String templateContent,
                                  Map<String, Object> data,
                                  String engineType) throws Exception {
        
        switch (engineType.toLowerCase()) {
            case "velocity":
                return renderVelocity(templateContent, data);
            case "freemarker":
                return renderFreemarker(templateContent, data);
            case "xhtml":
                return renderXhtml(templateContent, data);
            default:
                throw new IllegalArgumentException("不支持的模板引擎: " + engineType);
        }
    }
    
    private String renderVelocity(String templateContent, Map<String, Object> data) 
            throws Exception {
        // Velocity渲染实现...
        return templateContent; // 简化示例
    }
    
    private String renderFreemarker(String templateContent, Map<String, Object> data) 
            throws Exception {
        // FreeMarker渲染实现...
        return templateContent; // 简化示例
    }
    
    private String renderXhtml(String templateContent, Map<String, Object> data) {
        // XHTML简单变量替换
        String result = templateContent;
        for (Map.Entry<String, Object> entry : data.entrySet()) {
            String placeholder = "${" + entry.getKey() + "}";
            result = result.replace(placeholder, 
                entry.getValue() != null ? entry.getValue().toString() : "");
        }
        return result;
    }
    
    /**
     * 关闭服务
     */
    public void shutdown() {
        scheduler.shutdown();
        try {
            if (!scheduler.awaitTermination(5, TimeUnit.SECONDS)) {
                scheduler.shutdownNow();
            }
        } catch (InterruptedException e) {
            scheduler.shutdownNow();
            Thread.currentThread().interrupt();
        }
    }
}

关键问题解决方案

1. 中文字体显示问题

核心解决方案

java 复制代码
// 关键代码:添加中文字体
renderer.getFontResolver().addFont(
    "fonts/simsun.ttc",  // 字体文件路径
    BaseFont.IDENTITY_H, // 使用Unicode编码
    BaseFont.EMBEDDED    // 嵌入字体
);

字体文件获取

  1. Windows系统:C:/Windows/Fonts/simsun.ttc
  2. Linux系统:/usr/share/fonts/chinese/SimSun.ttf
  3. 将字体文件打包到项目resources中

2. 图片显示问题

Base64解决方案

在PDF中插入 png图片的base64格式

html 复制代码
<!-- 在HTML模板中使用Base64图片 -->
<img src="data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAA..." />

参考GitHub Issue

3. 性能优化建议

  1. 模板缓存:避免重复读取和解析模板文件
  2. 字体缓存:字体只需加载一次
  3. 异步生成:批量处理时使用线程池
  4. 图片优化
    • 压缩图片减小体积
    • 使用WebP格式(需转换)
    • 懒加载非必要图片

总结

本文提供了完整的Flying Saucer PDF生成解决方案,具有以下特点:

核心优势

  1. 多模板引擎支持:Velocity、FreeMarker、原生XHTML
  2. 完美中文支持:通过嵌入字体解决乱码问题
  3. 图片兼容性好:Base64编码避免路径问题
  4. 高性能设计:支持缓存和异步生成

使用场景

  • 业务报表:销售报表、财务报表
  • 证书文档:毕业证、荣誉证书
  • 合同协议:电子合同、协议文件
  • 发票单据:增值税发票、收据

注意事项

  1. 字体文件需要合法授权
  2. Base64图片会增加HTML体积
  3. 复杂布局需要CSS支持

参考资料

本文代码为原创实现,参考了实际项目经验。相关技术参考:

  1. Flying Saucer GitHub Repository
  2. Base64 Image Support Issue
  3. Velocity Engine Documentation
  4. FreeMarker Documentation
相关推荐
kong79069286 小时前
Java-Intellij IDEA 自动导包设置
java·ide·intellij-idea
twj_one10 小时前
Arthas使用
java
lizz3111 小时前
C++模板编程:从入门到精通
java·开发语言·c++
shoubepatien12 小时前
JAVA -- 05
java·开发语言
寰天柚子12 小时前
Java并发编程中的线程安全问题与解决方案全解析
java·开发语言·python
memgLIFE12 小时前
Springboot 分层结构
java·spring boot·spring
晚烛12 小时前
实战前瞻:构建高可靠、强协同的 Flutter + OpenHarmony 智慧教育平台
javascript·flutter·html
shoubepatien12 小时前
JAVA -- 08
java·后端·intellij-idea
kong790692812 小时前
Java新特性-(二)Java基础语法
java·新特性·java 基础语法