'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.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 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 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.encodePNG 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) -> An image into which the string has been encoded img Any JavaScript PNG image object str 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 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) -> undefined img The image into which the string has been encoded callback Callback function str 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 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 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; }; })();