imgcache.rs (6265B)
1 use crate::urls::{UrlCache, UrlMimes}; 2 use crate::Result; 3 use egui::TextureHandle; 4 use image::{Delay, Frame}; 5 use poll_promise::Promise; 6 7 use egui::ColorImage; 8 9 use std::collections::HashMap; 10 use std::fs::{create_dir_all, File}; 11 use std::sync::mpsc::Receiver; 12 use std::time::{Duration, Instant, SystemTime}; 13 14 use hex::ToHex; 15 use sha2::Digest; 16 use std::path; 17 use std::path::PathBuf; 18 use tracing::warn; 19 20 pub type MediaCacheValue = Promise<Result<TexturedImage>>; 21 pub type MediaCacheMap = HashMap<String, MediaCacheValue>; 22 23 pub enum TexturedImage { 24 Static(TextureHandle), 25 Animated(Animation), 26 } 27 28 pub struct Animation { 29 pub first_frame: TextureFrame, 30 pub other_frames: Vec<TextureFrame>, 31 pub receiver: Option<Receiver<TextureFrame>>, 32 } 33 34 impl Animation { 35 pub fn get_frame(&self, index: usize) -> Option<&TextureFrame> { 36 if index == 0 { 37 Some(&self.first_frame) 38 } else { 39 self.other_frames.get(index - 1) 40 } 41 } 42 43 pub fn num_frames(&self) -> usize { 44 self.other_frames.len() + 1 45 } 46 } 47 48 pub struct TextureFrame { 49 pub delay: Duration, 50 pub texture: TextureHandle, 51 } 52 53 pub struct ImageFrame { 54 pub delay: Duration, 55 pub image: ColorImage, 56 } 57 58 pub struct MediaCache { 59 pub cache_dir: path::PathBuf, 60 url_imgs: MediaCacheMap, 61 } 62 63 #[derive(Clone)] 64 pub enum MediaCacheType { 65 Image, 66 Gif, 67 } 68 69 impl MediaCache { 70 pub fn new(cache_dir: path::PathBuf) -> Self { 71 Self { 72 cache_dir, 73 url_imgs: HashMap::new(), 74 } 75 } 76 77 pub fn rel_dir(cache_type: MediaCacheType) -> &'static str { 78 match cache_type { 79 MediaCacheType::Image => "img", 80 MediaCacheType::Gif => "gif", 81 } 82 } 83 84 pub fn write(cache_dir: &path::Path, url: &str, data: ColorImage) -> Result<()> { 85 let file = Self::create_file(cache_dir, url)?; 86 let encoder = image::codecs::webp::WebPEncoder::new_lossless(file); 87 88 encoder.encode( 89 data.as_raw(), 90 data.size[0] as u32, 91 data.size[1] as u32, 92 image::ColorType::Rgba8.into(), 93 )?; 94 95 Ok(()) 96 } 97 98 fn create_file(cache_dir: &path::Path, url: &str) -> Result<File> { 99 let file_path = cache_dir.join(Self::key(url)); 100 if let Some(p) = file_path.parent() { 101 create_dir_all(p)?; 102 } 103 Ok(File::options() 104 .write(true) 105 .create(true) 106 .truncate(true) 107 .open(file_path)?) 108 } 109 110 pub fn write_gif(cache_dir: &path::Path, url: &str, data: Vec<ImageFrame>) -> Result<()> { 111 let file = Self::create_file(cache_dir, url)?; 112 113 let mut encoder = image::codecs::gif::GifEncoder::new(file); 114 for img in data { 115 let buf = color_image_to_rgba(img.image); 116 let frame = Frame::from_parts(buf, 0, 0, Delay::from_saturating_duration(img.delay)); 117 if let Err(e) = encoder.encode_frame(frame) { 118 tracing::error!("problem encoding frame: {e}"); 119 } 120 } 121 122 Ok(()) 123 } 124 125 pub fn key(url: &str) -> String { 126 let k: String = sha2::Sha256::digest(url.as_bytes()).encode_hex(); 127 PathBuf::from(&k[0..2]) 128 .join(&k[2..4]) 129 .join(k) 130 .to_string_lossy() 131 .to_string() 132 } 133 134 /// Migrate from base32 encoded url to sha256 url + sub-dir structure 135 pub fn migrate_v0(&self) -> Result<()> { 136 for file in std::fs::read_dir(&self.cache_dir)? { 137 let file = if let Ok(f) = file { 138 f 139 } else { 140 // not sure how this could fail, skip entry 141 continue; 142 }; 143 if !file.path().is_file() { 144 continue; 145 } 146 let old_filename = file.file_name().to_string_lossy().to_string(); 147 let old_url = if let Some(u) = 148 base32::decode(base32::Alphabet::Crockford, &old_filename) 149 .and_then(|s| String::from_utf8(s).ok()) 150 { 151 u 152 } else { 153 warn!("Invalid base32 filename: {}", &old_filename); 154 continue; 155 }; 156 let new_path = self.cache_dir.join(Self::key(&old_url)); 157 if let Some(p) = new_path.parent() { 158 create_dir_all(p)?; 159 } 160 161 if let Err(e) = std::fs::rename(file.path(), &new_path) { 162 warn!( 163 "Failed to migrate file from {} to {}: {:?}", 164 file.path().display(), 165 new_path.display(), 166 e 167 ); 168 } 169 } 170 Ok(()) 171 } 172 173 pub fn map(&self) -> &MediaCacheMap { 174 &self.url_imgs 175 } 176 177 pub fn map_mut(&mut self) -> &mut MediaCacheMap { 178 &mut self.url_imgs 179 } 180 } 181 182 fn color_image_to_rgba(color_image: ColorImage) -> image::RgbaImage { 183 let width = color_image.width() as u32; 184 let height = color_image.height() as u32; 185 186 let rgba_pixels: Vec<u8> = color_image 187 .pixels 188 .iter() 189 .flat_map(|color| color.to_array()) // Convert Color32 to `[u8; 4]` 190 .collect(); 191 192 image::RgbaImage::from_raw(width, height, rgba_pixels) 193 .expect("Failed to create RgbaImage from ColorImage") 194 } 195 196 pub struct Images { 197 pub static_imgs: MediaCache, 198 pub gifs: MediaCache, 199 pub urls: UrlMimes, 200 pub gif_states: GifStateMap, 201 } 202 203 impl Images { 204 /// path to directory to place [`MediaCache`]s 205 pub fn new(path: path::PathBuf) -> Self { 206 Self { 207 static_imgs: MediaCache::new(path.join(MediaCache::rel_dir(MediaCacheType::Image))), 208 gifs: MediaCache::new(path.join(MediaCache::rel_dir(MediaCacheType::Gif))), 209 urls: UrlMimes::new(UrlCache::new(path.join(UrlCache::rel_dir()))), 210 gif_states: Default::default(), 211 } 212 } 213 214 pub fn migrate_v0(&self) -> Result<()> { 215 self.static_imgs.migrate_v0()?; 216 self.gifs.migrate_v0() 217 } 218 } 219 220 pub type GifStateMap = HashMap<String, GifState>; 221 222 pub struct GifState { 223 pub last_frame_rendered: Instant, 224 pub last_frame_duration: Duration, 225 pub next_frame_time: Option<SystemTime>, 226 pub last_frame_index: usize, 227 }