基於html2canvas實現網頁儲存為圖片及圖片清晰度優化

小小云朵發表於2017-10-09

本次技術調研來源於H5專案中的一個重要功能需求:實現微信長按網頁儲存為截圖

這裡有個栗子(請用微信開啟,長按圖片即可儲存):3分鐘探索你的知識邊界

將整個網頁儲存為圖片是一個十分有趣的功能,常見於H5活動頁的結尾頁分享。以下則是專案中調研和踩坑的一些小結和彙總。


一、實現HTML頁面儲存為圖片

1.1 已知可行方案

現有已知能夠實現網頁儲存為圖片的方案包括:

  • 方案1:將DOM改寫為canvas,然後利用canvas的toDataURL方法實現將DOM輸出為包含圖片展示的data URI
  • 方案2:使用html2canvas.js實現(可選搭配Canvas2Image.js實現網頁儲存為圖片)
  • 方案3:使用rasterizeHTML.js實現

1.2 解決方案的選擇

  • 方案1:需要手動計算每個DOM元素的Computed Style,然後需要計算好元素在canvas的大小位置等屬性。

    方案1難點

    1. 相當於完全重寫了整個頁面的佈局樣式,增加了工作量。
    2. 由於canvas中沒有的物件概念,對於元素豐富、佈局複雜的頁面,不易重構。
    3. 所有DOM元素改寫進canvas會帶來一些困難,例如:難以支援響應式,圖片元素清晰度不佳和文字點選區域識別問題等。
  • 方案2:該類功能中Github上stars最多(至今仍在維護),Stack Overflow亦有豐富的討論。只需簡單呼叫html2canvas方法並設定配置項即可。
  • 方案3:該方案的限制較多,目前僅支援3類可轉為canvas的目標格式: 頁面url,html字串和document物件。

小結: html2canvas是目前實現網頁儲存為圖片功能的綜合最佳選擇。

1.3 html2canvas的使用方法

官方GitHub:https://github.com/niklasvh/h…

以下描述針對html2canvas版本是0.5.0-beta4

1.3.1 實現儲存為圖片的第一步:html轉為canvas

基於html2canvas.js可將一個元素渲染為canvas,只需要簡單的呼叫html2canvas(element[, options]);即可。下列html2canvas方法會返回一個包含有<canvas>元素的promise

html2canvas(document.body).then(function(canvas) {
    document.body.appendChild(canvas);
});

1.3.2 實現儲存為圖片的第二步:canvas轉image

上一步生成的canvas即為包含目標元素的<canvas>元素物件。實現儲存圖片的目標只需要將canvas轉image即可。

這裡的轉換方案有2種

  • 方案1:基於原生canvas的toDataURL方法將canvas輸出為data: URI型別的圖片地址,再將該圖片地址賦值給<image>元素的src屬性即可
  • 方案2:使用第三方庫Canvas2Image.js,呼叫其convertToImage方法即可(GitHub

實際上,Canvas2Image.js也是基於canvas.toDataURL的封裝,相比原生的canvas API對於轉為圖片的功能上考慮更為具體(未壓縮的包大小為7.4KB),適合專案使用。

二、生成圖片的清晰度優化方案

2.1 基礎的清晰度優化方案

最終圖片的清晰度取決於第一步中html轉換成的canvas的清晰度。

現有解決方案參考;

基本原理為:
canvas的屬性widthheight屬性放大為2倍(或者設定為devicePixelRatio倍),最後將canvas的CSS樣式width和height設定為原先1倍的大小。

例如:希望在html中實際顯示的<canvas>寬高分別為160px,90px則可作如下設定

<canvas width="320" height="180" style="width:160px;height:90px;"></canvas>

參考上述文件具體的使用案例如下;

convert2canvas() {

    var shareContent = YourTargetElem; 
    var width = shareContent.offsetWidth; 
    var height = shareContent.offsetHeight; 
    var canvas = document.createElement("canvas"); 
    var scale = 2; 

    canvas.width = width * scale; 
    canvas.height = height * scale; 
    canvas.getContext("2d").scale(scale, scale); 

    var opts = {
        scale: scale, 
        canvas: canvas, 
        logging: true, 
        width: width, 
        height: height 
    };
    html2canvas(shareContent, opts).then(function (canvas) {
        var context = canvas.getContext(`2d`);

        var img = Canvas2Image.convertToImage(canvas, canvas.width, canvas.height);

        document.body.appendChild(img);
        $(img).css({
            "width": canvas.width / 2 + "px",
            "height": canvas.height / 2 + "px",
        })
    });
}

2.2 進階的清晰度優化方案

上述設定可以解決通常情況下圖片不清晰的問題,不過探索並沒有結束。

實際在我們的專案中,即使作出2.1節的設定後,大果粒一般的渲染結果依然尷尬。

下面直接給出3條進一步的優化策略:

  1. 更改百分比佈局px佈局(如果原先是百分比佈局的話)
  2. 關閉canvas預設的抗鋸齒設
  3. 設定模糊元素的widthheight為素材原有寬高,然後通過transform: scale進行縮放。這裡scale的數值由具體需求決定。

基本原理

  1. 如果原來使用百分比設定元素寬高,請更改為px為單位的寬高,避免樣式二次計算導致的模糊
  2. 預設情況下,canvas的抗鋸齒是開啟的,需要關閉抗鋸齒來實現影像的銳化(MDN: imageSmoothingEnabled )
  3. 除了canvas可以通過擴大2倍寬高然後縮放至原有寬高來提高清晰度,對於DOM中其他的元素也可以使用css樣式scale來實現同樣的縮放

例: html2canvas配置

convert2canvas() {

    var cntElem = $(`#j-sec-end`)[0];

    var shareContent = cntElem;//需要截圖的包裹的(原生的)DOM 物件
    var width = shareContent.offsetWidth; //獲取dom 寬度
    var height = shareContent.offsetHeight; //獲取dom 高度
    var canvas = document.createElement("canvas"); //建立一個canvas節點
    var scale = 2; //定義任意放大倍數 支援小數
    canvas.width = width * scale; //定義canvas 寬度 * 縮放
    canvas.height = height * scale; //定義canvas高度 *縮放
    canvas.getContext("2d").scale(scale, scale); //獲取context,設定scale 
    var opts = {
        scale: scale, // 新增的scale 引數
        canvas: canvas, //自定義 canvas
        // logging: true, //日誌開關,便於檢視html2canvas的內部執行流程
        width: width, //dom 原始寬度
        height: height,
        useCORS: true // 【重要】開啟跨域配置
    };

    html2canvas(shareContent, opts).then(function (canvas) {

        var context = canvas.getContext(`2d`);
        // 【重要】關閉抗鋸齒
        context.mozImageSmoothingEnabled = false;
        context.webkitImageSmoothingEnabled = false;
        context.msImageSmoothingEnabled = false;
        context.imageSmoothingEnabled = false;
        
        // 【重要】預設轉化的格式為png,也可設定為其他格式
        var img = Canvas2Image.convertToJPEG(canvas, canvas.width, canvas.height);

        document.body.appendChild(img);

        $(img).css({
            "width": canvas.width / 2 + "px",
            "height": canvas.height / 2 + "px",
        }).addClass(`f-full`);

    });
}

例: DOM元素樣式:


.targetElem {width: 54px;height: 142px;margin-top:2px;margin-left:17px;transform: scale(0.5)}

三、含有跨域圖片的配置

由於canvas對於圖片資源的同源限制,如果畫布中包含跨域的圖片資源則會汙染畫布,造成生成圖片樣式混亂或者html2canvas方法不執行等問題。

以下主要解決兩類跨域的圖片資源:包括已配置過CORS的CDN中的圖片資源和微信使用者頭像圖片資源。

3.1 針對CDN中的圖片的配置

  1. 要求CDN的圖片配置好CORSCDN配置好後,通過chrome開發者工具可以看到響應頭中應含有Access-Control-Allow-Origin的欄位。
  2. 開啟html2canvasuseCORS配置項。即作如下設定:

var opts = {useCORS: true};

html2canvas(element, opts);

注意
如果沒有開啟html2canvasuseCORS配置項,html2canvas會正常執行且不會報錯,但是不會輸出對應的CDN圖片
(已測試同時包含CDN的圖片本地圖片的資源的頁面,但是隻有本地圖片能夠被正常渲染出來)

3.2 針對微信使用者頭像的配置

如果需要將微信平臺中的使用者頭像一併儲存為圖片,3.1的方案無能為力。可通過配置服務端代理轉發(forward)實現,此處不贅述。

其他注意事項

1. margin的遮擋問題

微信中,喚出長按儲存圖片的選單要求長按的物件直接是<image>元素,如果<image>元素上方存在遮擋,則不會喚出選單。
而事實上,引發遮擋的並不只是非<image>元素,還可能是margin屬性。例如:若在頁面底部,對一個絕對定位的元素設定了數值很大的margin-top,則margin-top所涉及的區域,均無法長按喚出選單。解決方案:將margin-top改用為top即可。

2. 安卓版微信儲存圖片失敗的問題

canvas2img預設儲存圖片的格式為png,而在安卓版微信中所生成的圖片儘管能長按喚出儲存圖片的選單,但是無法正確儲存到本地相簿解決方案:設定canvas2img的生成圖片格式配置項為jpeg即可。

3. JPEG的黑屏問題

設定canvas2img輸出格式為jpeg,會有一定機率導致生成的圖片包含大量的黑色塊。可能的解決方案:縮減部分圖片元素的體積和尺寸大小。

4. 不能保留動效

在圖片的轉化前,必須停止或者刪除動效後才能正確渲染出圖片,否則生成的圖片是破裂的。

參考文獻

相關文章