691 lines
24 KiB
TypeScript
691 lines
24 KiB
TypeScript
import type {
|
|
CaptchaConcreteImageStyle,
|
|
CaptchaGeneratorContext,
|
|
} from "../types.ts";
|
|
import {
|
|
createImage,
|
|
drawGlyph,
|
|
drawLine,
|
|
fillCircle,
|
|
fillRect,
|
|
pngDataUri,
|
|
setPixel,
|
|
type Rgba,
|
|
type RgbaImage,
|
|
} from "./png.ts";
|
|
|
|
const LIGHT_BACKGROUND: Rgba = [248, 250, 252, 255];
|
|
const ALT_BACKGROUND: Rgba = [241, 245, 249, 255];
|
|
const INK: Rgba = [15, 23, 42, 255];
|
|
const ACCENTS: readonly Rgba[] = [
|
|
[30, 64, 175, 235],
|
|
[126, 34, 206, 235],
|
|
[190, 24, 93, 230],
|
|
[15, 118, 110, 230],
|
|
[194, 65, 12, 230],
|
|
];
|
|
|
|
interface TextLayout {
|
|
scale: number;
|
|
spacing: number;
|
|
startX: number;
|
|
startY: number;
|
|
textWidth: number;
|
|
textHeight: number;
|
|
}
|
|
|
|
function clamp(value: number, minimum: number, maximum: number): number {
|
|
return Math.max(minimum, Math.min(maximum, value));
|
|
}
|
|
|
|
function disturbanceRatio(context: CaptchaGeneratorContext): number {
|
|
return clamp((context.disturbance - 25) / 50, 0, 1);
|
|
}
|
|
|
|
function randomColor(
|
|
context: CaptchaGeneratorContext,
|
|
minimum = 70,
|
|
maximum = 205,
|
|
alpha = 100,
|
|
): Rgba {
|
|
return [
|
|
context.randomInt(minimum, maximum),
|
|
context.randomInt(minimum, maximum),
|
|
context.randomInt(minimum, maximum),
|
|
alpha,
|
|
];
|
|
}
|
|
|
|
function rawPixel(image: RgbaImage, x: number, y: number): readonly [number, number, number, number] {
|
|
const px = clamp(Math.round(x), 0, image.width - 1);
|
|
const py = clamp(Math.round(y), 0, image.height - 1);
|
|
const index = (py * image.width + px) * 4;
|
|
return [
|
|
image.data[index]!,
|
|
image.data[index + 1]!,
|
|
image.data[index + 2]!,
|
|
image.data[index + 3]!,
|
|
];
|
|
}
|
|
|
|
function putRawPixel(
|
|
image: RgbaImage,
|
|
x: number,
|
|
y: number,
|
|
color: readonly [number, number, number, number],
|
|
): void {
|
|
const px = Math.round(x);
|
|
const py = Math.round(y);
|
|
if (px < 0 || py < 0 || px >= image.width || py >= image.height) return;
|
|
const index = (py * image.width + px) * 4;
|
|
image.data[index] = color[0];
|
|
image.data[index + 1] = color[1];
|
|
image.data[index + 2] = color[2];
|
|
image.data[index + 3] = color[3];
|
|
}
|
|
|
|
function cloneImage(image: RgbaImage): RgbaImage {
|
|
return { width: image.width, height: image.height, data: image.data.slice() };
|
|
}
|
|
|
|
function replaceImage(target: RgbaImage, source: RgbaImage): void {
|
|
target.data.set(source.data);
|
|
}
|
|
|
|
function layoutFor(text: string, style: CaptchaConcreteImageStyle): TextLayout {
|
|
const width = 300;
|
|
const height = 104;
|
|
const scale = text.length > 9 ? 3 : text.length > 7 ? 4 : 5;
|
|
const glyphWidth = scale * 5;
|
|
const normalSpacing = scale + 3;
|
|
const spacing = style === "collision"
|
|
? Math.max(-Math.floor(scale * 0.45), -2)
|
|
: style === "cross-shadow"
|
|
? scale
|
|
: normalSpacing;
|
|
const textWidth = text.length * glyphWidth + Math.max(0, text.length - 1) * spacing;
|
|
const textHeight = scale * 7;
|
|
return {
|
|
scale,
|
|
spacing,
|
|
startX: Math.max(12, Math.floor((width - textWidth) / 2)),
|
|
startY: Math.floor((height - textHeight) / 2),
|
|
textWidth,
|
|
textHeight,
|
|
};
|
|
}
|
|
|
|
function drawCircleOutline(
|
|
image: RgbaImage,
|
|
centerX: number,
|
|
centerY: number,
|
|
radius: number,
|
|
color: Rgba,
|
|
thickness = 1,
|
|
): void {
|
|
const steps = Math.max(24, Math.round(radius * 8));
|
|
for (let index = 0; index < steps; index++) {
|
|
const angle = (index / steps) * Math.PI * 2;
|
|
const x = centerX + Math.cos(angle) * radius;
|
|
const y = centerY + Math.sin(angle) * radius;
|
|
fillCircle(image, x, y, Math.max(0.75, thickness / 2), color);
|
|
}
|
|
}
|
|
|
|
function drawDashedLine(
|
|
image: RgbaImage,
|
|
x0: number,
|
|
y0: number,
|
|
x1: number,
|
|
y1: number,
|
|
color: Rgba,
|
|
dash = 5,
|
|
gap = 4,
|
|
thickness = 1,
|
|
): void {
|
|
const distance = Math.hypot(x1 - x0, y1 - y0);
|
|
if (distance <= 0) return;
|
|
const dx = (x1 - x0) / distance;
|
|
const dy = (y1 - y0) / distance;
|
|
for (let offset = 0; offset < distance; offset += dash + gap) {
|
|
const end = Math.min(distance, offset + dash);
|
|
drawLine(
|
|
image,
|
|
x0 + dx * offset,
|
|
y0 + dy * offset,
|
|
x0 + dx * end,
|
|
y0 + dy * end,
|
|
color,
|
|
thickness,
|
|
);
|
|
}
|
|
}
|
|
|
|
function addDots(
|
|
image: RgbaImage,
|
|
context: CaptchaGeneratorContext,
|
|
count: number,
|
|
alpha: number,
|
|
minimum = 75,
|
|
maximum = 220,
|
|
radius = 0,
|
|
): void {
|
|
for (let index = 0; index < count; index++) {
|
|
const x = context.randomInt(0, image.width - 1);
|
|
const y = context.randomInt(0, image.height - 1);
|
|
const color = randomColor(context, minimum, maximum, alpha);
|
|
if (radius > 0) fillCircle(image, x, y, context.randomInt(1, radius), color);
|
|
else setPixel(image, x, y, color);
|
|
}
|
|
}
|
|
|
|
function addCrossingLines(
|
|
image: RgbaImage,
|
|
context: CaptchaGeneratorContext,
|
|
count: number,
|
|
alpha: number,
|
|
thickness = 1,
|
|
): void {
|
|
for (let index = 0; index < count; index++) {
|
|
drawLine(
|
|
image,
|
|
context.randomInt(0, image.width - 1),
|
|
context.randomInt(0, image.height - 1),
|
|
context.randomInt(0, image.width - 1),
|
|
context.randomInt(0, image.height - 1),
|
|
randomColor(context, 65, 195, alpha),
|
|
thickness,
|
|
);
|
|
}
|
|
}
|
|
|
|
function drawCharacters(
|
|
image: RgbaImage,
|
|
text: string,
|
|
context: CaptchaGeneratorContext,
|
|
layout: TextLayout,
|
|
style: CaptchaConcreteImageStyle,
|
|
): void {
|
|
const ratio = disturbanceRatio(context);
|
|
const jitterX = Math.round(1 + ratio * 3);
|
|
const jitterY = Math.round(2 + ratio * 4);
|
|
const shear = 0.12 + ratio * 0.42;
|
|
let cursor = layout.startX;
|
|
|
|
for (let index = 0; index < text.length; index++) {
|
|
const character = text[index]!;
|
|
const x = cursor + context.randomInt(-jitterX, jitterX);
|
|
const y = layout.startY + context.randomInt(-jitterY, jitterY);
|
|
const glyphShear = (context.randomFloat() - 0.5) * shear;
|
|
const accent = ACCENTS[context.randomInt(0, ACCENTS.length - 1)]!;
|
|
const color = style === "collision" || style === "cross-shadow" || style === "pixel"
|
|
? accent
|
|
: INK;
|
|
|
|
if (style === "cross-shadow") {
|
|
drawGlyph(image, character, x - 3, y + 3, layout.scale, [37, 99, 235, 95], glyphShear);
|
|
drawGlyph(image, character, x + 3, y - 2, layout.scale, [220, 38, 38, 85], glyphShear);
|
|
} else if (style === "collision") {
|
|
drawGlyph(image, character, x - 2, y + 2, layout.scale, [15, 23, 42, 75], glyphShear);
|
|
if (index > 0 && context.randomFloat() < 0.65) {
|
|
drawGlyph(image, character, x + context.randomInt(-4, 1), y, layout.scale, [2, 6, 23, 60], -glyphShear);
|
|
}
|
|
} else if (context.randomFloat() < 0.28 + ratio * 0.35) {
|
|
drawGlyph(image, character, x + 1, y + 1, layout.scale, [15, 23, 42, 70], glyphShear);
|
|
}
|
|
|
|
drawGlyph(image, character, x, y, layout.scale, color, glyphShear);
|
|
if (style === "pixel" && context.randomFloat() < 0.45) {
|
|
drawGlyph(image, character, x + 1, y, layout.scale, [15, 23, 42, 90], glyphShear);
|
|
}
|
|
|
|
cursor += layout.scale * 5 + layout.spacing;
|
|
}
|
|
}
|
|
|
|
function shiftRows(
|
|
image: RgbaImage,
|
|
shiftForY: (y: number) => number,
|
|
background: Rgba = LIGHT_BACKGROUND,
|
|
): void {
|
|
const source = cloneImage(image);
|
|
const output = createImage(image.width, image.height, background);
|
|
for (let y = 0; y < image.height; y++) {
|
|
const shift = Math.round(shiftForY(y));
|
|
for (let x = 0; x < image.width; x++) {
|
|
const sourceX = x - shift;
|
|
if (sourceX >= 0 && sourceX < image.width) {
|
|
putRawPixel(output, x, y, rawPixel(source, sourceX, y));
|
|
}
|
|
}
|
|
}
|
|
replaceImage(image, output);
|
|
}
|
|
|
|
function shiftColumns(
|
|
image: RgbaImage,
|
|
shiftForX: (x: number) => number,
|
|
background: Rgba = LIGHT_BACKGROUND,
|
|
): void {
|
|
const source = cloneImage(image);
|
|
const output = createImage(image.width, image.height, background);
|
|
for (let x = 0; x < image.width; x++) {
|
|
const shift = Math.round(shiftForX(x));
|
|
for (let y = 0; y < image.height; y++) {
|
|
const sourceY = y - shift;
|
|
if (sourceY >= 0 && sourceY < image.height) {
|
|
putRawPixel(output, x, y, rawPixel(source, x, sourceY));
|
|
}
|
|
}
|
|
}
|
|
replaceImage(image, output);
|
|
}
|
|
|
|
function applyClassic(image: RgbaImage, context: CaptchaGeneratorContext): void {
|
|
const ratio = disturbanceRatio(context);
|
|
addCrossingLines(image, context, Math.round(3 + ratio * 5), Math.round(55 + ratio * 55), ratio > 0.7 ? 2 : 1);
|
|
addDots(image, context, Math.round(140 + ratio * 470), Math.round(45 + ratio * 50));
|
|
}
|
|
|
|
function applyCollision(image: RgbaImage, context: CaptchaGeneratorContext): void {
|
|
const ratio = disturbanceRatio(context);
|
|
addCrossingLines(image, context, Math.round(5 + ratio * 7), Math.round(60 + ratio * 60), ratio > 0.55 ? 2 : 1);
|
|
addDots(image, context, Math.round(120 + ratio * 320), 70);
|
|
const bars = Math.round(2 + ratio * 5);
|
|
for (let index = 0; index < bars; index++) {
|
|
fillRect(
|
|
image,
|
|
context.randomInt(0, image.width - 35),
|
|
context.randomInt(18, image.height - 20),
|
|
context.randomInt(18, 50),
|
|
context.randomInt(1, ratio > 0.65 ? 3 : 2),
|
|
randomColor(context, 35, 160, 75),
|
|
);
|
|
}
|
|
}
|
|
|
|
function applySnow(image: RgbaImage, context: CaptchaGeneratorContext): void {
|
|
const ratio = disturbanceRatio(context);
|
|
addDots(image, context, Math.round(420 + ratio * 950), Math.round(85 + ratio * 75), 120, 245, ratio > 0.5 ? 2 : 1);
|
|
for (let index = 0; index < Math.round(35 + ratio * 90); index++) {
|
|
const x = context.randomInt(0, image.width - 1);
|
|
const y = context.randomInt(0, image.height - 1);
|
|
drawLine(image, x - 2, y, x + 2, y, [255, 255, 255, 185], 1);
|
|
drawLine(image, x, y - 2, x, y + 2, [255, 255, 255, 185], 1);
|
|
}
|
|
addCrossingLines(image, context, Math.round(2 + ratio * 4), 55, 1);
|
|
}
|
|
|
|
function applyCorrosion(
|
|
image: RgbaImage,
|
|
context: CaptchaGeneratorContext,
|
|
layout: TextLayout,
|
|
): void {
|
|
const ratio = disturbanceRatio(context);
|
|
const holes = Math.round(70 + ratio * 230);
|
|
for (let index = 0; index < holes; index++) {
|
|
const x = context.randomInt(layout.startX - 5, layout.startX + layout.textWidth + 5);
|
|
const y = context.randomInt(layout.startY - 5, layout.startY + layout.textHeight + 5);
|
|
const size = context.randomInt(1, ratio > 0.65 ? 4 : 3);
|
|
fillRect(image, x, y, size, size, context.randomFloat() < 0.6 ? LIGHT_BACKGROUND : [203, 213, 225, 210]);
|
|
}
|
|
addDots(image, context, Math.round(170 + ratio * 430), 85, 90, 190, 2);
|
|
addCrossingLines(image, context, Math.round(2 + ratio * 4), 60, 1);
|
|
}
|
|
|
|
function applySpiderweb(image: RgbaImage, context: CaptchaGeneratorContext): void {
|
|
const ratio = disturbanceRatio(context);
|
|
const nodes = Array.from({ length: Math.round(8 + ratio * 12) }, () => ({
|
|
x: context.randomInt(0, image.width - 1),
|
|
y: context.randomInt(0, image.height - 1),
|
|
}));
|
|
for (let index = 0; index < nodes.length; index++) {
|
|
const current = nodes[index]!;
|
|
const next = nodes[(index + context.randomInt(1, Math.max(1, nodes.length - 1))) % nodes.length]!;
|
|
drawLine(image, current.x, current.y, next.x, next.y, [71, 85, 105, Math.round(65 + ratio * 65)], 1);
|
|
if (index % 2 === 0) drawCircleOutline(image, current.x, current.y, context.randomInt(2, 5), [100, 116, 139, 70], 1);
|
|
}
|
|
const anchorX = context.randomInt(Math.floor(image.width * 0.25), Math.floor(image.width * 0.75));
|
|
const anchorY = context.randomInt(Math.floor(image.height * 0.25), Math.floor(image.height * 0.75));
|
|
for (let index = 0; index < Math.round(7 + ratio * 7); index++) {
|
|
const angle = (index / Math.round(7 + ratio * 7)) * Math.PI * 2;
|
|
drawLine(
|
|
image,
|
|
anchorX,
|
|
anchorY,
|
|
anchorX + Math.cos(angle) * image.width,
|
|
anchorY + Math.sin(angle) * image.height,
|
|
[71, 85, 105, 65],
|
|
1,
|
|
);
|
|
}
|
|
addDots(image, context, Math.round(80 + ratio * 200), 60);
|
|
}
|
|
|
|
function applyCrossShadow(image: RgbaImage, context: CaptchaGeneratorContext): void {
|
|
const ratio = disturbanceRatio(context);
|
|
const centerY = Math.floor(image.height / 2);
|
|
drawLine(image, 0, centerY - 6, image.width - 1, centerY + 6, [37, 99, 235, 75], ratio > 0.55 ? 2 : 1);
|
|
drawLine(image, 0, centerY + 7, image.width - 1, centerY - 8, [220, 38, 38, 65], ratio > 0.55 ? 2 : 1);
|
|
addCrossingLines(image, context, Math.round(2 + ratio * 5), 55, 1);
|
|
addDots(image, context, Math.round(120 + ratio * 300), 60);
|
|
}
|
|
|
|
function applySplit(image: RgbaImage, context: CaptchaGeneratorContext): void {
|
|
const ratio = disturbanceRatio(context);
|
|
const bandHeight = context.randomInt(7, 13);
|
|
const amplitude = Math.round(4 + ratio * 13);
|
|
shiftRows(image, (y) => {
|
|
const band = Math.floor(y / bandHeight);
|
|
return band % 2 === 0 ? amplitude : -amplitude;
|
|
});
|
|
for (let y = bandHeight; y < image.height; y += bandHeight) {
|
|
drawLine(image, 0, y, image.width - 1, y, [100, 116, 139, 75], 1);
|
|
}
|
|
addDots(image, context, Math.round(90 + ratio * 250), 65);
|
|
}
|
|
|
|
function applySplit2(image: RgbaImage, context: CaptchaGeneratorContext): void {
|
|
const ratio = disturbanceRatio(context);
|
|
const stripWidth = context.randomInt(10, 18);
|
|
const amplitude = Math.round(3 + ratio * 10);
|
|
shiftColumns(image, (x) => {
|
|
const strip = Math.floor(x / stripWidth);
|
|
return strip % 2 === 0 ? amplitude : -amplitude;
|
|
});
|
|
for (let x = stripWidth; x < image.width; x += stripWidth) {
|
|
drawLine(image, x, 0, x, image.height - 1, [100, 116, 139, 65], 1);
|
|
}
|
|
addCrossingLines(image, context, Math.round(2 + ratio * 4), 55, 1);
|
|
}
|
|
|
|
function applyCut(
|
|
image: RgbaImage,
|
|
context: CaptchaGeneratorContext,
|
|
layout: TextLayout,
|
|
): void {
|
|
const ratio = disturbanceRatio(context);
|
|
const cuts = Math.round(4 + ratio * 8);
|
|
for (let index = 0; index < cuts; index++) {
|
|
const x0 = context.randomInt(layout.startX - 10, layout.startX + layout.textWidth);
|
|
const y0 = context.randomInt(layout.startY - 5, layout.startY + layout.textHeight + 5);
|
|
const x1 = x0 + context.randomInt(24, 72);
|
|
const y1 = y0 + context.randomInt(-18, 18);
|
|
drawLine(image, x0, y0, x1, y1, [248, 250, 252, 245], ratio > 0.6 ? 3 : 2);
|
|
drawLine(image, x0, y0 + 2, x1, y1 + 2, randomColor(context, 75, 170, 80), 1);
|
|
}
|
|
addDots(image, context, Math.round(90 + ratio * 260), 70);
|
|
}
|
|
|
|
function applyDarts(image: RgbaImage, context: CaptchaGeneratorContext): void {
|
|
const ratio = disturbanceRatio(context);
|
|
const targets = Math.round(2 + ratio * 3);
|
|
for (let index = 0; index < targets; index++) {
|
|
const x = context.randomInt(25, image.width - 25);
|
|
const y = context.randomInt(16, image.height - 16);
|
|
const maxRadius = context.randomInt(9, Math.round(14 + ratio * 11));
|
|
for (let radius = maxRadius; radius >= 4; radius -= 5) {
|
|
drawCircleOutline(image, x, y, radius, randomColor(context, 55, 175, 70), 1);
|
|
}
|
|
for (let ray = 0; ray < Math.round(3 + ratio * 4); ray++) {
|
|
const angle = context.randomFloat() * Math.PI * 2;
|
|
drawLine(
|
|
image,
|
|
x,
|
|
y,
|
|
x + Math.cos(angle) * context.randomInt(25, 70),
|
|
y + Math.sin(angle) * context.randomInt(18, 55),
|
|
randomColor(context, 55, 175, 70),
|
|
1,
|
|
);
|
|
}
|
|
}
|
|
addDots(image, context, Math.round(100 + ratio * 240), 60);
|
|
}
|
|
|
|
function applyDistortion(image: RgbaImage, context: CaptchaGeneratorContext): void {
|
|
const ratio = disturbanceRatio(context);
|
|
const amplitudeX = 3 + ratio * 9;
|
|
const amplitudeY = 2 + ratio * 5;
|
|
const phase = context.randomFloat() * Math.PI * 2;
|
|
shiftRows(image, (y) => Math.sin(y / (7 + ratio * 4) + phase) * amplitudeX);
|
|
shiftColumns(image, (x) => Math.sin(x / (20 - ratio * 6) + phase) * amplitudeY);
|
|
addCrossingLines(image, context, Math.round(3 + ratio * 5), 60, ratio > 0.7 ? 2 : 1);
|
|
addDots(image, context, Math.round(100 + ratio * 280), 60);
|
|
}
|
|
|
|
function applyStitch(image: RgbaImage, context: CaptchaGeneratorContext): void {
|
|
const ratio = disturbanceRatio(context);
|
|
const rows = Math.round(4 + ratio * 5);
|
|
for (let index = 0; index < rows; index++) {
|
|
const y = Math.round(((index + 1) / (rows + 1)) * image.height);
|
|
drawDashedLine(image, 0, y, image.width - 1, y + context.randomInt(-3, 3), [71, 85, 105, 75], 4, 4, 1);
|
|
}
|
|
const columns = Math.round(2 + ratio * 4);
|
|
for (let index = 0; index < columns; index++) {
|
|
const x = context.randomInt(10, image.width - 10);
|
|
drawDashedLine(image, x, 0, x + context.randomInt(-5, 5), image.height - 1, [100, 116, 139, 65], 3, 5, 1);
|
|
}
|
|
for (let index = 0; index < Math.round(18 + ratio * 35); index++) {
|
|
const x = context.randomInt(0, image.width - 1);
|
|
const y = context.randomInt(0, image.height - 1);
|
|
drawLine(image, x - 2, y - 2, x + 2, y + 2, [100, 116, 139, 65], 1);
|
|
drawLine(image, x + 2, y - 2, x - 2, y + 2, [100, 116, 139, 65], 1);
|
|
}
|
|
}
|
|
|
|
function applyStriped(image: RgbaImage, context: CaptchaGeneratorContext): void {
|
|
const ratio = disturbanceRatio(context);
|
|
const spacing = Math.round(10 - ratio * 4);
|
|
const offset = context.randomInt(-image.height, image.width);
|
|
for (let x = offset; x < image.width + image.height; x += spacing) {
|
|
drawLine(image, x, 0, x - image.height, image.height - 1, [71, 85, 105, Math.round(45 + ratio * 45)], ratio > 0.7 ? 2 : 1);
|
|
}
|
|
for (let y = context.randomInt(3, 9); y < image.height; y += context.randomInt(8, 14)) {
|
|
drawLine(image, 0, y, image.width - 1, y, [148, 163, 184, 45], 1);
|
|
}
|
|
addDots(image, context, Math.round(70 + ratio * 180), 55);
|
|
}
|
|
|
|
function applyWave(image: RgbaImage, context: CaptchaGeneratorContext): void {
|
|
const ratio = disturbanceRatio(context);
|
|
const phase = context.randomFloat() * Math.PI * 2;
|
|
const amplitude = 5 + ratio * 13;
|
|
shiftRows(image, (y) => Math.sin(y / (5.5 + ratio * 3) + phase) * amplitude);
|
|
shiftColumns(image, (x) => Math.cos(x / (24 - ratio * 7) + phase) * (2 + ratio * 6));
|
|
for (let index = 0; index < Math.round(2 + ratio * 3); index++) {
|
|
const baseY = context.randomInt(10, image.height - 10);
|
|
let previousX = 0;
|
|
let previousY = baseY;
|
|
for (let x = 8; x < image.width; x += 8) {
|
|
const y = baseY + Math.sin(x / 17 + phase + index) * (5 + ratio * 5);
|
|
drawLine(image, previousX, previousY, x, y, randomColor(context, 75, 180, 55), 1);
|
|
previousX = x;
|
|
previousY = y;
|
|
}
|
|
}
|
|
}
|
|
|
|
function applyGridNoise(image: RgbaImage, context: CaptchaGeneratorContext): void {
|
|
const ratio = disturbanceRatio(context);
|
|
const cell = Math.round(16 - ratio * 6);
|
|
for (let x = 0; x < image.width; x += cell) {
|
|
drawLine(image, x, 0, x, image.height - 1, [100, 116, 139, 55], 1);
|
|
}
|
|
for (let y = 0; y < image.height; y += cell) {
|
|
drawLine(image, 0, y, image.width - 1, y, [100, 116, 139, 55], 1);
|
|
}
|
|
for (let index = 0; index < Math.round(10 + ratio * 26); index++) {
|
|
const x = context.randomInt(0, Math.floor(image.width / cell)) * cell;
|
|
const y = context.randomInt(0, Math.floor(image.height / cell)) * cell;
|
|
fillRect(image, x, y, cell, cell, randomColor(context, 100, 220, Math.round(25 + ratio * 35)));
|
|
}
|
|
}
|
|
|
|
function applyScribble(image: RgbaImage, context: CaptchaGeneratorContext): void {
|
|
const ratio = disturbanceRatio(context);
|
|
const scribbles = Math.round(3 + ratio * 6);
|
|
for (let index = 0; index < scribbles; index++) {
|
|
let x = context.randomInt(0, image.width - 1);
|
|
let y = context.randomInt(0, image.height - 1);
|
|
const segments = context.randomInt(7, Math.round(12 + ratio * 13));
|
|
const color = randomColor(context, 45, 180, Math.round(55 + ratio * 45));
|
|
for (let segment = 0; segment < segments; segment++) {
|
|
const nextX = clamp(x + context.randomInt(-28, 28), 0, image.width - 1);
|
|
const nextY = clamp(y + context.randomInt(-18, 18), 0, image.height - 1);
|
|
drawLine(image, x, y, nextX, nextY, color, ratio > 0.7 && segment % 3 === 0 ? 2 : 1);
|
|
x = nextX;
|
|
y = nextY;
|
|
}
|
|
}
|
|
addDots(image, context, Math.round(80 + ratio * 220), 55);
|
|
}
|
|
|
|
function applyPixel(image: RgbaImage, context: CaptchaGeneratorContext): void {
|
|
const ratio = disturbanceRatio(context);
|
|
const blocks = Math.round(28 + ratio * 75);
|
|
for (let index = 0; index < blocks; index++) {
|
|
const size = context.randomInt(2, Math.round(4 + ratio * 5));
|
|
fillRect(
|
|
image,
|
|
context.randomInt(0, image.width - size),
|
|
context.randomInt(0, image.height - size),
|
|
size,
|
|
size,
|
|
randomColor(context, 65, 220, Math.round(45 + ratio * 45)),
|
|
);
|
|
}
|
|
const source = cloneImage(image);
|
|
for (let index = 0; index < Math.round(4 + ratio * 8); index++) {
|
|
const blockWidth = context.randomInt(12, 28);
|
|
const blockHeight = context.randomInt(6, 16);
|
|
const x = context.randomInt(0, image.width - blockWidth);
|
|
const y = context.randomInt(0, image.height - blockHeight);
|
|
const shift = context.randomInt(-8, 8);
|
|
for (let py = 0; py < blockHeight; py++) {
|
|
for (let px = 0; px < blockWidth; px++) {
|
|
const sourceX = clamp(x + px, 0, image.width - 1);
|
|
const targetX = x + px + shift;
|
|
putRawPixel(image, targetX, y + py, rawPixel(source, sourceX, y + py));
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
function applyBrokenLines(
|
|
image: RgbaImage,
|
|
context: CaptchaGeneratorContext,
|
|
layout: TextLayout,
|
|
): void {
|
|
const ratio = disturbanceRatio(context);
|
|
const gaps = Math.round(12 + ratio * 35);
|
|
for (let index = 0; index < gaps; index++) {
|
|
const horizontal = context.randomFloat() < 0.7;
|
|
const width = horizontal ? context.randomInt(6, 24) : context.randomInt(1, 4);
|
|
const height = horizontal ? context.randomInt(1, 3) : context.randomInt(7, 18);
|
|
fillRect(
|
|
image,
|
|
context.randomInt(layout.startX - 3, layout.startX + layout.textWidth),
|
|
context.randomInt(layout.startY - 3, layout.startY + layout.textHeight),
|
|
width,
|
|
height,
|
|
LIGHT_BACKGROUND,
|
|
);
|
|
}
|
|
for (let index = 0; index < Math.round(12 + ratio * 28); index++) {
|
|
const x = context.randomInt(0, image.width - 12);
|
|
const y = context.randomInt(0, image.height - 1);
|
|
drawLine(image, x, y, x + context.randomInt(4, 18), y + context.randomInt(-2, 2), randomColor(context, 45, 175, 75), 1);
|
|
}
|
|
addDots(image, context, Math.round(70 + ratio * 180), 60);
|
|
}
|
|
|
|
function applyStyle(
|
|
image: RgbaImage,
|
|
context: CaptchaGeneratorContext,
|
|
layout: TextLayout,
|
|
): void {
|
|
switch (context.imageStyle) {
|
|
case "collision":
|
|
applyCollision(image, context);
|
|
break;
|
|
case "snow":
|
|
applySnow(image, context);
|
|
break;
|
|
case "corrosion":
|
|
applyCorrosion(image, context, layout);
|
|
break;
|
|
case "spiderweb":
|
|
applySpiderweb(image, context);
|
|
break;
|
|
case "cross-shadow":
|
|
applyCrossShadow(image, context);
|
|
break;
|
|
case "split":
|
|
applySplit(image, context);
|
|
break;
|
|
case "split2":
|
|
applySplit2(image, context);
|
|
break;
|
|
case "cut":
|
|
applyCut(image, context, layout);
|
|
break;
|
|
case "darts":
|
|
applyDarts(image, context);
|
|
break;
|
|
case "distortion":
|
|
applyDistortion(image, context);
|
|
break;
|
|
case "stitch":
|
|
applyStitch(image, context);
|
|
break;
|
|
case "striped":
|
|
applyStriped(image, context);
|
|
break;
|
|
case "wave":
|
|
applyWave(image, context);
|
|
break;
|
|
case "grid-noise":
|
|
applyGridNoise(image, context);
|
|
break;
|
|
case "scribble":
|
|
applyScribble(image, context);
|
|
break;
|
|
case "pixel":
|
|
applyPixel(image, context);
|
|
break;
|
|
case "broken-lines":
|
|
applyBrokenLines(image, context, layout);
|
|
break;
|
|
default:
|
|
applyClassic(image, context);
|
|
}
|
|
}
|
|
|
|
function backgroundFor(style: CaptchaConcreteImageStyle): Rgba {
|
|
if (style === "snow" || style === "grid-noise" || style === "stitch") return ALT_BACKGROUND;
|
|
return LIGHT_BACKGROUND;
|
|
}
|
|
|
|
export function renderTextChallenge(
|
|
text: string,
|
|
context: CaptchaGeneratorContext,
|
|
): string {
|
|
const layout = layoutFor(text, context.imageStyle);
|
|
const image = createImage(300, 104, backgroundFor(context.imageStyle));
|
|
|
|
if (context.imageStyle === "grid-noise") applyGridNoise(image, context);
|
|
else if (context.imageStyle === "snow") addDots(image, context, 130, 45, 160, 235, 1);
|
|
else addDots(image, context, Math.round(35 + disturbanceRatio(context) * 80), 30);
|
|
|
|
drawCharacters(image, text, context, layout, context.imageStyle);
|
|
applyStyle(image, context, layout);
|
|
|
|
if (context.imageStyle !== "snow" && context.imageStyle !== "grid-noise") {
|
|
const ratio = disturbanceRatio(context);
|
|
addDots(image, context, Math.round(25 + ratio * 100), 35, 100, 220);
|
|
}
|
|
|
|
return pngDataUri(image);
|
|
}
|