mirror of https://gitgud.io/fatchan/jschan.git
Captcha improvements See merge request fatchan/jschan!258indiachan-spamvector
commit
51de38f757
26 changed files with 448 additions and 119 deletions
@ -0,0 +1,52 @@ |
|||||||
|
const gridv1 = require('./grid.js') |
||||||
|
, gridv2 = require('./grid2.js')
|
||||||
|
, falses = ['○','□','♘','♢','▽','△','♖','✧','♔','♘','♕','♗','♙','♧'] |
||||||
|
, trues = ['●','■','♞','♦','▼','▲','♜','✦','♚','♞','♛','♝','♟','♣']; |
||||||
|
|
||||||
|
const cases = [ |
||||||
|
{ name: '3n grid captcha', grid: { falses, trues, question: 'whatever', size: 3, imageSize: 120, iconYOffset: 0, edge: 0, noise: 0 }, numDistorts: { min: 0, max: 1 }, distortion: 0 }, |
||||||
|
{ name: '4n grid captcha', grid: { falses, trues, question: 'whatever', size: 4, imageSize: 120, iconYOffset: 0, edge: 0, noise: 0 }, numDistorts: { min: 0, max: 1 }, distortion: 0 }, |
||||||
|
{ name: '6n grid captcha', grid: { falses, trues, question: 'whatever', size: 6, imageSize: 120, iconYOffset: 0, edge: 0, noise: 0 }, numDistorts: { min: 0, max: 1 }, distortion: 0 }, |
||||||
|
{ name: '4n grid captcha with distortion', grid: { falses, trues, question: 'whatever', size: 4, imageSize: 120, iconYOffset: 0, edge: 0, noise: 0 }, numDistorts: { min: 1, max: 10 }, distortion: 10 }, |
||||||
|
{ name: '4n grid captcha with edge', grid: { falses, trues, question: 'whatever', size: 4, imageSize: 120, iconYOffset: 0, edge: 10, noise: 0 }, numDistorts: { min: 0, max: 1 }, distortion: 0 }, |
||||||
|
{ name: '4n grid captcha with noise', grid: { falses, trues, question: 'whatever', size: 4, imageSize: 120, iconYOffset: 0, edge: 0, noise: 10 }, numDistorts: { min: 0, max: 1 }, distortion: 0 }, |
||||||
|
{ name: '4n grid captcha with y offset', grid: { falses, trues, question: 'whatever', size: 4, imageSize: 120, iconYOffset: 15, edge: 0, noise: 0 }, numDistorts: { min: 0, max: 1 }, distortion: 0 }, |
||||||
|
{ name: '4n grid captcha with all effects', grid: { falses, trues, question: 'whatever', size: 4, imageSize: 120, iconYOffset: 0, edge: 0, noise: 0 }, numDistorts: { min: 0, max: 1 }, distortion: 0 }, |
||||||
|
{ name: '4n grid captcha with all effects and distortion', grid: { falses, trues, question: 'whatever', size: 4, imageSize: 120, iconYOffset: 150, edge: 10, noise: 10 }, numDistorts: { min: 1, max: 10 }, distortion: 10 }, |
||||||
|
{ name: '250px 6n grid captcha with all effects and distortion', grid: { falses, trues, question: 'whatever', size: 6, imageSize: 250, iconYOffset: 150, edge: 10, noise: 10 }, numDistorts: { min: 1, max: 10 }, distortion: 10 }, |
||||||
|
{ name: '123px 4n grid captcha with all effects and distortion', grid: { falses, trues, question: 'whatever', size: 4, imageSize: 123, iconYOffset: 150, edge: 10, noise: 10 }, numDistorts: { min: 1, max: 10 }, distortion: 10 }, |
||||||
|
{ name: '90px 3n grid captcha with all effects and distortion', grid: { falses, trues, question: 'whatever', size: 3, imageSize: 90, iconYOffset: 150, edge: 10, noise: 10 }, numDistorts: { min: 1, max: 10 }, distortion: 10 }, |
||||||
|
|
||||||
|
]; |
||||||
|
|
||||||
|
describe('generate gridv1 captcha', () => { |
||||||
|
for(let captchaOptions of cases) {
|
||||||
|
test(captchaOptions.name, async () => { |
||||||
|
const { captcha } = await gridv1(captchaOptions); |
||||||
|
expect(await new Promise((res, rej) => { |
||||||
|
captcha.write('/tmp/captcha.jpg', (err) => { |
||||||
|
if (err) { |
||||||
|
return rej(err); |
||||||
|
} |
||||||
|
res(); |
||||||
|
}); |
||||||
|
})); |
||||||
|
}); |
||||||
|
} |
||||||
|
}); |
||||||
|
|
||||||
|
describe('generate gridv2 captcha', () => { |
||||||
|
for(let captchaOptions of cases) {
|
||||||
|
test(captchaOptions.name, async () => { |
||||||
|
const { captcha } = await gridv2(captchaOptions); |
||||||
|
expect(await new Promise((res, rej) => { |
||||||
|
captcha.write('/tmp/captcha.jpg', (err) => { |
||||||
|
if (err) { |
||||||
|
return rej(err); |
||||||
|
} |
||||||
|
res(); |
||||||
|
}); |
||||||
|
})); |
||||||
|
}); |
||||||
|
} |
||||||
|
}); |
@ -0,0 +1,127 @@ |
|||||||
|
'use strict'; |
||||||
|
|
||||||
|
const gm = require('@fatchan/gm').subClass({ imageMagick: true }) |
||||||
|
, { promisify } = require('util') |
||||||
|
, getDistorts = require(__dirname+'/../getdistorts.js') |
||||||
|
, randomRange = promisify(require('crypto').randomInt) |
||||||
|
, randomBytes = promisify(require('crypto').randomBytes) |
||||||
|
, padding = 30 //pad edge of image to account for character size + distortion
|
||||||
|
, nArrows = ['↑', '↟', '↥', '↾', '↿', '⇑', '⇡'] |
||||||
|
, eArrows = ['➸', '→', '➳', '➵', '→', '↛', '↠', '↣', '↦', '↪', '↬', '↱', '↳', '⇉', '⇏', '⇒', '⇛', '⇝', '⇢'] |
||||||
|
, wArrows = [ '←', '↚', '↞', '↜', '↢', '↩', '↤', '↫', '↰', '↲', '↵', '⇇', '⇍', '⇐', '⇚', '⇜', '⇠'] |
||||||
|
, sArrows = ['↓', '↡', '↧', '↴', '⇂', '⇃', '⇊', '⇓', '⇣'] |
||||||
|
, allArrows = [...nArrows, ...eArrows, ...wArrows, ...sArrows] |
||||||
|
, randomBool = async (p) => { return ((await randomBytes(1))[0] > p); } |
||||||
|
, randomOf = async (arr) => { return arr[(await randomRange(0, arr.length))]; }; |
||||||
|
//TODO: last two could belong in lib/misc/(random?)
|
||||||
|
|
||||||
|
module.exports = async (captchaOptions) => { |
||||||
|
|
||||||
|
const { size, trues, falses, imageSize, noise, edge } = captchaOptions.grid; |
||||||
|
const width = imageSize+padding; //TODO: these will never be different, right?
|
||||||
|
const height = imageSize+padding; |
||||||
|
|
||||||
|
const charMatrix = new Array(size).fill(false) |
||||||
|
.map(() => new Array(size).fill(false)); |
||||||
|
const answerMatrix = new Array(size).fill(false) |
||||||
|
.map(() => new Array(size).fill(false)); |
||||||
|
|
||||||
|
//put the icon arrows should point at
|
||||||
|
const correctRow = await randomRange(0, size); |
||||||
|
const correctCol = await randomRange(0, size); |
||||||
|
charMatrix[correctRow][correctCol] = await randomOf(trues); |
||||||
|
|
||||||
|
//put correct and incorrect arrows in the row/column
|
||||||
|
const numArray = [...new Array(size).keys()]; |
||||||
|
const perpendicularRows = numArray.filter(x => x !== correctRow); |
||||||
|
for (let row of perpendicularRows) { |
||||||
|
/*TODO: necessary to memoize these "inverse" sets of arrows? or maybe instead of even doing a 50/50 |
||||||
|
random, it should just pick a random from allArrows then set the isCorrect based on if its in the correct set?*/ |
||||||
|
let arrows; |
||||||
|
const isCorrect = await randomBool(127); |
||||||
|
if (row < correctRow) { |
||||||
|
arrows = isCorrect ? sArrows : [...nArrows, ...eArrows, ...wArrows]; |
||||||
|
} else if (row > correctRow) { |
||||||
|
arrows = isCorrect ? nArrows : [...sArrows, ...eArrows, ...wArrows]; |
||||||
|
} |
||||||
|
charMatrix[row][correctCol] = await randomOf(arrows); |
||||||
|
answerMatrix[row][correctCol] = isCorrect;
|
||||||
|
} |
||||||
|
const perpendicularCols = numArray.filter(x => x !== correctCol); |
||||||
|
for (let col of perpendicularCols) { |
||||||
|
let arrows; |
||||||
|
const isCorrect = await randomBool(127); |
||||||
|
if (col < correctCol) { |
||||||
|
arrows = isCorrect ? eArrows : [...sArrows, ...nArrows, ...wArrows]; |
||||||
|
} else if (col > correctCol) { |
||||||
|
arrows = isCorrect ? wArrows : [...sArrows, ...nArrows, ...eArrows]; |
||||||
|
} |
||||||
|
charMatrix[correctRow][col] = await randomOf(arrows); |
||||||
|
answerMatrix[correctRow][col] = isCorrect; |
||||||
|
} |
||||||
|
//TODO: diagonals? need more arrows
|
||||||
|
|
||||||
|
//this sucks
|
||||||
|
if (!answerMatrix.flat().some(x => x === true)) { |
||||||
|
if ((await randomBool(127)) === true) { |
||||||
|
const randomRow = await randomOf(perpendicularRows); |
||||||
|
const arrows = randomRow < correctRow ? sArrows : nArrows; |
||||||
|
charMatrix[randomRow][correctCol] = await randomOf(arrows); |
||||||
|
answerMatrix[randomRow][correctCol] = true; |
||||||
|
} else { |
||||||
|
const randomCol = await randomOf(perpendicularCols); |
||||||
|
const arrows = randomCol < correctCol ? eArrows : wArrows; |
||||||
|
charMatrix[correctRow][randomCol] = await randomOf(arrows); |
||||||
|
answerMatrix[correctRow][randomCol] = true; |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
//fill the rest with junk arrows/falses
|
||||||
|
for (let row = 0; row < size; row++) { |
||||||
|
for (let col = 0; col < size; col++) { |
||||||
|
if (charMatrix[row][col] === false) { |
||||||
|
charMatrix[row][col] = (await randomBool(80)) |
||||||
|
? (await randomOf(allArrows)) |
||||||
|
: (await randomOf(falses)); |
||||||
|
} |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
const captcha = gm(width, height, '#ffffff') |
||||||
|
.fill('#000000') |
||||||
|
.font(__dirname+'/../font.ttf'); |
||||||
|
|
||||||
|
const spaceSize = (width-padding)/size; |
||||||
|
for (let row = 0; row < size; row++) { |
||||||
|
let cxOffset = Math.floor(spaceSize * 1.5); |
||||||
|
for (let col = 0; col < size; col++) { |
||||||
|
const cyOffset = captchaOptions.grid.iconYOffset; |
||||||
|
captcha.fontSize((await randomRange(20, 30))); |
||||||
|
captcha.drawText( |
||||||
|
(spaceSize * col) + cyOffset, |
||||||
|
(spaceSize * row) + cxOffset, |
||||||
|
charMatrix[row][col] |
||||||
|
);
|
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
//create an array of distortions and apply to the image, if distortion is enabled
|
||||||
|
const { distortion, numDistorts } = captchaOptions; |
||||||
|
if (distortion > 0) { |
||||||
|
const distorts = await getDistorts(width, height, numDistorts, distortion); |
||||||
|
captcha.distort(distorts, 'Shepards'); |
||||||
|
} |
||||||
|
|
||||||
|
//add optional edge effect
|
||||||
|
if (edge > 0) { |
||||||
|
captcha.edge(edge); |
||||||
|
} |
||||||
|
|
||||||
|
//add optional noise effect
|
||||||
|
if (noise > 0) { |
||||||
|
captcha.noise(noise); |
||||||
|
} |
||||||
|
|
||||||
|
return { captcha, solution: answerMatrix.flat() }; |
||||||
|
|
||||||
|
}; |
@ -0,0 +1,28 @@ |
|||||||
|
const generateCaptcha = require('./text.js'); |
||||||
|
|
||||||
|
describe('generate text captcha', () => { |
||||||
|
const cases = [ |
||||||
|
{ name: 'text captcha', text: { font: 'default', wave: 0, line: false, paint: 0, noise: 0 }, numDistorts: { min: 0, max: 1 }, distortion: 0 }, |
||||||
|
{ name: 'text captcha with distortion', text: { font: 'default', wave: 0, line: false, paint: 0, noise: 0 }, numDistorts: { min: 1, max: 10 }, distortion: 10 }, |
||||||
|
{ name: 'text captcha with wave', text: { font: 'default', wave: 5, line: false, paint: 0, noise: 0 }, numDistorts: { min: 0, max: 1 }, distortion: 0 }, |
||||||
|
{ name: 'text captcha with line', text: { font: 'default', wave: 0, line: true, paint: 0, noise: 0 }, numDistorts: { min: 0, max: 1 }, distortion: 0 }, |
||||||
|
{ name: 'text captcha with paint', text: { font: 'default', wave: 0, line: false, paint: 5, noise: 0 }, numDistorts: { min: 0, max: 1 }, distortion: 0 }, |
||||||
|
{ name: 'text captcha with noise', text: { font: 'default', wave: 0, line: false, paint: 0, noise: 5 }, numDistorts: { min: 0, max: 1 }, distortion: 0 }, |
||||||
|
{ name: 'text captcha with with all effects and distortion', text: { font: 'default', wave: 5, line: true, paint: 5, noise: 5 }, numDistorts: { min: 1, max: 10 }, distortion: 10 }, |
||||||
|
{ name: 'text captcha with non-default font', text: { font: '/usr/share/fonts/type1/gsfonts/p052003l.pfb', wave: 0, line: false, paint: 0, noise: 0 }, numDistorts: { min: 0, max: 1 }, distortion: 0 }, |
||||||
|
{ name: 'text captcha with all the above', text: { font: '/usr/share/fonts/type1/gsfonts/p052003l.pfb', wave: 5, line: true, paint: 5, noise: 5 }, numDistorts: { min: 1, max: 10 }, distortion: 10 }, |
||||||
|
]; |
||||||
|
for(let captchaOptions of cases) {
|
||||||
|
test(captchaOptions.name, async () => { |
||||||
|
const { captcha } = await generateCaptcha(captchaOptions); |
||||||
|
expect(await new Promise((res, rej) => { |
||||||
|
captcha.write('/tmp/captcha.jpg', (err) => { |
||||||
|
if (err) { |
||||||
|
return rej(err); |
||||||
|
} |
||||||
|
res(); |
||||||
|
}); |
||||||
|
})); |
||||||
|
}); |
||||||
|
} |
||||||
|
}); |
@ -0,0 +1,21 @@ |
|||||||
|
'use strict'; |
||||||
|
|
||||||
|
const fontList = require('child_process') |
||||||
|
.execSync('fc-list -f "%{file}:%{family[0]} %{style[0]}\n"') |
||||||
|
.toString() |
||||||
|
.split('\n') //split by newlines, like here ^
|
||||||
|
.filter(line => line) //filter empty lines
|
||||||
|
.map(line => { |
||||||
|
//map to an object with path and name
|
||||||
|
const [path, name] = line.split(':'); |
||||||
|
return { path, name }; |
||||||
|
}) |
||||||
|
.sort((a, b) => { |
||||||
|
//alphabetical name sort
|
||||||
|
return a.name.localeCompare(b.name); |
||||||
|
}); |
||||||
|
|
||||||
|
module.exports = { |
||||||
|
fontList, |
||||||
|
fontPaths: new Set(['default', ...fontList.map(f => f.path)]), //memoize paths
|
||||||
|
}; |
Loading…
Reference in new issue