[fractal] misc: Use the image crate to load images



commit 4a0e79642d2c2e6b32b8f35e440ee7da44f7c961
Author: Kévin Commaille <zecakeh tedomum fr>
Date:   Mon Oct 3 19:30:25 2022 +0200

    misc: Use the image crate to load images
    
    Handle more codecs than GDK-Pixbuf and add support for animated images.

 src/components/editable_avatar.rs                  |   4 +-
 src/components/image_paintable.rs                  | 397 +++++++++++++++++++++
 src/components/media_content_viewer.rs             |   6 +-
 src/components/mod.rs                              |   2 +
 src/session/avatar.rs                              |   4 +-
 .../content/room_history/message_row/media.rs      |   6 +-
 src/session/media_viewer.rs                        |   4 +-
 7 files changed, 411 insertions(+), 12 deletions(-)
---
diff --git a/src/components/editable_avatar.rs b/src/components/editable_avatar.rs
index 6ad2df76c..c826e46e1 100644
--- a/src/components/editable_avatar.rs
+++ b/src/components/editable_avatar.rs
@@ -8,7 +8,7 @@ use gtk::{
 };
 use log::error;
 
-use super::{ActionButton, ActionState};
+use super::{ActionButton, ActionState, ImagePaintable};
 use crate::{session::Avatar, spawn, toast};
 
 mod imp {
@@ -308,7 +308,7 @@ impl EditableAvatar {
 
     pub fn set_temp_image_from_file(&self, file: Option<&gio::File>) {
         self.imp().temp_image.replace(
-            file.and_then(|file| gdk::Texture::from_file(file).ok())
+            file.and_then(|file| ImagePaintable::from_file(file).ok())
                 .map(|texture| texture.upcast()),
         );
         self.notify("temp-image");
diff --git a/src/components/image_paintable.rs b/src/components/image_paintable.rs
new file mode 100644
index 000000000..bd87ac21d
--- /dev/null
+++ b/src/components/image_paintable.rs
@@ -0,0 +1,397 @@
+use std::{
+    io::{BufRead, BufReader, Cursor, Seek},
+    time::Duration,
+};
+
+use gtk::{gdk, gio, glib, graphene, prelude::*, subclass::prelude::*};
+use image::{
+    codecs::{gif::GifDecoder, png::PngDecoder},
+    flat::SampleLayout,
+    AnimationDecoder, DynamicImage, ImageFormat,
+};
+use log::error;
+
+/// A single frame of an animation.
+pub struct Frame {
+    pub texture: gdk::Texture,
+    pub duration: Duration,
+}
+
+impl From<image::Frame> for Frame {
+    fn from(f: image::Frame) -> Self {
+        let mut duration = Duration::from(f.delay());
+
+        // The convention is to use 100 milliseconds duration if it is defined as 0.
+        if duration.is_zero() {
+            duration = Duration::from_millis(100);
+        }
+
+        let sample = f.into_buffer().into_flat_samples();
+        let texture =
+            texture_from_data(&sample.samples, sample.layout, gdk::MemoryFormat::R8g8b8a8);
+
+        Frame {
+            texture: texture.upcast(),
+            duration,
+        }
+    }
+}
+
+mod imp {
+    use std::cell::{Cell, RefCell};
+
+    use once_cell::sync::Lazy;
+
+    use super::*;
+
+    #[derive(Default)]
+    pub struct ImagePaintable {
+        /// The frames of the animation, if any.
+        pub frames: RefCell<Option<Vec<Frame>>>,
+
+        /// The image if this is not an animation, otherwise this is the next
+        /// frame to display.
+        pub frame: RefCell<Option<gdk::Texture>>,
+
+        /// The current index in the animation.
+        pub current_idx: Cell<usize>,
+
+        /// The source ID of the timeout to load the next frame, if any.
+        pub timeout_source_id: RefCell<Option<glib::SourceId>>,
+    }
+
+    #[glib::object_subclass]
+    impl ObjectSubclass for ImagePaintable {
+        const NAME: &'static str = "ImagePaintable";
+        type Type = super::ImagePaintable;
+        type Interfaces = (gdk::Paintable,);
+    }
+
+    impl ObjectImpl for ImagePaintable {
+        fn properties() -> &'static [glib::ParamSpec] {
+            static PROPERTIES: Lazy<Vec<glib::ParamSpec>> = Lazy::new(|| {
+                vec![
+                    glib::ParamSpecBoolean::new(
+                        "is-animation",
+                        "Is Animation",
+                        "Whether this displays an animation",
+                        false,
+                        glib::ParamFlags::READABLE,
+                    ),
+                    glib::ParamSpecInt::new(
+                        "width",
+                        "Width",
+                        "The width of this paintable",
+                        i32::MIN,
+                        i32::MAX,
+                        -1,
+                        glib::ParamFlags::READABLE,
+                    ),
+                    glib::ParamSpecInt::new(
+                        "height",
+                        "Height",
+                        "The height of this paintable",
+                        i32::MIN,
+                        i32::MAX,
+                        -1,
+                        glib::ParamFlags::READABLE,
+                    ),
+                ]
+            });
+
+            PROPERTIES.as_ref()
+        }
+
+        fn property(&self, obj: &Self::Type, _id: usize, pspec: &glib::ParamSpec) -> glib::Value {
+            match pspec.name() {
+                "is-animation" => obj.is_animation().to_value(),
+                "width" => obj.intrinsic_width().to_value(),
+                "height" => obj.intrinsic_height().to_value(),
+                _ => unimplemented!(),
+            }
+        }
+    }
+
+    impl PaintableImpl for ImagePaintable {
+        fn intrinsic_height(&self, _paintable: &Self::Type) -> i32 {
+            self.frame
+                .borrow()
+                .as_ref()
+                .map(|texture| texture.height())
+                .unwrap_or(-1)
+        }
+
+        fn intrinsic_width(&self, _paintable: &Self::Type) -> i32 {
+            self.frame
+                .borrow()
+                .as_ref()
+                .map(|texture| texture.width())
+                .unwrap_or(-1)
+        }
+
+        fn snapshot(
+            &self,
+            _paintable: &Self::Type,
+            snapshot: &gdk::Snapshot,
+            width: f64,
+            height: f64,
+        ) {
+            if let Some(texture) = &*self.frame.borrow() {
+                texture.snapshot(snapshot, width, height);
+            } else {
+                let snapshot = snapshot.downcast_ref::<gtk::Snapshot>().unwrap();
+                snapshot.append_color(
+                    &gdk::RGBA::BLACK,
+                    &graphene::Rect::new(0f32, 0f32, width as f32, height as f32),
+                );
+            }
+        }
+
+        fn flags(&self, paintable: &Self::Type) -> gdk::PaintableFlags {
+            if paintable.is_animation() {
+                gdk::PaintableFlags::SIZE
+            } else {
+                gdk::PaintableFlags::SIZE | gdk::PaintableFlags::CONTENTS
+            }
+        }
+
+        fn current_image(&self, paintable: &Self::Type) -> gdk::Paintable {
+            self.frame
+                .borrow()
+                .clone()
+                .map(|frame| frame.upcast())
+                .or_else(|| {
+                    let snapshot = gtk::Snapshot::new();
+                    paintable.snapshot(snapshot.upcast_ref(), 1.0, 1.0);
+
+                    snapshot.to_paintable(None)
+                })
+                .expect("there should be a fallback paintable")
+        }
+    }
+}
+
+glib::wrapper! {
+    /// A paintable that loads images with the `image` crate.
+    ///
+    /// It handles more image types than GDK-Pixbuf and can also handle
+    /// animations from GIF and APNG files.
+    pub struct ImagePaintable(ObjectSubclass<imp::ImagePaintable>)
+        @implements gdk::Paintable;
+}
+
+impl ImagePaintable {
+    /// Load an image from the given reader in the optional format.
+    ///
+    /// The actual format will try to be guessed from the content.
+    pub fn new<R: BufRead + Seek>(
+        reader: R,
+        format: Option<ImageFormat>,
+    ) -> Result<Self, Box<dyn std::error::Error>> {
+        let obj =
+            glib::Object::new::<Self>(&[]).expect("Failed to create object of type ImagePaintable");
+
+        let mut reader = image::io::Reader::new(reader);
+
+        if let Some(format) = format {
+            reader.set_format(format);
+        }
+
+        let reader = reader.with_guessed_format()?;
+
+        obj.load_inner(reader)?;
+
+        Ok(obj)
+    }
+
+    /// Load an image or animation from the given reader.
+    fn load_inner<R: BufRead + Seek>(
+        &self,
+        reader: image::io::Reader<R>,
+    ) -> Result<(), Box<dyn std::error::Error>> {
+        let priv_ = self.imp();
+        let format = reader.format().ok_or("Could not detect image format")?;
+
+        let read = reader.into_inner();
+
+        // Handle animations.
+        match format {
+            image::ImageFormat::Gif => {
+                let decoder = GifDecoder::new(read)?;
+
+                let frames = decoder
+                    .into_frames()
+                    .collect_frames()?
+                    .into_iter()
+                    .map(Frame::from)
+                    .collect::<Vec<_>>();
+
+                if frames.len() == 1 {
+                    if let Some(frame) = frames.into_iter().next() {
+                        priv_.frame.replace(Some(frame.texture));
+                    }
+                } else {
+                    priv_.frames.replace(Some(frames));
+                    self.update_frame();
+                }
+            }
+            image::ImageFormat::Png => {
+                let decoder = PngDecoder::new(read)?;
+
+                if decoder.is_apng() {
+                    let decoder = decoder.apng();
+                    let frames = decoder
+                        .into_frames()
+                        .collect_frames()?
+                        .into_iter()
+                        .map(Frame::from)
+                        .collect::<Vec<_>>();
+                    priv_.frames.replace(Some(frames));
+                    self.update_frame();
+                } else {
+                    let image = DynamicImage::from_decoder(decoder)?;
+                    self.set_image(image);
+                }
+            }
+            _ => {
+                let image = image::load(read, format)?;
+                self.set_image(image);
+            }
+        }
+
+        Ok(())
+    }
+
+    /// Set the image that is displayed by this paintable.
+    fn set_image(&self, image: DynamicImage) {
+        let texture = match image.color() {
+            image::ColorType::L8 | image::ColorType::Rgb8 => {
+                let sample = image.into_rgb8().into_flat_samples();
+                texture_from_data(&sample.samples, sample.layout, gdk::MemoryFormat::R8g8b8)
+            }
+            image::ColorType::La8 | image::ColorType::Rgba8 => {
+                let sample = image.into_rgba8().into_flat_samples();
+                texture_from_data(&sample.samples, sample.layout, gdk::MemoryFormat::R8g8b8a8)
+            }
+            image::ColorType::L16 | image::ColorType::Rgb16 => {
+                let sample = image.into_rgb16().into_flat_samples();
+                let bytes = sample
+                    .samples
+                    .into_iter()
+                    .flat_map(|b| b.to_ne_bytes())
+                    .collect::<Vec<_>>();
+                texture_from_data(&bytes, sample.layout, gdk::MemoryFormat::R16g16b16)
+            }
+            image::ColorType::La16 | image::ColorType::Rgba16 => {
+                let sample = image.into_rgba16().into_flat_samples();
+                let bytes = sample
+                    .samples
+                    .into_iter()
+                    .flat_map(|b| b.to_ne_bytes())
+                    .collect::<Vec<_>>();
+                texture_from_data(&bytes, sample.layout, gdk::MemoryFormat::R16g16b16a16)
+            }
+            image::ColorType::Rgb32F => {
+                let sample = image.into_rgb32f().into_flat_samples();
+                let bytes = sample
+                    .samples
+                    .into_iter()
+                    .flat_map(|b| b.to_ne_bytes())
+                    .collect::<Vec<_>>();
+                texture_from_data(&bytes, sample.layout, gdk::MemoryFormat::R32g32b32Float)
+            }
+            image::ColorType::Rgba32F => {
+                let sample = image.into_rgb32f().into_flat_samples();
+                let bytes = sample
+                    .samples
+                    .into_iter()
+                    .flat_map(|b| b.to_ne_bytes())
+                    .collect::<Vec<_>>();
+                texture_from_data(&bytes, sample.layout, gdk::MemoryFormat::R32g32b32Float)
+            }
+            c => {
+                error!("Received image of unsupported color format: {c:?}");
+                return;
+            }
+        };
+
+        self.imp().frame.replace(Some(texture.upcast()));
+    }
+
+    /// Creates a new paintable by loading an image from the given file.
+    pub fn from_file(file: &gio::File) -> Result<Self, Box<dyn std::error::Error>> {
+        let stream = file.read(gio::Cancellable::NONE)?;
+        let reader = BufReader::new(stream.into_read());
+        let format = file
+            .path()
+            .and_then(|path| ImageFormat::from_path(path).ok());
+
+        Self::new(reader, format)
+    }
+
+    /// Creates a new paintable by loading an image from memory.
+    pub fn from_bytes(
+        bytes: &[u8],
+        content_type: Option<&str>,
+    ) -> Result<Self, Box<dyn std::error::Error>> {
+        let reader = Cursor::new(bytes);
+        let format = content_type.and_then(ImageFormat::from_mime_type);
+
+        Self::new(reader, format)
+    }
+
+    /// Update the current frame of the animation.
+    fn update_frame(&self) {
+        let priv_ = self.imp();
+        let frames_ref = priv_.frames.borrow();
+
+        // If it's not an animation, we return early.
+        let frames = match &*frames_ref {
+            Some(frames) => frames,
+            None => return,
+        };
+
+        let idx = priv_.current_idx.get();
+        let next_frame = frames.get(idx).unwrap();
+        priv_.frame.replace(Some(next_frame.texture.clone()));
+
+        // Invalidate the contents so that the new frame will be rendered.
+        self.invalidate_contents();
+
+        // Update the frame when the duration is elapsed.
+        let update_frame_callback = glib::clone!(@weak self as obj => move || {
+            obj.imp().timeout_source_id.take();
+            obj.update_frame();
+        });
+        let source_id = glib::timeout_add_local_once(next_frame.duration, update_frame_callback);
+        priv_.timeout_source_id.replace(Some(source_id));
+
+        // Update the index for the next call.
+        let mut new_idx = idx + 1;
+        if new_idx >= frames.len() {
+            new_idx = 0;
+        }
+        priv_.current_idx.set(new_idx);
+    }
+
+    /// Whether this `ImagePaintable` displays an animation.
+    pub fn is_animation(&self) -> bool {
+        self.imp().frames.borrow().is_some()
+    }
+}
+
+fn texture_from_data(
+    bytes: &[u8],
+    layout: SampleLayout,
+    format: gdk::MemoryFormat,
+) -> gdk::MemoryTexture {
+    let bytes = glib::Bytes::from(bytes);
+
+    gdk::MemoryTexture::new(
+        layout.width as i32,
+        layout.height as i32,
+        format,
+        &bytes,
+        layout.height_stride,
+    )
+}
diff --git a/src/components/media_content_viewer.rs b/src/components/media_content_viewer.rs
index cf53c3720..c4d554faa 100644
--- a/src/components/media_content_viewer.rs
+++ b/src/components/media_content_viewer.rs
@@ -4,7 +4,7 @@ use gettextrs::gettext;
 use gtk::{gdk, gio, glib, glib::clone, CompositeTemplate};
 use log::warn;
 
-use super::{AudioPlayer, LocationViewer};
+use super::{AudioPlayer, ImagePaintable, LocationViewer};
 use crate::spawn;
 
 pub enum ContentType {
@@ -189,7 +189,7 @@ impl MediaContentViewer {
     ///
     /// If you have an image file, you can also use
     /// [`MediaContentViewer::view_file()`].
-    pub fn view_image(&self, image: &gdk::Texture) {
+    pub fn view_image(&self, image: &impl IsA<gdk::Paintable>) {
         self.show_loading();
 
         let priv_ = self.imp();
@@ -239,7 +239,7 @@ impl MediaContentViewer {
             .unwrap_or_default();
 
         match content_type {
-            ContentType::Image => match gdk::Texture::from_file(&file) {
+            ContentType::Image => match ImagePaintable::from_file(&file) {
                 Ok(texture) => {
                     self.view_image(&texture);
                     return;
diff --git a/src/components/mod.rs b/src/components/mod.rs
index ceeee07c9..20a87b9c0 100644
--- a/src/components/mod.rs
+++ b/src/components/mod.rs
@@ -8,6 +8,7 @@ mod context_menu_bin;
 mod custom_entry;
 mod drag_overlay;
 mod editable_avatar;
+mod image_paintable;
 mod label_with_widgets;
 mod loading_listbox_row;
 mod location_viewer;
@@ -30,6 +31,7 @@ pub use self::{
     custom_entry::CustomEntry,
     drag_overlay::DragOverlay,
     editable_avatar::EditableAvatar,
+    image_paintable::ImagePaintable,
     label_with_widgets::{LabelWithWidgets, DEFAULT_PLACEHOLDER},
     loading_listbox_row::LoadingListBoxRow,
     location_viewer::LocationViewer,
diff --git a/src/session/avatar.rs b/src/session/avatar.rs
index 35db3377f..3bf25bf82 100644
--- a/src/session/avatar.rs
+++ b/src/session/avatar.rs
@@ -13,7 +13,7 @@ use matrix_sdk::{
     Client,
 };
 
-use crate::{session::Session, spawn, spawn_tokio};
+use crate::{components::ImagePaintable, session::Session, spawn, spawn_tokio};
 
 mod imp {
     use std::cell::{Cell, RefCell};
@@ -143,7 +143,7 @@ impl Avatar {
 
     fn set_image_data(&self, data: Option<Vec<u8>>) {
         let image = data
-            .and_then(|data| gdk::Texture::from_bytes(&glib::Bytes::from(&data)).ok())
+            .and_then(|data| ImagePaintable::from_bytes(&glib::Bytes::from(&data), None).ok())
             .map(|texture| texture.upcast());
         self.imp().image.replace(image);
         self.notify("image");
diff --git a/src/session/content/room_history/message_row/media.rs 
b/src/session/content/room_history/message_row/media.rs
index e283d5834..592c8e84a 100644
--- a/src/session/content/room_history/message_row/media.rs
+++ b/src/session/content/room_history/message_row/media.rs
@@ -1,7 +1,7 @@
 use adw::{prelude::*, subclass::prelude::*};
 use gettextrs::gettext;
 use gtk::{
-    gdk, gio,
+    gio,
     glib::{self, clone},
     CompositeTemplate,
 };
@@ -19,7 +19,7 @@ use matrix_sdk::{
 
 use super::ContentFormat;
 use crate::{
-    components::VideoPlayer,
+    components::{ImagePaintable, VideoPlayer},
     session::Session,
     spawn, spawn_tokio,
     utils::{cache_dir, media::media_type_uid, uint_to_i32},
@@ -422,7 +422,7 @@ impl MessageMedia {
                     Ok((Some(data), id)) => {
                         match media_type {
                             MediaType::Image | MediaType::Sticker => {
-                                match gdk::Texture::from_bytes(&glib::Bytes::from(&data))
+                                match ImagePaintable::from_bytes(&glib::Bytes::from(&data), None)
                                     {
                                         Ok(texture) => {
                                             let child = if let Some(Ok(child)) =
diff --git a/src/session/media_viewer.rs b/src/session/media_viewer.rs
index 847c88df2..47d8dc5ef 100644
--- a/src/session/media_viewer.rs
+++ b/src/session/media_viewer.rs
@@ -5,7 +5,7 @@ use matrix_sdk::ruma::events::{room::message::MessageType, AnyMessageLikeEventCo
 
 use super::room::EventActions;
 use crate::{
-    components::{ContentType, MediaContentViewer},
+    components::{ContentType, ImagePaintable, MediaContentViewer},
     session::room::SupportedEvent,
     spawn,
     utils::cache_dir,
@@ -222,7 +222,7 @@ impl MediaViewer {
 
                                 match event.get_media_content().await {
                                     Ok((_, _, data)) => {
-                                        match gdk::Texture::from_bytes(&glib::Bytes::from(&data)) {
+                                        match ImagePaintable::from_bytes(&glib::Bytes::from(&data), 
image.info.and_then(|info| info.mimetype).as_deref()) {
                                             Ok(texture) => {
                                                 priv_.media.view_image(&texture);
                                                 return;


[Date Prev][Date Next]   [Thread Prev][Thread Next]   [Thread Index] [Date Index] [Author Index]