UNPKG

captcha-canvas

Version:
311 lines (310 loc) 14 kB
"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;