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:
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>;