Browse Source

研判分析--完善字体注册

master
buaixuexideshitongxue 2 weeks ago
parent
commit
ed9c02f28c
  1. 91
      src/main/java/com/biutag/supervision/support/FontDiagnosticRunner.java
  2. 191
      src/main/java/com/biutag/supervision/util/ChartRenderUtil.java
  3. BIN
      src/main/resources/fonts/NotoSansCJKsc-Bold.otf
  4. BIN
      src/main/resources/fonts/NotoSansCJKsc-Regular.otf

91
src/main/java/com/biutag/supervision/support/FontDiagnosticRunner.java

@ -0,0 +1,91 @@
package com.biutag.supervision.support;
import com.biutag.supervision.util.ChartRenderUtil;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.boot.ApplicationArguments;
import org.springframework.boot.ApplicationRunner;
import org.springframework.stereotype.Component;
import java.awt.Font;
import java.awt.GraphicsEnvironment;
import java.util.Arrays;
import java.util.LinkedHashSet;
import java.util.Set;
@Component
public class FontDiagnosticRunner implements ApplicationRunner {
private static final Logger log = LoggerFactory.getLogger(FontDiagnosticRunner.class);
private static final String SAMPLE_TEXT = "查实情况分布单位问题数量暂无数据";
private static final String[] CANDIDATES = new String[]{
"Microsoft YaHei", "微软雅黑",
"PingFang SC", "苹方-简",
"Noto Sans CJK SC", "Noto Sans SC",
"Source Han Sans CN", "Source Han Sans SC",
"WenQuanYi Micro Hei", "文泉驿微米黑",
"SimHei", "黑体",
"SimSun", "宋体",
"Arial Unicode MS",
"SansSerif"
};
@Override
public void run(ApplicationArguments args) {
ChartRenderUtil.registerFontFromResource("/fonts/NotoSansCJKsc-Regular.otf");
ChartRenderUtil.registerFontFromResource("/fonts/NotoSansCJKsc-Bold.otf");
diagnose();
}
public static void diagnose() {
try {
boolean headless = GraphicsEnvironment.isHeadless();
GraphicsEnvironment ge = GraphicsEnvironment.getLocalGraphicsEnvironment();
String[] availableFonts = ge.getAvailableFontFamilyNames();
Set<String> fontSet = new LinkedHashSet<>(Arrays.asList(availableFonts));
log.info("========== Font Diagnostic Start ==========");
log.info("java.awt.headless={}", headless);
log.info("available font family count={}", availableFonts.length);
for (String candidate : CANDIDATES) {
String matched = findMatched(candidate, fontSet);
if (matched == null) {
log.info("font candidate not found: {}", candidate);
continue;
}
Font font = new Font(matched, Font.PLAIN, 18);
int unsupportedIndex = font.canDisplayUpTo(SAMPLE_TEXT);
log.info("font candidate found: candidate={}, matchedFont={}, family={}, fontName={}, psName={}, canDisplayAll={}",
candidate,
matched,
font.getFamily(),
font.getFontName(),
font.getPSName(),
unsupportedIndex == -1);
if (unsupportedIndex != -1) {
log.warn("font cannot fully display sample text: matchedFont={}, unsupportedIndex={}, sample={}",
matched, unsupportedIndex, SAMPLE_TEXT);
}
}
log.info("========== Font Diagnostic End ==========");
} catch (Throwable e) {
log.error("字体诊断失败", e);
}
}
private static String findMatched(String candidate, Set<String> fontSet) {
for (String fontName : fontSet) {
if (fontName.equalsIgnoreCase(candidate)) {
return fontName;
}
}
return null;
}
}

191
src/main/java/com/biutag/supervision/util/ChartRenderUtil.java

@ -3,6 +3,7 @@ package com.biutag.supervision.util;
import org.jfree.chart.ChartFactory;
import org.jfree.chart.ChartUtils;
import org.jfree.chart.JFreeChart;
import org.jfree.chart.block.BlockBorder;
import org.jfree.chart.labels.StandardCategoryItemLabelGenerator;
import org.jfree.chart.labels.StandardPieSectionLabelGenerator;
import org.jfree.chart.plot.CategoryPlot;
@ -12,15 +13,20 @@ import org.jfree.chart.renderer.category.BarRenderer;
import org.jfree.chart.title.LegendTitle;
import org.jfree.chart.ui.HorizontalAlignment;
import org.jfree.chart.ui.RectangleEdge;
import org.jfree.chart.ui.RectangleInsets;
import org.jfree.data.category.DefaultCategoryDataset;
import org.jfree.data.general.DefaultPieDataset;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.awt.BasicStroke;
import java.awt.Color;
import java.awt.Font;
import java.awt.GraphicsEnvironment;
import java.io.ByteArrayOutputStream;
import java.io.InputStream;
import java.text.DecimalFormat;
import java.util.Arrays;
import java.util.LinkedHashMap;
import java.util.LinkedHashSet;
import java.util.List;
@ -37,9 +43,16 @@ import java.util.Set;
*/
public class ChartRenderUtil {
private static final Logger log = LoggerFactory.getLogger(ChartRenderUtil.class);
private ChartRenderUtil() {
}
/**
* 字体探测样本文本
*/
private static final String FONT_PROBE_TEXT = "查实情况分布单位问题数量暂无数据";
/**
* 饼图配色
*/
@ -73,6 +86,21 @@ public class ChartRenderUtil {
private static final String DEFAULT_OTHER_NAME = "其余问题类型";
private static final String DEFAULT_EMPTY_NAME = "暂无数据";
/**
* 候选字体按优先级排序
*/
private static final String[] FONT_CANDIDATES = new String[]{
"Microsoft YaHei", "微软雅黑",
"PingFang SC", "苹方-简",
"Noto Sans CJK SC", "Noto Sans SC",
"Source Han Sans CN", "Source Han Sans SC",
"WenQuanYi Micro Hei", "文泉驿微米黑",
"SimHei", "黑体",
"SimSun", "宋体",
"Arial Unicode MS",
"SansSerif"
};
// =========================
// 对外 API:饼图(唯一入口)
// =========================
@ -94,14 +122,14 @@ public class ChartRenderUtil {
chartData.forEach(dataset::setValue);
JFreeChart chart = ChartFactory.createPieChart(
title,
safeTitle(title),
dataset,
true,
false,
false
);
beautifyPie(chart, chartData);
beautifyPie(chart, chartData, title);
return toPng(chart, w, h, "生成饼图失败");
}
@ -123,12 +151,12 @@ public class ChartRenderUtil {
DefaultCategoryDataset dataset = new DefaultCategoryDataset();
Map<String, Number> safeData = normalizeBarData(data);
safeData.forEach((category, value) -> dataset.addValue(value, yLabel, category));
safeData.forEach((category, value) -> dataset.addValue(value, safeLabel(yLabel, "数量"), category));
JFreeChart chart = ChartFactory.createBarChart(
title,
xLabel,
yLabel,
safeTitle(title),
safeLabel(xLabel, "类别"),
safeLabel(yLabel, "数量"),
dataset,
PlotOrientation.VERTICAL,
true,
@ -136,7 +164,7 @@ public class ChartRenderUtil {
false
);
beautifyBar(chart, 1);
beautifyBar(chart, 1, title);
return toPng(chart, w, h, "生成柱状图失败");
}
@ -176,9 +204,9 @@ public class ChartRenderUtil {
}
JFreeChart chart = ChartFactory.createBarChart(
title,
xLabel,
yLabel,
safeTitle(title),
safeLabel(xLabel, "类别"),
safeLabel(yLabel, "数量"),
dataset,
PlotOrientation.VERTICAL,
true,
@ -186,7 +214,7 @@ public class ChartRenderUtil {
false
);
beautifyBar(chart, safeSeriesData.size());
beautifyBar(chart, safeSeriesData.size(), title);
return toPng(chart, w, h, "生成分组柱状图失败");
}
@ -225,29 +253,31 @@ public class ChartRenderUtil {
// 美化:饼图
// =========================
private static void beautifyPie(JFreeChart chart, Map<String, Number> data) {
private static void beautifyPie(JFreeChart chart, Map<String, Number> data, String originalTitle) {
chart.setAntiAlias(true);
chart.setTextAntiAlias(true);
chart.setBackgroundPaint(Color.WHITE);
chart.setBorderVisible(false);
chart.setPadding(new RectangleInsets(8, 8, 8, 8));
Font titleFont = pickFont(Font.BOLD, 18);
Font legendFont = pickFont(Font.PLAIN, 12);
Font labelFont = pickFont(Font.PLAIN, 11);
Font titleFont = pickFont(Font.BOLD, 18, safeTitle(originalTitle));
Font legendFont = pickFont(Font.PLAIN, 12, "图例");
Font labelFont = pickFont(Font.PLAIN, 11, FONT_PROBE_TEXT);
if (chart.getTitle() != null) {
chart.getTitle().setFont(titleFont);
chart.getTitle().setPaint(new Color(51, 51, 51));
chart.getTitle().setHorizontalAlignment(HorizontalAlignment.CENTER);
chart.getTitle().setMargin(0, 0, 12, 0);
chart.getTitle().setMargin(8, 0, 14, 0);
}
LegendTitle legend = chart.getLegend();
if (legend != null) {
legend.setItemFont(legendFont);
legend.setBackgroundPaint(Color.WHITE);
legend.setBorder(0, 0, 0, 0);
legend.setFrame(BlockBorder.NONE);
legend.setPosition(RectangleEdge.TOP);
legend.setMargin(0, 0, 8, 0);
}
PiePlot<?> plot = (PiePlot<?>) chart.getPlot();
@ -290,29 +320,32 @@ public class ChartRenderUtil {
// 美化:柱状图
// =========================
private static void beautifyBar(JFreeChart chart, int seriesCount) {
private static void beautifyBar(JFreeChart chart, int seriesCount, String originalTitle) {
chart.setAntiAlias(true);
chart.setTextAntiAlias(true);
chart.setBackgroundPaint(Color.WHITE);
chart.setBorderVisible(false);
chart.setPadding(new RectangleInsets(8, 8, 8, 8));
Font titleFont = pickFont(Font.BOLD, 16);
Font legendFont = pickFont(Font.PLAIN, 12);
Font axisFont = pickFont(Font.PLAIN, 12);
Font valueFont = pickFont(Font.PLAIN, 11);
Font titleFont = pickFont(Font.BOLD, 16, safeTitle(originalTitle));
Font legendFont = pickFont(Font.PLAIN, 12, "图例");
Font axisFont = pickFont(Font.PLAIN, 12, FONT_PROBE_TEXT);
Font valueFont = pickFont(Font.PLAIN, 11, "100");
if (chart.getTitle() != null) {
chart.getTitle().setFont(titleFont);
chart.getTitle().setPaint(new Color(51, 51, 51));
chart.getTitle().setHorizontalAlignment(HorizontalAlignment.CENTER);
chart.getTitle().setMargin(8, 0, 12, 0);
}
LegendTitle legend = chart.getLegend();
if (legend != null) {
legend.setItemFont(legendFont);
legend.setBackgroundPaint(Color.WHITE);
legend.setBorder(0, 0, 0, 0);
legend.setFrame(BlockBorder.NONE);
legend.setPosition(RectangleEdge.TOP);
legend.setMargin(0, 0, 8, 0);
}
CategoryPlot plot = chart.getCategoryPlot();
@ -320,6 +353,7 @@ public class ChartRenderUtil {
plot.setOutlineVisible(false);
plot.setRangeGridlinePaint(new Color(230, 230, 230));
plot.setRangeGridlinesVisible(true);
plot.setInsets(new RectangleInsets(4, 8, 4, 8));
plot.getDomainAxis().setTickLabelFont(axisFont);
plot.getDomainAxis().setLabelFont(axisFont);
@ -331,7 +365,6 @@ public class ChartRenderUtil {
renderer.setShadowVisible(false);
renderer.setItemMargin(0.10);
// 多系列时适当放宽柱宽
if (seriesCount <= 1) {
renderer.setMaximumBarWidth(0.12);
} else if (seriesCount == 2) {
@ -528,35 +561,105 @@ public class ChartRenderUtil {
}
// =========================
// 字体兜底(避免 Linux 没微软雅黑导致中文丑/方块)
// 字体处理
// =========================
private static Font pickFont(int style, int size) {
try {
// 按优先级尝试(Windows 常见:微软雅黑;Linux 常见:Noto/文泉驿/DejaVu)
String[] candidates = new String[]{
"Microsoft YaHei", "微软雅黑",
"PingFang SC", "苹方-简",
"Noto Sans CJK SC", "Noto Sans SC",
"WenQuanYi Micro Hei", "文泉驿微米黑",
"SimSun", "宋体",
"Arial Unicode MS",
"SansSerif"
};
/**
* classpath 注册字体可选
* 例如/fonts/NotoSansCJKsc-Regular.otf
*/
public static void registerFontFromResource(String classpathLocation) {
if (classpathLocation == null || classpathLocation.isBlank()) {
return;
}
try (InputStream inputStream = ChartRenderUtil.class.getResourceAsStream(classpathLocation.trim())) {
if (inputStream == null) {
log.warn("字体资源未找到: {}", classpathLocation);
return;
}
Font font = Font.createFont(Font.TRUETYPE_FONT, inputStream);
GraphicsEnvironment ge = GraphicsEnvironment.getLocalGraphicsEnvironment();
boolean registered = ge.registerFont(font);
log.info("注册字体完成, location={}, fontName={}, family={}, registered={}",
classpathLocation, font.getFontName(), font.getFamily(), registered);
} catch (Exception e) {
log.warn("注册字体失败, location={}", classpathLocation, e);
}
}
/**
* 更稳妥的字体选择
* 1. 优先按候选名单查找
* 2. 必须校验 canDisplayUpTo(sampleText) == -1
* 3. 最后退回 SansSerif
*/
private static Font pickFont(int style, int size, String sampleText) {
String probe = safeProbeText(sampleText);
try {
GraphicsEnvironment ge = GraphicsEnvironment.getLocalGraphicsEnvironment();
String[] available = ge.getAvailableFontFamilyNames();
for (String c : candidates) {
for (String a : available) {
if (a.equalsIgnoreCase(c)) {
return new Font(a, style, size);
Set<String> availableSet = new LinkedHashSet<>(Arrays.asList(available));
for (String candidate : FONT_CANDIDATES) {
String matchedFont = findMatchedFont(candidate, availableSet);
if (matchedFont == null) {
continue;
}
Font font = new Font(matchedFont, style, size);
int unsupportedIndex = font.canDisplayUpTo(probe);
if (unsupportedIndex == -1) {
log.debug("选择字体成功, candidate={}, matchedFont={}, style={}, size={}, probe={}",
candidate, matchedFont, style, size, probe);
return font;
} else {
log.warn("字体存在缺字, candidate={}, matchedFont={}, unsupportedIndex={}, probe={}",
candidate, matchedFont, unsupportedIndex, probe);
}
}
} catch (Throwable e) {
// 字体系统初始化失败时兜底
System.err.println("Font detection failed, fallback to SansSerif: " + e.getMessage());
log.warn("字体检测失败,使用 SansSerif 兜底", e);
}
Font fallback = new Font("SansSerif", style, size);
int unsupportedIndex = fallback.canDisplayUpTo(probe);
if (unsupportedIndex != -1) {
log.warn("最终兜底字体 SansSerif 仍无法完整显示文本, unsupportedIndex={}, probe={}",
unsupportedIndex, probe);
} else {
log.debug("使用兜底字体 SansSerif 成功, style={}, size={}, probe={}", style, size, probe);
}
return fallback;
}
private static String findMatchedFont(String candidate, Set<String> availableSet) {
for (String fontName : availableSet) {
if (fontName.equalsIgnoreCase(candidate)) {
return fontName;
}
}
return null;
}
private static String safeProbeText(String text) {
if (text == null || text.isBlank()) {
return FONT_PROBE_TEXT;
}
return text.trim();
}
private static String safeTitle(String title) {
if (title == null || title.isBlank()) {
return DEFAULT_EMPTY_NAME;
}
return title.trim();
}
private static String safeLabel(String label, String defaultValue) {
if (label == null || label.isBlank()) {
return defaultValue;
}
return new Font("SansSerif", style, size);
return label.trim();
}
}

BIN
src/main/resources/fonts/NotoSansCJKsc-Bold.otf

Binary file not shown.

BIN
src/main/resources/fonts/NotoSansCJKsc-Regular.otf

Binary file not shown.
Loading…
Cancel
Save