AI 互動行銷頁實戰 — 目標九宮格 — 第 04 章
學完:讓使用者選照片當 AI 參考圖,在瀏覽器裡縮小、轉 base64、存進 IndexedDB——重新整理照片還在。
核心觀念
參考圖是給 AI 看的,不是要印海報。
長邊 1280 跟 4000×3000 原圖,AI 理解到的幾乎一樣,檔案差十倍以上。
原則只有一句:在瀏覽器裡先縮,再上傳。瀏覽器本身就是一台夠用的影像處理器。
<input id="refs-input" type="file" accept="image/*" multiple>
// 長邊縮到 maxEdge,輸出 JPEG blob(app/js/wallpaper.js)
export async function resizeImageBlob(blob, maxEdge = 1280, quality = 0.85) {
const bitmap = await createImageBitmap(blob); // 1. 解碼成點陣圖
const scale = Math.min(1, maxEdge / Math.max(bitmap.width, bitmap.height));
const w = Math.max(1, Math.round(bitmap.width * scale));
const h = Math.max(1, Math.round(bitmap.height * scale));
const c = document.createElement('canvas'); // 2. 開縮小後尺寸的畫布
c.width = w; c.height = h;
c.getContext('2d').drawImage(bitmap, 0, 0, w, h); // 3. 畫上去 = 縮放完成
bitmap.close();
return new Promise((resolve, reject) => {
c.toBlob((out) => out ? resolve(out) : reject(new Error('縮圖失敗')),
'image/jpeg', quality); // 4. 輸出 85% JPEG
});
}
Math.min(1, ...) 只縮小、不放大。一張 8MB 手機原圖走完通常剩 150-400KB,肉眼看內容沒差。
data:image/jpeg;base64,/9j/4AAQSkZJRg...
└────── 前綴 ──────────┘└── 真正的 base64 ──
// Blob → 純 base64(app/js/wallpaper.js)
export function blobToBase64(blob) {
return new Promise((resolve, reject) => {
const reader = new FileReader();
reader.onload = () => resolve(String(reader.result).split(',')[1] || '');
reader.onerror = () => reject(reader.error);
reader.readAsDataURL(blob);
});
}
去不去前綴沒有對錯,純看 API 怎麼約:文件說收 data URL 就留著,說收 raw base64 就切掉。撞到 400 時第一個檢查這裡。
| 路線 | 單張 | 三張 base64 後 | 結果 |
|---|---|---|---|
| 原圖直傳 | 6MB | 約 24MB (base64 膨脹 33%) | 撞爆 20MB 請求上限,連 AI 的面都沒見到 |
| 先縮長邊 1280 | 約 300KB | 約 1.2MB | 幾秒傳完,離所有上限都遠 |
就算過了 20MB,Worker 還有一道驗證:每張 base64 超過 4MB 回 400。體積問題在最靠近源頭的地方解決最便宜——源頭就是使用者的瀏覽器。
refsInput.addEventListener('change', async () => {
const files = [...refsInput.files].slice(0, 3 - refRecords.length); // 最多 3 張
refsInput.value = '';
for (const file of files) {
try {
const blob = await resizeImageBlob(file, 1280); // 先縮,控制 body 體積
await idbPut('refs', { blob, meta: { name: file.name },
createdAt: new Date().toISOString() });
} catch {
showMsg(bgMsgEl, '這張圖讀不進來,請換一張(支援一般圖片格式)。');
}
}
loadRefs(); // 重新從 IndexedDB 讀出來畫縮圖列表
});
try/catch 是必需品:部分格式(例如某些 HEIC)解不開會丟錯——接住、給一句人話,不要讓整個頁面停擺。
const img = document.createElement('img');
img.src = URL.createObjectURL(r.blob);
img.addEventListener('load', () => URL.revokeObjectURL(img.src), { once: true });
下一章:AI 桌布背景——一張圖要生 1-3 分鐘,長任務的正確姿勢是 job + 輪詢 + 一條等得下去的進度條。
對照成品:app/js/wallpaper.js · app/js/state.js · app/js/main.js(搜尋 refsInput)← → 翻頁