Skip to content

desktop: animate GIFs and animated WebP - #7365

Open
Narasimha-sc wants to merge 7 commits into
masterfrom
nd/desktop-animated-images
Open

desktop: animate GIFs and animated WebP#7365
Narasimha-sc wants to merge 7 commits into
masterfrom
nd/desktop-animated-images

Conversation

@Narasimha-sc

@Narasimha-sc Narasimha-sc commented Aug 11, 2026

Copy link
Copy Markdown
Collaborator

Animated images played only their first frame on desktop, while Android animates them with coil and iOS with SwiftyGif. The desktop SimpleAndAnimatedImageView drew a static BitmapPainter and was left marked // LALAL make it animated too, with a matching marker over the image branch in ImageFullScreenView. This decodes frames with Skia's Codec, which skiko already puts on the desktop classpath, so there is no new dependency — and because the frames come from a file somebody else composed, the decoder is bounded before it allocates, with anything outside the bounds or any failure keeping the still image the chat already renders.

Rationale, the measurements behind the cost limits, and the two optimisations that were measured and rejected are in plans/2026-08-11-desktop-animated-images.md.

What animates in the app today is GIFs. Desktop decodes still images with ImageIO, which has no WebP reader, so a received .webp never loads, and both the chat item and the full screen viewer reach this code only after that decode has succeeded — the animated WebP support here waits on that separate fix.

Not verified yet: this has not been run in the app — desktop and Android compile and the desktop test suite passes, but nobody has watched a GIF animate. Left out deliberately: WebP still images do not decode on desktop at all (ImageIO has no WebP reader), and the decode raster is left to the collector rather than released explicitly.

Skia's Codec is already on the desktop classpath through skiko and decodes
both GIF and animated WebP. The frames come from a file somebody else
composed, so the decoder is bounded before it allocates: the raster is
measured in bytes with the sides multiplied as Long, each side is capped
separately so an extreme aspect ratio cannot slip under the byte budget, and
the encoded size is checked before the bytes are copied into native memory.
Anything outside the bounds, or any failure, keeps the still image the chat
already renders.

Nothing calls this yet.
@Narasimha-sc
Narasimha-sc marked this pull request as ready for review August 11, 2026 16:16
@Narasimha-sc
Narasimha-sc force-pushed the nd/desktop-animated-images branch 3 times, most recently from f6e88cb to 25ca601 Compare August 11, 2026 16:58
Both views drew the first frame only. The full screen view also decoded its
still on every recomposition, which an animation recomposes once per frame,
so that decode is remembered against the data it comes from.

The chat list preview stays a still image: it is a 36dp box that the desktop
layout keeps on screen the whole time, so animating it would hold a raster and
spend a frame of work per listed chat, without pause.

Removes the two markers left for this work.
With media blur on, a blurred image is only revealed while the mouse is over
it, so every frame was decoded, uploaded and then blurred away again for
nobody - and the blur is a render effect re-run per frame. Frames now decode
only while the image can be seen, which also stops motion showing through a
blur that is there to hide it.

Passing the blur state to the view is why the shared signature changes; coil
drives its own animation on Android, so there is nothing to pause there.
@Narasimha-sc
Narasimha-sc force-pushed the nd/desktop-animated-images branch from 25ca601 to 1b6b885 Compare August 11, 2026 17:01
@Narasimha-sc
Narasimha-sc marked this pull request as draft August 11, 2026 17:02

@simplex-chat-agent simplex-chat-agent Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Adds desktop GIF/animated-WebP playback by decoding frames with skiko's Codec (already on the classpath) off the UI thread, and threads a blurred: State<Boolean> through the SimpleAndAnimatedImageView expect/actual so the desktop decoder can pause. Both actuals (android, desktop) and the two commonMain call sites are updated consistently; no other target implements this expect, so nothing else breaks.

The decoder is the interesting part and it is careful in the right places:

  • Untrusted input is bounded before allocation: magic-byte prefilter, file-size cap, per-side cap and a byte-measured raster budget computed in Long with bytesPerPixel read from the codec (no assumption of 4 bytes, no Int overflow). allocPixels failure is honoured. A single exception boundary wraps every native call and degrades to the still image rather than surfacing a crash — the correct failure mode for a sender-controlled file. rasterWithinBounds/looksAnimatable are pure and unit-tested as arithmetic.
  • Decode is confined to Dispatchers.Default.limitedParallelism(2) so it cannot starve message-delivery coroutines; delay and snapshotFlow.first suspend and release the thread, so only actual readPixels calls occupy the pool.
  • Frames are only decoded while visible (window shown, not behind the privacy blur), and the pathological-cost tail is capped by the two-slow-frames-in-a-row fallback.

The FullScreenImageView.desktop change also fixes a latent inefficiency: the still is now remember(data)d instead of re-decoded on every recomposition (which now happens per frame).

Nothing here blocks; two things worth addressing before it leaves draft:

  1. apps/multiplatform/DESKTOP_ANIMATED_IMAGES_PLAN.md is placed against the repo's convention. This repo keeps design notes in a top-level plans/ directory named plans/YYYY-MM-DD-topic.md (110 such files); no other plan doc is co-located with source. Move it to e.g. plans/2026-08-11-desktop-animated-images.md, or drop it — the rationale it carries is already in the code comments. (Can't be expressed as an inline suggestion since it's a file move.)

  2. framesAreSeen gates on simplexWindowState.windowVisible, which only tracks minimize-to-tray, not an AWT taskbar minimize (Frame.ICONIFIED, handled separately in DesktopApp.showWindow). A window iconified to the taskbar keeps decoding frames nobody sees. Matches the stated scope ("not while the app sits in the tray") but is a real gap in the "only decode while seen" goal; consider also gating on iconified state.

Minor: the comment in playFrames justifying never closing the reused Bitmap ("the wrapper handed to Compose points at its pixels") describes shared pixels, whereas asComposeImageBitmap on a mutable bitmap copies via Image.makeFromBitmap. The code is safe either way; the reasoning in the comment is off. Not worth changing on its own.

As the author notes, this hasn't been run in the app — the review covers the structure, not observed playback.

@Narasimha-sc
Narasimha-sc marked this pull request as ready for review August 12, 2026 11:40
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.

1 participant