release: WRNexusJS 0.4.0
This commit is contained in:
@@ -51,7 +51,13 @@ export class CalculationCaptchaGenerator implements CaptchaChallengeGenerator {
|
||||
answerKind: "text",
|
||||
image: renderTextChallenge(expression, context),
|
||||
inputMode: "numeric",
|
||||
audioSequence: ["what", "is", ...numberTokens(left), ...(OPERATOR_WORDS[operator] ?? []), ...numberTokens(right)],
|
||||
audioSequence: [
|
||||
"what",
|
||||
"is",
|
||||
...numberTokens(left),
|
||||
...(OPERATOR_WORDS[operator] ?? []),
|
||||
...numberTokens(right),
|
||||
],
|
||||
metadata: { operator, imageStyle: context.imageStyle },
|
||||
};
|
||||
}
|
||||
|
||||
@@ -4,7 +4,15 @@ import type {
|
||||
CaptchaImageItem,
|
||||
GeneratedCaptchaChallenge,
|
||||
} from "../types.ts";
|
||||
import { createImage, drawLine, fillCircle, fillPolygon, fillRect, pngDataUri, setPixel } from "./png.ts";
|
||||
import {
|
||||
createImage,
|
||||
drawLine,
|
||||
fillCircle,
|
||||
fillPolygon,
|
||||
fillRect,
|
||||
pngDataUri,
|
||||
setPixel,
|
||||
} from "./png.ts";
|
||||
|
||||
const SHAPES = ["circle", "square", "triangle", "diamond", "star"] as const;
|
||||
type Shape = (typeof SHAPES)[number];
|
||||
@@ -39,9 +47,26 @@ function shapeImage(shape: Shape, context: CaptchaGeneratorContext): string {
|
||||
if (shape === "circle") fillCircle(image, cx, cy, size, color);
|
||||
else if (shape === "square") fillRect(image, cx - size, cy - size, size * 2, size * 2, color);
|
||||
else if (shape === "triangle") {
|
||||
fillPolygon(image, [[cx, cy - size], [cx - size, cy + size], [cx + size, cy + size]], color);
|
||||
fillPolygon(
|
||||
image,
|
||||
[
|
||||
[cx, cy - size],
|
||||
[cx - size, cy + size],
|
||||
[cx + size, cy + size],
|
||||
],
|
||||
color,
|
||||
);
|
||||
} else if (shape === "diamond") {
|
||||
fillPolygon(image, [[cx, cy - size], [cx - size, cy], [cx, cy + size], [cx + size, cy]], color);
|
||||
fillPolygon(
|
||||
image,
|
||||
[
|
||||
[cx, cy - size],
|
||||
[cx - size, cy],
|
||||
[cx, cy + size],
|
||||
[cx + size, cy],
|
||||
],
|
||||
color,
|
||||
);
|
||||
} else {
|
||||
fillPolygon(image, starPoints(cx, cy, size, size * 0.45), color);
|
||||
}
|
||||
@@ -49,12 +74,12 @@ function shapeImage(shape: Shape, context: CaptchaGeneratorContext): string {
|
||||
const ratio = disturbanceRatio(context);
|
||||
const dots = Math.round(35 + ratio * 150);
|
||||
for (let index = 0; index < dots; index++) {
|
||||
setPixel(
|
||||
image,
|
||||
context.randomInt(0, 95),
|
||||
context.randomInt(0, 71),
|
||||
[context.randomInt(105, 225), context.randomInt(105, 225), context.randomInt(105, 225), Math.round(55 + ratio * 65)],
|
||||
);
|
||||
setPixel(image, context.randomInt(0, 95), context.randomInt(0, 71), [
|
||||
context.randomInt(105, 225),
|
||||
context.randomInt(105, 225),
|
||||
context.randomInt(105, 225),
|
||||
Math.round(55 + ratio * 65),
|
||||
]);
|
||||
}
|
||||
|
||||
const lines = Math.round(1 + ratio * 4);
|
||||
@@ -65,7 +90,12 @@ function shapeImage(shape: Shape, context: CaptchaGeneratorContext): string {
|
||||
context.randomInt(0, 71),
|
||||
context.randomInt(0, 95),
|
||||
context.randomInt(0, 71),
|
||||
[context.randomInt(100, 210), context.randomInt(100, 210), context.randomInt(100, 210), Math.round(45 + ratio * 55)],
|
||||
[
|
||||
context.randomInt(100, 210),
|
||||
context.randomInt(100, 210),
|
||||
context.randomInt(100, 210),
|
||||
Math.round(45 + ratio * 55),
|
||||
],
|
||||
ratio > 0.75 ? 2 : 1,
|
||||
);
|
||||
}
|
||||
@@ -105,7 +135,11 @@ export class ImageCaptchaGenerator implements CaptchaChallengeGenerator {
|
||||
}
|
||||
|
||||
shuffle(entries, context);
|
||||
const answer = entries.filter((entry) => entry.shape === target).map((entry) => entry.id).sort().join(",");
|
||||
const answer = entries
|
||||
.filter((entry) => entry.shape === target)
|
||||
.map((entry) => entry.id)
|
||||
.sort()
|
||||
.join(",");
|
||||
return {
|
||||
type: "image",
|
||||
presentation: "visual",
|
||||
|
||||
@@ -1,8 +1,16 @@
|
||||
import type { CaptchaChallengeGenerator } from "../types.ts";
|
||||
import { alphaCaptchaGenerator, alphanumericCaptchaGenerator, numberCaptchaGenerator } from "./text.ts";
|
||||
import {
|
||||
alphaCaptchaGenerator,
|
||||
alphanumericCaptchaGenerator,
|
||||
numberCaptchaGenerator,
|
||||
} from "./text.ts";
|
||||
import { calculationCaptchaGenerator } from "./calculation.ts";
|
||||
import { imageCaptchaGenerator } from "./image.ts";
|
||||
import { honeypotCaptchaGenerator, notRobotCaptchaGenerator, timingCaptchaGenerator } from "./invisible.ts";
|
||||
import {
|
||||
honeypotCaptchaGenerator,
|
||||
notRobotCaptchaGenerator,
|
||||
timingCaptchaGenerator,
|
||||
} from "./invisible.ts";
|
||||
|
||||
export * from "./text.ts";
|
||||
export * from "./calculation.ts";
|
||||
@@ -13,7 +21,8 @@ export * from "./styles.ts";
|
||||
|
||||
export function defineCaptchaGenerator<T extends CaptchaChallengeGenerator>(generator: T): T {
|
||||
if (!generator.type) throw new TypeError("CAPTCHA generator requires a stable type");
|
||||
if (typeof generator.generate !== "function") throw new TypeError("CAPTCHA generator requires generate()");
|
||||
if (typeof generator.generate !== "function")
|
||||
throw new TypeError("CAPTCHA generator requires generate()");
|
||||
return generator;
|
||||
}
|
||||
|
||||
|
||||
@@ -19,7 +19,8 @@ export class InvisibleCaptchaGenerator implements CaptchaChallengeGenerator {
|
||||
return {
|
||||
type: this.type,
|
||||
presentation: "invisible",
|
||||
prompt: this.type === "not-robot" ? "Confirm that you are not a robot" : "Automated abuse check",
|
||||
prompt:
|
||||
this.type === "not-robot" ? "Confirm that you are not a robot" : "Automated abuse check",
|
||||
answer: JSON.stringify({ honeypot: "", timingToken }),
|
||||
answerKind: "invisible",
|
||||
inputMode: "none",
|
||||
|
||||
@@ -8,7 +8,11 @@ export interface RgbaImage {
|
||||
|
||||
export type Rgba = readonly [number, number, number, number?];
|
||||
|
||||
export function createImage(width: number, height: number, background: Rgba = [255, 255, 255, 255]): RgbaImage {
|
||||
export function createImage(
|
||||
width: number,
|
||||
height: number,
|
||||
background: Rgba = [255, 255, 255, 255],
|
||||
): RgbaImage {
|
||||
const data = new Uint8Array(width * height * 4);
|
||||
const alpha = background[3] ?? 255;
|
||||
for (let index = 0; index < data.length; index += 4) {
|
||||
@@ -33,13 +37,28 @@ export function setPixel(image: RgbaImage, x: number, y: number, color: Rgba): v
|
||||
image.data[index + 3] = 255;
|
||||
}
|
||||
|
||||
export function fillRect(image: RgbaImage, x: number, y: number, width: number, height: number, color: Rgba): void {
|
||||
export function fillRect(
|
||||
image: RgbaImage,
|
||||
x: number,
|
||||
y: number,
|
||||
width: number,
|
||||
height: number,
|
||||
color: Rgba,
|
||||
): void {
|
||||
for (let py = Math.floor(y); py < Math.ceil(y + height); py++) {
|
||||
for (let px = Math.floor(x); px < Math.ceil(x + width); px++) setPixel(image, px, py, color);
|
||||
}
|
||||
}
|
||||
|
||||
export function drawLine(image: RgbaImage, x0: number, y0: number, x1: number, y1: number, color: Rgba, thickness = 1): void {
|
||||
export function drawLine(
|
||||
image: RgbaImage,
|
||||
x0: number,
|
||||
y0: number,
|
||||
x1: number,
|
||||
y1: number,
|
||||
color: Rgba,
|
||||
thickness = 1,
|
||||
): void {
|
||||
let x = Math.round(x0);
|
||||
let y = Math.round(y0);
|
||||
const targetX = Math.round(x1);
|
||||
@@ -50,7 +69,14 @@ export function drawLine(image: RgbaImage, x0: number, y0: number, x1: number, y
|
||||
const sy = y < targetY ? 1 : -1;
|
||||
let error = dx + dy;
|
||||
while (true) {
|
||||
fillRect(image, x - Math.floor(thickness / 2), y - Math.floor(thickness / 2), thickness, thickness, color);
|
||||
fillRect(
|
||||
image,
|
||||
x - Math.floor(thickness / 2),
|
||||
y - Math.floor(thickness / 2),
|
||||
thickness,
|
||||
thickness,
|
||||
color,
|
||||
);
|
||||
if (x === targetX && y === targetY) break;
|
||||
const twice = 2 * error;
|
||||
if (twice >= dy) {
|
||||
@@ -64,7 +90,13 @@ export function drawLine(image: RgbaImage, x0: number, y0: number, x1: number, y
|
||||
}
|
||||
}
|
||||
|
||||
export function fillCircle(image: RgbaImage, centerX: number, centerY: number, radius: number, color: Rgba): void {
|
||||
export function fillCircle(
|
||||
image: RgbaImage,
|
||||
centerX: number,
|
||||
centerY: number,
|
||||
radius: number,
|
||||
color: Rgba,
|
||||
): void {
|
||||
const r2 = radius * radius;
|
||||
for (let y = Math.floor(centerY - radius); y <= Math.ceil(centerY + radius); y++) {
|
||||
for (let x = Math.floor(centerX - radius); x <= Math.ceil(centerX + radius); x++) {
|
||||
@@ -75,7 +107,11 @@ export function fillCircle(image: RgbaImage, centerX: number, centerY: number, r
|
||||
}
|
||||
}
|
||||
|
||||
function pointInPolygon(x: number, y: number, points: readonly (readonly [number, number])[]): boolean {
|
||||
function pointInPolygon(
|
||||
x: number,
|
||||
y: number,
|
||||
points: readonly (readonly [number, number])[],
|
||||
): boolean {
|
||||
let inside = false;
|
||||
for (let i = 0, j = points.length - 1; i < points.length; j = i++) {
|
||||
const xi = points[i]![0];
|
||||
@@ -88,13 +124,18 @@ function pointInPolygon(x: number, y: number, points: readonly (readonly [number
|
||||
return inside;
|
||||
}
|
||||
|
||||
export function fillPolygon(image: RgbaImage, points: readonly (readonly [number, number])[], color: Rgba): void {
|
||||
export function fillPolygon(
|
||||
image: RgbaImage,
|
||||
points: readonly (readonly [number, number])[],
|
||||
color: Rgba,
|
||||
): void {
|
||||
const minX = Math.floor(Math.min(...points.map((point) => point[0])));
|
||||
const maxX = Math.ceil(Math.max(...points.map((point) => point[0])));
|
||||
const minY = Math.floor(Math.min(...points.map((point) => point[1])));
|
||||
const maxY = Math.ceil(Math.max(...points.map((point) => point[1])));
|
||||
for (let y = minY; y <= maxY; y++) {
|
||||
for (let x = minX; x <= maxX; x++) if (pointInPolygon(x + 0.5, y + 0.5, points)) setPixel(image, x, y, color);
|
||||
for (let x = minX; x <= maxX; x++)
|
||||
if (pointInPolygon(x + 0.5, y + 0.5, points)) setPixel(image, x, y, color);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -134,7 +175,15 @@ export function drawText(
|
||||
let cursor = options.x;
|
||||
for (let index = 0; index < text.length; index++) {
|
||||
const jitter = options.jitter?.(index) ?? { x: 0, y: 0, shear: 0 };
|
||||
drawGlyph(image, text[index]!, cursor + jitter.x, options.y + jitter.y, options.scale, options.color, jitter.shear);
|
||||
drawGlyph(
|
||||
image,
|
||||
text[index]!,
|
||||
cursor + jitter.x,
|
||||
options.y + jitter.y,
|
||||
options.scale,
|
||||
options.color,
|
||||
jitter.shear,
|
||||
);
|
||||
cursor += options.scale * 5 + spacing;
|
||||
}
|
||||
}
|
||||
@@ -170,7 +219,12 @@ function adler32(bytes: Uint8Array): number {
|
||||
}
|
||||
|
||||
function u32(value: number): Uint8Array {
|
||||
return Uint8Array.of((value >>> 24) & 255, (value >>> 16) & 255, (value >>> 8) & 255, value & 255);
|
||||
return Uint8Array.of(
|
||||
(value >>> 24) & 255,
|
||||
(value >>> 16) & 255,
|
||||
(value >>> 8) & 255,
|
||||
value & 255,
|
||||
);
|
||||
}
|
||||
|
||||
function concat(parts: readonly Uint8Array[]): Uint8Array {
|
||||
@@ -195,9 +249,15 @@ function deflateStored(data: Uint8Array): Uint8Array {
|
||||
const size = Math.min(65535, data.length - offset);
|
||||
const final = offset + size >= data.length;
|
||||
const length = size;
|
||||
const inverse = (~length) & 0xffff;
|
||||
const inverse = ~length & 0xffff;
|
||||
blocks.push(
|
||||
Uint8Array.of(final ? 1 : 0, length & 255, (length >>> 8) & 255, inverse & 255, (inverse >>> 8) & 255),
|
||||
Uint8Array.of(
|
||||
final ? 1 : 0,
|
||||
length & 255,
|
||||
(length >>> 8) & 255,
|
||||
inverse & 255,
|
||||
(inverse >>> 8) & 255,
|
||||
),
|
||||
data.slice(offset, offset + size),
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,7 +1,4 @@
|
||||
import type {
|
||||
CaptchaConcreteImageStyle,
|
||||
CaptchaImageStyle,
|
||||
} from "../types.ts";
|
||||
import type { CaptchaConcreteImageStyle, CaptchaImageStyle } from "../types.ts";
|
||||
|
||||
export const CAPTCHA_CONCRETE_IMAGE_STYLES = [
|
||||
"classic",
|
||||
@@ -52,9 +49,7 @@ export function normalizeCaptchaImageStyle(
|
||||
if (value === undefined || value === null || value === "") return fallback;
|
||||
const normalized = String(value).trim().toLowerCase();
|
||||
if (!STYLE_SET.has(normalized)) {
|
||||
throw new RangeError(
|
||||
`imageStyle must be one of: ${CAPTCHA_IMAGE_STYLES.join(", ")}`,
|
||||
);
|
||||
throw new RangeError(`imageStyle must be one of: ${CAPTCHA_IMAGE_STYLES.join(", ")}`);
|
||||
}
|
||||
return normalized as CaptchaImageStyle;
|
||||
}
|
||||
@@ -67,9 +62,7 @@ export function normalizeCaptchaImageStyleList(
|
||||
for (const entry of styleValues(value)) {
|
||||
if (entry === "random") continue;
|
||||
if (!CONCRETE_STYLE_SET.has(entry)) {
|
||||
throw new RangeError(
|
||||
`${name} contains an unknown image style: ${entry}`,
|
||||
);
|
||||
throw new RangeError(`${name} contains an unknown image style: ${entry}`);
|
||||
}
|
||||
const style = entry as CaptchaConcreteImageStyle;
|
||||
if (!styles.includes(style)) styles.push(style);
|
||||
@@ -95,17 +88,12 @@ export function resolveCaptchaImageStyle(
|
||||
options: ResolveCaptchaImageStyleOptions,
|
||||
): ResolvedCaptchaImageStyle {
|
||||
const requested = normalizeCaptchaImageStyle(options.imageStyle, "random");
|
||||
const allowed = normalizeCaptchaImageStyleList(
|
||||
options.allowedStyles,
|
||||
"allowedStyles",
|
||||
);
|
||||
const allowed = normalizeCaptchaImageStyleList(options.allowedStyles, "allowedStyles");
|
||||
const excluded = new Set(
|
||||
normalizeCaptchaImageStyleList(options.excludedStyles, "excludedStyles"),
|
||||
);
|
||||
|
||||
const source = allowed.length
|
||||
? allowed
|
||||
: [...CAPTCHA_CONCRETE_IMAGE_STYLES];
|
||||
const source = allowed.length ? allowed : [...CAPTCHA_CONCRETE_IMAGE_STYLES];
|
||||
const pool = source.filter((style) => !excluded.has(style));
|
||||
|
||||
if (!pool.length) {
|
||||
@@ -116,9 +104,7 @@ export function resolveCaptchaImageStyle(
|
||||
|
||||
if (!options.randomizeStyle && requested !== "random") {
|
||||
if (!pool.includes(requested)) {
|
||||
throw new RangeError(
|
||||
`imageStyle ${requested} is not available in the configured style pool`,
|
||||
);
|
||||
throw new RangeError(`imageStyle ${requested} is not available in the configured style pool`);
|
||||
}
|
||||
return { requested, resolved: requested, pool };
|
||||
}
|
||||
|
||||
@@ -11,7 +11,9 @@ const ALPHA = "ABCDEFGHJKMNPQRSTUVWXYZ";
|
||||
const ALPHANUMERIC = `${ALPHA}${NUMBERS}`;
|
||||
|
||||
function defaultLength(context: CaptchaGeneratorContext): number {
|
||||
return context.length ?? (context.difficulty === "easy" ? 4 : context.difficulty === "hard" ? 7 : 6);
|
||||
return (
|
||||
context.length ?? (context.difficulty === "easy" ? 4 : context.difficulty === "hard" ? 7 : 6)
|
||||
);
|
||||
}
|
||||
|
||||
function charset(type: CaptchaChallengeType): string {
|
||||
@@ -20,11 +22,15 @@ function charset(type: CaptchaChallengeType): string {
|
||||
return ALPHANUMERIC;
|
||||
}
|
||||
|
||||
function generateText(type: "number" | "alpha" | "alphanumeric", context: CaptchaGeneratorContext): GeneratedCaptchaChallenge {
|
||||
function generateText(
|
||||
type: "number" | "alpha" | "alphanumeric",
|
||||
context: CaptchaGeneratorContext,
|
||||
): GeneratedCaptchaChallenge {
|
||||
const source = charset(type);
|
||||
const length = Math.max(3, Math.min(10, defaultLength(context)));
|
||||
let answer = "";
|
||||
for (let index = 0; index < length; index++) answer += source[context.randomInt(0, source.length - 1)];
|
||||
for (let index = 0; index < length; index++)
|
||||
answer += source[context.randomInt(0, source.length - 1)];
|
||||
return {
|
||||
type,
|
||||
presentation: "visual",
|
||||
|
||||
@@ -1,7 +1,4 @@
|
||||
import type {
|
||||
CaptchaConcreteImageStyle,
|
||||
CaptchaGeneratorContext,
|
||||
} from "../types.ts";
|
||||
import type { CaptchaConcreteImageStyle, CaptchaGeneratorContext } from "../types.ts";
|
||||
import {
|
||||
createImage,
|
||||
drawGlyph,
|
||||
@@ -56,7 +53,11 @@ function randomColor(
|
||||
];
|
||||
}
|
||||
|
||||
function rawPixel(image: RgbaImage, x: number, y: number): readonly [number, number, number, number] {
|
||||
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;
|
||||
@@ -98,11 +99,12 @@ function layoutFor(text: string, style: CaptchaConcreteImageStyle): TextLayout {
|
||||
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 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 {
|
||||
@@ -218,9 +220,8 @@ function drawCharacters(
|
||||
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;
|
||||
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);
|
||||
@@ -228,7 +229,15 @@ function drawCharacters(
|
||||
} 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);
|
||||
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);
|
||||
@@ -283,13 +292,25 @@ function shiftColumns(
|
||||
|
||||
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);
|
||||
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);
|
||||
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++) {
|
||||
@@ -306,7 +327,15 @@ function applyCollision(image: RgbaImage, context: CaptchaGeneratorContext): voi
|
||||
|
||||
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);
|
||||
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);
|
||||
@@ -327,7 +356,14 @@ function applyCorrosion(
|
||||
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]);
|
||||
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);
|
||||
@@ -341,12 +377,32 @@ function applySpiderweb(image: RgbaImage, context: CaptchaGeneratorContext): voi
|
||||
}));
|
||||
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 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));
|
||||
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(
|
||||
@@ -365,8 +421,24 @@ function applySpiderweb(image: RgbaImage, context: CaptchaGeneratorContext): voi
|
||||
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);
|
||||
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);
|
||||
}
|
||||
@@ -399,11 +471,7 @@ function applySplit2(image: RgbaImage, context: CaptchaGeneratorContext): void {
|
||||
addCrossingLines(image, context, Math.round(2 + ratio * 4), 55, 1);
|
||||
}
|
||||
|
||||
function applyCut(
|
||||
image: RgbaImage,
|
||||
context: CaptchaGeneratorContext,
|
||||
layout: TextLayout,
|
||||
): void {
|
||||
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++) {
|
||||
@@ -459,12 +527,32 @@ function applyStitch(image: RgbaImage, context: CaptchaGeneratorContext): void {
|
||||
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);
|
||||
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);
|
||||
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);
|
||||
@@ -479,7 +567,15 @@ function applyStriped(image: RgbaImage, context: CaptchaGeneratorContext): void
|
||||
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);
|
||||
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);
|
||||
@@ -595,16 +691,20 @@ function applyBrokenLines(
|
||||
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);
|
||||
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 {
|
||||
function applyStyle(image: RgbaImage, context: CaptchaGeneratorContext, layout: TextLayout): void {
|
||||
switch (context.imageStyle) {
|
||||
case "collision":
|
||||
applyCollision(image, context);
|
||||
@@ -667,10 +767,7 @@ function backgroundFor(style: CaptchaConcreteImageStyle): Rgba {
|
||||
return LIGHT_BACKGROUND;
|
||||
}
|
||||
|
||||
export function renderTextChallenge(
|
||||
text: string,
|
||||
context: CaptchaGeneratorContext,
|
||||
): string {
|
||||
export function renderTextChallenge(text: string, context: CaptchaGeneratorContext): string {
|
||||
const layout = layoutFor(text, context.imageStyle);
|
||||
const image = createImage(300, 104, backgroundFor(context.imageStyle));
|
||||
|
||||
|
||||
Reference in New Issue
Block a user