notedeck

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

commit b9e2daf47abc1bcf929b358ccf71ee98737b7392
parent d227eb65517da931137db8ac16de213c0012b781
Author: kernelkind <kernelkind@gmail.com>
Date:   Sat,  8 Mar 2025 22:33:51 -0500

introduce blur

Signed-off-by: kernelkind <kernelkind@gmail.com>

Diffstat:
Acrates/notedeck_ui/src/blur.rs | 192+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Mcrates/notedeck_ui/src/lib.rs | 1+
2 files changed, 193 insertions(+), 0 deletions(-)

diff --git a/crates/notedeck_ui/src/blur.rs b/crates/notedeck_ui/src/blur.rs @@ -0,0 +1,192 @@ +use std::collections::HashMap; + +use nostrdb::Note; + +use crate::jobs::{Job, JobError, JobParamsOwned}; + +pub struct Blur<'a> { + pub blurhash: &'a str, + pub dimensions: Option<PixelDimensions>, // 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::<u32>().ok()?; + let height = split.next()?.parse::<u32>().ok()?; + + Some(PixelDimensions { + x: width, + y: height, + }) + }); + + Some(( + url, + Blur { + blurhash, + dimensions, + }, + )) +} + +pub enum ObfuscationType<'a> { + Blurhash(&'a Blur<'a>), + Default, +} + +pub(crate) fn compute_blurhash( + params: Option<JobParamsOwned>, + dims: PixelDimensions, +) -> Result<Job, JobError> { + #[allow(irrefutable_let_patterns)] + let Some(JobParamsOwned::Blurhash(params)) = params + else { + return Err(JobError::InvalidParameters); + }; + + let maybe_handle = match generate_blurhash_texturehandle( + &params.ctx, + &params.blurhash, + &params.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<egui::TextureHandle> { + 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())) +} diff --git a/crates/notedeck_ui/src/lib.rs b/crates/notedeck_ui/src/lib.rs @@ -1,4 +1,5 @@ pub mod anim; +pub mod blur; pub mod colors; pub mod constants; pub mod contacts;