1. 项目概述
在业务系统开发中,将富文本内容导出为PDF文档是一个常见需求。无论是合同文档生成、报告导出还是通知存档,都需要一个可靠的技术方案来实现HTML到PDF的转换。本文将详细介绍基于Java的富文本转PDF实现方案,支持自定义标题和二维码嵌入功能。
这个方案的核心价值在于:
- 直接使用业务系统中已有的HTML富文本内容
- 自动处理中文排版和字体问题
- 灵活添加二维码等辅助信息
- 提供可配置的样式选项
我曾在多个企业级项目中应用此方案,包括合同管理系统、报告生成平台等,累计处理过数十万份文档的生成需求。下面将分享这个经过实战检验的实现方案。
2. 技术选型分析
2.1 主流PDF生成方案对比
在Java生态中,常见的PDF生成方案有以下几种:
| 技术方案 | 核心优势 | 主要局限 | 适用场景 |
|---|---|---|---|
| Flying Saucer | HTML/CSS直接渲染、上手简单 | CSS3支持有限 | 简单HTML转PDF |
| iText | 功能强大、控制精细 | 学习曲线陡峭、AGPL许可证 | 复杂PDF生成 |
| OpenPDF | 开源免费(LGPL)、无许可证问题 | 富文本支持较弱 | 开源项目 |
| wkhtmltopdf | 渲染效果最佳、支持现代CSS/JS | 需安装外部程序 | 对样式要求高的场景 |
2.2 为什么选择Flying Saucer + iText组合
经过多个项目的实践验证,我最终选择了Flying Saucer作为基础渲染引擎,配合iText进行底层PDF生成,主要基于以下考虑:
- 开发效率:Flying Saucer可以直接渲染HTML/CSS,减少了学习成本
- 功能平衡:既满足了富文本渲染需求,又保持了足够的灵活性
- 中文支持:通过系统字体自动检测,解决了中文乱码问题
- 扩展性:二维码生成等附加功能可以方便地集成
提示:虽然iText采用AGPL许可证,但在仅使用Flying Saucer接口的情况下,不会触发AGPL条款。如需商业闭源,可考虑替换为OpenPDF。
3. 环境准备与依赖配置
3.1 Maven依赖配置
在项目的pom.xml中添加以下依赖:
xml复制<!-- Flying Saucer PDF渲染核心 -->
<dependency>
<groupId>org.xhtmlrenderer</groupId>
<artifactId>flying-saucer-pdf</artifactId>
<version>9.1.22</version>
</dependency>
<!-- 二维码生成库 -->
<dependency>
<groupId>com.google.zxing</groupId>
<artifactId>core</artifactId>
<version>3.5.1</version>
</dependency>
<dependency>
<groupId>com.google.zxing</groupId>
<artifactId>javase</artifactId>
<version>3.5.1</version>
</dependency>
<!-- HTML清理与防护 -->
<dependency>
<groupId>org.jsoup</groupId>
<artifactId>jsoup</artifactId>
<version>1.15.4</version>
</dependency>
3.2 字体环境准备
中文字体支持是本方案的关键点之一。根据不同的操作系统,需要确保以下字体可用:
-
Windows系统:
- 默认已安装宋体(simsun.ttc)和黑体(simhei.ttf)
- 路径通常为:C:/Windows/Fonts/
-
Linux系统:
- 需要手动安装中文字体包
- Ubuntu/Debian:
sudo apt install fonts-wqy-microhei - CentOS/RHEL:
sudo yum install wqy-microhei-fonts
注意事项:生产环境部署时,务必确认服务器已安装所需字体,否则会导致中文显示为空白或乱码。
4. 核心实现详解
4.1 整体架构设计
整个PDF生成流程分为四个主要步骤:
- HTML清理与安全处理:使用Jsoup过滤潜在XSS风险
- 二维码生成:通过ZXing库生成Base64编码的二维码图片
- HTML模板组装:将标题、内容和二维码组合成完整HTML文档
- PDF渲染输出:使用Flying Saucer将HTML转换为PDF字节数组
4.2 关键代码实现
4.2.1 HTML安全处理
java复制private static String sanitizeHtml(String html) {
if (html == null) return "";
Safelist safelist = Safelist.relaxed()
.addTags("h1", "h2", "h3", "h4", "h5", "h6")
.addTags("table", "thead", "tbody", "tr", "th", "td")
.addTags("ul", "ol", "li", "img", "a")
.addAttributes("table", "border", "cellpadding", "style", "width")
.addAttributes("img", "src", "alt", "width", "height", "style")
.addAttributes("a", "href", "target")
.addAttributes(":all", "style", "class");
return Jsoup.clean(html, safelist);
}
这段代码做了以下安全处理:
- 只允许安全的HTML标签和属性
- 移除了所有脚本相关的标签和事件属性
- 保留了基本的文本格式和表格结构
4.2.2 二维码生成
java复制public static String generateQrCodeBase64(String content, int size) {
try {
Map<EncodeHintType, Object> hints = new HashMap<>();
hints.put(EncodeHintType.ERROR_CORRECTION, ErrorCorrectionLevel.H);
hints.put(EncodeHintType.CHARACTER_SET, "UTF-8");
hints.put(EncodeHintType.MARGIN, 1);
QRCodeWriter qrCodeWriter = new QRCodeWriter();
BitMatrix bitMatrix = qrCodeWriter.encode(
content, BarcodeFormat.QR_CODE, size, size, hints);
ByteArrayOutputStream outputStream = new ByteArrayOutputStream();
MatrixToImageWriter.writeToStream(bitMatrix, "PNG", outputStream);
return "data:image/png;base64," +
Base64.getEncoder().encodeToString(outputStream.toByteArray());
} catch (WriterException | IOException e) {
throw new RuntimeException("生成二维码失败", e);
}
}
二维码生成的关键参数说明:
ErrorCorrectionLevel.H:最高容错级别,即使部分损坏仍可识别CHARACTER_SET:明确指定UTF-8编码,支持中文内容MARGIN:设置二维码边距为1,避免过于紧凑
4.2.3 PDF渲染核心
java复制private static byte[] renderPdf(String html) {
try (ByteArrayOutputStream outputStream = new ByteArrayOutputStream()) {
ITextRenderer renderer = new ITextRenderer();
renderer.getFontResolver().addFont(DEFAULT_FONT_PATH, "Identity-H", false);
renderer.setDocumentFromString(html);
renderer.layout();
renderer.createPDF(outputStream);
return outputStream.toByteArray();
} catch (Exception e) {
throw new RuntimeException("生成PDF失败", e);
}
}
字体处理是这段代码的关键点:
Identity-H表示使用Unicode水平书写false参数表示不嵌入字体到PDF中- 字体路径通过
getFontPath()方法自动检测系统字体
4.3 样式与配置管理
通过PdfConfig类提供灵活的样式配置:
java复制public static class PdfConfig {
private int titleFontSize = 18;
private String titleColor = "#333333";
private int qrCodeSize = DEFAULT_QR_CODE_SIZE;
private String qrCodeLabel;
// 链式setter方法
public PdfConfig setTitleFontSize(int titleFontSize) {
this.titleFontSize = titleFontSize;
return this;
}
// 其他setter方法...
}
默认样式通过CSS字符串定义:
java复制private static String getDefaultStyles(PdfConfig config) {
return "@page { size: A4; margin: 20mm 15mm; }" +
"body { font-family: SimSun, serif; font-size: 12pt; }" +
".document-title { text-align: center; font-size: " +
config.getTitleFontSize() + "pt; color: " + config.getTitleColor() + "; }" +
".qr-code-container { position: absolute; top: 0; right: 0; }" +
".content p { text-indent: 2em; }";
}
5. 使用示例与最佳实践
5.1 基础用法
java复制// 最简单的用法
byte[] pdfBytes = RichTextToPdfUtils.generatePdf(
"合同文档",
"<p>甲方:<b>XX公司</b></p><p>乙方:<b>YY公司</b></p>",
"https://example.com/contract/123"
);
// 保存到文件
Files.write(Paths.get("contract.pdf"), pdfBytes);
5.2 自定义配置
java复制// 创建自定义配置
PdfConfig config = new PdfConfig()
.setTitleFontSize(20)
.setTitleColor("#1a73e8")
.setQrCodeSize(120)
.setQrCodeLabel("扫码查看电子版");
// 生成带配置的PDF
byte[] pdfBytes = RichTextToPdfUtils.generatePdf(
"年度报告",
reportContent,
"https://example.com/report/2023",
config
);
5.3 性能优化建议
- 对象复用:频繁生成PDF时,可以复用ITextRenderer实例
- 字体缓存:将字体加载到内存中,避免重复读取
- 异步生成:大量文档生成时使用线程池处理
- 资源释放:确保正确关闭所有流资源
优化后的渲染代码示例:
java复制public class PdfGenerator {
private static ITextRenderer renderer;
private static byte[] fontBytes;
static {
try {
// 预加载字体
fontBytes = Files.readAllBytes(Paths.get(getFontPath()));
// 初始化渲染器
renderer = new ITextRenderer();
renderer.getFontResolver().addFont(
fontBytes, "Identity-H", true); // 嵌入字体
} catch (Exception e) {
throw new RuntimeException("初始化PDF渲染器失败", e);
}
}
public static byte[] generatePdf(String html) {
synchronized (renderer) {
try {
ByteArrayOutputStream outputStream = new ByteArrayOutputStream();
renderer.setDocumentFromString(html);
renderer.layout();
renderer.createPDF(outputStream);
return outputStream.toByteArray();
} catch (Exception e) {
throw new RuntimeException("生成PDF失败", e);
}
}
}
}
6. 常见问题与解决方案
6.1 中文显示异常
问题现象:生成的PDF中中文显示为空白或乱码
解决方案:
- 确认服务器已安装中文字体
- 检查字体路径是否正确
- 尝试显式指定字体:
java复制renderer.getFontResolver().addFont( "C:/Windows/Fonts/simsun.ttc", "Identity-H", true); // 第三个参数true表示嵌入字体
6.2 图片无法显示
问题现象:HTML中的图片在PDF中不显示
解决方案:
- 使用Base64内嵌图片:
html复制<img src="data:image/png;base64,iVBORw0KGgoAAA..."/> - 或使用绝对路径:
html复制<img src="file:///path/to/image.png"/>
6.3 样式渲染不一致
问题现象:某些CSS样式在PDF中效果与浏览器不同
解决方案:
- 避免使用CSS3特性,Flying Saucer仅支持CSS2.1
- 简化复杂的选择器和样式规则
- 使用内联样式替代外部样式表
6.4 性能问题
问题现象:生成大量PDF时速度慢或内存溢出
优化建议:
- 增加JVM内存:
-Xmx1024m - 使用try-with-resources确保资源释放
- 考虑分批次生成或使用消息队列异步处理
7. 扩展功能实现
7.1 添加页眉页脚
可以通过覆盖Flying Saucer的渲染器来实现:
java复制public class HeaderFooterRenderer extends ITextRenderer {
@Override
protected void layout(List<PageBox> pages) {
super.layout(pages);
addHeaderFooter(pages);
}
private void addHeaderFooter(List<PageBox> pages) {
for (PageBox page : pages) {
// 添加页眉
PdfContentByte cb = writer.getDirectContent();
cb.beginText();
cb.setFontAndSize(baseFont, 10);
cb.showTextAligned(PdfContentByte.ALIGN_CENTER,
"机密文档 - 禁止外传",
page.getWidth()/2,
page.getHeight()-20,
0);
cb.endText();
// 添加页脚
cb.beginText();
cb.showTextAligned(PdfContentByte.ALIGN_CENTER,
"第 " + (pages.indexOf(page)+1) + " 页",
page.getWidth()/2,
30,
0);
cb.endText();
}
}
}
7.2 支持分页控制
通过CSS控制分页行为:
css复制.page-break {
page-break-after: always; /* 强制分页 */
}
.no-break {
page-break-inside: avoid; /* 避免内容被分割 */
}
在HTML中使用:
html复制<div class="chapter">
<h2>第一章</h2>
<p>内容...</p>
</div>
<div class="page-break"></div>
<div class="chapter">
<h2>第二章</h2>
<p>内容...</p>
</div>
7.3 添加水印效果
通过PDF图层添加水印:
java复制private static void addWatermark(PdfWriter writer, String text) {
PdfContentByte canvas = writer.getDirectContentUnder();
BaseFont font = BaseFont.createFont(
BaseFont.HELVETICA, BaseFont.WINANSI, BaseFont.EMBEDDED);
canvas.beginText();
canvas.setColorFill(BaseColor.LIGHT_GRAY);
canvas.setFontAndSize(font, 60);
// 设置水印文字和角度
for (int i = 0; i < 5; i++) {
for (int j = 0; j < 5; j++) {
canvas.showTextAligned(PdfContentByte.ALIGN_CENTER,
text,
100 + i * 150,
100 + j * 150,
45);
}
}
canvas.endText();
}
8. 实际应用案例
8.1 合同管理系统
在某金融企业的合同管理系统中,我们使用此方案实现了:
- 自动生成带二维码的电子合同
- 合同内容与业务数据动态绑定
- 批量生成数百份合同文档
- 添加数字签名验证
关键实现代码:
java复制public byte[] generateContract(Contract contract, User signer) {
// 构建HTML内容
String html = templateEngine.process("contract-template",
createContext(contract, signer));
// 生成带签名的二维码
String qrContent = "https://contract.example.com/verify?"
+ "id=" + contract.getId()
+ "&signature=" + digitalSignatureService.sign(contract);
// 使用自定义配置
PdfConfig config = new PdfConfig()
.setQrCodeLabel("扫码验证合同真伪");
return RichTextToPdfUtils.generatePdf(
contract.getTitle(), html, qrContent, config);
}
8.2 报告生成平台
在某数据分析平台中,实现了:
- 可视化编辑报告模板
- 动态数据绑定
- 自动生成带图表的多页PDF报告
- 企业品牌样式定制
性能优化措施:
- 使用Thymeleaf模板引擎预处理HTML
- 实现PDF生成队列
- 添加内存缓存
- 支持断点续生成
9. 技术方案对比与替代选择
9.1 与商业方案对比
| 特性 | 本方案 | 商业PDF库(如Aspose) |
|---|---|---|
| 成本 | 免费 | 高昂的授权费用 |
| 功能完整性 | 满足基本需求 | 功能全面 |
| 维护需求 | 需自行维护 | 商业支持 |
| 定制灵活性 | 高 | 中等 |
| 部署复杂度 | 需配置字体环境 | 简单 |
9.2 替代技术方案
如果本方案不满足需求,可以考虑:
- OpenHTMLToPDF:Flying Saucer的现代分支,支持CSS3
- PDFBox:Apache项目,纯Java实现
- JasperReports:专业的报表工具,支持复杂布局
迁移到OpenHTMLToPDF的示例:
xml复制<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>
对应的渲染代码:
java复制try (OutputStream os = new FileOutputStream("output.pdf")) {
PdfRendererBuilder builder = new PdfRendererBuilder();
builder.withHtmlContent(html, null);
builder.toStream(os);
builder.run();
}
10. 安全注意事项
10.1 XSS防护
虽然使用了Jsoup清理HTML,但仍需注意:
- 避免直接使用用户输入的HTML
- 定期更新Jsoup版本
- 根据业务需求调整Safelist配置
增强的安全配置示例:
java复制Safelist safelist = new Safelist()
.addTags("p", "b", "i", "u", "br")
.addAttributes(":all", "style")
.addEnforcedAttribute("a", "rel", "nofollow");
10.2 文件安全
- 限制文件生成目录
- 设置适当的文件权限
- 对用户提供的文件路径进行规范化处理
安全的文件保存方法:
java复制public void savePdfSafely(byte[] content, String userProvidedName)
throws IOException {
// 规范化路径
Path safePath = Paths.get("safe_dir",
FilenameUtils.getName(userProvidedName)).normalize();
// 验证是否在允许的目录内
if (!safePath.startsWith(Paths.get("safe_dir").normalize())) {
throw new IOException("非法路径");
}
Files.write(safePath, content);
}
11. 测试策略
11.1 单元测试要点
-
测试各种HTML输入:
- 简单文本
- 复杂表格
- 嵌套列表
- 包含图片的内容
-
边界条件测试:
- 空内容
- 超长文本
- 特殊字符
-
二维码测试:
- 不同尺寸
- 包含中文的URL
- 超长内容
11.2 集成测试建议
-
性能测试:
- 单次生成时间
- 并发生成能力
- 内存占用情况
-
视觉回归测试:
- 对比不同版本的PDF输出
- 验证样式一致性
-
跨平台测试:
- 不同操作系统
- 不同Java版本
- 不同字体环境
12. 部署与监控
12.1 生产环境部署
-
字体打包:将所需字体打包到应用中
java复制// 从classpath加载字体 InputStream fontStream = getClass().getResourceAsStream("/fonts/simsun.ttc"); renderer.getFontResolver().addFont( fontStream, "Identity-H", true); -
内存配置:增加JVM堆内存
code复制-Xms512m -Xmx2048m -
健康检查:添加PDF生成健康端点
java复制@GetMapping("/health/pdf") public ResponseEntity<String> checkPdfHealth() { try { byte[] pdf = RichTextToPdfUtils.generatePdf( "健康检查", "<p>测试内容</p>", null); return pdf.length > 0 ? ResponseEntity.ok("OK") : ResponseEntity.status(503).body("生成失败"); } catch (Exception e) { return ResponseEntity.status(503) .body("PDF服务异常: " + e.getMessage()); } }
12.2 监控指标
建议监控以下指标:
- 生成成功率
- 平均生成时间
- 内存使用峰值
- 并发生成数量
- 错误类型统计
使用Micrometer示例:
java复制@Autowired
private MeterRegistry meterRegistry;
public byte[] generatePdfWithMetrics(String title, String content) {
Timer.Sample sample = Timer.start(meterRegistry);
try {
byte[] pdf = RichTextToPdfUtils.generatePdf(title, content, null);
sample.stop(meterRegistry.timer("pdf.generate.time"));
meterRegistry.counter("pdf.generate.success").increment();
return pdf;
} catch (Exception e) {
meterRegistry.counter("pdf.generate.errors").increment();
throw e;
}
}
13. 经验总结与建议
在实际项目中应用此方案时,我总结了以下几点经验:
-
字体问题排查:当遇到中文显示问题时,首先检查:
- 系统字体目录权限
- 字体文件是否损坏
- Java进程是否有权限读取字体
-
性能优化:对于高并发场景:
- 考虑预初始化ITextRenderer实例
- 使用对象池技术
- 异步生成+回调通知
-
样式调试技巧:
- 先确保HTML在浏览器中显示正常
- 使用简单的内联样式逐步测试
- 记录渲染警告和错误
-
异常处理:
java复制try { return generatePdf(content); } catch (Exception e) { logger.error("PDF生成失败,内容摘要: " + StringUtils.abbreviate(content, 100), e); throw new BusinessException("文档生成失败,请稍后重试"); } -
扩展建议:
- 添加PDF/A标准支持
- 实现数字签名功能
- 支持加密PDF生成
14. 未来改进方向
基于实际项目反馈,计划在以下方面进行改进:
- 现代CSS支持:评估迁移到OpenHTMLToPDF
- 模板引擎集成:深度整合Thymeleaf/FreeMarker
- 动态图表支持:将ECharts图表转为PDF
- 流式生成:支持超大文档的分块处理
- 云原生适配:优化容器化部署体验
图表支持示例原型:
java复制public String convertChartToHtml(ChartData data) {
// 使用ECharts生成图表
String option = "{...}"; // 根据data生成配置
return "<div id='chart' style='width:600px;height:400px'></div>" +
"<script>var chart = echarts.init(document.getElementById('chart'));" +
"chart.setOption(" + option + ");</script>";
}
15. 资源推荐
15.1 学习资源
15.2 实用工具
-
PDF调试:使用PDFBox分析生成的PDF
java复制PDDocument doc = PDDocument.load(new File("output.pdf")); PDFTextStripper stripper = new PDFTextStripper(); String text = stripper.getText(doc); -
HTML验证:使用W3C Validator检查HTML结构
-
CSS兼容性检查:参考Flying Saucer CSS支持列表
16. 完整工具类代码
以下是整合了所有优化和改进的完整工具类实现:
java复制package com.example.pdf;
import com.google.zxing.*;
import com.google.zxing.client.j2se.MatrixToImageWriter;
import com.google.zxing.common.BitMatrix;
import org.jsoup.Jsoup;
import org.jsoup.safety.Safelist;
import org.xhtmlrenderer.pdf.ITextRenderer;
import java.io.*;
import java.nio.file.Files;
import java.nio.file.Paths;
import java.util.Base64;
import java.util.HashMap;
import java.util.Map;
/**
* 增强版富文本转PDF工具
*/
public class EnhancedPdfGenerator {
private static final int DEFAULT_QR_SIZE = 150;
private static byte[] defaultFont;
static {
try {
// 预加载默认字体
defaultFont = loadDefaultFont();
} catch (IOException e) {
throw new RuntimeException("初始化字体失败", e);
}
}
/**
* 生成PDF文档
*/
public static byte[] generatePdf(PdfRequest request) throws PdfGenerationException {
try {
// 参数校验
validateRequest(request);
// 安全处理HTML
String safeHtml = sanitizeHtml(request.getContent());
// 生成二维码
String qrCode = request.getQrCodeUrl() != null
? generateQrCode(request.getQrCodeUrl(), request.getQrConfig())
: null;
// 构建完整HTML
String fullHtml = buildHtmlDocument(request, safeHtml, qrCode);
// 渲染PDF
return renderToPdf(fullHtml, request.getPdfConfig());
} catch (Exception e) {
throw new PdfGenerationException("PDF生成失败", e);
}
}
private static void validateRequest(PdfRequest request) {
if (request == null) {
throw new IllegalArgumentException("请求参数不能为null");
}
if (request.getContent() == null || request.getContent().trim().isEmpty()) {
throw new IllegalArgumentException("内容不能为空");
}
}
private static String sanitizeHtml(String html) {
Safelist safelist = Safelist.relaxed()
.addTags("h1", "h2", "h3", "h4", "h5", "h6")
.addTags("div", "span", "p", "br", "hr")
.addTags("table", "thead", "tbody", "tr", "th", "td")
.addTags("ul", "ol", "li", "img", "a")
.addAttributes(":all", "style", "class", "id")
.addAttributes("img", "src", "alt", "width", "height")
.addAttributes("a", "href", "target", "rel");
return Jsoup.clean(html, safelist);
}
private static String generateQrCode(String content, QrConfig config) throws WriterException, IOException {
if (config == null) {
config = new QrConfig();
}
Map<EncodeHintType, Object> hints = new HashMap<>();
hints.put(EncodeHintType.ERROR_CORRECTION, config.getErrorCorrection());
hints.put(EncodeHintType.MARGIN, config.getMargin());
hints.put(EncodeHintType.CHARACTER_SET, "UTF-8");
QRCodeWriter writer = new QRCodeWriter();
BitMatrix matrix = writer.encode(content, BarcodeFormat.QR_CODE,
config.getSize(), config.getSize(), hints);
try (ByteArrayOutputStream out = new ByteArrayOutputStream()) {
MatrixToImageWriter.writeToStream(matrix, "PNG", out);
return "data:image/png;base64," + Base64.getEncoder().encodeToString(out.toByteArray());
}
}
private static String buildHtmlDocument(PdfRequest request, String content, String qrCode) {
StringBuilder html = new StringBuilder();
html.append("<!DOCTYPE html><html><head><meta charset=\"UTF-8\"/>");
html.append("<style>").append(buildCss(request)).append("</style>");
html.append("</head><body>");
// 添加二维码
if (qrCode != null) {
html.append("<div class=\"qr-container\">")
.append("<img src=\"").append(qrCode).append("\"/>");
if (request.getQrConfig() != null && request.getQrConfig().getLabel() != null) {
html.append("<div class=\"qr-label\">")
.append(request.getQrConfig().getLabel())
.append("</div>");
}
html.append("</div>");
}
// 添加标题
if (request.getTitle() != null && !request.getTitle().isEmpty()) {
html.append("<h1 class=\"doc-title\">")
.append(escapeHtml(request.getTitle()))
.append("</h1>");
}
// 添加内容
html.append("<div class=\"doc-content\">").append(content).append("</div>");
html.append("</body></html>");
return html.toString();
}
private static String buildCss(PdfRequest request) {
PdfConfig pdfConfig = request.getPdfConfig() != null
? request.getPdfConfig()
: new PdfConfig();
return "@page { size: A4; margin: " + pdfConfig.getMargin() + "; }" +
"body { font-family: SimSun, serif; font-size: " + pdfConfig.getFontSize() + "pt; }" +
".doc-title { text-align: center; font-size: " + pdfConfig.getTitleSize() + "pt; " +
"color: " + pdfConfig.getTitleColor() + "; margin-bottom: 20pt; }" +
".qr-container { position: absolute; top: 10mm; right: 10mm; text-align: center; }" +
".qr-label { font-size: 8pt; margin-top: 2mm; }" +
".doc-content p { text-indent: 2em; margin: 0.5em 0; }" +
".doc-content table { border-collapse: collapse; width: 100%; margin: 1em 0; }" +
".doc-content th, .doc-content td { border: 1px solid #ddd; padding: 4pt; }" +
".doc-content th { background-color: #f5f5f5; }";
}
private static byte[] renderToPdf(String html, PdfConfig config) throws IOException {
try (ByteArrayOutputStream out = new ByteArrayOutputStream()) {
ITextRenderer renderer = new ITextRenderer();
// 设置字体
if (config != null && config.getCustomFont() != null) {
renderer.getFontResolver().addFont(
config.getCustomFont(),
"Identity-H",
config.isEmbedFont());
} else {
renderer.getFontResolver().addFont(
defaultFont,
"Identity-H",
false);
}
// 设置DPI
if (config != null && config.getDpi() > 0) {
renderer.setDPI(config.getDpi());
}
renderer.setDocumentFromString(html);
renderer.layout();
renderer.createPDF(out);
return out.toByteArray();
}
}
private static byte[] loadDefaultFont() throws IOException {
// 尝试从常见位置加载字体
String[] fontPaths = {
"/fonts/simsun.ttc", // 打包在jar中的字体
"C:/Windows/Fonts/simsun.ttc", // Windows
"/usr/share/fonts/wqy-microhei.ttc" // Linux
};
for (String path : fontPaths) {
try {
if (path.startsWith("/")) {
// 从classpath加载
try (InputStream is = EnhancedPdfGenerator.class.getResourceAsStream(path)) {
if (is != null) {
ByteArrayOutputStream buffer = new ByteArrayOutputStream();
byte[] data = new byte[16384];
int nRead;
while ((nRead = is.read(data, 0, data.length)) != -1) {
buffer.write(data, 0, nRead);
}
return buffer.toByteArray();
}
}
} else {
// 从文件系统加载
if (Files.exists(Paths.get(path))) {
return Files.readAllBytes(Paths.get(path));
}
}
} catch (IOException ignore) {
// 继续尝试下一个路径
}
}
throw new IOException("无法加载默认字体,请确认字体文件存在");
}
private static String escapeHtml(String text) {
if (text == null) return "";
return text.replace("&", "&")
.replace("<", "<")
.replace(">", ">")
.replace("\"", """)
.replace("'", "'");
}
// 配置类定义
public static class PdfRequest {
private String title;
private String content;
private String qrCodeUrl;
private QrConfig qrConfig;
private PdfConfig pdfConfig;
// getters and setters
}
public static class PdfConfig {
private int titleSize = 18;
private String titleColor = "#333333";
private int fontSize = 12;
private String margin = "20mm 15mm";
private int dpi = 96;
private byte[] customFont;
private boolean embedFont = false;
// getters and setters
}
public static class QrConfig {
private int size = DEFAULT_QR_SIZE;
private String label;
private ErrorCorrectionLevel errorCorrection = ErrorCorrectionLevel.H;
private int margin = 1;
// getters and setters
}
public static class PdfGenerationException extends Exception {
public PdfGenerationException(String message, Throwable cause) {
super(message, cause);
}
}
}
这个增强版工具类提供了:
1.