commit 927ba5b1373b9a7ca8022812f88638ec6e6fe901
parent 9cf11d953a2480f0279a3c485c28cffa6597145a
Author: William Casarin <jb55@jb55.com>
Date: Mon, 18 Dec 2023 08:52:19 -0800
move around some things, add code for pfps
Diffstat:
M | src/main.rs | | | 112 | ++++++++++++++++++------------------------------------------------------------- |
A | src/nip19.rs | | | 46 | ++++++++++++++++++++++++++++++++++++++++++++++ |
A | src/pfp.rs | | | 77 | +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ |
A | src/render.rs | | | 44 | ++++++++++++++++++++++++++++++++++++++++++++ |
4 files changed, 192 insertions(+), 87 deletions(-)
diff --git a/src/main.rs b/src/main.rs
@@ -8,104 +8,36 @@ use hyper::service::service_fn;
use hyper::{Request, Response, StatusCode};
use hyper_util::rt::TokioIo;
use log::{debug, info};
+use std::sync::Arc;
use tokio::net::TcpListener;
use crate::error::Error;
-use nostr_sdk::nips::nip19::Nip19;
use nostr_sdk::prelude::*;
use nostrdb::{Config, Ndb, Transaction};
use std::time::Duration;
-use nostr_sdk::Kind;
+use lru::LruCache;
mod error;
+mod nip19;
+mod pfp;
+mod render;
-#[derive(Debug, Clone)]
-struct Notecrumbs {
- ndb: Ndb,
- keys: Keys,
-
- /// How long do we wait for remote note requests
- timeout: Duration,
-}
-
-enum Target {
+pub enum Target {
Profile(XOnlyPublicKey),
Event(EventId),
}
-fn nip19_target(nip19: &Nip19) -> Option<Target> {
- match nip19 {
- Nip19::Event(ev) => Some(Target::Event(ev.event_id)),
- Nip19::EventId(evid) => Some(Target::Event(*evid)),
- Nip19::Profile(prof) => Some(Target::Profile(prof.public_key)),
- Nip19::Pubkey(pk) => Some(Target::Profile(*pk)),
- Nip19::Secret(_) => None,
- }
-}
-
-fn note_ui(app: &Notecrumbs, ctx: &egui::Context, content: &str) {
- use egui::{FontId, RichText};
-
- egui::CentralPanel::default().show(&ctx, |ui| {
- ui.horizontal(|ui| {
- ui.label(RichText::new("✏").font(FontId::proportional(120.0)));
- ui.vertical(|ui| {
- ui.label(RichText::new(content).font(FontId::proportional(40.0)));
- });
- })
- });
-}
-
-fn render_note(app: &Notecrumbs, content: &str) -> Vec<u8> {
- use egui_skia::{rasterize, RasterizeOptions};
- use skia_safe::EncodedImageFormat;
-
- let options = RasterizeOptions {
- pixels_per_point: 1.0,
- frames_before_screenshot: 1,
- };
-
- let mut surface = rasterize((1200, 630), |ctx| note_ui(app, ctx, content), Some(options));
-
- surface
- .image_snapshot()
- .encode_to_data(EncodedImageFormat::PNG)
- .expect("expected image")
- .as_bytes()
- .into()
-}
+type ImageCache = LruCache<XOnlyPublicKey, egui::TextureHandle>;
-fn nip19_to_filters(nip19: &Nip19) -> Result<Vec<Filter>, Error> {
- match nip19 {
- Nip19::Event(ev) => {
- let mut filters = vec![Filter::new().id(ev.event_id).limit(1)];
- if let Some(author) = ev.author {
- filters.push(Filter::new().author(author).kind(Kind::Metadata).limit(1))
- }
- Ok(filters)
- }
- Nip19::EventId(evid) => Ok(vec![Filter::new().id(*evid).limit(1)]),
- Nip19::Profile(prof) => Ok(vec![Filter::new()
- .author(prof.public_key)
- .kind(Kind::Metadata)
- .limit(1)]),
- Nip19::Pubkey(pk) => Ok(vec![Filter::new()
- .author(*pk)
- .kind(Kind::Metadata)
- .limit(1)]),
- Nip19::Secret(_sec) => Err(Error::InvalidNip19),
- }
-}
+#[derive(Debug, Clone)]
+pub struct Notecrumbs {
+ ndb: Ndb,
+ keys: Keys,
+ img_cache: Arc<ImageCache>,
-fn nip19_relays(nip19: &Nip19) -> Vec<String> {
- let mut relays: Vec<String> = vec![];
- match nip19 {
- Nip19::Event(ev) => relays.extend(ev.relays.clone()),
- Nip19::Profile(p) => relays.extend(p.relays.clone()),
- _ => (),
- }
- relays
+ /// How long do we wait for remote note requests
+ timeout: Duration,
}
async fn find_note(app: &Notecrumbs, nip19: &Nip19) -> Result<nostr_sdk::Event, Error> {
@@ -114,14 +46,14 @@ async fn find_note(app: &Notecrumbs, nip19: &Nip19) -> Result<nostr_sdk::Event,
let _ = client.add_relay("wss://relay.damus.io").await;
- let other_relays = nip19_relays(nip19);
+ let other_relays = nip19::to_relays(nip19);
for relay in other_relays {
let _ = client.add_relay(relay).await;
}
client.connect().await;
- let filters = nip19_to_filters(nip19)?;
+ let filters = nip19::to_filters(nip19)?;
client
.req_events_of(filters.clone(), Some(app.timeout))
@@ -159,7 +91,7 @@ async fn serve(
}
};
- let target = match nip19_target(&nip19) {
+ let target = match nip19::to_target(&nip19) {
Some(target) => target,
None => {
return Ok(Response::builder()
@@ -218,7 +150,7 @@ async fn serve(
}
};
- let data = render_note(&app, &content);
+ let data = render::render_note(&app, &content);
Ok(Response::builder()
.header(header::CONTENT_TYPE, "image/png")
@@ -248,7 +180,13 @@ async fn main() -> Result<(), Box<dyn std::error::Error + Send + Sync>> {
let ndb = Ndb::new(".", &cfg).expect("ndb failed to open");
let keys = Keys::generate();
let timeout = get_env_timeout();
- let app = Notecrumbs { ndb, keys, timeout };
+ let img_cache = Arc::new(LruCache::new(std::num::NonZeroUsize::new(64).unwrap()));
+ let app = Notecrumbs {
+ ndb,
+ keys,
+ timeout,
+ img_cache,
+ };
// We start a loop to continuously accept incoming connections
loop {
diff --git a/src/nip19.rs b/src/nip19.rs
@@ -0,0 +1,46 @@
+use crate::error::Error;
+use crate::Target;
+use nostr_sdk::nips::nip19::Nip19;
+use nostr_sdk::prelude::*;
+
+pub fn to_target(nip19: &Nip19) -> Option<Target> {
+ match nip19 {
+ Nip19::Event(ev) => Some(Target::Event(ev.event_id)),
+ Nip19::EventId(evid) => Some(Target::Event(*evid)),
+ Nip19::Profile(prof) => Some(Target::Profile(prof.public_key)),
+ Nip19::Pubkey(pk) => Some(Target::Profile(*pk)),
+ Nip19::Secret(_) => None,
+ }
+}
+
+pub fn to_filters(nip19: &Nip19) -> Result<Vec<Filter>, Error> {
+ match nip19 {
+ Nip19::Event(ev) => {
+ let mut filters = vec![Filter::new().id(ev.event_id).limit(1)];
+ if let Some(author) = ev.author {
+ filters.push(Filter::new().author(author).kind(Kind::Metadata).limit(1))
+ }
+ Ok(filters)
+ }
+ Nip19::EventId(evid) => Ok(vec![Filter::new().id(*evid).limit(1)]),
+ Nip19::Profile(prof) => Ok(vec![Filter::new()
+ .author(prof.public_key)
+ .kind(Kind::Metadata)
+ .limit(1)]),
+ Nip19::Pubkey(pk) => Ok(vec![Filter::new()
+ .author(*pk)
+ .kind(Kind::Metadata)
+ .limit(1)]),
+ Nip19::Secret(_sec) => Err(Error::InvalidNip19),
+ }
+}
+
+pub fn to_relays(nip19: &Nip19) -> Vec<String> {
+ let mut relays: Vec<String> = vec![];
+ match nip19 {
+ Nip19::Event(ev) => relays.extend(ev.relays.clone()),
+ Nip19::Profile(p) => relays.extend(p.relays.clone()),
+ _ => (),
+ }
+ relays
+}
diff --git a/src/pfp.rs b/src/pfp.rs
@@ -0,0 +1,77 @@
+use egui::{Color32, ColorImage};
+use image::imageops::FilterType;
+
+// Thank to gossip for this one!
+pub fn round_image(image: &mut ColorImage) {
+ #[cfg(feature = "profiling")]
+ puffin::profile_function!();
+
+ // The radius to the edge of of the avatar circle
+ let edge_radius = image.size[0] as f32 / 2.0;
+ let edge_radius_squared = edge_radius * edge_radius;
+
+ for (pixnum, pixel) in image.pixels.iter_mut().enumerate() {
+ // y coordinate
+ let uy = pixnum / image.size[0];
+ let y = uy as f32;
+ let y_offset = edge_radius - y;
+
+ // x coordinate
+ let ux = pixnum % image.size[0];
+ let x = ux as f32;
+ let x_offset = edge_radius - x;
+
+ // The radius to this pixel (may be inside or outside the circle)
+ let pixel_radius_squared: f32 = x_offset * x_offset + y_offset * y_offset;
+
+ // If inside of the avatar circle
+ if pixel_radius_squared <= edge_radius_squared {
+ // squareroot to find how many pixels we are from the edge
+ let pixel_radius: f32 = pixel_radius_squared.sqrt();
+ let distance = edge_radius - pixel_radius;
+
+ // If we are within 1 pixel of the edge, we should fade, to
+ // antialias the edge of the circle. 1 pixel from the edge should
+ // be 100% of the original color, and right on the edge should be
+ // 0% of the original color.
+ if distance <= 1.0 {
+ *pixel = Color32::from_rgba_premultiplied(
+ (pixel.r() as f32 * distance) as u8,
+ (pixel.g() as f32 * distance) as u8,
+ (pixel.b() as f32 * distance) as u8,
+ (pixel.a() as f32 * distance) as u8,
+ );
+ }
+ } else {
+ // Outside of the avatar circle
+ *pixel = Color32::TRANSPARENT;
+ }
+ }
+}
+
+fn process_pfp_bitmap(size: u32, image: &mut image::DynamicImage) -> ColorImage {
+ #[cfg(features = "profiling")]
+ puffin::profile_function!();
+
+ // 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
+}
diff --git a/src/render.rs b/src/render.rs
@@ -0,0 +1,44 @@
+struct ProfileRenderData {}
+
+use crate::Notecrumbs;
+
+struct NoteRenderData {
+ content: String,
+ profile: ProfileRenderData,
+}
+
+enum RenderData {
+ Note(NoteRenderData),
+}
+
+fn note_ui(app: &Notecrumbs, ctx: &egui::Context, content: &str) {
+ use egui::{FontId, RichText};
+
+ egui::CentralPanel::default().show(&ctx, |ui| {
+ ui.horizontal(|ui| {
+ ui.label(RichText::new("✏").font(FontId::proportional(120.0)));
+ ui.vertical(|ui| {
+ ui.label(RichText::new(content).font(FontId::proportional(40.0)));
+ });
+ })
+ });
+}
+
+pub fn render_note(app: &Notecrumbs, content: &str) -> Vec<u8> {
+ use egui_skia::{rasterize, RasterizeOptions};
+ use skia_safe::EncodedImageFormat;
+
+ let options = RasterizeOptions {
+ pixels_per_point: 1.0,
+ frames_before_screenshot: 1,
+ };
+
+ let mut surface = rasterize((1200, 630), |ctx| note_ui(app, ctx, content), Some(options));
+
+ surface
+ .image_snapshot()
+ .encode_to_data(EncodedImageFormat::PNG)
+ .expect("expected image")
+ .as_bytes()
+ .into()
+}