192
crates/notedeck_ui/src/blur.rs
Normal file
192
crates/notedeck_ui/src/blur.rs
Normal file
@@ -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(
|
||||
¶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<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()))
|
||||
}
|
||||
@@ -1,4 +1,5 @@
|
||||
pub mod anim;
|
||||
pub mod blur;
|
||||
pub mod colors;
|
||||
pub mod constants;
|
||||
pub mod contacts;
|
||||
|
||||
Reference in New Issue
Block a user