前言
好久没来总结项目经验了,今天刚好有时间总结一下。
最近有一个项目需求实现pdf文件导出,参考了一些网上的方案,真是五花八门,大部分都是直说了丢了一半;在导出pdf文件最重要的是字体文件的问题及在Linux服务器上无法显示中字体问题。好了!废话不多说上代码。
引入依赖包
pom
<!-- iText 7 PDF 核心 (兼容 Spring Boot 3) -->
<dependency>
<groupId>com.itextpdf</groupId>
<artifactId>itext7-core</artifactId>
<version>8.0.4</version>
<type>pom</type>
</dependency>
<!-- iText 7 html2pdf (HTML 转 PDF) -->
<dependency>
<groupId>com.itextpdf</groupId>
<artifactId>html2pdf</artifactId>
<version>5.0.4</version>
</dependency>
<!-- iText 7 亚洲字体支持 (中文等) -->
<dependency>
<groupId>com.itextpdf</groupId>
<artifactId>font-asian</artifactId>
<version>8.0.4</version>
</dependency>
Application.yaml配置文件
yaml
ac:
fonts:
simsun-path: /usr/share/fonts/chinese/ttf/simsun.ttf
msyh-path: /usr/share/fonts/chinese/ttf/msyh.ttf
spring:
### freemarker
freemarker:
suffix: .ftl
charset: UTF-8
request-context-attribute: request
settings:
number_format: 0.##########
template-loader-path: classpath:/templates/
封装工具类
Java
import com.itextpdf.html2pdf.ConverterProperties;
import com.itextpdf.html2pdf.HtmlConverter;
import com.itextpdf.html2pdf.resolver.font.DefaultFontProvider;
import com.itextpdf.kernel.font.PdfFont;
import com.itextpdf.kernel.font.PdfFontFactory;
import com.itextpdf.kernel.pdf.PdfDocument;
import com.itextpdf.kernel.pdf.PdfWriter;
import freemarker.template.Configuration;
import freemarker.template.Template;
import jakarta.servlet.ServletOutputStream;
import jakarta.servlet.http.HttpServletResponse;
import lombok.SneakyThrows;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.core.io.ClassPathResource;
import org.springframework.core.io.Resource;
import org.springframework.stereotype.Component;
import java.io.File;
import java.io.FileOutputStream;
import java.io.InputStream;
import java.io.StringWriter;
import java.util.Map;
@Slf4j
@Component
public class PdfExportUtil {
private final Configuration freemarkerConfiguration;
public PdfExportUtil(Configuration freemarkerConfiguration) {
this.freemarkerConfiguration = freemarkerConfiguration;
}
@Value("${ac.fonts.simsun-path:}")
private String songTi;
@Value("${ac.fonts.msyh-path:}")
private String yaHei;
@SneakyThrows
public String renderHtml(String templateName, Map<String, Object> dataMap) {
Template template = freemarkerConfiguration.getTemplate(templateName, "UTF-8");
StringWriter writer = new StringWriter();
template.process(dataMap, writer);
return writer.toString();
}
@SneakyThrows
public void htmlToPdf(String html, HttpServletResponse response, String fileName) {
response.setContentType("application/pdf;charset=UTF-8");
response.setHeader("Content-Disposition",
"attachment;filename=" + java.net.URLEncoder.encode(fileName, "UTF-8"));
ServletOutputStream outputStream = response.getOutputStream();
PdfWriter writer = new PdfWriter(outputStream);
PdfDocument pdf = new PdfDocument(writer);
ConverterProperties properties = new ConverterProperties();
// 创建字体提供者,启用系统字体扫描作为基础
DefaultFontProvider fontProvider = new DefaultFontProvider(true, true, true);
log.info("已启用系统字体扫描作为基础");
log.info("开始加载字体配置...");
log.info("宋体配置路径:{}", songTi);
log.info("微软雅黑配置路径:{}", yaHei);
try {
// 加载宋体
if (songTi != null && !songTi.isEmpty()) {
String simsunPath = loadFontFromClasspath(songTi, "SimSun");
if (simsunPath == null) {
File simsunFile = new File(songTi);
if (simsunFile.exists()) {
simsunPath = songTi;
}
}
if (simsunPath != null) {
File simsunFile = new File(simsunPath);
if (simsunFile.exists()) {
try {
// 使用 PdfFontFactory 创建字体(支持 TTC 格式)
// iText 8 API: createFont(String fontProgram, String encoding, boolean embedded)
PdfFont simsunFont = PdfFontFactory.createFont(
simsunPath,
com.itextpdf.io.font.PdfEncodings.IDENTITY_H
);
fontProvider.addFont(simsunFont.getFontProgram(), com.itextpdf.io.font.PdfEncodings.IDENTITY_H);
log.info("✓ 注册宋体字体,路径:{}, 文件大小:{} bytes", simsunPath, simsunFile.length());
} catch (Exception e) {
log.warn("注册宋体字体失败:{}", e.getMessage());
}
} else {
log.warn("✗ 宋体字体文件不存在:{}", simsunPath);
}
}
}
// 加载微软雅黑
if (yaHei != null && !yaHei.isEmpty()) {
String msyhPath = loadFontFromClasspath(yaHei, "MicrosoftYaHei");
if (msyhPath == null) {
File msyhFile = new File(yaHei);
if (msyhFile.exists()) {
msyhPath = yaHei;
}
}
if (msyhPath != null) {
File msyhFile = new File(msyhPath);
if (msyhFile.exists()) {
try {
// 使用 PdfFontFactory 创建字体(支持 TTC 格式)
PdfFont msyhFont = PdfFontFactory.createFont(
msyhPath,
com.itextpdf.io.font.PdfEncodings.IDENTITY_H
);
fontProvider.addFont(msyhFont.getFontProgram(), com.itextpdf.io.font.PdfEncodings.IDENTITY_H);
log.info("✓ 注册微软雅黑字体,路径:{}, 文件大小:{} bytes", msyhPath, msyhFile.length());
} catch (Exception e) {
log.warn("注册微软雅黑字体失败:{}", e.getMessage());
}
} else {
log.warn("✗ 微软雅黑字体文件不存在:{}", msyhPath);
}
}
}
log.info("字体加载完成");
} catch (Exception e) {
log.error("字体加载过程中发生异常,继续使用系统字体:{}", e.getMessage(), e);
}
properties.setFontProvider(fontProvider);
properties.setBaseUri("classpath:/static/");
log.info("开始转换 HTML 为 PDF...");
HtmlConverter.convertToPdf(html, pdf, properties);
log.info("PDF 转换完成");
pdf.close();
outputStream.flush();
outputStream.close();
}
/**
* 将 HTML 转换为 PDF 字节数组
*
* @param html HTML 内容
* @return PDF 文件字节数组
*/
@SneakyThrows
public byte[] htmlToPdfBytes(String html) {
java.io.ByteArrayOutputStream baos = new java.io.ByteArrayOutputStream();
PdfWriter writer = new PdfWriter(baos);
PdfDocument pdf = new PdfDocument(writer);
ConverterProperties properties = new ConverterProperties();
// 创建字体提供者,启用系统字体扫描作为基础
DefaultFontProvider fontProvider = new DefaultFontProvider(true, true, true);
try {
// 加载宋体
if (songTi != null && !songTi.isEmpty()) {
String simsunPath = loadFontFromClasspath(songTi, "SimSun");
if (simsunPath == null) {
File simsunFile = new File(songTi);
if (simsunFile.exists()) {
simsunPath = songTi;
}
}
if (simsunPath != null) {
File simsunFile = new File(simsunPath);
if (simsunFile.exists()) {
try {
// 使用 PdfFontFactory 创建字体(支持 TTC 格式)
PdfFont simsunFont = PdfFontFactory.createFont(
simsunPath,
com.itextpdf.io.font.PdfEncodings.IDENTITY_H
);
fontProvider.addFont(simsunFont.getFontProgram(), com.itextpdf.io.font.PdfEncodings.IDENTITY_H);
} catch (Exception e) {
log.warn("注册宋体字体失败:{}", e.getMessage());
}
}
}
}
// 加载微软雅黑
if (yaHei != null && !yaHei.isEmpty()) {
String msyhPath = loadFontFromClasspath(yaHei, "MicrosoftYaHei");
if (msyhPath == null) {
File msyhFile = new File(yaHei);
if (msyhFile.exists()) {
msyhPath = yaHei;
}
}
if (msyhPath != null) {
File msyhFile = new File(msyhPath);
if (msyhFile.exists()) {
try {
// 使用 PdfFontFactory 创建字体(支持 TTC 格式)
PdfFont msyhFont = PdfFontFactory.createFont(
msyhPath,
com.itextpdf.io.font.PdfEncodings.IDENTITY_H
);
fontProvider.addFont(msyhFont.getFontProgram(), com.itextpdf.io.font.PdfEncodings.IDENTITY_H);
} catch (Exception e) {
log.warn("注册微软雅黑字体失败:{}", e.getMessage());
}
}
}
}
} catch (Exception e) {
log.error("字体加载过程中发生异常,继续使用系统字体:{}", e.getMessage(), e);
}
properties.setFontProvider(fontProvider);
properties.setBaseUri("classpath:/static/");
HtmlConverter.convertToPdf(html, pdf, properties);
pdf.close();
return baos.toByteArray();
}
private String loadFontFromClasspath(String fontPath, String fontName) throws Exception {
String resourcePath = fontPath.startsWith("/") ? fontPath.substring(1) : fontPath;
if (resourcePath.startsWith("classpath:")) {
resourcePath = resourcePath.substring(10);
}
Resource resource = new ClassPathResource(resourcePath);
if (!resource.exists()) {
log.debug("字体资源不存在:{}", fontPath);
return null;
}
File tempDir = new File(System.getProperty("java.io.tmpdir"), "ac-fonts");
if (!tempDir.exists()) {
tempDir.mkdirs();
}
// 根据文件扩展名确定临时文件名
String fileName = resource.getFilename();
String extension = ".ttf"; // 默认扩展名
if (fileName != null && fileName.contains(".")) {
extension = fileName.substring(fileName.lastIndexOf("."));
}
File tempFontFile = new File(tempDir, fontName + extension);
if (tempFontFile.exists() && tempFontFile.length() > 0) {
log.debug("使用缓存的字体文件:{}", tempFontFile.getAbsolutePath());
return tempFontFile.getAbsolutePath();
}
try (InputStream inputStream = resource.getInputStream();
FileOutputStream outputStream = new FileOutputStream(tempFontFile)) {
byte[] buffer = new byte[8192];
int bytesRead;
while ((bytesRead = inputStream.read(buffer)) != -1) {
outputStream.write(buffer, 0, bytesRead);
}
}
log.info("字体文件已解压到临时目录:{}", tempFontFile.getAbsolutePath());
return tempFontFile.getAbsolutePath();
}
}
工具类有些冗余,可自行优化调整,这里是为了方面测试是否有成功读取到字体文件。
应用
java
@Slf4j
@RestController
public class DownloadCenterController {
@Autowired
private PdfExportUtil pdfExportUtil;
public void test(HttpServletResponse response) {
// 1. 构造模板数据
Map<String, Object> data = new HashMap<>();
// 2. 渲染模板生成 HTML
String renderHtml = pdfExportUtil.renderHtml("ftl/nvt-template.ftl", data);
// 3. 输出 PDF
String fileName = "test.pdf";
pdfExportUtil.htmlToPdf(renderHtml, response, fileName);
}
}
到此,本地idea跑项目是没有问题了。
注意!注意!注意! 重要的事情说三遍!
打包发布生产环境运行的问题。在发布一定要确保服务器系统上一定要有对应的字体文件,字体文件后缀问题,一定要使用TTF的文件后缀,如果使用TTC后缀会出现没有中文字体(这里也是我是使用ttc文件始终无法出现中文字体的原因所在)。
Linux上安装字体
这里主要安装宋体、微软雅黑
操作系统Ubuntu
首先,我们在windows系统找对应字段,C:\Windows\Fonts;找TTF文件,如果没有将TTC文件拷贝服务器,在服务器中从TTC文件提取TTF文件,操作如下:
bash
# 1. 安装字体提取工具
apt-get update && apt-get install -y fontforge
# 2. 创建输出目录
mkdir -p /usr/share/fonts/chinese/ttf
# 3. 从 simsun.ttc 中提取宋体
fontforge -lang=ff -c 'Open("/usr/share/fonts/chinese/simsun.ttc"); Generate("/usr/share/fonts/chinese/ttf/simsun.ttf")'
# 4. 从 msyh.ttc 中提取微软雅黑
fontforge -lang=ff -c 'Open("/usr/share/fonts/chinese/msyh.ttc"); Generate("/usr/share/fonts/chinese/ttf/msyh.ttf")'
# 5. 设置权限
chmod 644 /usr/share/fonts/chinese/ttf/*.ttf
# 6. 刷新字体缓存
fc-cache -fv
# 7. 验证新字体
fc-list | grep -E "simsun\.ttf|msyh\.ttf"
在 CentOS/RHEL 系统使用 yum 工具安装。
将文件拷贝到Docker容器内
方式一: Dockerfile
dockerfile
FROM your-base-image
# 在Dockerfile文件所在目录下
COPY fonts/simsun.ttf /usr/share/fonts/chinese/
COPY fonts/msyh.ttf /usr/share/fonts/chinese/
RUN fc-cache -fv
方式二:docker-compose.yaml
在配置挂在数据卷的volumes映射到容器内。
yml
services:
api:
volumes:
- /usr/share/fonts:/usr/share/fonts
映射路径根据你的实际使用做配置。