468 lines
No EOL
17 KiB
JavaScript
468 lines
No EOL
17 KiB
JavaScript
(function() {
|
|
|
|
function cap(width, height) {
|
|
// returns maximum encoding capacity of an image
|
|
return parseInt(width * height * 3/8) - 4;
|
|
}
|
|
|
|
function seek(data, px) {
|
|
// returns this, or the next, opaque pixel
|
|
while (data[px * 4 + 3] < 255) {
|
|
if (++px * 4 == data.length) {
|
|
throwPNGError('de');
|
|
}
|
|
}
|
|
return px;
|
|
}
|
|
|
|
function xor(byte) {
|
|
// returns "1"-bits-in-byte % 2
|
|
// use: num.toString(2).replace(/0/g, '').length % 2
|
|
var xor = 0;
|
|
Ox.range(8).forEach(function(i) {
|
|
xor ^= byte >> i & 1;
|
|
});
|
|
return xor;
|
|
}
|
|
|
|
function throwPNGError(str) {
|
|
throw new RangeError(
|
|
'PNG codec can\'t ' +
|
|
(str == 'en' ? 'encode data' : 'decode image')
|
|
);
|
|
}
|
|
|
|
function throwUTF8Error(byte, pos) {
|
|
throw new RangeError(
|
|
'UTF-8 codec can\'t decode byte 0x' +
|
|
byte.toString(16).toUpperCase() + ' at position ' + pos
|
|
);
|
|
}
|
|
|
|
/*@
|
|
Ox.encodeBase26 <b> Encode a number as base26
|
|
> Ox.encodeBase26(3758)
|
|
'FOO'
|
|
@*/
|
|
Ox.encodeBase26 = function(num) {
|
|
return Ox.map(num.toString(26), function(char) {
|
|
return Ox.char(65 + parseInt(char, 26));
|
|
}).join('');
|
|
};
|
|
|
|
/*@
|
|
Ox.decodeBase26 <f> Decodes a base26-encoded number
|
|
See <a href="http://www.crockford.com/wrmg/base32.html">Base 32</a>.
|
|
> Ox.decodeBase26('foo')
|
|
3758
|
|
@*/
|
|
Ox.decodeBase26 = function(str) {
|
|
return parseInt(Ox.map(str.toUpperCase(), function(char) {
|
|
return (char.charCodeAt(0) - 65).toString(26);
|
|
}).join(''), 26);
|
|
};
|
|
|
|
/*@
|
|
Ox.encodeBase32 <b> Encode a number as base32
|
|
See <a href="http://www.crockford.com/wrmg/base32.html">Base 32</a>.
|
|
> Ox.encodeBase32(15360)
|
|
'F00'
|
|
> Ox.encodeBase32(33819)
|
|
'110V'
|
|
@*/
|
|
Ox.encodeBase32 = function(num) {
|
|
return Ox.map(num.toString(32), function(char) {
|
|
return Ox.BASE_32_DIGITS[parseInt(char, 32)];
|
|
}).join('');
|
|
};
|
|
|
|
/*@
|
|
Ox.decodeBase32 <f> Decodes a base32-encoded number
|
|
See <a href="http://www.crockford.com/wrmg/base32.html">Base 32</a>.
|
|
> Ox.decodeBase32('foo')
|
|
15360
|
|
> Ox.decodeBase32('ILOU')
|
|
33819
|
|
> Ox.decodeBase32('?').toString()
|
|
'NaN'
|
|
@*/
|
|
Ox.decodeBase32 = function(str) {
|
|
return parseInt(Ox.map(str.toUpperCase(), function(char) {
|
|
var index = Ox.BASE_32_DIGITS.indexOf(Ox.BASE_32_ALIASES[char] || char);
|
|
return (index == -1 ? ' ' : index).toString(32);
|
|
}).join(''), 32);
|
|
};
|
|
|
|
/*@
|
|
Ox.encodeBase64 <f> Encode a number as base64
|
|
> Ox.encodeBase64(32394)
|
|
'foo'
|
|
@*/
|
|
Ox.encodeBase64 = function(num) {
|
|
return btoa(Ox.encodeBase256(num)).replace(/=/g, '');
|
|
};
|
|
|
|
/*@
|
|
Ox.decodeBase64 <f> Decodes a base64-encoded number
|
|
> Ox.decodeBase64('foo')
|
|
32394
|
|
@*/
|
|
Ox.decodeBase64 = function(str) {
|
|
return Ox.decodeBase256(atob(str));
|
|
};
|
|
|
|
/*@
|
|
Ox.encodeBase128 <f> Encode a number as base128
|
|
> Ox.encodeBase128(1685487)
|
|
'foo'
|
|
@*/
|
|
Ox.encodeBase128 = function(num) {
|
|
var str = '';
|
|
while (num) {
|
|
str = Ox.char(num & 127) + str;
|
|
num >>= 7;
|
|
}
|
|
return str || '0';
|
|
};
|
|
|
|
/*@
|
|
Ox.decodeBase128 <f> Decode a base128-encoded number
|
|
> Ox.decodeBase128('foo')
|
|
1685487
|
|
@*/
|
|
Ox.decodeBase128 = function(str) {
|
|
var num = 0, len = str.length;
|
|
Ox.forEach(str, function(char, i) {
|
|
num += char.charCodeAt(0) << (len - i - 1) * 7;
|
|
});
|
|
return num;
|
|
};
|
|
|
|
/*@
|
|
Ox.encodeBase256 <f> Encode a number as base256
|
|
> Ox.encodeBase256(6713199)
|
|
'foo'
|
|
@*/
|
|
Ox.encodeBase256 = function(num) {
|
|
var str = '';
|
|
while (num) {
|
|
str = Ox.char(num & 255) + str;
|
|
num >>= 8;
|
|
}
|
|
return str;
|
|
};
|
|
|
|
/*@
|
|
Ox.decodeBase256 <f> Decode a base256-encoded number
|
|
> Ox.decodeBase256('foo')
|
|
6713199
|
|
@*/
|
|
Ox.decodeBase256 = function(str) {
|
|
var num = 0, len = str.length;
|
|
Ox.forEach(str, function(char, i) {
|
|
num += char.charCodeAt(0) << (len - i - 1) * 8;
|
|
});
|
|
return num;
|
|
};
|
|
|
|
/*@
|
|
Ox.encodeDeflate <f> Encodes a string, using deflate
|
|
Since PNGs are deflate-encoded, the <code>canvas</code> object's
|
|
<code>toDataURL</code> method provides an efficient implementation.
|
|
The string is encoded as UTF-8 and written to the RGB channels of a
|
|
canvas element, then the PNG dataURL is decoded from base64, and some
|
|
head, tail and chunk names are removed.
|
|
(str) -> <s> The encoded string
|
|
str <s> The string to be encoded
|
|
# Test with: Ox.decodeDeflate(Ox.encodeDeflate('foo'), alert)
|
|
@*/
|
|
|
|
Ox.encodeDeflate = function(str, callback) {
|
|
// Make sure we can encode the full unicode range of characters.
|
|
str = Ox.encodeUTF8(str);
|
|
// We can only safely write to RGB, so we need 1 pixel for 3 bytes.
|
|
// The string length may not be a multiple of 3, so we need to encode
|
|
// the number of padding bytes (1 byte), the string, and non-0-bytes
|
|
// as padding, so that the combined length becomes a multiple of 3.
|
|
var len = 1 + str.length, c = Ox.canvas(Math.ceil(len / 3), 1),
|
|
data, idat, pad = (3 - len % 3) % 3;
|
|
str = Ox.char(pad) + str + Ox.repeat('\u00FF', pad);
|
|
Ox.loop(c.data.length, function(i) {
|
|
// Write character codes into RGB, and 255 into ALPHA
|
|
c.data[i] = i % 4 < 3 ? str.charCodeAt(i - parseInt(i / 4)) : 255;
|
|
});
|
|
c.context.putImageData(c.imageData, 0, 0);
|
|
// Get the PNG data from the data URL and decode it from base64.
|
|
str = atob(c.canvas.toDataURL().split(',')[1]);
|
|
// Discard bytes 0 to 15 (8 bytes PNG signature, 4 bytes IHDR length, 4
|
|
// bytes IHDR name), keep bytes 16 to 19 (width), discard bytes 20 to 29
|
|
// (4 bytes height, 5 bytes flags), keep bytes 29 to 32 (IHDR checksum),
|
|
// keep the rest (IDAT chunks), discard the last 12 bytes (IEND chunk).
|
|
data = str.substr(16, 4) + str.substr(29, 4);
|
|
idat = str.substr(33, str.length - 45);
|
|
while (idat) {
|
|
// Each IDAT chunk is 4 bytes length, 4 bytes name, length bytes
|
|
// data and 4 bytes checksum. We can discard the name parts.
|
|
len = idat.substr(0, 4);
|
|
data += len + idat.substr(8, 4 + (len = Ox.decodeBase256(len)));
|
|
idat = idat.substr(12 + len);
|
|
}
|
|
callback && callback(data);
|
|
return data;
|
|
};
|
|
|
|
/*@
|
|
Ox.decodeDeflate <f> Decodes an deflate-encoded string
|
|
Since PNGs are deflate-encoded, the <code>canvas</code> object's
|
|
<code>drawImage</code> method provides an efficient implementation. The
|
|
string will be wrapped as a PNG dataURL, encoded as base64, and drawn
|
|
onto a canvas element, then the RGB channels will be read, and the
|
|
result will be decoded from UTF8.
|
|
(str) -> <u> undefined
|
|
str <s> The string to be decoded
|
|
callback <f> Callback function
|
|
str <s> The decoded string
|
|
@*/
|
|
|
|
Ox.decodeDeflate = function(str, callback) {
|
|
var image = new Image(),
|
|
// PNG file signature and IHDR chunk
|
|
data = '\u0089PNG\r\n\u001A\n\u0000\u0000\u0000\u000DIHDR'
|
|
+ str.substr(0, 4) + '\u0000\u0000\u0000\u0001'
|
|
+ '\u0008\u0006\u0000\u0000\u0000' + str.substr(4, 4),
|
|
// IDAT chunks
|
|
idat = str.substr(8), len;
|
|
function error() {
|
|
throw new RangeError('Deflate codec can\'t decode data.');
|
|
}
|
|
while (idat) {
|
|
// Reinsert the IDAT chunk names
|
|
len = idat.substr(0, 4);
|
|
data += len + 'IDAT' + idat.substr(4, 4 + (len = Ox.decodeBase256(len)));
|
|
idat = idat.substr(8 + len);
|
|
}
|
|
// IEND chunk
|
|
data += '\u0000\u0000\u0000\u0000IEND\u00AE\u0042\u0060\u0082';
|
|
// Unfortunately, we can't synchronously set the source of an image,
|
|
// draw it onto a canvas, and read its data.
|
|
image.onload = function() {
|
|
str = Ox.makeArray(Ox.canvas(image).data).map(function(v, i) {
|
|
// Read one character per RGB byte, ignore ALPHA.
|
|
return i % 4 < 3 ? Ox.char(v) : '';
|
|
}).join('');
|
|
try {
|
|
// Parse the first byte as number of bytes to chop at the end,
|
|
// and the rest, without these bytes, as an UTF8-encoded string.
|
|
str = Ox.decodeUTF8(str.substr(1, str.length - 1 - str.charCodeAt(0)))
|
|
} catch (e) {
|
|
error();
|
|
}
|
|
callback(str);
|
|
}
|
|
image.onerror = error;
|
|
image.src = 'data:image/png;base64,' + btoa(data);
|
|
};
|
|
|
|
/*@
|
|
Ox.encodeHTML <f> HTML-encodes a string
|
|
> Ox.encodeHTML('\'<"&">\'')
|
|
''<"&">''
|
|
> Ox.encodeHTML('äbçdê')
|
|
'äbçdê'
|
|
@*/
|
|
Ox.encodeHTML = function(str) {
|
|
return Ox.map(str.toString(), function(v) {
|
|
var code = v.charCodeAt(0);
|
|
return code < 128 ? (v in Ox.HTML_ENTITIES ? Ox.HTML_ENTITIES[v] : v) :
|
|
'&#x' + Ox.pad(code.toString(16).toUpperCase(), 4) + ';';
|
|
}).join('');
|
|
};
|
|
|
|
/*@
|
|
Ox.decodeHTML <f> Decodes an HTML-encoded string
|
|
> Ox.decodeHTML(''<"&">'')
|
|
'\'<"&">\''
|
|
> Ox.decodeHTML(''<"&">'')
|
|
'\'<"&">\''
|
|
> Ox.decodeHTML('äbçdê')
|
|
'äbçdê'
|
|
> Ox.decodeHTML('äbçdê')
|
|
'äbçdê'
|
|
@*/
|
|
Ox.decodeHTML = function(str) {
|
|
// relies on dom, but shorter than using this:
|
|
// http://www.w3.org/TR/html5/named-character-references.html
|
|
return Ox.element('<div>').html(str)[0].childNodes[0].nodeValue;
|
|
};
|
|
|
|
/*@
|
|
Ox.encodePNG <f> Encodes a string into an image, returns a new image
|
|
The string is compressed with deflate (by proxy of canvas), prefixed
|
|
with its length (four bytes), and encoded bitwise into the red, green
|
|
and blue bytes of all fully opaque pixels of the image, by flipping, if
|
|
necessary, the least significant bit, so that for every byte, the total
|
|
number of bits set to to 1, modulo 2, is the bit that we are encoding.
|
|
(img, src) -> <e> An image into which the string has been encoded
|
|
img <e> Any JavaScript PNG image object
|
|
str <s> The string to be encoded
|
|
@*/
|
|
|
|
Ox.encodePNG = function(img, str) {
|
|
var c = Ox.canvas(img), i = 0;
|
|
// Compress the string
|
|
str = Ox.encodeDeflate(str);
|
|
// Prefix the string with its length, as a four-byte value
|
|
str = Ox.pad(Ox.encodeBase256(str.length), 4, Ox.char(0)) + str;
|
|
// Create an array of bit values
|
|
Ox.forEach(Ox.flatten(Ox.map(str, function(chr) {
|
|
return Ox.map(Ox.range(8), function(i) {
|
|
return chr.charCodeAt(0) >> 7 - i & 1;
|
|
});
|
|
})), function(bit) {
|
|
// Skip all pixels that are not fully opaque
|
|
while (i < c.data.length && i % 4 == 0 && c.data[i + 3] < 255) {
|
|
i += 4;
|
|
}
|
|
if (i == c.data.length) {
|
|
throw new RangeError('PNG codec can\'t encode data');
|
|
}
|
|
// If the number of bits set to one, modulo 2 is equal to the bit,
|
|
// do nothing, otherwise, flip the least significant bit.
|
|
c.data[i] += c.data[i].toString(2).replace(/0/g, '').length % 2
|
|
== bit ? 0 : c.data[i] % 2 ? -1 : 1;
|
|
i++;
|
|
});
|
|
c.context.putImageData(c.imageData, 0, 0);
|
|
img = new Image();
|
|
img.src = c.canvas.toDataURL();
|
|
return img;
|
|
/*
|
|
wishlist:
|
|
- only use deflate if it actually shortens the message
|
|
- encode a decoy message into the least significant bit
|
|
(and flip the second least significant bit, if at all)
|
|
- write an extra png chunk containing some key
|
|
*/
|
|
};
|
|
|
|
/*@
|
|
Ox.decodePNG <f> Decodes an image, returns a string
|
|
For every red, green and blue byte of every fully opaque pixel of the
|
|
image, one bit, namely the number of bits of the byte set to one, modulo
|
|
2, is being read, the result being the string, prefixed with its length
|
|
(four bytes), which is decompressed with deflate (by proxy of canvas).
|
|
(img, callback) -> <u> undefined
|
|
img <e> The image into which the string has been encoded
|
|
callback <f> Callback function
|
|
str <s> The decoded string
|
|
@*/
|
|
|
|
Ox.decodePNG = function(img, callback) {
|
|
var bits = '', data = Ox.canvas(img).data, flag = false, i = 0,
|
|
len = 4, str = '';
|
|
while (len) {
|
|
// Skip all pixels that are not fully opaque
|
|
while (i < data.length && i % 4 == 0 && data[i + 3] < 255) {
|
|
i += 4;
|
|
}
|
|
if (i == data.length) {
|
|
break;
|
|
}
|
|
// Read the number of bits set to one, modulo 2
|
|
bits += data[i].toString(2).replace(/0/g, '').length % 2;
|
|
if (++i % 8 == 0) {
|
|
// Every 8 bits, add one byte
|
|
str += Ox.char(parseInt(bits, 2));
|
|
bits = '';
|
|
// When length reaches 0 for the first time,
|
|
// decode the string and treat it as the new length
|
|
if (--len == 0 && !flag) {
|
|
flag = true;
|
|
len = Ox.decodeBase256(str);
|
|
str = '';
|
|
}
|
|
}
|
|
}
|
|
try {
|
|
Ox.decodeDeflate(str, callback);
|
|
} catch (e) {
|
|
throw new RangeError('PNG codec can\'t decode image');
|
|
}
|
|
};
|
|
|
|
/*@
|
|
Ox.encodeUTF8 <f> Encodes a string as UTF-8
|
|
see http://en.wikipedia.org/wiki/UTF-8
|
|
(string) -> <s> UTF-8 encoded string
|
|
string <s> Any string
|
|
> Ox.encodeUTF8("YES")
|
|
"YES"
|
|
> Ox.encodeUTF8("¥€$")
|
|
"\u00C2\u00A5\u00E2\u0082\u00AC\u0024"
|
|
@*/
|
|
Ox.encodeUTF8 = function(str) {
|
|
return Ox.map(str, function(chr) {
|
|
var code = chr.charCodeAt(0),
|
|
str = '';
|
|
if (code < 128) {
|
|
str = chr;
|
|
} else if (code < 2048) {
|
|
str = String.fromCharCode(code >> 6 | 192) +
|
|
String.fromCharCode(code & 63 | 128);
|
|
} else {
|
|
str = String.fromCharCode(code >> 12 | 224) +
|
|
String.fromCharCode(code >> 6 & 63 | 128) +
|
|
String.fromCharCode(code & 63 | 128);
|
|
}
|
|
return str;
|
|
}).join('');
|
|
};
|
|
|
|
/*@
|
|
Ox.decodeUTF8 <f> Decodes an UTF-8-encoded string
|
|
see http://en.wikipedia.org/wiki/UTF-8
|
|
(utf8) -> <s> string
|
|
utf8 <s> Any UTF-8-encoded string
|
|
> Ox.decodeUTF8('YES')
|
|
'YES'
|
|
> Ox.decodeUTF8('\u00C2\u00A5\u00E2\u0082\u00AC\u0024')
|
|
'¥€$'
|
|
@*/
|
|
Ox.decodeUTF8 = function(str) {
|
|
var bytes = Ox.map(str, function(v) {
|
|
return v.charCodeAt(0);
|
|
}),
|
|
i = 0,
|
|
len = str.length,
|
|
str = '';
|
|
while (i < len) {
|
|
if (bytes[i] <= 128) {
|
|
str += String.fromCharCode(bytes[i]);
|
|
i++;
|
|
} else if (
|
|
bytes[i] >= 192 && bytes[i] < 240 &&
|
|
i < len - (bytes[i] < 224 ? 1 : 2)
|
|
) {
|
|
if (bytes[i + 1] >= 128 && bytes[i + 1] < 192) {
|
|
if (bytes[i] < 224) {
|
|
str += String.fromCharCode((bytes[i] & 31) << 6 |
|
|
bytes[i + 1] & 63);
|
|
i += 2;
|
|
} else if (bytes[i + 2] >= 128 && bytes[i + 2] < 192) {
|
|
str += String.fromCharCode((bytes[i] & 15) << 12 |
|
|
(bytes[i + 1] & 63) << 6 | bytes[i + 2] & 63);
|
|
i += 3;
|
|
} else {
|
|
throwUTF8Error(bytes[i + 2], i + 2);
|
|
}
|
|
} else {
|
|
throwUTF8Error(bytes[i + 1], i + 1);
|
|
}
|
|
} else {
|
|
throwUTF8Error(bytes[i], i);
|
|
}
|
|
}
|
|
return str;
|
|
};
|
|
|
|
})(); |