captcha-canvas
Version:
A captcha generator by using canvas module.
311 lines (310 loc) • 14 kB
JavaScript
"use strict";
var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) {
function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }
return new (P || (P = Promise))(function (resolve, reject) {
function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); }
step((generator = generator.apply(thisArg, _arguments || [])).next());
});
};
Object.defineProperty(exports, "__esModule", { value: true });
const crypto_1 = require("crypto");
const util_1 = require("./util");
const constants_1 = require("./constants");
const canvas_1 = require("canvas");
const PD = 30;
/**
* @function getRandom
* @description Get a random number between two values
* @param {number} start - The start value
* @param {number} end - The end value
* @returns {number} - A random number
*/
function getRandom(start, end) {
start = start || 0;
end = end || 0;
return Math.round(Math.random() * Math.abs(end - start)) + Math.min(start, end);
}
/**
* @class CaptchaGenerator
* @description CaptchaGenerator class
* @example
* const captcha = new CaptchaGenerator();
* captcha.setDimension(150, 450);
* captcha.setCaptcha({font: "Comic Sans", size: 60});
* captcha.setDecoy({opacity: 0.5});
* captcha.setTrace({color: "blue"});
* const {buffer, text} = await captcha.generate();
*/
class CaptchaGenerator {
/**
* @constructor
* @param {SetDimensionOption} options - Options for the captcha generator
*/
constructor(options = {}) {
this.captchaSegments = [];
this.height = options.height || 100;
this.width = options.width || 300;
this.captcha = constants_1.defaultCaptchaOptions;
this.trace = constants_1.defaultTraceOptions;
this.decoy = constants_1.defaultDecoyOptions;
this.captcha.text = (0, crypto_1.randomBytes)(32)
.toString('hex')
.toUpperCase()
.replace(/[^a-z]/gi, '')
.substr(0, this.captcha.characters);
}
/**
* @method text
* @description Get the captcha text
* @returns {string} The captcha text
*/
get text() {
return this.captcha.text || '';
}
/**
* @method setDimension
* @description Set the dimension of the captcha
* @param {number} height - The height of the captcha
* @param {number} width - The width of the captcha
* @returns {this} The captcha generator instance
*/
setDimension(height, width) {
this.height = height;
this.width = width;
return this;
}
/**
* @method setBackground
* @description Set the background of the captcha
* @param {Buffer | string} image - The background image
* @returns {this} The captcha generator instance
*/
setBackground(image) {
this.background = image;
return this;
}
/**
* @method setCaptcha
* @description Set the captcha options
* @param {SetCaptchaOptions | SetCaptchaOptions[]} options - The captcha options
* @returns {this} The captcha generator instance
*/
setCaptcha(options) {
if (Array.isArray(options)) {
this.captchaSegments = options;
const textFromSegments = options.map(opt => opt.text).filter(Boolean).join('');
if (textFromSegments) {
this.captcha.text = textFromSegments;
this.captcha.characters = textFromSegments.length;
}
else if (!this.captcha.text || !this.captcha.characters) {
this.captcha.characters = this.captcha.characters || constants_1.defaultCaptchaOptions.characters;
this.captcha.text = (0, crypto_1.randomBytes)(32)
.toString('hex')
.toUpperCase()
.replace(/[^a-z]/gi, '')
.substr(0, this.captcha.characters);
}
}
else {
this.captchaSegments = [options];
this.captcha = (0, util_1.merge)(this.captcha, options);
if (options.text)
this.captcha.characters = options.text.length;
if (!options.text && options.characters) {
this.captcha.text = (0, crypto_1.randomBytes)(32)
.toString('hex')
.toUpperCase()
.replace(/[^a-z]/gi, '')
.substr(0, options.characters);
}
}
return this;
}
/**
* @method setTrace
* @description Set the trace options
* @param {SetTraceOptions} options - The trace options
* @returns {this} The captcha generator instance
*/
setTrace(options) {
this.trace = (0, util_1.merge)(this.trace, options);
return this;
}
/**
* @method setDecoy
* @description Set the decoy options
* @param {SetDecoyOptions} options - The decoy options
* @returns {this} The captcha generator instance
*/
setDecoy(options) {
this.decoy = (0, util_1.merge)(this.decoy, options);
return this;
}
/**
* @method generate
* @description Generate the captcha image
* @returns {Promise<Buffer>} The captcha image buffer
*/
generate() {
return __awaiter(this, void 0, void 0, function* () {
const canvas = (0, canvas_1.createCanvas)(this.width, this.height);
const ctx = canvas.getContext('2d');
ctx.lineJoin = 'miter';
ctx.textBaseline = 'middle';
let coordinates = [];
if (!this.captcha.characters)
this.captcha.characters = 0;
for (let i = 0; i < this.captcha.characters; i++) {
const widthGap = Math.floor(this.width / (this.captcha.characters || 1));
const coordinate = [];
const randomWidth = widthGap * (i + 0.2);
coordinate.push(randomWidth);
const randomHeight = getRandom(PD, this.height - PD);
coordinate.push(randomHeight);
coordinates.push(coordinate);
}
coordinates = coordinates.sort((a, b) => a[0] - b[0]);
if (this.background) {
const background = yield (0, canvas_1.loadImage)(this.background);
ctx.drawImage(background, 0, 0, this.width, this.height);
}
if (this.decoy.opacity) {
const decoyTextCount = Math.floor((this.height * this.width) / 10000);
const decoyText = (0, crypto_1.randomBytes)(decoyTextCount).toString('hex').split('');
ctx.font = `${this.decoy.size}px ${this.decoy.font}`;
ctx.globalAlpha = this.decoy.opacity;
ctx.fillStyle = this.decoy.color || '#000000';
for (let i = 0; i < decoyText.length; i++) {
ctx.fillText(decoyText[i], getRandom(PD, this.width - PD), getRandom(PD, this.height - PD));
}
}
if (this.trace.opacity) {
ctx.strokeStyle = this.trace.color || '#000000';
ctx.globalAlpha = this.trace.opacity;
ctx.beginPath();
ctx.moveTo(coordinates[0][0], coordinates[0][1]);
ctx.lineWidth = this.trace.size || 1;
for (let i = 1; i < coordinates.length; i++) {
ctx.lineTo(coordinates[i][0], coordinates[i][1]);
}
ctx.stroke();
}
if (this.captcha.opacity) {
for (let n = 0; n < coordinates.length; n++) {
const char = this.captcha.text ? this.captcha.text[n] : '';
let charOptions = Object.assign({}, this.captcha); // Default to global captcha options
// Find specific options for this character
for (const segmentOpt of this.captchaSegments) {
const start = segmentOpt.start !== undefined ? segmentOpt.start : 0;
const end = segmentOpt.end !== undefined ? segmentOpt.end : this.captcha.characters;
if (n >= start && n < end) {
charOptions = (0, util_1.merge)(charOptions, segmentOpt);
}
}
ctx.font = `${charOptions.size}px ${charOptions.font}`;
ctx.globalAlpha = charOptions.opacity || 1;
ctx.fillStyle = charOptions.color || '#000000';
ctx.save();
ctx.translate(coordinates[n][0], coordinates[n][1]);
if (charOptions.skew) {
ctx.transform(1, Math.random(), getRandom(0, 20) / 100, 1, 0, 0);
}
if (charOptions.rotate && charOptions.rotate > 0) {
ctx.rotate((getRandom(-charOptions.rotate, charOptions.rotate) * Math.PI) / 180);
}
if (charOptions.colors && charOptions.colors.length >= 2) {
ctx.fillStyle = charOptions.colors[getRandom(0, charOptions.colors.length - 1)];
}
ctx.fillText(char, 0, 0);
ctx.restore();
}
}
return canvas.toBuffer();
});
}
/**
* @method generateSync
* @description Generate the captcha image synchronously
* @param {object} options - The options for generating the captcha
* @param {Image} options.background - The background image
* @returns {Buffer} The captcha image buffer
*/
generateSync(options = {}) {
const canvas = (0, canvas_1.createCanvas)(this.width, this.height);
const ctx = canvas.getContext('2d');
ctx.lineJoin = 'miter';
ctx.textBaseline = 'middle';
let coordinates = [];
if (!this.captcha.characters)
this.captcha.characters = 0;
for (let i = 0; i < this.captcha.characters; i++) {
const widthGap = Math.floor(this.width / (this.captcha.characters || 1));
const coordinate = [];
const randomWidth = widthGap * (i + 0.2);
coordinate.push(randomWidth);
const randomHeight = getRandom(PD, this.height - PD);
coordinate.push(randomHeight);
coordinates.push(coordinate);
}
coordinates = coordinates.sort((a, b) => a[0] - b[0]);
if (options.background) {
ctx.drawImage(options.background, 0, 0, this.width, this.height);
}
if (this.decoy.opacity) {
const decoyTextCount = Math.floor((this.height * this.width) / 10000);
const decoyText = (0, crypto_1.randomBytes)(decoyTextCount).toString('hex').split('');
ctx.font = `${this.decoy.size}px ${this.decoy.font}`;
ctx.globalAlpha = this.decoy.opacity;
ctx.fillStyle = this.decoy.color || '#000000';
for (let i = 0; i < decoyText.length; i++) {
ctx.fillText(decoyText[i], getRandom(PD, this.width - PD), getRandom(PD, this.height - PD));
}
}
if (this.trace.opacity) {
ctx.strokeStyle = this.trace.color || '#000000';
ctx.globalAlpha = this.trace.opacity;
ctx.beginPath();
ctx.moveTo(coordinates[0][0], coordinates[0][1]);
ctx.lineWidth = this.trace.size || 1;
for (let i = 1; i < coordinates.length; i++) {
ctx.lineTo(coordinates[i][0], coordinates[i][1]);
}
ctx.stroke();
}
if (this.captcha.opacity) {
for (let n = 0; n < coordinates.length; n++) {
const char = this.captcha.text ? this.captcha.text[n] : '';
let charOptions = Object.assign({}, this.captcha); // Default to global captcha options
// Find specific options for this character
for (const segmentOpt of this.captchaSegments) {
const start = segmentOpt.start !== undefined ? segmentOpt.start : 0;
const end = segmentOpt.end !== undefined ? segmentOpt.end : this.captcha.characters;
if (n >= start && n < end) {
charOptions = (0, util_1.merge)(charOptions, segmentOpt);
}
}
ctx.font = `${charOptions.size}px ${charOptions.font}`;
ctx.globalAlpha = charOptions.opacity || 1;
ctx.fillStyle = charOptions.color || '#000000';
ctx.save();
ctx.translate(coordinates[n][0], coordinates[n][1]);
if (charOptions.skew) {
ctx.transform(1, Math.random(), getRandom(0, 20) / 100, 1, 0, 0);
}
if (charOptions.rotate && charOptions.rotate > 0) {
ctx.rotate((getRandom(-charOptions.rotate, charOptions.rotate) * Math.PI) / 180);
}
if (charOptions.colors && charOptions.colors.length >= 2) {
ctx.fillStyle = charOptions.colors[getRandom(0, charOptions.colors.length - 1)];
}
ctx.fillText(char, 0, 0);
ctx.restore();
}
}
return canvas.toBuffer();
}
}
exports.default = CaptchaGenerator;