|
|
@@ -7,6 +7,7 @@ import javax.imageio.ImageIO;
|
|
|
import java.awt.*;
|
|
|
import java.awt.image.BufferedImage;
|
|
|
import java.io.ByteArrayOutputStream;
|
|
|
+import java.io.InputStream;
|
|
|
import java.math.BigDecimal;
|
|
|
import java.text.DecimalFormat;
|
|
|
import java.util.ArrayList;
|
|
|
@@ -31,8 +32,23 @@ public class StatisticsComparisonImageUtil {
|
|
|
private static final Color SECTION_TITLE_COLOR = new Color(33, 33, 33); // #212121
|
|
|
private static final Color BORDER_COLOR = new Color(224, 224, 224); // #E0E0E0
|
|
|
|
|
|
- // 字体定义
|
|
|
- private static final String FONT_NAME = "Microsoft YaHei"; // 微软雅黑,如果系统没有则使用默认字体
|
|
|
+ // 字体:优先 classpath 内嵌字体(Linux 无微软雅黑时避免中文方框),其次系统支持中文的字体
|
|
|
+ private static final String FONT_CLASSPATH = "fonts/NotoSansSC-Regular.ttf";
|
|
|
+ private static final String[] FONT_FALLBACK_NAMES = {
|
|
|
+ "Microsoft YaHei",
|
|
|
+ "PingFang SC",
|
|
|
+ "WenQuanYi Zen Hei",
|
|
|
+ "WenQuanYi Micro Hei",
|
|
|
+ "Noto Sans CJK SC",
|
|
|
+ "Noto Sans SC",
|
|
|
+ "SimSun",
|
|
|
+ "STSong",
|
|
|
+ "Source Han Sans SC",
|
|
|
+ "DengXian"
|
|
|
+ };
|
|
|
+ private static volatile Font baseChineseFont; // 从 TTF 加载的基准字体
|
|
|
+ private static volatile String chineseFontName; // 系统字体名(当未加载 TTF 时)
|
|
|
+
|
|
|
private static final int TITLE_FONT_SIZE = 24;
|
|
|
private static final int SECTION_TITLE_FONT_SIZE = 18;
|
|
|
private static final int DATA_FONT_SIZE = 14;
|
|
|
@@ -49,6 +65,65 @@ public class StatisticsComparisonImageUtil {
|
|
|
private static final DecimalFormat PERCENT_FORMAT = new DecimalFormat("#,##0.00%");
|
|
|
|
|
|
/**
|
|
|
+ * 获取支持中文的字体(用于绘制报表,避免 Linux 下无微软雅黑导致中文方框)
|
|
|
+ * 优先:classpath 下 fonts/NotoSansSC-Regular.ttf;其次系统已安装的中文字体名
|
|
|
+ */
|
|
|
+ private static Font getChineseFont(int style, int size) {
|
|
|
+ if (baseChineseFont != null) {
|
|
|
+ return baseChineseFont.deriveFont(style, size);
|
|
|
+ }
|
|
|
+ if (chineseFontName != null) {
|
|
|
+ return new Font(chineseFontName, style, size);
|
|
|
+ }
|
|
|
+ synchronized (StatisticsComparisonImageUtil.class) {
|
|
|
+ if (baseChineseFont != null) {
|
|
|
+ return baseChineseFont.deriveFont(style, size);
|
|
|
+ }
|
|
|
+ if (chineseFontName != null) {
|
|
|
+ return new Font(chineseFontName, style, size);
|
|
|
+ }
|
|
|
+ try {
|
|
|
+ InputStream is = StatisticsComparisonImageUtil.class.getClassLoader().getResourceAsStream(FONT_CLASSPATH);
|
|
|
+ if (is != null) {
|
|
|
+ baseChineseFont = Font.createFont(Font.TRUETYPE_FONT, is).deriveFont(Font.PLAIN, 14);
|
|
|
+ is.close();
|
|
|
+ log.info("使用内嵌中文字体: {}", FONT_CLASSPATH);
|
|
|
+ return baseChineseFont.deriveFont(style, size);
|
|
|
+ }
|
|
|
+ } catch (Exception e) {
|
|
|
+ log.warn("加载内嵌中文字体失败,将尝试系统字体: {}", e.getMessage());
|
|
|
+ }
|
|
|
+ GraphicsEnvironment ge = GraphicsEnvironment.getLocalGraphicsEnvironment();
|
|
|
+ String[] names = ge.getAvailableFontFamilyNames();
|
|
|
+ if (names != null) {
|
|
|
+ for (String candidate : FONT_FALLBACK_NAMES) {
|
|
|
+ for (String name : names) {
|
|
|
+ if (candidate.equals(name)) {
|
|
|
+ Font f = new Font(name, Font.PLAIN, 14);
|
|
|
+ if (f.canDisplayUpTo("经营数据") < 0) {
|
|
|
+ chineseFontName = name;
|
|
|
+ log.info("使用系统中文字体: {}", name);
|
|
|
+ return new Font(chineseFontName, style, size);
|
|
|
+ }
|
|
|
+ }
|
|
|
+ }
|
|
|
+ }
|
|
|
+ for (String name : names) {
|
|
|
+ Font f = new Font(name, Font.PLAIN, 14);
|
|
|
+ if (f.canDisplayUpTo("经") < 0) {
|
|
|
+ chineseFontName = name;
|
|
|
+ log.info("使用系统中文字体(自动): {}", name);
|
|
|
+ return new Font(chineseFontName, style, size);
|
|
|
+ }
|
|
|
+ }
|
|
|
+ }
|
|
|
+ chineseFontName = Font.SANS_SERIF;
|
|
|
+ log.warn("未找到支持中文的字体,使用 SansSerif,Linux 下中文可能显示为方框,建议在 resources/fonts/ 放入 NotoSansSC-Regular.ttf");
|
|
|
+ return new Font(chineseFontName, style, size);
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
* 将统计数据对比转换为图片字节数组
|
|
|
*
|
|
|
* @param comparison 统计数据对比对象
|
|
|
@@ -174,7 +249,7 @@ public class StatisticsComparisonImageUtil {
|
|
|
* 绘制标题
|
|
|
*/
|
|
|
private static int drawTitle(Graphics2D g2d, int y, StoreOperationalStatisticsComparisonVo comparison) {
|
|
|
- Font titleFont = new Font(FONT_NAME, Font.BOLD, TITLE_FONT_SIZE);
|
|
|
+ Font titleFont = getChineseFont(Font.BOLD, TITLE_FONT_SIZE);
|
|
|
g2d.setFont(titleFont);
|
|
|
g2d.setColor(SECTION_TITLE_COLOR);
|
|
|
|
|
|
@@ -191,7 +266,7 @@ public class StatisticsComparisonImageUtil {
|
|
|
* 绘制日期范围
|
|
|
*/
|
|
|
private static int drawDateRange(Graphics2D g2d, int y, StoreOperationalStatisticsComparisonVo comparison) {
|
|
|
- Font dateFont = new Font(FONT_NAME, Font.PLAIN, DATA_FONT_SIZE);
|
|
|
+ Font dateFont = getChineseFont(Font.PLAIN, DATA_FONT_SIZE);
|
|
|
g2d.setFont(dateFont);
|
|
|
g2d.setColor(TEXT_COLOR);
|
|
|
|
|
|
@@ -225,18 +300,18 @@ public class StatisticsComparisonImageUtil {
|
|
|
}
|
|
|
|
|
|
// 绘制区块标题
|
|
|
- Font sectionFont = new Font(FONT_NAME, Font.BOLD, SECTION_TITLE_FONT_SIZE);
|
|
|
+ Font sectionFont = getChineseFont(Font.BOLD, SECTION_TITLE_FONT_SIZE);
|
|
|
g2d.setFont(sectionFont);
|
|
|
g2d.setColor(SECTION_TITLE_COLOR);
|
|
|
g2d.drawString(sectionTitle, PADDING, y);
|
|
|
y += 30;
|
|
|
-
|
|
|
+
|
|
|
// 绘制表头
|
|
|
y = drawTableHeader(g2d, y);
|
|
|
-
|
|
|
+
|
|
|
// 绘制数据行
|
|
|
- Font dataFont = new Font(FONT_NAME, Font.PLAIN, DATA_FONT_SIZE);
|
|
|
- Font labelFont = new Font(FONT_NAME, Font.PLAIN, LABEL_FONT_SIZE);
|
|
|
+ Font dataFont = getChineseFont(Font.PLAIN, DATA_FONT_SIZE);
|
|
|
+ Font labelFont = getChineseFont(Font.PLAIN, LABEL_FONT_SIZE);
|
|
|
|
|
|
for (DataRow row : rows) {
|
|
|
y = drawDataRow(g2d, y, row, dataFont, labelFont);
|
|
|
@@ -254,7 +329,7 @@ public class StatisticsComparisonImageUtil {
|
|
|
g2d.fillRect(PADDING, y, IMAGE_WIDTH - PADDING * 2, ROW_HEIGHT);
|
|
|
|
|
|
// 绘制表头文字
|
|
|
- Font headerFont = new Font(FONT_NAME, Font.BOLD, LABEL_FONT_SIZE);
|
|
|
+ Font headerFont = getChineseFont(Font.BOLD, LABEL_FONT_SIZE);
|
|
|
g2d.setFont(headerFont);
|
|
|
g2d.setColor(TEXT_COLOR);
|
|
|
|
|
|
@@ -650,19 +725,19 @@ public class StatisticsComparisonImageUtil {
|
|
|
}
|
|
|
|
|
|
// 绘制区块标题
|
|
|
- Font sectionFont = new Font(FONT_NAME, Font.BOLD, SECTION_TITLE_FONT_SIZE);
|
|
|
+ Font sectionFont = getChineseFont(Font.BOLD, SECTION_TITLE_FONT_SIZE);
|
|
|
g2d.setFont(sectionFont);
|
|
|
g2d.setColor(SECTION_TITLE_COLOR);
|
|
|
g2d.drawString(sectionTitle, PADDING, y);
|
|
|
y += 30;
|
|
|
-
|
|
|
- Font dataFont = new Font(FONT_NAME, Font.PLAIN, DATA_FONT_SIZE);
|
|
|
- Font labelFont = new Font(FONT_NAME, Font.PLAIN, LABEL_FONT_SIZE);
|
|
|
-
|
|
|
+
|
|
|
+ Font dataFont = getChineseFont(Font.PLAIN, DATA_FONT_SIZE);
|
|
|
+ Font labelFont = getChineseFont(Font.PLAIN, LABEL_FONT_SIZE);
|
|
|
+
|
|
|
// 遍历每个价目表
|
|
|
for (StoreOperationalStatisticsComparisonVo.PriceListRankingComparison ranking : rankings) {
|
|
|
// 绘制价目表名称(作为子标题)
|
|
|
- g2d.setFont(new Font(FONT_NAME, Font.BOLD, LABEL_FONT_SIZE));
|
|
|
+ g2d.setFont(getChineseFont(Font.BOLD, LABEL_FONT_SIZE));
|
|
|
g2d.setColor(new Color(66, 66, 66));
|
|
|
String priceListName = ranking.getPriceListItemName() != null ? ranking.getPriceListItemName() :
|
|
|
("价目表ID: " + ranking.getPriceId());
|