fix(diagram): 流程图汉字方框——CJK 字体探测链(物理字体canDisplay验证→SansSerif回退→font-path配置兜底) + 副标起笔与label重叠
build / build-push (push) Successful in 2m18s
build / build-push (push) Successful in 2m18s
This commit is contained in:
@@ -2,6 +2,7 @@ package com.yunhai.flowcenter.service;
|
|||||||
|
|
||||||
import com.yunhai.flowcenter.engine.FlowDefModel;
|
import com.yunhai.flowcenter.engine.FlowDefModel;
|
||||||
import com.yunhai.flowcenter.model.FlowModels;
|
import com.yunhai.flowcenter.model.FlowModels;
|
||||||
|
import lombok.extern.slf4j.Slf4j;
|
||||||
import org.springframework.stereotype.Service;
|
import org.springframework.stereotype.Service;
|
||||||
|
|
||||||
import javax.imageio.ImageIO;
|
import javax.imageio.ImageIO;
|
||||||
@@ -12,12 +13,14 @@ import java.awt.FontMetrics;
|
|||||||
import java.awt.Graphics2D;
|
import java.awt.Graphics2D;
|
||||||
import java.awt.RenderingHints;
|
import java.awt.RenderingHints;
|
||||||
import java.awt.geom.Path2D;
|
import java.awt.geom.Path2D;
|
||||||
|
import java.awt.GraphicsEnvironment;
|
||||||
import java.awt.image.BufferedImage;
|
import java.awt.image.BufferedImage;
|
||||||
import java.io.ByteArrayOutputStream;
|
import java.io.ByteArrayOutputStream;
|
||||||
import java.util.ArrayList;
|
import java.util.ArrayList;
|
||||||
import java.util.HashMap;
|
import java.util.HashMap;
|
||||||
import java.util.LinkedHashMap;
|
import java.util.LinkedHashMap;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
|
import java.util.Arrays;
|
||||||
import java.util.Map;
|
import java.util.Map;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -31,6 +34,7 @@ import java.util.Map;
|
|||||||
* 节点颜色按运行时状态 done/active/rejected/pending 上色,图例仅 instance 口径绘制。
|
* 节点颜色按运行时状态 done/active/rejected/pending 上色,图例仅 instance 口径绘制。
|
||||||
* 防御出图:环/孤儿节点/缺失端点均降级为"必出图",不抛渲染异常(外部 img 场景无错误页可跳)。
|
* 防御出图:环/孤儿节点/缺失端点均降级为"必出图",不抛渲染异常(外部 img 场景无错误页可跳)。
|
||||||
*/
|
*/
|
||||||
|
@Slf4j
|
||||||
@Service
|
@Service
|
||||||
public class DiagramImageService {
|
public class DiagramImageService {
|
||||||
|
|
||||||
@@ -243,13 +247,15 @@ public class DiagramImageService {
|
|||||||
FontMetrics lf = g.getFontMetrics(labelF);
|
FontMetrics lf = g.getFontMetrics(labelF);
|
||||||
g.setFont(labelF);
|
g.setFont(labelF);
|
||||||
g.setColor(C_TEXT);
|
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()) {
|
if (n.getSubLabel() != null && !n.getSubLabel().isEmpty()) {
|
||||||
g.setFont(subF);
|
g.setFont(subF);
|
||||||
g.setColor(C_SUB);
|
g.setColor(C_SUB);
|
||||||
FontMetrics sf = g.getFontMetrics(subF);
|
FontMetrics sf = g.getFontMetrics(subF);
|
||||||
List<String> lines = wrap(g, n.getSubLabel(), NODE_W - 20, 2);
|
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++) {
|
for (int i = 0; i < lines.size(); i++) {
|
||||||
g.drawString(lines.get(i), p[0] + 10, firstLineY + i * sf.getHeight());
|
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 行;溢出行以 … 结尾 */
|
/** 贪心按像素宽度折行,最多 maxLines 行;溢出行以 … 结尾 */
|
||||||
|
|||||||
@@ -68,6 +68,20 @@ class DiagramImageServiceTest {
|
|||||||
assertNotEquals(0, service.renderPng(vo(nodes, edges)).length);
|
assertNotEquals(0, service.renderPng(vo(nodes, edges)).length);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void smoke_writeSamplePng_forVisualCheck() throws Exception {
|
||||||
|
List<FlowModels.NodeVo> nodes = new ArrayList<>(Arrays.asList(
|
||||||
|
node("n1", "START", "开始", null, null),
|
||||||
|
node("n2", "APPROVAL", "教师入职审批", "刘文娟 · 时限48h", "done"),
|
||||||
|
node("n3", "APPROVAL", "科室审批", "教学处 · 云海小学001 · 任一人通过 · 时限24h", "active"),
|
||||||
|
node("n4", "END", "结束", null, null)));
|
||||||
|
List<FlowModels.EdgeVo> edges = new ArrayList<>(Arrays.asList(
|
||||||
|
edge("n1", "n2", null), edge("n2", "n3", "同意"), edge("n3", "n4", null)));
|
||||||
|
java.nio.file.Files.write(
|
||||||
|
java.nio.file.Paths.get("target", "diagram-smoke.png"),
|
||||||
|
service.renderPng(vo(nodes, edges)));
|
||||||
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
void render_defensive_missingEndpointAndCycle_stillProducesImage() {
|
void render_defensive_missingEndpointAndCycle_stillProducesImage() {
|
||||||
// 成环 + 指向不存在端点的边:不抛,仍出图(外部 img 场景防御)
|
// 成环 + 指向不存在端点的边:不抛,仍出图(外部 img 场景防御)
|
||||||
|
|||||||
Reference in New Issue
Block a user