上個月在千里碼刷題的時候,碰到了比較有意思的一道題——隱寫術。既然感覺有意思,又很久沒有玩過canvas,所以今天結合這兩塊內容帶大家探索一下。
隱寫術算是一種加密技術,權威的wiki說法是“隱寫術是一門關於資訊隱藏的技巧與科學,所謂資訊隱藏指的是不讓除預期的接收者之外的任何人知曉資訊的傳遞事件或者資訊的內容。”這看似高大上的定義,並不是近代新誕生的技術,早在13世紀末德國人Trithemius就寫出了《隱寫術》的著作,學過密碼學的同學可能知道。好了,說了這麼多,隱寫術到底是什麼技術,讓我們看一個例子。
下面是一張看似普通的圖片,但其中卻藏有另一個肉眼無法識別的影像哦。
這是如果把上圖每個色彩空間和數字3進行邏輯與運算,再把亮度增強85倍,可以得到下圖。
簡單的說,上述的處理過程可以理解為對圖片畫素的處理,也就是說,加密的資訊散佈在每個畫素點上。可是,13世紀還沒有“畫素”這個概念吧?!沒錯,上面這個例子只是隱寫術的一個現代技術實現,隱藏資訊的手段有很多,我們日常的鈔票防偽也算是隱寫術的一種,所以標題上也限定了我們的討論範圍——圖片隱寫術。
(電子水印與隱寫術有一些共通點)
聚焦到載體為圖片的隱寫術,一起來從前端角度分析其技術原理。
我們知道圖片的畫素資訊裡儲存著RGB的色值,R、G、B分別為該畫素的紅、綠、藍通道,每個通道的分量值範圍在0~255,16進位制則是00~FF。在CSS中經常使用其16進位制形式,比如指定部落格頭部背景色為#A9D5F4。其中R(紅色)的16進位制值為A9,換算成十進位制為169。這時候,對R分量的值+1,即為170,整個畫素RGB值為#AAD5F4,別說你看不出差別,就連火眼金金的“畫素眼”設計師都察覺不出來呢。於此同時,修改G、B的分量值,也是我們無法察覺的。因此可以得出重要結論:RGB分量值的小量變動,是肉眼無法分辨的,不影響對圖片的識別。
有了這個結論,那就給我們了利用空間,常用手段的就是對二進位制最低位進行操作,下面就用canvas來演示一下。
解開圖中的祕密
這是一張我們當家美女小蘭師姐的照片,為了讓例子足夠簡單,裡面的R通道分量被我加入了文字資訊,想知道其中的資訊,可以跟我用canvas程式碼來解開。
首先在頁面加入一個canvas標籤,並獲取到其上下文。
1 |
<canvas id="canvas" width="256" height="256"></canvas> |
1 |
var ctx = document.getElementById('canvas').getContext('2d'); |
接著將圖片先繪製在畫布上,然後獲取其畫素資料。
1 2 3 4 5 6 7 8 9 |
var img = new Image(); var orginalData; img.onload = function() { ctx.drawImage(img, 0, 0); // 獲取指定區域的canvas畫素資訊 orginalData = ctx.getImageData(0, 0, ctx.canvas.width, ctx.canvas.height); console.log(orginalData); }; img.src = 'xiaolan.png'; |
列印出資料,會看到有一個非常大的陣列。
這個一維陣列儲存了所有的畫素資訊,一共有 256 * 256 * 4 = 262144個值。其中4個值一組,為什麼呢?在瀏覽器中解析圖片,除了RGB值外,每組第4個值為透明度值,即畫素資訊實際為大家熟知的rgba值。
這裡的解密規則是對R通道進行處理,R的分量最低位為1則該畫素設為紅色,R的分量最低位為0則該畫素設為黑色,直接看程式碼實現,完成後我們再繪製到canvas,即可看到結果。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 |
var processData = function(originalData){ var data = originalData.data; for(var i = 0; i < data.length; i++){ if(i % 4 == 0){ // 紅色分量 if(data[i] % 2 == 0){ data[i] = 0; } else { data[i] = 255; } } else if(i % 4 == 3){ // alpha通道不做處理 continue; } else { // 關閉其他分量,不關閉也不影響答案,甚至更美觀 o(^▽^)o data[i] = 0; } } // 將結果繪製到畫布 ctx.putImageData(originalData, 0, 0); } |
在img onload事件中呼叫processData方法,就可以看到結果啦。
得到的結果可能是這個樣子的。
在圖片中隱藏資訊
講了基礎的解密過程,再來反向說說加密過程。
既然要在圖片中加入文字資訊,那麼首先要獲取文字的畫素資訊,這裡我先用canvas在畫布上列印文字,獲取畫素資訊。
1 2 3 4 5 |
var textData; // 這些canvas API,好久沒用,需要查API文件了T_T ctx.font = '30px Microsoft Yahei'; ctx.fillText('廣告位招租u', 60, 130); textData = ctx.getImageData(0, 0, ctx.canvas.width, ctx.canvas.height).data; |
先儲存文字的畫素資訊,接著載入圖片獲取其畫素資訊,然後對兩組畫素進行處理,我在這裡抽離了一個公共方法。
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 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 |
var mergeData = function(newData, color){ var oData = orginalData.data; var bit, offset; // offset的作用是找到alpha通道值,這裡需要大家自己動動腦筋 switch(color){ case 'R': bit = 0; offset = 3; break; case 'G': bit = 1; offset = 2; break; case 'B': bit = 2; offset = 1; break; } for(var i = 0; i < oData.length; i++){ if(i % 4 == bit){ // 只處理目標通道 if(newData[i + offset] === 0 && (oData[i] % 2 === 1)){ // 沒有資訊的畫素,該通道最低位置0,但不要越界 if(oData[i] === 255){ oData[i]--; } else { oData[i]++; } } else if (newData[i + offset] !== 0 && (oData[i] % 2 === 0)){ // // 有資訊的畫素,該通道最低位置1,可以想想上面的斑點效果是怎麼實現的 if(oData[i] === 255){ oData[i]--; } else { oData[i]++; } } } } ctx.putImageData(orginalData, 0, 0); } |
上述程式碼做的是,接受要隱藏的資料以及隱藏的顏色通道,然後對原圖進行操作,修改圖片該通道分量的最低位,如果有文字資訊,則最低位置為1,否則為0。從最文章開頭的結論知道,RGB的三個通道可以分別隱藏不同資訊。
在img.onload中呼叫mergeData(textData, ‘R’),處理好影像後,只要在瀏覽器中的canvas上右鍵儲存圖片即可。
這裡的例子比較簡單,只展示了基本的最低位隱藏文字資訊,像二維碼這些簡單圖形也可以這麼處理。現實中隱藏畫中畫則需要更專業的影像處理演算法,這裡就不再展開了。
應用價值
圖片隱寫術的應用價值很廣泛,比如程式設計師之間的表白(不限男女),不失為一種浪漫的方式~
上面的案例中我沒有放出師姐的原片,這意味著如果盜用上面的圖片,我是有辦法識別出來的,起到了簡單的一種簽名作用。當然你也有辦法消除掉裡面的資訊,而前提是你需要知道我的加密方式,可是實際應用中絕不會這麼簡單哦。有個成功案例就是大眾點評通過這種方式,成功證明食神app對其圖片的盜用,為自己的合法權益進行了有效維護。
好的,感謝閱讀到最後,作為回報,我將福利隱藏在了師姐的圖片中,請自行發現吧~