Skip to content

Commit

Permalink
Merge GIF support by kernel
Browse files Browse the repository at this point in the history
kernelkind (20):
      use bincode
      update ehttp to 0.5.0
      introduce UrlMimes
      use mime_guess
      add SupportedMimeType
      rename ImageCache -> MediaCache
      Use TexturedImage in MediaCache
      render MediaCache method
      move MediaCache rendering to render_media_cache call
      support multiple media cache files
      introduce Images
      render Images method
      migrate to using Images instead of MediaCache directly
      URL mime hosted completeness
      handle UrlCache io
      introduce gif animation
      handle gif state
      integrate gifs
      use SupportedMimeType for media_upload
      render gif in PostView
  • Loading branch information
jb55 committed Feb 26, 2025
2 parents 615e27c + 9d88ba1 commit a524bbd
Show file tree
Hide file tree
Showing 35 changed files with 1,068 additions and 347 deletions.
32 changes: 16 additions & 16 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

4 changes: 3 additions & 1 deletion Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ egui_extras = { version = "0.29.1", features = ["all_loaders"] }
egui_nav = { git = "https://github.com/damus-io/egui-nav", rev = "ac7d663307b76634757024b438dd4b899790da99" }
egui_tabs = "0.2.0"
egui_virtual_list = "0.5.0"
ehttp = "0.2.0"
ehttp = "0.5.0"
enostr = { path = "crates/enostr" }
ewebsock = { version = "0.2.0", features = ["tls"] }
hex = "0.4.3"
Expand Down Expand Up @@ -56,6 +56,8 @@ urlencoding = "2.1.3"
uuid = { version = "1.10.0", features = ["v4"] }
security-framework = "2.11.0"
sha2 = "0.10.8"
bincode = "1.3.3"
mime_guess = "2.0.5"

[patch.crates-io]
egui = { git = "https://github.com/damus-io/egui", branch = "update_layouter_0.29.1" }
Expand Down
3 changes: 3 additions & 0 deletions crates/notedeck/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,9 @@ thiserror = { workspace = true }
puffin = { workspace = true, optional = true }
puffin_egui = { workspace = true, optional = true }
sha2 = { workspace = true }
bincode = { workspace = true }
ehttp = {workspace = true }
mime_guess = { workspace = true }

[dev-dependencies]
tempfile = { workspace = true }
Expand Down
8 changes: 4 additions & 4 deletions crates/notedeck/src/app.rs
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
use crate::persist::{AppSizeHandler, ZoomHandler};
use crate::{
Accounts, AppContext, Args, DataPath, DataPathType, Directory, FileKeyStorage, ImageCache,
Accounts, AppContext, Args, DataPath, DataPathType, Directory, FileKeyStorage, Images,
KeyStorageType, NoteCache, RelayDebugView, ThemeHandler, UnknownIds,
};
use egui::ThemePreference;
Expand All @@ -19,7 +19,7 @@ pub trait App {
/// Main notedeck app framework
pub struct Notedeck {
ndb: Ndb,
img_cache: ImageCache,
img_cache: Images,
unknown_ids: UnknownIds,
pool: RelayPool,
note_cache: NoteCache,
Expand Down Expand Up @@ -129,7 +129,7 @@ impl Notedeck {

let _ = std::fs::create_dir_all(&dbpath_str);

let img_cache_dir = path.path(DataPathType::Cache).join(ImageCache::rel_dir());
let img_cache_dir = path.path(DataPathType::Cache);
let _ = std::fs::create_dir_all(img_cache_dir.clone());

let map_size = if cfg!(target_os = "windows") {
Expand Down Expand Up @@ -184,7 +184,7 @@ impl Notedeck {
}
}

let img_cache = ImageCache::new(img_cache_dir);
let img_cache = Images::new(img_cache_dir);
let note_cache = NoteCache::default();
let unknown_ids = UnknownIds::default();
let zoom = ZoomHandler::new(&path);
Expand Down
4 changes: 2 additions & 2 deletions crates/notedeck/src/context.rs
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
use crate::{Accounts, Args, DataPath, ImageCache, NoteCache, ThemeHandler, UnknownIds};
use crate::{Accounts, Args, DataPath, Images, NoteCache, ThemeHandler, UnknownIds};

use enostr::RelayPool;
use nostrdb::Ndb;
Expand All @@ -7,7 +7,7 @@ use nostrdb::Ndb;

pub struct AppContext<'a> {
pub ndb: &'a mut Ndb,
pub img_cache: &'a mut ImageCache,
pub img_cache: &'a mut Images,
pub unknown_ids: &'a mut UnknownIds,
pub pool: &'a mut RelayPool,
pub note_cache: &'a mut NoteCache,
Expand Down
163 changes: 131 additions & 32 deletions crates/notedeck/src/imgcache.rs
Original file line number Diff line number Diff line change
@@ -1,63 +1,88 @@
use crate::urls::{UrlCache, UrlMimes};
use crate::Result;
use egui::TextureHandle;
use image::{Delay, Frame};
use poll_promise::Promise;

use egui::ColorImage;

use std::collections::HashMap;
use std::fs::{create_dir_all, File};
use std::sync::mpsc::Receiver;
use std::time::{Duration, Instant, SystemTime};

use hex::ToHex;
use sha2::Digest;
use std::path;
use std::path::PathBuf;
use tracing::warn;

pub type ImageCacheValue = Promise<Result<TextureHandle>>;
pub type ImageCacheMap = HashMap<String, ImageCacheValue>;
pub type MediaCacheValue = Promise<Result<TexturedImage>>;
pub type MediaCacheMap = HashMap<String, MediaCacheValue>;

pub struct ImageCache {
pub enum TexturedImage {
Static(TextureHandle),
Animated(Animation),
}

pub struct Animation {
pub first_frame: TextureFrame,
pub other_frames: Vec<TextureFrame>,
pub receiver: Option<Receiver<TextureFrame>>,
}

impl Animation {
pub fn get_frame(&self, index: usize) -> Option<&TextureFrame> {
if index == 0 {
Some(&self.first_frame)
} else {
self.other_frames.get(index - 1)
}
}

pub fn num_frames(&self) -> usize {
self.other_frames.len() + 1
}
}

pub struct TextureFrame {
pub delay: Duration,
pub texture: TextureHandle,
}

pub struct ImageFrame {
pub delay: Duration,
pub image: ColorImage,
}

pub struct MediaCache {
pub cache_dir: path::PathBuf,
url_imgs: ImageCacheMap,
url_imgs: MediaCacheMap,
}

impl ImageCache {
#[derive(Clone)]
pub enum MediaCacheType {
Image,
Gif,
}

impl MediaCache {
pub fn new(cache_dir: path::PathBuf) -> Self {
Self {
cache_dir,
url_imgs: HashMap::new(),
}
}

pub fn rel_dir() -> &'static str {
"img"
}

/*
pub fn fetch(image: &str) -> Result<Image> {
let m_cached_promise = img_cache.map().get(image);
if m_cached_promise.is_none() {
let res = crate::images::fetch_img(
img_cache,
ui.ctx(),
&image,
ImageType::Content(width.round() as u32, height.round() as u32),
);
img_cache.map_mut().insert(image.to_owned(), res);
pub fn rel_dir(cache_type: MediaCacheType) -> &'static str {
match cache_type {
MediaCacheType::Image => "img",
MediaCacheType::Gif => "gif",
}
}
*/

pub fn write(cache_dir: &path::Path, url: &str, data: ColorImage) -> Result<()> {
let file_path = cache_dir.join(Self::key(url));
if let Some(p) = file_path.parent() {
create_dir_all(p)?;
}
let file = File::options()
.write(true)
.create(true)
.truncate(true)
.open(file_path)?;
let file = Self::create_file(cache_dir, url)?;
let encoder = image::codecs::webp::WebPEncoder::new_lossless(file);

encoder.encode(
Expand All @@ -70,6 +95,33 @@ impl ImageCache {
Ok(())
}

fn create_file(cache_dir: &path::Path, url: &str) -> Result<File> {
let file_path = cache_dir.join(Self::key(url));
if let Some(p) = file_path.parent() {
create_dir_all(p)?;
}
Ok(File::options()
.write(true)
.create(true)
.truncate(true)
.open(file_path)?)
}

pub fn write_gif(cache_dir: &path::Path, url: &str, data: Vec<ImageFrame>) -> Result<()> {
let file = Self::create_file(cache_dir, url)?;

let mut encoder = image::codecs::gif::GifEncoder::new(file);
for img in data {
let buf = color_image_to_rgba(img.image);
let frame = Frame::from_parts(buf, 0, 0, Delay::from_saturating_duration(img.delay));
if let Err(e) = encoder.encode_frame(frame) {
tracing::error!("problem encoding frame: {e}");
}
}

Ok(())
}

pub fn key(url: &str) -> String {
let k: String = sha2::Sha256::digest(url.as_bytes()).encode_hex();
PathBuf::from(&k[0..2])
Expand Down Expand Up @@ -118,11 +170,58 @@ impl ImageCache {
Ok(())
}

pub fn map(&self) -> &ImageCacheMap {
pub fn map(&self) -> &MediaCacheMap {
&self.url_imgs
}

pub fn map_mut(&mut self) -> &mut ImageCacheMap {
pub fn map_mut(&mut self) -> &mut MediaCacheMap {
&mut self.url_imgs
}
}

fn color_image_to_rgba(color_image: ColorImage) -> image::RgbaImage {
let width = color_image.width() as u32;
let height = color_image.height() as u32;

let rgba_pixels: Vec<u8> = color_image
.pixels
.iter()
.flat_map(|color| color.to_array()) // Convert Color32 to `[u8; 4]`
.collect();

image::RgbaImage::from_raw(width, height, rgba_pixels)
.expect("Failed to create RgbaImage from ColorImage")
}

pub struct Images {
pub static_imgs: MediaCache,
pub gifs: MediaCache,
pub urls: UrlMimes,
pub gif_states: GifStateMap,
}

impl Images {
/// path to directory to place [`MediaCache`]s
pub fn new(path: path::PathBuf) -> Self {
Self {
static_imgs: MediaCache::new(path.join(MediaCache::rel_dir(MediaCacheType::Image))),
gifs: MediaCache::new(path.join(MediaCache::rel_dir(MediaCacheType::Gif))),
urls: UrlMimes::new(UrlCache::new(path.join(UrlCache::rel_dir()))),
gif_states: Default::default(),
}
}

pub fn migrate_v0(&self) -> Result<()> {
self.static_imgs.migrate_v0()?;
self.gifs.migrate_v0()
}
}

pub type GifStateMap = HashMap<String, GifState>;

pub struct GifState {
pub last_frame_rendered: Instant,
pub last_frame_duration: Duration,
pub next_frame_time: Option<SystemTime>,
pub last_frame_index: usize,
}
7 changes: 6 additions & 1 deletion crates/notedeck/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ mod timecache;
mod timed_serializer;
pub mod ui;
mod unknowns;
mod urls;
mod user_account;

pub use accounts::{AccountData, Accounts, AccountsAction, AddAccountAction, SwitchAccountAction};
Expand All @@ -30,7 +31,10 @@ pub use context::AppContext;
pub use error::{Error, FilterError};
pub use filter::{FilterState, FilterStates, UnifiedSubscription};
pub use fonts::NamedFontFamily;
pub use imgcache::ImageCache;
pub use imgcache::{
Animation, GifState, GifStateMap, ImageFrame, Images, MediaCache, MediaCacheType,
MediaCacheValue, TextureFrame, TexturedImage,
};
pub use muted::{MuteFun, Muted};
pub use note::{NoteRef, RootIdError, RootNoteId, RootNoteIdBuf};
pub use notecache::{CachedNote, NoteCache};
Expand All @@ -46,6 +50,7 @@ pub use theme::ColorTheme;
pub use time::time_ago_since;
pub use timecache::TimeCached;
pub use unknowns::{get_unknown_note_ids, NoteRefsUnkIdAction, SingleUnkIdAction, UnknownIds};
pub use urls::{supported_mime_hosted_at_url, SupportedMimeType, UrlMimes};
pub use user_account::UserAccount;

// export libs
Expand Down
Loading

0 comments on commit a524bbd

Please sign in to comment.