Fix PDF font loading for project overview reports
This commit is contained in:
@@ -14,9 +14,11 @@ import com.ruoyi.common.exception.ServiceException;
|
||||
import com.ruoyi.common.utils.file.FileUtils;
|
||||
import jakarta.servlet.http.HttpServletResponse;
|
||||
import java.awt.Color;
|
||||
import java.io.Closeable;
|
||||
import java.io.File;
|
||||
import java.io.IOException;
|
||||
import java.math.BigDecimal;
|
||||
import java.nio.file.Files;
|
||||
import java.text.DecimalFormat;
|
||||
import java.time.LocalDateTime;
|
||||
import java.time.format.DateTimeFormatter;
|
||||
@@ -32,6 +34,7 @@ import org.apache.pdfbox.pdmodel.PDPage;
|
||||
import org.apache.pdfbox.pdmodel.PDPageContentStream;
|
||||
import org.apache.pdfbox.pdmodel.common.PDRectangle;
|
||||
import org.apache.pdfbox.pdmodel.font.PDType0Font;
|
||||
import org.springframework.beans.factory.annotation.Value;
|
||||
import org.springframework.stereotype.Component;
|
||||
|
||||
/**
|
||||
@@ -43,6 +46,11 @@ public class CcdiProjectOverviewReportPdfExporter {
|
||||
private static final String CONTENT_TYPE = "application/pdf";
|
||||
private static final DateTimeFormatter EXPORT_TIME_FORMATTER = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm");
|
||||
private static final DecimalFormat MONEY_FORMAT = new DecimalFormat("#,##0.00");
|
||||
private final String pdfFontPath;
|
||||
|
||||
public CcdiProjectOverviewReportPdfExporter(@Value("${ccdi.report.pdf-font-path:}") String pdfFontPath) {
|
||||
this.pdfFontPath = pdfFontPath;
|
||||
}
|
||||
|
||||
public void export(HttpServletResponse response, CcdiProjectOverviewReportVO report) throws IOException {
|
||||
response.setContentType(CONTENT_TYPE);
|
||||
@@ -51,9 +59,8 @@ public class CcdiProjectOverviewReportPdfExporter {
|
||||
safeFileName(report.getProject().getProjectName()) + "_初核结果报告.pdf"
|
||||
);
|
||||
|
||||
try (PDDocument document = new PDDocument()) {
|
||||
PDType0Font font = loadChineseFont(document);
|
||||
PdfPageWriter writer = new PdfPageWriter(document, font);
|
||||
try (PDDocument document = new PDDocument(); LoadedChineseFont loadedFont = loadChineseFont(document)) {
|
||||
PdfPageWriter writer = new PdfPageWriter(document, loadedFont.font());
|
||||
writer.newPage();
|
||||
writeCover(writer, report);
|
||||
writeUploadSubjects(writer, report.getUploadSubjects());
|
||||
@@ -204,44 +211,113 @@ public class CcdiProjectOverviewReportPdfExporter {
|
||||
);
|
||||
}
|
||||
|
||||
private PDType0Font loadChineseFont(PDDocument document) throws IOException {
|
||||
List<String> candidates = List.of(
|
||||
"C:/Windows/Fonts/NotoSansSC-VF.ttf",
|
||||
"C:/Windows/Fonts/simhei.ttf",
|
||||
"C:/Windows/Fonts/simsunb.ttf",
|
||||
"/usr/share/fonts/truetype/noto/NotoSansCJK-Regular.ttf",
|
||||
"/usr/share/fonts/opentype/noto/NotoSansCJK-Regular.ttf",
|
||||
"/usr/share/fonts/truetype/wqy/wqy-microhei.ttc"
|
||||
);
|
||||
for (String path : candidates) {
|
||||
File file = new File(path);
|
||||
if (!file.exists() || !file.isFile()) {
|
||||
continue;
|
||||
}
|
||||
String lowerPath = path.toLowerCase();
|
||||
if (lowerPath.endsWith(".ttf")) {
|
||||
return PDType0Font.load(document, file);
|
||||
}
|
||||
if (lowerPath.endsWith(".ttc")) {
|
||||
PDType0Font font = loadFirstCollectionFont(document, file);
|
||||
if (font != null) {
|
||||
return font;
|
||||
}
|
||||
}
|
||||
private LoadedChineseFont loadChineseFont(PDDocument document) throws IOException {
|
||||
if (pdfFontPath == null || pdfFontPath.isBlank()) {
|
||||
throw new ServiceException("未配置PDF中文字体路径,无法导出PDF报告");
|
||||
}
|
||||
throw new ServiceException("未找到可用中文字体,无法导出PDF报告");
|
||||
String path = pdfFontPath.trim();
|
||||
File file = new File(path);
|
||||
if (!file.exists() || !file.isFile()) {
|
||||
throw new ServiceException("配置的PDF中文字体路径不可用:" + path);
|
||||
}
|
||||
|
||||
String lowerPath = path.toLowerCase();
|
||||
if (lowerPath.endsWith(".ttf")) {
|
||||
return new LoadedChineseFont(PDType0Font.load(document, file), null);
|
||||
}
|
||||
if (lowerPath.endsWith(".ttc")) {
|
||||
LoadedChineseFont loadedFont = loadPreferredCollectionFont(document, file);
|
||||
if (loadedFont != null) {
|
||||
return loadedFont;
|
||||
}
|
||||
throw new ServiceException("配置的PDF中文字体不支持导出:" + path);
|
||||
}
|
||||
if (lowerPath.endsWith(".otf")) {
|
||||
return new LoadedChineseFont(loadOpenTypeFont(document, file), null);
|
||||
}
|
||||
|
||||
throw new ServiceException("配置的PDF中文字体格式不支持:" + path);
|
||||
}
|
||||
|
||||
private PDType0Font loadFirstCollectionFont(PDDocument document, File file) throws IOException {
|
||||
private PDType0Font loadOpenTypeFont(PDDocument document, File file) throws IOException {
|
||||
try (var inputStream = Files.newInputStream(file.toPath())) {
|
||||
return PDType0Font.load(document, inputStream, false);
|
||||
}
|
||||
}
|
||||
|
||||
private LoadedChineseFont loadPreferredCollectionFont(PDDocument document, File file) throws IOException {
|
||||
LoadedChineseFont font = loadCollectionFontByName(document, file);
|
||||
return font == null ? loadFirstCollectionFont(document, file) : font;
|
||||
}
|
||||
|
||||
private LoadedChineseFont loadCollectionFontByName(PDDocument document, File file) throws IOException {
|
||||
TrueTypeCollection collection = new TrueTypeCollection(file);
|
||||
AtomicReference<PDType0Font> font = new AtomicReference<>();
|
||||
try (TrueTypeCollection collection = new TrueTypeCollection(file)) {
|
||||
try {
|
||||
collection.processAllFonts(typeFont -> {
|
||||
if (font.get() == null) {
|
||||
if (font.get() == null && supportsTrueTypeSubset(typeFont)
|
||||
&& isSimplifiedChineseFont(typeFont.getName())) {
|
||||
font.set(PDType0Font.load(document, typeFont, true));
|
||||
}
|
||||
});
|
||||
if (font.get() != null) {
|
||||
return new LoadedChineseFont(font.get(), collection);
|
||||
}
|
||||
} finally {
|
||||
if (font.get() == null) {
|
||||
collection.close();
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
private boolean isSimplifiedChineseFont(String fontName) {
|
||||
if (fontName == null) {
|
||||
return false;
|
||||
}
|
||||
String lowerName = fontName.toLowerCase();
|
||||
return lowerName.contains("sc")
|
||||
|| lowerName.contains("hans")
|
||||
|| lowerName.contains("gb")
|
||||
|| lowerName.contains("hei")
|
||||
|| lowerName.contains("song")
|
||||
|| lowerName.contains("yahei")
|
||||
|| lowerName.contains("simsun")
|
||||
|| lowerName.contains("simhei");
|
||||
}
|
||||
|
||||
private LoadedChineseFont loadFirstCollectionFont(PDDocument document, File file) throws IOException {
|
||||
TrueTypeCollection collection = new TrueTypeCollection(file);
|
||||
AtomicReference<PDType0Font> font = new AtomicReference<>();
|
||||
try {
|
||||
collection.processAllFonts(typeFont -> {
|
||||
if (font.get() == null && supportsTrueTypeSubset(typeFont)) {
|
||||
font.set(PDType0Font.load(document, typeFont, true));
|
||||
}
|
||||
});
|
||||
if (font.get() != null) {
|
||||
return new LoadedChineseFont(font.get(), collection);
|
||||
}
|
||||
} finally {
|
||||
if (font.get() == null) {
|
||||
collection.close();
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
private boolean supportsTrueTypeSubset(org.apache.fontbox.ttf.TrueTypeFont typeFont) throws IOException {
|
||||
return typeFont.getTableMap().containsKey("glyf");
|
||||
}
|
||||
|
||||
private record LoadedChineseFont(PDType0Font font, Closeable fontSource) implements Closeable {
|
||||
|
||||
@Override
|
||||
public void close() throws IOException {
|
||||
if (fontSource != null) {
|
||||
fontSource.close();
|
||||
}
|
||||
}
|
||||
return font.get();
|
||||
}
|
||||
|
||||
private List<IndexedUploadSubject> indexedRows(List<CcdiProjectOverviewReportUploadSubjectVO> rows) {
|
||||
|
||||
@@ -12,6 +12,8 @@ import com.ruoyi.ccdi.project.domain.vo.CcdiProjectOverviewDashboardVO;
|
||||
import com.ruoyi.ccdi.project.domain.vo.CcdiProjectOverviewStatVO;
|
||||
import com.ruoyi.ccdi.project.domain.vo.CcdiProjectRiskHitTagVO;
|
||||
import com.ruoyi.ccdi.project.domain.vo.CcdiProjectRiskModelPeopleItemVO;
|
||||
import com.ruoyi.common.exception.ServiceException;
|
||||
import java.io.File;
|
||||
import java.math.BigDecimal;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.util.List;
|
||||
@@ -19,13 +21,14 @@ import org.junit.jupiter.api.Test;
|
||||
import org.springframework.mock.web.MockHttpServletResponse;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.assertEquals;
|
||||
import static org.junit.jupiter.api.Assertions.assertThrows;
|
||||
import static org.junit.jupiter.api.Assertions.assertTrue;
|
||||
|
||||
class CcdiProjectOverviewReportPdfExporterTest {
|
||||
|
||||
@Test
|
||||
void shouldExportOverviewReportPdf() throws Exception {
|
||||
CcdiProjectOverviewReportPdfExporter exporter = new CcdiProjectOverviewReportPdfExporter();
|
||||
CcdiProjectOverviewReportPdfExporter exporter = new CcdiProjectOverviewReportPdfExporter(resolveTestFontPath());
|
||||
MockHttpServletResponse response = new MockHttpServletResponse();
|
||||
|
||||
exporter.export(response, buildReport());
|
||||
@@ -36,6 +39,41 @@ class CcdiProjectOverviewReportPdfExporterTest {
|
||||
assertTrue(response.getContentAsByteArray().length > 1000);
|
||||
}
|
||||
|
||||
@Test
|
||||
void shouldRejectBlankPdfFontPath() {
|
||||
CcdiProjectOverviewReportPdfExporter exporter = new CcdiProjectOverviewReportPdfExporter("");
|
||||
MockHttpServletResponse response = new MockHttpServletResponse();
|
||||
|
||||
ServiceException exception = assertThrows(ServiceException.class, () -> exporter.export(response, buildReport()));
|
||||
|
||||
assertEquals("未配置PDF中文字体路径,无法导出PDF报告", exception.getMessage());
|
||||
}
|
||||
|
||||
@Test
|
||||
void shouldRejectUnavailablePdfFontPath() {
|
||||
String missingPath = "/tmp/ccdi-missing-pdf-font-" + System.nanoTime() + ".ttc";
|
||||
CcdiProjectOverviewReportPdfExporter exporter = new CcdiProjectOverviewReportPdfExporter(missingPath);
|
||||
MockHttpServletResponse response = new MockHttpServletResponse();
|
||||
|
||||
ServiceException exception = assertThrows(ServiceException.class, () -> exporter.export(response, buildReport()));
|
||||
|
||||
assertTrue(exception.getMessage().contains(missingPath));
|
||||
}
|
||||
|
||||
private String resolveTestFontPath() {
|
||||
List<String> candidates = List.of(
|
||||
"/System/Library/Fonts/STHeiti Medium.ttc",
|
||||
"/System/Library/Fonts/STHeiti Light.ttc",
|
||||
"/System/Library/Fonts/Hiragino Sans GB.ttc",
|
||||
"/System/Library/Fonts/Supplemental/Songti.ttc",
|
||||
"/usr/share/fonts/truetype/wqy/wqy-microhei.ttc"
|
||||
);
|
||||
return candidates.stream()
|
||||
.filter(path -> new File(path).isFile())
|
||||
.findFirst()
|
||||
.orElse(candidates.get(0));
|
||||
}
|
||||
|
||||
private CcdiProjectOverviewReportVO buildReport() {
|
||||
CcdiProjectOverviewReportVO report = new CcdiProjectOverviewReportVO();
|
||||
CcdiProject project = new CcdiProject();
|
||||
|
||||
Reference in New Issue
Block a user