notedeck

One damus client to rule them all
git clone git://jb55.com/notedeck
Log | Files | Refs | README | LICENSE

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 }