diff --git a/src/main.ts b/src/main.ts index bda0cfc..ee02adc 100644 --- a/src/main.ts +++ b/src/main.ts @@ -124,8 +124,143 @@ function calculateDisorder(imgdata: ImageData) { return res / (total === 0 ? 1 : total); } +/* + * decide if a pixel is closer to black than to white. + * return 0 for white, 1 for black + */ +function pxlBlackOrWhite(r: number, g: number, b: number) { + return (r + g + b > 384) ? 0 : 1; +} + +/* + * Get bordering pixels of transparent areas (the outline of the circles) + * and return their coordinates with the neighboring color. + */ +function getBoundries(imgdata: ImageData) { + const data = imgdata.data; + const width = imgdata.width; + + let i = data.length - 1; + let cl = 0; + let cr = 0; + const chkArray = []; + let opq = true; + while (i > 0) { + // alpha channel above 128 is assumed opaque + const a = data[i] > 128; + if (a !== opq) { + if ((data[i - 4] > 128) === opq) { + // ignore just 1-width areas + i -= 4; + continue; + } + if (a) { + /* transparent pixel to its right */ + /* + // set to color blue (for debugging) + data[i + 4] = 255; + data[i + 3] = 255; + data[i + 2] = 0; + data[i + 1] = 0; + */ + const pos = (i + 1) / 4; + const x = pos % width; + const y = (pos - x) / width; + // 1: black, 0: white + const clr = pxlBlackOrWhite(data[i - 1], data[i - 2], data[i - 3]); + chkArray.push([x, y, clr]); + cr += 1; + } else { + /* opaque pixel to its right */ + /* + // set to color red (for debugging) + data[i] = 255; + data[i - 1] = 0; + data[i - 2] = 0; + data[i - 3] = 255; + */ + const pos = (i - 3) / 4; + const x = pos % width; + const y = (pos - x) / width; + // 1: black, 0: white + const clr = pxlBlackOrWhite(data[i + 1], data[i + 2], data[i + 3]); + chkArray.push([x, y, clr]); + cl += 1; + } + opq = a; + } + i -= 4; + } + return chkArray; +} +/* + * slide the background image and compare the colors of the border pixels in + * chkArray, the position with the most matches wins + * Return in slider-percentage. + */ +function getBestPos(bgdata: ImageData, chkArray: number[][], slideWidth: number) { + const data = bgdata.data; + const width = bgdata.width; + let bestSimilarity = 0; + let bestPos = 0; + + for (let s = 0; s <= slideWidth; s += 1) { + let similarity = 0; + const amount = chkArray.length; + for (let p = 0; p < amount; p += 1) { + const chk = chkArray[p]; + const x = chk[0] + s; + const y = chk[1]; + const clr = chk[2]; + const off = (y * width + x) * 4; + const bgclr = pxlBlackOrWhite(data[off], data[off + 1], data[off + 2]); + if (bgclr === clr) { + similarity += 1; + } + } + if (similarity > bestSimilarity) { + bestSimilarity = similarity; + bestPos = s; + } + } + return bestPos / slideWidth * 100; +} + +async function getImageDataFromURI(uri: string) { + const image = await imageFromUri(uri); + if (!image) + throw new Error("No image"); + const canvas = document.createElement('canvas'); + canvas.width = image.width; + canvas.height = image.height; + const ctx = canvas.getContext('2d')!; + ctx.drawImage(image, 0, 0); + return ctx.getImageData(0, 0, canvas.width, canvas.height); +} + +async function slideCaptcha(tfgElement: HTMLElement, tbgElement: HTMLElement, sliderElement: HTMLInputElement) { + // get data uris for captcha back- and foreground + const tbgUri = tbgElement.style.backgroundImage.slice(5, -2); + const tfgUri = tfgElement.style.backgroundImage.slice(5, -2); + + // load foreground (image with holes) + const igd = await getImageDataFromURI(tfgUri); + // get array with pixels of foreground + // that we compare to background + const chkArray = getBoundries(igd); + // load background (image that gets slid) + const sigd = await getImageDataFromURI(tbgUri); + const slideWidth = sigd.width - igd.width; + // slide, compare and get best matching position + const sliderPos = getBestPos(sigd, chkArray, slideWidth); + // slide in the UI + sliderElement.value = '' + sliderPos; + (sliderElement as any).dispatchEvent(new Event('input'), { bubbles: true }); + return 0 - (sliderPos / 2); +} + // returns ImageData from captcha's background image, foreground image, and offset (ranging from 0 to -50) -function imageFromCanvas(img: HTMLImageElement, bg: HTMLImageElement, off: number) { +async function imageFromCanvas(img: HTMLImageElement, bg: HTMLImageElement, off: number | null) { const h = img.height; const w = img.width; const th = 80; @@ -163,44 +298,11 @@ function imageFromCanvas(img: HTMLImageElement, bg: HTMLImageElement, off: numbe ctx.drawImage(img, -w / 2, -h / 2, w, h); }; - // if off is not specified and background image is present, try to figure out - // the best offset automatically; select the offset that has smallest value of - // calculateDisorder for the resulting image if (bg && off == null) { - let bestDisorder = 999; - let bestImagedata: ImageData | null = null; - let bestOff = -1; - - for (let off = 0; off >= -50; off--) { - draw(off); - - let imgdata = ctx.getImageData(0, 0, canvas.width, canvas.height); - const disorder = calculateDisorder(imgdata); - - if (disorder < bestDisorder) { - bestDisorder = disorder; - draw(off); - imgdata = ctx.getImageData(0, 0, canvas.width, canvas.height); - bestImagedata = imgdata; - bestOff = off; - } - } - - // not the best idea to do this here - setTimeout(function () { - const bg = document.getElementById('t-bg'); - const slider = document.getElementById('t-slider') as HTMLInputElement; - if (!bg || !slider) return; - - slider.value = '' + (-bestOff * 2); - bg.style.backgroundPositionX = bestOff + 'px'; - }, 1); - draw(bestOff); - return bestImagedata; - } else { - draw(off); - return ctx.getImageData(0, 0, canvas.width, canvas.height); + off = await slideCaptcha(document.getElementById('t-fg')!, document.getElementById('t-bg')!, document.getElementById('t-slider') as HTMLInputElement); } + draw(off || 0); + return ctx.getImageData(0, 0, canvas.width, canvas.height); } // for debugging purposes @@ -306,7 +408,7 @@ async function predict(img: HTMLImageElement, bg: HTMLImageElement, off: number) if (!model) { model = await load(); } - const image = imageFromCanvas(img, bg, off); + const image = await imageFromCanvas(img, bg, off); if (!image) throw new Error("Failed to gen image"); const mono = toMonochrome(image.data);