Ox.load.Image = function(options, callback) { //@ Image ------------------------------------------------------------------ /*@ Ox.Image Generic image object (src, callback) -> undefined (width, height, callback) -> undefined (width, height, background, callback) -> undefined src Image source (local, remote or data URL) width Width in px image Height in px background <[n]> Background color (RGB or RGBA) @*/ Ox.Image = function() { var self = {}, that = {}; self.callback = arguments[arguments.length - 1]; if (arguments.length == 2) { self.src = arguments[0]; self.image = new Image(); self.image.onload = init; self.image.src = self.src; } else { self.width = arguments[0]; self.height = arguments[1]; self.background = arguments.length == 4 ? arguments[2] : [0, 0, 0, 0]; init(); } function error(mode) { throw new RangeError('PNG codec can\'t ' + mode + ' ' + ( mode == 'encode' ? 'data' : 'image' )); } function getCapacity(bpb) { var capacity = 0; that.forEach(function(rgba) { capacity += rgba[3] == 255 ? bpb * 3/8 : 0; }); return capacity; } function getIndex() { if (arguments.length == 1) { xy = arguments[0]; } else { xy = [arguments[0], arguments[1]]; } return ( Ox.mod(xy[0], self.width) + Ox.mod(xy[1] * self.width, self.width * self.height) ) * 4; } function getXY(index) { index /= 4; return [index % self.width, Math.floor(index / self.width)]; } function init() { if (self.image) { self.width = self.image.width; self.height = self.image.height; } self.canvas = Ox.element('').attr({ width: self.width, height: self.height }); self.context = self.canvas[0].getContext('2d'); if (self.image) { self.context.drawImage(self.image, 0, 0); } else if (Ox.sum(self.background)) { that.fillStyle(self.background); that.fillRect(0, 0, self.width, self.height); } self.imageData = self.context.getImageData(0, 0, self.width, self.height); self.data = self.imageData.data; self.callback(that); } function setSL(sl, d) { var c = sl == 's' ? 1 : 2; return that.map(function(rgba) { var hsl = Ox.hsl([rgba[0], rgba[1], rgba[2]]), rgb; hsl[c] = d < 0 ? hsl[c] * (d + 1) : hsl[c] + (1 - hsl[c]) * d; rgb = Ox.rgb(hsl); return Ox.merge(rgb, rgba[3]); }); } that.blur = function(val) { var filter = [], size = val * 2 + 1; sum = 0 Ox.loop(size, function(x) { Ox.loop(size, function(y) { var isInCircle = +(Math.sqrt( Math.pow(x - val, 2) + Math.pow(y - val, 2) ) <= val); sum += isInCircle; filter.push(isInCircle) }); }); filter = filter.map(function(val) { return val / sum; }) return that.filter(filter); }; that.channel = function(str) { str = str[0].toLowerCase(); return that.map(function(rgba) { var i = ['r', 'g', 'b', 'a'].indexOf(str), rgb, val; if (i > -1) { return Ox.map(rgba, function(v, c) { return str == 'a' ? (c < 3 ? rgba[3] : 255) : (c == i || c == 3 ? v : 0); }); } else { i = ['h', 's', 'l'].indexOf(str); val = Ox.hsl([rgba[0], rgba[1], rgba[2]])[i]; rgb = i == 0 ? Ox.rgb([val, 1, 0.5]) : Ox.map(Ox.range(3), function(v) { return parseInt(val * 255); }); return Ox.merge(rgb, rgba[3]); } }); } that.contour = function(val) { return that.filter([ +1, +1, +1, +1, -7, +1, +1, +1, +1 ]); }; that.depth = function(val) { var pow = Math.pow(2, 8 - val); return that.map(function(rgba) { return rgba.map(function(v, i) { return i < 3 ? Math.floor(v / pow) * pow : v }); }); } that.edges = function(val) { return that.filter([ -1, -1, -1, -1, +8, -1, -1, -1, -1 ]).saturation(-1); }; that.emboss = function(val) { return that.filter([ -1, -1, +0, -1, +0, +1, +0, +1, +1 ], 128).saturation(-1); }; /*@ encode Encodes a string into the image For most purposes, deflate and mode should be omitted, since the defaults make the existence of the message harder to detect. A valid use case for deflate and mode would be to encode, additionally to the secret string, a more easily detected decoy string: image.encode(decoy, false, 1, function(image) { image.encode(secret, -1, callback); }). (str, callback) -> The image object (unmodified) (str, deflate, callback) -> The image object (unmodified) (str, mode, callback) -> The image object (unmodified) (str, deflate, mode, callback) -> The image object (unmodified) (str, mode, deflate, callback) -> The image object (unmodified) str The string to be encoded callback Callback function image The image object (modified) deflate If true, compress string with deflate mode Encoding mode If mode is between -7 and 0, the string will be encoded one bit per byte, as the number of bits within that byte set to 1, modulo 2, by flipping, if necessary, the most (mode -7) to least (mode 0) significant bit. If mode is between 1 and 255, the string will be encoded bitwise into all bits per byte that, in mode, are set to 1. @*/ that.encode = function(str) { var callback = arguments[arguments.length - 1], deflate = Ox.isBoolean(arguments[1]) ? arguments[1] : Ox.isBoolean(arguments[2]) ? arguments[2] : true, mode = Ox.isNumber(arguments[1]) ? arguments[1] : Ox.isNumber(arguments[2]) ? arguments[2] : 0, b = 0, bin, bits = mode < 1 ? [-mode] : Ox.map(Ox.range(8), function(bit) { return mode & 1 << bit ? bit : null; }), cap = getCapacity(bits.length), len; Ox.print("CAPACITY", cap) // Compress the string str = Ox[deflate ? 'encodeDeflate' : 'encodeUTF8'](str); len = str.length; // Prefix the string with its length, as a four-byte value str = Ox.pad(Ox.encodeBase256(len), 4, '\u0000') + str; str.length > cap && error('encode'); while (str.length < cap) { str += str.substr(4, len); } str = str.substr(0, Math.ceil(cap)); // Create an array of bit values bin = Ox.flatten(Ox.map(str, function(chr) { return Ox.map(Ox.range(8), function(i) { return chr.charCodeAt(0) >> 7 - i & 1; }); })); b = 0; that.forEach(function(rgba, xy) { // If alpha is not 255, the RGB values may not be preserved if (rgba[3] == 255) { var index = getIndex(xy); Ox.loop(3, function(c) { var i = index + c; Ox.forEach(bits, function(bit) { if (( mode < 1 // If the number of bits set to 1, mod 2 ? Ox.sum(Ox.range(8).map(function(bit) { return +!!(self.data[i] & 1 << bit); })) % 2 // or the one bit in question : +!!(self.data[i] & 1 << bit) // is not equal to the data bit ) != bin[b++]) { // then flip the bit self.data[i] ^= 1 << bit; } }); }); } }); self.context.putImageData(self.imageData, 0, 0); callback(that); return that; }; that.decode = function() { var callback = arguments[arguments.length - 1], deflate = Ox.isBoolean(arguments[0]) ? arguments[0] : Ox.isBoolean(arguments[1]) ? arguments[1] : true, mode = Ox.isNumber(arguments[0]) ? arguments[0] : Ox.isNumber(arguments[1]) ? arguments[1] : 0, bin = '', bits = mode < 1 ? [-mode] : Ox.map(Ox.range(8), function(b) { return mode & 1 << b ? b : null; }), done = 0; len = 4, str = ''; that.forEach(function(rgba, xy) { if (rgba[3] == 255) { var index = getIndex(xy); Ox.loop(3, function(c) { var i = index + c; Ox.forEach(bits, function(bit) { bin += mode < 1 ? Ox.sum(Ox.range(8).map(function(bit) { return +!!(self.data[i] & 1 << bit); })) % 2 : +!!(self.data[i] & 1 << bit); if (bin.length == 8) { str += Ox.char(parseInt(bin, 2)); bin = ''; if (str.length == len) { if (++done == 1) { len = Ox.decodeBase256(str); Ox.print("LEN", len) len + 4 > getCapacity(bits.length) && error('decode'); str = ''; } else { return false; } } } }); }); if (done == 2) { return false; } } }); try { if (deflate) { Ox.print('DEFLATE') Ox.decodeDeflate(str, callback); } else { callback(Ox.decodeUTF8(str)); } } catch (e) { error('decode'); } } that.fillRect = function(x, y, w, h) { self.context.fillRect(x, y, w, h); return that; }; that.fillStyle = function() { if (arguments.length == 0) { return self.context.fillStyle; } else { self.context.fillStyle = arguments[0]; return that; } }; that.filter = function(filter, bias) { bias = bias || 0; var filterSize = Math.sqrt(filter.length), d = (filterSize - 1) / 2, imageData = self.context.createImageData(self.width, self.height), data = []; self.imageData = self.context.getImageData(0, 0, self.width, self.height); self.data = self.imageData.data; Ox.loop(0, self.data.length, 4, function(i) { var filterIndex = 0, xy = getXY(i); Ox.loop(3, function(c) { data[i + c] = 0; }); Ox.loop(-d, d + 1, function(x) { Ox.loop(-d, d + 1, function(y) { var pixelIndex = getIndex(xy[0] + x, xy[1] + y); Ox.loop(3, function(c) { data[i + c] += self.data[pixelIndex + c] * filter[filterIndex]; }); filterIndex++; }); }); }); Ox.loop(0, self.data.length, 4, function(i) { Ox.loop(4, function(c) { imageData.data[i + c] = c < 3 ? Ox.limit(Math.round(data[i + c] + bias), 0, 255) : self.data[i + c]; }); }); self.context.putImageData(imageData, 0, 0); self.imageData = imageData; self.data = data; return that; }; /*@ forEach Pixel-wise forEach function (fn) -> The image object fn Iterator function rgba <[n]> RGBA values xy <[n]> XY coordinates @*/ that.forEach = function(fn) { Ox.loop(0, self.data.length, 4, function(i) { return fn([ self.data[i], self.data[i + 1], self.data[i + 2], self.data[i + 3] ], getXY(i)); }) return that; }; that.hue = function(val) { return that.map(function(rgba) { var hsl = Ox.hsl([rgba[0], rgba[1], rgba[2]]), rgb; hsl[0] = (hsl[0] + val) % 360; rgb = Ox.rgb(hsl); return Ox.merge(rgb, rgba[3]); }); }; that.invert = function() { return that.map(function(rgba) { return [255 - rgba[0], 255 - rgba[1], 255 - rgba[2], rgba[3]]; }); }; that.lightness = function(val) { return setSL('l', val); }; /*@ map Pixel-wise map function (fn) -> The image object fn Iterator function rgba <[n]> RGBA values xy <[n]> XY coordinates @*/ that.map = function(fn) { self.imageData = self.context.getImageData(0, 0, self.width, self.height); self.data = self.imageData.data; Ox.loop(0, self.data.length, 4, function(i) { fn([ self.data[i], self.data[i + 1], self.data[i + 2], self.data[i + 3] ], getXY(i)).forEach(function(val, c) { self.data[i + c] = val; }); }) self.context.putImageData(self.imageData, 0, 0); return that; }; that.motionBlur = function() { return that.filter([ 0.2, 0.0, 0.0, 0.0, 0.0, 0.0, 0.2, 0.0, 0.0, 0.0, 0.0, 0.0, 0.2, 0.0, 0.0, 0.0, 0.0, 0.0, 0.2, 0.0, 0.0, 0.0, 0.0, 0.0, 0.2 ]); }; that.pixel = function(x, y, val) { var i = getIndex(x, y); if (!val) { return Ox.range(4).map(function(c) { return self.data[i + c]; }); } else { val.forEach(function(v, c) { self.data[i + c] = v; }); } } that.posterize = function() { return that.blur(3).map(function(rgba) { return [ Math.floor(rgba[0] / 64) * 64, Math.floor(rgba[1] / 64) * 64, Math.floor(rgba[2] / 64) * 64, rgba[3] ]; }); }; that.resize = function(width, height) { self.canvas.attr({ width: width, height: height }); return that; } that.saturation = function(val) { return setSL('s', val); }; that.sharpen = function(val) { return that.filter([ -1, -1, -1, -1, +9, -1, -1, -1, -1 ]); }; that.solarize = function() { return that.map(function(rgba) { return [ rgba[0] < 128 ? rgba[0] : 255 - rgba[0], rgba[1] < 128 ? rgba[1] : 255 - rgba[1], rgba[2] < 128 ? rgba[2] : 255 - rgba[2], rgba[3] ]; }); } that.src = function() { if (arguments.length == 0) { return self.canvas[0].toDataURL(); } else { var callback = arguments[1]; self.src = arguments[0]; self.image = new Image(); self.image.onload = function() { self.width = self.image.width; self.height = self.image.height; self.canvas.attr({ width: self.width, height: self.height }); self.context.drawImage(self.image, 0, 0); self.imageData = self.context.getImageData(0, 0, self.width, self.height); self.data = self.imageData.data; callback && callback(that); } self.image.src = self.src; return that; } }; that.toDataURL = that.src; }; callback(true); }