1.1.44 渲染word模板并输出PDF、添加字体

dev
yinq 1 month ago
parent 3661c5afe8
commit d4e080e83c

@ -60,6 +60,8 @@
<mybatis-pj.version>1.5.4</mybatis-pj.version> <mybatis-pj.version>1.5.4</mybatis-pj.version>
<!-- Word 模板导出 --> <!-- Word 模板导出 -->
<poi-tl.version>1.12.2</poi-tl.version> <poi-tl.version>1.12.2</poi-tl.version>
<!-- Word(docx) 转 PDF -->
<xdocreport.poi.version>2.0.4</xdocreport.poi.version>
<!-- 插件版本 --> <!-- 插件版本 -->
<maven-compiler-plugin.version>3.14.0</maven-compiler-plugin.version> <maven-compiler-plugin.version>3.14.0</maven-compiler-plugin.version>
@ -373,6 +375,23 @@
<version>${warm-flow.version}</version> <version>${warm-flow.version}</version>
</dependency> </dependency>
<!-- Word 模板导出poi-tl及 Word 转 PDF -->
<dependency>
<groupId>com.deepoove</groupId>
<artifactId>poi-tl</artifactId>
<version>${poi-tl.version}</version>
</dependency>
<dependency>
<groupId>fr.opensagres.xdocreport</groupId>
<artifactId>fr.opensagres.poi.xwpf.converter.pdf</artifactId>
<version>${xdocreport.poi.version}</version>
</dependency>
<dependency>
<groupId>fr.opensagres.xdocreport</groupId>
<artifactId>fr.opensagres.xdocreport.itext.extension</artifactId>
<version>${xdocreport.poi.version}</version>
</dependency>
</dependencies> </dependencies>
</dependencyManagement> </dependencyManagement>

@ -6,7 +6,7 @@
<parent> <parent>
<groupId>org.dromara</groupId> <groupId>org.dromara</groupId>
<artifactId>ruoyi-common</artifactId> <artifactId>ruoyi-common</artifactId>
<version>2.5.0</version> <version>${revision}</version>
</parent> </parent>
<artifactId>hwbm-common-workflow</artifactId> <artifactId>hwbm-common-workflow</artifactId>

@ -19,14 +19,21 @@
<dependency> <dependency>
<groupId>com.deepoove</groupId> <groupId>com.deepoove</groupId>
<artifactId>poi-tl</artifactId> <artifactId>poi-tl</artifactId>
<version>${poi-tl.version}</version>
</dependency> </dependency>
<!-- POI 依赖由 poi-tl 传递引入,如需锁定版本可在顶层 dependencyManagement 中统一管理 --> <!-- POI 依赖由 poi-tl 传递引入 -->
<!-- 公共核心工具依赖 --> <!-- 公共核心工具依赖 -->
<dependency> <dependency>
<groupId>org.dromara</groupId> <groupId>org.dromara</groupId>
<artifactId>ruoyi-common-core</artifactId> <artifactId>ruoyi-common-core</artifactId>
<version>${revision}</version> </dependency>
<!-- Word 转 PDF -->
<dependency>
<groupId>fr.opensagres.xdocreport</groupId>
<artifactId>fr.opensagres.poi.xwpf.converter.pdf</artifactId>
</dependency>
<dependency>
<groupId>fr.opensagres.xdocreport</groupId>
<artifactId>fr.opensagres.xdocreport.itext.extension</artifactId>
</dependency> </dependency>
</dependencies> </dependencies>

@ -0,0 +1,131 @@
package org.dromara.common.word.pdf;
import com.lowagie.text.Font;
import com.lowagie.text.pdf.BaseFont;
import fr.opensagres.poi.xwpf.converter.pdf.PdfOptions;
import fr.opensagres.xdocreport.itext.extension.font.IFontProvider;
import fr.opensagres.xdocreport.itext.extension.font.ITextFontRegistry;
import lombok.AccessLevel;
import lombok.NoArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import java.awt.Color;
import java.io.InputStream;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
/**
* Word PDF +
* <p>
* PDF 线
*/
@Slf4j
@NoArgsConstructor(access = AccessLevel.PRIVATE)
public final class WordPdfOptionsFactory {
private static final String FONT_CACHE_KEY = "zh-cn";
private static final Map<String, BaseFont> BASE_FONT_CACHE = new ConcurrentHashMap<>();
/**
* PDF
*/
public static PdfOptions createChinesePdfOptions() {
PdfOptions options = PdfOptions.create();
options.fontEncoding(BaseFont.IDENTITY_H);
options.fontProvider(chineseFontProvider());
return options;
}
private static IFontProvider chineseFontProvider() {
return (familyName, encoding, size, style, color) -> {
try {
BaseFont baseFont = resolveChineseBaseFont();
Font font = new Font(baseFont, size, style, color == null ? Color.BLACK : color);
if (familyName != null) {
font.setFamily(familyName);
}
return font;
} catch (Exception e) {
log.warn("加载中文字体失败,使用 PDF 默认字体, familyName={}", familyName, e);
return ITextFontRegistry.getRegistry().getFont(familyName, encoding, size, style, color);
}
};
}
private static BaseFont resolveChineseBaseFont() throws Exception {
BaseFont cached = BASE_FONT_CACHE.get(FONT_CACHE_KEY);
if (cached != null) {
return cached;
}
synchronized (WordPdfOptionsFactory.class) {
cached = BASE_FONT_CACHE.get(FONT_CACHE_KEY);
if (cached != null) {
return cached;
}
BaseFont loaded = loadChineseBaseFont();
BASE_FONT_CACHE.put(FONT_CACHE_KEY, loaded);
log.info("PDF 导出已加载中文字体: {}", loaded.getPostscriptFontName());
return loaded;
}
}
private static BaseFont loadChineseBaseFont() throws Exception {
// 1) classpath 内置字体fonts/simsun.ttc、msyh.ttc 或 .ttf
BaseFont classpathFont = loadClasspathFont("fonts/simsun.ttf", "simsun.ttf");
if (classpathFont != null) {
return classpathFont;
}
classpathFont = loadClasspathFont("fonts/simsun.ttc", "simsun.ttc,0");
if (classpathFont != null) {
return classpathFont;
}
classpathFont = loadClasspathFont("fonts/msyh.ttf", "msyh.ttf");
if (classpathFont != null) {
return classpathFont;
}
classpathFont = loadClasspathFont("fonts/msyh.ttc", "msyh.ttc,0");
if (classpathFont != null) {
return classpathFont;
}
// 2) 操作系统字体(开发机 Windows / Linux 常见路径)
List<String> candidates = new ArrayList<>();
String os = System.getProperty("os.name", "").toLowerCase();
if (os.contains("win")) {
candidates.add("C:/Windows/Fonts/simsun.ttc,0");
candidates.add("C:/Windows/Fonts/msyh.ttc,0");
candidates.add("C:/Windows/Fonts/simhei.ttf");
candidates.add("C:/Windows/Fonts/arialuni.ttf");
} else {
candidates.add("/usr/share/fonts/truetype/wqy/wqy-microhei.ttc,0");
candidates.add("/usr/share/fonts/opentype/noto/NotoSansCJK-Regular.ttc,0");
candidates.add("/usr/share/fonts/truetype/arphic/uming.ttc,0");
}
for (String fontPath : candidates) {
String filePart = fontPath.contains(",") ? fontPath.substring(0, fontPath.indexOf(',')) : fontPath;
if (Files.isRegularFile(Path.of(filePart))) {
return BaseFont.createFont(fontPath, BaseFont.IDENTITY_H, BaseFont.EMBEDDED);
}
}
throw new IllegalStateException(
"未找到可用的中文字体,请在 ruoyi-common-word/src/main/resources/fonts/ 下放置 simsun.ttc/msyh.ttc或 .ttf"
+ "或在服务器安装中文字体");
}
private static BaseFont loadClasspathFont(String resourcePath, String fontNameForCreate) throws Exception {
try (InputStream is = Thread.currentThread().getContextClassLoader().getResourceAsStream(resourcePath)) {
if (is == null) {
return null;
}
return BaseFont.createFont(fontNameForCreate, BaseFont.IDENTITY_H, BaseFont.EMBEDDED, true,
is.readAllBytes(), null);
}
}
}

@ -2,6 +2,9 @@ package org.dromara.common.word.util;
import com.deepoove.poi.XWPFTemplate; import com.deepoove.poi.XWPFTemplate;
import com.deepoove.poi.config.Configure; import com.deepoove.poi.config.Configure;
import fr.opensagres.poi.xwpf.converter.pdf.PdfConverter;
import fr.opensagres.poi.xwpf.converter.pdf.PdfOptions;
import org.dromara.common.word.pdf.WordPdfOptionsFactory;
import jakarta.servlet.http.HttpServletResponse; import jakarta.servlet.http.HttpServletResponse;
import lombok.AccessLevel; import lombok.AccessLevel;
import lombok.NoArgsConstructor; import lombok.NoArgsConstructor;
@ -227,6 +230,53 @@ public final class WordTemplateUtil {
return bos.toByteArray(); return bos.toByteArray();
} }
/**
* PDF HttpServletResponse Word
*
* @param templatePath classpath
* @param fileName .pdf
* @param data
* @param response HttpServletResponse
*/
public static void renderToPdfResponse(String templatePath, String fileName, Map<String, Object> data,
HttpServletResponse response) {
renderToPdfResponse(templatePath, fileName, data, null, response);
}
/**
* PDF HttpServletResponse Configure
*/
public static void renderToPdfResponse(String templatePath, String fileName, Map<String, Object> data,
Configure config, HttpServletResponse response) {
Objects.requireNonNull(response, "HttpServletResponse must not be null");
if (StringUtils.isBlank(templatePath)) {
throw new ServiceException("Word 模板路径不能为空");
}
String safeFileName = StringUtils.isBlank(fileName) ? "export" : fileName.trim();
Map<String, Object> safeData = (data == null ? Collections.emptyMap() : data);
log.info("Word模板导出PDF开始, templatePath={}, fileName={}", templatePath, safeFileName);
try (XWPFTemplate template = config == null
? buildTemplate(templatePath, safeData)
: buildTemplate(templatePath, safeData, config);
OutputStream os = resetPdfResponse(safeFileName, response)) {
PdfOptions options = WordPdfOptionsFactory.createChinesePdfOptions();
PdfConverter.getInstance().convert(template.getXWPFDocument(), os, options);
os.flush();
log.info("Word模板导出PDF成功, fileName={}", safeFileName);
} catch (Exception e) {
log.error("Word 模板导出 PDF 失败, templatePath={}, fileName={}", templatePath, safeFileName, e);
throw new ServiceException("Word 模板导出 PDF 失败,请联系管理员").setDetailMessage(e.getMessage());
}
}
private static OutputStream resetPdfResponse(String fileName, HttpServletResponse response) throws IOException {
String realName = fileName.endsWith(".pdf") ? fileName : fileName + ".pdf";
FileUtils.setAttachmentResponseHeader(response, realName);
response.setContentType("application/pdf");
return response.getOutputStream();
}
// ===================================================================== // =====================================================================
// 典型调用示例(仅作为文档说明,不参与编译): // 典型调用示例(仅作为文档说明,不参与编译):
// ===================================================================== // =====================================================================

Loading…
Cancel
Save