暗水印顯隱術助力生產(chǎn)排障提效
一、背景
對于水印,相信大家都不陌生。在很多內(nèi)部平臺、對數(shù)據(jù)信息較為敏感的中后臺系統(tǒng)當中,我們基本上都會在系統(tǒng)關鍵數(shù)據(jù)展示區(qū)域中,加上一個半透明的文字水?。ㄍǔJ怯脩裘蛴脩鬷d等能夠唯一識別用戶的標識),以防止使用者通過截圖、拍照等方式將目標頁面的數(shù)據(jù)泄露出去。即使是泄露出去之后,也可以根據(jù)水印中的用戶標識能夠迅速定位到“始作俑者”,并采取一些必要的手段將泄密損失降到最低。
上述場景是水印最為常用的一個場景,但不代表水印只能用于這種場景,今天我們從水印技術的前世今生,一步步展開暗水印顯隱術的神秘面紗,揭秘暗水印顯隱術與 OCR的邂逅,如何提升測試、生產(chǎn)排障效率的。
二、水印技術演進簡史
圖片
水印從剛開始如同舊社會的窗紙一般一戳就破的毫無防護狀態(tài),通過不斷的演進與優(yōu)化,逐步增加水印的安全性,為水印功能增加了防刪除、防隱身、防覆蓋、防感知的能力,如同為窗戶披上了一層又一層的合金防爆裝甲,牢牢地守護著企業(yè)的核心數(shù)據(jù)安全。
演進實現(xiàn)演示
原始版水?。篤1
圖片
上述代碼比較簡單,原理就是用 canvas 繪制水印內(nèi)容生成圖片,然后以平鋪的背景圖的方式插入到頁面當中,這邊就不再贅述,上述代碼的效果:
圖片
防刪除水印:V2
上面實現(xiàn)的基礎版水印,有個很明顯的漏洞,那就是只要我們打開控制臺,找到展示水印的那個 div 元素,然后直接刪除,頁面上的水印就消失了。
由此可見我們實現(xiàn)的基礎版本的弱雞屬性。
那么,我們要如何防止用戶刪除元素呢?
既然用戶有可能通過刪除節(jié)點來去掉水印,那么,我們是否能夠監(jiān)聽節(jié)點的變化,一旦用戶刪除目標節(jié)點時,我們就將刪除的節(jié)點重新加回去,這樣不就可以解決問題了嗎?
說干就干,我們先來看看要如何監(jiān)聽節(jié)點的變化。
經(jīng)過查閱文檔后,找到了 MutationObserver 這個API,可以幫助我們監(jiān)聽目標元素的相關變化,話不多說,我們來看看效果如何。
圖片
上述示例運行后的效果是這樣的:
防隱身水印:V3
那么,這種操作我們要怎么防止呢?其實也很簡單,依然還是使用 MutationObserver ,它不僅可以幫我們監(jiān)聽子節(jié)點的變化,還能監(jiān)聽屬性的變化。既然用戶想用樣式style讓我們的水印隱身,那么我們就在用戶改變水印的樣式時,重置它的樣式即可。
圖片
我們來看看效果如何:
看起來已經(jīng)有了點狗皮膏藥的意思了,甩也甩不掉,改也改不了。
但是,這樣就算完了嗎?不!還沒完!
防覆蓋水?。篤4
別以為這樣就完了,既然動不了水印元素,那咱還可以曲線救國嘛,通過修改我們想要看的那個元素的樣式,讓它覆蓋在水印元素之上,這樣不是也可以達到我們的目的嗎?
這個問題其實是很多水印 SDK 和項目實現(xiàn)水印時忽略的問題,比如說我們工作中最常使用的飛書文檔,它的水印也做到了防刪除和防隱身,但卻沒有做到防覆蓋,導致我們只需要修改內(nèi)容區(qū)層級,就可以繞過水印了。
確實,通過修改內(nèi)容區(qū)的z-index也能達到繞過我們的水印直接截圖的目的,但這也難不倒聰明的前端工程師們。既然我們可以監(jiān)聽水印元素的屬性變化,監(jiān)聽內(nèi)容元素屬性變化也是如出一轍。我們只需要監(jiān)聽到調(diào)整z-index的變化時,始終確保水印元素的層級是最高的即可。
由于z-index屬性除了auto外,只能傳入整型,所以,它的最大值也就是整型的最大值,那么,我們可以調(diào)整一下我們水印的層級,讓它就等于整型的最大值。然后監(jiān)聽內(nèi)容區(qū)所有元素的屬性變化,只要修改了層級,并且層級大于或等于整型最大值的,就讓它的層級變成最大值減一即可。
圖片
Nice!這樣一來,無論你是改水印元素還是該內(nèi)容元素,水印都已經(jīng)死死地貼在了內(nèi)容上面了,怎么甩都甩不掉了。
防感知水?。篤5
通過上述一頓操作,其實我們的水印已經(jīng)算是相對安全了,但我們上面使用的水印,用戶還是能夠看的見的,有時還可能影響用戶查看一些內(nèi)容,我們有沒有辦法可以利用技術手段,讓我們?nèi)庋劭床怀鰜磉@個水印,但一旦出現(xiàn)數(shù)據(jù)泄密,我們拿到截圖后,又能夠迅速定位泄密者呢?
這就可以利用到「暗水印」技術了。暗水印是相對于「明水印」而言的說法,我們上面使用的就是明水印。所謂的暗水印,其實就是將我們的水印的透明度調(diào)到極低,讓用戶肉眼基本無法看到,這樣,用戶看不到水印,自然也不會想辦法破解,也為我們的水印再披上了一層隱身衣。但當我們需要的時候,又可以一些方式,讓他們顯現(xiàn)出原型。舉個例子,利用RGBA通道的特性,將R、G、B三個通道中的兩個通道設置清空掉(設置為 0 或 255),僅保留一個通道,這樣就可以將我們想要的信息提取出來。
圖片
上述代碼當中,我們的test.png是長這樣的:
圖片
看起來是不是就是一個無比正常并且沒有加水印的頁面呢?別急,我們把這張截圖使用decodeWatermark方法解密一下看看。
圖片
二、暗水印顯隱術與OCR的邂逅
上面,我們通過簡單的代碼示例,一步一步的對我們水印的安全性進行提升,可以說已經(jīng)是:“經(jīng)拉又經(jīng)拽,經(jīng)蹬又經(jīng)踹”了。就像前文所說的,水印最常使用的場景是數(shù)據(jù)安全與防護,但絕不是只能用于這個場景。
由于暗水印的優(yōu)勢在于,對于用戶來說全無感知,無論是B端應用,還是C端應用,都可以通過暗水印技術在在頁面上打印一些關鍵信息,例如在B端可以打印用戶id、當前頁面路由信息等,在C端可以打印當前用戶的id、設備關鍵信息、網(wǎng)絡狀況等可以輔助我們排查用戶遇到問題的一些信息。用戶反饋時,只需要提供頁面的截圖,而不需要用戶提交一堆跟技術相關的信息,我們就可以通過將暗水印解碼出來并獲取到這些信息,輔助我們快速定位用戶問題,提升用戶反饋體驗。
功能設計與架構
結合目前我們負責的會場搭建器的一些應用場景,想要在項目中利用「暗水印」技術為搭建器(一個用于快速搭建頁面的無代碼平臺)打上特定的標識,而這些標識,能夠幫我們迅速的定位目標會場(即頁面,后文如無特殊說明,會場均代表頁面)。我們的實際應用場景是這樣的:用戶在搭建會場的時候出現(xiàn)了某些問題,對會場搭建頁面的部分頁面進行截圖反饋,而相關開發(fā)人員可以僅通過用戶提供的截圖,通過將截圖中的暗水印顯示出來,我們的開發(fā)人員就可以通過暗水印中的會場相關信息,快速定位相關會場,排查問題。
當然,如果更進一步,我們還可以通過用戶上傳的截圖,在解碼出暗水印之后,使用「光學字符識別技術(OCR)」提取出我們關注的會場信息,進行自動搜索目標會場的操作。
圖片
方案分析與選擇
暗水印解碼方案
目前市面上針對暗水印的解碼方案大概有以下幾種:
- 利用【顏色通道屏蔽】的方式實現(xiàn)解碼(上面示例使用的就是這個方法)
圖片
- 利用【圖片二極化】的方式實現(xiàn)解碼,解碼出來的圖片只有黑白兩色
圖片
- 利用【混合模式疊加圖層蒙版】的方式實現(xiàn)解碼
圖片
從上面的截圖來看,好像三種方案都能達到暗水印解碼的目的,但實際上,前兩種情況太容易因為背景噪音而產(chǎn)生干擾了,如果所在頁面的背景顏色比較復雜,很容易影響暗水印識別的準確性。因此,我們最終選擇使用上述的第三種方案,即:利用【混合模式疊加圖層蒙版】的方式實現(xiàn)解碼。后文中也會對這個方案做詳細的介紹。
光學字符識別(OCR)方案
如果只有暗水印的話,每次我們都需要通過肉眼識別,如果字符不多還好,如果字符多了直接眼瞎,例如水印內(nèi)容是頁面 id 的場景。因此,我們我們還需要研究一下應該如何根據(jù)圖片提取相關的文字信息。
很多辦公、社交軟件,如:微信、QQ、飛書、釘釘?shù)龋加刑崛×奶靸?nèi)容中的圖片中的文本內(nèi)容的功能。由于我們公司使用飛書作為辦公平臺,因此,我第一時間就在想:“飛書會不會有提供相關的開放API給我們調(diào)用呢?”
- 一些開放的光學字符識別接口,如:飛書開放平臺的光學字符識別接口
圖片
圖片
- 純前端光學字符識別
開放接口雖好,但有各種限制,需要注冊應用,還可能限流以及產(chǎn)生費用等。因此,我們期望能有接入更加方便且成本更低的方式實現(xiàn)。因此在 Github 社區(qū)上一頓搜索,最后找到一個社區(qū)活躍且功能非常強大的前端OCR庫:tesseract.js 。
圖片
至此,截圖搜所依賴的的前置拼圖都已經(jīng)整理出來了,接下來就是將這些拼圖組合成完整的功能并對相應場景進行針對性的優(yōu)化。
四、OCR識別準確率提升
截圖搜功能有兩個核心模塊,第一個就是「暗水印解析模塊」,第二個就是「OCR 光學字符識別模塊」,這兩個模塊的執(zhí)行效果都會直接影響到我們最終識別結果的優(yōu)劣。因此,如果需要提升OCR識別的準確率,我們就需要分別從這兩個核心模塊上下功夫。
暗水印解析模塊
針對暗水印解析模塊,我們優(yōu)化提升的目標是在各種復雜的場景下,盡可能保證暗水印的清晰度,以此確保人眼識別和OCR識別的準確性。
首先,我們來看一下,我們之前的方案都有哪些問題:
- 在水印字體顏色與頁面背景顏色接近時暗水印無法辨識
圖片
- 原始頁面背景復雜時對水印文案的影響太大
圖片
以上兩個問題,都會嚴重影響我們識別水印中內(nèi)容的準確性,因此我們需要想辦法解決。
更換暗水印解碼方案
我們的原始方案采用的是:「圖片二極化處理」的方案,經(jīng)過大量的嘗試,發(fā)現(xiàn)這種方案沒辦法適應各種復雜場景和與水印顏色接近的背景的場景,因此,我們不得不嘗試探索其他的解決方案。
以下為這邊探索和思考目標解決方案的心路歷程:
1. 梳理要達成的目標:讓暗水印凸顯讓背景淡化;
2. 記得曾經(jīng)使用 PhotoShop 查看設計稿的時候,設計師們經(jīng)常使用類似這樣的一個技巧:在某些文字上面想要加入一些底紋和特效的時候,通常會在目標文字圖層之外再建一個新圖層,在這個圖層繪制目標底紋和特效,然后通過圖層蒙版的形式將這個底紋和特效附加到文字圖層上。我們是不是也可以利用類似這樣的思路,在我們用戶提供的截圖基礎上,疊加一個新的圖層,利用圖層的一些特定的顏色或圖案,讓我們的文字凸顯,讓背景淡化呢?
圖片
3. 嘗試在搜索引擎和 GPT 中搜索瀏覽其中 js 可以使用的一些滿足上面條件的 API,最后,在 MDN 文檔中找到了以下文檔參考:
- 組合 Compositing - Web API | MDN
- CanvasRenderingContext2D.globalCompositeOperation - Web API | MDN
4. 利用 canvas 對于混合模式的支持,似乎可以實現(xiàn)我們想要的效果,寫個 demo 實驗一下。
圖片
5. 嘗試一下上述代碼的解碼效果
圖片
暗水印被完美解碼出來了。至此,敲定使用這個方案替換原本的二極化解碼方案。
原圖色調(diào)選擇
在嘗試上面的混合模式方案時,有一點需要注意的。由于我們選擇的混合模式是:overlay
圖片
也就是讓暗的更暗,亮的更亮。上面我們用的原始圖片是偏亮色色調(diào)的,我們使用 #000 作為蒙層的色調(diào)可以讓原本白色的水印更加凸顯,但如果我們原本的圖片本身是暗色色調(diào)的呢?我們使用 #000 會不會有問題呢?我們直接來試一下:
原圖:
圖片
解碼圖:
圖片
上面的示例中,我們選擇的模式是亮色背景的模式,但我們卻傳了一個暗色背景的圖片,我們可以發(fā)現(xiàn),完全看不見水印文字。這是因為此時,我們的水印文字已經(jīng)由于跟蒙層背景融合的原因,導致跟背景色完全融合在一起了,所以我們看不見。
為了解決這個問題,我們可以在暗色背景是使用#fff作為蒙層的底紋色,這樣,就可以讓我們混合了蒙層顏色的水印凸顯出來了:
圖片
因此,我們需要改造一下上述的解碼方法,讓用戶可以根據(jù)需要自行選擇亮暗色調(diào),以適應不同主色調(diào)的場景:
圖片
當然,其實我們是有一些手段可以自動識別出當前圖片的主色調(diào)的,利用自動識別的主色調(diào)進行自動匹配模式,就不需要用戶自己選擇了。不過目前因為我們C端頁面大部分都是亮色的,實現(xiàn)這個功能必要性不大,暫且默認亮色,如果確實有需要自行切換模式即可。如果以后我們的頁面支持暗色調(diào)頁面,例如頁面色調(diào)隨系統(tǒng)變化時,我們就可以采用上述方案自動識別主色調(diào)。
自定義調(diào)節(jié)暗水印對比度
上面給出的代碼中可以看到,我們在疊加蒙版的時候,這邊使用了 for 循環(huán)疊加了4次,那么,究竟疊加多少次比較合適呢?一定要固定嗎?
實則不然,我們其實可以讓用戶自行選擇我們疊加的蒙版的層數(shù),這樣用戶可以根據(jù)自己的需求通過調(diào)整疊加蒙版的層數(shù)來微調(diào)暗水印跟背景的對比度。
舉個例子,當我們只選擇疊加4層蒙版時,我們水印看起來非常淡,基本看不清楚(1~3的時候幾乎看不怎么出來,這里就不演示了):
圖片
而當我們將蒙層疊加數(shù)量調(diào)整到最高等級8時:
圖片
水印的內(nèi)容就已經(jīng)相當明顯了。
那么,是不是我們疊加蒙層的數(shù)量越多越好呢?其實也不是,如果我們的背景比較復雜的時候,如果蒙層疊加較多,可能會導致我們的水印文字與原圖背景顏色過于接近而導致無法區(qū)分,因此,我們選擇默認使用一個中間值,即默認疊加4層蒙版,如果實際效果覺得太淡,我們可以讓使用者自行調(diào)整,這樣就可以更好的適應各種情況了。
圖片
優(yōu)化至此,我們已經(jīng)可以比較穩(wěn)定的解析出無論是人眼還是OCR辨識度較高的包含暗水印的圖片了。接下來,如果我們還想繼續(xù)提升OCR模塊識別的準確性,就要從OCR引擎上下功夫了。
OCR光學字符識別模塊
同樣的,在進行優(yōu)化之前,我們首先要明確OCR模塊的瓶頸究竟在哪里,我們才能針對瓶頸點進行針對性優(yōu)化。
經(jīng)過分析,總結出主要有以下三個瓶頸:
- 原始圖片中的水印受到背景噪音干擾容易混淆(這個我們通過對暗水印解析模塊的優(yōu)化其實已經(jīng)解決了絕大部分場景的問題了)。
- 圖片中內(nèi)容過多或體積過大,不僅會對識別準確性帶來干擾,還影響OCR識別的速度。
- OCR針對相同的輸入得到結果的準確性。
由于上述的第一個問題我們上面已經(jīng)通過對暗水印解析模塊的優(yōu)化解決了,在此,我們就針對后兩點聊一下應該如何優(yōu)化。
選區(qū)識別解決背景噪音問題
既然問題是出在圖片內(nèi)容過多或體積過大上,那么我們就針對性的解決這個問題就可以了。那么,我們究竟應該選擇如何減少圖片內(nèi)部的無關內(nèi)容呢?
之前使用的二極化方案,可以將圖片中很多復雜的像素點都變成純粹的黑白色,從而減少一些干擾點,但也僅僅只是減少色調(diào),如果圖片中圖形過于復雜,還是會造成極大的干擾,如:
圖片
經(jīng)過一番嘗試,最終還是放棄了在二極化這條道路上一路走到黑的想法。而是選擇了另一種方案,那就是參考飛書中的OCR功能,由人眼辨別圖片當中那一塊區(qū)域是干擾項最少的、最容易識別的區(qū)域,然后人工劃定選取選擇目標區(qū)域,而我們OCR識別的時候,也只需要識別選出來的選區(qū)中的內(nèi)容即可,極大地降低了因圖片背景噪音帶來的識別準確率降低的可能性:
圖片
決定了使用這個方案之后,又有一個問題讓我犯難了,在一張圖片中劃定選取裁剪這個功能,說簡單也簡單,說復雜其實也挺復雜的,本著能不重復造輪子就不重復造輪子的思想,這邊在網(wǎng)上一頓搜索,看是否有滿足我需求的第三方庫可以直接使用,最后,在npm上找到了:react-image-crop 這個庫,試了一下,使用方便,而且完全可以滿足我的需求,因此就直接安排上了。
圖片
使用起來也簡單,只需要用提供的React組件包裹待裁剪的組件即可:
圖片
我們只需要在onComplete的回調(diào)當中,監(jiān)聽當前選中的區(qū)域和位置,然后將選中區(qū)域的圖片裁剪下來:
圖片
通過上面的代碼處理,我們將會得到一個僅包含選區(qū)內(nèi)容的圖片base64,類似以下的圖片:
圖片
這樣,就直接解決了圖片過大和圖片背景噪音過多造成的干擾了。
替換 OCR 識別方案
背景噪音問題解決之后,我們再來看一下,能否進一步的提升OCR的識別準確性,這邊調(diào)研了三種方案:
- (上一版本采用此方案)使用 tesseract.js npm 包進行本地識別
優(yōu)勢
- 純前端,不需要依賴外部服務
- 免費
- 接入簡單
- 支持指定選取識別
劣勢
整體體驗下來識別精準度不高,肉眼幾乎很明顯的文字,容易識別錯誤
- 飛書的OCR API
識別準確率高
溝通成本比較大,需要聯(lián)系公司商務跟飛書方談
可能會產(chǎn)生一定的接口訪問費用
接入比較麻煩,應該要注冊飛書開發(fā)者平臺獲取 token
優(yōu)勢
劣勢
- 公司算法中心的通用OCR服務
優(yōu)勢
- 公司內(nèi)部接口,如果有定制化需求可以聯(lián)系提供方定制開發(fā)
- 接入簡單
- 在測試環(huán)境測試了一下,識別成功率挺高的,比npm包高
- 在支持了劃定選取截圖識別的情況下,由于圖片體積整體比較小,識別速度反而比本地的npm包識別速度更快
劣勢
目前測試環(huán)境接口好像不是很穩(wěn)定,能識別出來的時候準確率很高,但有些時候,相同的輸出,返回的結果卻是空的。
由于使用本地npm識別準確率太低,而識別準確率最高的飛書API由于相關成本過高也只能無奈放棄,因此,這邊最終選擇主要使用第三個方案,即使用公司內(nèi)部算法中心提供的通用OCR服務。
從測試接口看起來,這個服務的準確率還是可以的:
圖片
因此,我們最終選擇以內(nèi)部OCR識別能力為主,tesseract.js 本地識別為輔的方案,盡可能的提供更加穩(wěn)定且準確的OCR識別服務。
至此,我們本次針對OCR識別準確率的專項優(yōu)化就算是完成了。
五、最后
到這里,我們本次針對截圖搜功能的設計與識別準確率、體驗優(yōu)化就算是大功告成了,一個看似簡單的水印功能,如果我們使用得當,研究深入,或許也能夠在不同的應用場景當中產(chǎn)生不同的價值。個人認為,我們自己在開發(fā)過程中,別小瞧了某一些看似非常簡單的功能,實際上它們或許是外表包著石皮的璞玉呢?在業(yè)務開發(fā)過程中,多一些對于技術的思考,對于業(yè)務的反思,或許能為我們展現(xiàn)出完全不一樣的天地。