notedeck

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

commit e17b73ababcea20def8c6c0657383bbc7374cbaf
parent 453e8b7002d4987067ad197d6fecedc4937c98b4
Author: William Casarin <jb55@jb55.com>
Date:   Wed,  5 Jul 2023 18:15:15 -0700

Profile picture processing

This is still single-threaded, so perf is pretty bad. Will need to think
about how to do this more efficiently in a web context where we don't
have threading.

Diffstat:
Msrc/app.rs | 52++++++++--------------------------------------------
Msrc/error.rs | 9++++++++-
Msrc/images.rs | 70+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++-
Msrc/result.rs | 2+-
4 files changed, 86 insertions(+), 47 deletions(-)

diff --git a/src/app.rs b/src/app.rs @@ -2,8 +2,9 @@ use egui_extras::RetainedImage; use crate::contacts::Contacts; use crate::fonts::setup_fonts; +use crate::images::fetch_img; use crate::{Error, Result}; -use egui::Context; +use egui::{ColorImage, Context, TextureHandle, TextureId}; use enostr::{ClientMessage, EventId, Filter, Profile, Pubkey, RelayEvent, RelayMessage}; use poll_promise::Promise; use std::collections::{HashMap, HashSet}; @@ -26,7 +27,7 @@ impl UrlKey<'_> { } } -type ImageCache = HashMap<u64, Promise<Result<RetainedImage>>>; +type ImageCache = HashMap<u64, Promise<Result<TextureHandle>>>; #[derive(Eq, PartialEq, Clone)] pub enum DamusState { @@ -236,44 +237,6 @@ impl Damus { } } -#[allow(clippy::needless_pass_by_value)] -fn parse_img_response(response: ehttp::Response) -> Result<RetainedImage> { - let content_type = response.content_type().unwrap_or_default(); - - if content_type.starts_with("image/svg") { - Ok(RetainedImage::from_svg_bytes( - &response.url, - &response.bytes, - )?) - } else if content_type.starts_with("image/") { - Ok(RetainedImage::from_image_bytes( - &response.url, - &response.bytes, - )?) - } else { - Err(format!("Expected image, found content-type {:?}", content_type).into()) - } -} - -fn fetch_img(ctx: &egui::Context, url: &str) -> Promise<Result<RetainedImage>> { - // TODO: fetch image from local cache - fetch_img_from_net(ctx, url) -} - -fn fetch_img_from_net(ctx: &egui::Context, url: &str) -> Promise<Result<RetainedImage>> { - let (sender, promise) = Promise::new(); - let request = ehttp::Request::get(url); - let ctx = ctx.clone(); - ehttp::fetch(request, move |response| { - let image = response - .map_err(Error::Generic) - .and_then(parse_img_response); - sender.send(image); // send the results back to the UI thread. - ctx.request_repaint(); - }); - promise -} - fn render_pfp(ui: &mut egui::Ui, img_cache: &mut ImageCache, url: &str) { let urlkey = UrlKey::Orig(url).to_u64(); let m_cached_promise = img_cache.get(&urlkey); @@ -310,18 +273,19 @@ fn render_pfp(ui: &mut egui::Ui, img_cache: &mut ImageCache, url: &str) { ui.label("❌"); } Some(Ok(img)) => { - pfp_image(ui, img, pfp_size); + pfp_image(ui, img.into(), pfp_size); } } } Some(Ok(img)) => { - pfp_image(ui, img, pfp_size); + pfp_image(ui, img.into(), pfp_size); } } } -fn pfp_image(ui: &mut egui::Ui, img: &RetainedImage, size: f32) -> egui::Response { - img.show_max_size(ui, egui::vec2(size, size)) +fn pfp_image(ui: &mut egui::Ui, img: TextureId, size: f32) -> egui::Response { + //img.show_max_size(ui, egui::vec2(size, size)) + ui.image(img, egui::vec2(size, size)) //.with_options() } diff --git a/src/error.rs b/src/error.rs @@ -1,9 +1,10 @@ use shatter::parser; -#[derive(Eq, PartialEq, Debug)] +#[derive(Debug)] pub enum Error { Nostr(enostr::Error), Shatter(parser::Error), + Image(image::error::ImageError), Generic(String), } @@ -19,6 +20,12 @@ impl From<parser::Error> for Error { } } +impl From<image::error::ImageError> for Error { + fn from(err: image::error::ImageError) -> Self { + Error::Image(err) + } +} + impl From<enostr::Error> for Error { fn from(err: enostr::Error) -> Self { Error::Nostr(err) diff --git a/src/images.rs b/src/images.rs @@ -1,4 +1,9 @@ -use egui::{Color32, ColorImage}; +use crate::error::Error; +use crate::result::Result; +use egui::{Color32, ColorImage, TextureHandle}; +use egui_extras::image::FitTo; +use image::imageops::FilterType; +use poll_promise::Promise; pub fn round_image(image: &mut ColorImage) { // The radius to the edge of of the avatar circle @@ -43,3 +48,66 @@ pub fn round_image(image: &mut ColorImage) { } } } + +fn process_pfp_bitmap(size: u32, image: &mut image::DynamicImage) -> ColorImage { + // Crop square + let smaller = image.width().min(image.height()); + + if image.width() > smaller { + let excess = image.width() - smaller; + *image = image.crop_imm(excess / 2, 0, image.width() - excess, image.height()); + } else if image.height() > smaller { + let excess = image.height() - smaller; + *image = image.crop_imm(0, excess / 2, image.width(), image.height() - excess); + } + let image = image.resize(size, size, FilterType::CatmullRom); // DynamicImage + let image_buffer = image.into_rgba8(); // RgbaImage (ImageBuffer) + let mut color_image = ColorImage::from_rgba_unmultiplied( + [ + image_buffer.width() as usize, + image_buffer.height() as usize, + ], + image_buffer.as_flat_samples().as_slice(), + ); + round_image(&mut color_image); + color_image +} + +fn parse_img_response(response: ehttp::Response) -> Result<ColorImage> { + let content_type = response.content_type().unwrap_or_default(); + let size: u32 = 100; + + if content_type.starts_with("image/svg") { + let mut color_image = + egui_extras::image::load_svg_bytes_with_size(&response.bytes, FitTo::Size(size, size))?; + round_image(&mut color_image); + Ok(color_image) + } else if content_type.starts_with("image/") { + let mut dyn_image = image::load_from_memory(&response.bytes)?; + Ok(process_pfp_bitmap(size, &mut dyn_image)) + } else { + Err(format!("Expected image, found content-type {:?}", content_type).into()) + } +} + +pub fn fetch_img(ctx: &egui::Context, url: &str) -> Promise<Result<TextureHandle>> { + // TODO: fetch image from local cache + fetch_img_from_net(ctx, url) +} + +fn fetch_img_from_net(ctx: &egui::Context, url: &str) -> Promise<Result<TextureHandle>> { + let (sender, promise) = Promise::new(); + let request = ehttp::Request::get(url); + let ctx = ctx.clone(); + let cloned_url = url.to_owned(); + ehttp::fetch(request, move |response| { + let handle = response + .map_err(Error::Generic) + .and_then(parse_img_response) + .map(|img| ctx.load_texture(&cloned_url, img, Default::default())); + + sender.send(handle); // send the results back to the UI thread. + ctx.request_repaint(); + }); + promise +} diff --git a/src/result.rs b/src/result.rs @@ -1,3 +1,3 @@ use crate::error::Error; -type Result<T> = std::result::Result<T, Error>; +pub type Result<T> = std::result::Result<T, Error>;