From df175d6c9822026a31cc02722809ac7af9a1ac8d Mon Sep 17 00:00:00 2001 From: Maarifa Maarifa Date: Thu, 23 Nov 2023 23:02:46 +0300 Subject: [PATCH] Prevent image resolution from being lowered when already low. --- src/core/api/tv_maze/image.rs | 91 +++++++++++++++++++++++ src/core/api/tv_maze/mod.rs | 66 +--------------- src/core/caching.rs | 6 +- src/core/caching/show_images.rs | 7 +- src/gui/series_page/series/cast_widget.rs | 4 +- src/gui/series_page/series/mod.rs | 2 +- src/gui/tabs/discover_tab/searching.rs | 5 +- src/gui/troxide_widget.rs | 9 ++- 8 files changed, 111 insertions(+), 79 deletions(-) create mode 100644 src/core/api/tv_maze/image.rs diff --git a/src/core/api/tv_maze/image.rs b/src/core/api/tv_maze/image.rs new file mode 100644 index 00000000..4433ceff --- /dev/null +++ b/src/core/api/tv_maze/image.rs @@ -0,0 +1,91 @@ +use std::io::Write; + +use bytes::Bytes; +use tracing::error; + +const POSTER_WIDTH: u32 = 480; +const POSTER_HEIGHT: u32 = 853; +const BACKGROUND_WIDTH: u32 = 1280; +const BACKGROUND_HEIGHT: u32 = 720; + +pub enum ImageResolution { + Original(ImageKind), + Medium, +} + +#[derive(Clone, Copy)] +pub enum ImageKind { + Poster, + Background, +} + +/// Loads the image from the provided url +/// +/// Since Original images from TvMaze may have extremely high resolution up to 4k which can cause `wgpu` to crash, +/// this function will thumbnail the original image to the size that is good enough to be displayed in the GUI. +pub async fn load_image(image_url: String, image_resolution: ImageResolution) -> Option { + loop { + match reqwest::get(&image_url).await { + Ok(response) => { + if let Ok(bytes) = response.bytes().await { + let image = image::load_from_memory(&bytes) + .map_err(|err| error!("failed to load image from the api: {}", err)) + .ok()?; + + break match image_resolution { + ImageResolution::Original(image_kind) => { + if should_lower_resolution(&image, image_kind) { + lower_image_resolution(image, image_kind) + } else { + Some(bytes) + } + } + ImageResolution::Medium => Some(bytes), + }; + } + } + Err(ref err) => { + if err.is_request() { + super::random_async_sleep().await; + } else { + break None; + } + } + } + } +} + +fn should_lower_resolution(image: &image::DynamicImage, image_kind: ImageKind) -> bool { + match image_kind { + ImageKind::Poster => image.height() > POSTER_HEIGHT || image.width() > POSTER_WIDTH, + ImageKind::Background => { + image.height() > BACKGROUND_HEIGHT || image.width() > BACKGROUND_WIDTH + } + } +} + +fn lower_image_resolution( + image: image::DynamicImage, + image_kind: ImageKind, +) -> Option { + let img = match image_kind { + ImageKind::Poster => image.thumbnail(POSTER_WIDTH, POSTER_WIDTH), + ImageKind::Background => image.thumbnail(BACKGROUND_WIDTH, BACKGROUND_HEIGHT), + }; + + let mut writer = std::io::BufWriter::new(vec![]); + + let mut jpeg_encoder = image::codecs::jpeg::JpegEncoder::new_with_quality(&mut writer, 100); + + jpeg_encoder + .encode_image(&img) + .map_err(|err| error!("failed to encode image: {}", err)) + .ok()?; + + writer + .flush() + .map_err(|err| error!("failed to flush image bytes: {}", err)) + .ok()?; + + Some(bytes::Bytes::copy_from_slice(writer.get_ref())) +} diff --git a/src/core/api/tv_maze/mod.rs b/src/core/api/tv_maze/mod.rs index db4cf414..68e5db45 100644 --- a/src/core/api/tv_maze/mod.rs +++ b/src/core/api/tv_maze/mod.rs @@ -1,11 +1,8 @@ -use std::io::Write; - -use bytes::Bytes; use serde::{Deserialize, Serialize}; use thiserror::Error; -use tracing::error; pub mod episodes_information; +pub mod image; pub mod seasons_list; pub mod series_information; pub mod series_searching; @@ -50,67 +47,6 @@ pub struct Image { pub medium_image_url: String, } -pub enum ImageType { - Original(OriginalType), - Medium, -} - -pub enum OriginalType { - Poster, - Background, -} - -/// Loads the image from the provided url -/// -/// Since Original images from TvMaze may have extremely high resolution up to 4k which can cause `wgpu` to crash, -/// this function will thumbnail the original image to the size that is good enough to be displayed in the GUI. -pub async fn load_image(image_url: String, image_type: ImageType) -> Option { - loop { - match reqwest::get(&image_url).await { - Ok(response) => { - if let Ok(bytes) = response.bytes().await { - break Some(if let ImageType::Original(original_type) = image_type { - let img = image::load_from_memory(&bytes) - .map_err(|err| error!("failed to load image from the api: {}", err)) - .ok()?; - - let img = match original_type { - OriginalType::Poster => img.thumbnail(480, 853), - OriginalType::Background => img.thumbnail(1280, 720), - }; - - let mut writer = std::io::BufWriter::new(vec![]); - - let mut jpeg_encoder = - image::codecs::jpeg::JpegEncoder::new_with_quality(&mut writer, 100); - - jpeg_encoder - .encode_image(&img) - .map_err(|err| error!("failed to encode image: {}", err)) - .ok()?; - - writer - .flush() - .map_err(|err| error!("failed to flush image bytes: {}", err)) - .ok()?; - - bytes::Bytes::copy_from_slice(writer.get_ref()) - } else { - bytes - }); - } - } - Err(ref err) => { - if err.is_request() { - random_async_sleep().await; - } else { - break None; - } - } - } - } -} - /// T fn try_bad_json(json_string: &str) -> Option<(String, String)> { if let Ok(bad_response) = serde_json::from_str::(json_string) { diff --git a/src/core/caching.rs b/src/core/caching.rs index 3b083578..76f9970f 100644 --- a/src/core/caching.rs +++ b/src/core/caching.rs @@ -29,8 +29,8 @@ use bytes::Bytes; use std::io::{self, ErrorKind}; use std::path; +pub use super::api::tv_maze::image::{ImageKind, ImageResolution}; use super::api::tv_maze::{series_information::SeriesMainInformation, ApiError}; -pub use super::api::tv_maze::{ImageType, OriginalType}; use super::paths; use crate::core::api::tv_maze::{self, deserialize_json}; use lazy_static::lazy_static; @@ -136,7 +136,7 @@ impl Cacher { } /// Loads the image from the provided url -pub async fn load_image(image_url: String, image_type: ImageType) -> Option { +pub async fn load_image(image_url: String, image_type: ImageResolution) -> Option { // Hashing the image url as a file name as the forward slashes in web urls // mimic paths use sha2::{Digest, Sha256}; @@ -153,7 +153,7 @@ pub async fn load_image(image_url: String, image_type: ImageType) -> Option { if err.kind() == ErrorKind::NotFound { info!("falling back online for image with link {}", image_url); - if let Some(image_bytes) = tv_maze::load_image(image_url, image_type).await { + if let Some(image_bytes) = tv_maze::image::load_image(image_url, image_type).await { write_cache(&image_bytes, &image_path).await; Some(image_bytes) } else { diff --git a/src/core/caching/show_images.rs b/src/core/caching/show_images.rs index 49686cb4..446df671 100644 --- a/src/core/caching/show_images.rs +++ b/src/core/caching/show_images.rs @@ -1,8 +1,7 @@ use std::io::ErrorKind; use super::{ - load_image, read_cache, write_cache, CacheFilePath, ImageType as ImageKind, OriginalType, - CACHER, + load_image, read_cache, write_cache, CacheFilePath, ImageKind, ImageResolution, CACHER, }; use crate::core::api::tv_maze::{ deserialize_json, @@ -45,7 +44,7 @@ pub async fn get_recent_banner(series_id: u32) -> Option { { return load_image( recent_background.resolutions.original.url.clone(), - ImageKind::Original(OriginalType::Background), + ImageResolution::Original(ImageKind::Background), ) .await; }; @@ -58,7 +57,7 @@ pub async fn get_recent_banner(series_id: u32) -> Option { load_image( recent_banner.resolutions.original.url, - ImageKind::Original(OriginalType::Background), + ImageResolution::Original(ImageKind::Background), ) .await } diff --git a/src/gui/series_page/series/cast_widget.rs b/src/gui/series_page/series/cast_widget.rs index 40aa04b3..39c6f411 100644 --- a/src/gui/series_page/series/cast_widget.rs +++ b/src/gui/series_page/series/cast_widget.rs @@ -347,7 +347,7 @@ mod cast_poster { fn load_person_image(image: Option) -> Command { if let Some(image) = image { Command::perform( - caching::load_image(image.medium_image_url, caching::ImageType::Medium), + caching::load_image(image.medium_image_url, caching::ImageResolution::Medium), Message::PersonImageLoaded, ) } else { @@ -358,7 +358,7 @@ mod cast_poster { fn load_character_image(image: Option) -> Command { if let Some(image) = image { Command::perform( - caching::load_image(image.medium_image_url, caching::ImageType::Medium), + caching::load_image(image.medium_image_url, caching::ImageResolution::Medium), Message::CharacterImageLoaded, ) } else { diff --git a/src/gui/series_page/series/mod.rs b/src/gui/series_page/series/mod.rs index 8e59ce49..67f4f420 100644 --- a/src/gui/series_page/series/mod.rs +++ b/src/gui/series_page/series/mod.rs @@ -203,7 +203,7 @@ fn load_images(series_info_image: Option, series_id: u32) -> [Command