oxjs/source/Ox/js/Encoding.js

490 lines
17 KiB
JavaScript
Raw Normal View History

2011-11-05 16:46:53 +00:00
'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 <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('\'<"&">\'')
'&apos;&lt;&quot;&amp;&quot;&gt;&apos;'
> Ox.encodeHTML('äbçdê')
'&#x00E4;b&#x00E7;d&#x00EA;'
@*/
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('&apos;&lt;&quot;&amp;&quot;&gt;&apos;')
'\'<"&">\''
> Ox.decodeHTML('&#x0027;&#x003C;&#x0022;&#x0026;&#x0022;&#x003E;&#x0027;')
'\'<"&">\''
> Ox.decodeHTML('&auml;b&ccedil;d&ecirc;')
'äbçdê'
> Ox.decodeHTML('&#x00E4;b&#x00E7;d&#x00EA;')
'äbçdê'
> Ox.decodeHTML('<b>bold</b>')
'<b>bold</b>'
@*/
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('<div>').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 <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;
};
})();