UNPKG

click-captcha

Version:

Chinese Character Sequence Click Verification System

159 lines (158 loc) 5.65 kB
"use strict"; 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;