最近有个想法,将文本转换成图片像素,会是个什么样子的。
为什么会有这样的想法?因为很久之前就发现像素图片是由 rgb 或者 rgba 构成像素组成的。每一个色素的范围是 $[0,255]$,刚好是$2^8=256$。而 unicode 恰好又是 6 位 16 进制表示的,那么’0xff’刚好又是 255。
所以我们可以将文字转为 unicode 后,平分为三分,分别就是 r,g,b。构成一个像素。
理论可行,开始操作。
将文字转为 16 进制
1 2
| '中'.charCodeAt(0).toString(16).padStart(6, "0");
|
这样子我们就能切割出 rgb 了。
1
| const [Red, Green, Blue] = [parseInt(bit16.slice(0, 2), 16), parseInt(bit16.slice(2, 4), 16), parseInt(bit16.slice(4, 6), 16)];
|
保存为 png 文件
这里使用现成的 pngjs,新建实例化对象时,需要传入长宽数据。
最简单的方法是获取到所有字符的个数,开方后,向上取整得到宽和高
1
| const size = Math.ceil(Math.sqrt(book.length));
|
初始化
1 2 3 4 5
| const png = new PNG({ filterType: -1, width: size, height: size });
|
因为使用的 png,有 Alpha 通道,即透明度,所以一个像素占用 4 个 byte。
1 2 3 4 5 6 7 8 9 10 11
| for (let x = 0; x < size; x++) { for (let y = 0; y < size; y++) { const bit16 = book[x * size + y] ? book[x * size + y].charCodeAt(0).toString(16).padStart(6, "0") : "000000"; const [Red, Green, Blue] = [parseInt(bit16.slice(0, 2), parseInt(bit16.slice(2, 4), 16), parseInt(bit16.slice(4, 6), 16)]; const idx = (size * x + y) << 2; png.data[idx] = Red; png.data[idx + 1] = Green; png.data[idx + 2] = Blue; png.data[idx + 3] = (Green === 0 && Blue === 0) ? 0 : 255; } }
|
最后保存图片
1
| png.pack().pipe(fs.createWriteStream(pngPath));
|
png 转 txt
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26
| fs.createReadStream(pngPath) .pipe( new PNG({ filterType: -1 }) ) .on("parsed", function() { let bookData = ""; for (let x = 0; x < this.height; x++) { for (let y = 0; y < this.width; y++) { const idx = (this.width * x + y) << 2; if (this.data[idx + 3] === 0) { fs.writeFile(textPath, bookData, (err, data) => { if (err) { console.log(err); } resolve(bookData); }); return; } const unicode = `00${(this.data[idx + 1]).toString(16).padStart(2, 0)}${(this.data[idx + 2]).toString(16).padStart(2, 0)}`; bookData += String.fromCharCode(parseInt(unicode, 16)); } } }); });
|
优化
查一下常用的中文 unicode 编码,高 2 位其实都是”00”,如果确定是存中文和英文,其实一个像素能存下两个字符。
高 4 位一个字符,低 4 位一个字符。这样图片的宽高就会缩小一倍。不过这样的话,png 转 txt 的模块代码也要做相应的修改。