Skip to content

Commit 935f5ee

Browse files
sergey3bvclaude
andcommitted
feat: WEBP support
Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
1 parent 5633c4c commit 935f5ee

File tree

12 files changed

+692
-7
lines changed

12 files changed

+692
-7
lines changed

Cargo.lock

Lines changed: 21 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

Cargo.toml

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,7 @@ members = [
1717

1818
[workspace.dependencies]
1919
opener = "0.8.2"
20-
chrono = "0.4.40"
20+
chrono = "0.4.40"
2121
crossbeam-channel = "0.5"
2222
crossbeam = "0.8.4"
2323
base32 = "0.4.0"
@@ -49,13 +49,14 @@ http-body-util = "0.1.3"
4949
# - Other platforms: aws-lc-rs (better performance)
5050
rustls = { version = "0.23.28", default-features = false, features = ["std", "tls12", "logging"] }
5151
ring = "0.17"
52-
enostr = { path = "crates/enostr" }
52+
enostr = { path = "crates/enostr" }
5353
ewebsock = { version = "0.2.0", features = ["tls"] }
5454
fluent = "0.17.0"
5555
fluent-resmgr = "0.0.8"
5656
fluent-langneg = "0.13"
5757
hex = { version = "0.4.3", features = ["serde"] }
5858
image = { version = "0.25", features = ["jpeg", "png", "webp"] }
59+
webp = "0.3.1"
5960
indexmap = "2.6.0"
6061
log = "0.4.17"
6162
md5 = "0.7.0"

crates/notedeck/Cargo.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@ egui = { workspace = true }
1818
egui_extras = { workspace = true }
1919
eframe = { workspace = true }
2020
image = { workspace = true }
21+
webp = { workspace = true }
2122
base32 = { workspace = true }
2223
poll-promise = { workspace = true }
2324
tracing = { workspace = true }

crates/notedeck/src/imgcache.rs

Lines changed: 93 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ use egui::TextureHandle;
1414
use image::{Delay, Frame};
1515

1616
use egui::ColorImage;
17+
use webp::AnimFrame;
1718

1819
use std::collections::HashMap;
1920
use std::fs::{self, create_dir_all, File};
@@ -31,6 +32,7 @@ pub struct TexturesCache {
3132
pub static_image: StaticImgTexCache,
3233
pub blurred: BlurCache,
3334
pub animated: AnimatedImgTexCache,
35+
pub webp: crate::media::webp::WebpTexCache,
3436
}
3537

3638
impl TexturesCache {
@@ -43,6 +45,9 @@ impl TexturesCache {
4345
animated: AnimatedImgTexCache::new(
4446
base_dir.join(MediaCache::rel_dir(MediaCacheType::Gif)),
4547
),
48+
webp: crate::media::webp::WebpTexCache::new(
49+
base_dir.join(MediaCache::rel_dir(MediaCacheType::Webp)),
50+
),
4651
}
4752
}
4853
}
@@ -53,6 +58,12 @@ pub enum TextureState<T> {
5358
Loaded(T),
5459
}
5560

61+
impl<T> TextureState<T> {
62+
pub fn is_loaded(&self) -> bool {
63+
matches!(self, TextureState::Loaded(_))
64+
}
65+
}
66+
5667
impl<T> std::fmt::Debug for TextureState<T> {
5768
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
5869
match self {
@@ -102,6 +113,7 @@ pub struct MediaCache {
102113
pub enum MediaCacheType {
103114
Image,
104115
Gif,
116+
Webp,
105117
}
106118

107119
impl MediaCache {
@@ -136,6 +148,7 @@ impl MediaCache {
136148
match cache_type {
137149
MediaCacheType::Image => "img",
138150
MediaCacheType::Gif => "gif",
151+
MediaCacheType::Webp => "webp",
139152
}
140153
}
141154

@@ -180,6 +193,60 @@ impl MediaCache {
180193
Ok(())
181194
}
182195

196+
pub fn write_webp(cache_dir: &path::Path, url: &str, data: Vec<ImageFrame>) -> Result<()> {
197+
if data.is_empty() {
198+
return Err(crate::Error::Generic(
199+
"No frames provided to write_webp".to_owned(),
200+
));
201+
}
202+
203+
let file_path = cache_dir.join(Self::key(url));
204+
if let Some(p) = file_path.parent() {
205+
create_dir_all(p)?;
206+
}
207+
208+
// TODO: makes sense to make it static
209+
let mut config = webp::WebPConfig::new().or(Err(crate::Error::Generic(
210+
"Failed to configure webp encoder".to_owned(),
211+
)))?;
212+
config.lossless = 1;
213+
config.alpha_compression = 0;
214+
215+
let reference_frame: &ImageFrame = data.first().ok_or(crate::Error::Generic(
216+
"No frames provided to write_webp".to_owned(),
217+
))?;
218+
let mut encoder = webp::AnimEncoder::new(
219+
reference_frame.image.size[0] as u32,
220+
reference_frame.image.size[1] as u32,
221+
&config,
222+
);
223+
224+
let _ = data.iter().fold(0i32, |acc_timestamp, frame| {
225+
let [width, height] = frame.image.size;
226+
let delay = frame.delay.as_millis();
227+
let frame_delay = if delay < i32::MAX as u128 {
228+
delay as i32
229+
} else {
230+
300i32
231+
};
232+
233+
let timestamp = acc_timestamp;
234+
235+
encoder.add_frame(AnimFrame::from_rgba(
236+
frame.image.as_raw(),
237+
width as u32,
238+
height as u32,
239+
timestamp,
240+
));
241+
242+
acc_timestamp.saturating_add(frame_delay)
243+
});
244+
245+
let webp = encoder.encode();
246+
247+
Ok(std::fs::write(file_path, &*webp)?)
248+
}
249+
183250
pub fn key(url: &str) -> String {
184251
let k: String = sha2::Sha256::digest(url.as_bytes()).encode_hex();
185252
PathBuf::from(&k[0..2])
@@ -272,11 +339,13 @@ pub struct Images {
272339
pub base_path: path::PathBuf,
273340
pub static_imgs: MediaCache,
274341
pub gifs: MediaCache,
342+
pub webps: MediaCache,
275343
pub textures: TexturesCache,
276344
pub urls: UrlMimes,
277345
/// cached imeta data
278346
pub metadata: HashMap<String, ImageMetadata>,
279347
pub gif_states: GifStateMap,
348+
pub webp_states: WebpStateMap,
280349
}
281350

282351
impl Images {
@@ -286,16 +355,19 @@ impl Images {
286355
base_path: path.clone(),
287356
static_imgs: MediaCache::new(&path, MediaCacheType::Image),
288357
gifs: MediaCache::new(&path, MediaCacheType::Gif),
358+
webps: MediaCache::new(&path, MediaCacheType::Webp),
289359
urls: UrlMimes::new(UrlCache::new(path.join(UrlCache::rel_dir()))),
290360
gif_states: Default::default(),
361+
webp_states: Default::default(),
291362
metadata: Default::default(),
292363
textures: TexturesCache::new(path.clone()),
293364
}
294365
}
295366

296367
pub fn migrate_v0(&self) -> Result<()> {
297368
self.static_imgs.migrate_v0()?;
298-
self.gifs.migrate_v0()
369+
self.gifs.migrate_v0()?;
370+
self.webps.migrate_v0()
299371
}
300372

301373
pub fn get_renderable_media(&mut self, url: &str) -> Option<RenderableMedia> {
@@ -334,7 +406,9 @@ impl Images {
334406
let mut loader = NoLoadingLatestTex::new(
335407
&self.textures.static_image,
336408
&self.textures.animated,
409+
&self.textures.webp,
337410
&mut self.gif_states,
411+
&mut self.webp_states,
338412
);
339413
loader.latest(jobs, ui.ctx(), url, cache_type, img_type, animation_mode)
340414
}
@@ -343,13 +417,15 @@ impl Images {
343417
match cache_type {
344418
MediaCacheType::Image => &self.static_imgs,
345419
MediaCacheType::Gif => &self.gifs,
420+
MediaCacheType::Webp => &self.webps,
346421
}
347422
}
348423

349424
pub fn get_cache_mut(&mut self, cache_type: MediaCacheType) -> &mut MediaCache {
350425
match cache_type {
351426
MediaCacheType::Image => &mut self.static_imgs,
352427
MediaCacheType::Gif => &mut self.gifs,
428+
MediaCacheType::Webp => &mut self.webps,
353429
}
354430
}
355431

@@ -368,7 +444,9 @@ impl Images {
368444
self.urls.cache.clear();
369445
self.static_imgs.clear();
370446
self.gifs.clear();
447+
self.webps.clear();
371448
self.gif_states.clear();
449+
self.webp_states.clear();
372450

373451
Ok(())
374452
}
@@ -378,7 +456,9 @@ impl Images {
378456
NoLoadingLatestTex::new(
379457
&self.textures.static_image,
380458
&self.textures.animated,
459+
&self.textures.webp,
381460
&mut self.gif_states,
461+
&mut self.webp_states,
382462
),
383463
&self.textures.blurred,
384464
)
@@ -392,14 +472,17 @@ impl Images {
392472
NoLoadingLatestTex::new(
393473
&self.textures.static_image,
394474
&self.textures.animated,
475+
&self.textures.webp,
395476
&mut self.gif_states,
477+
&mut self.webp_states,
396478
)
397479
}
398480

399481
pub fn user_trusts_img(&self, url: &str, media_type: MediaCacheType) -> bool {
400482
match media_type {
401483
MediaCacheType::Image => self.textures.static_image.contains(url),
402484
MediaCacheType::Gif => self.textures.animated.contains(url),
485+
MediaCacheType::Webp => self.textures.webp.contains(url),
403486
}
404487
}
405488
}
@@ -413,6 +496,15 @@ pub struct GifState {
413496
pub last_frame_index: usize,
414497
}
415498

499+
pub type WebpStateMap = HashMap<String, WebpState>;
500+
501+
pub struct WebpState {
502+
pub last_frame_rendered: Instant,
503+
pub last_frame_duration: Duration,
504+
pub next_frame_time: Option<SystemTime>,
505+
pub last_frame_index: usize,
506+
}
507+
416508
pub struct LatestTexture {
417509
pub texture: TextureHandle,
418510
pub request_next_repaint: Option<SystemTime>,

crates/notedeck/src/jobs/media.rs

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ pub enum MediaJobKind {
1515
Blurhash,
1616
StaticImg,
1717
AnimatedImg,
18+
WebpImg,
1819
}
1920

2021
pub enum MediaJobResult {
@@ -42,7 +43,6 @@ pub fn deliver_completed_media_job(
4243
Ok(a) => TextureState::Loaded(a),
4344
Err(e) => TextureState::Error(e),
4445
};
45-
4646
tex_cache.animated.cache.insert(id, r);
4747
}
4848
MediaJobResult::Blurhash(texture_handle) => {
@@ -74,5 +74,8 @@ pub fn run_media_job_pre_action(job_id: &JobId<MediaJobKind>, tex_cache: &mut Te
7474
MediaJobKind::AnimatedImg => {
7575
tex_cache.animated.cache.insert(id, TextureState::Pending);
7676
}
77+
MediaJobKind::WebpImg => {
78+
tex_cache.webp.cache.insert(id, TextureState::Pending);
79+
}
7780
}
7881
}

crates/notedeck/src/lib.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -57,7 +57,7 @@ pub use fonts::NamedFontFamily;
5757
pub use i18n::{CacheStats, FluentArgs, FluentValue, LanguageIdentifier, Localization};
5858
pub use imgcache::{
5959
Animation, GifState, GifStateMap, ImageFrame, Images, LatestTexture, MediaCache,
60-
MediaCacheType, TextureFrame, TextureState, TexturesCache,
60+
MediaCacheType, TextureFrame, TextureState, TexturesCache, WebpState, WebpStateMap,
6161
};
6262
pub use jobs::{
6363
deliver_completed_media_job, run_media_job_pre_action, JobCache, JobPool, MediaJobSender,

crates/notedeck/src/media/action.rs

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -116,6 +116,12 @@ impl MediaAction {
116116
.animated
117117
.request(jobs, ctx, &url, ImageType::Content(None))
118118
}
119+
MediaCacheType::Webp => {
120+
images
121+
.textures
122+
.webp
123+
.request(jobs, ctx, &url, ImageType::Content(None))
124+
}
119125
},
120126
MediaAction::DoneLoading { url, cache_type: _ } => {
121127
images.textures.blurred.finished_transitioning(&url);

0 commit comments

Comments
 (0)