fix(diagram): 流程图汉字方框——CJK 字体探测链(物理字体canDisplay验证→SansSerif回退→font-path配置兜底) + 副标起笔与label重叠
build / build-push (push) Successful in 2m18s

This commit is contained in:
chenxing
2026-09-17 08:22:13 +08:00
parent 6afc7d7b92
commit bfa2dfc713
2 changed files with 93 additions and 4 deletions
@@ -2,6 +2,7 @@ package com.yunhai.flowcenter.service;
import com.yunhai.flowcenter.engine.FlowDefModel;
import com.yunhai.flowcenter.model.FlowModels;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Service;
import javax.imageio.ImageIO;
@@ -12,12 +13,14 @@ import java.awt.FontMetrics;
import java.awt.Graphics2D;
import java.awt.RenderingHints;
import java.awt.geom.Path2D;
import java.awt.GraphicsEnvironment;
import java.awt.image.BufferedImage;
import java.io.ByteArrayOutputStream;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Arrays;
import java.util.Map;
/**
@@ -31,6 +34,7 @@ import java.util.Map;
* 节点颜色按运行时状态 done/active/rejected/pending 上色,图例仅 instance 口径绘制。
* 防御出图:环/孤儿节点/缺失端点均降级为"必出图",不抛渲染异常(外部 img 场景无错误页可跳)。
*/
@Slf4j
@Service
public class DiagramImageService {
@@ -243,13 +247,15 @@ public class DiagramImageService {
FontMetrics lf = g.getFontMetrics(labelF);
g.setFont(labelF);
g.setColor(C_TEXT);
g.drawString(truncate(g, n.getLabel(), NODE_W - 20), p[0] + 10, p[1] + 12 + lf.getAscent());
String label = truncate(g, n.getLabel(), NODE_W - 20);
g.drawString(label, p[0] + 10, p[1] + 12 + lf.getAscent());
if (n.getSubLabel() != null && !n.getSubLabel().isEmpty()) {
g.setFont(subF);
g.setColor(C_SUB);
FontMetrics sf = g.getFontMetrics(subF);
List<String> lines = wrap(g, n.getSubLabel(), NODE_W - 20, 2);
int firstLineY = p[1] + 12 + lf.getHeight();
// 副标从 label 基线下方一整行处起步,避免与 label 重叠
int firstLineY = p[1] + 12 + lf.getAscent() + sf.getAscent();
for (int i = 0; i < lines.size(); i++) {
g.drawString(lines.get(i), p[0] + 10, firstLineY + i * sf.getHeight());
}
@@ -329,8 +335,77 @@ public class DiagramImageService {
}
private static Font cjkFont(int style, int size) {
return new Font("Microsoft YaHei", style, size);
/**
* 可选指定字体文件(TTF/OTF,支持 .ttc 首个 face):容器/精简 OS 无 CJK 系统字体时的终极兜底,
* 配置路径后 createFont 注册为程序字体并优先命中。空(缺省)走系统字体探测。
*/
@org.springframework.beans.factory.annotation.Value("${flow-center.diagram.font-path:}")
private String fontPathConfig;
/** 候选族名(探测顺序:Windows/macOS 常见 → Linux 常见发行字体;依次用 canDisplay 验证真含字形) */
private static final String[] CJK_FAMILY_PREFS = {
"Microsoft YaHei", "微软雅黑", "SimHei", "NSimSun", "PingFang SC",
"Noto Sans CJK SC", "Source Han Sans SC", "WenQuanYi Micro Hei", "WenQuanYi Zen Hei",
};
private static volatile Font CJK_BASE;
/** 样式派生统一走这里保证全图同族(宽度换行/截断的 FontMetrics 与绘制必须一致) */
private Font cjkFont(int style, int size) {
return baseFont().deriveFont(style, (float) size);
}
/**
* CJK 基准字体(惰性单例):新物理字体名在含 CJK 字形时命中;否则回退逻辑族 SansSerif
* (JRE fontconfig 正常时自动复合映射系统 CJK)。全部失败时仍返回 SansSerif——
* 该状态说明宿主无任何 CJK 字体,需要配 flow-center.diagram.font-path(告警打在 resolve 里)。
*/
private Font baseFont() {
Font base = CJK_BASE;
if (base != null) {
return base;
}
synchronized (DiagramImageService.class) {
if (CJK_BASE == null) {
CJK_BASE = resolveCJK();
}
return CJK_BASE;
}
}
private Font resolveCJK() {
// 配置文件路径最优先(容器部署兜底)
GraphicsEnvironment env = GraphicsEnvironment.getLocalGraphicsEnvironment();
if (fontPathConfig != null && !fontPathConfig.trim().isEmpty()) {
try (java.io.InputStream in = new java.io.FileInputStream(fontPathConfig.trim())) {
Font loaded = Font.createFont(Font.TRUETYPE_FONT, in);
env.registerFont(loaded);
log.info("[flow-center] 流程图字体已加载配置文件: {} → {}", fontPathConfig, loaded.getFontName());
return loaded;
} catch (Exception e) {
log.error("[flow-center] flow-center.diagram.font-path 加载失败: {}({}),改走系统探测", fontPathConfig, e.getMessage());
}
}
String sample = "教师入职审批·任一人通过时限";
for (String family : CJK_FAMILY_PREFS) {
Font f = new Font(family, Font.PLAIN, 14);
// 命中同名物理字体且真含 CJK 字形才算(未命中时 new Font 返回 Dialog 映射、canDisplay 成立也不能认)
if (Arrays.asList(env.getAvailableFontFamilyNames()).contains(family)
&& f.canDisplayUpTo(sample) == -1) {
return f;
}
}
Font fallback = tofuWarning(new Font(Font.SANS_SERIF, Font.PLAIN, 14), sample);
if (fallback != null) {
return fallback;
}
log.error("[flow-center] 找不到含中文的字体(流程图汉字将显示为方框):请在容器镜像安装字体"
+ "(如 fonts-noto-cjk),或配置 flow-center.diagram.font-path 指向 TTF 文件");
return new Font(Font.SANS_SERIF, Font.PLAIN, 14);
}
private Font tofuWarning(Font f, String sample) {
return f.canDisplayUpTo(sample) == -1 ? f : null;
}
/** 贪心按像素宽度折行,最多 maxLines 行;溢出行以 … 结尾 */