Coding4Fun - 將網頁 img src 轉成 Data URI
4 |
最近又在寫程式爬網頁轉電子書,其中一件麻煩事是圖檔。
之前我的做法是依據 img src 下載圖檔,以自定檔名(通常會用流水號)寫入該電子書的附檔資料夾統一儲存,再將 img src 改為圖檔相對路徑。
這回處理的網頁引用圖片不多,每篇頂多兩三張,照片也不大,故我想試試更簡單的做法 - 將圖檔直接轉成 Data URI 內嵌在 HTML,乾淨俐落,省去處理額外附檔的工夫。
最初想到的做法是用 img.onload 事件加 canvas 轉 Data URI,如下範例:
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<title>Image Conversion Demo</title>
<style>img {
display: block; margin: 6px;
}</style>
</head>
<body>
<img id='logo' src="icon180x180.png" alt="logo" style="display: block;" />
<button type="button" onclick="test()">Convert To DataUri</button>
<script>
function getImageDataUri(url) {
let image = new Image();
let imgType = url.match(/\.jpg/) ? 'image/jpeg' : 'image/png';
return new Promise((resolve, reject) => {
image.onload = function () {
let canvas = document.createElement('canvas');
canvas.width = this.naturalWidth;
canvas.height = this.naturalHeight;
canvas.getContext('2d').drawImage(this, 0, 0);
resolve(canvas.toDataURL(imgType));
};
image.onerror = function () {
reject('Error: Image load failed');
};
image.src = url;
});
}
function test() {
let logo = document.getElementById('logo');
getImageDataUri(logo.src).then(res => {
logo.src = res;
}).catch(err => {
alert(err);
});
}
</script>
</body>
</html>
點一下按鈕 src 變成 data:image/png;base64,... ,圖片顯示結果不變:
所以這樣就搞定了?有經驗的老鳥都知道事情沒這麼簡單,基於安全考量,Canvas 存取網頁元素時會檢查來源,只要沾到來自其他網站的資源馬上破功。延伸閱讀:HTML5 Canvas的Origin-Clean安全原則
當 img src 來源改成其他網站的圖檔,toDataURL() 立即噴出錯誤 Uncaught DOMException: Failed to execute 'toDataURL' on 'HTMLCanvasElement': Tainted canvases may not be exported.
:
一旦撞上 Same Origin Policy 限制,即便改走 XHR、Fetch API 也無濟於事。簡單解法是在本機跑個迷你 Poxy 解套,若是爬蟲程式則可改用 .NET WebClient、HttpClient 抓檔案餵進去。這裡來寫幾行 C# 示範用 ASP.NET Core Minimal API 當 Proxy 下載圖檔轉 Data URI。(應有現成解決方案,但這種練習 CORS 存取的機會怎能放過?)
using System.Net;
var builder = WebApplication.CreateBuilder(args);
// 允許跨域存取,不限呼叫網頁來源(AllowAnyOrigin)具危險性,故後面會限定本機存取及圖檔
builder.Services.AddCors(options => options.AddDefaultPolicy(builder => builder.AllowAnyOrigin().AllowAnyHeader().AllowAnyMethod()));
var app = builder.Build();
var httpClient = new HttpClient();
app.UseCors(); // 允許跨域存取
app.MapGet("/{*path}", async (string? path, HttpContext ctx) =>
{
// 限定本機存取
if (!"127.0.0.1,::1".Split(',').Contains(ctx.Connection.RemoteIpAddress.ToString()) ||
string.IsNullOrEmpty(path) || path.StartsWith("favicon.ico")) return string.Empty;
path = Uri.UnescapeDataString(path);
try
{
var response = await httpClient.GetAsync(path);
if (response.StatusCode != HttpStatusCode.OK)
throw new ApplicationException($"Status Code {response.StatusCode}");
var contentType = response.Content.Headers.ContentType.MediaType ?? string.Empty;
if (!contentType.StartsWith("image")) // 只處理圖片
throw new ApplicationException($"{response.Content.Headers.ContentType.MediaType} not supported");
return contentType + ";base64," + Convert.ToBase64String(await response.Content.ReadAsByteArrayAsync());
}
catch (Exception ex)
{
return $"ERROR - {ex.Message}";
}
});
app.Run();
Porgram.cs 30 行搞定,由於下載 Proxy 容易被當成攻擊跳板,我加上兩道安全防護,限定 localhost 存取及只處理 image/* 類型的內容,以彌補 CORS 未限定來源 URL 的風險。
使用時傳入 encodeURIComponent() 編碼的 URL 可取得 Data URI 字串,接著我們改寫 HTML 透過 fetch API 呼叫 ASP.NET Core 網站:
<script>
function getImageDataUri(url) {
return new Promise((resolve, reject) => {
fetch('http://localhost:5158/' + encodeURIComponent(url)).then(res => {
if (res.status !== 200) {
reject('Error: Image load failed');
}
return res.text();
}).then(text => {
if (text.includes('base64'))
resolve(text);
else
reject(text);
}).catch(err => {
reject(err);
});
});
}
function test() {
let logo = document.getElementById('logo');
getImageDataUri(logo.src).then(res => {
logo.src = 'data:' + res;
}).catch(err => {
alert(err);
});
}
</script>
測試成功。
Comments
# by Cash
提外話,我後來大多使用 SingleFile 把網頁存成 html 檔,然後丟到 obsidian 裡面,就可以跨裝置閱讀。 給黑大參考一下,雖然跟本文無關 XDD
# by Jeffrey
to Cash, 感謝分享。單純轉單一 HTML,SingleFile 這個工具很好用。
# by Samuel Leung
fetch(url) 之後不是可以 response.blob() 拿到 blob 之後用 FileReader.readAsDataURL() 就拿到 data url 了
# by access
SingleFile看起來是好物一件,感謝 Cash分享~