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