Files
tianai-captcha-enhanced/tools/icon-render/RenderIcons.java
T
abcv7 dbe9b58b0c feat: 增强版验证码全套实现 — 16种类型生成器/加密/风控/站点管理/平台后端/前端SDK/UI
- 核心新增: ICON_CLICK(PNG图标方案,预渲染资源)/SCRATCH/JIGSAW/CURVE_SLIDER(V1-V3)/ANGLE/CURVE_DRAW/WORD_ORDER_CLICK/PROOF_OF_WORK 等生成器
- 图标点选: classpath PNG 加载替代运行时字体渲染(Linux容器无emoji字体),提示条图作为 templateImage 返回前端
- 新增模块: crypto(AES+RSA)/obfuscator(背景乱序/噪声/扭曲)/risk(风控/IP黑名单/限流)/site(站点管理)/ml(轨迹规则引擎)
- 平台后端: 站点管理/验证码API(generate/verify/secondary-verify)/统计/ML轨迹学习
- 前端SDK: TPCaptcha兼容,支持全部新型号渲染与交互
- 工具: tools/icon-render 图标预渲染工具
2026-08-26 09:58:15 +08:00

133 lines
5.2 KiB
Java

import javax.imageio.ImageIO;
import java.awt.*;
import java.awt.image.BufferedImage;
import java.io.File;
import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.util.LinkedHashMap;
import java.util.Map;
/**
* One-off tool: pre-render emoji icons to PNG assets for IconClickImageCaptchaGenerator.
* Must run on a machine that has a color/outline emoji font (e.g. Windows Segoe UI Emoji).
* Output: META-INF/captcha-icons/<name>.png (96x96, white backing disc + glyph).
*
* Usage: java tools/icon-render/RenderIcons.java <output-dir>
*/
public class RenderIcons {
private static final int CANVAS = 96;
private static final float GLYPH_SIZE = 60f;
private static final Map<String, String> ICONS = new LinkedHashMap<>() {{
// fruits
put("apple", "\uD83C\uDF4E");
put("banana", "\uD83C\uDF4C");
put("cherry", "\uD83C\uDF52");
put("grapes", "\uD83C\uDF47");
put("lemon", "\uD83C\uDF4B");
put("orange", "\uD83C\uDF4A");
put("strawberry", "\uD83C\uDF53");
put("watermelon", "\uD83C\uDF49");
// animals
put("dog", "\uD83D\uDC36");
put("cat", "\uD83D\uDC31");
put("mouse", "\uD83D\uDC2D");
put("hamster", "\uD83D\uDC39");
put("rabbit", "\uD83D\uDC30");
put("fox", "\uD83E\uDD8A");
put("bear", "\uD83D\uDC3B");
put("panda", "\uD83D\uDC28");
// sports
put("soccer", "\u26BD");
put("basketball", "\uD83C\uDFC0");
put("tennis", "\uD83C\uDFBE");
put("volleyball", "\uD83C\uDFD0");
put("billiards", "\uD83C\uDFB1");
put("pingpong", "\uD83C\uDFD3");
put("trophy", "\uD83C\uDFC6");
put("boxing", "\uD83E\uDD4A");
// vehicles
put("car", "\uD83D\uDE97");
put("taxi", "\uD83D\uDE95");
put("suv", "\uD83D\uDE99");
put("bus", "\uD83D\uDE8C");
put("racecar", "\uD83C\uDFCE");
put("police", "\uD83D\uDE93");
put("ambulance", "\uD83D\uDE91");
put("firetruck", "\uD83D\uDE92");
// tech
put("phone", "\uD83D\uDCF1");
put("laptop", "\uD83D\uDCBB");
put("desktop", "\uD83D\uDDA5");
put("printer", "\uD83D\uDDA8");
put("keyboard", "\u2328");
put("computer-mouse", "\uD83D\uDDB1");
put("cd", "\uD83D\uDCBF");
put("camera", "\uD83D\uDCF7");
}};
public static void main(String[] args) throws IOException {
Path outDir = Paths.get(args.length > 0 ? args[0] : ".");
Files.createDirectories(outDir);
GraphicsEnvironment ge = GraphicsEnvironment.getLocalGraphicsEnvironment();
Font emojiFont = pickFont(ge);
System.out.println("Using font: " + emojiFont.getFontName());
int failures = 0;
for (Map.Entry<String, String> e : ICONS.entrySet()) {
BufferedImage img = render(e.getValue(), emojiFont);
File out = outDir.resolve(e.getKey() + ".png").toFile();
ImageIO.write(img, "png", out);
if (emojiFont.canDisplayUpTo(e.getValue()) != -1) {
System.out.println("[WARN] partial glyph coverage: " + e.getKey());
failures++;
}
System.out.println("OK " + out.getName());
}
System.out.println("Done. failures(warn)=" + failures);
}
private static Font pickFont(GraphicsEnvironment ge) {
for (String name : new String[]{"Segoe UI Emoji", "Apple Color Emoji", "Noto Color Emoji", "EmojiOne"}) {
for (Font f : ge.getAllFonts()) {
if (f.getFontName().equalsIgnoreCase(name)) {
return f.deriveFont(Font.PLAIN, GLYPH_SIZE);
}
}
}
System.out.println("[WARN] no known emoji font found, falling back to Dialog");
return new Font(Font.DIALOG, Font.PLAIN, (int) GLYPH_SIZE);
}
private static BufferedImage render(String emoji, Font font) {
BufferedImage img = new BufferedImage(CANVAS, CANVAS, BufferedImage.TYPE_INT_ARGB);
Graphics2D g = img.createGraphics();
g.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON);
g.setRenderingHint(RenderingHints.KEY_TEXT_ANTIALIASING, RenderingHints.VALUE_TEXT_ANTIALIAS_ON);
g.setRenderingHint(RenderingHints.KEY_FRACTIONALMETRICS, RenderingHints.VALUE_FRACTIONALMETRICS_ON);
g.setRenderingHint(RenderingHints.KEY_INTERPOLATION, RenderingHints.VALUE_INTERPOLATION_BILINEAR);
try {
// translucent white backing disc -> keeps icons readable on any photo background
g.setColor(new Color(255, 255, 255, 216));
g.fillOval(2, 2, CANVAS - 5, CANVAS - 5);
g.setColor(new Color(0, 0, 0, 46));
g.setStroke(new BasicStroke(2f));
g.drawOval(3, 3, CANVAS - 7, CANVAS - 7);
g.setColor(Color.BLACK);
g.setFont(font);
FontMetrics fm = g.getFontMetrics();
int x = (CANVAS - fm.stringWidth(emoji)) / 2;
int y = (CANVAS - fm.getHeight()) / 2 + fm.getAscent();
g.drawString(emoji, x, y);
} finally {
g.dispose();
}
return img;
}
}