magical markdown slides
at main 3.8 kB view raw
1use image::DynamicImage; 2use ratatui_image::{picker::Picker, protocol::StatefulProtocol}; 3use std::collections::HashMap; 4use std::io; 5use std::path::{Path, PathBuf}; 6 7/// Manages image loading and protocol state for terminal rendering 8/// 9/// Handles image loading from paths, protocol detection, and caching of loaded images. 10pub struct ImageManager { 11 picker: Picker, 12 protocols: HashMap<String, StatefulProtocol>, 13 base_path: Option<PathBuf>, 14} 15 16impl ImageManager { 17 /// Create a new ImageManager with protocol detection 18 pub fn new() -> io::Result<Self> { 19 let picker = Picker::from_query_stdio().map_err(io::Error::other)?; 20 21 Ok(Self { picker, protocols: HashMap::new(), base_path: None }) 22 } 23 24 /// Set the base path for resolving relative image paths 25 pub fn set_base_path(&mut self, path: impl AsRef<Path>) { 26 self.base_path = Some(path.as_ref().to_path_buf()); 27 } 28 29 /// Load an image from a path and create a protocol for it 30 /// 31 /// Returns a reference to the protocol if successful. 32 pub fn load_image(&mut self, path: &str) -> io::Result<&mut StatefulProtocol> { 33 if !self.protocols.contains_key(path) { 34 let image_path = self.resolve_path(path); 35 let dyn_img = load_image_from_path(&image_path)?; 36 let protocol = self.picker.new_resize_protocol(dyn_img); 37 self.protocols.insert(path.to_string(), protocol); 38 } 39 40 Ok(self.protocols.get_mut(path).unwrap()) 41 } 42 43 /// Check if an image is already loaded 44 pub fn has_image(&self, path: &str) -> bool { 45 self.protocols.contains_key(path) 46 } 47 48 /// Get a mutable reference to a loaded image protocol 49 pub fn get_protocol_mut(&mut self, path: &str) -> Option<&mut StatefulProtocol> { 50 self.protocols.get_mut(path) 51 } 52 53 /// Resolve a path relative to the base path if set 54 fn resolve_path(&self, path: &str) -> PathBuf { 55 let path = Path::new(path); 56 57 if path.is_absolute() { 58 return path.to_path_buf(); 59 } 60 61 if let Some(base) = &self.base_path { 62 if let Some(parent) = base.parent() { 63 return parent.join(path); 64 } 65 } 66 67 path.to_path_buf() 68 } 69} 70 71impl Default for ImageManager { 72 fn default() -> Self { 73 Self::new().unwrap_or_else(|_| Self { 74 picker: Picker::from_fontsize((8, 16)), 75 protocols: HashMap::new(), 76 base_path: None, 77 }) 78 } 79} 80 81/// Load an image from a file path 82fn load_image_from_path(path: &Path) -> io::Result<DynamicImage> { 83 image::ImageReader::open(path) 84 .map_err(|e| io::Error::new(io::ErrorKind::NotFound, e))? 85 .decode() 86 .map_err(|e| io::Error::new(io::ErrorKind::InvalidData, e)) 87} 88 89#[cfg(test)] 90mod tests { 91 use super::*; 92 93 #[test] 94 fn resolve_path_absolute() { 95 let mut manager = ImageManager::default(); 96 manager.set_base_path("/home/user/slides.md"); 97 let resolved = manager.resolve_path("/tmp/image.png"); 98 assert_eq!(resolved, PathBuf::from("/tmp/image.png")); 99 } 100 101 #[test] 102 fn resolve_path_relative() { 103 let mut manager = ImageManager::default(); 104 manager.set_base_path("/home/user/slides.md"); 105 let resolved = manager.resolve_path("images/test.png"); 106 assert_eq!(resolved, PathBuf::from("/home/user/images/test.png")); 107 } 108 109 #[test] 110 fn resolve_path_no_base() { 111 let manager = ImageManager::default(); 112 let resolved = manager.resolve_path("test.png"); 113 assert_eq!(resolved, PathBuf::from("test.png")); 114 } 115 116 #[test] 117 fn has_image_returns_false_for_unloaded() { 118 let manager = ImageManager::default(); 119 assert!(!manager.has_image("test.png")); 120 } 121}