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.
This commit is contained in:
52
src/app.rs
52
src/app.rs
@@ -2,8 +2,9 @@ use egui_extras::RetainedImage;
|
|||||||
|
|
||||||
use crate::contacts::Contacts;
|
use crate::contacts::Contacts;
|
||||||
use crate::fonts::setup_fonts;
|
use crate::fonts::setup_fonts;
|
||||||
|
use crate::images::fetch_img;
|
||||||
use crate::{Error, Result};
|
use crate::{Error, Result};
|
||||||
use egui::Context;
|
use egui::{ColorImage, Context, TextureHandle, TextureId};
|
||||||
use enostr::{ClientMessage, EventId, Filter, Profile, Pubkey, RelayEvent, RelayMessage};
|
use enostr::{ClientMessage, EventId, Filter, Profile, Pubkey, RelayEvent, RelayMessage};
|
||||||
use poll_promise::Promise;
|
use poll_promise::Promise;
|
||||||
use std::collections::{HashMap, HashSet};
|
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)]
|
#[derive(Eq, PartialEq, Clone)]
|
||||||
pub enum DamusState {
|
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) {
|
fn render_pfp(ui: &mut egui::Ui, img_cache: &mut ImageCache, url: &str) {
|
||||||
let urlkey = UrlKey::Orig(url).to_u64();
|
let urlkey = UrlKey::Orig(url).to_u64();
|
||||||
let m_cached_promise = img_cache.get(&urlkey);
|
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("❌");
|
ui.label("❌");
|
||||||
}
|
}
|
||||||
Some(Ok(img)) => {
|
Some(Ok(img)) => {
|
||||||
pfp_image(ui, img, pfp_size);
|
pfp_image(ui, img.into(), pfp_size);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
Some(Ok(img)) => {
|
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 {
|
fn pfp_image(ui: &mut egui::Ui, img: TextureId, size: f32) -> egui::Response {
|
||||||
img.show_max_size(ui, egui::vec2(size, size))
|
//img.show_max_size(ui, egui::vec2(size, size))
|
||||||
|
ui.image(img, egui::vec2(size, size))
|
||||||
//.with_options()
|
//.with_options()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,9 +1,10 @@
|
|||||||
use shatter::parser;
|
use shatter::parser;
|
||||||
|
|
||||||
#[derive(Eq, PartialEq, Debug)]
|
#[derive(Debug)]
|
||||||
pub enum Error {
|
pub enum Error {
|
||||||
Nostr(enostr::Error),
|
Nostr(enostr::Error),
|
||||||
Shatter(parser::Error),
|
Shatter(parser::Error),
|
||||||
|
Image(image::error::ImageError),
|
||||||
Generic(String),
|
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 {
|
impl From<enostr::Error> for Error {
|
||||||
fn from(err: enostr::Error) -> Self {
|
fn from(err: enostr::Error) -> Self {
|
||||||
Error::Nostr(err)
|
Error::Nostr(err)
|
||||||
|
|||||||
@@ -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) {
|
pub fn round_image(image: &mut ColorImage) {
|
||||||
// The radius to the edge of of the avatar circle
|
// 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
|
||||||
|
}
|
||||||
|
|||||||
@@ -1,3 +1,3 @@
|
|||||||
use crate::error::Error;
|
use crate::error::Error;
|
||||||
|
|
||||||
type Result<T> = std::result::Result<T, Error>;
|
pub type Result<T> = std::result::Result<T, Error>;
|
||||||
|
|||||||
Reference in New Issue
Block a user