646 lines
24 KiB
JavaScript
646 lines
24 KiB
JavaScript
Ox.load.Image = function(options, callback) {
|
|
|
|
//@ Image ------------------------------------------------------------------
|
|
|
|
/*@
|
|
Ox.Image <f> Generic image object
|
|
To render the image as an image element, use its <code>src()</code>
|
|
method, to render it as a canvas, use its <code>canvas</code> property.
|
|
(src, callback) -> <u> undefined
|
|
(width, height, callback) -> <u> undefined
|
|
(width, height, background, callback) -> <u> undefined
|
|
src <s> Image source (local, remote or data URL)
|
|
width <n> Width in px
|
|
image <n> 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;
|
|
}
|
|
that.canvas = Ox.element('<canvas>').attr({
|
|
width: self.width,
|
|
height: self.height
|
|
});
|
|
that.context = that.canvas[0].getContext('2d');
|
|
if (self.image) {
|
|
that.context.drawImage(self.image, 0, 0);
|
|
} else if (Ox.sum(self.background)) {
|
|
that.context.fillStyle(self.background);
|
|
that.context.fillRect(0, 0, self.width, self.height);
|
|
}
|
|
that.imageData = that.context.getImageData(
|
|
0, 0, self.width, self.height
|
|
);
|
|
self.data = that.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]);
|
|
});
|
|
}
|
|
|
|
/*@
|
|
blur <f> Apply blur filter
|
|
(val) -> <o> The image object
|
|
val <n> Amount of blur (1 to 5, more is slow)
|
|
@*/
|
|
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);
|
|
};
|
|
|
|
/*@
|
|
channel <f> Reduce the image to one channel
|
|
(channel) -> <o> The image object
|
|
channel <str> 'r', 'g', 'b', 'a', 'h', 's' or 'l'
|
|
@*/
|
|
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]);
|
|
}
|
|
});
|
|
}
|
|
|
|
//@ context <o> 2D drawing context
|
|
|
|
/*@
|
|
contour <f> Apply contour filter
|
|
() -> <o> The image object
|
|
@*/
|
|
that.contour = function(val) {
|
|
return that.filter([
|
|
+1, +1, +1,
|
|
+1, -7, +1,
|
|
+1, +1, +1
|
|
]);
|
|
};
|
|
|
|
/*@
|
|
depth <f> Reduce the bit depth
|
|
(depth) -> <o> The image object
|
|
depth <n> Bits per channel (1 to 7)
|
|
@*/
|
|
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/* * 255 / val*/ : v;
|
|
});
|
|
});
|
|
};
|
|
|
|
/*@
|
|
edges <f> Apply edges filter
|
|
() -> <o> The image object
|
|
@*/
|
|
that.edges = function(val) {
|
|
return that.filter([
|
|
-1, -1, -1,
|
|
-1, +8, -1,
|
|
-1, -1, -1
|
|
]).saturation(-1);
|
|
};
|
|
|
|
/*@
|
|
emboss <f> Apply emboss filter
|
|
() -> <o> The image object
|
|
@*/
|
|
that.emboss = function(val) {
|
|
return that.filter([
|
|
-1, -1, +0,
|
|
-1, +0, +1,
|
|
+0, +1, +1
|
|
], 128).saturation(-1);
|
|
};
|
|
|
|
/*@
|
|
encode <f> 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 first encode a more easily
|
|
detected decoy string, and only then the secret string: <code>
|
|
image.encode(decoy, false, 1, function(image) { image.encode(secret,
|
|
-1, callback); })</code>.
|
|
(str, callback) -> <o> The image object (unmodified)
|
|
(str, deflate, callback) -> <o> The image object (unmodified)
|
|
(str, mode, callback) -> <o> The image object (unmodified)
|
|
(str, deflate, mode, callback) -> <o> The image object (unmodified)
|
|
(str, mode, deflate, callback) -> <o> The image object (unmodified)
|
|
str <s> The string to be encoded
|
|
callback <f> Callback function
|
|
image <o> The image object (modified)
|
|
deflate <b|true> If true, encode the string with deflate
|
|
mode <n|0> 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,
|
|
// Array of bits per byte to be modified (0 is LSB)
|
|
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) {
|
|
// fixme: use: var data = that.context.imageData.data[i + 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;
|
|
}
|
|
});
|
|
});
|
|
}
|
|
});
|
|
that.context.putImageData(that.imageData, 0, 0);
|
|
callback(that);
|
|
return that;
|
|
};
|
|
|
|
/*@
|
|
decode <f> Decode encoded string
|
|
(callback) -> <o> The image object (unmodified)
|
|
(deflate, callback) -> <o> The image object (unmodified)
|
|
(mode, callback) -> <o> The image object (unmodified)
|
|
(deflate, mode, callback) -> <o> The image object (unmodified)
|
|
(mode, deflate, callback) -> <o> The image object (unmodified)
|
|
deflate <b|true> If true, decode the string with deflate
|
|
mode <n|0> See encode method
|
|
callback <f> Callback function
|
|
image <o> The image object (modified)
|
|
@*/
|
|
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 = '',
|
|
// Array of bits per byte to be modified (0 is LSB)
|
|
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
|
|
// Read 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);
|
|
if (bin.length == 8) {
|
|
// Every 8 bits, add one byte to the string
|
|
str += Ox.char(parseInt(bin, 2));
|
|
bin = '';
|
|
if (str.length == len) {
|
|
if (++done == 1) {
|
|
// After 4 bytes, parse string as length
|
|
len = Ox.decodeBase256(str);
|
|
if (
|
|
len <= 0 ||
|
|
len > getCapacity(bits.length) - 4
|
|
) {
|
|
error('decode');
|
|
}
|
|
str = '';
|
|
} else {
|
|
// After length more bytes, break
|
|
return false;
|
|
}
|
|
}
|
|
}
|
|
});
|
|
return done != 2;
|
|
});
|
|
return done != 2;
|
|
}
|
|
});
|
|
try {
|
|
if (deflate) {
|
|
Ox.decodeDeflate(str, callback);
|
|
} else {
|
|
callback(Ox.decodeUTF8(str));
|
|
}
|
|
} catch (e) {
|
|
error('decode');
|
|
}
|
|
}
|
|
|
|
/*@
|
|
filter <f> Pixel-wise filter function
|
|
Undocumented, see source code
|
|
(filter) -> <o> The image object
|
|
(filter, bias) -> <o> The image object
|
|
filter <[n]> Filter matrix
|
|
bias <n> Bias
|
|
@*/
|
|
that.filter = function(filter, bias) {
|
|
bias = bias || 0;
|
|
var filterSize = Math.sqrt(filter.length),
|
|
d = (filterSize - 1) / 2,
|
|
imageData = that.context.createImageData(self.width, self.height),
|
|
data = [];
|
|
that.imageData = that.context.getImageData(0, 0, self.width, self.height);
|
|
self.data = that.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];
|
|
});
|
|
});
|
|
that.context.putImageData(imageData, 0, 0);
|
|
that.imageData = imageData;
|
|
self.data = data;
|
|
return that;
|
|
};
|
|
|
|
/*@
|
|
forEach <f> Pixel-wise forEach function
|
|
(fn) -> <o> The image object
|
|
fn <f> 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]);
|
|
});
|
|
};
|
|
|
|
//@ imageData <o> Image data object
|
|
|
|
/*@
|
|
invert <f> Apply invert filter
|
|
() -> <o> The image object
|
|
@*/
|
|
that.invert = function() {
|
|
return that.map(function(rgba) {
|
|
return [255 - rgba[0], 255 - rgba[1], 255 - rgba[2], rgba[3]];
|
|
});
|
|
};
|
|
|
|
/*@
|
|
lightness <f> Apply lightness filter
|
|
(val) -> <o> The image object
|
|
val <n> Amount, from -1 (darkest) to 1 (lightest)
|
|
@*/
|
|
that.lightness = function(val) {
|
|
return setSL('l', val);
|
|
};
|
|
|
|
/*@
|
|
map <f> Pixel-wise map function
|
|
(fn) -> <o> The image object
|
|
fn <f> Iterator function
|
|
rgba <[n]> RGBA values
|
|
xy <[n]> XY coordinates
|
|
@*/
|
|
that.map = function(fn) {
|
|
that.imageData = that.context.getImageData(0, 0, self.width, self.height);
|
|
self.data = that.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;
|
|
});
|
|
})
|
|
that.context.putImageData(that.imageData, 0, 0);
|
|
return that;
|
|
};
|
|
|
|
/*@
|
|
mosaic <f> Apply mosaic filter
|
|
(size) -> <o> The image object
|
|
size <n> Mosaic size
|
|
@*/
|
|
that.mosaic = function(size) {
|
|
that.forEach(function(rgba, xy) {
|
|
if (xy[0] % size == 0 && xy[1] % size == 0) {
|
|
Ox.loop(size, function(x) {
|
|
Ox.loop(size, function(y) {
|
|
var hsl, rgb;
|
|
if ((x == 0 || y == 0) && !(x == size - 1 || y == size - 1)) {
|
|
that.pixel(xy[0] + x, xy[1] + y, rgba.map(function(c, i) {
|
|
return i < 3 ? Math.min(c + 16, 255) : c;
|
|
}));
|
|
} else if ((x == size - 1 || y == size - 1) && !(x == 0 || y == 0)) {
|
|
that.pixel(xy[0] + x, xy[1] + y, rgba.map(function(c, i) {
|
|
return i < 3 ? Math.max(c - 16, 0) : c;
|
|
}));
|
|
} else {
|
|
that.pixel(xy[0] + x, xy[1] + y, rgba);
|
|
}
|
|
});
|
|
});
|
|
}
|
|
});
|
|
that.context.putImageData(that.imageData, 0, 0);
|
|
return that;
|
|
}
|
|
|
|
/*@
|
|
motionBlur <f> Apply motion blur filter
|
|
() -> <o> The image object
|
|
@*/
|
|
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
|
|
]);
|
|
};
|
|
|
|
/*@
|
|
photocopy <f> Apply photocopy filter
|
|
() -> <o> The image object
|
|
@*/
|
|
that.photocopy = function(val) {
|
|
return that.saturation(-1).depth(1).blur(1);
|
|
};
|
|
|
|
/*@
|
|
pixel <f> Get or set pixel values
|
|
(x, y) -> <[n]> RGBA values
|
|
(x, y, val) -> <o> The image object
|
|
x <n> X coordinate
|
|
y <n> Y coordinate
|
|
val <[n]> RGBA values
|
|
@*/
|
|
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;
|
|
});
|
|
}
|
|
}
|
|
|
|
/*@
|
|
posterize <f> Apply posterize filter
|
|
() -> <o> The image object
|
|
@*/
|
|
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) {
|
|
// fixme: may not work this way
|
|
that.canvas.attr({
|
|
width: width,
|
|
height: height
|
|
});
|
|
return that;
|
|
}
|
|
|
|
/*@
|
|
saturation <f> Apply saturation filter
|
|
(val) -> <o> The image object
|
|
val <n> Amount, from -1 (lowest) to 1 (highest)
|
|
@*/
|
|
that.saturation = function(val) {
|
|
return setSL('s', val);
|
|
};
|
|
|
|
/*@
|
|
sharpen <f> Apply sharpen filter
|
|
() -> <o> The image object
|
|
@*/
|
|
that.sharpen = function(val) {
|
|
return that.filter([
|
|
-1, -1, -1,
|
|
-1, +9, -1,
|
|
-1, -1, -1
|
|
]);
|
|
};
|
|
|
|
/*@
|
|
solarize <f> Apply solarize filter
|
|
() -> <o> The image object
|
|
@*/
|
|
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]
|
|
];
|
|
});
|
|
}
|
|
|
|
/*@
|
|
src <f> Get or set the image source
|
|
() -> <s> Data URL
|
|
(src) -> <o> Image object, with new source
|
|
src <s> Image source (local, remote or data URL)
|
|
@*/
|
|
that.src = function() {
|
|
if (arguments.length == 0) {
|
|
return that.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;
|
|
that.canvas.attr({
|
|
width: self.width,
|
|
height: self.height
|
|
});
|
|
that.context.drawImage(self.image, 0, 0);
|
|
that.imageData = that.context.getImageData(0, 0, self.width, self.height);
|
|
self.data = that.imageData.data;
|
|
callback && callback(that);
|
|
}
|
|
self.image.src = self.src;
|
|
return that;
|
|
}
|
|
};
|
|
|
|
};
|
|
|
|
callback(true);
|
|
|
|
}
|
|
|