上傳照片時自動在瀏覽器端縮圖壓縮

根本原因:
照片一律以原始解析度存檔(手機相機動輒 2000px 以上、數 MB),除了先前已修的編譯端問題,也拖慢上傳、佔用 NAS 空間與管理頁縮圖載入。

影響:
上傳大照片時傳輸慢、data 目錄膨脹;縮圖修正前上傳的原圖也一直以全尺寸存放。

修法:
管理頁上傳前先在瀏覽器把照片縮到長邊 1280px、以 JPEG 90% 重新編碼(canvas 會把 EXIF 方向烘進像素,直式照片不會轉向),訊息列顯示壓縮前後大小;MAX_EDGE 常數與編譯端共用,編譯端縮圖保留作為舊照片的雙保險。已在本機實測:2244x4124 / 1.4MB 原圖上傳後存為 696x1280 / 170KB。

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
2026-07-28 11:07:08 +08:00
co-authored by Claude Fable 5
parent c894b98e48
commit 1c806d45e1
+45 -6
View File
@@ -59,7 +59,7 @@
<div class="card">
<h2>新增配對</h2>
<form id="addForm">
<label>照片(jpg / png<input type="file" name="photo" accept=".jpg,.jpeg,.png" required></label>
<label>照片(jpg / png,上傳時自動壓縮<input type="file" name="photo" accept=".jpg,.jpeg,.png" required></label>
<label>影片(mp4 建議 H.264<input type="file" name="video" accept=".mp4,.m4v,.mov,.webm" required></label>
<button type="submit" id="addBtn">上傳</button>
</form>
@@ -146,15 +146,55 @@ async function refresh() {
}
}
// 照片統一縮到這個長邊尺寸:對 MindAR 特徵品質足夠,
// 且掃描端(尤其 iOS Safari)的 WebGL 記憶體撐得住
const MAX_EDGE = 1280;
function loadImageFromFile(file) {
return new Promise((resolve, reject) => {
const url = URL.createObjectURL(file);
const img = new Image();
img.onload = () => { URL.revokeObjectURL(url); resolve(img); };
img.onerror = () => { URL.revokeObjectURL(url); reject(new Error('照片檔案無法讀取')); };
img.src = url;
});
}
// 上傳前先在瀏覽器縮圖壓縮(長邊 MAX_EDGE、JPEG 90%),
// 伺服器不需裝任何影像處理套件。canvas 重新編碼時
// 瀏覽器會把 EXIF 方向烘進像素,直式照片不會轉向
async function optimizePhoto(file) {
const img = await loadImageFromFile(file);
const scale = Math.min(1, MAX_EDGE / Math.max(img.naturalWidth, img.naturalHeight));
if (scale === 1) return file;
const c = document.createElement('canvas');
c.width = Math.round(img.naturalWidth * scale);
c.height = Math.round(img.naturalHeight * scale);
c.getContext('2d').drawImage(img, 0, 0, c.width, c.height);
const blob = await new Promise((r) => c.toBlob(r, 'image/jpeg', 0.9));
if (!blob) throw new Error('照片壓縮失敗');
return new File([blob], file.name.replace(/\.\w+$/, '') + '.jpg', { type: 'image/jpeg' });
}
document.getElementById('addForm').addEventListener('submit', async (e) => {
e.preventDefault();
const btn = document.getElementById('addBtn');
const msg = document.getElementById('msg');
btn.disabled = true;
msg.className = '';
msg.textContent = '上傳中…';
try {
const res = await fetch('/api/pairs', { method: 'POST', body: new FormData(e.target) });
const rawPhoto = e.target.photo.files[0];
msg.textContent = '照片壓縮中…';
const photo = await optimizePhoto(rawPhoto);
const mb = (n) => (n / 1024 / 1024).toFixed(1) + 'MB';
msg.textContent = photo === rawPhoto
? '上傳中…'
: `上傳中…(照片 ${mb(rawPhoto.size)}${mb(photo.size)}`;
const fd = new FormData();
fd.append('photo', photo, photo.name);
fd.append('video', e.target.video.files[0]);
const res = await fetch('/api/pairs', { method: 'POST', body: fd });
const json = await res.json();
if (!res.ok) throw new Error(json.error || res.statusText);
msg.textContent = '已新增,請記得重新編譯。';
@@ -211,11 +251,10 @@ document.getElementById('compileBtn').addEventListener('click', async () => {
pairs.map((p) => loadImage('/data/photos/' + encodeURIComponent(p.photo)))
);
// 大圖先縮到長邊 1280px 再編譯:特徵品質足夠
// 且 .mind 檔與掃描端(尤其 iOS Safari)的 WebGL 記憶體用量大幅下降
// 大圖先縮到長邊 MAX_EDGE 再編譯(雙保險:新上傳的照片已在上傳時縮圖
// 這裡再保護縮圖修正前上傳的舊照片)
// 曾發生 4 張 2244x4131 原圖直接編譯後,iPhone 掃任何照片都沒反應
// (桌面 Chrome 正常,iOS WebGL 資源上限較低,辨識引擎無聲掛掉)
const MAX_EDGE = 1280;
const compileInputs = images.map((img) => {
const scale = Math.min(1, MAX_EDGE / Math.max(img.naturalWidth, img.naturalHeight));
if (scale === 1) return img;