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