在座的各位大大,今天天气晴朗哈,楼下这位即将给大家表演下“隐身术”,大家有钱捧个钱场,没钱的先去赚钱再来捧场哈
biu~~ biu~~ biu~~
哇塞,gif, 江湖骗术~~~~~ 拉出去砍了!!!
今天呢,我们也用前端玩玩这个“隐身术”, 不过呢,在前端,这个技术大多称为"隐写术"
接下来,我们就通过一个例子,来了解下“图片隐写术”
案件:(大概是这样的)
2013年年底,美团起诉大众点评盗用其平台上部分摄影图片
经法院判定,证据确凿,大众点评所属公司最后向美团所属公司赔付49400元
那么关于这个案件具体起因经过结果是如何的,大家可以多问问度娘,我们今天来聊聊这个“证据确凿”
证据:
美团在图片上留下了证据,大众点评盗用图片踩了坑
难道美团在图片上加了水印? 大众点评睁一只眼闭一只眼拿去用了?
事情还真是这么个回事,但是大众点评绝对不是"睁一只眼,闭一只眼"
而是因为美团在自己的图片上加了隐写术水印,肉眼看不出来哦
我们举个栗子:
下面是博主敲代码的机子,博主早就看这个机子不舒服了,用娱乐大师测了下,心里“mmp”
于是就抱怨了下,用隐写术写下了“我想换显卡”这句话,处理后肉眼看不出图片上有任何字体
于是乎,隐写术处理后:
但当原形毕露时:
那么是如何将文字写入图片之中,且隐藏起来的呢?
canvas 和 rgba 有话要说!!!
我们都知道,在啊html中,img标签可以展示图片,canvas也可以将图片展示出来,canvas是基于像素的图片API,简单说就是
能够把图片一像素一像素地绘制出来。也可以理解为一个超级牛的画家,在空白的canvas画板上绘制了一幅画,而这幅画和你的img
一模一样~~~~
然后rgba听够了canvas的吹嘘,早已难以忍受
rgba自称,你们看到的五颜六色,色彩斑然,那不都是来源于我的功劳?
我有R, G, B 对应红绿蓝三种颜色通道,还有A [alpha(阿尔法)]控制透明度,我才是绘制图片的大神!!!
博主只好劝架了,
今天就合作一下,一起搞“图片隐写术”
先贴上代码:
构建一个canvas画布,宽高根据你要处理的图片来~~~
<div class="wrap"> <canvas id="canvas" width="546" height="366"></canvas> </div>
然后贴出核心处理代码
// 加密 var ctx = document.getElementById('canvas').getContext('2d'); var img = new Image(); var hideDate; // 定义隐藏数据,这里文案就是想要隐藏的数据 var showData; // 定义展示数据,图片是想要展示的数据 ctx.font = '40px Microsoft Yahei'; ctx.fillText('我想换显卡', 150, 240); // 获取隐藏文字的数据 hideDate = ctx.getImageData(0, 0, ctx.canvas.width, ctx.canvas.height).data; // 定义隐藏数据赋值 img.onload = function() { ctx.drawImage(img, 0, 0); showData = ctx.getImageData(0, 0, ctx.canvas.width, ctx.canvas.height); makeHide(hideDate, 'R'); }; img.src = "../img/diy.png"; // 加密方法 function makeHide(newData, color) { var bit, alpha; // alpha的作用是找到alpha通道值, switch(color){ case 'R': bit = 0; alpha = 3; break; case 'G': bit = 1; alpha = 2; break; case 'B': bit = 2; alpha = 1; break; } for(var i = 0; i < showData.data.length; i++){ if(i % 4 == bit){ // 没有文字信息的像素 if(newData[i + alpha] == 0 && (showData.data[i] % 2 == 1)){ showData.data[i]--; // 为何将非文字区域R像素的奇数-1转化为偶数? 1、改变小看不出,2、解密时R值为偶数直接归0,配合其对应rgba组的其他值归0呈现全黑色,(虽然自加也能够使得奇数变偶数,但是255++ 就会超出,所以这边必须使用自减) // 有信息的像素 } else if (newData[i + alpha] != 0 && (showData.data[i] % 2 == 0)){ showData.data[i]++; // 为何将文字所对应图片区域的R像素的偶数+1转化为奇数? 1、改变小看不出,2、解密时可以将文字区域R值直接转化为255,(同理这边不能使用--) } } } ctx.putImageData(showData, 0, 0); }
接下来我们一步步讲解下
首先我们要先了解下canvas的 getImageData 这个API,不妨将上面的showData打印出来,我们得到如下结果:
没错,getImageData能够获取canvas绘制图片的宽高信息,还能获取到data 也就是Uint8ClampedArray的数据。
那么 Uint8ClampedArray 是什么呢?
度娘告诉我们,它是8位无符号整型固定数组,表示一个由值固定在0-255区间的8位无符号整型组成的数组
额~~~em,em,em~~~,听着好像有点懂,又有点麻烦的样子~~~
要不这样吧,我们且看它所说的0-255, 这不就是rgba中各个通道值的范围吗? 于是我们明白,这个数组存储的就是这张照片的
每一个像素rgba的信息,为什么说是每一个像素呢?
我们发现数组后面有一个数字,799344,然后我们这样计算
546(width) * 366(height) * 4(每一个像素有r,g,b,a4种值) = 799344
最终能证实我们的猜想是对的,我们也可以将其展开,可以看到里面确实存储着这张图片你的所有rgba信息
所以showData是canvas上图片的rgba信息,那么hideData则是只写了文案的canvas 的rgba信息,
hideData一样具备799344个rgba数组元素信息,只不过绝大部分是透明的canvas像素点,这点需要理解哦!
没错,它也是一个546 * 366的canvas
接下来,我们就需要对这两组rgba进行“偷梁换柱”
所以先埋个问题: 我们想把文案放到图片上,那是不是等同于把hideData上记载着文案的rgba值和
showData位置rgba值进行处理?
因此,我们对两者的Uint8ClampedArray数组进行操作:
由于图片有R, G, B三个颜色通道,我们任选其一进行操作,博主我选择R,红色,象征着~~~~~
我们回归主要的方法函数 makeHide
var bit, alpha; // alpha的作用是找到alpha通道值, switch(color){ case 'R': bit = 0; alpha = 3; break; case 'G': bit = 1; alpha = 2; break; case 'B': bit = 2; alpha = 1; break; }
我们定义了bit,和alpha两个变量,那么这两个变量用来干嘛的呢?
主要作用还是确定R,G,B,A的值在数组中的索引
我们知道rgba每一组有4个值,但是Uint8ClampedArray存储的却是一维数组,也就是4个值4个值一直拼接在一起
因此R通道在每一组rgba值中的索引是0,G通道索引为1, B通道索引为2
那alpha呢? 定义的alpha是要干嘛的呢? 这里很重要,这里相当重要,这里极其重要!!!
我们先把hideData中的Uint8ClampedArray打印出来看下
也就意味着,我们需要隐藏的文案,可以通过alpha透明度是否为0来判断,也就是非透明~
然后我们进行下一步,这里的newData参数传的就是hideData,我们操作的是R通道,因此
bit = 0, alpha = 3,然后接下来的通道颜色自增自减就需要各位脑洞打开去想象~~~ em, em, em 确实要靠想象一下
switch(color){ case 'R': bit = 0; alpha = 3; break; case 'G': bit = 1; alpha = 2; break; case 'B': bit = 2; alpha = 1; break; } for(var i = 0; i < showData.data.length; i++){ if(i % 4 == bit){ // 没有文字信息的像素 if(newData[i + alpha] == 0 && (showData.data[i] % 2 == 1)){ showData.data[i]--; // 为何将图片区域R通道的奇数自减1转化为偶数? (我们这边以R通道为例)
1、颜色改变小看不出,2、解密时R值为偶数直接归0,配合其对应rgba组的其他值归0呈现全黑色,(虽然自加也能够使得奇数变偶数,但是255++ 就会超出,所以这边必须使用自减) // 有信息的像素 } else if (newData[i + alpha] != 0 && (showData.data[i] % 2 == 0)){ showData.data[i]++; // 为何将文字所对应图片区域的R像素的偶数+1转化为奇数? 1、颜色改变小看不出,2、解密时可以将文字区域R值直接转化为255,(同理这边不能使用自减) } } } ctx.putImageData(showData, 0, 0);
其实我们对所需的rgba值进行自增自减属于自己制定的法则,为的就是在解密时能够确定处理那些rgba值,
所以这边的处理方法,各位大大不妨脑洞大开,还有别的方式
接下来贴出解密代码,一样以R通道为例子,记得事先把隐写好的图片右键保存下来,解密时可以出场~
// 解密 var ctx = document.getElementById('canvas').getContext('2d'); var img = new Image(); var showData; img.onload = function() { ctx.drawImage(img, 0, 0); // 获取指定区域的canvas像素信息 showData = ctx.getImageData(0, 0, ctx.canvas.width, ctx.canvas.height); getShow(showData, 'R'); }; img.src = '../img/d_r.png'; // d_r.png图片是隐写术后的canvas右键保存下来的 function getShow(showData, color) { var colorNum; switch(color){ case 'R': colorNum = 0; break; case 'G': colorNum = 1; break; case 'B': colorNum = 2; break; } for(var i = 0; i < showData.data.length; i++){ // 红/绿/蓝色分量判断 if(i % 4 == colorNum){ if(showData.data[i] % 2 == 0){ showData.data[i] = 0; } else { showData.data[i] = 255; } } else if(i % 4 == 3){ // alpha透明度不改变 continue; } else { // rgba组的其他值归0,替换掉,干扰像素的颜色,背景置黑,不替换更好看 // showData.data[i] = 0; } } // 将结果绘制到画布 ctx.putImageData(showData, 0, 0); }
解开隐写术其实是一个反向的操作~~~
大家不妨试下,这里理解rgba通道的处理确实需要一些想象~~~
如果有什么问题,或者发现其中的不足和BUG,欢迎提出~~~~~~~~~~