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