[小菜一碟] 在前端使用 JavaScript 操作 Canvas 來合併 SVG(Scalable Vector Graphics)圖片

前一篇文章介紹了用 Canvas 來合併、縮放、裁切圖片,文章裡面範例的圖片來源是 HTMLImageElement,這天我們美編丟了兩段 SVG(Scalable Vector Graphics)格式的 HTML,我就想說都是圖片應該都一樣吧,依樣畫葫蘆想合併這兩張圖片,結果…

瀏覽器直接噴錯:

Uncaught TypeError: Failed to execute 'drawImage' on 'CanvasRenderingContext2D': The provided value is not of type '(CSSImageValue or HTMLCanvasElement or HTMLImageElement or HTMLVideoElement or ImageBitmap or OffscreenCanvas or SVGImageElement or VideoFrame)'.

錯誤訊息很清楚,告訴我們 drawImage() 方法的圖片來源只支援括號中的這些元件,但是 SVG 不在列,因為 SVG 的內容其實是繪圖手法的描述資訊,是圖片產生的過程,而不是圖片產生的結果,所以 drawImage() 不支援也是合理的。

既然這樣,我們就把 SVG 輸出成圖片,不就可以丟給 drawImage() 處理了嗎?於是乎,我開始爬文研究如何把 SVG 輸入成 drawImage() 可以支援的圖片來源,結果意外地容易,大致上的邏輯如下:

  1. 把 SVG 的內容轉成 Blob,類型為 image/svg+xml
  2. 利用轉好的 Blob 建立一個 Object URL
  3. 將建好的 Object URL 指定給 <img>src 屬性
  4. 最後再將 <img> 顯示的圖片合併起來

程式碼如下,相關邏輯也有寫在程式碼的註解中。

<p>
    <svg id="bigger"><!-- ...略... --></svg>
    <svg id="smaller"><!-- ...略... --></svg>
    <button id="merge">Merge</button>
    <canvas id="result"></canvas>
</p>
<script>
    if (!Object.prototype.ppap) {
        Object.defineProperty(Object.prototype, "ppap", {
            value: function (name, value) {
                if (typeof name === "function") {
                    name(this);
                } else {
                    this[name] = value;
                }

                return this;
            }
        });
    }

    document.querySelector("#merge").addEventListener("click", function (e) {
        // 1. 把 SVG 的內容轉成 Blob,類型為 image/svg+xml。
        // 2. 利用轉好的 Blob 建立一個 Object URL
        const biggerImgUrl = URL.createObjectURL(new Blob([document.querySelector("#bigger").outerHTML], { type: "image/svg+xml" }));
        const smallerImgUrl = URL.createObjectURL(new Blob([document.querySelector("#smaller").outerHTML], { type: "image/svg+xml" }));

        // 此處需加入 ppap 擴充方法
        const biggerImg = document.createElement("img").ppap("name", "biggerImg");
        const smallerImg = document.createElement("img").ppap("name", "smallerImg");

        // 4. 最後再將 <img> 顯示的圖片合併起來
        const merge = () => {
            const canvas = document.querySelector("#result");
            const canvasCtx = canvas.getContext("2d");

            // Canvas 預設大小是 300×150,配合繪製圖片大小,調整 Canvas 的大小。
            canvas.width = biggerImg.width;
            canvas.height = biggerImg.height;

            canvasCtx.drawImage(biggerImg, 0, 0);
            canvasCtx.drawImage(smallerImg, (biggerImg.width / 2) - (smallerImg.width / 2), (biggerImg.height / 2) - (smallerImg.height / 2));
        }

        // 建立 img onload 的 callback 方法
        const imgLoaded = (() => {
            let biggerImgLoaded = false;
            let smallerImgLoaded = false;

            return (e) => {
                if (e.target.name === "biggerImg") {
                    biggerImgLoaded = true;
                    URL.revokeObjectURL(biggerImgUrl);
                }
                if (e.target.name === "smallerImg") {
                    smallerImgLoaded = true;
                    URL.revokeObjectURL(smallerImgUrl);
                }

                if (biggerImgLoaded && smallerImgLoaded) {
                    merge();
                }
            }
        })();

        biggerImg.onload = imgLoaded;
        smallerImg.onload = imgLoaded;

        // 3. 將建好的 Object URL 指定給 <img> 的 src 屬性
        biggerImg.src = biggerImgUrl;
        smallerImg.src = smallerImgUrl;
    });
</script>

雖然這只是一個小小的需求,但是過程當中還是有一些知識點需要掌握,包括 SVG、Blob、Object URL、...等,要有一定的了解,以上,一個簡單的 SVG 圖片合併的做法分享給大家,希望對有類似需求的朋友,有一點幫助。

參考資料

相關資源

C# 指南
ASP.NET 教學
ASP.NET MVC 指引
Azure SQL Database 教學
SQL Server 教學
Xamarin.Forms 教學