notecrumbs

a nostr opengraph server build on nostrdb and egui
git clone git://jb55.com/notecrumbs
Log | Files | Refs | README | LICENSE

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 }