系列:一張投影片背後的工程:從零打造網頁簡報編輯器
對應程式版本:Git tag
v0.4.0版本說明:本文記錄圖片 frame、圖片資產與裁切設定彼此分離的第一版,並補充後續加入的未使用圖片清理。
使用者替換圖片時,通常想保留已排好的位置和大小。若先刪除舊物件再建立新物件,ID、群組、圖層與 frame 都可能跟著消失。這一版只更換圖片來源,不刪掉原本的投影片物件。
ImageElement 的 x、y、寬高來自共用 frame;圖片本身只用 assetId 指向 IndexedDB 裡的 Blob,另存替代文字、顯示方式與裁切位置。
替換時會先驗證 PNG/JPEG 與 20 MB 上限,再把新檔案存成新的資產。成功後只送出新的 assetId 與檔名(當作替代文字),reducer 把它們合併進原本的物件:
if (element.type === 'image' && action.type === 'element/set-image') {
const image = { ...element, ...action.image }
// 比較圖片欄位後回傳 image
}
React state 仍會建立不可變的新值,但元素的 ID、frame、群組與圖層位置都保留下來,顯示方式與裁切位置也沿用原設定。新圖片的比例不同時,frame 仍維持原本的寬高,多出來的空間交給下一節的 contain/cover 處理。若資產寫入失敗,文件也不會先指向不存在的圖片。
圖片框只提供兩種顯示方式:contain 完整顯示,cover 填滿後裁切。裁切位置可選中央、上、右、下或左,最後直接對應到原生 CSS:
style={{
objectFit: fit ?? 'contain',
objectPosition: position ?? 'center',
}}
位置在 cover 時決定保留圖片的哪一側,在 contain 時則決定留白出現在哪一邊,兩種模式都能調整。這個做法不會修改原始 Blob,也不必另外產生裁切後的圖片。AssetImage 從 IndexedDB 取得 Blob 後建立暫時的 object URL,元件卸載或資產改變時再撤銷 URL。
介面測試先匯入 640 × 480 的 chart.png,確認文件與 Blob 都已保存;接著替換成 replacement.png,確認編輯畫布、縮圖與列印頁面三處都改顯示新圖片;重新讀取保存的文件後,圖片物件也已換成新檔名。
替換後不會立即刪除舊的資產,這其實是必要的:按下復原會把物件的 assetId 換回舊值,若舊 Blob 已經刪除,復原後只會看到「圖片無法載入」。備份則只收集文件仍在使用的圖片,不會把舊圖一起帶走。後來的修正(commit 4bd1dbd)把清理放在開啟編輯器時:這時復原紀錄還是空的,文件就是唯一的引用來源,可以安全刪除沒用到的圖片;若 Web Locks API 顯示其他分頁也開著編輯器就先略過,因為那邊的復原紀錄可能還需要它們。