diff --git a/src/assets/captcha/bg/A Large Body of Water Surrounded By Mountains.jpg b/src/assets/captcha/bg/A Large Body of Water Surrounded By Mountains.jpg new file mode 100644 index 00000000..91430877 Binary files /dev/null and b/src/assets/captcha/bg/A Large Body of Water Surrounded By Mountains.jpg differ diff --git a/src/assets/captcha/bg/A Trail of Footprints In The Sand.jpg b/src/assets/captcha/bg/A Trail of Footprints In The Sand.jpg new file mode 100644 index 00000000..a2bf0463 Binary files /dev/null and b/src/assets/captcha/bg/A Trail of Footprints In The Sand.jpg differ diff --git a/src/assets/captcha/bg/Ashim DSilva.jpg b/src/assets/captcha/bg/Ashim DSilva.jpg new file mode 100644 index 00000000..2a1f85a4 Binary files /dev/null and b/src/assets/captcha/bg/Ashim DSilva.jpg differ diff --git a/src/assets/captcha/bg/Canazei Granite Ridges.jpg b/src/assets/captcha/bg/Canazei Granite Ridges.jpg new file mode 100644 index 00000000..b8b10abe Binary files /dev/null and b/src/assets/captcha/bg/Canazei Granite Ridges.jpg differ diff --git a/src/assets/captcha/bg/Martin Adams.jpg b/src/assets/captcha/bg/Martin Adams.jpg new file mode 100644 index 00000000..1a9040d6 Binary files /dev/null and b/src/assets/captcha/bg/Martin Adams.jpg differ diff --git a/src/assets/captcha/bg/Morskie Oko.jpg b/src/assets/captcha/bg/Morskie Oko.jpg new file mode 100644 index 00000000..0a0acdf2 Binary files /dev/null and b/src/assets/captcha/bg/Morskie Oko.jpg differ diff --git a/src/assets/captcha/bg/Mr. Lee.jpg b/src/assets/captcha/bg/Mr. Lee.jpg new file mode 100644 index 00000000..1b91ece5 Binary files /dev/null and b/src/assets/captcha/bg/Mr. Lee.jpg differ diff --git a/src/assets/captcha/bg/Nattu Adnan.jpg b/src/assets/captcha/bg/Nattu Adnan.jpg new file mode 100644 index 00000000..4017b8f6 Binary files /dev/null and b/src/assets/captcha/bg/Nattu Adnan.jpg differ diff --git a/src/assets/captcha/bg/Photo by SpaceX.jpg b/src/assets/captcha/bg/Photo by SpaceX.jpg new file mode 100644 index 00000000..72ec3286 Binary files /dev/null and b/src/assets/captcha/bg/Photo by SpaceX.jpg differ diff --git a/src/assets/captcha/bg/Photo of Valley.jpg b/src/assets/captcha/bg/Photo of Valley.jpg new file mode 100644 index 00000000..ccab4249 Binary files /dev/null and b/src/assets/captcha/bg/Photo of Valley.jpg differ diff --git a/src/assets/captcha/bg/Snow-Capped Mountain.jpg b/src/assets/captcha/bg/Snow-Capped Mountain.jpg new file mode 100644 index 00000000..cb01d6ee Binary files /dev/null and b/src/assets/captcha/bg/Snow-Capped Mountain.jpg differ diff --git a/src/assets/captcha/bg/Sunset by the Pier.jpg b/src/assets/captcha/bg/Sunset by the Pier.jpg new file mode 100644 index 00000000..d2ce7269 Binary files /dev/null and b/src/assets/captcha/bg/Sunset by the Pier.jpg differ diff --git a/src/assets/captcha/bg/Tj Holowaychuk.jpg b/src/assets/captcha/bg/Tj Holowaychuk.jpg new file mode 100644 index 00000000..3524983e Binary files /dev/null and b/src/assets/captcha/bg/Tj Holowaychuk.jpg differ diff --git a/src/assets/captcha/bg/Viktor Forgacs.jpg b/src/assets/captcha/bg/Viktor Forgacs.jpg new file mode 100644 index 00000000..740e1f79 Binary files /dev/null and b/src/assets/captcha/bg/Viktor Forgacs.jpg differ diff --git a/src/assets/captcha/bg/copyright.txt b/src/assets/captcha/bg/copyright.txt new file mode 100644 index 00000000..04374e83 --- /dev/null +++ b/src/assets/captcha/bg/copyright.txt @@ -0,0 +1,22 @@ +Unsplash photos published before June 8, 2017 are CC0 (public domain): + +Ashim D'Silva +Canazei Granite Ridges +Mr. Lee +Photo by SpaceX +Sunset by the Pier + +Unsplash photos published on or after June 8, 2017 are free to use, modify, and redistribute subject to the Unsplash license : + +Martin Adams +Morskie Oko +Nattu Adnan +Tj Holowaychuk +Viktor Forgacs +“A Large Body of Water Surrounded By Mountains” by Peter Thomas +“A Trail of Footprints In The Sand” by David Emrich +“Photo of Valley” by Aniket Doele + +Pexels photos are free to use, modify, and redistribute subject to the Pexels license : + +Snow-Capped Mountain diff --git a/src/assets/captcha/bg/tj-holowaychuk.jpg b/src/assets/captcha/bg/tj-holowaychuk.jpg deleted file mode 100644 index e00526d5..00000000 Binary files a/src/assets/captcha/bg/tj-holowaychuk.jpg and /dev/null differ diff --git a/src/assets/captcha/puzzle-hole.png b/src/assets/captcha/puzzle-hole.png index 89bb8608..2dd9bb0b 100644 Binary files a/src/assets/captcha/puzzle-hole.png and b/src/assets/captcha/puzzle-hole.png differ diff --git a/src/assets/captcha/puzzle-mask.png b/src/assets/captcha/puzzle-mask.png index 3171f4b3..ffc3a78e 100644 Binary files a/src/assets/captcha/puzzle-mask.png and b/src/assets/captcha/puzzle-mask.png differ diff --git a/src/controllers/api/captcha.ts b/src/controllers/api/captcha.ts index 4dfaacc7..b93b58a8 100644 --- a/src/controllers/api/captcha.ts +++ b/src/controllers/api/captcha.ts @@ -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(); +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 { + 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 { + 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);