| | |
| | | /** |
| | | * 二维码标签图片工具类 |
| | | * <p> |
| | | * 生成"二维码 + 右侧可读文本"的合成图片:二维码只编码纯码值,文本叠加关键信息, |
| | | * 生成"二维码 + 右侧可读文本"的合成图片:二维码编码内容由调用方传入(配置了溯源地址时为 |
| | | * 带 code 参数的溯源 URL,未配置时为纯码值),右侧叠加关键信息文本, |
| | | * 即使数据库丢失也能从打印图片还原关键信息。 |
| | | */ |
| | | @Slf4j |
| | |
| | | /** 字段间距 */ |
| | | private static final int FIELD_GAP = 6; |
| | | |
| | | /** 中文字体候选(按优先级),避免 Linux headless 下文字变方框 */ |
| | | /** |
| | | * 中文字体候选(按优先级)。 |
| | | * <p> |
| | | * 只允许收录可渲染中文的字体:纯拉丁字体(如 Arial)一旦入选,中文会全部渲染成方框, |
| | | * 且导出过程不报错,属于最隐蔽的一类事故。 |
| | | */ |
| | | private static final String[] FONT_CANDIDATES = { |
| | | "Noto Sans CJK SC", "WenQuanYi Zen Hei", "Microsoft YaHei", "SimSun", "Arial" |
| | | "Noto Sans CJK SC", "Source Han Sans SC", "WenQuanYi Zen Hei", "PingFang SC", |
| | | "Hiragino Sans GB", "Microsoft YaHei UI", "Microsoft YaHei", "SimSun" |
| | | }; |
| | | |
| | | /** 中文字形探针字符(用于探测字体是否具备中文字形) */ |
| | | private static final char CJK_PROBE = '袋'; |
| | | |
| | | /** 解析出的中文字体族名;为 null 表示当前环境没有任何可渲染中文的字体 */ |
| | | private static final String FONT_NAME = resolveFontName(); |
| | | |
| | | /** |
| | |
| | | * @return 合成图片 |
| | | */ |
| | | public static BufferedImage createQrLabel(String code, LinkedHashMap<String, String> keyValues) { |
| | | // 无中文字体时直接失败,避免导出中文为方框的标签 |
| | | assertCjkFontAvailable(); |
| | | QrConfig config = new QrConfig(); |
| | | config.setErrorCorrection(ErrorCorrectionLevel.H); // 打印标签易磨损,用高容错 |
| | | config.setMargin(1); // 保证静区,扫码率 |
| | |
| | | |
| | | // ==================== 私有方法 ==================== |
| | | |
| | | /** |
| | | * 解析当前环境可用的中文字体族名。 |
| | | * <p> |
| | | * 不能按字体族名做精确匹配:同一字体在不同系统上的族名可能不同(例如 Windows 上实际注册为 |
| | | * {@code Microsoft YaHei UI} 而非 {@code Microsoft YaHei}),精确匹配落空后会一路退到纯拉丁字体, |
| | | * 中文即渲染为方框。因此这里统一用 {@link Font#canDisplay(char)} 探测中文字形。 |
| | | * |
| | | * @return 可渲染中文的字体族名;无可用字体时返回 {@code null} |
| | | */ |
| | | private static String resolveFontName() { |
| | | Set<String> families = Set.of(GraphicsEnvironment.getLocalGraphicsEnvironment().getAvailableFontFamilyNames()); |
| | | for (String candidate : FONT_CANDIDATES) { |
| | | if (families.contains(candidate)) { |
| | | if (families.contains(candidate) && canDisplayCjk(candidate)) { |
| | | return candidate; |
| | | } |
| | | } |
| | | return Font.SANS_SERIF; |
| | | // 逻辑字体由 JDK 负责字形回退,多数环境可渲染中文 |
| | | if (canDisplayCjk(Font.SANS_SERIF)) { |
| | | return Font.SANS_SERIF; |
| | | } |
| | | return null; |
| | | } |
| | | |
| | | /** |
| | | * 探测指定字体族能否渲染中文 |
| | | */ |
| | | private static boolean canDisplayCjk(String family) { |
| | | return new Font(family, Font.PLAIN, FONT_SIZE).canDisplay(CJK_PROBE); |
| | | } |
| | | |
| | | /** |
| | | * 校验当前环境存在可渲染中文的字体,否则抛出异常。 |
| | | * <p> |
| | | * 供启动期调用:宁可在启动时失败,也不能把中文渲染成方框的标签交付印刷。 |
| | | */ |
| | | public static void assertCjkFontAvailable() { |
| | | if (FONT_NAME == null) { |
| | | throw new IllegalStateException("当前环境没有任何可渲染中文的字体,二维码标签的中文将显示为方框," |
| | | + "拒绝生成标签。请在服务器安装中文字体(如 fonts-noto-cjk)后重启。"); |
| | | } |
| | | } |
| | | |
| | | private static int measureTextHeight(LinkedHashMap<String, String> keyValues, Font labelFont, Font valueFont) { |