click-captcha
Version:
Chinese Character Sequence Click Verification System
159 lines (158 loc) • 5.65 kB
JavaScript
;
Object.defineProperty(exports, "__esModule", { value: true });
exports.SvgBuilder = void 0;
/*
* @Author: Qing
* @Description:
* @Date: 2025-02-05 13:29:30
* @LastEditTime: 2025-07-24 14:43:50
*/
const random_1 = require("../utils/random");
const opentype = require("opentype.js");
const isDev = false;
class SvgBuilder {
/**
* 初始化字体
*/
static initializeFont(path) {
if (!this.font) {
const fontData = Buffer.from(path, "base64");
this.font = opentype.parse(fontData.buffer);
}
if (!this.hintFont) {
const fontData = Buffer.from(path, "base64");
this.hintFont = opentype.parse(fontData.buffer);
}
}
/**
* 生成干扰线
*/
static generateNoiseLine(width, height) {
const start = `${random_1.Random.int(1, 21)} ${random_1.Random.int(1, height - 1)}`;
const end = `${random_1.Random.int(width - 21, width - 1)} ${random_1.Random.int(1, height - 1)}`;
const mid1 = `${random_1.Random.int(width / 2 - 21, width / 2 + 21)} ${random_1.Random.int(1, height - 1)}`;
const mid2 = `${random_1.Random.int(width / 2 - 21, width / 2 + 21)} ${random_1.Random.int(1, height - 1)}`;
const color = random_1.Random.color();
return `<path d="M${start} C${mid1},${mid2},${end}" stroke="${color}" fill="none" opacity="0.5"/>`;
}
/**
* 字符转路径
*/
static charToPath(fontFamily, char, fontSize) {
if (!fontFamily) {
throw new Error("字体未初始化");
}
const path = fontFamily.getPath(char, 0, 0, fontSize);
const bbox = path.getBoundingBox();
return {
path: path.toPathData(3),
bbox,
};
}
/**
* 生成文字路径
*/
static generateText(charData, centerX, centerY) {
const { path, bbox } = charData;
// 计算包围盒的尺寸
const width = bbox.x2 - bbox.x1;
const height = bbox.y2 - bbox.y1;
// 计算偏移量(将左下角移动到中心)
const offsetX = centerX - width / 2 - bbox.x1;
const offsetY = centerY - height / 2 - bbox.y1;
// 变形
const rotate = random_1.Random.int(-30, 30);
const skewX = random_1.Random.int(-30, 30);
const scaleX = random_1.Random.float(0.8, 1.2);
const scaleY = random_1.Random.float(0.8, 1.2);
/* const rotate = 0;
const skewX = 0;
const scaleX = 1;
const scaleY = 1; */
// 随机颜色
const fillColor = random_1.Random.color();
const strokeColor = random_1.Random.color();
// 应用变换矩阵
return `
<g transform="
translate(${offsetX} ${offsetY})
rotate(${rotate})
skewX(${skewX})
scale(${scaleX} ${scaleY})
">
<path
d="${path}"
fill="${fillColor}"
stroke="${strokeColor}"
stroke-width="1"
/>
</g>
`;
}
/**
* 生成提示图片
*/
static buildHint(chars, options) {
this.initializeFont(options.font.fontPath);
const { hint } = options;
const { width, height } = hint.dimensions;
const { spacing, fontSize } = hint.font;
let svg = `<svg xmlns="http://www.w3.org/2000/svg" width="${width}" height="${height}" viewBox="0 0 ${width} ${height}">`;
// 背景
svg += `<rect width="100%" height="100%" fill="transparent" />`;
// 文字
chars.forEach(({ char }, index) => {
const pathData = this.charToPath(this.hintFont, char, fontSize);
const x = spacing + index * spacing;
const y = height / 2;
// 计算包围盒偏移量
const width = pathData.bbox.x2 - pathData.bbox.x1;
const height2 = pathData.bbox.y2 - pathData.bbox.y1;
const offsetX = x - width / 2 - pathData.bbox.x1;
const offsetY = y - height2 / 2 - pathData.bbox.y1;
const rotate = random_1.Random.int(-34, 34);
// 应用位移变换
svg += `
<g transform="
translate(${offsetX} ${offsetY})
rotate(${rotate})
">
<path
d="${pathData.path}"
stroke-width="1"
fill="black"
/>
</g>
`;
});
svg += "</svg>";
return svg;
}
/**
* 生成完整的SVG
*/
static buildMain(chars, options) {
this.initializeFont(options.font.fontPath);
const { dimensions, effects } = options;
let svg = `<svg xmlns="http://www.w3.org/2000/svg" width="${dimensions.width}" height="${dimensions.height}" viewBox="0 0 ${dimensions.width} ${dimensions.height}">`;
// 背景
svg += `<rect width="100%" height="100%" fill="${effects.backgroundColor}"/>`;
// 干扰线
for (let i = 0; i < effects.noiseLines; i++) {
svg += this.generateNoiseLine(dimensions.width, dimensions.height);
}
// 文字
chars.forEach(({ char, coordinates: { x, y } }) => {
const pathData = this.charToPath(this.font, char, options.font.fontSize);
svg += this.generateText(pathData, x, y);
if (isDev) {
svg += `<circle cx="${x}" cy="${y}" r="3" fill="red" />`;
}
});
svg += "</svg>";
return svg;
}
}
exports.SvgBuilder = SvgBuilder;
SvgBuilder.font = null;
SvgBuilder.hintFont = null;