一、代码引用
首先,如果你想直接用,可以直接用下面这个类。
可以调用CaptchaGenerator类中的captchaCreateImage方法,其方法参数列表为(int width, int height, int captchaLength, String[] returnCaptcha, int degree),方法返回验证码图像。
width -----------------------文字验证码图片的宽度
height-----------------------文字验证码图片的高度
captchaLength-----------文字验证码的长度
returnCaptcha-----------返回的文字验证码
degree---------------------干扰的程度(1-5,不在范围默认为5)
一 | ![]() |
二 | ![]() |
三 | ![]() |
四 | ![]() |
五 | ![]() |
代码引用处
import java.awt.*; // 导入 AWT 图形库
import java.awt.geom.AffineTransform; // 导入用于执行几何变换的类
import java.awt.image.BufferedImage; // 导入用于处理图像的类
import java.util.Random; // 导入随机数生成器类
import java.util.concurrent.ThreadLocalRandom;
public class CaptchaGenerator {
// 创建随机数生成器
private final Random random = ThreadLocalRandom.current();
// 创建验证码的方法
private String createCaptcha(int length) {
// 定义可选字符池(不包含容易混淆的字符0和O等)
String charPool = "ABCDEFGHJKLMNPQRSTUVWXYZabcdefghjkmnpqrstuvwxyz23456789";
StringBuilder result = new StringBuilder(); // 用于构建验证码字符串
// 随机选择字符生成验证码
for (int i = 0; i < length; i++) {
result.append(charPool.charAt(random.nextInt(charPool.length()))); // 从字符池中随机选择字符
}
return result.toString(); // 返回生成的验证码字符串
}
// 创建颜色的方法,生成指定范围内随机颜色
private Color createColor(int min, int max) {
int r = min + random.nextInt(max - min+1); // 随机生成红色分量
int g = min + random.nextInt(max - min+1); // 随机生成绿色分量
int b = min + random.nextInt(max - min+1); // 随机生成蓝色分量
return new Color(r, g, b); // 创建并返回颜色对象
}
// 添加干扰元素的方法
private void addInterference(Graphics2D g, int degree, int width, int height) {
// 确保干扰元素的数量在0到5之间
degree = (degree <= 0 || degree > 5) ? 5 : degree;
// 根据度数生成干扰元素
for (int i = 0; i < degree * 20; i++) {
int x = random.nextInt(width); // 随机生成x坐标
int y = random.nextInt(height); // 随机生成y坐标
// 随机选择干扰元素的颜色
Color color = (random.nextBoolean()) ? createColor(0, 255) : ((random.nextBoolean()) ? Color.WHITE : Color.BLACK);
g.setColor(color); // 设置画笔颜色
// 随机选择干扰元素的类型并画出
switch (random.nextInt(3)) {
case 0 -> g.fillOval(x, y, random.nextInt(3) + 1, random.nextInt(3) + 1); // 画圆点
case 1 -> {
int change = random.nextInt(3); // 随机变化值
// 画出线条构成的随机图形
g.drawLine(x, y, x + change, y + change);
g.drawLine(x + change, y + change, x + 2 * change, y);
g.drawLine(x, y, x + change, y - change);
g.drawLine(x + change, y - change, x + 2 * change, y);
}
case 2 -> g.drawLine(x, y, x + random.nextInt(5) + 1, y + random.nextInt(5) + 1); // 画线
}
}
// 生成更多随机干扰线
for (int i = 0; i < 5 * degree; i++) {
Color color = (random.nextBoolean()) ? createColor(0, 255) : ((random.nextBoolean()) ? Color.WHITE : Color.BLACK);
g.setColor(color);
// 随机生成干扰线的起止点
g.drawLine(random.nextInt(width), random.nextInt(height), random.nextInt(width), random.nextInt(height));
}
}
// 创建验证码图像的方法
private BufferedImage createImage(int width, int height, int captchaLength, String[] returnCaptcha, int degree) {
// 创建新图像
BufferedImage image = new BufferedImage(width, height, BufferedImage.TYPE_INT_RGB);
Graphics2D g = image.createGraphics(); // 获取图形上下文
g.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON); // 启用抗锯齿
// 创建背景色
Color backgroundColor = createColor(0, 255);
g.setColor(backgroundColor);
g.fillRect(0, 0, width, height); // 填充背景
// 生成验证码
String captcha = createCaptcha(captchaLength);
returnCaptcha[0] = captcha; // 将生成的验证码存入数组
// 设置字体大小和干扰详细参数
int fontSize = (int) (height * 0.5);
AffineTransform at = new AffineTransform(); // 创建变换对象
at.shear(random.nextDouble() * 0.4 - 0.2, random.nextDouble() * 0.4 - 0.2); // 随机倾斜变换
Font font = new Font(Font.SANS_SERIF, Font.BOLD, fontSize); // 创建字体对象
font = font.deriveFont(at); // 生成倾斜字体
addInterference(g, degree, width, height); // 添加干扰元素
Color fontColor, prevFontColor = null; // 字体颜色和前一个字体颜色
int fontX, fontY, fontWidth, changeX; // 不同的坐标和宽度
FontMetrics fontMetrics = g.getFontMetrics(); // 字体度量
fontWidth = fontMetrics.stringWidth(captcha) + (captchaLength - 1) * (int) (width * 0.05); // 计算验证码的宽度
fontX = (width - fontWidth) / 2; // 计算x坐标以居中对齐
fontY = (height - (fontMetrics.getAscent() + fontMetrics.getDescent())) / 2 + fontMetrics.getAscent(); // 计算y坐标
double tempX = fontX; // 保存当前x坐标
// 逐个绘制验证码字符
for (int i = 0; i < captchaLength; i++) {
// 根据背景色的亮度生成对比度较强的字体颜色
fontColor = (backgroundColor.getRed() > 180) ? createColor(0, 160) : createColor(200, 255);
int maxAttempts = 10,count=0;//设置最大循环数,避免死循环
// 确保字体颜色与前一个字体颜色不相近
while (prevFontColor != null&&count++<maxAttempts) {
double brightness = (backgroundColor.getRed() * 299 + backgroundColor.getBlue() * 114 + backgroundColor.getGreen() * 587) / 1000.0;
fontColor = (brightness > 128) ? createColor(0, 128) : createColor(128, 255); // 确定字体颜色
int dr = fontColor.getBlue() - prevFontColor.getBlue();
int dg = fontColor.getGreen() - prevFontColor.getGreen();
int db = fontColor.getRed() - prevFontColor.getRed();
prevFontColor = fontColor; // 更新前一个颜色
// 如果颜色差异大于亮度则退出循环
if (Math.sqrt(dr * dr + dg * dg + db * db) > brightness) break;
}
prevFontColor = fontColor; // 更新前一个颜色
g.setFont(font); // 设置当前字体
g.setColor(fontColor); // 设置当前字体颜色
// 随机旋转角度
int RotationAngle = random.nextInt(60) - 30;
g.rotate(Math.toRadians(RotationAngle), tempX, fontY); // 绕中心点旋转
// 绘制字符
g.drawString(String.valueOf(captcha.charAt(i)), (int) tempX, fontY);
g.rotate(-Math.toRadians(RotationAngle), tempX, fontY); // 逆旋转恢复状态
changeX = fontMetrics.stringWidth(String.valueOf(captcha.charAt(i))); // 获取当前字符宽度
tempX += (changeX + (int) (width * 0.05)); // 更新临时x坐标,为下一个字符准备空间
}
g.dispose(); // 释放图形上下文资源
if(captchaLength <= 0|| returnCaptcha[0].isEmpty()) {
throw new IllegalArgumentException("returnCaptcha array must be non-null and have at least one element");
}
return image; // 返回生成的图像
}
public BufferedImage captchaCreateImage(int width, int height, int captchaLength, String[] returnCaptcha, int degree){
return createImage(width, height, captchaLength, returnCaptcha, degree);
}
}
二、代码实现
1.生成验证码
由于0和O等字符容易混淆,因此需要去除这些字符。
private String createCaptcha(int length){
String charPool = "ABCDEFGHJKLMNPQRSTUVWXYZabcdefghjkmnpqrstuvwxyz23456789";
StringBuilder result = new StringBuilder();
for (int i = 0; i < length; i++) {
result.append(charPool.charAt(random.nextInt(charPool.length())));
}
return result.toString();
}
2.生成图片前的准备
(1)创建一个指定宽度、高度和类型的BufferedImage对象
BufferedImage image = new BufferedImage(width, height, BufferedImage.TYPE_INT_RGB);
(2)获取Graphics2D对象,用于绘制图像,并启用抗锯齿
Graphics2D g = image.createGraphics();
g.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON);
(3)填充背景颜色
Color backgroundColor = createColor(0, 255); // 随机生成背景颜色
g.setColor(backgroundColor); // 设置背景颜色
g.fillRect(0, 0, width, height); // 填充背景色
(4)获取验证码
String captcha = createCaptcha(captchaLength);
returnCaptcha[0]=captcha;
3.图片添加干扰
总体预览
private void addInterference(Graphics2D g,int degree,int width,int height){
degree=(degree <= 0||degree > 5)?5:degree;
for (int i = 0; i < degree*20; i++) {
int x=random.nextInt(width);
int y=random.nextInt(height);
Color color=(random.nextBoolean())?createColor(0,255):((random.nextBoolean())?Color.WHITE:Color.BLACK);
g.setColor(color);
switch (random.nextInt(3)){
case 0-> g.fillOval(x,y, random.nextInt(3)+1,random.nextInt(3)+1);
case 1-> {
int change=random.nextInt(3);
g.drawLine(x,y,x+change,y+change);
g.drawLine(x+change,y+change,x+2*change,y);
g.drawLine(x,y,x+change,y-change);
g.drawLine(x+change,y-change,x+2*change,y);
}
case 2->g.drawLine(x,y,x+random.nextInt(5)+1,y+random.nextInt(5)+1);
}
}
for(int i=0;i<5*degree;i++){
Color color=(random.nextBoolean())?createColor(0,255):((random.nextBoolean())?Color.WHITE:Color.BLACK);
g.setColor(color);
g.drawLine(random.nextInt(width),random.nextInt(height),random.nextInt(width),random.nextInt(height));
}
}
(1)确保程度合法
degree=(degree <= 0||degree > 5)?5:degree;
(2)生成噪点
a.随机坐标
int x=random.nextInt(width);
int y=random.nextInt(height);
b.颜色设置
要么彩色,要么黑白。
Color color=(random.nextBoolean())?createColor(0,255):((random.nextBoolean())?Color.WHITE:Color.BLACK);
g.setColor(color);
c.噪点样式选择
总体预览
switch (random.nextInt(3)){
case 0-> g.fillOval(x,y, random.nextInt(3)+1,random.nextInt(3)+1);
case 1-> {
int change=random.nextInt(3);
g.drawLine(x,y,x+change,y+change);
g.drawLine(x+change,y+change,x+2*change,y);
g.drawLine(x,y,x+change,y-change);
g.drawLine(x+change,y-change,x+2*change,y);
}
case 2->g.drawLine(x,y,x+random.nextInt(5)+1,y+random.nextInt(5)+1);
}
1.椭圆形
case 0-> g.fillOval(x,y, random.nextInt(3)+1,random.nextInt(3)+1);
2.菱形
case 1-> {
int change=random.nextInt(3);
g.drawLine(x,y,x+change,y+change);
g.drawLine(x+change,y+change,x+2*change,y);
g.drawLine(x,y,x+change,y-change);
g.drawLine(x+change,y-change,x+2*change,y);
}
3.随机短线段
case 2->g.drawLine(x,y,x+random.nextInt(5)+1,y+random.nextInt(5)+1);
4.验证码内容处理
(1)处理字体
总体预览
int fontSize=(int)(height*0.5);
AffineTransform at = new AffineTransform();
at.shear(random.nextDouble()*0.4-0.2,random.nextDouble()*0.4-0.2);
Font font=new Font("微软雅黑", Font.BOLD,fontSize);
font=font.deriveFont(at);
g.setFont(font);
a.设置字体大小
int fontSize=(int)(height*0.5);
b.创建AffineTransform对象,用于几何变换,将字体扭曲,程度为[-0.2,0.2]。
AffineTransform at = new AffineTransform();
at.shear(random.nextDouble()*0.4-0.2,random.nextDouble()*0.4-0.2);
c.设置字体样式,并应用扭曲
Font font=new Font("微软雅黑", Font.BOLD,fontSize);
font=font.deriveFont(at);
(2)字符居中处理
总体预览
int fontX,fontY,fontWidth,changeX;
FontMetrics fontMetrics = g.getFontMetrics();
fontWidth=fontMetrics.stringWidth(captcha)+(captchaLength-1)*(int)(width*0.05);
fontX=(width-fontWidth)/2;
fontY=(height - (fontMetrics.getAscent() + fontMetrics.getDescent())) / 2 + fontMetrics.getAscent();
a.获取横坐标
fontMetrics.stringWidth(captcha)是所有的字符的总宽度。
(captchaLength-1)*(int)(width*0.05)是字符之间的总间隔大小,其中(captchaLength-1)为间隔数,(width*0.05)为间隔大小。
fontWidth=fontMetrics.stringWidth(captcha)+(captchaLength-1)*(int)(width*0.05);
fontX=(width-fontWidth)/2;
b.获取纵坐标
FontMetrics fontMetrics = g.getFontMetrics();
fontY=(height - (fontMetrics.getAscent() + fontMetrics.getDescent())) / 2 + fontMetrics.getAscent();
fontMetrics.getAscent():基线到字体最高点的距离
fontMetrics.getDescent():基线到字体最低点的距离
怎么理解?
FontMetrics
类提供了关键参数:
Ascent:基线到字体最高点的距离(如字母"h"的顶部)。
Descent:基线到字体最低点的距离(如字母"g"的尾部)。
Leading:行间距(通常较小)。
Height:总高度(
Ascent + Descent + Leading
)。
一、将文字想象成一个“盒子”
假设每个字符是一个矩形盒子,其高度由三部分组成:
Ascent(上升) :基线(baseline)到盒子顶部的距离(如字母"h"的顶部)。
Descent(下降) :基线到盒子底部的距离(如字母"g"的尾部)。
Leading(行间距) :盒子下方预留的空白(通常很小,可暂时忽略)。
二、Java绘图的“基线对齐”
当调用 g.drawString(text, x, y)
时:
参数
y
是基线的位置,而非文字区域的顶部或中心。如果直接将画布中点(
height/2
)作为基线位置,文字会整体偏下,因为Ascent部分会向上延伸,而Descent部分会向下延伸。
画布顶部(y=50)
| |
| |
| —————————— ← Ascent(y=35)
|
| —————————— ← 基线(y=30)
|
| —————————— ← 画布中心(y=25)
| ▲
| |
| ▼
| —————————— ← Descent(y=15)
| |
| |
画布底部(y=0)
(3)字符颜色处理
总体预览
fontColor=(backgroundColor.getRed()>180)?createColor(0,160):createColor(200,255);
int maxAttempts = 10,count=0;
while (prevFontColor!=null&&count++<maxAttempts) {
double brightness=(backgroundColor.getRed()*299+backgroundColor.getBlue()*114+backgroundColor.getGreen()*587)/1000.0;
fontColor=(brightness>128)?createColor(0,128):createColor(128,255);
int dr=fontColor.getBlue()-prevFontColor.getBlue();
int dg=fontColor.getGreen()-prevFontColor.getGreen();
int db=fontColor.getRed()-prevFontColor.getRed();
prevFontColor=fontColor;
if(Math.sqrt(dr*dr+dg*dg+db*db)>brightness)break;
}
prevFontColor=fontColor;
g.setColor(fontColor);
a.随机颜色
避免与背景颜色相近。
fontColor=(backgroundColor.getRed()>180)?createColor(0,160):createColor(200,255);
b.处理相邻颜色相近与对比度不明显问题
总体预览
while (prevFontColor!=null&&count++<maxAttempts) {
double brightness=(backgroundColor.getRed()*299+backgroundColor.getBlue()*114+backgroundColor.getGreen()*587)/1000.0;
fontColor=(brightness>128)?createColor(0,128):createColor(128,255);
int dr=fontColor.getBlue()-prevFontColor.getBlue();
int dg=fontColor.getGreen()-prevFontColor.getGreen();
int db=fontColor.getRed()-prevFontColor.getRed();
prevFontColor=fontColor;
if(Math.sqrt(dr*dr+dg*dg+db*db)>brightness)break;
}
prevFontColor=fontColor;
1.颜色差异检测机制(欧氏距离)
颜色距离公式:
使用欧氏距离公式计算颜色差异:
\Delta = \sqrt{(R_1-R_2)^2 + (G_1-G_2)^2 + (B_1-B_2)^2}
该公式能综合衡量RGB三个通道的差异,更符合人眼对颜色差异的感知
2.背景对比度优化(YIQ模型)
YIQ亮度模型:
使用公式:Y = 0.299R + 0.587G + 0.114B
该模型更贴近人眼对亮度的敏感度(绿色权重最高,蓝色最低)。
对比度标准:
若背景亮度
Y > 128
(亮色背景),选择深色字体(如黑色、深蓝)若
Y ≤ 128
(暗色背景),选择浅色字体(如白色、亮黄)综合效果
抗机器识别:
颜色差异和色相跳跃破坏OCR工具的颜色聚类算法,使其难以分割相邻字符。
边缘描边干扰轮廓识别算法。
人类可读性:
高对比度确保文字清晰,符合人眼对亮度、色相的敏感特性。
随机旋转和扭曲保留验证码的防机器特性,但不会影响人类阅读。
(4)字符旋转处理
总体预览
int RotationAngle=random.nextInt(60)-30;
g.rotate(Math.toRadians(RotationAngle),tempX,fontY);
g.drawString(String.valueOf(captcha.charAt(i)),(int)tempX,fontY);
g.rotate(-Math.toRadians(RotationAngle),tempX,fontY);
changeX=fontMetrics.stringWidth(String.valueOf(captcha.charAt(i)));
tempX+=(changeX+(int)(width*0.05));
a.随机角度[-30°,30°]
int RotationAngle=random.nextInt(60)-30;
b.旋转字符
1.旋转画布
g.rotate(Math.toRadians(RotationAngle),tempX,fontY);
g.rotate(double theta, double x, double y)
方法用于旋转当前的Graphics
上下文,影响之后的绘图操作。Math.toRadians(RotationAngle)
:将角度从度转换为弧度,因为 Java 的rotate
方法需要以弧度为单位的旋转角度。tempX
和fontY
:这里指定了旋转中心的坐标。tempX
:是文本的当前 x 坐标,通常根据文本的长度动态计算,使得字符绘制在适当的位置。fontY
:是文本的 y 坐标,通常是一个固定值,确保文本在画布的某一高度。
通过这行代码,当前画布将围绕指定的 (tempX, fontY)
点旋转,旋转角度为 RotationAngle
。
2.绘制字符
g.drawString(String.valueOf(captcha.charAt(i)),(int)tempX,fontY);
g.drawString(String str, int x, int y)
方法用于在画布上绘制字符串。String.valueOf(captcha.charAt(i))
:从名为captcha
的字符串中获取索引为i
的字符,并将其转换为字符串(通常可以直接使用captcha.charAt(i)
)。(int)tempX
和fontY
:这是绘制文本的坐标。tempX
:是在之前计算的 x 坐标,代表文本在水平方向上的位置。fontY
:是文本在垂直方向上的固定高度。
这行代码则在旋转后的画布上绘制当前字符。
3.恢复画布的旋转状态
g.rotate(-Math.toRadians(RotationAngle),tempX,fontY);
这行代码用于撤销之前的旋转操作。
-Math.toRadians(RotationAngle)
:使用负的角度进行旋转,与之前的旋转相反。tempX
和fontY
:依然是旋转中心的坐标。
这个步骤确保在绘制完当前字符后,画布的状态返回到原始位置,方便后续绘制其他字符时不会受到影响。
c.绘制字符位置处理
changeX=fontMetrics.stringWidth(String.valueOf(captcha.charAt(i)));
tempX+=(changeX+(int)(width*0.05));
1. 计算字符宽度
captcha.charAt(i)
:从captcha
字符串中获取索引i
处的字符。这是当前需要绘制的字符。String.valueOf(...)
:将获取的字符转换为字符串。虽然在 Java 中,char
类型可以直接用于字符串操作,但使用String.valueOf
方法能确保我们在需要时转换为字符串类型。fontMetrics.stringWidth(...)
:这个方法是FontMetrics
类中的一个方法,它可以返回指定字符串在当前字体下的宽度(以像素为单位)。也就是说,它计算出当前正在使用的字体绘制String.valueOf(captcha.charAt(i))
这一个字符所需要的宽度。changeX
:这是一个变量,用于存储当前字符的宽度。这将被用来调整下一个字符的绘制位置。
2.更新绘制位置
tempX
:这是一个变量,表示当前字符在绘图上下文中的 x 坐标。它记录了下一个字符应该绘制的 horizontal (水平方向) 位置。changeX
:上面计算获得的当前字符宽度。(int)(width * 0.05)
:这部分代码用于在字符之间添加一定的间距。在这里,width
是整张图片的宽度,而width * 0.05
表示取宽度的 5%。通过将结果转换为int
,确保我们将这个浮点值作为整数处理,适配绘图 API 对坐标的要求。+=
:这个操作符用于将当前的tempX
值加上当前字符的宽度和额外的间距,然后更新tempX
的值。这一步确保下一个字符绘制时不会重叠,而是位于上一个字符的右侧,并且留有一定的间隙。