From 2f3b0d7790dbaf265d4af0073eeccc832b824eea Mon Sep 17 00:00:00 2001 From: Skyler Lehmkuhl Date: Sun, 21 Jun 2026 00:44:04 -0400 Subject: [PATCH] Phase 3.5b: persist image assets in the .beam container MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Image asset bytes are now stored as MediaKind::ImageAsset rows in the SQLite container (chunked, kept-in-place on re-save) instead of base64-embedded in the project JSON — the pageable storage Phase 4 needs. - ImageAsset.data is `#[serde(default, skip_serializing)]`: never written to JSON, but still deserialized for old projects (base64) which then migrate to the container on the next save. - save_beam writes each asset's bytes (keyed by asset id; ext from the source path), keeping an existing row when bytes aren't resident; live_media covers them so orphan cleanup doesn't drop them. - load_beam_sqlite eager-reads the bytes back into `data` (Phase 4 makes this lazy + LRU). Old base64 projects keep their JSON-deserialized data (no container row). --- .../lightningbeam-core/src/clip.rs | 8 ++-- .../lightningbeam-core/src/file_io.rs | 39 +++++++++++++++++++ 2 files changed, 44 insertions(+), 3 deletions(-) diff --git a/lightningbeam-ui/lightningbeam-core/src/clip.rs b/lightningbeam-ui/lightningbeam-core/src/clip.rs index 6367c9b..01e24dc 100644 --- a/lightningbeam-ui/lightningbeam-core/src/clip.rs +++ b/lightningbeam-ui/lightningbeam-core/src/clip.rs @@ -263,9 +263,11 @@ pub struct ImageAsset { /// Image height in pixels pub height: u32, - /// Embedded image data (for project portability) - /// If None, the image will be loaded from path when needed - #[serde(skip_serializing_if = "Option::is_none")] + /// Raw image file bytes. NOT serialized to project JSON — persisted as a + /// `MediaKind::ImageAsset` row in the `.beam` container (chunked, pageable) and + /// read back on load. `default` so new projects (bytes in the container, not JSON) + /// deserialize; old projects with base64-embedded `data` still load via deserialize. + #[serde(default, skip_serializing)] pub data: Option>, /// Folder this asset belongs to (None = root of category) diff --git a/lightningbeam-ui/lightningbeam-core/src/file_io.rs b/lightningbeam-ui/lightningbeam-core/src/file_io.rs index b27eec9..41b7fff 100644 --- a/lightningbeam-ui/lightningbeam-core/src/file_io.rs +++ b/lightningbeam-ui/lightningbeam-core/src/file_io.rs @@ -462,6 +462,32 @@ pub fn save_beam( } } + // --- image assets -> media rows (original file bytes), keyed by asset id --- + let mut image_count = 0usize; + for (id, asset) in &document.image_assets { + if let Some(ref data) = asset.data { + let ext = asset + .path + .extension() + .and_then(|e| e.to_str()) + .unwrap_or("img") + .to_lowercase(); + txn.put_media_packed( + *id, + MediaKind::ImageAsset, + &ext, + data, + MediaMeta { width: Some(asset.width), height: Some(asset.height), ..Default::default() }, + )?; + live_media.insert(*id); + image_count += 1; + } else if txn.media_exists(*id)? { + // Bytes not resident (paged out) but already stored — keep the row. + live_media.insert(*id); + } + } + let _ = image_count; + // --- orphan cleanup: drop media for removed clips/keyframes --- let removed = txn.retain_media(&live_media)?; @@ -625,6 +651,19 @@ fn load_beam_sqlite(path: &Path) -> Result { } let _ = proxy_load_count; + // Eager-read image-asset bytes from the container into `data` (Phase 4 will make + // this lazy + LRU). Old projects keep their base64-deserialized `data` and have no + // container row — those are skipped (`data` already Some). + for (id, asset) in document.image_assets.iter_mut() { + if asset.data.is_none() { + if let Ok(Some(_)) = archive.media_info(*id) { + if let Ok(bytes) = archive.read_media_full(*id) { + asset.data = Some(bytes); + } + } + } + } + // Missing external files (referenced entries whose file no longer exists). let project_dir = path.parent().unwrap_or_else(|| Path::new(".")); let missing_files: Vec = restored_entries