Skip to content

Adding WEBP support#1294

Open
sergey3bv wants to merge 3 commits intodamus-io:masterfrom
sergey3bv:feat/webp-support
Open

Adding WEBP support#1294
sergey3bv wants to merge 3 commits intodamus-io:masterfrom
sergey3bv:feat/webp-support

Conversation

@sergey3bv
Copy link
Contributor

@sergey3bv sergey3bv commented Feb 12, 2026

Addressing #753 to support both static and animated WEBPs.

Summary by CodeRabbit

  • New Features

    • Added WebP support (static and animated): auto-detection, on-disk/network caching, decoding, and per-frame playback integrated with renderers. GIF and WebP now share a unified animation playback system; FPS inference treats WebP like GIF.
  • Chores

    • Wired WebP handling throughout media pipelines and added workspace WebP dependency.

@coderabbitai
Copy link

coderabbitai bot commented Feb 12, 2026

Note

Reviews paused

It looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the reviews.auto_review.auto_pause_after_reviewed_commits setting.

Use the following commands to manage reviews:

  • @coderabbitai resume to resume automatic reviews.
  • @coderabbitai review to trigger a single review.

Use the checkboxes below for quick actions:

  • ▶️ Resume reviews
  • 🔍 Trigger review
📝 Walkthrough

Walkthrough

Adds first-class WebP support: new media/webp module, WebP cache and state tracking, job/loader integration for static and animated WebP, MIME mapping, workspace webp dependency, and UI rendering/repainters wired across media/cache/job layers.

Changes

Cohort / File(s) Summary
Workspace deps
Cargo.toml, crates/notedeck/Cargo.toml
Removed image crate webp feature; added workspace dependency webp = "0.3.1"; minor formatting tweaks.
WebP implementation
crates/notedeck/src/media/webp.rs
New module: WebP type detection (static vs animated), decoding, animation packaging, texture/frame generation, disk/network job handlers, and public WebpTexCache API.
Animated core
crates/notedeck/src/media/animated.rs
New animation primitives: AnimatedFrameState, ProcessedAnimatedFrame, process_animation_frame, compute_next_frame_time, and AnimationBuilder used by GIF/WebP.
Media cache core
crates/notedeck/src/imgcache.rs
Added TexturesCache.webp, Images.webps, webp_states, WebpState/WebpStateMap, MediaCacheType::Webp, MediaCache write-bytes helper, migration and loader wiring for WebP.
Job system
crates/notedeck/src/jobs/media.rs
Added MediaJobKind::WebpImg and corresponding MediaJobResult variant; job lifecycle initializes and delivers WebP cache entries into tex_cache.webp.
Latest-texture processing
crates/notedeck/src/media/latest.rs
Extended NoLoadingLatestTex with webp_cache/webp_state; integrated process_webp_frame flow and WebP branch into latest_state and repaint scheduling.
Media actions & MIME
crates/notedeck/src/media/action.rs, crates/notedeck/src/urls.rs
FetchImage routes MediaCacheType::Webp to images.textures.webp.request; MIME mapping returns MediaCacheType::Webp for image/webp.
Module layout & exports
crates/notedeck/src/media/mod.rs, crates/notedeck/src/lib.rs
Added pub mod webp and pub mod animated; re-exported WebpState and WebpStateMap from imgcache.
GIF refactor
crates/notedeck/src/media/gif.rs
GIF frame logic refactored to reuse animated core: replaced custom frame processing with aliases to ProcessedAnimatedFrame and process_animation_frame.
UI rendering
crates/notedeck_ui/src/note/media.rs
Default FPS inference treats WebP like GIF (24.0) for animation-mode selection.

Sequence Diagram(s)

sequenceDiagram
    actor Client
    participant UI as UI Renderer
    participant Action as Action System
    participant Jobs as Job System
    participant Loader as WebP Loader
    participant Cache as WebpTexCache

    Client->>UI: request render (WebP URL)
    UI->>Action: FetchImage(WebP)
    Action->>Jobs: enqueue WebP load (url)
    Jobs->>Loader: load from disk or network
    Loader->>Loader: detect_webp_type(bytes)
    alt Animated
        Loader->>Loader: generate_webp_anim_pkg()
        Loader->>Cache: insert Animation + frames
    else Static
        Loader->>Cache: insert TextureHandle
    end
    Cache-->>UI: texture + WebpState
    UI->>UI: schedule repaints / render frames
    UI-->>Client: display updated frames
Loading

Estimated Code Review Effort

🎯 4 (Complex) | ⏱️ ~60 minutes

Poem

🐰 I found a webp in the meadow bright,

Frames that hop and hold and hop again,
Cached in my burrow, timed just right,
I twitch my whiskers at each light,
Pixels leap — I thump with joy!

🚥 Pre-merge checks | ✅ 3
✅ Passed checks (3 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title 'Adding WEBP support' directly and concisely describes the main change: introduction of WebP image format support throughout the codebase.
Docstring Coverage ✅ Passed Docstring coverage is 100.00% which is sufficient. The required threshold is 80.00%.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 8

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
crates/notedeck/src/jobs/media.rs (1)

26-64: ⚠️ Potential issue | 🟠 Major

Fix WebP cache routing inconsistency in pre-action phase.

WebP files are marked Pending in animated.cache (media.rs:81-82) but results are delivered to webp.cache (media.rs:49-50). Since requests query the type-specific cache (webp_cache for MediaCacheType::Webp), the Pending state is never found, causing duplicate requests for the same WebP file. Align pre-action to mark Pending in webp.cache for WebP files, or detect the file extension during job kind assignment to avoid extension-based routing altogether.

🤖 Fix all issues with AI agents
In `@Cargo.toml`:
- Line 59: Update the webp crate version in Cargo.toml from the open-ended "0.3"
to the fixed release "0.3.1" to address RUSTSEC-2024-0443; locate the dependency
entry for the webp crate (the line containing webp = "0.3") and change it to
webp = "0.3.1" so Cargo will pull the patched 0.3.1 release (this affects the
standalone webp crate APIs like AnimDecoder and Encoder).

In `@crates/notedeck/src/imgcache.rs`:
- Around line 232-258: The current animated-WebP branch encodes frames into
encoded_frames but then writes only the first frame to disk
(encoded_frames.first()) causing animated images to be cached as static; fix by
caching the original network bytes instead of re-encoding: in the else branch
where you iterate over data/frame_data and build encoded_frames, replace the
re-encode-and-write logic with writing the original downloaded byte buffer (the
raw bytes that produced data) to file_path so from_disk_job_run and WebpType
detection see the original animated WebP; reference the variables
encoded_frames, frame_data, file_path, data, and the
from_disk_job_run/WebpType::Static behavior when making the change and add a
TODO comment to implement proper animation encoding later.

In `@crates/notedeck/src/jobs/media.rs`:
- Around line 47-53: The routing currently inspects
id.to_lowercase().ends_with(".webp") in the delivery path and always inserts
TextureState::Pending into tex_cache.animated.cache in run_media_job_pre_action,
causing mismatches for WebP results; update the job metadata instead: add a
MediaJobKind::WebpImg (or include a MediaCacheType field on the job) and change
run_media_job_pre_action to insert Pending into the appropriate cache based on
that kind/field, then change the delivery code (the branch that now checks
id.ends_with(".webp")) to consult job.kind/MediaCacheType and insert the result
into the matching tex_cache.webp.cache or tex_cache.animated.cache so pre-action
and delivery use the same canonical cache routing.
- Line 32: The local binding job_kind (completed.job_id.job_kind.clone()) is
unused and causes a compiler warning; either remove that line or use job_kind to
drive the routing logic instead of relying on file extension string matching.
Locate the code that currently inspects file extensions for dispatch
(referencing completed and completed.job_id) and replace the extension checks
with a match/if on the job_kind value (or, if you prefer removal, delete the let
job_kind = ... line and keep the existing extension logic). Ensure you update
any pattern matching or routing functions so they consume job_kind consistently
and eliminate the unused-variable warning.

In `@crates/notedeck/src/media/webp.rs`:
- Around line 345-384: get_or_request currently dispatches a job (via jobs.send
and JobPackage in get_or_request) but never records the URL as pending in
self.cache, which lets duplicate jobs be sent; after successfully sending the
job (both the sync from_disk branch and the async from_net branch) insert a
Pending marker into self.cache keyed by MediaCache::key(url) (i.e.
self.cache.insert(key.clone(), TextureState::Pending) or equivalent) so
subsequent calls return the cached Pending state instead of re-dispatching; also
verify run_media_job_pre_action updates the same cache (webp cache) used here
rather than animated.cache so the post-job state transitions operate on the same
cache.
- Around line 81-101: The fallback detector try_detect_with_image_crate
currently decodes and collects all frames via decoder.into_frames().collect()
just to check frames.len(), causing heavy allocation/decoding for animated WebP;
instead implement a lightweight check that avoids full frame decoding (e.g.,
iterate frames streamingly without collecting or better: parse the RIFF
container for ANMF chunks) and return WebpType::Animated on the first ANMF/frame
header found, WebpType::Static if only a single frame/header is present, and an
error if none; update try_detect_with_image_crate to use either a streaming
iterator over decoder.into_frames() (counting up to 2 frames) or a RIFF/ANMF
parser rather than building a VecDeque<Frame>, keeping function signature and
error messages intact.
- Around line 52-78: detect_webp_type currently calls
webp::AnimDecoder::new(...).decode() which fully decodes frames (via
anim_decoder.decode()) and is expensive; instead either parse the WebP RIFF
headers to look for the ANIM chunk (i.e., inspect the header bytes for the
"RIFF" / "WEBP" / "ANIM" chunk markers) to determine animation without decoding,
or change detect_webp_type to use a lightweight header-only check before falling
back to try_detect_with_image_crate; update the function (detect_webp_type) to
implement the header-chunk inspection path (or adjust the docstring to state
that full decode occurs) and remove the full decode call (anim_decoder.decode())
from the hot path so large animated WebPs are not fully decoded unnecessarily.
- Around line 117-141: The decode_static_webp function currently assumes
decode() returns RGBA and passes image.as_ref() into
ColorImage::from_rgba_unmultiplied, which will panic for RGB data; update
decode_static_webp to mirror decode_static_webp_processed: check
image.color_mode() or image.as_ref().len() to detect 3 vs 4 bytes per pixel,
convert RGB->RGBA by inserting an opaque alpha byte for each pixel when
necessary, then construct the ColorImage via ColorImage::from_rgba_unmultiplied
and call load_texture_checked as before (referencing decode_static_webp,
decode_static_webp_processed, ColorImage::from_rgba_unmultiplied, and
load_texture_checked to locate the code).
🧹 Nitpick comments (7)
crates/notedeck/src/media/webp.rs (3)

221-298: process_webp_frame duplicates GIF frame processing logic.

WebpState is structurally identical to GifState, and process_webp_frame mirrors the GIF equivalent. This violates DRY — consider a generic process_animation_frame function parameterized over the state type, or unify GifState/WebpState into a single AnimationFrameState.


456-465: Unnecessary img.clone() for static WebP from network.

Line 465: img.clone() copies the entire ColorImage pixel buffer just to pass it into the write-to-disk closure. Since the ColorImage is consumed by load_texture_checked only for its data (which is uploaded to GPU), consider restructuring to avoid the clone — e.g., write to disk first, then create the texture, or use Arc<ColorImage>.


558-564: Each animation frame's ColorImage is cloned.

Line 563: image: img.clone() copies the full pixel buffer for every frame to build ImageFrame for disk caching, after the same data is also used to create a GPU texture. For a 100-frame animation at 512×512, this is ~100 MB of extra allocations. Consider taking ownership of img for the ImageFrame and creating the texture from borrowed data, or using Arc.

crates/notedeck/src/imgcache.rs (1)

513-520: WebpState is structurally identical to GifState — consider unifying.

Both structs have the same four fields with the same types. A single AnimationFrameState type alias or struct could serve both, reducing maintenance burden and enabling shared frame-processing logic.

crates/notedeck_ui/src/note/media.rs (1)

134-140: Nit: combine identical match arms.

Gif and Webp both return Some(24.0). You can merge them into a single arm for conciseness.

♻️ Suggested diff
-        let fps = match media_type {
-            MediaCacheType::Gif => Some(24.0),
-            MediaCacheType::Webp => Some(24.0),
-            MediaCacheType::Image => None,
-        };
+        let fps = match media_type {
+            MediaCacheType::Gif | MediaCacheType::Webp => Some(24.0),
+            MediaCacheType::Image => None,
+        };
crates/notedeck/src/media/latest.rs (2)

116-136: LGTM — but consider extracting shared animation logic.

The WebP branch is correct and follows the established GIF pattern. However, the GIF (lines 93–115) and WebP (lines 116–136) branches are nearly identical, and the underlying process_gif_frame / process_webp_frame functions (in gif.rs and webp.rs) are also structurally identical. This duplication will compound as more animated formats are added.

Consider introducing a common AnimatedCache trait (or a generic helper) that both GIF and WebP caches implement, so latest_state can handle any animated format through a single code path. This isn't blocking, but worth tracking for a follow-up.


33-55: NoLoadingLatestTex grows linearly with each new animated format.

Each new animated format (GIF, WebP, and any future ones) adds two fields (cache + state map) to this struct and its constructor. A map-based or trait-object approach would keep this struct stable as formats are added. Same recommendation as above — not blocking, but good to keep in mind.

@alltheseas
Copy link
Contributor

@sergey3bv thanks for the PR. Can you please address coderabbitai review feedback, and run your code against AGENTS.md found in #1263

@sergey3bv
Copy link
Contributor Author

sergey3bv commented Feb 12, 2026

Can you please address coderabbitai review feedback

Yes, I am working on it

@sergey3bv sergey3bv marked this pull request as draft February 13, 2026 14:12
@sergey3bv sergey3bv force-pushed the feat/webp-support branch 3 times, most recently from e02fbb8 to e5f035d Compare February 18, 2026 10:09
@sergey3bv sergey3bv marked this pull request as ready for review February 18, 2026 10:09
Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🧹 Nitpick comments (3)
crates/notedeck/src/media/webp.rs (2)

405-424: Minor: prefer is_empty() over len() == 0.

Line 413 uses f.len() == 0. Clippy's len_zero lint typically suggests f.is_empty() if available on the type. Verify the type returned by AnimDecoder::decode() supports is_empty(); if so, switch to it. As per coding guidelines: "Run cargo fmt, cargo clippy, cargo test before committing."

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@crates/notedeck/src/media/webp.rs` around lines 405 - 424, In
generate_webp_anim_pkg, replace the length check f.len() == 0 with f.is_empty()
(if the collection returned by AnimDecoder::decode() supports is_empty()) to
satisfy Clippy's len_zero lint; update the match branch that handles the decoded
frames from AnimDecoder::decode() to use f.is_empty() for the empty-case check
and then proceed to return f as before, and run cargo fmt, cargo clippy, cargo
test before committing.

296-330: from_disk_job_run generates and discards img_frames for animated WebP loaded from cache.

generate_webp_anim_pkg produces both anim (GPU textures) and img_frames (CPU ColorImage copies). The disk path only uses .map(|f| f.anim), meaning every frame's ColorImage is cloned (line 452) and immediately dropped. For large animations this is a non-trivial waste of memory and CPU.

Consider a flag or a separate decode path that skips img_frames generation when loading from disk.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@crates/notedeck/src/media/webp.rs` around lines 296 - 330, from_disk_job_run
is calling generate_webp_anim_pkg and then immediately discarding img_frames by
mapping to f.anim, causing unnecessary cloning of ColorImage frames; change
generate_webp_anim_pkg to accept a flag (e.g., produce_cpu_frames: bool) or add
a new function (e.g., generate_webp_anim_pkg_no_cpu_frames) that decodes only
GPU textures (anim) without creating img_frames, then call that new/flagged path
from the Animated branch in from_disk_job_run so it returns only anim and avoids
generating or cloning ColorImage frames (refer to generate_webp_anim_pkg,
img_frames, anim, and buffer_to_color_image to locate the code to change).
crates/notedeck/src/imgcache.rs (1)

489-505: WebpState is structurally identical to GifState — consider a shared animation state type.

Both GifState and WebpState have the exact same fields (last_frame_rendered, last_frame_duration, next_frame_time, last_frame_index). Similarly, process_webp_frame in webp.rs is a near-verbatim copy of process_gif_frame in gif.rs. Extracting a generic AnimFrameState (and a generic process_anim_frame) would eliminate this duplication and simplify adding future animated formats.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@crates/notedeck/src/imgcache.rs` around lines 489 - 505, GifState and
WebpState are identical duplicates (and GifStateMap/WebpStateMap mirror each
other) and process_gif_frame/process_webp_frame are near-verbatim; replace them
with a single generic AnimFrameState struct (or AnimState) containing
last_frame_rendered, last_frame_duration, next_frame_time, last_frame_index and
use a type alias (e.g., AnimStateMap) for HashMap<String, AnimFrameState>, then
refactor process_gif_frame and process_webp_frame into a shared
process_anim_frame function that accepts format-specific behavior via parameters
or a trait so both gif.rs and webp.rs call the same generic logic with only
format-specific pieces provided.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@crates/notedeck/src/imgcache.rs`:
- Around line 224-241: The timestamp calculation uses index *
current_frame_delay which breaks for variable delays; change it to compute a
cumulative presentation timestamp by summing each frame's delay (with the same
as_millis() -> clamp-to-300ms fallback logic) for all frames up to the current
one and pass that running total to AnimFrame::from_rgba; implement a mutable
accumulator (e.g., let mut cumulative_ts = 0i32) outside/above the for (index,
frame) in data.iter().enumerate() loop and update it per-frame before calling
encoder.add_frame so encoder.add_frame(..., cumulative_ts) reflects the correct
ms-from-start presentation time.

---

Duplicate comments:
In `@crates/notedeck/src/media/webp.rs`:
- Around line 254-293: get_or_request currently takes &self and dispatches jobs
without marking the URL Pending in the webp cache, causing duplicate jobs;
either update the job pre-action in jobs/media.rs (run_media_job_pre_action) so
that when handling MediaJobKind::WebpImg it inserts
TextureState::<WebpCacheEntry>::Pending into the webp cache key for the URL, or
change get_or_request to take &mut self and immediately insert Pending into
self.cache (using MediaCache::key(url) and TextureState::Pending) right after
dispatching the JobPackage; reference get_or_request,
TextureState::<WebpCacheEntry>::Pending, MediaJobKind::WebpImg, and
run_media_job_pre_action to locate the places to update.

---

Nitpick comments:
In `@crates/notedeck/src/imgcache.rs`:
- Around line 489-505: GifState and WebpState are identical duplicates (and
GifStateMap/WebpStateMap mirror each other) and
process_gif_frame/process_webp_frame are near-verbatim; replace them with a
single generic AnimFrameState struct (or AnimState) containing
last_frame_rendered, last_frame_duration, next_frame_time, last_frame_index and
use a type alias (e.g., AnimStateMap) for HashMap<String, AnimFrameState>, then
refactor process_gif_frame and process_webp_frame into a shared
process_anim_frame function that accepts format-specific behavior via parameters
or a trait so both gif.rs and webp.rs call the same generic logic with only
format-specific pieces provided.

In `@crates/notedeck/src/media/webp.rs`:
- Around line 405-424: In generate_webp_anim_pkg, replace the length check
f.len() == 0 with f.is_empty() (if the collection returned by
AnimDecoder::decode() supports is_empty()) to satisfy Clippy's len_zero lint;
update the match branch that handles the decoded frames from
AnimDecoder::decode() to use f.is_empty() for the empty-case check and then
proceed to return f as before, and run cargo fmt, cargo clippy, cargo test
before committing.
- Around line 296-330: from_disk_job_run is calling generate_webp_anim_pkg and
then immediately discarding img_frames by mapping to f.anim, causing unnecessary
cloning of ColorImage frames; change generate_webp_anim_pkg to accept a flag
(e.g., produce_cpu_frames: bool) or add a new function (e.g.,
generate_webp_anim_pkg_no_cpu_frames) that decodes only GPU textures (anim)
without creating img_frames, then call that new/flagged path from the Animated
branch in from_disk_job_run so it returns only anim and avoids generating or
cloning ColorImage frames (refer to generate_webp_anim_pkg, img_frames, anim,
and buffer_to_color_image to locate the code to change).

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🧹 Nitpick comments (4)
crates/notedeck/src/media/webp.rs (2)

411-414: Prefer is_empty() over len() == 0.

Clippy's clippy::len_zero lint flags this pattern. As per coding guidelines, cargo clippy must pass before committing.

♻️ Proposed fix
-        if image.len() == 0 {
+        if image.is_empty() {
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@crates/notedeck/src/media/webp.rs` around lines 411 - 414, Replace the manual
length check used when decoding frames with the idiomatic is_empty() call to
satisfy clippy's len_zero lint: in the AnimDecoder::new(webp_bytes).decode()
match arm where you currently test `if image.len() == 0` (the block that returns
Err(Generic("No frames found in webp"...))), change it to `if image.is_empty()`
so the decoded-empty check uses is_empty() instead of len() == 0.

405-410: Missing #[profiling::function] on generate_webp_anim_pkg and Copy bound needs verification.

Two concerns:

  1. generate_webp_anim_pkg fully decodes all frames and uploads GPU textures for each — this is the single most expensive call in the WebP loading path. Per coding guidelines, performance-critical functions should be annotated with #[profiling::function].

  2. The process_to_egui: impl Fn(DynamicImage) -> ColorImage + Send + Copy + 'static bound requires the closure to be Copy. In from_net_run (Line 380) this is move |img| process_image(imgtype, img), which is Copy only if ImageType: Copy. Verify that ImageType derives Copy; if not, this is a compile error.

♻️ Proposed annotation
+#[profiling::function]
 fn generate_webp_anim_pkg(

As per coding guidelines: "Mark performance-critical functions with #[profiling::function] for visibility in the puffin profiler."

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@crates/notedeck/src/media/webp.rs` around lines 405 - 410, The function
generate_webp_anim_pkg is performance-critical and should be annotated with
#[profiling::function]; add that attribute above its signature. Also remove or
relax the unnecessary Copy requirement on the process_to_egui parameter
(currently impl Fn(DynamicImage) -> ColorImage + Send + Copy + 'static) — either
drop + Copy or change the bound to a Clone/Arc-wrapped closure as appropriate;
if you keep Copy then ensure the captured ImageType used in from_net_run is
actually derived Copy (verify ImageType derives Copy) and if not, make ImageType
Copy or change the closure bound instead so from_net_run's move |img|
process_image(...) compiles. Ensure only one of these fixes is applied to match
how the closure and ImageType are used.
crates/notedeck/src/imgcache.rs (2)

196-248: Previous encoding bugs addressed; fold for side effects is a minor code smell.

Both previously flagged issues — dropping all frames except the first, and the incorrect non-cumulative timestamp — are now correctly handled: all frames are written via AnimEncoder, and the fold accumulates a running cumulative timestamp. Good.

The fold is used solely for its side effect of mutating encoder, which is unconventional. A for loop with an explicit acc variable is more readable and avoids let _ = suppression.

♻️ Proposed refactor
-        let _ = data.iter().fold(0i32, |acc_timestamp, frame| {
-            let [width, height] = frame.image.size;
-            let delay = frame.delay.as_millis();
-            let frame_delay = if delay < i32::MAX as u128 {
-                delay as i32
-            } else {
-                300i32
-            };
-
-            let timestamp = acc_timestamp;
-
-            encoder.add_frame(AnimFrame::from_rgba(
-                frame.image.as_raw(),
-                width as u32,
-                height as u32,
-                timestamp,
-            ));
-
-            acc_timestamp.saturating_add(frame_delay)
-        });
+        let mut cumulative_ts = 0i32;
+        for frame in &data {
+            let [width, height] = frame.image.size;
+            let delay_ms = {
+                let d = frame.delay.as_millis();
+                if d < i32::MAX as u128 { d as i32 } else { 300i32 }
+            };
+            encoder.add_frame(AnimFrame::from_rgba(
+                frame.image.as_raw(),
+                width as u32,
+                height as u32,
+                cumulative_ts,
+            ));
+            cumulative_ts = cumulative_ts.saturating_add(delay_ms);
+        }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@crates/notedeck/src/imgcache.rs` around lines 196 - 248, The write_webp
function currently uses data.iter().fold(...) solely for side effects on encoder
which is a code smell; replace the fold with an explicit for loop that declares
let mut acc_timestamp = 0i32 and then iterates over data (or data.iter())
computing frame_delay and timestamp the same way and calling
encoder.add_frame(AnimFrame::from_rgba(...)) inside the loop, updating
acc_timestamp with acc_timestamp = acc_timestamp.saturating_add(frame_delay);
keep the same uses of encoder, frame_delay, timestamp, and AnimFrame::from_rgba
to preserve behavior.

490-506: WebpState and GifState are identical structs.

Both contain exactly the same four fields (last_frame_rendered, last_frame_duration, next_frame_time, last_frame_index). Consider unifying them into a single generic AnimFrameState struct to avoid duplication and keep future maintenance in sync.

♻️ Proposed refactor
-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,
-}
-
-pub type WebpStateMap = HashMap<String, WebpState>;
-
-pub struct WebpState {
-    pub last_frame_rendered: Instant,
-    pub last_frame_duration: Duration,
-    pub next_frame_time: Option<SystemTime>,
-    pub last_frame_index: usize,
-}
+/// Shared playback state for frame-based animations (GIF, WebP, etc.)
+pub struct AnimFrameState {
+    pub last_frame_rendered: Instant,
+    pub last_frame_duration: Duration,
+    pub next_frame_time: Option<SystemTime>,
+    pub last_frame_index: usize,
+}
+
+pub type GifStateMap = HashMap<String, AnimFrameState>;
+pub type GifState = AnimFrameState; // back-compat alias
+pub type WebpStateMap = HashMap<String, AnimFrameState>;
+pub type WebpState = AnimFrameState; // back-compat alias
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@crates/notedeck/src/imgcache.rs` around lines 490 - 506, The two identical
structs GifState and WebpState should be merged into a single AnimFrameState
struct to remove duplication: replace both GifState and WebpState definitions
with one pub struct AnimFrameState containing the existing fields
(last_frame_rendered, last_frame_duration, next_frame_time, last_frame_index)
and update GifStateMap and WebpStateMap to be type aliases using AnimFrameState
(e.g., pub type GifStateMap = HashMap<String, AnimFrameState>; pub type
WebpStateMap = HashMap<String, AnimFrameState>), then update any references to
GifState or WebpState to use AnimFrameState (or keep short type aliases if you
want to retain names).
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Duplicate comments:
In `@crates/notedeck/src/media/webp.rs`:
- Around line 254-293: get_or_request currently uses &self so it never records a
Pending marker in self.cache, allowing duplicate job dispatches; fix by either
changing get_or_request (and request) to take &mut self and immediately insert
TextureState::Pending into self.cache (or replace the cache entry) right after
enqueueing the JobPackage (use the same key produced by MediaCache::key(url)),
or keep &self and add an interior-mutable in-flight set to WebpTexCache (e.g.,
HashSet<String> behind Mutex/RwLock/RefCell) and check/insert the url into that
set before sending the job via jobs.send to prevent re-dispatch. Ensure you
reference WebpTexCache, get_or_request, self.cache, TextureState::Pending, and
the JobPackage enqueue points when making the change.

---

Nitpick comments:
In `@crates/notedeck/src/imgcache.rs`:
- Around line 196-248: The write_webp function currently uses
data.iter().fold(...) solely for side effects on encoder which is a code smell;
replace the fold with an explicit for loop that declares let mut acc_timestamp =
0i32 and then iterates over data (or data.iter()) computing frame_delay and
timestamp the same way and calling encoder.add_frame(AnimFrame::from_rgba(...))
inside the loop, updating acc_timestamp with acc_timestamp =
acc_timestamp.saturating_add(frame_delay); keep the same uses of encoder,
frame_delay, timestamp, and AnimFrame::from_rgba to preserve behavior.
- Around line 490-506: The two identical structs GifState and WebpState should
be merged into a single AnimFrameState struct to remove duplication: replace
both GifState and WebpState definitions with one pub struct AnimFrameState
containing the existing fields (last_frame_rendered, last_frame_duration,
next_frame_time, last_frame_index) and update GifStateMap and WebpStateMap to be
type aliases using AnimFrameState (e.g., pub type GifStateMap = HashMap<String,
AnimFrameState>; pub type WebpStateMap = HashMap<String, AnimFrameState>), then
update any references to GifState or WebpState to use AnimFrameState (or keep
short type aliases if you want to retain names).

In `@crates/notedeck/src/media/webp.rs`:
- Around line 411-414: Replace the manual length check used when decoding frames
with the idiomatic is_empty() call to satisfy clippy's len_zero lint: in the
AnimDecoder::new(webp_bytes).decode() match arm where you currently test `if
image.len() == 0` (the block that returns Err(Generic("No frames found in
webp"...))), change it to `if image.is_empty()` so the decoded-empty check uses
is_empty() instead of len() == 0.
- Around line 405-410: The function generate_webp_anim_pkg is
performance-critical and should be annotated with #[profiling::function]; add
that attribute above its signature. Also remove or relax the unnecessary Copy
requirement on the process_to_egui parameter (currently impl Fn(DynamicImage) ->
ColorImage + Send + Copy + 'static) — either drop + Copy or change the bound to
a Clone/Arc-wrapped closure as appropriate; if you keep Copy then ensure the
captured ImageType used in from_net_run is actually derived Copy (verify
ImageType derives Copy) and if not, make ImageType Copy or change the closure
bound instead so from_net_run's move |img| process_image(...) compiles. Ensure
only one of these fixes is applied to match how the closure and ImageType are
used.

@sergey3bv
Copy link
Contributor Author

@alltheseas, I think the PR is ready for a review.

@alltheseas
Copy link
Contributor

  1. High — WebP rendering is broken due to cache routing mismatch

run_media_job_pre_action inserts Pending into tex_cache.webp.cache (jobs/media.rs:77):

MediaJobKind::WebpImg => {
tex_cache.webp.cache.insert(id, TextureState::Pending);
}

Completed jobs return MediaJobResult::StaticImg / Animation, which deliver_completed_media_job routes to the wrong caches (jobs/media.rs:39,46):

MediaJobResult::StaticImg(job_complete) => {
// ...
tex_cache.static_image.cache.insert(id, r); // not webp.cache
}
MediaJobResult::Animation(animation) => {
// ...
tex_cache.animated.cache.insert(id, r); // not webp.cache
}

The UI reads only webp_cache (latest.rs:116):

MediaCacheType::Webp => match self.webp_cache.get_or_request(jobs, ctx, url, imgtype) {

Entries stay Pending indefinitely. Feature is non-functional.

  1. High — Redundant C FFI dependency

Adds webp = "0.3.1" (Cargo.toml:59) pulling libwebp-sys, when image already has WebP enabled (Cargo.toml:58):

image = { version = "0.25", features = ["jpeg", "png", "webp"] }
webp = "0.3.1" # redundant — image already decodes WebP

The image crate's WebPDecoder implements AnimationDecoder::into_frames() — the same trait used by GifDecoder (gif.rs:22,255):

// Existing GIF pattern:
let decoder = GifDecoder::new(reader)?;
let frames = decoder.into_frames().collect()?;

// Could reuse for WebP:
let decoder = WebPDecoder::new(reader)?;
let frames = decoder.into_frames().collect()?;

  1. Medium — ~525 lines of duplicated code

Near-copy pairs. Example — WebpState vs GifState (imgcache.rs:501 vs imgcache.rs:492):

// GifState
pub struct GifState {
pub last_frame_rendered: Instant,
pub last_frame_duration: Duration,
pub next_frame_time: Option,
pub last_frame_index: usize,
}

// WebpState — identical
pub struct WebpState {
pub last_frame_rendered: Instant,
pub last_frame_duration: Duration,
pub next_frame_time: Option,
pub last_frame_index: usize,
}

Same applies to process_webp_frame/process_gif_frame (webp.rs:133 vs gif.rs:33), WebpTexCache/AnimatedImgTexCache (webp.rs:218 vs gif.rs:112), from_disk_job_run
(webp.rs:296 vs gif.rs:185), and generate_webp_anim_pkg/generate_anim_pkg (webp.rs:405 vs gif.rs:244). This duplication/parallel pipeline contributed to the routing
bug in finding #1.

  1. Medium — Static WebP decode path is redundant

decode_static_webp / decode_static_webp_processed (webp.rs:69-123) duplicate what two existing paths already do via image::load_from_memory(). Network path
(images.rs:195):

} else if content_type.starts_with("image/") {
profiling::scope!("load_from_memory");
let dyn_image = image::load_from_memory(&response.bytes)?;
Ok(process_image(imgtyp, dyn_image))
}

And the disk static loader (static_imgs.rs:114). Both already handle all image/* types including WebP.

  1. Medium — write_webp re-encodes frames instead of saving original bytes

write_webp (imgcache.rs:196-248) re-encodes every frame through AnimEncoder:

let mut config = webp::WebPConfig::new().or(/* ... */)?;
config.lossless = 1;
// ... re-encodes all frames through AnimEncoder
let webp = encoder.encode();
Ok(std::fs::write(file_path, &*webp)?)

Lossless, so no quality loss — but the original network bytes could be written directly to disk, avoiding unnecessary CPU work.

  1. Low — from_disk_job_run wastes allocations

generate_webp_anim_pkg produces both anim and img_frames. The disk path discards img_frames immediately (webp.rs:313):

generate_webp_anim_pkg(ctx.clone(), url.to_owned(), webp_bytes.as_slice(), |img| {
buffer_to_color_image(img.as_flat_samples_u8(), img.width(), img.height())
})
.map(|f| f.anim), // img_frames cloned at webp.rs:449,452 then thrown away

  1. Low — Single 692-line commit across 12 files

Not human-reviewable per project guidelines. Includes unrelated whitespace changes (Cargo.toml:20,52):

-chrono = "0.4.40"
+chrono = "0.4.40"
-enostr = { path = "crates/enostr" }
+enostr = { path = "crates/enostr" }

Should be 3-4 logically distinct standalone commits.

  1. Low — Missing #[profiling::function] on hot paths

generate_webp_anim_pkg (webp.rs:405) does full frame decode + texture upload with no profiling annotation:

fn generate_webp_anim_pkg( // no #[profiling::function]
ctx: egui::Context,
url: String,
webp_bytes: &[u8],
process_to_egui: impl Fn(DynamicImage) -> ColorImage + Send + Copy + 'static,
) -> Result<AnimationPackage, Error> {

Comparable existing functions (images.rs:52,129,176) all use #[profiling::function].

  1. Low — fold used for side effects in write_webp

(imgcache.rs:224):

let _ = data.iter().fold(0i32, |acc_timestamp, frame| {
// ... mutates encoder as side effect
encoder.add_frame(AnimFrame::from_rgba(/* ... */));
acc_timestamp.saturating_add(frame_delay)
});

A for loop with an explicit accumulator is more readable.

  1. Low — Identical match arms not combined

(notedeck_ui/src/note/media.rs:134-135):

let fps = match media_type {
MediaCacheType::Gif => Some(24.0),
MediaCacheType::Webp => Some(24.0), // should be: Gif | Webp => Some(24.0)
MediaCacheType::Image => None,
};

@sergey3bv
Copy link
Contributor Author

sergey3bv commented Feb 19, 2026

@alltheseas, got it, will fix it next week

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 2

🧹 Nitpick comments (1)
crates/notedeck/src/imgcache.rs (1)

196-247: Add profiler annotation on the new WebP encode hot path.

write_webp is CPU-heavy (frame loop + encoding). It should be annotated for puffin visibility.

Proposed fix
+    #[profiling::function]
     pub fn write_webp(cache_dir: &path::Path, url: &str, data: Vec<ImageFrame>) -> Result<()> {

As per coding guidelines "Mark performance-critical functions with #[profiling::function] for visibility in the puffin profiler".

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@crates/notedeck/src/imgcache.rs` around lines 196 - 247, Add a puffin
profiler annotation to the CPU-heavy WebP encode path by putting the attribute
#[profiling::function] directly above the pub fn write_webp(...) declaration in
imgcache.rs; this marks the write_webp function for profiling (ensure the
profiling crate is in scope/compiled in this crate as required).
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@crates/notedeck/src/imgcache.rs`:
- Around line 61-65: Add missing rustdoc comments for the newly added public
API: document TextureState::is_loaded (what it checks/returns), the write_webp
function (purpose, parameters, return/behavior), the WebpStateMap type (what it
maps/usage), and the WebpState enum (variants meaning and lifecycle). Place
concise /// comments above the definitions of TextureState::is_loaded,
write_webp, WebpStateMap, and WebpState describing their intent, inputs/outputs,
and any important invariants or thread-safety/async notes so they meet the
crate's docstring coverage guidelines.
- Around line 218-241: The animated WebP encoder is built from
reference_frame.image.size but subsequent frames are added without checking
dimensions, which can break AnimEncoder; before calling AnimFrame::from_rgba and
encoder.add_frame in the loop, validate that frame.image.size equals
reference_frame.image.size (compare width and height) and handle mismatches
(either resize/pad the frame to the canvas size or return an error/skip the
frame) so that all frames match the canvas; update the loop around
encoder.add_frame / timestamp to perform this size check using the
frame.image.size, and if you choose to error, return a descriptive error; if you
choose to resize, use the existing image utils/helpers so frames passed to
AnimFrame::from_rgba always match the encoder canvas.

---

Nitpick comments:
In `@crates/notedeck/src/imgcache.rs`:
- Around line 196-247: Add a puffin profiler annotation to the CPU-heavy WebP
encode path by putting the attribute #[profiling::function] directly above the
pub fn write_webp(...) declaration in imgcache.rs; this marks the write_webp
function for profiling (ensure the profiling crate is in scope/compiled in this
crate as required).

ℹ️ Review info

Configuration used: defaults

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 935f5ee and 901d848.

📒 Files selected for processing (2)
  • crates/notedeck/src/imgcache.rs
  • crates/notedeck_ui/src/note/media.rs
🚧 Files skipped from review as they are similar to previous changes (1)
  • crates/notedeck_ui/src/note/media.rs

@sergey3bv sergey3bv marked this pull request as draft March 2, 2026 13:03
@sergey3bv sergey3bv force-pushed the feat/webp-support branch 3 times, most recently from d634888 to e3124f0 Compare March 4, 2026 12:02
@sergey3bv sergey3bv marked this pull request as ready for review March 4, 2026 12:03
Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 2

♻️ Duplicate comments (2)
crates/notedeck/src/media/webp.rs (1)

54-56: ⚠️ Potential issue | 🟠 Major

detect_webp_type still does a full decode on fallback.

This path decodes frames just to classify static vs animated, then decoding happens again in the actual load path. That is expensive for large files and was already flagged earlier.

For Rust crate `webp` version `0.3.1`, does `AnimDecoder::decode()` eagerly decode all frame pixel data into memory? Please cite docs.rs source lines.
crates/notedeck/src/imgcache.rs (1)

60-64: ⚠️ Potential issue | 🟠 Major

Add rustdoc for newly added/modified public API items.

TextureState::is_loaded, GifState, GifStateMap, WebpState, and WebpStateMap still need doc comments to satisfy repo standards.

Suggested patch
 impl<T> TextureState<T> {
+    /// Returns `true` when the texture state is fully loaded.
     pub fn is_loaded(&self) -> bool {
         matches!(self, TextureState::Loaded(_))
     }
 }
@@
+/// Per-URL playback state map for GIF animations.
 pub type GifStateMap = HashMap<String, GifState>;
+/// Playback timing/index state for a single GIF animation.
 pub type GifState = crate::media::animated::AnimatedFrameState;
 
+/// Per-URL playback state map for animated WebP media.
 pub type WebpStateMap = HashMap<String, WebpState>;
+/// Playback timing/index state for a single animated WebP media item.
 pub type WebpState = crate::media::animated::AnimatedFrameState;

As per coding guidelines "Ensure docstring coverage for any code added or modified".

Also applies to: 447-451

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@crates/notedeck/src/imgcache.rs` around lines 60 - 64, Add missing rustdoc
comments for the newly added public API: document TextureState::is_loaded, the
GifState type, GifStateMap, WebpState, and WebpStateMap; locate the impl block
containing pub fn is_loaded and the type definitions/aliases for
GifState/GifStateMap and WebpState/WebpStateMap and add brief /// comments
describing their purpose, behavior, and any important invariants or usage notes
so they meet the repo's docstring coverage standards.
🧹 Nitpick comments (3)
crates/notedeck/src/media/webp.rs (1)

281-297: Static WebP network path re-encodes before disk cache write.

At Line 293, MediaCache::write(&path, &url, image) writes re-encoded content, while the original bytes are already available from the response. If byte-preserving cache is desired (as in animated path), write source bytes after successful decode/validation.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@crates/notedeck/src/media/webp.rs` around lines 281 - 297, The static WebP
branch currently calls MediaCache::write(&path, &url, image) which writes the
re-encoded image bytes instead of preserving the original response bytes; after
successful decode/validation in decode_static_webp_processed (and after creating
the texture via load_texture_checked and constructing WebpCacheEntry::Static)
change the cache write to use the original response bytes (res.bytes) rather
than the decoded/re-encoded image, and keep the write guarded so it only runs on
the Ok(image) path (i.e., after decode_static_webp_processed returns Ok) to
ensure byte-preserving disk caching consistent with the animated path.
crates/notedeck/src/media/latest.rs (1)

93-139: Consider extracting shared animated-render logic for GIF/WebP branches.

Both branches duplicate the same frame-state update + repaint scheduling flow. A small helper would reduce divergence risk and simplify future animation fixes.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@crates/notedeck/src/media/latest.rs` around lines 93 - 139, Both GIF and WebP
branches duplicate the same "process frame -> update stored per-url state ->
schedule repaint -> return texture" logic; extract that into a small helper to
avoid duplication. Create a helper (e.g., handle_animated_next_state) that
accepts the result from process_gif_frame/process_webp_frame (the next_state), a
mutable reference to the per-url state map (gif_state or webp_state), the url,
and the ctx; have it insert maybe_new_state into the provided state map,
schedule repaint via ctx.request_repaint_after when next_state.repaint_at
exists, and return LatestImageTex::Loaded(next_state.texture). Replace the
duplicated blocks in the MediaCacheType::Gif branch (after process_gif_frame)
and the WebpCacheEntry::Animated branch (after process_webp_frame) with calls to
this helper, keeping error/pending handling unchanged for TextureState and
WebpCacheEntry::Static.
crates/notedeck/src/media/gif.rs (1)

119-123: Disk GIF load still builds img_frames that are immediately discarded.

This path returns only anim (.map(|f| f.anim)) but generate_anim_pkg still clones/collects every frame into img_frames. Consider a disk-specific builder path that skips img_frames entirely.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@crates/notedeck/src/media/gif.rs` around lines 119 - 123, The disk-load path
is wasting work by building img_frames in generate_anim_pkg even though the
caller only needs f.anim (see the map(|f| f.anim) usage); modify or add a
disk-specific builder variant (e.g., a new function or a flag on
generate_anim_pkg) that skips cloning/collecting per-frame data (img_frames) and
instead constructs the AnimPackage/anim directly from the encoded gif_bytes
using buffer_to_color_image only as needed for metadata or first-frame
operations; update the call site currently invoking generate_anim_pkg(..., |img|
buffer_to_color_image(...)) to call the new disk-only builder so no img_frames
are allocated when only anim is returned.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@Cargo.toml`:
- Around line 64-65: The image crate's webp feature was removed but code uses
image::codecs::webp::WebPEncoder and generic image::load_from_memory paths that
require that feature; re-enable the webp feature on the image dependency in
Cargo.toml (restore features = ["jpeg","png","webp"] or add "webp") so the
WebPEncoder and WebP decoding paths compile, ensuring the image dependency entry
(image = { version = "0.25", features = [...] }) includes "webp".

In `@crates/notedeck/src/media/gif.rs`:
- Line 32: The GIF wrapper call to process_animation_frame(animation,
frame_state, animation_mode, false) disables scheduling of the initial repaint;
change the call so the initial render passes true (or otherwise sets repaint_at)
so the function can set repaint_at for GIFs — e.g., detect first-frame/initial
state via frame_state or animation_mode and call process_animation_frame(...,
true) to enable initial repaint scheduling instead of passing false.

---

Duplicate comments:
In `@crates/notedeck/src/imgcache.rs`:
- Around line 60-64: Add missing rustdoc comments for the newly added public
API: document TextureState::is_loaded, the GifState type, GifStateMap,
WebpState, and WebpStateMap; locate the impl block containing pub fn is_loaded
and the type definitions/aliases for GifState/GifStateMap and
WebpState/WebpStateMap and add brief /// comments describing their purpose,
behavior, and any important invariants or usage notes so they meet the repo's
docstring coverage standards.

---

Nitpick comments:
In `@crates/notedeck/src/media/gif.rs`:
- Around line 119-123: The disk-load path is wasting work by building img_frames
in generate_anim_pkg even though the caller only needs f.anim (see the map(|f|
f.anim) usage); modify or add a disk-specific builder variant (e.g., a new
function or a flag on generate_anim_pkg) that skips cloning/collecting per-frame
data (img_frames) and instead constructs the AnimPackage/anim directly from the
encoded gif_bytes using buffer_to_color_image only as needed for metadata or
first-frame operations; update the call site currently invoking
generate_anim_pkg(..., |img| buffer_to_color_image(...)) to call the new
disk-only builder so no img_frames are allocated when only anim is returned.

In `@crates/notedeck/src/media/latest.rs`:
- Around line 93-139: Both GIF and WebP branches duplicate the same "process
frame -> update stored per-url state -> schedule repaint -> return texture"
logic; extract that into a small helper to avoid duplication. Create a helper
(e.g., handle_animated_next_state) that accepts the result from
process_gif_frame/process_webp_frame (the next_state), a mutable reference to
the per-url state map (gif_state or webp_state), the url, and the ctx; have it
insert maybe_new_state into the provided state map, schedule repaint via
ctx.request_repaint_after when next_state.repaint_at exists, and return
LatestImageTex::Loaded(next_state.texture). Replace the duplicated blocks in the
MediaCacheType::Gif branch (after process_gif_frame) and the
WebpCacheEntry::Animated branch (after process_webp_frame) with calls to this
helper, keeping error/pending handling unchanged for TextureState and
WebpCacheEntry::Static.

In `@crates/notedeck/src/media/webp.rs`:
- Around line 281-297: The static WebP branch currently calls
MediaCache::write(&path, &url, image) which writes the re-encoded image bytes
instead of preserving the original response bytes; after successful
decode/validation in decode_static_webp_processed (and after creating the
texture via load_texture_checked and constructing WebpCacheEntry::Static) change
the cache write to use the original response bytes (res.bytes) rather than the
decoded/re-encoded image, and keep the write guarded so it only runs on the
Ok(image) path (i.e., after decode_static_webp_processed returns Ok) to ensure
byte-preserving disk caching consistent with the animated path.

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: fec05267-b26e-44ab-979b-eea4df3f91b4

📥 Commits

Reviewing files that changed from the base of the PR and between 901d848 and e3124f0.

⛔ Files ignored due to path filters (1)
  • Cargo.lock is excluded by !**/*.lock
📒 Files selected for processing (13)
  • Cargo.toml
  • crates/notedeck/Cargo.toml
  • crates/notedeck/src/imgcache.rs
  • crates/notedeck/src/jobs/media.rs
  • crates/notedeck/src/lib.rs
  • crates/notedeck/src/media/action.rs
  • crates/notedeck/src/media/animated.rs
  • crates/notedeck/src/media/gif.rs
  • crates/notedeck/src/media/latest.rs
  • crates/notedeck/src/media/mod.rs
  • crates/notedeck/src/media/webp.rs
  • crates/notedeck/src/urls.rs
  • crates/notedeck_ui/src/note/media.rs
🚧 Files skipped from review as they are similar to previous changes (4)
  • crates/notedeck/src/media/action.rs
  • crates/notedeck/src/media/mod.rs
  • crates/notedeck_ui/src/note/media.rs
  • crates/notedeck/Cargo.toml

sergey3bv and others added 3 commits March 4, 2026 15:34
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Co-Authored-By: Codex <codex@openai.com>
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Co-Authored-By: Codex <codex@openai.com>
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Co-Authored-By: Codex <codex@openai.com>
@sergey3bv sergey3bv force-pushed the feat/webp-support branch from e3124f0 to 291e4e3 Compare March 4, 2026 12:36
@sergey3bv
Copy link
Contributor Author

sergey3bv commented Mar 4, 2026

@alltheseas @kernelkind @jb55, I think now the PR is ready.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants