captcha: show a random image, preload backgrounds into memory

This commit is contained in:
Alex Gleason 2024-10-04 16:40:52 -05:00
parent c81005a050
commit 18f1a94520
No known key found for this signature in database
GPG key ID: 7211D1F99744FBB7
19 changed files with 66 additions and 15 deletions

Binary file not shown.

After

Width:  |  Height:  |  Size: 26 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 16 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 24 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 32 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 28 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 13 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 17 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 20 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 30 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 22 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 16 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 19 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 26 KiB

View file

@ -0,0 +1,22 @@
Unsplash photos published before June 8, 2017 are CC0 (public domain):
Ashim D'Silva <https://unsplash.com/photos/WeYamle9fDM>
Canazei Granite Ridges <https://unsplash.com/photos/yrwpJwDNSHE>
Mr. Lee <https://unsplash.com/photos/v7r8kZStqFw>
Photo by SpaceX <https://unsplash.com/photos/VBNb52J8Trk>
Sunset by the Pier <https://unsplash.com/photos/ces8_Bo7bhQ>
Unsplash photos published on or after June 8, 2017 are free to use, modify, and redistribute subject to the Unsplash license <https://unsplash.com/license>:
Martin Adams <https://unsplash.com/photos/MpTdvXlAsVE>
Morskie Oko <https://unsplash.com/photos/_1UF_3TlKcQ>
Nattu Adnan <https://unsplash.com/photos/Ai2TRdvI6gM>
Tj Holowaychuk <https://unsplash.com/photos/iGrsa9rL11o>
Viktor Forgacs <https://unsplash.com/photos/q8XSCZYh6D8>
“A Large Body of Water Surrounded By Mountains” by Peter Thomas <https://unsplash.com/photos/Dxod5pdRtsk>
“A Trail of Footprints In The Sand” by David Emrich <https://unsplash.com/photos/A9mr3TPoj0k>
“Photo of Valley” by Aniket Doele <https://unsplash.com/photos/M6XC789HLe8>
Pexels photos are free to use, modify, and redistribute subject to the Pexels license <https://www.pexels.com/license/>:
Snow-Capped Mountain <https://www.pexels.com/photo/photo-of-snow-capped-mountain-during-evening-2440024/>

Binary file not shown.

Before

Width:  |  Height:  |  Size: 22 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.3 KiB

After

Width:  |  Height:  |  Size: 1.2 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1 KiB

After

Width:  |  Height:  |  Size: 997 B

View file

@ -1,4 +1,4 @@
import { createCanvas, loadImage } from '@gfx/canvas-wasm';
import { createCanvas, Image, loadImage } from '@gfx/canvas-wasm';
import TTLCache from '@isaacs/ttlcache';
import { z } from 'zod';
@ -17,16 +17,15 @@ interface Dimensions {
}
const captchas = new TTLCache<string, Point>();
const imagesAsync = getImages();
const BG_SIZE = { w: 370, h: 400 };
const PUZZLE_SIZE = { w: 65, h: 65 };
/** Puzzle captcha controller. */
export const captchaController: AppController = async (c) => {
const { bg, puzzle, solution } = await generateCaptcha(
await Deno.readFile(new URL('../../assets/captcha/bg/tj-holowaychuk.jpg', import.meta.url)),
await Deno.readFile(new URL('../../assets/captcha/puzzle-mask.png', import.meta.url)),
await Deno.readFile(new URL('../../assets/captcha/puzzle-hole.png', import.meta.url)),
const { bg, puzzle, solution } = generateCaptcha(
await imagesAsync,
BG_SIZE,
PUZZLE_SIZE,
);
@ -47,11 +46,44 @@ export const captchaController: AppController = async (c) => {
});
};
interface CaptchaImages {
bgImages: Image[];
puzzleMask: Image;
puzzleHole: Image;
}
async function getImages(): Promise<CaptchaImages> {
const bgImages = await getBackgroundImages();
const puzzleMask = await loadImage(
await Deno.readFile(new URL('../../assets/captcha/puzzle-mask.png', import.meta.url)),
);
const puzzleHole = await loadImage(
await Deno.readFile(new URL('../../assets/captcha/puzzle-hole.png', import.meta.url)),
);
return { bgImages, puzzleMask, puzzleHole };
}
async function getBackgroundImages(): Promise<Image[]> {
const path = new URL('../../assets/captcha/bg/', import.meta.url);
const images: Image[] = [];
for await (const dirEntry of Deno.readDir(path)) {
if (dirEntry.isFile && dirEntry.name.endsWith('.jpg')) {
const file = await Deno.readFile(new URL(dirEntry.name, path));
const image = await loadImage(file);
images.push(image);
}
}
return images;
}
/** Generate a puzzle captcha, returning canvases for the board and piece. */
async function generateCaptcha(
from: Uint8Array,
mask: Uint8Array,
hole: Uint8Array,
function generateCaptcha(
{ bgImages, puzzleMask, puzzleHole }: CaptchaImages,
bgSize: Dimensions,
puzzleSize: Dimensions,
) {
@ -61,19 +93,16 @@ async function generateCaptcha(
const ctx = bg.getContext('2d');
const pctx = puzzle.getContext('2d');
const bgImage = await loadImage(from);
const maskImage = await loadImage(mask);
const holeImage = await loadImage(hole);
const solution = generateSolution(bgSize, puzzleSize);
const bgImage = bgImages[Math.floor(Math.random() * bgImages.length)];
// Draw the background image.
ctx.drawImage(bgImage, 0, 0, bg.width, bg.height);
ctx.globalCompositeOperation = 'source-atop';
ctx.drawImage(holeImage, solution.x, solution.y, puzzle.width, puzzle.height);
ctx.drawImage(puzzleHole, solution.x, solution.y, puzzle.width, puzzle.height);
// Draw the puzzle piece.
pctx.drawImage(maskImage, 0, 0, puzzle.width, puzzle.height);
pctx.drawImage(puzzleMask, 0, 0, puzzle.width, puzzle.height);
pctx.globalCompositeOperation = 'source-in';
pctx.drawImage(bgImage, solution.x, solution.y, puzzle.width, puzzle.height, 0, 0, puzzle.width, puzzle.height);