Merge character paths and join them with random lines
Remove color option Make stroke and fill styling optional Change README and links to this repo, but refer to the original svg-captcha in README Change example pics to our current captcha
This commit is contained in:
parent
a5257ace35
commit
7c6601d2d5
|
@ -1,3 +1,4 @@
|
|||
/node_modules
|
||||
test.*
|
||||
|
||||
\.idea/
|
||||
|
|
13
HISTORY.md
13
HISTORY.md
|
@ -1,3 +1,14 @@
|
|||
1.6.0 / 2021-02-03
|
||||
==================
|
||||
|
||||
* Merge character paths together
|
||||
* Join them with a randomly generated path
|
||||
|
||||
1.5.0 / 2020-03-23
|
||||
==================
|
||||
|
||||
* Fixed [#45](https://github.com/produck/svg-captcha/issues/45)
|
||||
|
||||
1.4.0 / 2019-05-03
|
||||
===================
|
||||
|
||||
|
@ -32,4 +43,4 @@
|
|||
===================
|
||||
|
||||
* Bump `opentype.js@0.7.1`
|
||||
* Fixed some code style inconsistence
|
||||
* Fixed some code style inconsistence
|
||||
|
|
62
README.md
62
README.md
|
@ -1,17 +1,26 @@
|
|||
![svg-captcha](media/header.png)
|
||||
|
||||
<div align="center">
|
||||
# ppfun-captcha
|
||||
|
||||
[![Build Status](https://img.shields.io/travis/lemonce/svg-captcha/master.svg?style=flat-square)](https://travis-ci.org/lemonce/svg-captcha)
|
||||
[![NPM Version](https://img.shields.io/npm/v/svg-captcha.svg?style=flat-square)](https://www.npmjs.com/package/svg-captcha)
|
||||
[![NPM Downloads](https://img.shields.io/npm/dm/svg-captcha.svg?style=flat-square)](https://www.npmjs.com/package/svg-captcha)
|
||||
|
||||
</div>
|
||||
> generate single-path svg captcha in node.js
|
||||
|
||||
> generate svg captcha in node.js
|
||||
## sample image
|
||||
|
||||
## Translations
|
||||
[中文](README_CN.md)
|
||||
default captcha image:
|
||||
|
||||
![image](media/example.png)
|
||||
|
||||
with using fill instead of stroke:
|
||||
|
||||
![image2](media/example-2.png)
|
||||
|
||||
## Origin
|
||||
|
||||
Credits go to the original author [produck](https://github.com/produck).
|
||||
This is a fork with added features and fixes for [svg-captcha](https://github.com/produck/svg-captcha) which didn't merge important [Bugfixes](https://github.com/produck/svg-captcha/pull/47) and is at the current published state [cracked](https://github.com/produck/svg-captcha/issues/45).
|
||||
If you want the original svg-capcha, check out [svg-captcha-fixed](https://www.npmjs.com/package/svg-captcha-fixed).
|
||||
|
||||
## useful if you
|
||||
|
||||
|
@ -19,19 +28,23 @@
|
|||
- have issue with install c++ addon
|
||||
|
||||
## install
|
||||
|
||||
```
|
||||
npm install --save svg-captcha
|
||||
npm install --save ppfun-captcha
|
||||
```
|
||||
|
||||
## usage
|
||||
|
||||
```Javascript
|
||||
var svgCaptcha = require('svg-captcha');
|
||||
var svgCaptcha = require('ppfun-captcha');
|
||||
|
||||
var captcha = svgCaptcha.create();
|
||||
console.log(captcha);
|
||||
// {data: '<svg.../svg>', text: 'abcd'}
|
||||
```
|
||||
|
||||
with express
|
||||
|
||||
```Javascript
|
||||
var svgCaptcha = require('svg-captcha');
|
||||
|
||||
|
@ -51,8 +64,9 @@ If no option is passed, you will get a random string of four characters and corr
|
|||
|
||||
* `size`: 4 // size of random string
|
||||
* `ignoreChars`: '0o1i' // filter out some characters like 0o1i
|
||||
* `color`: true // characters will have distinct colors instead of grey, true if background option is set
|
||||
* `background`: '#cc9966' // background color of the svg image
|
||||
* `stroke`: 'black' // style/color of the svg path stroke
|
||||
* `fill`: 'black' // style/color of the svg strokes fill
|
||||
|
||||
This function returns an object that has the following property:
|
||||
* `data`: string // svg path data
|
||||
|
@ -93,30 +107,24 @@ return a svg captcha based on text provided.
|
|||
In pre 1.1.0 version you have to call these two functions,
|
||||
now you can call create() to save some key strokes ;).
|
||||
|
||||
## sample image
|
||||
default captcha image:
|
||||
## FAQ
|
||||
|
||||
![image](media/example.png)
|
||||
|
||||
math expression image with color options:
|
||||
|
||||
![image2](media/example-2.png)
|
||||
|
||||
## why use svg?
|
||||
|
||||
It does not require any c++ addon.
|
||||
The result image is smaller than jpeg image.
|
||||
|
||||
> This has to be a joke. /\<text.+\>;.+\<\/text\>/g.test...
|
||||
### This has to be a joke. /\<text.+\>;.+\<\/text\>/g.test...
|
||||
|
||||
svg captcha uses opentype.js underneath, which means that there is no
|
||||
'<text>1234</text>'.
|
||||
You get
|
||||
'<path fill="#444" d="M104.83 19.74L107.85 19.74L112 33.56L116.13 19.74L119.15 19.74L113.48 36.85...'
|
||||
instead.
|
||||
|
||||
Even though you can write a program that convert svg to png, svg captcha has done its job
|
||||
—— make captcha recognition harder
|
||||
|
||||
### Why no noise line option?
|
||||
|
||||
Noise lines can be easily filtered by path length and we decided to rather join characters with random paths, in order to to make the whole svg one single continuous path, which has the same purpose.
|
||||
|
||||
### Why no colors?
|
||||
|
||||
Colors don't add any security and can be easily changed to black & white anyway. The color of the stroke and fill can be manually choosen if it is neccessary for custom styles.
|
||||
|
||||
## License
|
||||
|
||||
[MIT](LICENSE.md)
|
||||
|
|
|
@ -23,12 +23,16 @@ declare class ConfigObject {
|
|||
* random character preset
|
||||
*/
|
||||
charPreset?: string;
|
||||
/**
|
||||
* default: false
|
||||
* if false, captcha will be black and white
|
||||
* otherwise, it will be randomly colorized
|
||||
*/
|
||||
color?: boolean;
|
||||
/**
|
||||
* default: 'black'
|
||||
* Stroke Style of svg path, can be color or none
|
||||
*/
|
||||
stroke?: string;
|
||||
/**
|
||||
* default: 'none'
|
||||
* Fill Style of svg path, can be color or none
|
||||
*/
|
||||
fill?: string;
|
||||
/**
|
||||
* default: false
|
||||
* if set to true, it will draw with light grey color
|
||||
|
|
|
@ -1,141 +1,6 @@
|
|||
'use strict';
|
||||
const assert = require('assert');
|
||||
|
||||
function rndPathCmd(cmd) {
|
||||
const r = (Math.random() * 0.2) - 0.1;
|
||||
|
||||
switch (cmd.type) {
|
||||
case 'M':
|
||||
case 'L':
|
||||
cmd.x += r;
|
||||
cmd.y += r;
|
||||
break;
|
||||
case 'Q':
|
||||
case 'C':
|
||||
cmd.x += r;
|
||||
cmd.y += r;
|
||||
cmd.x1 += r;
|
||||
cmd.y1 += r;
|
||||
break;
|
||||
default:
|
||||
// Close path cmd
|
||||
break;
|
||||
}
|
||||
|
||||
return cmd;
|
||||
}
|
||||
|
||||
//https://riptutorial.com/zh-CN/html5-canvas/example/19077/%E5%9C%A8%E4%BD%8D%E7%BD%AE%E6%8B%86%E5%88%86%E8%B4%9D%E5%A1%9E%E5%B0%94%E6%9B%B2%E7%BA%BF
|
||||
function splitQuadraticBezier(position, x1, y1, x2, y2, x3, y3) {
|
||||
let v1, v2, v3, retPoints, i, c;
|
||||
|
||||
if (position <= 0 || position >= 1) {
|
||||
throw RangeError("spliteCurveAt requires position > 0 && position < 1");
|
||||
}
|
||||
|
||||
retPoints = []; // array of coordinates
|
||||
i = 0;
|
||||
v1 = {};
|
||||
v2 = {};
|
||||
v3 = {};
|
||||
v1.x = x1;
|
||||
v1.y = y1;
|
||||
v2.x = x2;
|
||||
v2.y = y2;
|
||||
v3.x = x3;
|
||||
v3.y = y3;
|
||||
|
||||
c = position;
|
||||
retPoints[i++] = v1.x; // start point
|
||||
retPoints[i++] = v1.y;
|
||||
retPoints[i++] = (v1.x += (v2.x - v1.x) * c); // new control point for first curve
|
||||
retPoints[i++] = (v1.y += (v2.y - v1.y) * c);
|
||||
v2.x += (v3.x - v2.x) * c;
|
||||
v2.y += (v3.y - v2.y) * c;
|
||||
retPoints[i++] = v1.x + (v2.x - v1.x) * c; // new end and start of first and second curves
|
||||
retPoints[i++] = v1.y + (v2.y - v1.y) * c;
|
||||
retPoints[i++] = v2.x; // new control point for second curve
|
||||
retPoints[i++] = v2.y;
|
||||
retPoints[i++] = v3.x; // new endpoint of second curve
|
||||
retPoints[i++] = v3.y;
|
||||
return retPoints;
|
||||
}
|
||||
|
||||
function randomRange(min, max) {
|
||||
return Math.random() * (max - min) + min;
|
||||
}
|
||||
|
||||
function randomizePathNodes(commands, opts) {
|
||||
// 随机化路径节点
|
||||
// 规则:
|
||||
// 如果当前节点是 L(Line),下一个节点也是 Line,那么随机插入一个中间点
|
||||
// 如果当前节点是 Q,且前节点为 L 或 M,那么拆分这个曲线
|
||||
const result = [];
|
||||
for (let i = 0; i < commands.length - 1; i++) {
|
||||
const command = commands[i];
|
||||
if (command.type === "L") {
|
||||
const next = commands[i + 1];
|
||||
if (next.type === "L" && Math.random() > opts.truncateLineProbability) {
|
||||
const r = randomRange(-0.1, 0.1);
|
||||
result.push(command);
|
||||
result.push({
|
||||
type: "L",
|
||||
x: (command.x + next.x) / 2 + r,
|
||||
y: (command.y + next.y) / 2 + r,
|
||||
});
|
||||
} else {
|
||||
result.push(command);
|
||||
}
|
||||
} else if (command.type === "Q" && i >= 1) {
|
||||
const prev = commands[i - 1];
|
||||
if ((prev.type === "L" || prev.type === "M") && Math.random() > opts.truncateCurveProbability) {
|
||||
const p0_x = prev.x;
|
||||
const p0_y = prev.y;
|
||||
const r = randomRange(-0.1, 0.1);
|
||||
const cp_x = command.x1 + r;
|
||||
const cp_y = command.y1 + r;
|
||||
const p1_x = command.x + r;
|
||||
const p1_y = command.y + r;
|
||||
const newCurve = splitQuadraticBezier(randomRange(opts.truncateCurvePositionMin, opts.truncateCurvePositionMax), p0_x, p0_y, cp_x, cp_y, p1_x, p1_y);
|
||||
|
||||
const q1 = {
|
||||
type: "Q",
|
||||
x1: newCurve[2],
|
||||
y1: newCurve[3],
|
||||
x: newCurve[4],
|
||||
y: newCurve[5],
|
||||
};
|
||||
const l1 = {
|
||||
type: "L",
|
||||
x: newCurve[4],
|
||||
y: newCurve[5],
|
||||
};
|
||||
const q2 = {
|
||||
type: "Q",
|
||||
x1: newCurve[6],
|
||||
y1: newCurve[7],
|
||||
x: newCurve[8],
|
||||
y: newCurve[9],
|
||||
};
|
||||
const l2 = {
|
||||
type: "L",
|
||||
x: newCurve[8],
|
||||
y: newCurve[9],
|
||||
};
|
||||
result.push(q1);
|
||||
// 插入一个 L 是因为貌似原本的 Path 里不会存在连续的 QQ
|
||||
result.push(l1);
|
||||
result.push(q2);
|
||||
result.push(l2);
|
||||
}
|
||||
|
||||
} else {
|
||||
result.push(command)
|
||||
}
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
module.exports = function (text, opts) {
|
||||
const ch = text[0];
|
||||
assert(ch, 'expect a string');
|
||||
|
@ -150,11 +15,6 @@ module.exports = function (text, opts) {
|
|||
const height = (opts.ascender + opts.descender) * fontScale;
|
||||
const top = opts.y + (height / 2);
|
||||
const path = glyph.getPath(left, top, fontSize);
|
||||
// Randomize path commands
|
||||
path.commands.forEach(rndPathCmd);
|
||||
path.commands = randomizePathNodes(path.commands, opts);
|
||||
|
||||
const pathData = path.toPathData();
|
||||
|
||||
return pathData;
|
||||
return path;
|
||||
};
|
||||
|
|
48
lib/index.js
48
lib/index.js
|
@ -1,15 +1,14 @@
|
|||
'use strict';
|
||||
const chToPath = require('./ch-to-path');
|
||||
const randomizePath = require('./randomize-path');
|
||||
const random = require('./random');
|
||||
const optionMngr = require('./option-manager');
|
||||
|
||||
const opts = optionMngr.options;
|
||||
|
||||
const getText = function (text, width, height, options) {
|
||||
const getTextPath = function (text, width, height, options) {
|
||||
const len = text.length;
|
||||
const spacing = (width - 2) / (len + 1);
|
||||
const min = options.inverse ? 10 : 0;
|
||||
const max = options.inverse ? 14 : 4;
|
||||
let i = -1;
|
||||
const out = [];
|
||||
|
||||
|
@ -17,33 +16,49 @@ const getText = function (text, width, height, options) {
|
|||
const x = spacing * (i + 1);
|
||||
const y = height / 2;
|
||||
const charPath = chToPath(text[i], Object.assign({x, y}, options));
|
||||
|
||||
const color = options.color ?
|
||||
random.color(options.background) : random.greyColor(min, max);
|
||||
out.push(`<path fill="${color}" d="${charPath}"/>`);
|
||||
out.push(charPath);
|
||||
}
|
||||
|
||||
return out;
|
||||
};
|
||||
|
||||
function mergePaths(paths) {
|
||||
if (!paths.length) {
|
||||
return [];
|
||||
}
|
||||
const out = paths[0];
|
||||
for (let i = 1; i < paths.length; i += 1) {
|
||||
out.commands = out.commands.concat(
|
||||
paths[i].commands,
|
||||
);
|
||||
}
|
||||
return out;
|
||||
}
|
||||
|
||||
const createCaptcha = function (text, options) {
|
||||
text = text || random.captchaText();
|
||||
options = Object.assign({}, opts, options);
|
||||
const width = options.width;
|
||||
const height = options.height;
|
||||
const bg = options.background;
|
||||
if (bg) {
|
||||
options.color = true;
|
||||
}
|
||||
|
||||
const bgRect = bg ?
|
||||
`<rect width="100%" height="100%" fill="${bg}"/>` : '';
|
||||
const paths =
|
||||
[].concat(getText(text, width, height, options))
|
||||
.sort(() => Math.random() - 0.5)
|
||||
.join('');
|
||||
|
||||
/* Create character paths and order them randomly */
|
||||
let path =
|
||||
[].concat(getTextPath(text, width, height, options))
|
||||
.sort(() => Math.random() - 0.5);
|
||||
/* Join paths together to one */
|
||||
path = mergePaths(path);
|
||||
/* Randomize nodes and randomly split them */
|
||||
path = randomizePath(path, options);
|
||||
/* Join characters with random lines */
|
||||
path = randomizePath.removeGaps(path);
|
||||
|
||||
/* Create xml */
|
||||
const start = `<svg xmlns="http://www.w3.org/2000/svg" width="${width}" height="${height}" viewBox="0,0,${width},${height}">`;
|
||||
const xml = `${start}${bgRect}${paths}</svg>`;
|
||||
path = `<path fill="${options.fill}" stroke="${options.stroke}" d="${path.toPathData()}"/>`;
|
||||
const xml = `${start}${bgRect}${path}</svg>`;
|
||||
|
||||
return xml;
|
||||
};
|
||||
|
@ -66,6 +81,7 @@ const createMathExpr = function (options) {
|
|||
module.exports = createCaptcha;
|
||||
module.exports.randomText = random.captchaText;
|
||||
module.exports.create = create;
|
||||
|
||||
module.exports.createMathExpr = createMathExpr;
|
||||
module.exports.options = opts;
|
||||
module.exports.loadFont = optionMngr.loadFont;
|
||||
|
|
|
@ -11,7 +11,8 @@ const descender = font.descender;
|
|||
const options = {
|
||||
width: 150,
|
||||
height: 50,
|
||||
color: false,
|
||||
stroke: 'black',
|
||||
fill: 'none',
|
||||
background: '',
|
||||
size: 4,
|
||||
ignoreChars: '',
|
||||
|
@ -20,7 +21,7 @@ const options = {
|
|||
truncateLineProbability: 0.5,
|
||||
truncateCurveProbability: 0.5,
|
||||
truncateCurvePositionMin: 0.4,
|
||||
truncateCurvePositionMax: 0.6,
|
||||
truncateCurvePositionMax: 0.6
|
||||
};
|
||||
|
||||
const loadFont = filepath => {
|
||||
|
|
|
@ -44,17 +44,17 @@ exports.captchaText = function (options) {
|
|||
return out;
|
||||
};
|
||||
|
||||
const mathExprPlus = function(leftNumber, rightNumber){
|
||||
const mathExprPlus = function (leftNumber, rightNumber) {
|
||||
const text = (leftNumber + rightNumber).toString();
|
||||
const equation = leftNumber + '+' + rightNumber;
|
||||
return {text, equation}
|
||||
}
|
||||
return {text, equation};
|
||||
};
|
||||
|
||||
const mathExprMinus = function(leftNumber, rightNumber){
|
||||
const mathExprMinus = function (leftNumber, rightNumber) {
|
||||
const text = (leftNumber - rightNumber).toString();
|
||||
const equation = leftNumber + '-' + rightNumber;
|
||||
return {text, equation}
|
||||
}
|
||||
return {text, equation};
|
||||
};
|
||||
|
||||
/**
|
||||
* Creates a simple math expression using either the + or - operator
|
||||
|
@ -69,11 +69,11 @@ exports.mathExpr = function (min, max, operator) {
|
|||
operator = operator || '+';
|
||||
const left = randomInt(min, max);
|
||||
const right = randomInt(min, max);
|
||||
switch(operator){
|
||||
switch (operator) {
|
||||
case '+':
|
||||
return mathExprPlus(left, right)
|
||||
return mathExprPlus(left, right);
|
||||
case '-':
|
||||
return mathExprMinus(left, right)
|
||||
return mathExprMinus(left, right);
|
||||
default:
|
||||
return (randomInt(1, 2) % 2) ? mathExprPlus(left, right) : mathExprMinus(left, right);
|
||||
}
|
||||
|
|
|
@ -0,0 +1,259 @@
|
|||
'use strict';
|
||||
const assert = require('assert');
|
||||
|
||||
function rndPathCmd(cmd) {
|
||||
const r = (Math.random() * 0.2) - 0.1;
|
||||
|
||||
switch (cmd.type) {
|
||||
case 'M':
|
||||
case 'L':
|
||||
cmd.x += r;
|
||||
cmd.y += r;
|
||||
break;
|
||||
case 'Q':
|
||||
case 'C':
|
||||
cmd.x += r;
|
||||
cmd.y += r;
|
||||
cmd.x1 += r;
|
||||
cmd.y1 += r;
|
||||
break;
|
||||
default:
|
||||
// Close path cmd
|
||||
break;
|
||||
}
|
||||
|
||||
return cmd;
|
||||
}
|
||||
|
||||
// https://riptutorial.com/zh-CN/html5-canvas/example/19077/%E5%9C%A8%E4%BD%8D%E7%BD%AE%E6%8B%86%E5%88%86%E8%B4%9D%E5%A1%9E%E5%B0%94%E6%9B%B2%E7%BA%BF
|
||||
function splitQuadraticBezier(position, x1, y1, x2, y2, x3, y3) {
|
||||
let v1, v2, v3, retPoints, i, c;
|
||||
|
||||
if (position <= 0 || position >= 1) {
|
||||
throw new RangeError('spliteCurveAt requires position > 0 && position < 1');
|
||||
}
|
||||
|
||||
retPoints = []; // Array of coordinates
|
||||
i = 0;
|
||||
v1 = {};
|
||||
v2 = {};
|
||||
v3 = {};
|
||||
v1.x = x1;
|
||||
v1.y = y1;
|
||||
v2.x = x2;
|
||||
v2.y = y2;
|
||||
v3.x = x3;
|
||||
v3.y = y3;
|
||||
|
||||
c = position;
|
||||
retPoints[i++] = v1.x; // Start point
|
||||
retPoints[i++] = v1.y;
|
||||
retPoints[i++] = (v1.x += (v2.x - v1.x) * c); // New control point for first curve
|
||||
retPoints[i++] = (v1.y += (v2.y - v1.y) * c);
|
||||
v2.x += (v3.x - v2.x) * c;
|
||||
v2.y += (v3.y - v2.y) * c;
|
||||
retPoints[i++] = v1.x + (v2.x - v1.x) * c; // New end and start of first and second curves
|
||||
retPoints[i++] = v1.y + (v2.y - v1.y) * c;
|
||||
retPoints[i++] = v2.x; // New control point for second curve
|
||||
retPoints[i++] = v2.y;
|
||||
retPoints[i++] = v3.x; // New endpoint of second curve
|
||||
retPoints[i++] = v3.y;
|
||||
return retPoints;
|
||||
}
|
||||
|
||||
function randomRange(min, max) {
|
||||
return Math.random() * (max - min) + min;
|
||||
}
|
||||
|
||||
function distance(x1, y1, x2, y2) {
|
||||
return Math.sqrt((x2 - x1) ** 2 + (y2 - y1) ** 2);
|
||||
}
|
||||
|
||||
function randomizePathNodes(commands, opts) {
|
||||
// 随机化路径节点
|
||||
// 规则:
|
||||
// 如果当前节点是 L(Line),下一个节点也是 Line,那么随机插入一个中间点
|
||||
// 如果当前节点是 Q,且前节点为 L 或 M,那么拆分这个曲线
|
||||
const result = [];
|
||||
for (let i = 0; i < commands.length - 1; i++) {
|
||||
const command = commands[i];
|
||||
if (command.type === 'L') {
|
||||
const next = commands[i + 1];
|
||||
if (next.type === 'L' && Math.random() > opts.truncateLineProbability) {
|
||||
const r = randomRange(-0.1, 0.1);
|
||||
result.push(command);
|
||||
result.push({
|
||||
type: 'L',
|
||||
x: (command.x + next.x) / 2 + r,
|
||||
y: (command.y + next.y) / 2 + r
|
||||
});
|
||||
} else {
|
||||
result.push(command);
|
||||
}
|
||||
} else if (command.type === 'Q' && i >= 1) {
|
||||
const prev = commands[i - 1];
|
||||
if ((prev.type === 'L' || prev.type === 'M') && Math.random() > opts.truncateCurveProbability) {
|
||||
const p0_x = prev.x;
|
||||
const p0_y = prev.y;
|
||||
const r = randomRange(-0.1, 0.1);
|
||||
const cp_x = command.x1 + r;
|
||||
const cp_y = command.y1 + r;
|
||||
const p1_x = command.x + r;
|
||||
const p1_y = command.y + r;
|
||||
const newCurve = splitQuadraticBezier(randomRange(opts.truncateCurvePositionMin, opts.truncateCurvePositionMax), p0_x, p0_y, cp_x, cp_y, p1_x, p1_y);
|
||||
|
||||
const q1 = {
|
||||
type: 'Q',
|
||||
x1: newCurve[2],
|
||||
y1: newCurve[3],
|
||||
x: newCurve[4],
|
||||
y: newCurve[5]
|
||||
};
|
||||
const l1 = {
|
||||
type: 'L',
|
||||
x: newCurve[4],
|
||||
y: newCurve[5]
|
||||
};
|
||||
const q2 = {
|
||||
type: 'Q',
|
||||
x1: newCurve[6],
|
||||
y1: newCurve[7],
|
||||
x: newCurve[8],
|
||||
y: newCurve[9]
|
||||
};
|
||||
const l2 = {
|
||||
type: 'L',
|
||||
x: newCurve[8],
|
||||
y: newCurve[9]
|
||||
};
|
||||
result.push(q1);
|
||||
// 插入一个 L 是因为貌似原本的 Path 里不会存在连续的 QQ
|
||||
// result.push(l1);
|
||||
result.push(q2);
|
||||
// Result.push(l2);
|
||||
}
|
||||
} else {
|
||||
result.push(command);
|
||||
}
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
/*
|
||||
* Connect two points with a random path
|
||||
* @param avgDist average distance between nodes
|
||||
* @param qDist ratio of curves to lines
|
||||
* @return array of path commands
|
||||
*/
|
||||
function connectPoints(xa, ya, xb, yb, qDist, avgDist) {
|
||||
const dist = distance(xa, ya, xb, yb);
|
||||
const min = avgDist / 15;
|
||||
const max = avgDist * 4;
|
||||
let drawDist = randomRange(min, max);
|
||||
let xp = xa;
|
||||
let yp = ya;
|
||||
const path = [];
|
||||
while (drawDist < dist) {
|
||||
const ratio = drawDist / dist;
|
||||
const x = (xb - xa) * ratio + xa + Math.random() * 8 - 4;
|
||||
const y = (yb - ya) * ratio + ya + Math.random() * 8 - 4;
|
||||
const point = {};
|
||||
if (Math.random() < qDist) {
|
||||
const x1 = randomRange(xp, x) + Math.random() * 2 - 1;
|
||||
const y1 = randomRange(yp, y) + Math.random() * 2 - 1;
|
||||
point.type = 'Q';
|
||||
point.x1 = x1;
|
||||
point.y1 = y1;
|
||||
} else {
|
||||
point.type = 'L';
|
||||
}
|
||||
point.x = x;
|
||||
point.y = y;
|
||||
path.push(point);
|
||||
xp = x;
|
||||
yp = y;
|
||||
drawDist += randomRange(min, max);
|
||||
}
|
||||
path.push({type: 'L', x: xb, y: yb});
|
||||
return path;
|
||||
}
|
||||
|
||||
/*
|
||||
* Removes gaps in path (Z and following M command)
|
||||
*/
|
||||
function removeGaps(path) {
|
||||
if (!path.commands.length) {
|
||||
return path;
|
||||
}
|
||||
let commands = [path.commands[0]];
|
||||
// Calculate metadata of path and filter zero-length paths
|
||||
let points = 0;
|
||||
let qCount = 0;
|
||||
let length = 0;
|
||||
let i = 1;
|
||||
while (i < path.commands.length) {
|
||||
const command = path.commands[i];
|
||||
const type = command.type;
|
||||
commands.push(command);
|
||||
if (type === 'L' || type === 'Q') {
|
||||
const prevCommand = path.commands[i - 1];
|
||||
if (prevCommand.x) {
|
||||
const dist = distance(
|
||||
prevCommand.x, prevCommand.y, command.x, command.y,
|
||||
);
|
||||
if (!dist) {
|
||||
commands.pop();
|
||||
} else {
|
||||
points += 1;
|
||||
if (type === 'Q') {
|
||||
qCount += 1;
|
||||
}
|
||||
length += dist;
|
||||
}
|
||||
}
|
||||
}
|
||||
i += 1;
|
||||
}
|
||||
const avgDist = length / points;
|
||||
const qDist = qCount / points;
|
||||
console.log(`qCount: ${qCount} / ${points} = ${qDist}`);
|
||||
console.log(`avgDist: ${avgDist}`);
|
||||
|
||||
commands = [path.commands[0]];
|
||||
path.commands.push(path.commands[0]);
|
||||
i = 1;
|
||||
while (i < path.commands.length) {
|
||||
const command = path.commands[i];
|
||||
if (command.type === 'Z') {
|
||||
/*
|
||||
* Is it save to assume that every Z command is always
|
||||
* leaded and followed by a command with x,y?
|
||||
* Might not be save outside of glyphs.
|
||||
*/
|
||||
const prevCommand = path.commands[i - 1];
|
||||
const nextCommand = path.commands[i += 1];
|
||||
console.log(`Starting point: ${prevCommand.x} ${prevCommand.y}`);
|
||||
console.log(`End point: ${nextCommand.x} ${nextCommand.y}`);
|
||||
const points = connectPoints(
|
||||
prevCommand.x, prevCommand.y,
|
||||
nextCommand.x, nextCommand.y,
|
||||
qDist, avgDist,
|
||||
);
|
||||
console.log(points);
|
||||
commands = commands.concat(points);
|
||||
} else {
|
||||
commands.push(command);
|
||||
}
|
||||
i += 1;
|
||||
}
|
||||
path.commands = commands;
|
||||
return path;
|
||||
}
|
||||
|
||||
module.exports = function (path, opts) {
|
||||
// Randomize path commands
|
||||
path.commands.forEach(rndPathCmd);
|
||||
path.commands = randomizePathNodes(path.commands, opts);
|
||||
return path;
|
||||
};
|
||||
module.exports.removeGaps = removeGaps;
|
Binary file not shown.
Before Width: | Height: | Size: 17 KiB After Width: | Height: | Size: 18 KiB |
Binary file not shown.
Before Width: | Height: | Size: 18 KiB After Width: | Height: | Size: 21 KiB |
BIN
media/header.png
BIN
media/header.png
Binary file not shown.
Before Width: | Height: | Size: 163 KiB |
BIN
media/header.psd
BIN
media/header.psd
Binary file not shown.
|
@ -2288,7 +2288,8 @@
|
|||
"ansi-regex": {
|
||||
"version": "2.1.1",
|
||||
"bundled": true,
|
||||
"dev": true
|
||||
"dev": true,
|
||||
"optional": true
|
||||
},
|
||||
"aproba": {
|
||||
"version": "1.2.0",
|
||||
|
@ -2309,12 +2310,14 @@
|
|||
"balanced-match": {
|
||||
"version": "1.0.0",
|
||||
"bundled": true,
|
||||
"dev": true
|
||||
"dev": true,
|
||||
"optional": true
|
||||
},
|
||||
"brace-expansion": {
|
||||
"version": "1.1.11",
|
||||
"bundled": true,
|
||||
"dev": true,
|
||||
"optional": true,
|
||||
"requires": {
|
||||
"balanced-match": "^1.0.0",
|
||||
"concat-map": "0.0.1"
|
||||
|
@ -2329,17 +2332,20 @@
|
|||
"code-point-at": {
|
||||
"version": "1.1.0",
|
||||
"bundled": true,
|
||||
"dev": true
|
||||
"dev": true,
|
||||
"optional": true
|
||||
},
|
||||
"concat-map": {
|
||||
"version": "0.0.1",
|
||||
"bundled": true,
|
||||
"dev": true
|
||||
"dev": true,
|
||||
"optional": true
|
||||
},
|
||||
"console-control-strings": {
|
||||
"version": "1.1.0",
|
||||
"bundled": true,
|
||||
"dev": true
|
||||
"dev": true,
|
||||
"optional": true
|
||||
},
|
||||
"core-util-is": {
|
||||
"version": "1.0.2",
|
||||
|
@ -2456,7 +2462,8 @@
|
|||
"inherits": {
|
||||
"version": "2.0.3",
|
||||
"bundled": true,
|
||||
"dev": true
|
||||
"dev": true,
|
||||
"optional": true
|
||||
},
|
||||
"ini": {
|
||||
"version": "1.3.5",
|
||||
|
@ -2468,6 +2475,7 @@
|
|||
"version": "1.0.0",
|
||||
"bundled": true,
|
||||
"dev": true,
|
||||
"optional": true,
|
||||
"requires": {
|
||||
"number-is-nan": "^1.0.0"
|
||||
}
|
||||
|
@ -2482,6 +2490,7 @@
|
|||
"version": "3.0.4",
|
||||
"bundled": true,
|
||||
"dev": true,
|
||||
"optional": true,
|
||||
"requires": {
|
||||
"brace-expansion": "^1.1.7"
|
||||
}
|
||||
|
@ -2489,12 +2498,14 @@
|
|||
"minimist": {
|
||||
"version": "0.0.8",
|
||||
"bundled": true,
|
||||
"dev": true
|
||||
"dev": true,
|
||||
"optional": true
|
||||
},
|
||||
"minipass": {
|
||||
"version": "2.3.5",
|
||||
"bundled": true,
|
||||
"dev": true,
|
||||
"optional": true,
|
||||
"requires": {
|
||||
"safe-buffer": "^5.1.2",
|
||||
"yallist": "^3.0.0"
|
||||
|
@ -2513,6 +2524,7 @@
|
|||
"version": "0.5.1",
|
||||
"bundled": true,
|
||||
"dev": true,
|
||||
"optional": true,
|
||||
"requires": {
|
||||
"minimist": "0.0.8"
|
||||
}
|
||||
|
@ -2593,7 +2605,8 @@
|
|||
"number-is-nan": {
|
||||
"version": "1.0.1",
|
||||
"bundled": true,
|
||||
"dev": true
|
||||
"dev": true,
|
||||
"optional": true
|
||||
},
|
||||
"object-assign": {
|
||||
"version": "4.1.1",
|
||||
|
@ -2605,6 +2618,7 @@
|
|||
"version": "1.4.0",
|
||||
"bundled": true,
|
||||
"dev": true,
|
||||
"optional": true,
|
||||
"requires": {
|
||||
"wrappy": "1"
|
||||
}
|
||||
|
@ -2690,7 +2704,8 @@
|
|||
"safe-buffer": {
|
||||
"version": "5.1.2",
|
||||
"bundled": true,
|
||||
"dev": true
|
||||
"dev": true,
|
||||
"optional": true
|
||||
},
|
||||
"safer-buffer": {
|
||||
"version": "2.1.2",
|
||||
|
@ -2726,6 +2741,7 @@
|
|||
"version": "1.0.2",
|
||||
"bundled": true,
|
||||
"dev": true,
|
||||
"optional": true,
|
||||
"requires": {
|
||||
"code-point-at": "^1.0.0",
|
||||
"is-fullwidth-code-point": "^1.0.0",
|
||||
|
@ -2745,6 +2761,7 @@
|
|||
"version": "3.0.1",
|
||||
"bundled": true,
|
||||
"dev": true,
|
||||
"optional": true,
|
||||
"requires": {
|
||||
"ansi-regex": "^2.0.0"
|
||||
}
|
||||
|
@ -2788,12 +2805,14 @@
|
|||
"wrappy": {
|
||||
"version": "1.0.2",
|
||||
"bundled": true,
|
||||
"dev": true
|
||||
"dev": true,
|
||||
"optional": true
|
||||
},
|
||||
"yallist": {
|
||||
"version": "3.0.3",
|
||||
"bundled": true,
|
||||
"dev": true
|
||||
"dev": true,
|
||||
"optional": true
|
||||
}
|
||||
}
|
||||
},
|
||||
|
|
10
package.json
10
package.json
|
@ -1,7 +1,7 @@
|
|||
{
|
||||
"name": "svg-captcha",
|
||||
"version": "1.4.0",
|
||||
"description": "generate svg captcha in node.js or express.js",
|
||||
"name": "ppfun-captcha",
|
||||
"version": "1.6.0",
|
||||
"description": "Generate single-path svg captcha in node.js or express.js",
|
||||
"main": "index.js",
|
||||
"scripts": {
|
||||
"test": "jest",
|
||||
|
@ -19,8 +19,8 @@
|
|||
"captcha alternative"
|
||||
],
|
||||
"author": {
|
||||
"name": "Weilin Shi",
|
||||
"email": "934587911@qq.com"
|
||||
"name": "ppfun",
|
||||
"email": "pixelplanetdev@gmail.com"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=4.x"
|
||||
|
|
|
@ -29,7 +29,7 @@ test('Generate math expression using default values 1, 9 and +', () => {
|
|||
|
||||
test('Generate math expression using non default numbers but default +, 1, 20', () => {
|
||||
for (let i = 0; i < 10; i++) {
|
||||
const expr = random.mathExpr(1,20);
|
||||
const expr = random.mathExpr(1, 20);
|
||||
expect(expr.text).toMatch(/^-?[0-9]\d*$/);
|
||||
expect(expr.equation).toMatch(/^\d+[+]\d+$/);
|
||||
}
|
||||
|
@ -37,7 +37,7 @@ test('Generate math expression using non default numbers but default +, 1, 20',
|
|||
|
||||
test('Generate math expression using non default numbers with minus', () => {
|
||||
for (let i = 0; i < 10; i++) {
|
||||
const expr = random.mathExpr(1,20, '-');
|
||||
const expr = random.mathExpr(1, 20, '-');
|
||||
expect(expr.text).toMatch(/^-?[0-9]\d*$/);
|
||||
expect(expr.equation).toMatch(/^\d+[-]\d+$/);
|
||||
}
|
||||
|
@ -45,7 +45,7 @@ test('Generate math expression using non default numbers with minus', () => {
|
|||
|
||||
test('Generate math expression using non default numbers with "+/-"', () => {
|
||||
for (let i = 0; i < 10; i++) {
|
||||
const expr = random.mathExpr(1,20, '-');
|
||||
const expr = random.mathExpr(1, 20, '-');
|
||||
expect(expr.text).toMatch(/^-?[0-9]\d*$/);
|
||||
expect(expr.equation).toMatch(/^\d+[+/-]\d+$/);
|
||||
}
|
||||
|
|
Loading…
Reference in New Issue