| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628 |
- package com.malk.mankalong.util;
- import com.alibaba.fastjson.JSON;
- import com.alibaba.fastjson.JSONArray;
- import com.alibaba.fastjson.JSONObject;
- import com.malk.mankalong.config.ImageConfig;
- import com.malk.mankalong.entity.dto.FieldDefinitionDTO;
- import java.awt.*;
- import java.awt.image.BufferedImage;
- import java.text.SimpleDateFormat;
- import java.util.*;
- import java.util.List;
- /**
- * 表格图片生成器
- *
- * <p>使用 Java AWT Graphics2D 将宜搭子表数据渲染为表格图片。</p>
- * <p>支持:中文字体自适应、列宽自适应、长文本自动换行、表头样式、隔行变色。</p>
- *
- * <p>JDK 1.8 兼容,无外部图片库依赖。</p>
- */
- public class TableImageGenerator {
- // ===== 颜色常量 =====
- private static final Color HEADER_BG = Color.WHITE;
- private static final Color HEADER_FG = Color.BLACK;
- private static final Color ROW_BG_EVEN = Color.WHITE;
- private static final Color ROW_BG_ODD = Color.WHITE;
- private static final Color GRID_COLOR = new Color(220, 223, 230);
- private static final Color TEXT_COLOR = new Color(48, 49, 51);
- private static final Color TITLE_COLOR = new Color(48, 49, 51);
- // ===== 字体候选列表(按优先级,覆盖 Windows / Linux 常见中文字体) =====
- private static final String[] FONT_CANDIDATES = {
- "Microsoft YaHei", // Windows 微软雅黑
- "SimHei", // Windows 黑体
- "SimSun", // Windows 宋体
- "Noto Sans CJK SC", // Linux 思源黑体
- "WenQuanYi Micro Hei", // Linux 文泉驿微米黑
- "SansSerif" // 通用回退
- };
- private final ImageConfig config;
- private final Font titleFont;
- private final Font headerFont;
- private final Font cellFont;
- public TableImageGenerator(ImageConfig config) {
- this.config = config;
- String fontName = resolveChineseFont();
- this.titleFont = new Font(fontName, Font.BOLD, config.getTitleFontSize());
- this.headerFont = new Font(fontName, Font.BOLD, config.getHeaderFontSize());
- this.cellFont = new Font(fontName, Font.PLAIN, config.getCellFontSize());
- }
- /**
- * 解析可用的中文字体
- */
- private String resolveChineseFont() {
- // 中文字符测试
- char testChar = '\u4e2d'; // "中"
- for (String name : FONT_CANDIDATES) {
- Font f = new Font(name, Font.PLAIN, 12);
- if (f.canDisplay(testChar)) {
- return name;
- }
- }
- return Font.SANS_SERIF;
- }
- /**
- * 生成表格图片
- *
- * @param title 表格标题(可为 null,如子表名称)
- * @param fieldDefs 字段定义列表(决定列顺序和列标题)
- * @param dataRows 子表原始数据(每行一个 Map)
- * @return 生成的 BufferedImage
- */
- public BufferedImage generate(String title,
- List<FieldDefinitionDTO> fieldDefs,
- List<Map<String, Object>> dataRows) {
- // 1. 准备表格数据:列标题 + 字符串化的行数据
- List<String> headers = new ArrayList<>();
- List<String> fieldIds = new ArrayList<>();
- for (FieldDefinitionDTO fd : fieldDefs) {
- headers.add(fd.getLabel());
- fieldIds.add(fd.getFieldId());
- }
- List<List<String>> stringRows = new ArrayList<>();
- for (Map<String, Object> row : dataRows) {
- List<String> stringRow = new ArrayList<>();
- for (int i = 0; i < fieldIds.size(); i++) {
- String fieldId = fieldIds.get(i);
- FieldDefinitionDTO fd = fieldDefs.get(i);
- Object value = row.get(fieldId);
- stringRow.add(formatCellValue(value, fd));
- }
- stringRows.add(stringRow);
- }
- // 2. 计算列宽
- int[] colWidths = calculateColumnWidths(headers, stringRows);
- // 3. 计算行高(考虑文本换行)
- int titleHeight = (title != null && !title.isEmpty()) ? config.getTitleFontSize() + 24 : 0;
- int[] rowHeights = calculateRowHeights(headers, stringRows, colWidths);
- int headerHeight = config.getRowHeight();
- // 4. 计算图片总尺寸
- int totalWidth = config.getCellPadding() * 2; // 左右边距
- for (int w : colWidths) {
- totalWidth += w;
- }
- int totalHeight = titleHeight + headerHeight;
- for (int h : rowHeights) {
- totalHeight += h;
- }
- // 确保最小尺寸
- totalWidth = Math.max(totalWidth, 200);
- totalHeight = Math.max(totalHeight, 100);
- // 5. 创建图片
- BufferedImage image = new BufferedImage(totalWidth, totalHeight, BufferedImage.TYPE_INT_RGB);
- Graphics2D g2d = image.createGraphics();
- // 抗锯齿
- g2d.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON);
- g2d.setRenderingHint(RenderingHints.KEY_TEXT_ANTIALIASING, RenderingHints.VALUE_TEXT_ANTIALIAS_ON);
- g2d.setRenderingHint(RenderingHints.KEY_RENDERING, RenderingHints.VALUE_RENDER_QUALITY);
- // 6. 绘制背景
- Color bgColor = parseColor(config.getBackground(), Color.WHITE);
- g2d.setColor(bgColor);
- g2d.fillRect(0, 0, totalWidth, totalHeight);
- int y = 0;
- // 7. 绘制标题
- if (titleHeight > 0) {
- g2d.setColor(TITLE_COLOR);
- g2d.setFont(titleFont);
- FontMetrics titleFm = g2d.getFontMetrics();
- int titleX = config.getCellPadding();
- int titleY = y + (titleHeight - titleFm.getHeight()) / 2 + titleFm.getAscent();
- g2d.drawString(title, titleX, titleY);
- y += titleHeight;
- // 标题下方的分割线
- g2d.setColor(GRID_COLOR);
- g2d.drawLine(0, y - 1, totalWidth, y - 1);
- }
- // 8. 绘制表头
- g2d.setColor(HEADER_BG);
- g2d.fillRect(0, y, totalWidth, headerHeight);
- g2d.setColor(HEADER_FG);
- g2d.setFont(headerFont);
- FontMetrics headerFm = g2d.getFontMetrics();
- int x = config.getCellPadding();
- for (int i = 0; i < headers.size(); i++) {
- String headerText = headers.get(i);
- int textWidth = headerFm.stringWidth(headerText);
- int availWidth = colWidths[i] - config.getCellPadding() * 2;
- // 表头不换行,超长则截断
- if (textWidth > availWidth) {
- headerText = truncateText(headerText, availWidth, headerFm);
- }
- int textY = y + (headerHeight - headerFm.getHeight()) / 2 + headerFm.getAscent();
- g2d.drawString(headerText, x + config.getCellPadding(), textY);
- x += colWidths[i];
- }
- y += headerHeight;
- // 9. 绘制数据行
- g2d.setFont(cellFont);
- FontMetrics cellFm = g2d.getFontMetrics();
- for (int rowIdx = 0; rowIdx < stringRows.size(); rowIdx++) {
- List<String> rowData = stringRows.get(rowIdx);
- int rowHeight = rowHeights[rowIdx];
- // 隔行变色
- Color rowBg = (rowIdx % 2 == 0) ? ROW_BG_EVEN : ROW_BG_ODD;
- g2d.setColor(rowBg);
- g2d.fillRect(0, y, totalWidth, rowHeight);
- // 绘制每个单元格
- int cellX = config.getCellPadding();
- for (int colIdx = 0; colIdx < rowData.size(); colIdx++) {
- String cellText = rowData.get(colIdx);
- int availWidth = colWidths[colIdx] - config.getCellPadding() * 2;
- // 文本换行处理
- List<String> lines = wrapText(cellText, availWidth, cellFm);
- g2d.setColor(TEXT_COLOR);
- int lineY = y + config.getCellPadding() + cellFm.getAscent();
- int maxLines = Math.max(1, (rowHeight - config.getCellPadding() * 2) / cellFm.getHeight());
- for (int lineIdx = 0; lineIdx < Math.min(lines.size(), maxLines); lineIdx++) {
- String line = lines.get(lineIdx);
- // 最后一行如果截断了,加省略号
- if (lineIdx == maxLines - 1 && lines.size() > maxLines && !line.endsWith("...")) {
- line = truncateText(line, availWidth, cellFm);
- }
- g2d.drawString(line, cellX + config.getCellPadding(), lineY);
- lineY += cellFm.getHeight();
- }
- cellX += colWidths[colIdx];
- }
- y += rowHeight;
- }
- // 10. 绘制网格线
- drawGridLines(g2d, totalWidth, titleHeight, headerHeight, rowHeights, colWidths);
- g2d.dispose();
- return image;
- }
- // ===== 私有方法 =====
- /**
- * 计算各列宽度(基于表头和内容,限制在 min ~ max 范围内)
- */
- private int[] calculateColumnWidths(List<String> headers, List<List<String>> rows) {
- int[] widths = new int[headers.size()];
- // 使用临时 BufferedImage 获取 FontMetrics
- BufferedImage temp = new BufferedImage(1, 1, BufferedImage.TYPE_INT_RGB);
- Graphics2D g = temp.createGraphics();
- g.setFont(cellFont);
- FontMetrics cellFm = g.getFontMetrics();
- g.setFont(headerFont);
- FontMetrics headerFm = g.getFontMetrics();
- int padding = config.getCellPadding() * 2;
- for (int i = 0; i < headers.size(); i++) {
- int maxContent = headerFm.stringWidth(headers.get(i)) + padding;
- for (List<String> row : rows) {
- if (i < row.size()) {
- String cellText = row.get(i);
- // 对于多行文本,取第一行宽度作为估算
- int textWidth = cellFm.stringWidth(cellText);
- // 如果超长,按最大列宽估算
- if (textWidth > config.getMaxColWidth() - padding) {
- textWidth = config.getMaxColWidth() - padding;
- }
- int cellWidth = textWidth + padding;
- if (cellWidth > maxContent) {
- maxContent = cellWidth;
- }
- }
- }
- widths[i] = Math.max(config.getMinColWidth(), Math.min(maxContent, config.getMaxColWidth()));
- }
- g.dispose();
- return widths;
- }
- /**
- * 计算各行高度(考虑文本换行)
- */
- private int[] calculateRowHeights(List<String> headers, List<List<String>> rows, int[] colWidths) {
- BufferedImage temp = new BufferedImage(1, 1, BufferedImage.TYPE_INT_RGB);
- Graphics2D g = temp.createGraphics();
- g.setFont(cellFont);
- FontMetrics cellFm = g.getFontMetrics();
- int[] heights = new int[rows.size()];
- int defaultHeight = config.getRowHeight();
- int padding = config.getCellPadding() * 2;
- int lineHeight = cellFm.getHeight();
- for (int rowIdx = 0; rowIdx < rows.size(); rowIdx++) {
- List<String> row = rows.get(rowIdx);
- int maxLines = 1;
- for (int colIdx = 0; colIdx < row.size() && colIdx < colWidths.length; colIdx++) {
- String cellText = row.get(colIdx);
- int availWidth = colWidths[colIdx] - padding;
- List<String> lines = wrapText(cellText, availWidth, cellFm);
- if (lines.size() > maxLines) {
- maxLines = lines.size();
- }
- }
- int calculatedHeight = padding + maxLines * lineHeight;
- heights[rowIdx] = Math.max(defaultHeight, calculatedHeight);
- }
- g.dispose();
- return heights;
- }
- /**
- * 文本换行:将文本按可用宽度拆分为多行
- */
- private List<String> wrapText(String text, int availWidth, FontMetrics fm) {
- List<String> lines = new ArrayList<>();
- if (text == null || text.isEmpty()) {
- lines.add("");
- return lines;
- }
- if (availWidth <= 0) {
- lines.add(text);
- return lines;
- }
- // 按已有换行符分割
- String[] paragraphs = text.split("\n");
- for (String para : paragraphs) {
- if (para.isEmpty()) {
- lines.add("");
- continue;
- }
- if (fm.stringWidth(para) <= availWidth) {
- lines.add(para);
- continue;
- }
- // 逐字符拆分(兼容中英文混合)
- StringBuilder current = new StringBuilder();
- for (int i = 0; i < para.length(); i++) {
- char c = para.charAt(i);
- String candidate = current.toString() + c;
- if (fm.stringWidth(candidate) > availWidth) {
- if (current.length() > 0) {
- lines.add(current.toString());
- current = new StringBuilder();
- }
- // 即使单个字符也超宽,也强制加入
- current.append(c);
- } else {
- current.append(c);
- }
- }
- if (current.length() > 0) {
- lines.add(current.toString());
- }
- }
- return lines;
- }
- /**
- * 截断文本并添加省略号
- */
- private String truncateText(String text, int availWidth, FontMetrics fm) {
- if (text == null || text.isEmpty()) {
- return "";
- }
- String ellipsis = "...";
- int ellipsisWidth = fm.stringWidth(ellipsis);
- if (availWidth <= ellipsisWidth) {
- return ellipsis;
- }
- StringBuilder sb = new StringBuilder();
- for (int i = 0; i < text.length(); i++) {
- String candidate = sb.toString() + text.charAt(i);
- if (fm.stringWidth(candidate) + ellipsisWidth > availWidth) {
- break;
- }
- sb.append(text.charAt(i));
- }
- return sb.toString() + ellipsis;
- }
- /**
- * 绘制网格线
- */
- private void drawGridLines(Graphics2D g2d, int totalWidth, int titleHeight,
- int headerHeight, int[] rowHeights, int[] colWidths) {
- g2d.setColor(GRID_COLOR);
- g2d.setStroke(new BasicStroke(1f));
- int startY = titleHeight;
- // 水平线:表头底部 + 每行底部
- int y = startY + headerHeight;
- g2d.drawLine(0, y, totalWidth, y);
- for (int h : rowHeights) {
- y += h;
- g2d.drawLine(0, y, totalWidth, y);
- }
- // 表头与数据之间的分隔线(稍粗)
- g2d.setColor(new Color(200, 203, 210));
- g2d.setStroke(new BasicStroke(1.5f));
- g2d.drawLine(0, startY + headerHeight, totalWidth, startY + headerHeight);
- g2d.setStroke(new BasicStroke(1f));
- g2d.setColor(GRID_COLOR);
- // 垂直线
- int x = config.getCellPadding();
- for (int i = 0; i < colWidths.length; i++) {
- x += colWidths[i];
- if (i < colWidths.length - 1) {
- g2d.drawLine(x, startY, x, y);
- }
- }
- // 外边框
- g2d.drawRect(1, startY - 1, totalWidth - 3, y - startY);
- }
- /**
- * 格式化单元格值
- * <p>根据字段类型将原始值转换为显示文本</p>
- */
- @SuppressWarnings("unchecked")
- private String formatCellValue(Object value, FieldDefinitionDTO fieldDef) {
- if (value == null) {
- return "";
- }
- String type = fieldDef.getType();
- if (type == null) {
- type = "";
- }
- try {
- switch (type) {
- case "NumberField":
- case "MoneyField":
- return formatNumber(value, type);
- case "DateField":
- case "DateTimeField":
- case "TimeField":
- return formatDate(value);
- case "EmployeeField":
- return formatEmployee(value);
- case "DepartmentField":
- return formatDepartment(value);
- case "AttachmentField":
- case "ImageUploadField":
- return formatAttachment(value);
- case "CheckboxField":
- case "MultiSelectField":
- return formatMultiValue(value);
- case "AddressField":
- return formatAddress(value);
- default:
- // 默认:如果是字符串直接返回,否则尝试 JSON 解析取 name/title
- if (value instanceof String) {
- return (String) value;
- }
- if (value instanceof Number) {
- return String.valueOf(value);
- }
- if (value instanceof Boolean) {
- return (Boolean) value ? "是" : "否";
- }
- // 尝试解析为 JSON 对象,提取常见的显示字段
- return extractDisplayName(value);
- }
- } catch (Exception e) {
- // 格式化失败,返回原始 toString
- return String.valueOf(value);
- }
- }
- private String formatNumber(Object value, String type) {
- if (value instanceof Number) {
- double d = ((Number) value).doubleValue();
- if (d == Math.floor(d)) {
- return String.valueOf((long) d);
- }
- if ("MoneyField".equals(type)) {
- return String.format("%.2f", d);
- }
- return String.valueOf(d);
- }
- return String.valueOf(value);
- }
- private String formatDate(Object value) {
- if (value instanceof Number) {
- long ts = ((Number) value).longValue();
- // 宜搭日期通常是毫秒时间戳
- return new SimpleDateFormat("yyyy-MM-dd").format(new Date(ts));
- }
- return String.valueOf(value);
- }
- private String formatEmployee(Object value) {
- // 宜搭 EmployeeField 返回值可能是 JSON 数组: [{"value":"xxx","label":"张三"}]
- if (value instanceof String) {
- String str = (String) value;
- try {
- if (str.startsWith("[")) {
- JSONArray arr = JSON.parseArray(str);
- StringBuilder sb = new StringBuilder();
- for (int i = 0; i < arr.size(); i++) {
- JSONObject obj = arr.getJSONObject(i);
- if (sb.length() > 0) sb.append(", ");
- sb.append(obj.getString("label"));
- }
- return sb.toString();
- }
- } catch (Exception ignored) {
- }
- return str;
- }
- return extractDisplayName(value);
- }
- private String formatDepartment(Object value) {
- return formatEmployee(value); // 结构类似
- }
- private String formatAttachment(Object value) {
- if (value instanceof String) {
- String str = (String) value;
- try {
- if (str.startsWith("[")) {
- JSONArray arr = JSON.parseArray(str);
- return arr.size() + " 个文件";
- }
- } catch (Exception ignored) {
- }
- }
- return String.valueOf(value);
- }
- private String formatMultiValue(Object value) {
- if (value instanceof String) {
- String str = (String) value;
- try {
- if (str.startsWith("[")) {
- JSONArray arr = JSON.parseArray(str);
- StringBuilder sb = new StringBuilder();
- for (int i = 0; i < arr.size(); i++) {
- if (sb.length() > 0) sb.append(", ");
- Object item = arr.get(i);
- if (item instanceof JSONObject) {
- sb.append(((JSONObject) item).getString("label"));
- } else {
- sb.append(String.valueOf(item));
- }
- }
- return sb.toString();
- }
- } catch (Exception ignored) {
- }
- return str;
- }
- if (value instanceof List) {
- return String.join(", ", (List<String>) value);
- }
- return String.valueOf(value);
- }
- private String formatAddress(Object value) {
- if (value instanceof String) {
- String str = (String) value;
- try {
- JSONObject obj = JSON.parseObject(str);
- // 宜搭地址格式: {"province":"xxx","city":"xxx","district":"xxx","address":"xxx"}
- StringBuilder sb = new StringBuilder();
- String[] keys = {"province", "city", "district", "address"};
- for (String key : keys) {
- String v = obj.getString(key);
- if (v != null && !v.isEmpty()) {
- sb.append(v);
- }
- }
- return sb.toString();
- } catch (Exception ignored) {
- }
- return str;
- }
- return String.valueOf(value);
- }
- /**
- * 从复杂对象中提取显示名称
- */
- private String extractDisplayName(Object value) {
- if (value instanceof Map) {
- Map<String, Object> map = (Map<String, Object>) value;
- // 尝试常见的字段名
- for (String key : new String[]{"label", "name", "title", "text", "value"}) {
- Object v = map.get(key);
- if (v != null) {
- return String.valueOf(v);
- }
- }
- }
- if (value instanceof List) {
- List<?> list = (List<?>) value;
- StringBuilder sb = new StringBuilder();
- for (Object item : list) {
- if (sb.length() > 0) sb.append(", ");
- sb.append(extractDisplayName(item));
- }
- return sb.toString();
- }
- return String.valueOf(value);
- }
- /**
- * 解析十六进制颜色字符串
- */
- private Color parseColor(String hex, Color defaultColor) {
- if (hex == null || hex.isEmpty()) {
- return defaultColor;
- }
- try {
- return Color.decode(hex);
- } catch (NumberFormatException e) {
- return defaultColor;
- }
- }
- }
|