1027 lines
34 KiB
JavaScript
1027 lines
34 KiB
JavaScript
function _defineProperty(obj, key, value) { key = _toPropertyKey(key); if (key in obj) { Object.defineProperty(obj, key, { value: value, enumerable: true, configurable: true, writable: true }); } else { obj[key] = value; } return obj; }
|
|
function _toPropertyKey(arg) { var key = _toPrimitive(arg, "string"); return typeof key === "symbol" ? key : String(key); }
|
|
function _toPrimitive(input, hint) { if (typeof input !== "object" || input === null) return input; var prim = input[Symbol.toPrimitive]; if (prim !== undefined) { var res = prim.call(input, hint || "default"); if (typeof res !== "object") return res; throw new TypeError("@@toPrimitive must return a primitive value."); } return (hint === "string" ? String : Number)(input); }
|
|
import fs from "fs";
|
|
import Path from "path";
|
|
import EventEmitter from "events";
|
|
import { isNodePattern, throwError, scan, scanIterator } from "@jimp/utils";
|
|
import anyBase from "any-base";
|
|
import pixelMatch from "pixelmatch";
|
|
import tinyColor from "tinycolor2";
|
|
import ImagePHash from "./modules/phash";
|
|
import request from "./request";
|
|
import composite from "./composite";
|
|
import promisify from "./utils/promisify";
|
|
import * as MIME from "./utils/mime";
|
|
import { parseBitmap, getBuffer, getBufferAsync } from "./utils/image-bitmap";
|
|
import * as constants from "./constants";
|
|
const alphabet = "0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ$_";
|
|
|
|
// an array storing the maximum string length of hashes at various bases
|
|
// 0 and 1 do not exist as possible hash lengths
|
|
const maxHashLength = [NaN, NaN];
|
|
for (let i = 2; i < 65; i++) {
|
|
const maxHash = anyBase(anyBase.BIN, alphabet.slice(0, i))(new Array(64 + 1).join("1"));
|
|
maxHashLength.push(maxHash.length);
|
|
}
|
|
|
|
// no operation
|
|
function noop() {}
|
|
|
|
// error checking methods
|
|
|
|
function isArrayBuffer(test) {
|
|
return Object.prototype.toString.call(test).toLowerCase().indexOf("arraybuffer") > -1;
|
|
}
|
|
|
|
// Prepare a Buffer object from the arrayBuffer. Necessary in the browser > node conversion,
|
|
// But this function is not useful when running in node directly
|
|
function bufferFromArrayBuffer(arrayBuffer) {
|
|
const buffer = Buffer.alloc(arrayBuffer.byteLength);
|
|
const view = new Uint8Array(arrayBuffer);
|
|
for (let i = 0; i < buffer.length; ++i) {
|
|
buffer[i] = view[i];
|
|
}
|
|
return buffer;
|
|
}
|
|
function loadFromURL(options, cb) {
|
|
request(options, (err, data) => {
|
|
if (err) {
|
|
return cb(err);
|
|
}
|
|
if (typeof data === "object" && Buffer.isBuffer(data)) {
|
|
return cb(null, data);
|
|
}
|
|
if (typeof data === "object" && isArrayBuffer(data)) {
|
|
return cb(null, bufferFromArrayBuffer(data));
|
|
}
|
|
return new Error(`Could not load Buffer from <${options.url}>`);
|
|
});
|
|
}
|
|
function loadBufferFromPath(src, cb) {
|
|
if (fs && typeof fs.readFile === "function" && !src.match(/^(http|ftp)s?:\/\/./)) {
|
|
fs.readFile(src, cb);
|
|
} else {
|
|
loadFromURL({
|
|
url: src
|
|
}, cb);
|
|
}
|
|
}
|
|
function isRawRGBAData(obj) {
|
|
return obj && typeof obj === "object" && typeof obj.width === "number" && typeof obj.height === "number" && (Buffer.isBuffer(obj.data) || obj.data instanceof Uint8Array || typeof Uint8ClampedArray === "function" && obj.data instanceof Uint8ClampedArray) && (obj.data.length === obj.width * obj.height * 4 || obj.data.length === obj.width * obj.height * 3);
|
|
}
|
|
function makeRGBABufferFromRGB(buffer) {
|
|
if (buffer.length % 3 !== 0) {
|
|
throw new Error("Buffer length is incorrect");
|
|
}
|
|
const rgbaBuffer = Buffer.allocUnsafe(buffer.length / 3 * 4);
|
|
let j = 0;
|
|
for (let i = 0; i < buffer.length; i++) {
|
|
rgbaBuffer[j] = buffer[i];
|
|
if ((i + 1) % 3 === 0) {
|
|
rgbaBuffer[++j] = 255;
|
|
}
|
|
j++;
|
|
}
|
|
return rgbaBuffer;
|
|
}
|
|
const emptyBitmap = {
|
|
data: null,
|
|
width: null,
|
|
height: null
|
|
};
|
|
|
|
/**
|
|
* Jimp constructor (from a file)
|
|
* @param path a path to the image
|
|
* @param {function(Error, Jimp)} cb (optional) a function to call when the image is parsed to a bitmap
|
|
*/
|
|
|
|
/**
|
|
* Jimp constructor (from a url with options)
|
|
* @param options { url, otherOptions}
|
|
* @param {function(Error, Jimp)} cb (optional) a function to call when the image is parsed to a bitmap
|
|
*/
|
|
|
|
/**
|
|
* Jimp constructor (from another Jimp image or raw image data)
|
|
* @param image a Jimp image to clone
|
|
* @param {function(Error, Jimp)} cb a function to call when the image is parsed to a bitmap
|
|
*/
|
|
|
|
/**
|
|
* Jimp constructor (from a Buffer)
|
|
* @param data a Buffer containing the image data
|
|
* @param {function(Error, Jimp)} cb a function to call when the image is parsed to a bitmap
|
|
*/
|
|
|
|
/**
|
|
* Jimp constructor (to generate a new image)
|
|
* @param w the width of the image
|
|
* @param h the height of the image
|
|
* @param {function(Error, Jimp)} cb (optional) a function to call when the image is parsed to a bitmap
|
|
*/
|
|
|
|
/**
|
|
* Jimp constructor (to generate a new image)
|
|
* @param w the width of the image
|
|
* @param h the height of the image
|
|
* @param background color to fill the image with
|
|
* @param {function(Error, Jimp)} cb (optional) a function to call when the image is parsed to a bitmap
|
|
*/
|
|
|
|
class Jimp extends EventEmitter {
|
|
// An object representing a bitmap in memory, comprising:
|
|
// - data: a buffer of the bitmap data
|
|
// - width: the width of the image in pixels
|
|
// - height: the height of the image in pixels
|
|
|
|
// Default colour to use for new pixels
|
|
|
|
// Default MIME is PNG
|
|
|
|
// Exif data for the image
|
|
|
|
// Whether Transparency supporting formats will be exported as RGB or RGBA
|
|
|
|
constructor() {
|
|
for (var _len = arguments.length, args = new Array(_len), _key = 0; _key < _len; _key++) {
|
|
args[_key] = arguments[_key];
|
|
}
|
|
super();
|
|
_defineProperty(this, "bitmap", emptyBitmap);
|
|
_defineProperty(this, "_background", 0x00000000);
|
|
_defineProperty(this, "_originalMime", Jimp.MIME_PNG);
|
|
_defineProperty(this, "_exif", null);
|
|
_defineProperty(this, "_rgba", true);
|
|
_defineProperty(this, "writeAsync", path => promisify(this.write, this, path));
|
|
_defineProperty(this, "getBase64Async", mime => promisify(this.getBase64, this, mime));
|
|
_defineProperty(this, "getBuffer", getBuffer);
|
|
_defineProperty(this, "getBufferAsync", getBufferAsync);
|
|
_defineProperty(this, "getPixelColour", this.getPixelColor);
|
|
_defineProperty(this, "setPixelColour", this.setPixelColor);
|
|
const jimpInstance = this;
|
|
let cb = noop;
|
|
if (isArrayBuffer(args[0])) {
|
|
args[0] = bufferFromArrayBuffer(args[0]);
|
|
}
|
|
function finish() {
|
|
for (var _len2 = arguments.length, args = new Array(_len2), _key2 = 0; _key2 < _len2; _key2++) {
|
|
args[_key2] = arguments[_key2];
|
|
}
|
|
const [err] = args;
|
|
const evData = err || {};
|
|
evData.methodName = "constructor";
|
|
setTimeout(() => {
|
|
// run on next tick.
|
|
if (err && cb === noop) {
|
|
jimpInstance.emitError("constructor", err);
|
|
} else if (!err) {
|
|
jimpInstance.emitMulti("constructor", "initialized");
|
|
}
|
|
cb.call(jimpInstance, ...args);
|
|
}, 1);
|
|
}
|
|
if (typeof args[0] === "number" && typeof args[1] === "number" || parseInt(args[0], 10) && parseInt(args[1], 10)) {
|
|
// create a new image
|
|
const w = parseInt(args[0], 10);
|
|
const h = parseInt(args[1], 10);
|
|
cb = args[2];
|
|
|
|
// with a hex color
|
|
if (typeof args[2] === "number") {
|
|
this._background = args[2];
|
|
cb = args[3];
|
|
}
|
|
|
|
// with a css color
|
|
if (typeof args[2] === "string") {
|
|
this._background = Jimp.cssColorToHex(args[2]);
|
|
cb = args[3];
|
|
}
|
|
if (typeof cb === "undefined") {
|
|
cb = noop;
|
|
}
|
|
if (typeof cb !== "function") {
|
|
return throwError.call(this, "cb must be a function", finish);
|
|
}
|
|
this.bitmap = {
|
|
data: Buffer.alloc(w * h * 4),
|
|
width: w,
|
|
height: h
|
|
};
|
|
for (let i = 0; i < this.bitmap.data.length; i += 4) {
|
|
this.bitmap.data.writeUInt32BE(this._background, i);
|
|
}
|
|
finish(null, this);
|
|
} else if (typeof args[0] === "object" && args[0].url) {
|
|
cb = args[1] || noop;
|
|
if (typeof cb !== "function") {
|
|
return throwError.call(this, "cb must be a function", finish);
|
|
}
|
|
loadFromURL(args[0], (err, data) => {
|
|
if (err) {
|
|
return throwError.call(this, err, finish);
|
|
}
|
|
this.parseBitmap(data, args[0].url, finish);
|
|
});
|
|
} else if (args[0] instanceof Jimp) {
|
|
// clone an existing Jimp
|
|
const [original] = args;
|
|
cb = args[1];
|
|
if (typeof cb === "undefined") {
|
|
cb = noop;
|
|
}
|
|
if (typeof cb !== "function") {
|
|
return throwError.call(this, "cb must be a function", finish);
|
|
}
|
|
this.bitmap = {
|
|
data: Buffer.from(original.bitmap.data),
|
|
width: original.bitmap.width,
|
|
height: original.bitmap.height
|
|
};
|
|
this._quality = original._quality;
|
|
this._deflateLevel = original._deflateLevel;
|
|
this._deflateStrategy = original._deflateStrategy;
|
|
this._filterType = original._filterType;
|
|
this._rgba = original._rgba;
|
|
this._background = original._background;
|
|
this._originalMime = original._originalMime;
|
|
finish(null, this);
|
|
} else if (isRawRGBAData(args[0])) {
|
|
const [imageData] = args;
|
|
cb = args[1] || noop;
|
|
const isRGBA = imageData.width * imageData.height * 4 === imageData.data.length;
|
|
const buffer = isRGBA ? Buffer.from(imageData.data) : makeRGBABufferFromRGB(imageData.data);
|
|
this.bitmap = {
|
|
data: buffer,
|
|
width: imageData.width,
|
|
height: imageData.height
|
|
};
|
|
finish(null, this);
|
|
} else if (typeof args[0] === "string") {
|
|
// read from a path
|
|
const path = args[0];
|
|
cb = args[1];
|
|
if (typeof cb === "undefined") {
|
|
cb = noop;
|
|
}
|
|
if (typeof cb !== "function") {
|
|
return throwError.call(this, "cb must be a function", finish);
|
|
}
|
|
loadBufferFromPath(path, (err, data) => {
|
|
if (err) {
|
|
return throwError.call(this, err, finish);
|
|
}
|
|
this.parseBitmap(data, path, finish);
|
|
});
|
|
} else if (typeof args[0] === "object" && Buffer.isBuffer(args[0])) {
|
|
// read from a buffer
|
|
const data = args[0];
|
|
cb = args[1];
|
|
if (typeof cb !== "function") {
|
|
return throwError.call(this, "cb must be a function", finish);
|
|
}
|
|
this.parseBitmap(data, null, finish);
|
|
} else {
|
|
// Allow client libs to add new ways to build a Jimp object.
|
|
// Extra constructors must be added by `Jimp.appendConstructorOption()`
|
|
cb = args[args.length - 1];
|
|
if (typeof cb !== "function") {
|
|
// TODO: try to solve the args after cb problem.
|
|
cb = args[args.length - 2];
|
|
if (typeof cb !== "function") {
|
|
cb = noop;
|
|
}
|
|
}
|
|
const extraConstructor = Jimp.__extraConstructors.find(c => c.test(...args));
|
|
if (extraConstructor) {
|
|
new Promise((resolve, reject) => {
|
|
extraConstructor.run.call(this, resolve, reject, ...args);
|
|
}).then(() => finish(null, this)).catch(finish);
|
|
} else {
|
|
return throwError.call(this, "No matching constructor overloading was found. " + "Please see the docs for how to call the Jimp constructor.", finish);
|
|
}
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Parse a bitmap with the loaded image types.
|
|
*
|
|
* @param {Buffer} data raw image data
|
|
* @param {string} path optional path to file
|
|
* @param {function(Error, Jimp)} finish (optional) a callback for when complete
|
|
* @memberof Jimp
|
|
*/
|
|
parseBitmap(data, path, finish) {
|
|
parseBitmap.call(this, data, null, finish);
|
|
}
|
|
|
|
/**
|
|
* Sets the type of the image (RGB or RGBA) when saving in a format that supports transparency (default is RGBA)
|
|
* @param {boolean} bool A Boolean, true to use RGBA or false to use RGB
|
|
* @param {function(Error, Jimp)} cb (optional) a callback for when complete
|
|
* @returns {Jimp} this for chaining of methods
|
|
*/
|
|
rgba(bool, cb) {
|
|
if (typeof bool !== "boolean") {
|
|
return throwError.call(this, "bool must be a boolean, true for RGBA or false for RGB", cb);
|
|
}
|
|
this._rgba = bool;
|
|
if (isNodePattern(cb)) {
|
|
cb.call(this, null, this);
|
|
}
|
|
return this;
|
|
}
|
|
|
|
/**
|
|
* Emit for multiple listeners
|
|
* @param {string} methodName name of the method to emit an error for
|
|
* @param {string} eventName name of the eventName to emit an error for
|
|
* @param {object} data to emit
|
|
*/
|
|
emitMulti(methodName, eventName) {
|
|
let data = arguments.length > 2 && arguments[2] !== undefined ? arguments[2] : {};
|
|
data = Object.assign(data, {
|
|
methodName,
|
|
eventName
|
|
});
|
|
this.emit("any", data);
|
|
if (methodName) {
|
|
this.emit(methodName, data);
|
|
}
|
|
this.emit(eventName, data);
|
|
}
|
|
emitError(methodName, err) {
|
|
this.emitMulti(methodName, "error", err);
|
|
}
|
|
|
|
/**
|
|
* Get the current height of the image
|
|
* @return {number} height of the image
|
|
*/
|
|
getHeight() {
|
|
return this.bitmap.height;
|
|
}
|
|
|
|
/**
|
|
* Get the current width of the image
|
|
* @return {number} width of the image
|
|
*/
|
|
getWidth() {
|
|
return this.bitmap.width;
|
|
}
|
|
|
|
/**
|
|
* Nicely format Jimp object when sent to the console e.g. console.log(image)
|
|
* @returns {string} pretty printed
|
|
*/
|
|
inspect() {
|
|
return "<Jimp " + (this.bitmap === emptyBitmap ? "pending..." : this.bitmap.width + "x" + this.bitmap.height) + ">";
|
|
}
|
|
|
|
/**
|
|
* Nicely format Jimp object when converted to a string
|
|
* @returns {string} pretty printed
|
|
*/
|
|
toString() {
|
|
return "[object Jimp]";
|
|
}
|
|
|
|
/**
|
|
* Returns the original MIME of the image (default: "image/png")
|
|
* @returns {string} the MIME
|
|
*/
|
|
getMIME() {
|
|
const mime = this._originalMime || Jimp.MIME_PNG;
|
|
return mime;
|
|
}
|
|
|
|
/**
|
|
* Returns the appropriate file extension for the original MIME of the image (default: "png")
|
|
* @returns {string} the file extension
|
|
*/
|
|
getExtension() {
|
|
const mime = this.getMIME();
|
|
return MIME.getExtension(mime);
|
|
}
|
|
|
|
/**
|
|
* Writes the image to a file
|
|
* @param {string} path a path to the destination file
|
|
* @param {function(Error, Jimp)} cb (optional) a function to call when the image is saved to disk
|
|
* @returns {Jimp} this for chaining of methods
|
|
*/
|
|
write(path, cb) {
|
|
if (!fs || !fs.createWriteStream) {
|
|
throw new Error("Cant access the filesystem. You can use the getBase64 method.");
|
|
}
|
|
if (typeof path !== "string") {
|
|
return throwError.call(this, "path must be a string", cb);
|
|
}
|
|
if (typeof cb === "undefined") {
|
|
cb = noop;
|
|
}
|
|
if (typeof cb !== "function") {
|
|
return throwError.call(this, "cb must be a function", cb);
|
|
}
|
|
const mime = MIME.getType(path) || this.getMIME();
|
|
const pathObj = Path.parse(path);
|
|
if (pathObj.dir) {
|
|
fs.mkdirSync(pathObj.dir, {
|
|
recursive: true
|
|
});
|
|
}
|
|
this.getBuffer(mime, (err, buffer) => {
|
|
if (err) {
|
|
return throwError.call(this, err, cb);
|
|
}
|
|
const stream = fs.createWriteStream(path);
|
|
stream.on("open", () => {
|
|
stream.write(buffer);
|
|
stream.end();
|
|
}).on("error", err => {
|
|
return throwError.call(this, err, cb);
|
|
});
|
|
stream.on("finish", () => {
|
|
cb.call(this, null, this);
|
|
});
|
|
});
|
|
return this;
|
|
}
|
|
/**
|
|
* Converts the image to a base 64 string
|
|
* @param {string} mime the mime type of the image data to be created
|
|
* @param {function(Error, Jimp)} cb a Node-style function to call with the buffer as the second argument
|
|
* @returns {Jimp} this for chaining of methods
|
|
*/
|
|
getBase64(mime, cb) {
|
|
if (mime === Jimp.AUTO) {
|
|
// allow auto MIME detection
|
|
mime = this.getMIME();
|
|
}
|
|
if (typeof mime !== "string") {
|
|
return throwError.call(this, "mime must be a string", cb);
|
|
}
|
|
if (typeof cb !== "function") {
|
|
return throwError.call(this, "cb must be a function", cb);
|
|
}
|
|
this.getBuffer(mime, function (err, data) {
|
|
if (err) {
|
|
return throwError.call(this, err, cb);
|
|
}
|
|
const src = "data:" + mime + ";base64," + data.toString("base64");
|
|
cb.call(this, null, src);
|
|
});
|
|
return this;
|
|
}
|
|
/**
|
|
* Generates a perceptual hash of the image <https://en.wikipedia.org/wiki/Perceptual_hashing>. And pads the string. Can configure base.
|
|
* @param {number} base (optional) a number between 2 and 64 representing the base for the hash (e.g. 2 is binary, 10 is decimal, 16 is hex, 64 is base 64). Defaults to 64.
|
|
* @param {function(Error, Jimp)} cb (optional) a callback for when complete
|
|
* @returns {string} a string representing the hash
|
|
*/
|
|
hash(base, cb) {
|
|
base = base || 64;
|
|
if (typeof base === "function") {
|
|
cb = base;
|
|
base = 64;
|
|
}
|
|
if (typeof base !== "number") {
|
|
return throwError.call(this, "base must be a number", cb);
|
|
}
|
|
if (base < 2 || base > 64) {
|
|
return throwError.call(this, "base must be a number between 2 and 64", cb);
|
|
}
|
|
let hash = this.pHash();
|
|
hash = anyBase(anyBase.BIN, alphabet.slice(0, base))(hash);
|
|
while (hash.length < maxHashLength[base]) {
|
|
hash = "0" + hash; // pad out with leading zeros
|
|
}
|
|
|
|
if (isNodePattern(cb)) {
|
|
cb.call(this, null, hash);
|
|
}
|
|
return hash;
|
|
}
|
|
|
|
/**
|
|
* Calculates the perceptual hash
|
|
* @returns {number} the perceptual hash
|
|
*/
|
|
pHash() {
|
|
const pHash = new ImagePHash();
|
|
return pHash.getHash(this);
|
|
}
|
|
|
|
/**
|
|
* Calculates the hamming distance of the current image and a hash based on their perceptual hash
|
|
* @param {hash} compareHash hash to compare to
|
|
* @returns {number} a number ranging from 0 to 1, 0 means they are believed to be identical
|
|
*/
|
|
distanceFromHash(compareHash) {
|
|
const pHash = new ImagePHash();
|
|
const currentHash = pHash.getHash(this);
|
|
return pHash.distance(currentHash, compareHash);
|
|
}
|
|
|
|
/**
|
|
* Converts the image to a buffer
|
|
* @param {string} mime the mime type of the image buffer to be created
|
|
* @param {function(Error, Jimp)} cb a Node-style function to call with the buffer as the second argument
|
|
* @returns {Jimp} this for chaining of methods
|
|
*/
|
|
|
|
/**
|
|
* Returns the offset of a pixel in the bitmap buffer
|
|
* @param {number} x the x coordinate
|
|
* @param {number} y the y coordinate
|
|
* @param {number} edgeHandling (optional) define how to sum pixels from outside the border
|
|
* @param {number} cb (optional) a callback for when complete
|
|
* @returns {number} the index of the pixel or -1 if not found
|
|
*/
|
|
getPixelIndex(x, y, edgeHandling, cb) {
|
|
let xi;
|
|
let yi;
|
|
if (typeof edgeHandling === "function" && typeof cb === "undefined") {
|
|
cb = edgeHandling;
|
|
edgeHandling = null;
|
|
}
|
|
if (!edgeHandling) {
|
|
edgeHandling = Jimp.EDGE_EXTEND;
|
|
}
|
|
if (typeof x !== "number" || typeof y !== "number") {
|
|
return throwError.call(this, "x and y must be numbers", cb);
|
|
}
|
|
|
|
// round input
|
|
x = Math.round(x);
|
|
y = Math.round(y);
|
|
xi = x;
|
|
yi = y;
|
|
if (edgeHandling === Jimp.EDGE_EXTEND) {
|
|
if (x < 0) xi = 0;
|
|
if (x >= this.bitmap.width) xi = this.bitmap.width - 1;
|
|
if (y < 0) yi = 0;
|
|
if (y >= this.bitmap.height) yi = this.bitmap.height - 1;
|
|
}
|
|
if (edgeHandling === Jimp.EDGE_WRAP) {
|
|
if (x < 0) {
|
|
xi = this.bitmap.width + x;
|
|
}
|
|
if (x >= this.bitmap.width) {
|
|
xi = x % this.bitmap.width;
|
|
}
|
|
if (y < 0) {
|
|
yi = this.bitmap.height + y;
|
|
}
|
|
if (y >= this.bitmap.height) {
|
|
yi = y % this.bitmap.height;
|
|
}
|
|
}
|
|
let i = this.bitmap.width * yi + xi << 2;
|
|
|
|
// if out of bounds index is -1
|
|
if (xi < 0 || xi >= this.bitmap.width) {
|
|
i = -1;
|
|
}
|
|
if (yi < 0 || yi >= this.bitmap.height) {
|
|
i = -1;
|
|
}
|
|
if (isNodePattern(cb)) {
|
|
cb.call(this, null, i);
|
|
}
|
|
return i;
|
|
}
|
|
|
|
/**
|
|
* Returns the hex colour value of a pixel
|
|
* @param {number} x the x coordinate
|
|
* @param {number} y the y coordinate
|
|
* @param {function(Error, Jimp)} cb (optional) a callback for when complete
|
|
* @returns {number} the color of the pixel
|
|
*/
|
|
getPixelColor(x, y, cb) {
|
|
if (typeof x !== "number" || typeof y !== "number") return throwError.call(this, "x and y must be numbers", cb);
|
|
const idx = this.getPixelIndex(x, y);
|
|
const hex = this.bitmap.data.readUInt32BE(idx);
|
|
if (isNodePattern(cb)) {
|
|
cb.call(this, null, hex);
|
|
}
|
|
return hex;
|
|
}
|
|
/**
|
|
* Returns the hex colour value of a pixel
|
|
* @param {number} hex color to set
|
|
* @param {number} x the x coordinate
|
|
* @param {number} y the y coordinate
|
|
* @param {function(Error, Jimp)} cb (optional) a callback for when complete
|
|
* @returns {number} the index of the pixel or -1 if not found
|
|
*/
|
|
setPixelColor(hex, x, y, cb) {
|
|
if (typeof hex !== "number" || typeof x !== "number" || typeof y !== "number") return throwError.call(this, "hex, x and y must be numbers", cb);
|
|
const idx = this.getPixelIndex(x, y);
|
|
this.bitmap.data.writeUInt32BE(hex, idx);
|
|
if (isNodePattern(cb)) {
|
|
cb.call(this, null, this);
|
|
}
|
|
return this;
|
|
}
|
|
/**
|
|
* Determine if the image contains opaque pixels.
|
|
* @return {boolean} hasAlpha whether the image contains opaque pixels
|
|
*/
|
|
hasAlpha() {
|
|
const {
|
|
width,
|
|
height,
|
|
data
|
|
} = this.bitmap;
|
|
const byteLen = width * height << 2;
|
|
for (let idx = 3; idx < byteLen; idx += 4) {
|
|
if (data[idx] !== 0xff) {
|
|
return true;
|
|
}
|
|
}
|
|
return false;
|
|
}
|
|
|
|
/**
|
|
* Iterate scan through a region of the bitmap
|
|
* @param {number} x the x coordinate to begin the scan at
|
|
* @param {number} y the y coordinate to begin the scan at
|
|
* @param w the width of the scan region
|
|
* @param h the height of the scan region
|
|
* @returns {IterableIterator<{x: number, y: number, idx: number, image: Jimp}>}
|
|
*/
|
|
scanIterator(x, y, w, h) {
|
|
if (typeof x !== "number" || typeof y !== "number") {
|
|
return throwError.call(this, "x and y must be numbers");
|
|
}
|
|
if (typeof w !== "number" || typeof h !== "number") {
|
|
return throwError.call(this, "w and h must be numbers");
|
|
}
|
|
return scanIterator(this, x, y, w, h);
|
|
}
|
|
}
|
|
export function addConstants(constants) {
|
|
let jimpInstance = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : Jimp;
|
|
Object.entries(constants).forEach(_ref => {
|
|
let [name, value] = _ref;
|
|
jimpInstance[name] = value;
|
|
});
|
|
}
|
|
export function addJimpMethods(methods) {
|
|
let jimpInstance = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : Jimp;
|
|
Object.entries(methods).forEach(_ref2 => {
|
|
let [name, value] = _ref2;
|
|
jimpInstance.prototype[name] = value;
|
|
});
|
|
}
|
|
addConstants(constants);
|
|
addJimpMethods({
|
|
composite
|
|
});
|
|
Jimp.__extraConstructors = [];
|
|
|
|
/**
|
|
* Allow client libs to add new ways to build a Jimp object.
|
|
* @param {string} name identify the extra constructor.
|
|
* @param {function} test a function that returns true when it accepts the arguments passed to the main constructor.
|
|
* @param {function} run where the magic happens.
|
|
*/
|
|
Jimp.appendConstructorOption = function (name, test, run) {
|
|
Jimp.__extraConstructors.push({
|
|
name,
|
|
test,
|
|
run
|
|
});
|
|
};
|
|
|
|
/**
|
|
* Read an image from a file or a Buffer. Takes the same args as the constructor
|
|
* @returns {Promise} a promise
|
|
*/
|
|
Jimp.read = function () {
|
|
for (var _len3 = arguments.length, args = new Array(_len3), _key3 = 0; _key3 < _len3; _key3++) {
|
|
args[_key3] = arguments[_key3];
|
|
}
|
|
return new Promise((resolve, reject) => {
|
|
// eslint-disable-next-line no-new
|
|
new Jimp(...args, (err, image) => {
|
|
if (err) reject(err);else resolve(image);
|
|
});
|
|
});
|
|
};
|
|
Jimp.create = Jimp.read;
|
|
|
|
/**
|
|
* A static helper method that converts RGBA values to a single integer value
|
|
* @param {number} r the red value (0-255)
|
|
* @param {number} g the green value (0-255)
|
|
* @param {number} b the blue value (0-255)
|
|
* @param {number} a the alpha value (0-255)
|
|
* @param {function(Error, Jimp)} cb (optional) A callback for when complete
|
|
* @returns {number} an single integer colour value
|
|
*/
|
|
Jimp.rgbaToInt = function (r, g, b, a, cb) {
|
|
if (typeof r !== "number" || typeof g !== "number" || typeof b !== "number" || typeof a !== "number") {
|
|
return throwError.call(this, "r, g, b and a must be numbers", cb);
|
|
}
|
|
if (r < 0 || r > 255) {
|
|
return throwError.call(this, "r must be between 0 and 255", cb);
|
|
}
|
|
if (g < 0 || g > 255) {
|
|
throwError.call(this, "g must be between 0 and 255", cb);
|
|
}
|
|
if (b < 0 || b > 255) {
|
|
return throwError.call(this, "b must be between 0 and 255", cb);
|
|
}
|
|
if (a < 0 || a > 255) {
|
|
return throwError.call(this, "a must be between 0 and 255", cb);
|
|
}
|
|
let i = r & 0xff;
|
|
i <<= 8;
|
|
i |= g & 0xff;
|
|
i <<= 8;
|
|
i |= b & 0xff;
|
|
i <<= 8;
|
|
i |= a & 0xff;
|
|
|
|
// Ensure sign is correct
|
|
i >>>= 0;
|
|
if (isNodePattern(cb)) {
|
|
cb.call(this, null, i);
|
|
}
|
|
return i;
|
|
};
|
|
|
|
/**
|
|
* A static helper method that converts RGBA values to a single integer value
|
|
* @param {number} i a single integer value representing an RGBA colour (e.g. 0xFF0000FF for red)
|
|
* @param {function(Error, Jimp)} cb (optional) A callback for when complete
|
|
* @returns {object} an object with the properties r, g, b and a representing RGBA values
|
|
*/
|
|
Jimp.intToRGBA = function (i, cb) {
|
|
if (typeof i !== "number") {
|
|
return throwError.call(this, "i must be a number", cb);
|
|
}
|
|
const rgba = {};
|
|
rgba.r = Math.floor(i / Math.pow(256, 3));
|
|
rgba.g = Math.floor((i - rgba.r * Math.pow(256, 3)) / Math.pow(256, 2));
|
|
rgba.b = Math.floor((i - rgba.r * Math.pow(256, 3) - rgba.g * Math.pow(256, 2)) / Math.pow(256, 1));
|
|
rgba.a = Math.floor((i - rgba.r * Math.pow(256, 3) - rgba.g * Math.pow(256, 2) - rgba.b * Math.pow(256, 1)) / Math.pow(256, 0));
|
|
if (isNodePattern(cb)) {
|
|
cb.call(this, null, rgba);
|
|
}
|
|
return rgba;
|
|
};
|
|
|
|
/**
|
|
* Converts a css color (Hex, 8-digit (RGBA) Hex, RGB, RGBA, HSL, HSLA, HSV, HSVA, Named) to a hex number
|
|
* @param {string} cssColor a number
|
|
* @returns {number} a hex number representing a color
|
|
*/
|
|
Jimp.cssColorToHex = function (cssColor) {
|
|
cssColor = cssColor || 0; // 0, null, undefined, NaN
|
|
|
|
if (typeof cssColor === "number") return Number(cssColor);
|
|
return parseInt(tinyColor(cssColor).toHex8(), 16);
|
|
};
|
|
|
|
/**
|
|
* Limits a number to between 0 or 255
|
|
* @param {number} n a number
|
|
* @returns {number} the number limited to between 0 or 255
|
|
*/
|
|
Jimp.limit255 = function (n) {
|
|
n = Math.max(n, 0);
|
|
n = Math.min(n, 255);
|
|
return n;
|
|
};
|
|
|
|
/**
|
|
* Diffs two images and returns
|
|
* @param {Jimp} img1 a Jimp image to compare
|
|
* @param {Jimp} img2 a Jimp image to compare
|
|
* @param {number} threshold (optional) a number, 0 to 1, the smaller the value the more sensitive the comparison (default: 0.1)
|
|
* @returns {object} an object { percent: percent similar, diff: a Jimp image highlighting differences }
|
|
*/
|
|
Jimp.diff = function (img1, img2) {
|
|
let threshold = arguments.length > 2 && arguments[2] !== undefined ? arguments[2] : 0.1;
|
|
if (!(img1 instanceof Jimp) || !(img2 instanceof Jimp)) return throwError.call(this, "img1 and img2 must be an Jimp images");
|
|
const bmp1 = img1.bitmap;
|
|
const bmp2 = img2.bitmap;
|
|
if (bmp1.width !== bmp2.width || bmp1.height !== bmp2.height) {
|
|
if (bmp1.width * bmp1.height > bmp2.width * bmp2.height) {
|
|
// img1 is bigger
|
|
img1 = img1.cloneQuiet().resize(bmp2.width, bmp2.height);
|
|
} else {
|
|
// img2 is bigger (or they are the same in area)
|
|
img2 = img2.cloneQuiet().resize(bmp1.width, bmp1.height);
|
|
}
|
|
}
|
|
if (typeof threshold !== "number" || threshold < 0 || threshold > 1) {
|
|
return throwError.call(this, "threshold must be a number between 0 and 1");
|
|
}
|
|
const diff = new Jimp(bmp1.width, bmp1.height, 0xffffffff);
|
|
const numDiffPixels = pixelMatch(bmp1.data, bmp2.data, diff.bitmap.data, diff.bitmap.width, diff.bitmap.height, {
|
|
threshold
|
|
});
|
|
return {
|
|
percent: numDiffPixels / (diff.bitmap.width * diff.bitmap.height),
|
|
image: diff
|
|
};
|
|
};
|
|
|
|
/**
|
|
* Calculates the hamming distance of two images based on their perceptual hash
|
|
* @param {Jimp} img1 a Jimp image to compare
|
|
* @param {Jimp} img2 a Jimp image to compare
|
|
* @returns {number} a number ranging from 0 to 1, 0 means they are believed to be identical
|
|
*/
|
|
Jimp.distance = function (img1, img2) {
|
|
const phash = new ImagePHash();
|
|
const hash1 = phash.getHash(img1);
|
|
const hash2 = phash.getHash(img2);
|
|
return phash.distance(hash1, hash2);
|
|
};
|
|
|
|
/**
|
|
* Calculates the hamming distance of two images based on their perceptual hash
|
|
* @param {hash} hash1 a pHash
|
|
* @param {hash} hash2 a pHash
|
|
* @returns {number} a number ranging from 0 to 1, 0 means they are believed to be identical
|
|
*/
|
|
Jimp.compareHashes = function (hash1, hash2) {
|
|
const phash = new ImagePHash();
|
|
return phash.distance(hash1, hash2);
|
|
};
|
|
|
|
/**
|
|
* Compute color difference
|
|
* 0 means no difference, 1 means maximum difference.
|
|
* @param {number} rgba1: first color to compare.
|
|
* @param {number} rgba2: second color to compare.
|
|
* Both parameters must be an color object {r:val, g:val, b:val, a:val}
|
|
* Where `a` is optional and `val` is an integer between 0 and 255.
|
|
* @returns {number} float between 0 and 1.
|
|
*/
|
|
Jimp.colorDiff = function (rgba1, rgba2) {
|
|
const pow = n => Math.pow(n, 2);
|
|
const {
|
|
max
|
|
} = Math;
|
|
const maxVal = 255 * 255 * 3;
|
|
if (rgba1.a !== 0 && !rgba1.a) {
|
|
rgba1.a = 255;
|
|
}
|
|
if (rgba2.a !== 0 && !rgba2.a) {
|
|
rgba2.a = 255;
|
|
}
|
|
return (max(pow(rgba1.r - rgba2.r), pow(rgba1.r - rgba2.r - rgba1.a + rgba2.a)) + max(pow(rgba1.g - rgba2.g), pow(rgba1.g - rgba2.g - rgba1.a + rgba2.a)) + max(pow(rgba1.b - rgba2.b), pow(rgba1.b - rgba2.b - rgba1.a + rgba2.a))) / maxVal;
|
|
};
|
|
|
|
/**
|
|
* Helper to create Jimp methods that emit events before and after its execution.
|
|
* @param {string} methodName The name to be appended to Jimp prototype.
|
|
* @param {string} evName The event name to be called.
|
|
* It will be prefixed by `before-` and emitted when on method call.
|
|
* It will be appended by `ed` and emitted after the method run.
|
|
* @param {function} method A function implementing the method itself.
|
|
* It will also create a quiet version that will not emit events, to not
|
|
* mess the user code with many `changed` event calls. You can call with
|
|
* `methodName + "Quiet"`.
|
|
*
|
|
* The emitted event comes with a object parameter to the listener with the
|
|
* `methodName` as one attribute.
|
|
*/
|
|
export function jimpEvMethod(methodName, evName, method) {
|
|
const evNameBefore = "before-" + evName;
|
|
const evNameAfter = evName.replace(/e$/, "") + "ed";
|
|
Jimp.prototype[methodName] = function () {
|
|
let wrappedCb;
|
|
for (var _len4 = arguments.length, args = new Array(_len4), _key4 = 0; _key4 < _len4; _key4++) {
|
|
args[_key4] = arguments[_key4];
|
|
}
|
|
const cb = args[method.length - 1];
|
|
const jimpInstance = this;
|
|
if (typeof cb === "function") {
|
|
wrappedCb = function () {
|
|
for (var _len5 = arguments.length, args = new Array(_len5), _key5 = 0; _key5 < _len5; _key5++) {
|
|
args[_key5] = arguments[_key5];
|
|
}
|
|
const [err, data] = args;
|
|
if (err) {
|
|
jimpInstance.emitError(methodName, err);
|
|
} else {
|
|
jimpInstance.emitMulti(methodName, evNameAfter, {
|
|
[methodName]: data
|
|
});
|
|
}
|
|
cb.apply(this, args);
|
|
};
|
|
args[args.length - 1] = wrappedCb;
|
|
} else {
|
|
wrappedCb = false;
|
|
}
|
|
this.emitMulti(methodName, evNameBefore);
|
|
let result;
|
|
try {
|
|
result = method.apply(this, args);
|
|
if (!wrappedCb) {
|
|
this.emitMulti(methodName, evNameAfter, {
|
|
[methodName]: result
|
|
});
|
|
}
|
|
} catch (error) {
|
|
error.methodName = methodName;
|
|
this.emitError(methodName, error);
|
|
}
|
|
return result;
|
|
};
|
|
Jimp.prototype[methodName + "Quiet"] = method;
|
|
}
|
|
|
|
/**
|
|
* Creates a new image that is a clone of this one.
|
|
* @param {function(Error, Jimp)} cb (optional) A callback for when complete
|
|
* @returns the new image
|
|
*/
|
|
jimpEvMethod("clone", "clone", function (cb) {
|
|
const clone = new Jimp(this);
|
|
if (isNodePattern(cb)) {
|
|
cb.call(clone, null, clone);
|
|
}
|
|
return clone;
|
|
});
|
|
|
|
/**
|
|
* Simplify jimpEvMethod call for the common `change` evName.
|
|
* @param {string} methodName name of the method
|
|
* @param {function} method to watch changes for
|
|
*/
|
|
export function jimpEvChange(methodName, method) {
|
|
jimpEvMethod(methodName, "change", method);
|
|
}
|
|
|
|
/**
|
|
* Sets the type of the image (RGB or RGBA) when saving as PNG format (default is RGBA)
|
|
* @param b A Boolean, true to use RGBA or false to use RGB
|
|
* @param {function(Error, Jimp)} cb (optional) a callback for when complete
|
|
* @returns {Jimp} this for chaining of methods
|
|
*/
|
|
jimpEvChange("background", function (hex, cb) {
|
|
if (typeof hex !== "number") {
|
|
return throwError.call(this, "hex must be a hexadecimal rgba value", cb);
|
|
}
|
|
this._background = hex;
|
|
if (isNodePattern(cb)) {
|
|
cb.call(this, null, this);
|
|
}
|
|
return this;
|
|
});
|
|
|
|
/**
|
|
* Scans through a region of the bitmap, calling a function for each pixel.
|
|
* @param {number} x the x coordinate to begin the scan at
|
|
* @param {number} y the y coordinate to begin the scan at
|
|
* @param w the width of the scan region
|
|
* @param h the height of the scan region
|
|
* @param f a function to call on even pixel; the (x, y) position of the pixel
|
|
* and the index of the pixel in the bitmap buffer are passed to the function
|
|
* @param {function(Error, Jimp)} cb (optional) a callback for when complete
|
|
* @returns {Jimp} this for chaining of methods
|
|
*/
|
|
jimpEvChange("scan", function (x, y, w, h, f, cb) {
|
|
if (typeof x !== "number" || typeof y !== "number") {
|
|
return throwError.call(this, "x and y must be numbers", cb);
|
|
}
|
|
if (typeof w !== "number" || typeof h !== "number") {
|
|
return throwError.call(this, "w and h must be numbers", cb);
|
|
}
|
|
if (typeof f !== "function") {
|
|
return throwError.call(this, "f must be a function", cb);
|
|
}
|
|
const result = scan(this, x, y, w, h, f);
|
|
if (isNodePattern(cb)) {
|
|
cb.call(this, null, result);
|
|
}
|
|
return result;
|
|
});
|
|
if (process.env.ENVIRONMENT === "BROWSER") {
|
|
// For use in a web browser or web worker
|
|
/* global self */
|
|
let gl;
|
|
if (typeof window !== "undefined" && typeof window === "object") {
|
|
gl = window;
|
|
}
|
|
if (typeof self !== "undefined" && typeof self === "object") {
|
|
gl = self;
|
|
}
|
|
gl.Jimp = Jimp;
|
|
gl.Buffer = Buffer;
|
|
}
|
|
export { addType } from "./utils/mime";
|
|
export default Jimp;
|
|
//# sourceMappingURL=index.js.map
|