notedeck

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

images.rs (8903B)


      1 use egui::{pos2, Color32, ColorImage, Rect, Sense, SizeHint, TextureHandle};
      2 use image::imageops::FilterType;
      3 use notedeck::ImageCache;
      4 use notedeck::Result;
      5 use poll_promise::Promise;
      6 use std::path;
      7 use tokio::fs;
      8 
      9 //pub type ImageCacheKey = String;
     10 //pub type ImageCacheValue = Promise<Result<TextureHandle>>;
     11 //pub type ImageCache = HashMap<String, ImageCacheValue>;
     12 
     13 // NOTE(jb55): chatgpt wrote this because I was too dumb to
     14 pub fn aspect_fill(
     15     ui: &mut egui::Ui,
     16     sense: Sense,
     17     texture_id: egui::TextureId,
     18     aspect_ratio: f32,
     19 ) -> egui::Response {
     20     let frame = ui.available_rect_before_wrap(); // Get the available frame space in the current layout
     21     let frame_ratio = frame.width() / frame.height();
     22 
     23     let (width, height) = if frame_ratio > aspect_ratio {
     24         // Frame is wider than the content
     25         (frame.width(), frame.width() / aspect_ratio)
     26     } else {
     27         // Frame is taller than the content
     28         (frame.height() * aspect_ratio, frame.height())
     29     };
     30 
     31     let content_rect = Rect::from_min_size(
     32         frame.min
     33             + egui::vec2(
     34                 (frame.width() - width) / 2.0,
     35                 (frame.height() - height) / 2.0,
     36             ),
     37         egui::vec2(width, height),
     38     );
     39 
     40     // Set the clipping rectangle to the frame
     41     //let clip_rect = ui.clip_rect(); // Preserve the original clipping rectangle
     42     //ui.set_clip_rect(frame);
     43 
     44     let uv = Rect::from_min_max(pos2(0.0, 0.0), pos2(1.0, 1.0));
     45 
     46     let (response, painter) = ui.allocate_painter(ui.available_size(), sense);
     47 
     48     // Draw the texture within the calculated rect, potentially clipping it
     49     painter.rect_filled(content_rect, 0.0, ui.ctx().style().visuals.window_fill());
     50     painter.image(texture_id, content_rect, uv, Color32::WHITE);
     51 
     52     // Restore the original clipping rectangle
     53     //ui.set_clip_rect(clip_rect);
     54     response
     55 }
     56 
     57 pub fn round_image(image: &mut ColorImage) {
     58     #[cfg(feature = "profiling")]
     59     puffin::profile_function!();
     60 
     61     // The radius to the edge of of the avatar circle
     62     let edge_radius = image.size[0] as f32 / 2.0;
     63     let edge_radius_squared = edge_radius * edge_radius;
     64 
     65     for (pixnum, pixel) in image.pixels.iter_mut().enumerate() {
     66         // y coordinate
     67         let uy = pixnum / image.size[0];
     68         let y = uy as f32;
     69         let y_offset = edge_radius - y;
     70 
     71         // x coordinate
     72         let ux = pixnum % image.size[0];
     73         let x = ux as f32;
     74         let x_offset = edge_radius - x;
     75 
     76         // The radius to this pixel (may be inside or outside the circle)
     77         let pixel_radius_squared: f32 = x_offset * x_offset + y_offset * y_offset;
     78 
     79         // If inside of the avatar circle
     80         if pixel_radius_squared <= edge_radius_squared {
     81             // squareroot to find how many pixels we are from the edge
     82             let pixel_radius: f32 = pixel_radius_squared.sqrt();
     83             let distance = edge_radius - pixel_radius;
     84 
     85             // If we are within 1 pixel of the edge, we should fade, to
     86             // antialias the edge of the circle. 1 pixel from the edge should
     87             // be 100% of the original color, and right on the edge should be
     88             // 0% of the original color.
     89             if distance <= 1.0 {
     90                 *pixel = Color32::from_rgba_premultiplied(
     91                     (pixel.r() as f32 * distance) as u8,
     92                     (pixel.g() as f32 * distance) as u8,
     93                     (pixel.b() as f32 * distance) as u8,
     94                     (pixel.a() as f32 * distance) as u8,
     95                 );
     96             }
     97         } else {
     98             // Outside of the avatar circle
     99             *pixel = Color32::TRANSPARENT;
    100         }
    101     }
    102 }
    103 
    104 fn process_pfp_bitmap(imgtyp: ImageType, image: &mut image::DynamicImage) -> ColorImage {
    105     #[cfg(feature = "profiling")]
    106     puffin::profile_function!();
    107 
    108     match imgtyp {
    109         ImageType::Content(w, h) => {
    110             let image = image.resize(w, h, FilterType::CatmullRom); // DynamicImage
    111             let image_buffer = image.into_rgba8(); // RgbaImage (ImageBuffer)
    112             let color_image = ColorImage::from_rgba_unmultiplied(
    113                 [
    114                     image_buffer.width() as usize,
    115                     image_buffer.height() as usize,
    116                 ],
    117                 image_buffer.as_flat_samples().as_slice(),
    118             );
    119             color_image
    120         }
    121         ImageType::Profile(size) => {
    122             // Crop square
    123             let smaller = image.width().min(image.height());
    124 
    125             if image.width() > smaller {
    126                 let excess = image.width() - smaller;
    127                 *image = image.crop_imm(excess / 2, 0, image.width() - excess, image.height());
    128             } else if image.height() > smaller {
    129                 let excess = image.height() - smaller;
    130                 *image = image.crop_imm(0, excess / 2, image.width(), image.height() - excess);
    131             }
    132             let image = image.resize(size, size, FilterType::CatmullRom); // DynamicImage
    133             let image_buffer = image.into_rgba8(); // RgbaImage (ImageBuffer)
    134             let mut color_image = ColorImage::from_rgba_unmultiplied(
    135                 [
    136                     image_buffer.width() as usize,
    137                     image_buffer.height() as usize,
    138                 ],
    139                 image_buffer.as_flat_samples().as_slice(),
    140             );
    141             round_image(&mut color_image);
    142             color_image
    143         }
    144     }
    145 }
    146 
    147 fn parse_img_response(response: ehttp::Response, imgtyp: ImageType) -> Result<ColorImage> {
    148     #[cfg(feature = "profiling")]
    149     puffin::profile_function!();
    150 
    151     let content_type = response.content_type().unwrap_or_default();
    152     let size_hint = match imgtyp {
    153         ImageType::Profile(size) => SizeHint::Size(size, size),
    154         ImageType::Content(w, h) => SizeHint::Size(w, h),
    155     };
    156 
    157     if content_type.starts_with("image/svg") {
    158         #[cfg(feature = "profiling")]
    159         puffin::profile_scope!("load_svg");
    160 
    161         let mut color_image =
    162             egui_extras::image::load_svg_bytes_with_size(&response.bytes, Some(size_hint))?;
    163         round_image(&mut color_image);
    164         Ok(color_image)
    165     } else if content_type.starts_with("image/") {
    166         #[cfg(feature = "profiling")]
    167         puffin::profile_scope!("load_from_memory");
    168         let mut dyn_image = image::load_from_memory(&response.bytes)?;
    169         Ok(process_pfp_bitmap(imgtyp, &mut dyn_image))
    170     } else {
    171         Err(format!("Expected image, found content-type {:?}", content_type).into())
    172     }
    173 }
    174 
    175 fn fetch_img_from_disk(
    176     ctx: &egui::Context,
    177     url: &str,
    178     path: &path::Path,
    179 ) -> Promise<Result<TextureHandle>> {
    180     let ctx = ctx.clone();
    181     let url = url.to_owned();
    182     let path = path.to_owned();
    183     Promise::spawn_async(async move {
    184         let data = fs::read(path).await?;
    185         let image_buffer = image::load_from_memory(&data).map_err(notedeck::Error::Image)?;
    186 
    187         // TODO: remove unwrap here
    188         let flat_samples = image_buffer.as_flat_samples_u8().unwrap();
    189         let img = ColorImage::from_rgba_unmultiplied(
    190             [
    191                 image_buffer.width() as usize,
    192                 image_buffer.height() as usize,
    193             ],
    194             flat_samples.as_slice(),
    195         );
    196 
    197         Ok(ctx.load_texture(&url, img, Default::default()))
    198     })
    199 }
    200 
    201 /// Controls type-specific handling
    202 #[derive(Debug, Clone, Copy)]
    203 pub enum ImageType {
    204     /// Profile Image (size)
    205     Profile(u32),
    206     /// Content Image (width, height)
    207     Content(u32, u32),
    208 }
    209 
    210 pub fn fetch_img(
    211     img_cache: &ImageCache,
    212     ctx: &egui::Context,
    213     url: &str,
    214     imgtyp: ImageType,
    215 ) -> Promise<Result<TextureHandle>> {
    216     let key = ImageCache::key(url);
    217     let path = img_cache.cache_dir.join(key);
    218 
    219     if path.exists() {
    220         fetch_img_from_disk(ctx, url, &path)
    221     } else {
    222         fetch_img_from_net(&img_cache.cache_dir, ctx, url, imgtyp)
    223     }
    224 
    225     // TODO: fetch image from local cache
    226 }
    227 
    228 fn fetch_img_from_net(
    229     cache_path: &path::Path,
    230     ctx: &egui::Context,
    231     url: &str,
    232     imgtyp: ImageType,
    233 ) -> Promise<Result<TextureHandle>> {
    234     let (sender, promise) = Promise::new();
    235     let request = ehttp::Request::get(url);
    236     let ctx = ctx.clone();
    237     let cloned_url = url.to_owned();
    238     let cache_path = cache_path.to_owned();
    239     ehttp::fetch(request, move |response| {
    240         let handle = response
    241             .map_err(notedeck::Error::Generic)
    242             .and_then(|resp| parse_img_response(resp, imgtyp))
    243             .map(|img| {
    244                 let texture_handle = ctx.load_texture(&cloned_url, img.clone(), Default::default());
    245 
    246                 // write to disk
    247                 std::thread::spawn(move || ImageCache::write(&cache_path, &cloned_url, img));
    248 
    249                 texture_handle
    250             });
    251 
    252         sender.send(handle); // send the results back to the UI thread.
    253         ctx.request_repaint();
    254     });
    255 
    256     promise
    257 }