pfp.rs (5704B)
1 use crate::Error; 2 use bytes::Bytes; 3 use egui::{Color32, ColorImage}; 4 use hyper::body::Incoming; 5 use image::imageops::FilterType; 6 7 pub const PFP_SIZE: u32 = 64; 8 9 // Thank to gossip for this one! 10 pub fn round_image(image: &mut ColorImage) { 11 #[cfg(feature = "profiling")] 12 puffin::profile_function!(); 13 14 // The radius to the edge of of the avatar circle 15 let edge_radius = image.size[0] as f32 / 2.0; 16 let edge_radius_squared = edge_radius * edge_radius; 17 18 for (pixnum, pixel) in image.pixels.iter_mut().enumerate() { 19 // y coordinate 20 let uy = pixnum / image.size[0]; 21 let y = uy as f32; 22 let y_offset = edge_radius - y; 23 24 // x coordinate 25 let ux = pixnum % image.size[0]; 26 let x = ux as f32; 27 let x_offset = edge_radius - x; 28 29 // The radius to this pixel (may be inside or outside the circle) 30 let pixel_radius_squared: f32 = x_offset * x_offset + y_offset * y_offset; 31 32 // If inside of the avatar circle 33 if pixel_radius_squared <= edge_radius_squared { 34 // squareroot to find how many pixels we are from the edge 35 let pixel_radius: f32 = pixel_radius_squared.sqrt(); 36 let distance = edge_radius - pixel_radius; 37 38 // If we are within 1 pixel of the edge, we should fade, to 39 // antialias the edge of the circle. 1 pixel from the edge should 40 // be 100% of the original color, and right on the edge should be 41 // 0% of the original color. 42 if distance <= 1.0 { 43 *pixel = Color32::from_rgba_premultiplied( 44 (pixel.r() as f32 * distance) as u8, 45 (pixel.g() as f32 * distance) as u8, 46 (pixel.b() as f32 * distance) as u8, 47 (pixel.a() as f32 * distance) as u8, 48 ); 49 } 50 } else { 51 // Outside of the avatar circle 52 *pixel = Color32::TRANSPARENT; 53 } 54 } 55 } 56 57 pub fn process_pfp_bitmap(image: &mut image::DynamicImage) -> ColorImage { 58 #[cfg(features = "profiling")] 59 puffin::profile_function!(); 60 61 let size = PFP_SIZE; 62 63 // Crop square 64 let smaller = image.width().min(image.height()); 65 66 if image.width() > smaller { 67 let excess = image.width() - smaller; 68 *image = image.crop_imm(excess / 2, 0, image.width() - excess, image.height()); 69 } else if image.height() > smaller { 70 let excess = image.height() - smaller; 71 *image = image.crop_imm(0, excess / 2, image.width(), image.height() - excess); 72 } 73 let image = image.resize(size, size, FilterType::CatmullRom); // DynamicImage 74 let image_buffer = image.into_rgba8(); // RgbaImage (ImageBuffer) 75 let mut color_image = ColorImage::from_rgba_unmultiplied( 76 [ 77 image_buffer.width() as usize, 78 image_buffer.height() as usize, 79 ], 80 image_buffer.as_flat_samples().as_slice(), 81 ); 82 round_image(&mut color_image); 83 color_image 84 } 85 86 async fn fetch_url(url: &str) -> Result<(Vec<u8>, hyper::Response<Incoming>), Error> { 87 use http_body_util::BodyExt; 88 use http_body_util::Empty; 89 use hyper::Request; 90 use hyper_util::rt::tokio::TokioIo; 91 use tokio::net::TcpStream; 92 93 let mut data: Vec<u8> = vec![]; 94 let url = url.parse::<hyper::Uri>()?; 95 let host = url.host().expect("uri has no host"); 96 let port = url.port_u16().unwrap_or(80); 97 let addr = format!("{}:{}", host, port); 98 let stream = TcpStream::connect(addr).await?; 99 let io = TokioIo::new(stream); 100 101 let (mut sender, conn) = hyper::client::conn::http1::handshake(io).await?; 102 tokio::task::spawn(async move { 103 if let Err(err) = conn.await { 104 println!("Connection failed: {:?}", err); 105 } 106 }); 107 108 let authority = url.authority().unwrap().clone(); 109 110 let req = Request::builder() 111 .uri(url) 112 .header(hyper::header::HOST, authority.as_str()) 113 .body(Empty::<Bytes>::new())?; 114 115 let mut res: hyper::Response<Incoming> = sender.send_request(req).await?; 116 117 // Stream the body, writing each chunk to stdout as we get it 118 // (instead of buffering and printing at the end). 119 while let Some(next) = res.frame().await { 120 let frame = next?; 121 if let Some(chunk) = frame.data_ref() { 122 if data.len() + chunk.len() > 52428800 123 /* 50 MiB */ 124 { 125 return Err(Error::TooBig); 126 } 127 data.extend(chunk); 128 } 129 } 130 131 Ok((data, res)) 132 } 133 134 pub async fn fetch_pfp(url: &str) -> Result<ColorImage, Error> { 135 let (data, res) = fetch_url(url).await?; 136 parse_img_response(data, res) 137 } 138 139 fn parse_img_response( 140 data: Vec<u8>, 141 response: hyper::Response<Incoming>, 142 ) -> Result<ColorImage, Error> { 143 use egui_extras::image::FitTo; 144 145 #[cfg(feature = "profiling")] 146 puffin::profile_function!(); 147 148 let content_type = response.headers()["content-type"] 149 .to_str() 150 .unwrap_or_default(); 151 152 let size = PFP_SIZE; 153 154 if content_type.starts_with("image/svg") { 155 #[cfg(feature = "profiling")] 156 puffin::profile_scope!("load_svg"); 157 158 let mut color_image = egui_extras::image::load_svg_bytes_with_size( 159 &data, 160 FitTo::Size(size as u32, size as u32), 161 )?; 162 round_image(&mut color_image); 163 Ok(color_image) 164 } else if content_type.starts_with("image/") { 165 #[cfg(feature = "profiling")] 166 puffin::profile_scope!("load_from_memory"); 167 let mut dyn_image = image::load_from_memory(&data)?; 168 Ok(process_pfp_bitmap(&mut dyn_image)) 169 } else { 170 Err(Error::InvalidProfilePic) 171 } 172 }