canvas 圖片、文字模糊問題

lzxxxxxxxxx發表於2018-05-03

注:[n]標識為遺留問題,在文章末尾遺留問題部分有詳細解釋說明。

之前做了一個線上給圖片新增文字框的工具,大體思路是先把圖片載入到一個 DOM 結構中,然後通過 html2canvas 匯出到一個canvas,最後通過 canvas 自帶的 toDataURL 方法匯出成圖片。

這個思路並不複雜,但是中間遇到幾個小問題:

  1. 跨域圖片的匯出問題:你可以把圖片繪製到 canvas 中,但是不能做任何有關匯出資料的操作(比如 toDataURL ),因為 canvas 認為它自己是被汙染(tainted)的。(當然本地上傳的圖片是不存在這個問題的)

    This protects users from having private data exposed by using images to pull information from remote web sites without permission.

    ——出自 canvas-todataurl-securityerror

    大概意思是說,這樣可以保護使用者隱私資料不被暴露。

  2. 在 retina 螢幕上canvas 的內容顯示變模糊。

  3. 圖片模糊就算了,為什麼fillText輸入的文字也會模糊?而且匯出來會清晰一點(但是還是模糊)

解決過程:

  1. 第一個問題其實就是解決我們熟悉的跨域問題。這個工具的主要使用場景是在海外的 i8n 專案,圖片一般放在海外的圖片伺服器上。我給圖片新增 crossorigin:anonymous 不生效,所以決定換條路。

    既然傳統的跨域用法是失敗的,但是我們知道 <img>src 屬性可以用 base64編碼後的資料表示圖片的內容,這樣不會存在跨域問題。所以我想用 FileReader 轉換圖片格式。但是後來才發現 FileReader 同樣不允許處理跨域資源…計劃泡湯。

    然後發現這麼個工具CORS Anywhere,是給你的請求頭部加 CORS header 的。這樣一來應該可以解決跨域問題。(未具體嘗試)

  2. 這個問題才是今天想講的主題。

    先把網上的解決方法貼出來:

    devicePixelRatio = window.devicePixelRatio || 1,
    backingStoreRatio = context.webkitBackingStorePixelRatio || 1,
    ratio = devicePixelRatio / backingStoreRatio;
    
    var w = $("#code").width();
    var h = $("#code").height();
    
    //要將 canvas 的寬高設定成容器寬高的 2 倍
    var canvas = document.createElement("canvas");
    canvas.width = w * ratio;
    canvas.height = h * ratio;
    canvas.style.width = w + "px";
    canvas.style.height = h + "px";
    var context = canvas.getContext("2d");
    //然後將畫布縮放,將影象放大兩倍畫到畫布上
    context.scale(ratio,ratio);
    複製程式碼

    上面的程式碼我們分兩部分看,先忽略上面定義 ratio 值的部分,往下看。 說明一下,canvas 的屬性 width/height和樣式表裡指定的寬高不同,前者確定了這個畫布的內容大小,而後者只是顯示上的大小。所以上面程式碼就不難理解了,其實是把畫布的內容高寬放大二倍,而樣式上不變,視覺上就會變得精細很多,和二倍圖的原理基本上是類似的。

    道理我都懂,但是程式碼開頭那一大堆在算什麼?

    按照上面的邏輯來說,我們只需要通過 devicePixelRatio 判斷裝置是不是 retina 螢幕(不嚴格地說)就可以了。為什麼要算他和backingStoreRatio的比值,這又是個什麼東西?

    我們在往 canvas 裡畫任何東西的時候,實際上瀏覽器都在把這些寫到了一個後備儲存空間裡。瀏覽器在重新繪製到螢幕時候,資料就是來自這裡。webkitBackingStorePixelRatio這個值告訴我們的是後備空間相對 canvas 本身容量的大小。

    現在我們知道了這個值的作用,它是如何控制展示的?

    1x1x1

    上圖展示的是 dpr:bk === 1 的情況,就像沒有出現 retina 螢幕這件事一樣,匯出和匯入兩不相干。 關鍵是兩者值都為2的時候也是如此。所以即使是在 retina 螢幕上,也有可能不做多餘的程式碼處理圖片也可以很清楚。這也是為什麼我們說計算 ratio 的值時我們要算二者的比值而不是單純用 dpr。 而且這兩個更多時候確實沒有任何關係,並不是 dpr 為2 bk 的值就也一定高。

    1x1x1
    dpr:bk === 2問題出現了。我們原樣把圖片放進來,canvas 因為 bk 值為1所以沒有對圖片做其他處理,再展示到頁面上的時候就會模糊。這其實跟一般的圖片在 retina 螢幕上模糊的原因相同。

    比如我們有一個長寬都為30px的圖,放到 retina 螢幕上佔有 30 csspx 的寬度,但是實際上填充他寬度的有60個物理畫素。我們的圖片只提供了30個已知的畫素值,其餘的30個只能靠瀏覽器根據周圍的畫素點去計算。所以會模糊。

  3. 下面來討論為什麼文字模糊的問題。 剛開始看到文字模糊的時候覺得沒什麼難理解的,明顯是和圖片一個套路。但是細想覺得不對,圖片是因為在 dpr 為2的情況下,圖片內容寬和圖片樣式寬卻是相等的所以模糊。但是文字在我打到頁面上到畫到 canvas 的過程中,實際畫素數是足夠的,為什麼會模糊?

    在查了部分資料之後發現,在頁面上字型的展示和在 Canvas 裡 用fillText 去繪製文字是不一樣的,後者其實是在 canvas 裡「畫」字,而這個畫的結果的展示單元和上面圖片是一樣的,到現在為止我們可以把這個過程和圖片展示想成相同的了。

    至於為什麼下載後會清楚一些但是卻不「那麼清楚」,我們當做兩個問題來解答。 為什麼會清楚一些?因為模糊實際上是瀏覽器渲染時候的行為,下載之後檢視圖片是沒有這個畫素估算的過程的。 為什麼卻不那麼清楚?詳細的我不想講了,具體的可以看這個回答

遺留問題: [1]: 傳送的 file 協議的請求到伺服器端判斷跨域的時候和 http 是一樣的標準嗎?我個人覺得其實應該是的,因為同源策略本身的目的就是出於安全,這一點和你客戶端的協議其實是沒關係的。

參考文章:

High DPI Canvas

裝置畫素,裝置獨立畫素,CSS畫素

Canvas text rendering (blurry)

相關文章