最近重操舊業,想將網站上的技術系列文章轉成 ePub 電子書

電子書的圖檔部分我喜歡將<img>Data URI 內嵌簡化管理,方法不難,用一小段 JavaScript 透過 Canvas 可輕易實現:

document.querySelectorAll('.article_content img').forEach(img => {
    const canvas = document.createElement('canvas');
    const ctx = canvas.getContext('2d');
    const image = new Image();
    image.src = img.src;
    image.crossOrigin = 'Anonymous';
    image.onload = () => {
        canvas.width = image.width;
        canvas.height = image.height;
        ctx.drawImage(image, 0, 0, image.width, image.height);
        img.src = canvas.toDataURL();
    };
});

但老鳥都知道事實沒這麼簡單,遇上圖檔來自第三方網站或自家圖床伺服器,當圖檔與網頁 HTML 分屬不同站台,便會撞上 Canvas 的 Origin-Clean 安全原則這道高牆。

這個問題去年遇過也解決了,當時我寫了 ASP.NET Core Minimal API 負責轉換以克服限制。幾個月前認識輕巧快速的 Go 語言,覺得這個小題目超適合當成 Go 的自主練習題。沒想太多,動手寫下去就對。

簡單拼湊了一個可用版本(程式隨小,記得要限定本機存取及圖檔型別,這類代理程式要留意被當跳板的資安風險),約 60 行搞定。

package main

import (
	"encoding/base64"
	"fmt"
	"io"
	"log"
	"net/http"
	"os"
	"strconv"
)

func main() {
	http.HandleFunc("/", imgBase64Handler)
	// 預設 port 為 13463,但可透過參數指定,語法:datauri-microsvc.exe 12345
	port := 13463
	if len(os.Args) > 1 {
		argPort, err := strconv.Atoi(os.Args[1])
		if err == nil {
			port = argPort
		}
	}
	fmt.Printf("DataUri Microsevice on port [%d]...\n", port)
	// 只接受本機連線,避免被當成跳板
	log.Fatal(http.ListenAndServe("localhost:"+strconv.Itoa(port), nil))
}

func imgBase64Handler(w http.ResponseWriter, r *http.Request) {
	url := r.URL.Query().Get("u")
	if url == "" {
		http.Error(w, "u parameter is missing", http.StatusBadRequest)
		return
	}
	resp, err := http.Get(url)
	if err != nil {
		http.Error(w, "failed to fetch image: "+url, http.StatusInternalServerError)
		return
	}
	defer resp.Body.Close()
	// 限定只處理 image/* MIME type,避免被惡意利用
	mimeType := resp.Header.Get("Content-Type")
	if len(mimeType) < 6 || mimeType[:5] != "image" {
		http.Error(w, "invalid image mime type: "+mimeType, http.StatusBadRequest)
		return
	}
	imageData, err := io.ReadAll(resp.Body)
	if err != nil {
		http.Error(w, "failed to read image data", http.StatusInternalServerError)
		return
	}
	base64Str := base64.StdEncoding.EncodeToString(imageData)
	base64Str = "data:" + mimeType + ";base64," + base64Str
	w.Header().Set("Content-Type", "text/plain")
	// 允許跨域存取
	w.Header().Set("Access-Control-Allow-Origin", "*")
	w.Write([]byte(base64Str))
}

編譯成 .exe 檔只有 7.1MB,不需要任何 Runtime 或 DLL,一個小小執行檔搞定所有事,是我最喜歡 Go 的一點。

網頁執行部分要稍做修改,改用 fetch 呼叫 Go API,這個有個小眉角:fetch 為非同步作業,若網頁上有多張圖檔,應等到所有圖都轉換完再讀取 HTML 內容。這次順便多練習了 async/await/Promise.all(),同一個題目做第二次還是會學到新東西。

const pending = [];
content.querySelectorAll('img').forEach(async (img) => {
    if (!img.src.startsWith('http')) return;
    var url = 'http://localhost:13463/?u=' + encodeURIComponent(img.src);
    var job = fetch(url).then(res => res.text());
    pending.push(job);
    img.src = await job;
});
Promise.all(pending).then(() => {
    // 所有 fetch 動作完成後執行作業
});

練習完畢。


Comments

Be the first to post a comment

Post a comment