Go 練習 - 圖檔轉 DataUri 微服務
1 |
最近重操舊業,想將網站上的技術系列文章轉成 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
# by someone
from flask import Flask, request, Response import requests import base64 app = Flask(__name__) @app.route('/') def img_base64_handler(): url = request.args.get('u') if not url: return Response("u parameter is missing", status=400) try: response = requests.get(url) response.raise_for_status() # Raise an error for bad status codes except requests.RequestException as e: return Response(f"Failed to fetch image: {e}", status=500) mime_type = response.headers.get('Content-Type', '') if not mime_type.startswith('image/'): return Response(f"Invalid image mime type: {mime_type}", status=400) image_data = response.content base64_str = base64.b64encode(image_data).decode('utf-8') base64_response = f"data:{mime_type};base64,{base64_str}" # Allow CORS response = Response(base64_response, content_type='text/plain') response.headers['Access-Control-Allow-Origin'] = '*' return response if __name__ == '__main__': import sys port = 13463 if len(sys.argv) > 1: try: port = int(sys.argv[1]) except ValueError: print("Invalid port number, using default 13463") print(f"DataUri Microservice on port [{port}]...") app.run(host='localhost', port=port)