magical markdown slides
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}