'use strict'; (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 Encode a number as bijective base26 See Bijective numeration. > Ox.encodeBase26(4461) 'FOO' @*/ Ox.encodeBase26 = function(num) { var ret = ''; while (num) { ret = String.fromCharCode(64 + num % 26) + ret; num = parseInt(num / 26); } return ret; }; /*@ Ox.decodeBase26 Decodes a bijective base26-encoded number See Bijective numeration. > Ox.decodeBase26('foo') 4461 @*/ Ox.decodeBase26 = function(str) { return str.toUpperCase().split('').reverse().reduce(function(p, v, i) { return p + (v.charCodeAt(0) - 64) * Math.pow(26, i); }, 0); }; /*@ Ox.encodeBase32 Encode a number as base32 See Base 32. > 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 Decodes a base32-encoded number See Base 32. > 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 Encode a number as base64 > Ox.encodeBase64(32394) 'foo' @*/ Ox.encodeBase64 = function(num) { return btoa(Ox.encodeBase256(num)).replace(/=/g, ''); }; /*@ Ox.decodeBase64 Decodes a base64-encoded number > Ox.decodeBase64('foo') 32394 @*/ Ox.decodeBase64 = function(str) { return Ox.decodeBase256(atob(str)); }; /*@ Ox.encodeBase128 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; }; /*@ Ox.decodeBase128 Decode a base128-encoded number > Ox.decodeBase128('foo') 1685487 @*/ Ox.decodeBase128 = function(str) { return str.split('').reverse().reduce(function(p, v, i) { return p + (v.charCodeAt(0) << i * 7); }, 0); }; /*@ Ox.encodeBase256 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 Decode a base256-encoded number > Ox.decodeBase256('foo') 6713199 @*/ Ox.decodeBase256 = function(str) { return str.split('').reverse().reduce(function(p, v, i) { return p + (v.charCodeAt(0) << i * 8); }, 0); }; /*@ Ox.encodeDeflate Encodes a string, using deflate Since PNGs are deflate-encoded, the canvas object's toDataURL 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) -> The encoded string str 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 Decodes an deflate-encoded string Since PNGs are deflate-encoded, the canvas object's drawImage 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) -> undefined str The string to be decoded callback Callback function str 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.toArray(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 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) + ';'; }); }; /*@ Ox.decodeHTML Decodes an HTML-encoded string > Ox.decodeHTML(''<"&">'') '\'<"&">\'' > Ox.decodeHTML(''<"&">'') '\'<"&">\'' > Ox.decodeHTML('äbçdê') 'äbçdê' > Ox.decodeHTML('äbçdê') 'äbçdê' > Ox.decodeHTML('bold') 'bold' @*/ Ox.decodeHTML = function(str) { // relies on dom, but shorter than using this: // http://www.w3.org/TR/html5/named-character-references.html return Ox.decodeHTMLEntities(Ox.element('
').html(str).html()); }; Ox.encodeHTMLEntities = function(str) { return str.replace( new RegExp('(' + Object.keys(Ox.HTML_ENTITIES).join('|') + ')', 'g'), function(match) { return Ox.HTML_ENTITIES[match]; } ); }; Ox.decodeHTMLEntities = function(str) { return str.replace( new RegExp('(' + Ox.values(Ox.HTML_ENTITIES).join('|') + ')', 'g'), function(match) { return Ox.keyOf(Ox.HTML_ENTITIES, match); } ); }; /*@ Ox.encodeUTF8 Encodes a string as UTF-8 see http://en.wikipedia.org/wiki/UTF-8 (string) -> UTF-8 encoded string string 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 Decodes an UTF-8-encoded string see http://en.wikipedia.org/wiki/UTF-8 (utf8) -> string utf8 Any UTF-8-encoded string > Ox.decodeUTF8('YES') 'YES' > Ox.decodeUTF8('\u00C2\u00A5\u00E2\u0082\u00AC\u0024') '¥€$' @*/ Ox.decodeUTF8 = function(str) { var code, i = 0, len = str.length, ret = ''; while (i < len) { code = Ox.range(3).map(function(o) { return str.charCodeAt(i + o); }); if (code[0] <= 128) { ret += str[i]; i++; } else if ( code[0] >= 192 && code[0] < 240 && i < len - (code[0] < 224 ? 1 : 2) ) { if (code[1] >= 128 && code[1] < 192) { if (code[0] < 224) { ret += String.fromCharCode( (code[0] & 31) << 6 | code[1] & 63 ); i += 2; } else if (code[2] >= 128 && code[2] < 192) { ret += String.fromCharCode( (code[0] & 15) << 12 | (code[1] & 63) << 6 | code[2] & 63 ); i += 3; } else { throwUTF8Error(code[2], i + 2); } } else { throwUTF8Error(code[1], i + 1); } } else { throwUTF8Error(code[0], i); } } return ret; }; })();