- 核心新增: 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 图标预渲染工具
133 lines
5.2 KiB
Java
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;
|
|
}
|
|
}
|