New Captcha Package added
This commit is contained in:
@@ -0,0 +1,240 @@
|
||||
import { BITMAP_FONT } from "./bitmap.ts";
|
||||
|
||||
export interface RgbaImage {
|
||||
width: number;
|
||||
height: number;
|
||||
data: Uint8Array;
|
||||
}
|
||||
|
||||
export type Rgba = readonly [number, number, number, number?];
|
||||
|
||||
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) {
|
||||
data[index] = background[0];
|
||||
data[index + 1] = background[1];
|
||||
data[index + 2] = background[2];
|
||||
data[index + 3] = alpha;
|
||||
}
|
||||
return { width, height, data };
|
||||
}
|
||||
|
||||
export function setPixel(image: RgbaImage, x: number, y: number, color: Rgba): 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;
|
||||
const alpha = (color[3] ?? 255) / 255;
|
||||
const inverse = 1 - alpha;
|
||||
image.data[index] = Math.round(color[0] * alpha + image.data[index] * inverse);
|
||||
image.data[index + 1] = Math.round(color[1] * alpha + image.data[index + 1] * inverse);
|
||||
image.data[index + 2] = Math.round(color[2] * alpha + image.data[index + 2] * inverse);
|
||||
image.data[index + 3] = 255;
|
||||
}
|
||||
|
||||
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 {
|
||||
let x = Math.round(x0);
|
||||
let y = Math.round(y0);
|
||||
const targetX = Math.round(x1);
|
||||
const targetY = Math.round(y1);
|
||||
const dx = Math.abs(targetX - x);
|
||||
const dy = -Math.abs(targetY - y);
|
||||
const sx = x < targetX ? 1 : -1;
|
||||
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);
|
||||
if (x === targetX && y === targetY) break;
|
||||
const twice = 2 * error;
|
||||
if (twice >= dy) {
|
||||
error += dy;
|
||||
x += sx;
|
||||
}
|
||||
if (twice <= dx) {
|
||||
error += dx;
|
||||
y += sy;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
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++) {
|
||||
const dx = x - centerX;
|
||||
const dy = y - centerY;
|
||||
if (dx * dx + dy * dy <= r2) setPixel(image, x, y, color);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
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];
|
||||
const yi = points[i]![1];
|
||||
const xj = points[j]![0];
|
||||
const yj = points[j]![1];
|
||||
const intersects = yi > y !== yj > y && x < ((xj - xi) * (y - yi)) / (yj - yi || 1) + xi;
|
||||
if (intersects) inside = !inside;
|
||||
}
|
||||
return inside;
|
||||
}
|
||||
|
||||
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);
|
||||
}
|
||||
}
|
||||
|
||||
export function drawGlyph(
|
||||
image: RgbaImage,
|
||||
character: string,
|
||||
x: number,
|
||||
y: number,
|
||||
scale: number,
|
||||
color: Rgba,
|
||||
shear = 0,
|
||||
): void {
|
||||
const glyph = BITMAP_FONT[character.toUpperCase()] ?? BITMAP_FONT["?"]!;
|
||||
for (let row = 0; row < glyph.length; row++) {
|
||||
const line = glyph[row]!;
|
||||
for (let column = 0; column < line.length; column++) {
|
||||
if (line[column] !== "1") continue;
|
||||
const offset = Math.round((glyph.length - row) * shear);
|
||||
fillRect(image, x + column * scale + offset, y + row * scale, scale, scale, color);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export function drawText(
|
||||
image: RgbaImage,
|
||||
text: string,
|
||||
options: {
|
||||
x: number;
|
||||
y: number;
|
||||
scale: number;
|
||||
color: Rgba;
|
||||
spacing?: number;
|
||||
jitter?: (index: number) => { x: number; y: number; shear: number };
|
||||
},
|
||||
): void {
|
||||
const spacing = options.spacing ?? options.scale * 2;
|
||||
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);
|
||||
cursor += options.scale * 5 + spacing;
|
||||
}
|
||||
}
|
||||
|
||||
const CRC32_TABLE = (() => {
|
||||
const table = new Uint32Array(256);
|
||||
for (let index = 0; index < table.length; index++) {
|
||||
let value = index;
|
||||
for (let bit = 0; bit < 8; bit++) {
|
||||
value = (value >>> 1) ^ (value & 1 ? 0xedb88320 : 0);
|
||||
}
|
||||
table[index] = value >>> 0;
|
||||
}
|
||||
return table;
|
||||
})();
|
||||
|
||||
function crc32(bytes: Uint8Array): number {
|
||||
let crc = 0xffffffff;
|
||||
for (const byte of bytes) {
|
||||
crc = (crc >>> 8) ^ CRC32_TABLE[(crc ^ byte) & 255]!;
|
||||
}
|
||||
return (crc ^ 0xffffffff) >>> 0;
|
||||
}
|
||||
|
||||
function adler32(bytes: Uint8Array): number {
|
||||
let a = 1;
|
||||
let b = 0;
|
||||
for (const byte of bytes) {
|
||||
a = (a + byte) % 65521;
|
||||
b = (b + a) % 65521;
|
||||
}
|
||||
return ((b << 16) | a) >>> 0;
|
||||
}
|
||||
|
||||
function u32(value: number): Uint8Array {
|
||||
return Uint8Array.of((value >>> 24) & 255, (value >>> 16) & 255, (value >>> 8) & 255, value & 255);
|
||||
}
|
||||
|
||||
function concat(parts: readonly Uint8Array[]): Uint8Array {
|
||||
const length = parts.reduce((sum, part) => sum + part.length, 0);
|
||||
const out = new Uint8Array(length);
|
||||
let offset = 0;
|
||||
for (const part of parts) {
|
||||
out.set(part, offset);
|
||||
offset += part.length;
|
||||
}
|
||||
return out;
|
||||
}
|
||||
|
||||
function chunk(type: string, data: Uint8Array): Uint8Array {
|
||||
const typeBytes = new TextEncoder().encode(type);
|
||||
return concat([u32(data.length), typeBytes, data, u32(crc32(concat([typeBytes, data])))]);
|
||||
}
|
||||
|
||||
function deflateStored(data: Uint8Array): Uint8Array {
|
||||
const blocks: Uint8Array[] = [Uint8Array.of(0x78, 0x01)];
|
||||
for (let offset = 0; offset < data.length; offset += 65535) {
|
||||
const size = Math.min(65535, data.length - offset);
|
||||
const final = offset + size >= data.length;
|
||||
const length = size;
|
||||
const inverse = (~length) & 0xffff;
|
||||
blocks.push(
|
||||
Uint8Array.of(final ? 1 : 0, length & 255, (length >>> 8) & 255, inverse & 255, (inverse >>> 8) & 255),
|
||||
data.slice(offset, offset + size),
|
||||
);
|
||||
}
|
||||
blocks.push(u32(adler32(data)));
|
||||
return concat(blocks);
|
||||
}
|
||||
|
||||
export function encodePng(image: RgbaImage): Uint8Array {
|
||||
const stride = image.width * 4;
|
||||
const scanlines = new Uint8Array((stride + 1) * image.height);
|
||||
for (let y = 0; y < image.height; y++) {
|
||||
const target = y * (stride + 1);
|
||||
scanlines[target] = 0;
|
||||
scanlines.set(image.data.slice(y * stride, (y + 1) * stride), target + 1);
|
||||
}
|
||||
const header = new Uint8Array(13);
|
||||
header.set(u32(image.width), 0);
|
||||
header.set(u32(image.height), 4);
|
||||
header[8] = 8;
|
||||
header[9] = 6;
|
||||
return concat([
|
||||
Uint8Array.of(137, 80, 78, 71, 13, 10, 26, 10),
|
||||
chunk("IHDR", header),
|
||||
chunk("IDAT", deflateStored(scanlines)),
|
||||
chunk("IEND", new Uint8Array()),
|
||||
]);
|
||||
}
|
||||
|
||||
export function bytesToBase64(bytes: Uint8Array): string {
|
||||
let binary = "";
|
||||
const size = 0x8000;
|
||||
for (let offset = 0; offset < bytes.length; offset += size) {
|
||||
binary += String.fromCharCode(...bytes.subarray(offset, Math.min(bytes.length, offset + size)));
|
||||
}
|
||||
return btoa(binary);
|
||||
}
|
||||
|
||||
export function pngDataUri(image: RgbaImage): string {
|
||||
return `data:image/png;base64,${bytesToBase64(encodePng(image))}`;
|
||||
}
|
||||
Reference in New Issue
Block a user