notedeck

One damus client to rule them all
git clone git://jb55.com/notedeck
Log | Files | Refs | README | LICENSE

blur.rs (4705B)


      1 use std::collections::HashMap;
      2 
      3 use nostrdb::Note;
      4 
      5 use crate::jobs::{Job, JobError, JobParamsOwned};
      6 
      7 #[derive(Clone)]
      8 pub struct Blur<'a> {
      9     pub blurhash: &'a str,
     10     pub dimensions: Option<PixelDimensions>, // width and height in pixels
     11 }
     12 
     13 #[derive(Clone, Debug)]
     14 pub struct PixelDimensions {
     15     pub x: u32,
     16     pub y: u32,
     17 }
     18 
     19 impl PixelDimensions {
     20     pub fn to_points(&self, ppp: f32) -> PointDimensions {
     21         PointDimensions {
     22             x: (self.x as f32) / ppp,
     23             y: (self.y as f32) / ppp,
     24         }
     25     }
     26 }
     27 
     28 #[derive(Clone, Debug)]
     29 pub struct PointDimensions {
     30     pub x: f32,
     31     pub y: f32,
     32 }
     33 
     34 impl PointDimensions {
     35     pub fn to_pixels(self, ui: &egui::Ui) -> PixelDimensions {
     36         PixelDimensions {
     37             x: (self.x * ui.pixels_per_point()).round() as u32,
     38             y: (self.y * ui.pixels_per_point()).round() as u32,
     39         }
     40     }
     41 
     42     pub fn to_vec(self) -> egui::Vec2 {
     43         egui::Vec2::new(self.x, self.y)
     44     }
     45 }
     46 
     47 impl Blur<'_> {
     48     pub fn scaled_pixel_dimensions(
     49         &self,
     50         ui: &egui::Ui,
     51         available_points: PointDimensions,
     52     ) -> PixelDimensions {
     53         let max_pixels = available_points.to_pixels(ui);
     54 
     55         let Some(defined_dimensions) = &self.dimensions else {
     56             return max_pixels;
     57         };
     58 
     59         if defined_dimensions.x == 0 || defined_dimensions.y == 0 {
     60             tracing::error!("The blur dimensions should not be zero");
     61             return max_pixels;
     62         }
     63 
     64         if defined_dimensions.y <= max_pixels.y {
     65             return defined_dimensions.clone();
     66         }
     67 
     68         let scale_factor = (max_pixels.y as f32) / (defined_dimensions.y as f32);
     69         let max_width_scaled = scale_factor * (defined_dimensions.x as f32);
     70 
     71         PixelDimensions {
     72             x: (max_width_scaled.round() as u32),
     73             y: max_pixels.y,
     74         }
     75     }
     76 }
     77 
     78 pub fn imeta_blurhashes<'a>(note: &'a Note) -> HashMap<&'a str, Blur<'a>> {
     79     let mut blurs = HashMap::new();
     80 
     81     for tag in note.tags() {
     82         let mut tag_iter = tag.into_iter();
     83         if tag_iter
     84             .next()
     85             .and_then(|s| s.str())
     86             .filter(|s| *s == "imeta")
     87             .is_none()
     88         {
     89             continue;
     90         }
     91 
     92         let Some((url, blur)) = find_blur(tag_iter) else {
     93             continue;
     94         };
     95 
     96         blurs.insert(url, blur);
     97     }
     98 
     99     blurs
    100 }
    101 
    102 fn find_blur(tag_iter: nostrdb::TagIter) -> Option<(&str, Blur)> {
    103     let mut url = None;
    104     let mut blurhash = None;
    105     let mut dims = None;
    106 
    107     for tag_elem in tag_iter {
    108         let Some(s) = tag_elem.str() else { continue };
    109         let mut split = s.split_whitespace();
    110 
    111         let Some(first) = split.next() else { continue };
    112         let Some(second) = split.next() else { continue };
    113 
    114         match first {
    115             "url" => url = Some(second),
    116             "blurhash" => blurhash = Some(second),
    117             "dim" => dims = Some(second),
    118             _ => {}
    119         }
    120 
    121         if url.is_some() && blurhash.is_some() && dims.is_some() {
    122             break;
    123         }
    124     }
    125 
    126     let url = url?;
    127     let blurhash = blurhash?;
    128 
    129     let dimensions = dims.and_then(|d| {
    130         let mut split = d.split('x');
    131         let width = split.next()?.parse::<u32>().ok()?;
    132         let height = split.next()?.parse::<u32>().ok()?;
    133 
    134         Some(PixelDimensions {
    135             x: width,
    136             y: height,
    137         })
    138     });
    139 
    140     Some((
    141         url,
    142         Blur {
    143             blurhash,
    144             dimensions,
    145         },
    146     ))
    147 }
    148 
    149 #[derive(Clone)]
    150 pub enum ObfuscationType<'a> {
    151     Blurhash(Blur<'a>),
    152     Default,
    153 }
    154 
    155 pub(crate) fn compute_blurhash(
    156     params: Option<JobParamsOwned>,
    157     dims: PixelDimensions,
    158 ) -> Result<Job, JobError> {
    159     #[allow(irrefutable_let_patterns)]
    160     let Some(JobParamsOwned::Blurhash(params)) = params
    161     else {
    162         return Err(JobError::InvalidParameters);
    163     };
    164 
    165     let maybe_handle = match generate_blurhash_texturehandle(
    166         &params.ctx,
    167         &params.blurhash,
    168         &params.url,
    169         dims.x,
    170         dims.y,
    171     ) {
    172         Ok(tex) => Some(tex),
    173         Err(e) => {
    174             tracing::error!("failed to render blurhash: {e}");
    175             None
    176         }
    177     };
    178 
    179     Ok(Job::Blurhash(maybe_handle))
    180 }
    181 
    182 fn generate_blurhash_texturehandle(
    183     ctx: &egui::Context,
    184     blurhash: &str,
    185     url: &str,
    186     width: u32,
    187     height: u32,
    188 ) -> notedeck::Result<egui::TextureHandle> {
    189     let bytes = blurhash::decode(blurhash, width, height, 1.0)
    190         .map_err(|e| notedeck::Error::Generic(e.to_string()))?;
    191 
    192     let img = egui::ColorImage::from_rgba_unmultiplied([width as usize, height as usize], &bytes);
    193     Ok(ctx.load_texture(url, img, Default::default()))
    194 }