use std::collections::HashMap; use nostrdb::Note; use crate::jobs::{Job, JobError, JobParamsOwned}; #[derive(Clone)] pub struct Blur<'a> { pub blurhash: &'a str, pub dimensions: Option, // width and height in pixels } #[derive(Clone, Debug)] pub struct PixelDimensions { pub x: u32, pub y: u32, } impl PixelDimensions { pub fn to_points(&self, ppp: f32) -> PointDimensions { PointDimensions { x: (self.x as f32) / ppp, y: (self.y as f32) / ppp, } } } #[derive(Clone, Debug)] pub struct PointDimensions { pub x: f32, pub y: f32, } impl PointDimensions { pub fn to_pixels(self, ui: &egui::Ui) -> PixelDimensions { PixelDimensions { x: (self.x * ui.pixels_per_point()).round() as u32, y: (self.y * ui.pixels_per_point()).round() as u32, } } pub fn to_vec(self) -> egui::Vec2 { egui::Vec2::new(self.x, self.y) } } impl Blur<'_> { pub fn scaled_pixel_dimensions( &self, ui: &egui::Ui, available_points: PointDimensions, ) -> PixelDimensions { let max_pixels = available_points.to_pixels(ui); let Some(defined_dimensions) = &self.dimensions else { return max_pixels; }; if defined_dimensions.x == 0 || defined_dimensions.y == 0 { tracing::error!("The blur dimensions should not be zero"); return max_pixels; } if defined_dimensions.y <= max_pixels.y { return defined_dimensions.clone(); } let scale_factor = (max_pixels.y as f32) / (defined_dimensions.y as f32); let max_width_scaled = scale_factor * (defined_dimensions.x as f32); PixelDimensions { x: (max_width_scaled.round() as u32), y: max_pixels.y, } } } pub fn imeta_blurhashes<'a>(note: &'a Note) -> HashMap<&'a str, Blur<'a>> { let mut blurs = HashMap::new(); for tag in note.tags() { let mut tag_iter = tag.into_iter(); if tag_iter .next() .and_then(|s| s.str()) .filter(|s| *s == "imeta") .is_none() { continue; } let Some((url, blur)) = find_blur(tag_iter) else { continue; }; blurs.insert(url, blur); } blurs } fn find_blur(tag_iter: nostrdb::TagIter) -> Option<(&str, Blur)> { let mut url = None; let mut blurhash = None; let mut dims = None; for tag_elem in tag_iter { let Some(s) = tag_elem.str() else { continue }; let mut split = s.split_whitespace(); let Some(first) = split.next() else { continue }; let Some(second) = split.next() else { continue }; match first { "url" => url = Some(second), "blurhash" => blurhash = Some(second), "dim" => dims = Some(second), _ => {} } if url.is_some() && blurhash.is_some() && dims.is_some() { break; } } let url = url?; let blurhash = blurhash?; let dimensions = dims.and_then(|d| { let mut split = d.split('x'); let width = split.next()?.parse::().ok()?; let height = split.next()?.parse::().ok()?; Some(PixelDimensions { x: width, y: height, }) }); Some(( url, Blur { blurhash, dimensions, }, )) } #[derive(Clone)] pub enum ObfuscationType<'a> { Blurhash(Blur<'a>), Default, } pub(crate) fn compute_blurhash( params: Option, dims: PixelDimensions, ) -> Result { #[allow(irrefutable_let_patterns)] let Some(JobParamsOwned::Blurhash(params)) = params else { return Err(JobError::InvalidParameters); }; let maybe_handle = match generate_blurhash_texturehandle( ¶ms.ctx, ¶ms.blurhash, ¶ms.url, dims.x, dims.y, ) { Ok(tex) => Some(tex), Err(e) => { tracing::error!("failed to render blurhash: {e}"); None } }; Ok(Job::Blurhash(maybe_handle)) } fn generate_blurhash_texturehandle( ctx: &egui::Context, blurhash: &str, url: &str, width: u32, height: u32, ) -> notedeck::Result { let bytes = blurhash::decode(blurhash, width, height, 1.0) .map_err(|e| notedeck::Error::Generic(e.to_string()))?; let img = egui::ColorImage::from_rgba_unmultiplied([width as usize, height as usize], &bytes); Ok(ctx.load_texture(url, img, Default::default())) }