notecrumbs

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

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 }