diff --git a/source/Ox/js/Encoding.js b/source/Ox/js/Encoding.js index 8e4e44d5..926b107e 100644 --- a/source/Ox/js/Encoding.js +++ b/source/Ox/js/Encoding.js @@ -1,404 +1,371 @@ 'use strict'; -(function() { - - function cap(width, height) { - // returns maximum encoding capacity of an image - return parseInt(width * height * 3/8) - 4; +/*@ +Ox.encodeBase26 Encode a number as bijective base26 + See + Bijective numeration. + > Ox.encodeBase26(4461) + 'FOO' +@*/ +Ox.encodeBase26 = function(number) { + var string = ''; + while (number) { + string = String.fromCharCode(64 + number % 26) + string; + number = parseInt(number / 26); } + return string; +}; - 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; - } +/*@ +Ox.decodeBase26 Decodes a bijective base26-encoded number + See + Bijective numeration. + > Ox.decodeBase26('foo') + 4461 +@*/ +Ox.decodeBase26 = function(string) { + return string.toUpperCase().split('').reverse().reduce(function(p, c, i) { + return p + (c.charCodeAt(0) - 64) * Math.pow(26, i); + }, 0); +}; - 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; - } +/*@ +Ox.encodeBase32 Encode a number as base32 + See Base 32. + > Ox.encodeBase32(15360) + 'F00' + > Ox.encodeBase32(33819) + '110V' +@*/ +Ox.encodeBase32 = function(number) { + return Ox.map(number.toString(32), function(character) { + return Ox.BASE_32_DIGITS[parseInt(character, 32)]; + }); +}; - function throwPNGError(str) { - throw new RangeError( - 'PNG codec can\'t ' + - (str == 'en' ? 'encode data' : 'decode image') +/*@ +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(string) { + return parseInt(Ox.map(string.toUpperCase(), function(character) { + var index = Ox.BASE_32_DIGITS.indexOf( + Ox.BASE_32_ALIASES[character] || character ); - } + return (index == -1 ? ' ' : index).toString(32); + }), 32); +}; - function throwUTF8Error(byte, pos) { +/*@ +Ox.encodeBase64 Encode a number as base64 + > Ox.encodeBase64(32394) + 'foo' +@*/ +Ox.encodeBase64 = function(number) { + return btoa(Ox.encodeBase256(number)).replace(/=/g, ''); +}; + +/*@ +Ox.decodeBase64 Decodes a base64-encoded number + > Ox.decodeBase64('foo') + 32394 +@*/ +Ox.decodeBase64 = function(string) { + return Ox.decodeBase256(atob(string)); +}; + +/*@ +Ox.encodeBase128 Encode a number as base128 + > Ox.encodeBase128(1685487) + 'foo' +@*/ +Ox.encodeBase128 = function(number) { + var string = ''; + while (number) { + string = Ox.char(number & 127) + string; + number >>= 7; + } + return string; +}; + +/*@ +Ox.decodeBase128 Decode a base128-encoded number + > Ox.decodeBase128('foo') + 1685487 +@*/ +Ox.decodeBase128 = function(string) { + return string.split('').reverse().reduce(function(p, c, i) { + return p + (c.charCodeAt(0) << i * 7); + }, 0); +}; + +/*@ +Ox.encodeBase256 Encode a number as base256 + > Ox.encodeBase256(6713199) + 'foo' +@*/ +Ox.encodeBase256 = function(number) { + var string = ''; + while (number) { + string = Ox.char(number & 255) + string; + number >>= 8; + } + return string; +}; + +/*@ +Ox.decodeBase256 Decode a base256-encoded number + > Ox.decodeBase256('foo') + 6713199 +@*/ +Ox.decodeBase256 = function(string) { + return string.split('').reverse().reduce(function(p, c, i) { + return p + (c.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(string, callback) { + // Make sure we can encode the full unicode range of characters. + string = Ox.encodeUTF8(string); + // 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 length = 1 + string.length, c = Ox.canvas(Math.ceil(length / 3), 1), + data, idat, pad = (3 - length % 3) % 3; + string = Ox.char(pad) + string + 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 ? string.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. + string = 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 = string.slice(16, 20) + string.slice(29, 33); + idat = string.slice(33, -12); + 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. + length = idat.slice(0, 4); + data += length + idat.slice(8, 12 + ( + length = Ox.decodeBase256(length) + )); + idat = idat.slice(12 + length); + } + // Allow for async use, symmetrical to Ox.decodeDeflate + 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(string, callback) { + var image = new Image(), + // PNG file signature and IHDR chunk + data = '\u0089PNG\r\n\u001A\n\u0000\u0000\u0000\u000DIHDR' + + string.slice(0, 4) + '\u0000\u0000\u0000\u0001' + + '\u0008\u0006\u0000\u0000\u0000' + string.slice(4, 8), + // IDAT chunks + idat = string.slice(8), length; + function error() { + throw new RangeError('Deflate codec can\'t decode data.'); + } + while (idat) { + // Reinsert the IDAT chunk names + length = idat.slice(0, 4); + data += length + 'IDAT' + idat.slice(4, 8 + ( + length = Ox.decodeBase256(length) + )); + idat = idat.slice(8 + length); + } + // 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() { + string = Ox.toArray(Ox.canvas(image).data).map(function(value, index) { + // Read one character per RGB byte, ignore ALPHA. + return index % 4 < 3 ? Ox.char(value) : ''; + }).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. + string = Ox.decodeUTF8(string.slice(1, -string.charCodeAt(0))); + } catch (e) { + error(); + } + callback(string); + } + 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; + }); +}; + +/*@ +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(string) { + var code, i = 0, length = string.length, ret = ''; + function error(byte, position) { throw new RangeError( 'UTF-8 codec can\'t decode byte 0x' + - byte.toString(16).toUpperCase() + ' at position ' + pos + byte.toString(16).toUpperCase() + ' at position ' + position ); } - - /*@ - 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)]; - }); - }; - - /*@ - 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); - }), 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.slice(16, 20) + str.slice(29, 33); - idat = str.slice(33, -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.slice(0, 4); - data += len + idat.slice(8, 12 + (len = Ox.decodeBase256(len))); - idat = idat.slice(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.slice(0, 4) + '\u0000\u0000\u0000\u0001' - + '\u0008\u0006\u0000\u0000\u0000' + str.slice(4, 8), - // IDAT chunks - idat = str.slice(8), len; - function error() { - throw new RangeError('Deflate codec can\'t decode data.'); - } - while (idat) { - // Reinsert the IDAT chunk names - len = idat.slice(0, 4); - data += len + 'IDAT' + idat.slice(4, 8 + (len = Ox.decodeBase256(len))); - idat = idat.slice(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.slice(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; - }); - }; - - /*@ - 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); - } + while (i < length) { + code = [ + string.charCodeAt(i), + string.charCodeAt(i + 1), + string.charCodeAt(i + 2) + ]; + if (code[0] <= 128) { + ret += string[i]; + i++; + } else if ( + code[0] >= 192 && code[0] < 240 + && i < length - (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[1], i + 1); + error(code[2], i + 2); } } else { - throwUTF8Error(code[0], i); + error(code[1], i + 1); } + } else { + error(code[0], i); } - return ret; - }; - -})(); + } + return ret; +};