thedesk/app/node_modules/jimp/index.js
2018-02-19 02:41:25 +09:00

2589 lines
92 KiB
JavaScript

if (process.env.ENVIRONMENT !== "BROWSER") var FS = require("fs");
var PNG = require("pngjs").PNG;
var JPEG = require("jpeg-js");
var BMP = require("bmp-js");
var GIF = require("./omggif.js");
var MIME = require("mime");
var TinyColor = require("tinycolor2");
var Resize = require("./resize.js");
var Resize2 = require("./resize2.js");
var StreamToBuffer = require("stream-to-buffer");
var ReadChunk = require("read-chunk");
var FileType = require("file-type");
var PixelMatch = require("pixelmatch");
var EXIFParser = require("exif-parser");
var ImagePHash = require("./phash.js");
var BigNumber = require('bignumber.js');
var URLRegEx = require("url-regex");
var BMFont = require("load-bmfont");
var Path = require("path");
var MkDirP = require("mkdirp");
if (process.env.ENVIRONMENT !== "BROWSER") {
//If we run into electron renderer process, use XHR method instead of Request node module
if (process.versions.hasOwnProperty("electron") && process.type === "renderer" && typeof XMLHttpRequest === "function") {
var Request = function (url,cb) {
var xhr = new XMLHttpRequest();
xhr.open( "GET", url, true );
xhr.responseType = "arraybuffer";
xhr.onload = function() {
if (xhr.status < 400) {
try {
var data = Buffer.from(this.response);
} catch (e) {
return cb("Response is not a buffer for url "+url)
}
cb(null, xhr, data);
}
else cb("HTTP Status " + xhr.status + " for url "+url);
};
xhr.onerror = function(e) {
cb(e);
};
xhr.send();
};
} else {
var Request = require('request').defaults({ encoding: null });
}
}
// polyfill Promise for Node < 0.12
var Promise = global.Promise || require('es6-promise').Promise;
// logging methods
var chars = 0;
function log(msg) {
clear();
process.stdout.write(msg);
chars = msg.length;
}
function clear() {
while (chars-- > 0) {
process.stdout.write("\b");
}
}
process.on("exit", clear);
// no operation
function noop(){};
// error checking methods
function isNodePattern(cb) {
if ("undefined" == typeof cb) return false;
if ("function" != typeof cb)
throw new Error("Callback must be a function");
return true;
}
function throwError(error, cb) {
if ("string" == typeof error) error = console.error(error);
if ("function" == typeof cb) return cb.call(this, error);
else throw error;
}
/**
* Jimp constructor (from a file)
* @param path a path to the image
* @param (optional) cb a function to call when the image is parsed to a bitmap
*/
/**
* Jimp constructor (from another Jimp image)
* @param image a Jimp image to clone
* @param 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 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 (optional) cb a function to call when the image is parsed to a bitmap
*/
function Jimp() {
if ("number" == typeof arguments[0] && "number" == typeof arguments[1]) {
// create a new image
var w = arguments[0];
var h = arguments[1];
var cb = arguments[2];
if ("number" == typeof arguments[2]) {
this._background = arguments[2];
var cb = arguments[3];
}
if ("undefined" == typeof cb) cb = noop;
if ("function" != typeof cb)
return throwError.call(this, "cb must be a function", cb);
this.bitmap = {
data: new Buffer(w * h * 4),
width: w,
height: h
};
for (var i = 0; i < this.bitmap.data.length; i=i+4) {
this.bitmap.data.writeUInt32BE(this._background, i);
}
cb.call(this, null, this);
} else if (arguments[0] instanceof Jimp) {
// clone an existing Jimp
var original = arguments[0];
var cb = arguments[1];
if ("undefined" == typeof cb) cb = noop;
if ("function" != typeof cb)
return throwError.call(this, "cb must be a function", cb);
var bitmap = new Buffer(original.bitmap.data.length);
original.scan(0, 0, original.bitmap.width, original.bitmap.height, function (x, y, idx) {
var data = original.bitmap.data.readUInt32BE(idx, true);
bitmap.writeUInt32BE(data, idx, true);
});
this.bitmap = {
data: bitmap,
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;
cb.call(this, null, this);
} else if (URLRegEx({exact: true}).test(arguments[0])) {
// read from a URL
var url = arguments[0];
var cb = arguments[1];
if ("undefined" == typeof cb) cb = noop;
if ("function" != typeof cb)
return throwError.call(this, "cb must be a function", cb);
var that = this;
Request(url, function (err, response, data) {
if (err) return throwError.call(that, err, cb);
if ("object" == typeof data && Buffer.isBuffer(data)) {
var mime = getMIMEFromBuffer(data);
if ("string" != typeof mime)
return throwError.call(that, "Could not find MIME for Buffer <" + url + "> (HTTP: " + response.statusCode + ")", cb);
parseBitmap.call(that, data, mime, cb);
} else return throwError.call(that, "Could not load Buffer from URL <" + url + "> (HTTP: " + response.statusCode + ")", cb);
});
} else if ("string" == typeof arguments[0]) {
// read from a path
var path = arguments[0];
var cb = arguments[1];
if ("undefined" == typeof cb) cb = noop;
if ("function" != typeof cb)
return throwError.call(this, "cb must be a function", cb);
var that = this;
getMIMEFromPath(path, function (err, mime) {
FS.readFile(path, function (err, data) {
if (err) return throwError.call(that, err, cb);
parseBitmap.call(that, data, mime, cb);
});
});
} else if ("object" == typeof arguments[0]) {
// read from a buffer
var data = arguments[0];
var mime = getMIMEFromBuffer(data);
var cb = arguments[1];
if (!Buffer.isBuffer(data))
return throwError.call(this, "data must be a Buffer", cb);
if ("string" != typeof mime)
return throwError.call(this, "mime must be a string", cb);
if ("function" != typeof cb)
return throwError.call(this, "cb must be a function", cb);
parseBitmap.call(this, data, mime, cb);
} else {
return throwError.call(this, "No matching constructor overloading was found. Please see the docs for how to call the Jimp constructor.", cb);
}
}
/**
* Read an image from a file or a Buffer
* @param src the path to the file or a Buffer containing the file data
* @param cb (optional) a callback function when the file is read
* @retuns a promise
*/
Jimp.read = function(src, cb) {
var promise = new Promise(
function(resolve, reject) {
cb = cb || function(err, image) {
if (err) reject(err);
else resolve(image);
}
if ("string" != typeof src && ("object" != typeof src || !Buffer.isBuffer(src)))
return throwError.call(this, "src must be a string or a Buffer", cb);
var img = new Jimp(src, cb);
}
);
return promise;
}
// MIME type methods
function getMIMEFromBuffer(buffer, path) {
var fileTypeFromBuffer = FileType(buffer);
if (fileTypeFromBuffer) {
// If FileType returns something for buffer, then return the mime given
return fileTypeFromBuffer.mime;
}
else if (path) {
// If a path is supplied, and FileType yields no results, then retry with MIME
// Path can be either a file path or a url
return MIME.lookup(path)
} else {
return null;
}
}
// gets a MIME type of a file from the path to it
function getMIMEFromPath(path, cb) {
ReadChunk(path, 0, 262, function (err, buffer) {
if (err) {
cb(null, "");
} else {
var fileType = FileType(buffer);
return cb && cb(null, fileType && fileType.mime || "");
}
});
}
//=> {ext: 'png', mime: 'image/png'}
// gets image data from a GIF buffer
function getBitmapFromGIF(data){
var gifObj = new GIF.GifReader(data);
var gifData = new Buffer(gifObj.width * gifObj.height * 4);
gifObj.decodeAndBlitFrameRGBA(0, gifData);
return {
data: gifData,
width: gifObj.width,
height: gifObj.height
};
}
// parses a bitmap from the constructor to the JIMP bitmap property
function parseBitmap(data, mime, cb) {
var that = this;
this._originalMime = mime.toLowerCase();
switch (this.getMIME()) {
case Jimp.MIME_PNG:
var png = new PNG();
png.parse(data, function(err, data) {
if (err) return throwError.call(that, err, cb);
that.bitmap = {
data: new Buffer(data.data),
width: data.width,
height: data.height
};
return cb.call(that, null, that);
});
break;
case Jimp.MIME_JPEG:
try {
this.bitmap = JPEG.decode(data);
try { this._exif = EXIFParser.create(data).parse(); }
catch (err) { /* meh */ }
return cb.call(this, null, this);
} catch(err) {
return cb.call(this, err, this);
}
case Jimp.MIME_BMP:
case Jimp.MIME_X_MS_BMP:
this.bitmap = BMP.decode(data);
return cb.call(this, null, this);
case Jimp.MIME_GIF:
this.bitmap = getBitmapFromGIF(data);
return cb.call(this, null, this);
default:
return throwError.call(this, "Unsupported MIME type: " + mime, cb);
}
}
// used to auto resizing etc.
Jimp.AUTO = -1;
// supported mime types
Jimp.MIME_PNG = "image/png";
Jimp.MIME_JPEG = "image/jpeg";
Jimp.MIME_BMP = "image/bmp";
Jimp.MIME_X_MS_BMP = "image/x-ms-bmp";
Jimp.MIME_GIF = "image/gif";
// PNG filter types
Jimp.PNG_FILTER_AUTO = -1;
Jimp.PNG_FILTER_NONE = 0;
Jimp.PNG_FILTER_SUB = 1;
Jimp.PNG_FILTER_UP = 2;
Jimp.PNG_FILTER_AVERAGE = 3;
Jimp.PNG_FILTER_PAETH = 4;
Jimp.RESIZE_NEAREST_NEIGHBOR = 'nearestNeighbor';
Jimp.RESIZE_BILINEAR = 'bilinearInterpolation';
Jimp.RESIZE_BICUBIC = 'bicubicInterpolation';
Jimp.RESIZE_HERMITE = 'hermiteInterpolation';
Jimp.RESIZE_BEZIER = 'bezierInterpolation';
// Align modes for cover, contain, bit masks
Jimp.HORIZONTAL_ALIGN_LEFT = 1;
Jimp.HORIZONTAL_ALIGN_CENTER = 2;
Jimp.HORIZONTAL_ALIGN_RIGHT = 4;
Jimp.VERTICAL_ALIGN_TOP = 8;
Jimp.VERTICAL_ALIGN_MIDDLE = 16;
Jimp.VERTICAL_ALIGN_BOTTOM = 32;
// Font locations
Jimp.FONT_SANS_8_BLACK = Path.join(__dirname, "fonts/open-sans/open-sans-8-black/open-sans-8-black.fnt");
Jimp.FONT_SANS_16_BLACK = Path.join(__dirname, "fonts/open-sans/open-sans-16-black/open-sans-16-black.fnt");
Jimp.FONT_SANS_32_BLACK = Path.join(__dirname, "fonts/open-sans/open-sans-32-black/open-sans-32-black.fnt");
Jimp.FONT_SANS_64_BLACK = Path.join(__dirname, "fonts/open-sans/open-sans-64-black/open-sans-64-black.fnt");
Jimp.FONT_SANS_128_BLACK = Path.join(__dirname, "fonts/open-sans/open-sans-128-black/open-sans-128-black.fnt");
Jimp.FONT_SANS_8_WHITE = Path.join(__dirname, "fonts/open-sans/open-sans-8-white/open-sans-8-white.fnt");
Jimp.FONT_SANS_16_WHITE = Path.join(__dirname, "fonts/open-sans/open-sans-16-white/open-sans-16-white.fnt");
Jimp.FONT_SANS_32_WHITE = Path.join(__dirname, "fonts/open-sans/open-sans-32-white/open-sans-32-white.fnt");
Jimp.FONT_SANS_64_WHITE = Path.join(__dirname, "fonts/open-sans/open-sans-64-white/open-sans-64-white.fnt");
Jimp.FONT_SANS_128_WHITE = Path.join(__dirname, "fonts/open-sans/open-sans-128-white/open-sans-128-white.fnt");
// Edge Handling
Jimp.EDGE_EXTEND = 1;
Jimp.EDGE_WRAP = 2;
Jimp.EDGE_CROP = 3;
/**
* A static helper method that converts RGBA values to a single integer value
* @param r the red value (0-255)
* @param g the green value (0-255)
* @param b the blue value (0-255)
* @param a the alpha value (0-255)
* @param cb (optional) A callback for when complete
* @returns an single integer colour value
*/
Jimp.rgbaToInt = function(r, g, b, a, cb){
if ("number" != typeof r || "number" != typeof g || "number" != typeof b || "number" != typeof a)
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);
r = Math.round(r);
b = Math.round(b);
g = Math.round(g);
a = Math.round(a);
var i = (r * Math.pow(256, 3)) + (g * Math.pow(256, 2)) + (b * Math.pow(256, 1)) + (a * Math.pow(256, 0));
if (isNodePattern(cb)) return cb.call(this, null, i);
else return i;
}
/**
* A static helper method that converts RGBA values to a single integer value
* @param i a single integer value representing an RGBA colour (e.g. 0xFF0000FF for red)
* @param cb (optional) A callback for when complete
* @returns an object with the properties r, g, b and a representing RGBA values
*/
Jimp.intToRGBA = function(i, cb){
if ("number" != typeof i)
return throwError.call(this, "i must be a number", cb);
var 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)) return cb.call(this, null, rgba);
else return rgba;
}
/**
* Limits a number to between 0 or 255
* @param n a number
* @returns 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 img1 a Jimp image to compare
* @param img2 a Jimp image to compare
* @param (optional) threshold a number, 0 to 1, the smaller the value the more sensitive the comparison (default: 0.1)
* @returns an object { percent: percent similar, diff: a Jimp image highlighting differences }
*/
Jimp.diff = function (img1, img2, threshold) {
if (!(img1 instanceof Jimp) || !(img2 instanceof Jimp))
return throwError.call(this, "img1 and img2 must be an Jimp images");
if (img1.bitmap.width != img2.bitmap.width || img1.bitmap.height != img2.bitmap.height) {
switch (img1.bitmap.width * img1.bitmap.height > img2.bitmap.width * img2.bitmap.height) {
case true: // img1 is bigger
img1 = img1.clone().resize(img2.bitmap.width, img2.bitmap.height);
break;
default:
// img2 is bigger (or they are the same in area)
img2 = img2.clone().resize(img1.bitmap.width, img1.bitmap.height);
break;
}
}
threshold = threshold || 0.1;
if ("number" != typeof threshold || threshold < 0 || threshold > 1)
return throwError.call(this, "threshold must be a number between 0 and 1");
var diff = new Jimp(img1.bitmap.width, img1.bitmap.height, 0xFFFFFFFF);
var numDiffPixels = PixelMatch(
img1.bitmap.data,
img2.bitmap.data,
diff.bitmap.data,
diff.bitmap.width,
diff.bitmap.height,
{threshold: 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 img1 a Jimp image to compare
* @param img2 a Jimp image to compare
* @returns a number ranging from 0 to 1, 0 means they are believed to be identical
*/
Jimp.distance = function (img1, img2) {
var phash = new ImagePHash();
var hash1 = phash.getHash(img1);
var hash2 = phash.getHash(img2);
return phash.distance(hash1, hash2);
}
// 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
Jimp.prototype.bitmap = {
data: null,
width: null,
height: null
};
// The quality to be used when saving JPEG images
Jimp.prototype._quality = 100;
Jimp.prototype._deflateLevel = 9;
Jimp.prototype._deflateStrategy = 3;
Jimp.prototype._filterType = Jimp.PNG_FILTER_AUTO;
// Whether PNGs will be exported as RGB or RGBA
Jimp.prototype._rgba = true;
// Default colour to use for new pixels
Jimp.prototype._background = 0x00000000;
// Default MIME is PNG
Jimp.prototype._originalMime = Jimp.MIME_PNG;
// Exif data for the image
Jimp.prototype._exif = null;
/**
* Creates a new image that is a clone of this one.
* @param cb (optional) A callback for when complete
* @returns the new image
*/
Jimp.prototype.clone = function (cb) {
var clone = new Jimp(this);
if (isNodePattern(cb)) return cb.call(clone, null, clone);
else return clone;
};
/**
* Sets the quality of the image when saving as JPEG format (default is 100)
* @param n The quality to use 0-100
* @param (optional) cb a callback for when complete
* @returns this for chaining of methods
*/
Jimp.prototype.quality = function (n, cb) {
if ("number" != typeof n)
return throwError.call(this, "n must be a number", cb);
if (n < 0 || n > 100)
return throwError.call(this, "n must be a number 0 - 100", cb);
this._quality = Math.round(n);
if (isNodePattern(cb)) return cb.call(this, null, this);
else return this;
};
/**
* Sets the deflate level used when saving as PNG format (default is 9)
* @param l Deflate level to use 0-9. 0 is no compression. 9 (default) is maximum compression.
* @param (optional) cb a callback for when complete
* @returns this for chaining of methods
*/
Jimp.prototype.deflateLevel = function (l, cb) {
if ("number" != typeof l)
return throwError.call(this, "l must be a number", cb);
if (l < 0 || l > 9)
return throwError.call(this, "l must be a number 0 - 9", cb);
this._deflateLevel = Math.round(l);
if (isNodePattern(cb)) return cb.call(this, null, this);
else return this;
};
/**
* Sets the deflate strategy used when saving as PNG format (default is 3)
* @param s Deflate strategy to use 0-3.
* @param (optional) cb a callback for when complete
* @returns this for chaining of methods
*/
Jimp.prototype.deflateStrategy = function (s, cb) {
if ("number" != typeof s)
return throwError.call(this, "s must be a number", cb);
if (s < 0 || s > 3)
return throwError.call(this, "s must be a number 0 - 3", cb);
this._deflateStrategy = Math.round(s);
if (isNodePattern(cb)) return cb.call(this, null, this);
else return this;
};
/**
* Sets the filter type used when saving as PNG format (default is automatic filters)
* @param f The quality to use -1-4.
* @param (optional) cb a callback for when complete
* @returns this for chaining of methods
*/
Jimp.prototype.filterType = function (f, cb) {
if ("number" != typeof f)
return throwError.call(this, "n must be a number", cb);
if (f < -1 || f > 4)
return throwError.call(this, "n must be -1 (auto) or a number 0 - 4", cb);
this._filterType = Math.round(f);
if (isNodePattern(cb)) return cb.call(this, null, this);
else return this;
};
/**
* Sets the type of the image (RGB or RGBA) when saving as PNG format (default is RGBA)
* @param bool A Boolean, true to use RGBA or false to use RGB
* @param (optional) cb a callback for when complete
* @returns this for chaining of methods
*/
Jimp.prototype.rgba = function (bool, cb) {
if ("boolean" != typeof bool)
return throwError.call(this, "bool must be a boolean, true for RGBA or false for RGB", cb);
this._rgba = bool;
if (isNodePattern(cb)) return cb.call(this, null, this);
else return this;
};
/**
* 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 (optional) cb a callback for when complete
* @returns this for chaining of methods
*/
Jimp.prototype.background = function (hex, cb) {
if ("number" != typeof hex)
return throwError.call(this, "hex must be a hexadecimal rgba value", cb);
this._background = hex;
if (isNodePattern(cb)) return cb.call(this, null, this);
else return this;
};
/**
* Scanes through a region of the bitmap, calling a function for each pixel.
* @param x the x coordinate to begin the scan at
* @param y the y coordiante 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 (optional) cb a callback for when complete
* @returns this for chaining of methods
*/
Jimp.prototype.scan = function (x, y, w, h, f, cb) {
if ("number" != typeof x || "number" != typeof y)
return throwError.call(this, "x and y must be numbers", cb);
if ("number" != typeof w || "number" != typeof h)
return throwError.call(this, "w and h must be numbers", cb);
if ("function" != typeof f)
return throwError.call(this, "f must be a function", cb);
// round input
x = Math.round(x);
y = Math.round(y);
w = Math.round(w);
h = Math.round(h);
for (var _y = y; _y < (y + h); _y++) {
for (var _x = x; _x < (x + w); _x++) {
var idx = (this.bitmap.width * _y + _x) << 2;
f.call(this, _x, _y, idx);
}
}
if (isNodePattern(cb)) return cb.call(this, null, this);
else return this;
};
/**
* Returns the original MIME of the image (default: "image/png")
* @returns the MIME as a string
*/
Jimp.prototype.getMIME = function(){
var mime = this._originalMime || Jimp.MIME_PNG;
return mime;
}
/**
* Returns the appropriate file extension for the original MIME of the image (default: "png")
* @returns the file extension as a string
*/
Jimp.prototype.getExtension = function(){
var mime = this.getMIME();
return MIME.extension(mime);
}
/**
* Returns the offset of a pixel in the bitmap buffer
* @param x the x coordinate
* @param y the y coordinate
* @param (optional) edgeHandling define how to sum pixels from outside the border
* @param (optional) cb a callback for when complete
* @returns the index of the pixel or -1 if not found
*/
Jimp.prototype.getPixelIndex = function (x, y, edgeHandling, cb) {
var xi, yi;
if ("function" == typeof edgeHandling && "undefined" == typeof cb) {
cb = edgeHandling;
edgeHandling = null;
}
if (!edgeHandling) edgeHandling = Jimp.EDGE_EXTEND;
if ("number" != typeof x || "number" != typeof y)
return throwError.call(this, "x and y must be numbers", cb);
// round input
xi = x = Math.round(x);
yi = y = Math.round(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) xi = this.bitmap.height + y;
if (y>=this.bitmap.height) yi = y % this.bitmap.height;
}
var 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)) return cb.call(this, null, i);
else return i;
};
/**
* Returns the hex colour value of a pixel
* @param x the x coordinate
* @param y the y coordinate
* @param (optional) cb a callback for when complete
* @returns the index of the pixel or -1 if not found
*/
Jimp.prototype.getPixelColor = Jimp.prototype.getPixelColour = function (x, y, cb) {
if ("number" != typeof x || "number" != typeof y)
return throwError.call(this, "x and y must be numbers", cb);
// round input
x = Math.round(x);
y = Math.round(y);
var idx = this.getPixelIndex(x, y);
var hex = this.bitmap.data.readUInt32BE(idx);
if (isNodePattern(cb)) return cb.call(this, null, hex);
else return hex;
};
/**
* Returns the hex colour value of a pixel
* @param x the x coordinate
* @param y the y coordinate
* @param (optional) cb a callback for when complete
* @returns the index of the pixel or -1 if not found
*/
Jimp.prototype.setPixelColor = Jimp.prototype.setPixelColour = function (hex, x, y, cb) {
if ("number" != typeof hex || "number" != typeof x || "number" != typeof y)
return throwError.call(this, "hex, x and y must be numbers", cb);
// round input
x = Math.round(x);
y = Math.round(y);
var idx = this.getPixelIndex(x, y);
this.bitmap.data.writeUInt32BE(hex, idx, true);
if (isNodePattern(cb)) return cb.call(this, null, this);
else return this;
};
// an array storing the maximum string length of hashes at various bases
var maxHashLength = [];
for (var i = 0; i < 65; i++) {
var l = (i > 1) ? (new BigNumber(Array(64 + 1).join("1"), 2)).toString(i) : NaN;
maxHashLength.push(l.length);
}
/**
* Generates a perceptual hash of the image <https://en.wikipedia.org/wiki/Perceptual_hashing>.
* @param base (optional) a number between 2 and 64 representing the base for the hash (e.g. 2 is binary, 10 is decimaal, 16 is hex, 64 is base 64). Defaults to 64.
* @param (optional) cb a callback for when complete
* @returns a string representing the hash
*/
Jimp.prototype.hash = function(base, cb){
base = base || 64;
if ("function" == typeof base) {
cb = base;
base = 64;
}
if ("number" != typeof base)
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);
var hash = (new ImagePHash()).getHash(this);
hash = (new BigNumber(hash, 2)).toString(base);
while (hash.length < maxHashLength[base]) {
hash = "0" + hash; // pad out with leading zeros
}
if (isNodePattern(cb)) return cb.call(this, null, hash);
else return hash;
}
/*
* Automagically rotates an image based on its EXIF data (if present)
* @param (optional) cb a callback for when complete
* @returns this for chaining of methods
*/
Jimp.prototype.exifRotate = function (cb) {
if (this._exif && this._exif.tags && this._exif.tags.Orientation) {
switch (this._exif.tags.Orientation) {
case 1: // Horizontal (normal)
// do nothing
break;
case 2: // Mirror horizontal
this.mirror(true, false);
break;
case 3: // Rotate 180
this.rotate(180);
break;
case 4: // Mirror vertical
this.mirror(false, true);
break;
case 5: // Mirror horizontal and rotate 270 CW
this.mirror(true, false).rotate(270);
break;
case 6: // Rotate 90 CW
this.rotate(90);
break;
case 7: // Mirror horizontal and rotate 90 CW
this.mirror(true, false).rotate(90);
break;
case 8: // Rotate 270 CW
this.rotate(270);
break;
}
}
if (isNodePattern(cb)) return cb.call(this, null, this);
else return this;
}
/**
* Crops the image at a given point to a give size
* @param x the x coordinate to crop form
* @param y the y coordiante to crop form
* @param w the width of the crop region
* @param h the height of the crop region
* @param (optional) cb a callback for when complete
* @returns this for chaining of methods
*/
Jimp.prototype.crop = function (x, y, w, h, cb) {
if ("number" != typeof x || "number" != typeof y)
return throwError.call(this, "x and y must be numbers", cb);
if ("number" != typeof w || "number" != typeof h)
return throwError.call(this, "w and h must be numbers", cb);
// round input
x = Math.round(x);
y = Math.round(y);
w = Math.round(w);
h = Math.round(h);
var bitmap = new Buffer(this.bitmap.data.length);
var offset = 0;
this.scan(x, y, w, h, function (x, y, idx) {
var data = this.bitmap.data.readUInt32BE(idx, true);
bitmap.writeUInt32BE(data, offset, true);
offset += 4;
});
this.bitmap.data = new Buffer(bitmap);
this.bitmap.width = w;
this.bitmap.height = h;
if (isNodePattern(cb)) return cb.call(this, null, this);
else return this;
};
/**
* Autocrop same color borders from this image
* @param (optional) tolerance: a percent value of tolerance for
* pixels color difference (default: 0.0002%)
* @param (optional) cropOnlyFrames: flag to crop only real frames:
* all 4 sides of the image must have some border (default: true)
* @param (optional) cb: a callback for when complete (default: no callback)
* @returns this for chaining of methods
*/
Jimp.prototype.autocrop = function() {
var w = this.bitmap.width;
var h = this.bitmap.height;
var minPixelsPerSide = 1; // to avoid cropping completely the image, resulting in an invalid 0 sized image
var cb; // callback
var tolerance = 0.0002; // percent of color difference tolerance (default value)
var cropOnlyFrames = true; // flag to force cropping only if the image has a real "frame"
// i.e. all 4 sides have some border (default value)
// parse arguments
for (var a = 0, len = arguments.length; a < len; a++) {
if ("number" == typeof arguments[a]) { // tolerance value passed
tolerance = arguments[a];
}
if ("boolean" == typeof arguments[a]) { // tolerance value passed
cropOnlyFrames = arguments[a];
}
if ("function" == typeof arguments[a]) { // callback value passed
cb = arguments[a];
}
}
/**
* North and East borders must be of the same color as the top left pixel, to be cropped.
* South and West borders must be of the same color as the bottom right pixel, to be cropped.
* It should be possible to crop borders each with a different color,
* but since there are many ways for corners to intersect, it would
* introduce unnecessary complexity to the algorithm.
*/
// scan each side for same color borders
var colorTarget = this.getPixelColor(0, 0); // top left pixel color is the target color
// for north and east sides
var northPixelsToCrop = 0;
var eastPixelsToCrop = 0;
var southPixelsToCrop = 0;
var westPixelsToCrop = 0;
var rgba1 = Jimp.intToRGBA(colorTarget);
north: // north side (scan rows from north to south)
for (var y = 0; y < h - minPixelsPerSide; y++) {
for (var x = 0; x < w; x++) {
var colorXY = this.getPixelColor(x, y);
var rgba2 = Jimp.intToRGBA(colorXY);
var difference =
Math.abs(
Math.max((rgba1.r - rgba2.r) ^ 2, (rgba1.r - rgba2.r - rgba1.a + rgba2.a) ^ 2) +
Math.max((rgba1.g - rgba2.g) ^ 2, (rgba1.g - rgba2.g - rgba1.a + rgba2.a) ^ 2) +
Math.max((rgba1.b - rgba2.b) ^ 2, (rgba1.b - rgba2.b - rgba1.a + rgba2.a) ^ 2)
) / (256 * 256 * 3);
if (difference > tolerance) {
// this pixel is too distant from the first one: abort this side scan
break north;
}
}
// this row contains all pixels with the same color: increment this side pixels to crop
northPixelsToCrop++;
}
east: // east side (scan columns from east to west)
for (var x = w - 1; x >= 0; x--) {
for (var y = northPixelsToCrop; y < h; y++) {
var colorXY = this.getPixelColor(x, y);
var rgba2 = Jimp.intToRGBA(colorXY);
var difference =
Math.abs(
Math.max((rgba1.r - rgba2.r) ^ 2, (rgba1.r - rgba2.r - rgba1.a + rgba2.a) ^ 2) +
Math.max((rgba1.g - rgba2.g) ^ 2, (rgba1.g - rgba2.g - rgba1.a + rgba2.a) ^ 2) +
Math.max((rgba1.b - rgba2.b) ^ 2, (rgba1.b - rgba2.b - rgba1.a + rgba2.a) ^ 2)
) / (256 * 256 * 3);
if (difference > tolerance) {
// this pixel is too distant from the first one: abort this side scan
break east;
}
}
// this column contains all pixels with the same color: increment this side pixels to crop
eastPixelsToCrop++;
}
colorTarget = this.getPixelColor(w - 1, h - 1); // bottom right pixel color is the target color
// for south and west sides
south: // south side (scan rows from south to north)
for (var y = h - 1; y >= northPixelsToCrop + minPixelsPerSide; y--) {
for (var x = w - eastPixelsToCrop - 1; x >= 0; x--) {
var colorXY = this.getPixelColor(x, y);
var rgba2 = Jimp.intToRGBA(colorXY);
var difference =
Math.abs(
Math.max((rgba1.r - rgba2.r) ^ 2, (rgba1.r - rgba2.r - rgba1.a + rgba2.a) ^ 2) +
Math.max((rgba1.g - rgba2.g) ^ 2, (rgba1.g - rgba2.g - rgba1.a + rgba2.a) ^ 2) +
Math.max((rgba1.b - rgba2.b) ^ 2, (rgba1.b - rgba2.b - rgba1.a + rgba2.a) ^ 2)
) / (256 * 256 * 3);
if (difference > tolerance) {
// this pixel is too distant from the first one: abort this side scan
break south;
}
}
// this row contains all pixels with the same color: increment this side pixels to crop
southPixelsToCrop++;
}
west: // west side (scan columns from west to east)
for (var x = 0; x <= w - eastPixelsToCrop - minPixelsPerSide; x++) {
for (var y = h - southPixelsToCrop; y >= northPixelsToCrop; y--) {
var colorXY = this.getPixelColor(x, y);
var rgba2 = Jimp.intToRGBA(colorXY);
var difference =
Math.abs(
Math.max((rgba1.r - rgba2.r) ^ 2, (rgba1.r - rgba2.r - rgba1.a + rgba2.a) ^ 2) +
Math.max((rgba1.g - rgba2.g) ^ 2, (rgba1.g - rgba2.g - rgba1.a + rgba2.a) ^ 2) +
Math.max((rgba1.b - rgba2.b) ^ 2, (rgba1.b - rgba2.b - rgba1.a + rgba2.a) ^ 2)
) / (256 * 256 * 3);
if (difference > tolerance) {
// this pixel is too distant from the first one: abort this side scan
break west;
}
}
// this column contains all pixels with the same color: increment this side pixels to crop
westPixelsToCrop++;
}
// safety checks
var widthOfPixelsToCrop = w - (westPixelsToCrop + eastPixelsToCrop);
widthOfPixelsToCrop >= 0 ? widthOfPixelsToCrop : 0;
var heightOfPixelsToCrop = h - (southPixelsToCrop + northPixelsToCrop);
heightOfPixelsToCrop >= 0 ? heightOfPixelsToCrop : 0;
// decide if a crop is needed
var doCrop = false;
if (cropOnlyFrames) { // crop image if all sides should be cropped
doCrop = (
eastPixelsToCrop !== 0 &&
northPixelsToCrop !== 0 &&
westPixelsToCrop !== 0 &&
southPixelsToCrop !== 0
);
} else { // crop image if at least one side should be cropped
doCrop = (
eastPixelsToCrop !== 0 ||
northPixelsToCrop !== 0 ||
westPixelsToCrop !== 0 ||
southPixelsToCrop !== 0
);
}
if (doCrop) { // do the real crop
this.crop(
westPixelsToCrop,
northPixelsToCrop,
widthOfPixelsToCrop,
heightOfPixelsToCrop
);
}
if (isNodePattern(cb)) return cb.call(this, null, this);
else return this;
};
/**
* Blits a source image on to this image
* @param src the source Jimp instance
* @param x the x position to blit the image
* @param y the y position to blit the image
* @param srcx (optional) the x position from which to crop the source image
* @param srcy (optional) the y position from which to crop the source image
* @param srcw (optional) the width to which to crop the source image
* @param srch (optional) the height to which to crop the source image
* @param (optional) cb a callback for when complete
* @returns this for chaining of methods
*/
Jimp.prototype.blit = function (src, x, y, srcx, srcy, srcw, srch, cb) {
if (!(src instanceof Jimp))
return throwError.call(this, "The source must be a Jimp image", cb);
if ("number" != typeof x || "number" != typeof y)
return throwError.call(this, "x and y must be numbers", cb);
if (typeof srcx === "function") {
cb = srcx;
srcx = 0;
srcy = 0;
srcw = src.bitmap.width;
srch = src.bitmap.height;
} else if (typeof srcx == typeof srcy && typeof srcy == typeof srcw && typeof srcw == typeof srch) {
srcx = srcx || 0;
srcy = srcy || 0;
srcw = srcw || src.bitmap.width;
srch = srch || src.bitmap.height;
} else {
return throwError.call(this, "srcx, srcy, srcw, srch must be numbers", cb);
}
// round input
x = Math.round(x);
y = Math.round(y);
// round input
srcx = Math.round(srcx);
srcy = Math.round(srcy);
srcw = Math.round(srcw);
srch = Math.round(srch);
var that = this;
src.scan(srcx, srcy, srcw, srch, function(sx, sy, idx) {
var dstIdx = that.getPixelIndex(x+sx-srcx, y+sy-srcy);
that.bitmap.data[dstIdx] = this.bitmap.data[idx];
that.bitmap.data[dstIdx+1] = this.bitmap.data[idx+1];
that.bitmap.data[dstIdx+2] = this.bitmap.data[idx+2];
that.bitmap.data[dstIdx+3] = this.bitmap.data[idx+3];
});
if (isNodePattern(cb)) return cb.call(this, null, this);
else return this;
};
/**
* Masks a source image on to this image using average pixel colour. A completely black pixel on the mask will turn a pixel in the image completely transparent.
* @param src the source Jimp instance
* @param x the x position to blit the image
* @param y the y position to blit the image
* @param (optional) cb a callback for when complete
* @returns this for chaining of methods
*/
Jimp.prototype.mask = function (src, x, y, cb) {
if (!(src instanceof Jimp))
return throwError.call(this, "The source must be a Jimp image", cb);
if ("number" != typeof x || "number" != typeof y)
return throwError.call(this, "x and y must be numbers", cb);
// round input
x = Math.round(x);
y = Math.round(y);
var that = this;
src.scan(0, 0, src.bitmap.width, src.bitmap.height, function(sx, sy, idx) {
var dstIdx = that.getPixelIndex(x+sx, y+sy);
var avg = (this.bitmap.data[idx+0] + this.bitmap.data[idx+1] + this.bitmap.data[idx+2]) / 3;
that.bitmap.data[dstIdx+3] *= avg / 255;
});
if (isNodePattern(cb)) return cb.call(this, null, this);
else return this;
};
/**
* Composites a source image over to this image respecting alpha channels
* @param src the source Jimp instance
* @param x the x position to blit the image
* @param y the y position to blit the image
* @param (optional) cb a callback for when complete
* @returns this for chaining of methods
*/
Jimp.prototype.composite = function (src, x, y, cb) {
if (!(src instanceof Jimp))
return throwError.call(this, "The source must be a Jimp image", cb);
if ("number" != typeof x || "number" != typeof y)
return throwError.call(this, "x and y must be numbers", cb);
// round input
x = Math.round(x);
y = Math.round(y);
var that = this;
src.scan(0, 0, src.bitmap.width, src.bitmap.height, function(sx, sy, idx) {
// http://stackoverflow.com/questions/7438263/alpha-compositing-algorithm-blend-modes
var dstIdx = that.getPixelIndex(x+sx, y+sy);
var fg = {
r: this.bitmap.data[idx + 0] / 255,
g: this.bitmap.data[idx + 1] / 255,
b: this.bitmap.data[idx + 2] / 255,
a: this.bitmap.data[idx + 3] / 255
}
var bg = {
r: that.bitmap.data[dstIdx + 0] / 255,
g: that.bitmap.data[dstIdx + 1] / 255,
b: that.bitmap.data[dstIdx + 2] / 255,
a: that.bitmap.data[dstIdx + 3] / 255
}
var a = bg.a + fg.a - bg.a * fg.a;
var r = ((fg.r * fg.a) + (bg.r * bg.a) * (1 - fg.a)) / a;
var g = ((fg.g * fg.a) + (bg.g * bg.a) * (1 - fg.a)) / a;
var b = ((fg.b * fg.a) + (bg.b * bg.a) * (1 - fg.a)) / a;
that.bitmap.data[dstIdx + 0] = Jimp.limit255(r * 255);
that.bitmap.data[dstIdx + 1] = Jimp.limit255(g * 255);
that.bitmap.data[dstIdx + 2] = Jimp.limit255(b * 255);
that.bitmap.data[dstIdx + 3] = Jimp.limit255(a * 255);
});
if (isNodePattern(cb)) return cb.call(this, null, this);
else return this;
};
/**
* Adjusts the brightness of the image
* @param val the amount to adjust the brightness, a number between -1 and +1
* @param (optional) cb a callback for when complete
* @returns this for chaining of methods
*/
Jimp.prototype.brightness = function (val, cb) {
if ("number" != typeof val)
return throwError.call(this, "val must be numbers", cb);
if (val < -1 || val > +1)
return throwError.call(this, "val must be a number between -1 and +1", cb);
this.scan(0, 0, this.bitmap.width, this.bitmap.height, function (x, y, idx) {
if (val < 0.0) {
this.bitmap.data[idx] = this.bitmap.data[idx] * (1 + val);
this.bitmap.data[idx+1] = this.bitmap.data[idx+1] * (1 + val);
this.bitmap.data[idx+2] = this.bitmap.data[idx+2] * (1 + val);
} else {
this.bitmap.data[idx] = this.bitmap.data[idx] + ((255 - this.bitmap.data[idx]) * val);
this.bitmap.data[idx+1] = this.bitmap.data[idx+1] + ((255 - this.bitmap.data[idx+1]) * val);
this.bitmap.data[idx+2] = this.bitmap.data[idx+2] + ((255 - this.bitmap.data[idx+2]) * val);
}
});
if (isNodePattern(cb)) return cb.call(this, null, this);
else return this;
};
/**
* Adjusts the contrast of the image
* val the amount to adjust the contrast, a number between -1 and +1
* @param (optional) cb a callback for when complete
* @returns this for chaining of methods
*/
Jimp.prototype.contrast = function (val, cb) {
if ("number" != typeof val)
return throwError.call(this, "val must be numbers", cb);
if (val < -1 || val > +1)
return throwError.call(this, "val must be a number between -1 and +1", cb);
function adjust(value) {
if (val < 0) {
var x = (value > 127) ? 1 - value / 255 : value / 255;
if (x < 0) x = 0;
x = 0.5 * Math.pow (x * 2, 1 + val);
return (value > 127) ? (1.0 - x) * 255 : x * 255;
} else {
var x = (value > 127) ? 1 - value / 255 : value / 255;
if (x < 0) x = 0;
x = 0.5 * Math.pow (2 * x, ((val == 1) ? 127 : 1 / (1 - val)));
return (value > 127) ? (1 - x) * 255 : x * 255;
}
}
this.scan(0, 0, this.bitmap.width, this.bitmap.height, function (x, y, idx) {
this.bitmap.data[idx] = adjust(this.bitmap.data[idx]);
this.bitmap.data[idx+1] = adjust(this.bitmap.data[idx+1]);
this.bitmap.data[idx+2] = adjust(this.bitmap.data[idx+2]);
});
if (isNodePattern(cb)) return cb.call(this, null, this);
else return this;
};
/**
* Apply a posterize effect
* val the amount to adjust the contrast, minimum threshold is two
* @param (optional) cb a callback for when complete
* @returns this for chaining of methods
*/
Jimp.prototype.posterize = function (n, cb) {
if ("number" != typeof n)
return throwError.call(this, "n must be numbers", cb);
if (n < 2) n = 2; // minumum of 2 levels
this.scan(0, 0, this.bitmap.width, this.bitmap.height, function (x, y, idx) {
this.bitmap.data[idx] = (Math.floor(this.bitmap.data[idx] / 255 * (n - 1)) / (n - 1)) * 255;
this.bitmap.data[idx+1] = (Math.floor(this.bitmap.data[idx+1] / 255 * (n - 1)) / (n - 1)) * 255;
this.bitmap.data[idx+2] = (Math.floor(this.bitmap.data[idx+2] / 255 * (n - 1)) / (n - 1)) * 255;
});
if (isNodePattern(cb)) return cb.call(this, null, this);
else return this;
};
/**
* Get an image's histogram
* @return {object} An object with an array of color occurence counts for each channel (r,g,b)
*/
function histogram() {
var histogram = {
r: new Array(256).fill(0),
g: new Array(256).fill(0),
b: new Array(256).fill(0)
};
this.scan(0, 0, this.bitmap.width, this.bitmap.height, function(x, y, index){
histogram.r[this.bitmap.data[index+0]]++;
histogram.g[this.bitmap.data[index+1]]++;
histogram.b[this.bitmap.data[index+2]]++;
});
return histogram;
}
/**
* Normalizes the image
* @param (optional) cb a callback for when complete
* @returns this for chaining of methods
*/
Jimp.prototype.normalize = function (cb) {
var h = histogram.call(this);
/**
* Normalize values
* @param {integer} value Pixel channel value.
* @param {integer} min Minimum value for channel
* @param {integer} max Maximum value for channel
* @return {integer}
*/
var normalize = function (value, min, max) {
return (value - min) * 255 / (max - min);
};
var getBounds = function (histogramChannel) {
return [
histogramChannel.findIndex(function(value) {
return value > 0;
}),
255 - histogramChannel.slice().reverse().findIndex(function(value) {
return value > 0;
})
];
};
// store bounds (minimum and maximum values)
var bounds = {
r: getBounds(h.r),
g: getBounds(h.g),
b: getBounds(h.b)
};
// apply value transformations
this.scan(0, 0, this.bitmap.width, this.bitmap.height, function (x, y, idx) {
var r = this.bitmap.data[idx + 0];
var g = this.bitmap.data[idx + 1];
var b = this.bitmap.data[idx + 2];
this.bitmap.data[idx + 0] = normalize(r, bounds.r[0], bounds.r[1]);
this.bitmap.data[idx + 1] = normalize(g, bounds.g[0], bounds.g[1]);
this.bitmap.data[idx + 2] = normalize(b, bounds.b[0], bounds.b[1]);
});
if (isNodePattern(cb)) return cb.call(this, null, this);
else return this;
}
/**
* Inverts the image
* @param (optional) cb a callback for when complete
* @returns this for chaining of methods
*/
Jimp.prototype.invert = function (cb) {
this.scan(0, 0, this.bitmap.width, this.bitmap.height, function (x, y, idx) {
this.bitmap.data[idx] = 255 - this.bitmap.data[idx];
this.bitmap.data[idx+1] = 255 - this.bitmap.data[idx+1];
this.bitmap.data[idx+2] = 255 - this.bitmap.data[idx+2];
});
if (isNodePattern(cb)) return cb.call(this, null, this);
else return this;
};
/**
* Flip the image horizontally
* @param horizontal a Boolean, if true the image will be flipped horizontally
* @param vertical a Boolean, if true the image will be flipped vertically
* @param (optional) cb a callback for when complete
* @returns this for chaining of methods
*/
Jimp.prototype.mirror = Jimp.prototype.flip = function (horizontal, vertical, cb) {
if ("boolean" != typeof horizontal || "boolean" != typeof vertical)
return throwError.call(this, "horizontal and vertical must be Booleans", cb);
var bitmap = new Buffer(this.bitmap.data.length);
this.scan(0, 0, this.bitmap.width, this.bitmap.height, function (x, y, idx) {
var _x = (horizontal) ? (this.bitmap.width - 1 - x) : x;
var _y = (vertical) ? (this.bitmap.height - 1 - y) : y;
var _idx = (this.bitmap.width * _y + _x) << 2;
var data = this.bitmap.data.readUInt32BE(idx, true);
bitmap.writeUInt32BE(data, _idx, true);
});
this.bitmap.data = new Buffer(bitmap);
if (isNodePattern(cb)) return cb.call(this, null, this);
else return this;
};
/**
* Applies a true Gaussian blur to the image (warning: this is VERY slow)
* @param r the pixel radius of the blur
* @param (optional) cb a callback for when complete
* @returns this for chaining of methods
*/
Jimp.prototype.gaussian = function (r, cb) {
// http://blog.ivank.net/fastest-gaussian-blur.html
if ("number" != typeof r)
return throwError.call(this, "r must be a number", cb);
if (r < 1)
return throwError.call(this, "r must be greater than 0", cb);
var rs = Math.ceil(r * 2.57); // significant radius
for (var y = 0; y < this.bitmap.height; y++) {
log("Gaussian: " + Math.round(y / this.bitmap.height * 100) + "%");
for (var x = 0; x < this.bitmap.width; x++) {
var red = 0;
var green = 0;
var blue = 0;
var alpha = 0;
var wsum = 0;
for (var iy = y - rs; iy < y + rs + 1; iy++) {
for (var ix = x - rs; ix < x + rs + 1; ix++) {
var x1 = Math.min(this.bitmap.width - 1, Math.max(0, ix));
var y1 = Math.min(this.bitmap.height - 1, Math.max(0, iy));
var dsq = (ix - x) * (ix - x) + (iy - y) * (iy - y);
var wght = Math.exp( -dsq / (2*r*r) ) / (Math.PI*2*r*r);
var idx = (y1 * this.bitmap.width + x1) << 2;
red += this.bitmap.data[idx] * wght;
green += this.bitmap.data[idx+1] * wght;
blue += this.bitmap.data[idx+2] * wght;
alpha += this.bitmap.data[idx+3] * wght;
wsum += wght;
}
var idx = (y * this.bitmap.width + x) << 2;
this.bitmap.data[idx] = Math.round( red / wsum);
this.bitmap.data[idx+1] = Math.round( green / wsum);
this.bitmap.data[idx+2] = Math.round( blue / wsum);
this.bitmap.data[idx+3] = Math.round( alpha / wsum);
}
}
}
clear(); // clear the log
if (isNodePattern(cb)) return cb.call(this, null, this);
else return this;
};
/*
Superfast Blur (0.5)
http://www.quasimondo.com/BoxBlurForCanvas/FastBlur.js
Copyright (c) 2011 Mario Klingemann
Permission is hereby granted, free of charge, to any person
obtaining a copy of this software and associated documentation
files (the "Software"), to deal in the Software without
restriction, including without limitation the rights to use,
copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the
Software is furnished to do so, subject to the following
conditions:
The above copyright notice and this permission notice shall be
included in all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES
OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT
HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
OTHER DEALINGS IN THE SOFTWARE.
*/
var mul_table = [1,57,41,21,203,34,97,73,227,91,149,62,105,45,39,137,241,107,3,173,39,71,65,238,219,101,187,87,81,151,141,133,249,117,221,209,197,187,177,169,5,153,73,139,133,127,243,233,223,107,103,99,191,23,177,171,165,159,77,149,9,139,135,131,253,245,119,231,224,109,211,103,25,195,189,23,45,175,171,83,81,79,155,151,147,9,141,137,67,131,129,251,123,30,235,115,113,221,217,53,13,51,50,49,193,189,185,91,179,175,43,169,83,163,5,79,155,19,75,147,145,143,35,69,17,67,33,65,255,251,247,243,239,59,29,229,113,111,219,27,213,105,207,51,201,199,49,193,191,47,93,183,181,179,11,87,43,85,167,165,163,161,159,157,155,77,19,75,37,73,145,143,141,35,138,137,135,67,33,131,129,255,63,250,247,61,121,239,237,117,29,229,227,225,111,55,109,216,213,211,209,207,205,203,201,199,197,195,193,48,190,47,93,185,183,181,179,178,176,175,173,171,85,21,167,165,41,163,161,5,79,157,78,154,153,19,75,149,74,147,73,144,143,71,141,140,139,137,17,135,134,133,66,131,65,129,1];
var shg_table = [0,9,10,10,14,12,14,14,16,15,16,15,16,15,15,17,18,17,12,18,16,17,17,19,19,18,19,18,18,19,19,19,20,19,20,20,20,20,20,20,15,20,19,20,20,20,21,21,21,20,20,20,21,18,21,21,21,21,20,21,17,21,21,21,22,22,21,22,22,21,22,21,19,22,22,19,20,22,22,21,21,21,22,22,22,18,22,22,21,22,22,23,22,20,23,22,22,23,23,21,19,21,21,21,23,23,23,22,23,23,21,23,22,23,18,22,23,20,22,23,23,23,21,22,20,22,21,22,24,24,24,24,24,22,21,24,23,23,24,21,24,23,24,22,24,24,22,24,24,22,23,24,24,24,20,23,22,23,24,24,24,24,24,24,24,23,21,23,22,23,24,24,24,22,24,24,24,23,22,24,24,25,23,25,25,23,24,25,25,24,22,25,25,25,24,23,24,25,25,25,25,25,25,25,25,25,25,25,25,23,25,23,24,25,25,25,25,25,25,25,25,25,24,22,25,25,23,25,25,20,24,25,24,25,25,22,24,25,24,25,24,25,25,24,25,25,25,25,22,25,25,25,24,25,24,25,18];
/**
* A fast blur algorithm that produces similar effect to a Gausian blur - but MUCH quicker
* @param r the pixel radius of the blur
* @param (optional) cb a callback for when complete
* @returns this for chaining of methods
*/
Jimp.prototype.blur = function (r, cb) {
if ("number" != typeof r)
return throwError.call(this, "r must be a number", cb);
if (r < 1)
return throwError.call(this, "r must be greater than 0", cb);
var rsum, gsum, bsum, asum, x, y, i, p, p1, p2, yp, yi, yw, idx, pa;
var wm = this.bitmap.width - 1;
var hm = this.bitmap.height - 1;
var wh = this.bitmap.width * this.bitmap.height;
var rad1 = r + 1;
var mul_sum = mul_table[r];
var shg_sum = shg_table[r];
var red = [];
var green = [];
var blue = [];
var alpha = [];
var vmin = [];
var vmax = [];
var iterations = 2;
while (iterations-- > 0) {
yw = yi = 0;
for (y = 0; y < this.bitmap.height; y++) {
rsum = this.bitmap.data[yw] * rad1;
gsum = this.bitmap.data[yw + 1] * rad1;
bsum = this.bitmap.data[yw + 2] * rad1;
asum = this.bitmap.data[yw + 3] * rad1;
for (i = 1; i <= r; i++) {
p = yw + (((i > wm ? wm : i)) << 2);
rsum += this.bitmap.data[p++];
gsum += this.bitmap.data[p++];
bsum += this.bitmap.data[p++];
asum += this.bitmap.data[p];
}
for (x = 0; x < this.bitmap.width; x++) {
red[yi] = rsum;
green[yi] = gsum;
blue[yi] = bsum;
alpha[yi] = asum;
if (y == 0) {
vmin[x] = ((p = x + rad1) < wm ? p : wm) << 2;
vmax[x] = ((p = x - r) > 0 ? p << 2 : 0);
}
p1 = yw + vmin[x];
p2 = yw + vmax[x];
rsum += this.bitmap.data[p1++] - this.bitmap.data[p2++];
gsum += this.bitmap.data[p1++] - this.bitmap.data[p2++];
bsum += this.bitmap.data[p1++] - this.bitmap.data[p2++];
asum += this.bitmap.data[p1] - this.bitmap.data[p2];
yi++;
}
yw += (this.bitmap.width << 2);
}
for (x = 0; x < this.bitmap.width; x++) {
yp = x;
rsum = red[yp] * rad1;
gsum = green[yp] * rad1;
bsum = blue[yp] * rad1;
asum = alpha[yp] * rad1;
for (i = 1; i <= r; i++) {
yp += (i > hm ? 0 : this.bitmap.width);
rsum += red[yp];
gsum += green[yp];
bsum += blue[yp];
asum += alpha[yp];
}
yi = x << 2;
for (y = 0; y < this.bitmap.height; y++) {
this.bitmap.data[yi + 3] = pa = (asum * mul_sum) >>> shg_sum;
if (pa > 255) this.bitmap.data[yi + 3] = 255; // normalise alpha
if (pa > 0) {
pa = 255 / pa;
this.bitmap.data[yi] = ((rsum * mul_sum) >>> shg_sum) * pa;
this.bitmap.data[yi + 1] = ((gsum * mul_sum) >>> shg_sum) * pa;
this.bitmap.data[yi + 2] = ((bsum * mul_sum) >>> shg_sum) * pa;
} else {
this.bitmap.data[yi] = this.bitmap.data[yi + 1] = this.bitmap.data[yi + 2] = 0;
}
if (x == 0) {
vmin[y] = ((p = y + rad1) < hm ? p : hm) * this.bitmap.width;
vmax[y] = ((p = y - r) > 0 ? p * this.bitmap.width : 0);
}
p1 = x + vmin[y];
p2 = x + vmax[y];
rsum += red[p1] - red[p2];
gsum += green[p1] - green[p2];
bsum += blue[p1] - blue[p2];
asum += alpha[p1] - alpha[p2];
yi += this.bitmap.width << 2;
}
}
}
if (isNodePattern(cb)) return cb.call(this, null, this);
else return this;
};
/**
* Removes colour from the image using ITU Rec 709 luminance values
* @param (optional) cb a callback for when complete
* @returns this for chaining of methods
*/
Jimp.prototype.greyscale = function (cb) {
this.scan(0, 0, this.bitmap.width, this.bitmap.height, function (x, y, idx) {
var grey = parseInt(.2126 * this.bitmap.data[idx] + .7152 * this.bitmap.data[idx+1] + .0722 * this.bitmap.data[idx+2], 10);
this.bitmap.data[idx] = grey;
this.bitmap.data[idx+1] = grey;
this.bitmap.data[idx+2] = grey;
});
if (isNodePattern(cb)) return cb.call(this, null, this);
else return this;
};
// Alias of greyscale for our American friends
Jimp.prototype.grayscale = Jimp.prototype.greyscale;
/**
* Applies a sepia tone to the image
* @param (optional) cb a callback for when complete
* @returns this for chaining of methods
*/
Jimp.prototype.sepia = function (cb) {
this.scan(0, 0, this.bitmap.width, this.bitmap.height, function (x, y, idx) {
var red = this.bitmap.data[idx];
var green = this.bitmap.data[idx+1];
var blue = this.bitmap.data[idx+2];
red = (red * 0.393) + (green * 0.769) + (blue * 0.189);
green = (red * 0.349) + (green * 0.686) + (blue * 0.168);
blue = (red * 0.272) + (green * 0.534) + (blue * 0.131);
this.bitmap.data[idx] = (red < 255) ? red : 255;
this.bitmap.data[idx+1] = (green < 255) ? green : 255;
this.bitmap.data[idx+2] = (blue < 255) ? blue : 255;
});
if (isNodePattern(cb)) return cb.call(this, null, this);
else return this;
};
/**
* Multiplies the opacity of each pixel by a factor between 0 and 1
* @param f A number, the factor by wich to multiply the opacity of each pixel
* @param (optional) cb a callback for when complete
* @returns this for chaining of methods
*/
Jimp.prototype.opacity = function (f, cb) {
if ("number" != typeof f)
return throwError.call(this, "f must be a number", cb);
if (f < 0 || f > 1)
return throwError.call(this, "f must be a number from 0 to 1", cb);
this.scan(0, 0, this.bitmap.width, this.bitmap.height, function (x, y, idx) {
var v = this.bitmap.data[idx+3] * f;
this.bitmap.data[idx+3] = v;
});
if (isNodePattern(cb)) return cb.call(this, null, this);
else return this;
};
/**
* Fades each pixel by a factor between 0 and 1
* @param f A number from 0 to 1. 0 will haven no effect. 1 will turn the image completely transparent.
* @param (optional) cb a callback for when complete
* @returns this for chaining of methods
*/
Jimp.prototype.fade = function (f, cb) {
if ("number" != typeof f)
return throwError.call(this, "f must be a number", cb);
if (f < 0 || f > 1)
return throwError.call(this, "f must be a number from 0 to 1", cb);
// this method is an alternative to opacity (which may be deprecated)
this.opacity(1 - f);
if (isNodePattern(cb)) return cb.call(this, null, this);
else return this;
};
/**
* Set the alpha channel on every pixel to fully opaque
* @param (optional) cb a callback for when complete
* @returns this for chaining of methods
*/
Jimp.prototype.opaque = function (cb) {
this.scan(0, 0, this.bitmap.width, this.bitmap.height, function (x, y, idx) {
this.bitmap.data[idx+3] = 255;
});
if (isNodePattern(cb)) return cb.call(this, null, this);
else return this;
};
/**
* Resizes the image to a set width and height using a 2-pass bilinear algorithm
* @param w the width to resize the image to (or Jimp.AUTO)
* @param h the height to resize the image to (or Jimp.AUTO)
* @param (optional) mode a scaling method (e.g. Jimp.RESIZE_BEZIER)
* @param (optional) cb a callback for when complete
* @returns this for chaining of methods
*/
Jimp.prototype.resize = function (w, h, mode, cb) {
if ("number" != typeof w || "number" != typeof h)
return throwError.call(this, "w and h must be numbers", cb);
if ("function" == typeof mode && "undefined" == typeof cb) {
cb = mode;
mode = null;
}
if (w == Jimp.AUTO && h == Jimp.AUTO)
return throwError.call(this, "w and h cannot both the set to auto", cb);
if (w == Jimp.AUTO) w = this.bitmap.width * (h / this.bitmap.height);
if (h == Jimp.AUTO) h = this.bitmap.height * (w / this.bitmap.width);
// round inputs
w = Math.round(w);
h = Math.round(h);
if ("function" == typeof Resize2[mode]) {
var dst = {
data: new Buffer(w * h * 4),
width: w,
height: h
};
Resize2[mode](this.bitmap, dst);
this.bitmap = dst;
} else {
var that = this;
var resize = new Resize(this.bitmap.width, this.bitmap.height, w, h, true, true, function (buffer) {
that.bitmap.data = new Buffer(buffer);
that.bitmap.width = w;
that.bitmap.height = h;
});
resize.resize(this.bitmap.data);
}
if (isNodePattern(cb)) return cb.call(this, null, this);
else return this;
};
/**
* Scale the image so the given width and height keeping the aspect ratio. Some parts of the image may be clipped.
* @param w the width to resize the image to
* @param h the height to resize the image to
* @param (optional) alignBits A bitmask for horizontal and vertical alignment
* @param (optional) mode a scaling method (e.g. Jimp.RESIZE_BEZIER)
* @param (optional) cb a callback for when complete
* @returns this for chaining of methods
*/
Jimp.prototype.cover = function (w, h, alignBits, mode, cb) {
if ("number" != typeof w || "number" != typeof h)
return throwError.call(this, "w and h must be numbers", cb);
if (alignBits && "function" == typeof alignBits && "undefined" == typeof cb) {
cb = alignBits;
alignBits = null;
mode = null;
} else if ("function" == typeof mode && "undefined" == typeof cb) {
cb = mode;
mode = null;
}
alignBits = alignBits || (Jimp.HORIZONTAL_ALIGN_CENTER | Jimp.VERTICAL_ALIGN_MIDDLE);
var hbits = ((alignBits) & ((1<<(3))-1));
var vbits = alignBits >> 3;
// check if more flags than one is in the bit sets
if(!(((hbits != 0) && !(hbits & (hbits - 1))) || ((vbits != 0) && !(vbits & (vbits - 1)))))
return throwError.call(this, "only use one flag per alignment direction", cb);
var align_h = (hbits >> 1); // 0, 1, 2
var align_v = (vbits >> 1); // 0, 1, 2
var f = (w/h > this.bitmap.width/this.bitmap.height) ?
w/this.bitmap.width : h/this.bitmap.height;
this.scale(f, mode);
this.crop(((this.bitmap.width - w) / 2) * align_h, ((this.bitmap.height - h) / 2) * align_v, w, h);
if (isNodePattern(cb)) return cb.call(this, null, this);
else return this;
};
/**
* Scale the image to the given width and height keeping the aspect ratio. Some parts of the image may be letter boxed.
* @param w the width to resize the image to
* @param h the height to resize the image to
* @param (optional) alignBits A bitmask for horizontal and vertical alignment
* @param (optional) mode a scaling method (e.g. Jimp.RESIZE_BEZIER)
* @param (optional) cb a callback for when complete
* @returns this for chaining of methods
*/
Jimp.prototype.contain = function (w, h, alignBits, mode, cb) {
if ("number" != typeof w || "number" != typeof h)
return throwError.call(this, "w and h must be numbers", cb);
//permit any sort of optional parameters combination
switch (typeof alignBits) {
case 'string':
if ("function" == typeof mode && "undefined" == typeof cb) cb = mode;
mode = alignBits;
alignBits = null;
case 'function':
if ("undefined" == typeof cb) cb = alignBits;
mode = null;
alignBits = null;
default:
if ("function" == typeof mode && "undefined" == typeof cb) {
cb = mode;
mode = null;
}
}
alignBits = alignBits || (Jimp.HORIZONTAL_ALIGN_CENTER | Jimp.VERTICAL_ALIGN_MIDDLE);
var hbits = ((alignBits) & ((1<<(3))-1));
var vbits = alignBits >> 3;
// check if more flags than one is in the bit sets
if(!(((hbits != 0) && !(hbits & (hbits - 1))) || ((vbits != 0) && !(vbits & (vbits - 1)))))
return throwError.call(this, "only use one flag per alignment direction", cb);
var align_h = (hbits >> 1); // 0, 1, 2
var align_v = (vbits >> 1); // 0, 1, 2
var f = (w/h > this.bitmap.width/this.bitmap.height) ?
h/this.bitmap.height : w/this.bitmap.width;
var c = this.clone().scale(f, mode);
this.resize(w, h, mode);
this.scan(0, 0, this.bitmap.width, this.bitmap.height, function (x, y, idx) {
this.bitmap.data.writeUInt32BE(this._background, idx);
});
this.blit(c, ((this.bitmap.width - c.bitmap.width) / 2) * align_h, ((this.bitmap.height - c.bitmap.height) / 2) * align_v);
if (isNodePattern(cb)) return cb.call(this, null, this);
else return this;
};
/**
* Uniformly scales the image by a factor.
* @param f the factor to scale the image by
* @param (optional) mode a scaling method (e.g. Jimp.RESIZE_BEZIER)
* @param (optional) cb a callback for when complete
* @returns this for chaining of methods
*/
Jimp.prototype.scale = function (f, mode, cb) {
if ("number" != typeof f)
return throwError.call(this, "f must be a number", cb);
if (f < 0)
return throwError.call(this, "f must be a positive number", cb);
if ("function" == typeof mode && "undefined" == typeof cb) {
cb = mode;
mode = null;
}
var w = this.bitmap.width * f;
var h = this.bitmap.height * f;
this.resize(w, h, mode);
if (isNodePattern(cb)) return cb.call(this, null, this);
else return this;
};
/**
* Scale the image to the largest size that fits inside the rectangle that has the given width and height.
* @param w the width to resize the image to
* @param h the height to resize the image to
* @param (optional) mode a scaling method (e.g. Jimp.RESIZE_BEZIER)
* @param (optional) cb a callback for when complete
* @returns this for chaining of methods
*/
Jimp.prototype.scaleToFit = function (w, h, mode, cb) {
if ("number" != typeof w || "number" != typeof h)
return throwError.call(this, "w and h must be numbers", cb);
if ("function" == typeof mode && "undefined" == typeof cb) {
cb = mode;
mode = null;
}
var f = (w/h > this.bitmap.width/this.bitmap.height) ?
h/this.bitmap.height : w/this.bitmap.width;
this.scale(f, mode);
if (isNodePattern(cb)) return cb.call(this, null, this);
else return this;
};
/**
* Pixelates the image or a region
* @param size the size of the pixels
* @param (optional) x the x position of the region to pixelate
* @param (optional) y the y position of the region to pixelate
* @param (optional) w the width of the region to pixelate
* @param (optional) h the height of the region to pixelate
* @param (optional) cb a callback for when complete
* @returns this for chaining of methods
*/
Jimp.prototype.pixelate = function (size, x, y, w, h, cb) {
if ("function" == typeof x) {
cb = x;
x = y = w = h = undefined;
} else {
if ("number" != typeof size)
return throwError.call(this, "size must be a number", cb);
if (x !== undefined)
if ("number" != typeof x)
return throwError.call(this, "x must be a number", cb);
if (y !== undefined)
if ("number" != typeof y)
return throwError.call(this, "y must be a number", cb);
if (w !== undefined)
if ("number" != typeof w)
return throwError.call(this, "w must be a number", cb);
if (h !== undefined)
if ("number" != typeof h)
return throwError.call(this, "h must be a number", cb);
}
var kernel = [
[1 / 16, 2 / 16, 1 / 16],
[2 / 16, 4 / 16, 2 / 16],
[1 / 16, 2 / 16, 1 / 16]
];
x = x !== undefined ? x : 0;
y = y !== undefined ? y : 0;
w = w !== undefined ? w : this.bitmap.width - x;
h = h !== undefined ? h : this.bitmap.height - y;
var source = this.clone();
this.scan(x, y, w, h, function (xx, yx, idx) {
xx = size * Math.floor(xx / size);
yx = size * Math.floor(yx / size);
var value = applyKernel(source, kernel, xx, yx);
this.bitmap.data[idx] = value[0];
this.bitmap.data[idx + 1] = value[1];
this.bitmap.data[idx + 2] = value[2];
});
if (isNodePattern(cb)) return cb.call(this, null, this);
else return this;
}
/**
* Applies a convolution kernel to the image or a region
* @param kernel the convolution kernel
* @param (optional) x the x position of the region to apply convolution to
* @param (optional) y the y position of the region to apply convolution to
* @param (optional) w the width of the region to apply convolution to
* @param (optional) h the height of the region to apply convolution to
* @param (optional) cb a callback for when complete
* @returns this for chaining of methods
*/
Jimp.prototype.convolute = function (kernel, x, y, w, h, cb) {
if (!Array.isArray(kernel))
return throwError.call(this, "the kernel must be an array", cb);
if ("function" == typeof x) {
cb = x;
x = y = w = h = undefined;
} else {
if (x !== undefined)
if ("number" != typeof x)
return throwError.call(this, "x must be a number", cb);
if (y !== undefined)
if ("number" != typeof y)
return throwError.call(this, "y must be a number", cb);
if (w !== undefined)
if ("number" != typeof w)
return throwError.call(this, "w must be a number", cb);
if (h !== undefined)
if ("number" != typeof h)
return throwError.call(this, "h must be a number", cb);
}
var ksize = (kernel.length - 1) / 2;
x = x !== undefined ? x : ksize;
y = y !== undefined ? y : ksize;
w = w !== undefined ? w : this.bitmap.width - x;
h = h !== undefined ? h : this.bitmap.height - y;
var source = this.clone();
this.scan(x, y, w, h, function (xx, yx, idx) {
var value = applyKernel(source, kernel, xx, yx);
this.bitmap.data[idx] = value[0];
this.bitmap.data[idx + 1] = value[1];
this.bitmap.data[idx + 2] = value[2];
});
if (isNodePattern(cb)) return cb.call(this, null, this);
else return this;
};
function applyKernel(im, kernel, x, y) {
var value = [0, 0, 0];
var size = (kernel.length - 1) / 2;
for (var kx = 0; kx < kernel.length; kx += 1) {
for (var ky = 0; ky < kernel[kx].length; ky += 1) {
var idx = im.getPixelIndex(x + kx - size, y + ky - size);
value[0] += im.bitmap.data[idx] * kernel[kx][ky];
value[1] += im.bitmap.data[idx + 1] * kernel[kx][ky];
value[2] += im.bitmap.data[idx + 2] * kernel[kx][ky];
}
}
return value;
}
/**
* Rotates an image clockwise by a number of degrees rounded to the nearest 90 degrees. NB: 'this' must be a Jimp object.
* @param deg the number of degress to rotate the image by
* @returns nothing
*/
function simpleRotate(deg) {
var i = Math.round(deg / 90) % 4;
while (i < 0) i += 4;
while (i > 0) {
// https://github.com/ekulabuhov/jimp/commit/9a0c7cff88292d88c32a424b11256c76f1e20e46
var dstBuffer = new Buffer(this.bitmap.data.length);
var dstOffset = 0;
for (var x = 0; x < this.bitmap.width; x++) {
for (var y = this.bitmap.height - 1; y >= 0; y--) {
var srcOffset = (this.bitmap.width * y + x) << 2;
var data = this.bitmap.data.readUInt32BE(srcOffset, true);
dstBuffer.writeUInt32BE(data, dstOffset, true);
dstOffset += 4;
}
}
this.bitmap.data = new Buffer(dstBuffer);
var tmp = this.bitmap.width;
this.bitmap.width = this.bitmap.height;
this.bitmap.height = tmp;
i--;
}
}
/**
* Rotates an image clockwise by an arbitary number of degrees. NB: 'this' must be a Jimp object.
* @param deg the number of degress to rotate the image by
* @param (optional) mode resize mode or a boolean, if false then the width and height of the image will not be changed
* @returns nothing
*/
function advancedRotate(deg, mode) {
var rad = (deg % 360) * Math.PI / 180;
var cosine = Math.cos(rad);
var sine = Math.sin(rad);
var w, h; // the final width and height if resize == true
if (mode == true || "string" == typeof mode) {
// resize the image to it maximum dimention and blit the existing image onto the centre so that when it is rotated the image is kept in bounds
// http://stackoverflow.com/questions/3231176/how-to-get-size-of-a-rotated-rectangle
w = Math.round(Math.abs(this.bitmap.width * cosine) + Math.abs(this.bitmap.height * sine));
h = Math.round(Math.abs(this.bitmap.width * sine) + Math.abs(this.bitmap.height * cosine));
var c = this.clone();
this.scan(0, 0, this.bitmap.width, this.bitmap.height, function (x, y, idx) {
this.bitmap.data.writeUInt32BE(this._background, idx);
});
var max= Math.max(w,h,this.bitmap.width,this.bitmap.height)
this.resize(max, max, mode);
this.blit(c, this.bitmap.width / 2 - c.bitmap.width / 2, this.bitmap.height / 2 - c.bitmap.height / 2);
}
var dstBuffer = new Buffer(this.bitmap.data.length);
function createTranslationFunction(deltaX, deltaY) {
return function(x, y) {
return {
x : (x + deltaX),
y : (y + deltaY)
};
}
}
var translate2Cartesian = createTranslationFunction(-(this.bitmap.width / 2), -(this.bitmap.height / 2));
var translate2Screen = createTranslationFunction(this.bitmap.width / 2, this.bitmap.height / 2);
for (var y = 0; y < this.bitmap.height; y++) {
for (var x = 0; x < this.bitmap.width; x++) {
var cartesian = translate2Cartesian(x, this.bitmap.height - y);
var source = translate2Screen(
cosine * cartesian.x - sine * cartesian.y,
cosine * cartesian.y + sine * cartesian.x
);
if (source.x >= 0 && source.x < this.bitmap.width
&& source.y >= 0 && source.y < this.bitmap.height) {
var srcIdx = (this.bitmap.width * (this.bitmap.height - source.y - 1 | 0) + source.x | 0) << 2;
var pixelRGBA = this.bitmap.data.readUInt32BE(srcIdx, true);
var dstIdx = (this.bitmap.width * y + x) << 2;
dstBuffer.writeUInt32BE(pixelRGBA, dstIdx);
} else {
// reset off-image pixels
var dstIdx = (this.bitmap.width * y + x) << 2;
dstBuffer.writeUInt32BE(this._background, dstIdx);
}
}
}
this.bitmap.data = dstBuffer;
if (mode == true || "string" == typeof mode) {
// now crop the image to the final size
var x = (this.bitmap.width / 2) - (w/2);
var y = (this.bitmap.height / 2) - (h/2);
this.crop(x, y, w, h);
}
};
/**
* Rotates the image clockwise by a number of degrees. By default the width and height of the image will be resized appropriately.
* @param deg the number of degress to rotate the image by
* @param (optional) mode resize mode or a boolean, if false then the width and height of the image will not be changed
* @param (optional) cb a callback for when complete
* @returns this for chaining of methods
*/
Jimp.prototype.rotate = function (deg, mode, cb) {
// enable overloading
if ("undefined" == typeof mode || mode === null) {
// e.g. image.resize(120);
// e.g. image.resize(120, null, cb);
// e.g. image.resize(120, undefined, cb);
mode = true;
}
if ("function" == typeof mode && "undefined" == typeof cb) {
// e.g. image.resize(120, cb);
cb = mode;
mode = true;
}
if ("number" != typeof deg)
return throwError.call(this, "deg must be a number", cb);
if ("boolean" != typeof mode && "string" != typeof mode)
return throwError.call(this, "mode must be a boolean or a string", cb);
if (deg % 90 == 0 && mode !== false) simpleRotate.call(this, deg, cb);
else advancedRotate.call(this, deg, mode, cb);
if (isNodePattern(cb)) return cb.call(this, null, this);
else return this;
};
/**
* Displaces the image based on the provided displacement map
* @param map the source Jimp instance
* @param offset the maximum displacement value
* @param (optional) cb a callback for when complete
* @returns this for chaining of methods
*/
Jimp.prototype.displace = function (map, offset, cb) {
if ("object" != typeof map || map.constructor != Jimp)
return throwError.call(this, "The source must be a Jimp image", cb);
if ("number" != typeof offset)
return throwError.call(this, "factor must be a number", cb);
var source = this.clone();
this.scan(0, 0, this.bitmap.width, this.bitmap.height, function (x, y, idx) {
var displacement = map.bitmap.data[idx] / 256 * offset;
displacement = Math.round(displacement);
var ids = this.getPixelIndex(x + displacement, y);
this.bitmap.data[ids] = source.bitmap.data[idx];
this.bitmap.data[ids + 1] = source.bitmap.data[idx + 1];
this.bitmap.data[ids + 2] = source.bitmap.data[idx + 2];
});
if (isNodePattern(cb)) return cb.call(this, null, this);
else return this;
};
/**
* Converts the image to a buffer
* @param mime the mime type of the image buffer to be created
* @param cb a Node-style function to call with the buffer as the second argument
* @returns this for chaining of methods
*/
Jimp.prototype.getBuffer = function (mime, cb) {
if (mime == Jimp.AUTO) { // allow auto MIME detection
mime = this.getMIME();
}
if ("string" != typeof mime)
return throwError.call(this, "mime must be a string", cb);
if ("function" != typeof cb)
return throwError.call(this, "cb must be a function", cb);
switch (mime.toLowerCase()) {
case Jimp.MIME_PNG:
var that = this;
var png = new PNG({
width: this.bitmap.width,
height:this.bitmap.height,
bitDepth: 8,
deflateLevel: this._deflateLevel,
deflateStrategy: this._deflateStrategy,
filterType: this._filterType,
colorType: (this._rgba) ? 6 : 2,
inputHasAlpha: true
});
if (this._rgba) png.data = new Buffer(this.bitmap.data);
else png.data = compositeBitmapOverBackground(this).data; // when PNG doesn't support alpha
StreamToBuffer(png.pack(), function (err, buffer) {
return cb.call(that, null, buffer);
});
break;
case Jimp.MIME_JPEG:
// composite onto a new image so that the background shows through alpha channels
var jpeg = JPEG.encode(compositeBitmapOverBackground(this), this._quality);
return cb.call(this, null, jpeg.data);
case Jimp.MIME_BMP:
case Jimp.MIME_X_MS_BMP:
// composite onto a new image so that the background shows through alpha channels
var bmp = BMP.encode(compositeBitmapOverBackground(this));
return cb.call(this, null, bmp.data);
default:
return cb.call(this, "Unsupported MIME type: " + mime);
}
return this;
};
function compositeBitmapOverBackground(image){
return (new Jimp(image.bitmap.width, image.bitmap.height, image._background)).composite(image, 0, 0).bitmap;
}
/**
* Converts the image to a base 64 string
* @param mime the mime type of the image data to be created
* @param cb a Node-style function to call with the buffer as the second argument
* @returns this for chaining of methods
*/
Jimp.prototype.getBase64 = function (mime, cb) {
if (mime == Jimp.AUTO) { // allow auto MIME detection
mime = this.getMIME();
}
if ("string" != typeof mime)
return throwError.call(this, "mime must be a string", cb);
if ("function" != typeof cb)
return throwError.call(this, "cb must be a function", cb);
this.getBuffer(mime, function(err, data) {
var src = "data:" + mime + ";base64," + data.toString("base64");
return cb.call(this, null, src);
});
return this;
};
/**
* Apply a ordered dithering effect
* @param (optional) cb a callback for when complete
* @returns this for chaining of methods
*/
Jimp.prototype.dither565 = function (cb) {
var rgb565_matrix = [
1, 9, 3, 11,
13, 5, 15, 7,
4, 12, 2, 10,
16, 8, 14, 6
];
this.scan(0, 0, this.bitmap.width, this.bitmap.height, function (x, y, idx) {
var tresshold_id = ((y & 3) << 2) + (x % 4);
var dither = rgb565_matrix[tresshold_id];
this.bitmap.data[idx ] = Math.min(this.bitmap.data[idx] + dither, 0xff);
this.bitmap.data[idx+1] = Math.min(this.bitmap.data[idx+1] + dither, 0xff);
this.bitmap.data[idx+2] = Math.min(this.bitmap.data[idx+2] + dither, 0xff);
});
if (isNodePattern(cb)) return cb.call(this, null, this);
else return this;
}
// alternative reference
Jimp.prototype.dither16 = Jimp.prototype.dither565;
/**
* Apply multiple color modification rules
* @param actions list of color modification rules, in following format: { apply: '<rule-name>', params: [ <rule-parameters> ] }
* @param (optional) cb a callback for when complete
* @returns this for chaining of methods
*/
Jimp.prototype.color = Jimp.prototype.colour = function (actions, cb) {
if (!actions || !Array.isArray(actions))
return throwError.call(this, "actions must be an array", cb);
var originalScope = this;
this.scan(0, 0, this.bitmap.width, this.bitmap.height, function (x, y, idx) {
var clr = TinyColor({r: this.bitmap.data[idx], g: this.bitmap.data[idx + 1], b: this.bitmap.data[idx + 2]});
var colorModifier = function (i, amount) {
c = clr.toRgb();
c[i] = Math.max(0, Math.min(c[i] + amount, 255));
return TinyColor(c);
}
actions.forEach(function (action) {
if (action.apply === "mix") {
clr = TinyColor.mix(clr, action.params[0], action.params[1]);
} else if (action.apply === "tint") {
clr = TinyColor.mix(clr, "white", action.params[0]);
} else if (action.apply === "shade") {
clr = TinyColor.mix(clr, "black", action.params[0]);
} else if (action.apply === "xor") {
var clr2 = TinyColor(action.params[0]).toRgb();
clr = clr.toRgb();
clr = TinyColor({ r: clr.r ^ clr2.r, g: clr.g ^ clr2.g, b: clr.b ^ clr2.b});
} else if (action.apply === "red") {
clr = colorModifier("r", action.params[0]);
} else if (action.apply === "green") {
clr = colorModifier("g", action.params[0]);
} else if (action.apply === "blue") {
clr = colorModifier("b", action.params[0]);
} else {
if (action.apply === "hue") {
action.apply = "spin";
}
var fn = clr[action.apply];
if (!fn) {
return throwError.call(originalScope, "action " + action.apply + " not supported", cb);
}
clr = fn.apply(clr, action.params);
}
});
clr = clr.toRgb();
this.bitmap.data[idx ] = clr.r;
this.bitmap.data[idx+1] = clr.g;
this.bitmap.data[idx+2] = clr.b;
});
if (isNodePattern(cb)) return cb.call(this, null, this);
else return this;
}
/**
* Loads a bitmap font from a file
* @param file the file path of a .fnt file
* @param (optional) cb a function to call when the font is loaded
* @returns a promise
*/
Jimp.loadFont = function (file, cb) {
if ("string" != typeof file)
return throwError.call(this, "file must be a string", cb);
var that = this;
return new Promise(function (resolve, reject) {
cb = cb || function(err, font) {
if (err) reject(err);
else resolve(font);
}
BMFont(file, function(err, font) {
var chars = {}, kernings = {};
if (err) return throwError.call(that, err, cb);
for (var i = 0; i < font.chars.length; i++) {
chars[String.fromCharCode(font.chars[i].id)] = font.chars[i];
}
for (var i = 0; i < font.kernings.length; i++) {
var firstString = String.fromCharCode(font.kernings[i].first);
kernings[firstString] = kernings[firstString] || {};
kernings[firstString][String.fromCharCode(font.kernings[i].second)] = font.kernings[i].amount;
}
loadPages(Path.dirname(file), font.pages).then(function (pages) {
cb(null, {
chars: chars,
kernings: kernings,
pages: pages,
common: font.common,
info: font.info
});
});
});
});
};
function loadPages(dir, pages) {
var newPages = pages.map(function (page) {
return Jimp.read(dir + '/' + page);
});
return Promise.all(newPages);
}
/**
* Draws a text on a image on a given boundary
* @param font a bitmap font loaded from `Jimp.loadFont` command
* @param x the x position to start drawing the text
* @param y the y position to start drawing the text
* @param text the text to draw
* @param maxWidth (optional) the boundary width to draw in
* @param (optional) cb a function to call when the text is written
* @returns this for chaining of methods
*/
Jimp.prototype.print = function (font, x, y, text, maxWidth, cb) {
if ("function" == typeof maxWidth && "undefined" == typeof cb) {
cb = maxWidth;
maxWidth = Infinity;
}
if ("undefined" == typeof maxWidth) {
maxWidth = Infinity;
}
if ("object" != typeof font)
return throwError.call(this, "font must be a Jimp loadFont", cb);
if ("number" != typeof x || "number" != typeof y || "number" != typeof maxWidth)
return throwError.call(this, "x, y and maxWidth must be numbers", cb);
if ("string" != typeof text)
return throwError.call(this, "text must be a string", cb);
if ("number" != typeof maxWidth)
return throwError.call(this, "maxWidth must be a number", cb);
var that = this;
var words = text.split(' ');
var line = '';
for (var n = 0; n < words.length; n++) {
var testLine = line + words[n] + ' ';
var testWidth = measureText(font, testLine);
if (testWidth > maxWidth && n > 0) {
that = that.print(font, x, y, line);
line = words[n] + ' ';
y += font.common.lineHeight;
} else {
line = testLine;
}
}
printText.call(this, font, x, y, line);
if (isNodePattern(cb)) return cb.call(this, null, that);
else return that;
};
function printText(font, x, y, text) {
for (var i = 0; i < text.length; i++) {
if (font.chars[text[i]]) {
drawCharacter(this, font, x, y, font.chars[text[i]]);
x += (font.kernings[text[i]] && font.kernings[text[i]][text[i+1]] ? font.kernings[text[i]][text[i+1]] : 0) + (font.chars[text[i]].xadvance || 0);
}
}
};
function drawCharacter(image, font, x, y, char) {
if (char.width > 0 && char.height > 0) {
var imageChar = font.pages[char.page].clone().crop(char.x, char.y, char.width, char.height);
return image.composite(imageChar, x + char.xoffset, y + char.yoffset);
}
return image;
};
function measureText(font, text) {
var x = 0;
for (var i = 0; i < text.length; i++) {
if (font.chars[text[i]]) {
x += font.chars[text[i]].xoffset
+ (font.kernings[text[i]] && font.kernings[text[i]][text[i+1]] ? font.kernings[text[i]][text[i+1]] : 0)
+ (font.chars[text[i]].xadvance || 0);
}
}
return x;
};
/**
* Writes the image to a file
* @param path a path to the destination file (either PNG or JPEG)
* @param (optional) cb a function to call when the image is saved to disk
* @returns this for chaining of methods
*/
Jimp.prototype.write = function (path, cb) {
if ("string" != typeof path)
return throwError.call(this, "path must be a string", cb);
if ("undefined" == typeof cb) cb = function () {};
if ("function" != typeof cb)
return throwError.call(this, "cb must be a function", cb);
var that = this;
var mime = MIME.lookup(path);
var pathObj = Path.parse(path);
if (pathObj.dir) MkDirP.sync(pathObj.dir);
this.getBuffer(mime, function(err, buffer) {
if (err) return throwError.call(that, err, cb);
var stream = FS.createWriteStream(path);
stream.on("open", function(fh) {
stream.write(buffer);
stream.end();
}).on("error", function(err) {
return throwError.call(that, err, cb);
});
stream.on("finish", function(fh) {
return cb.call(that, null, that);
});
});
return this;
};
/* Nicely format Jimp object when sent to the console e.g. console.log(imgage) */
Jimp.prototype.inspect = function () {
return '<Jimp ' +
(this.bitmap === Jimp.prototype.bitmap ? 'pending...' :
this.bitmap.width + 'x' + this.bitmap.height) +
'>';
};
// Nicely format Jimp object when converted to a string
Jimp.prototype.toString = function () {
return '[object Jimp]';
};
if (process.env.ENVIRONMENT === "BROWSER") {
// For use in a web browser or web worker
var gl;
if (typeof window == "object") gl = window;
if (typeof self == "object") gl = self;
gl.Jimp = Jimp;
gl.Buffer = Buffer;
} else {
module.exports = Jimp;
}