簡單的網頁截圖功能 - html2canvas、Canvas2image
Tags: javascript, html2canvas, tutorial, webdev
Published: 2022-09-16 12:28:26

## 本篇要解決的問題

幾年前公司有一個案子，是要讓使用者可以做一個簡單的卡片，然後把這卡片存成圖檔分享給朋友，那時就有寫過一次把指定的 `<div>` 存成圖檔的 JS，結果，案子太久遠了，竟然沒把當時寫存圖檔的 function 給存下來 XD，直到前陣子前同事在問才想起這件事。

把網頁的區塊存成圖檔，總覺得以後有機會再用到，就先整理這篇筆記文，以後真的要用的時候就可以直接 ~~複製貼上~~ 複習一下怎麼寫。

要把頁面存成圖檔有二個步驟，各自用了一個套件。

1. 把指定的 HTML 區塊轉成 Canvas：[html2canvas](https://html2canvas.hertzen.com/)
2. 把 Canvas 轉存成圖檔：[Canvas2image](https://github.com/hongru/canvas2image)

本篇有做一個 Demo 出來，在繼續往下閱讀前大家可以先玩玩看：

<https://letswritetw.github.io/letswrite-web-to-image/>

---

## 將指定區塊轉成 Canvas

[html2canvas](https://html2canvas.hertzen.com/) 這個套件很方便，基本上官方文件上就明明白白的寫了使用方法：

```javascript
const el = document.querySelector("#something");
html2canvas(el, [option])
  .then(canvas => {
    document.body.appendChild(canvas)
  });
```

`option` 是選填，可寫可不寫，項目請參考官方文件的 [Configuration](https://html2canvas.hertzen.com/configuration)，如果去爬一些教學文，會看見最常寫到的就是 `useCORS: true`。

`useCORS: true` 是在我們要抓圖的 div 裡如果存在其它網域的圖片，就必須告訴 html2canvas 說我們有跨網域的圖片。

啊不過，經 August 人體實驗證明，當有圖片是跨網域的來源，很常會抓圖失敗，失敗機率是 3 張失敗 1 ~ 2 張，如果真要用這套把 div 轉成 Canvas，div 裡的圖要用同網域的會比較保險。

在本篇的範例裡，除了 `useCORS`，也有用到 `backgroundColor`，直接把空白的地方填入網頁裡的背景色，看上去才會無違和。

如果要好玩一點，也可以背後再接一個 [Color Thief](https://www.letswrite.tw/color-thief/) 抓出圖檔的主要色系當背景色，就不用寫死背景色的值。

---

## 把 Canvas 轉存成圖檔

[Canvas2image](https://github.com/hongru/canvas2image) 這套很妙，一開始 August 在寫 Demo 時，JS 是引用 CDN 上的絕對路徑，然後一直無法自訂圖檔的檔名，可是看官方的原始碼裡又確實有參數是讓人放檔名的。

如此如此，這般這般，就決定直接從官方 GitHub 上下載 JS 檔來用，然後果不其然就直接成功設定圖檔檔名勒。

只是官方給的 JS 要用 `import` 才能使用，如果對 `import` 還不熟的朋友，可以考慮下載 August 前陣子寫的 [開發初始檔](https://www.letswrite.tw/vue-tailwind-esbuild-rollup/)，就能直接編譯 ES6 的 JS 檔。

Canvas2image 的使用方式很簡單：

```javascript
Canvas2Image.saveAsImage(canvasObj, width, height, type, filename)
Canvas2Image.saveAsPNG(canvasObj, width, height, filename)
Canvas2Image.saveAsJPEG(canvasObj, width, height, filename)
Canvas2Image.saveAsGIF(canvasObj, width, height, filename)
Canvas2Image.saveAsBMP(canvasObj, width, height, filename)
```

官方的說明文件沒有寫上 `filename` 這個參數，應該是漏了寫，因為 August 去看了 JS 原始碼是有這個參數的。

`canvasObj` 就直接寫我們從 **html2canvas** 取得的 `canvas` 就行。

`width`、`height` 這二個的值 canvas 本身就會有，如果沒有其它需求可以直接寫 `canvas.width`、`canvas.height`。

第一個的 `saveAsImage` 需要多帶 `type` 參數，寫上要什麼檔案類型就行，比方 `png`、`jpeg`。不過建議就直接用後面四個 function 吧，不讓使用者選直接存成我們希望使用者擁有的類型省事多了。

總合上面二個步驟，程式碼簡單整理如下。

```javascript
// 將指定區塊轉成 Canvas
function htmlToCanvas(id) {
  const el = document.getElementById(id);
  html2canvas(el).then(canvas => {
    canvasToImage(canvas);
  });
}

// 把 Canvas 轉存成圖檔
function canvasToImage(canvas) {
  const filename = 'xxxxx';
  Canvas2Image.saveAsPNG(
    canvas,
    canvas.width, canvas.height,
    filename
  );
}
```

---

## 截多個 div 存成圖

假設今天我們要截的圖是頁面上幾個 div 併在一起的，方法很簡單，在本篇的 Demo 裡就有寫出來，那個「截圖 Section 1 + 2」的按鈕就是在示範這件事。

Demo 中有二個 sectoin，分別是 `<section id="section1">`、`<section id="section2">`。

我們的目標是一個按鈕，存出來的圖片包含這二個 section。

首先，建立第三個 section，隨意取叫 `<section id="section3">`，然後用 JS 把 1、2 二個 section 的 HTML 塞進去。

塞進 3 後，就可以用 html2canvas 來指定 3 轉成 Canvas。

這樣會遇到一個小狀況，就是 html2canvas 要的是真的要存在頁面上的區塊才行，而當我們把 1、2 的內容都塞到 3 後，3 就會有內容，就會被使用者看到。

把 3 寫上 `display: none` 是不行的，因為在網頁上就不會有寬高，會被認定是圖片不存在，就無法透過 html2canvas 轉成 Canvas。

山不轉路轉，藏起來不行，那我們就把它移到畫面之外，讓使用者在螢幕上看不到就行。

August 在 Demo 裡的作法如下：

```css
html, body {
  width: 100;
  overflow-x: hidden;
}
#section3 {
  position: absolute;
  left: -100%;
}
```

這樣，就可以順利截圖，而使用者是看不到那個被我們偷偷塞了內容的 section3 的。

---

## 範例及原始碼

原始碼存在 GitHub 上，也用了 GitHub Pages 生成 Demo 頁。

要取用前麻煩分享本篇，或是 GitHub 上點個星星，你的小小動作對本站都是大大的鼓勵。

原始碼：<https://github.com/letswritetw/letswrite-web-to-image>

Demo：<https://letswritetw.github.io/letswrite-web-to-image/>