From ba0c860b2e4098318fb1a07f67ac81d7c08f230d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E4=B8=89=E5=92=B2=E9=9B=85=20=C2=B7=20Misaki=20Masa?= Date: Thu, 25 Jan 2024 15:47:59 +0800 Subject: [PATCH] feat: new `image_quality` and `sixel_fraction` options to allow users to configure the image preview quality (#576) --- yazi-adaptor/src/image.rs | 78 +++++++++++++++++------------- yazi-adaptor/src/sixel.rs | 3 +- yazi-config/preset/yazi.toml | 2 + yazi-config/src/preview/preview.rs | 18 ++++++- 4 files changed, 65 insertions(+), 36 deletions(-) diff --git a/yazi-adaptor/src/image.rs b/yazi-adaptor/src/image.rs index 9eb86bd6..e639c47f 100644 --- a/yazi-adaptor/src/image.rs +++ b/yazi-adaptor/src/image.rs @@ -2,7 +2,7 @@ use std::{fs::File, io::BufReader, path::{Path, PathBuf}}; use anyhow::Result; use exif::{In, Tag}; -use image::{imageops::{self, FilterType}, io::Limits, DynamicImage, ImageFormat}; +use image::{codecs::jpeg::JpegEncoder, imageops::{self, FilterType}, io::Limits, DynamicImage}; use ratatui::layout::Rect; use yazi_config::{PREVIEW, TASKS}; use yazi_shared::term::Term; @@ -20,23 +20,32 @@ impl Image { .await??; let (mut w, mut h) = (PREVIEW.max_width, PREVIEW.max_height); - tokio::task::spawn_blocking(move || { - if (5..=8).contains(&orientation) { - (w, h) = (h, w); - } + if (5..=8).contains(&orientation) { + (w, h) = (h, w); + } + let buf = tokio::task::spawn_blocking(move || { if img.width() > w || img.height() > h { img = img.resize(w, h, FilterType::Triangle); } img = Self::rotate(img, orientation); - Ok(match img { - DynamicImage::ImageRgb8(buf) => buf.save_with_format(cache, ImageFormat::Jpeg), - DynamicImage::ImageRgba8(buf) => buf.save_with_format(cache, ImageFormat::Jpeg), - buf => buf.into_rgb8().save_with_format(cache, ImageFormat::Jpeg), - }?) + if !matches!(img, DynamicImage::ImageRgb8(_) | DynamicImage::ImageRgba8(_)) { + img = DynamicImage::ImageRgb8(img.into_rgb8()); + } + + let mut buf = Vec::new(); + JpegEncoder::new_with_quality(&mut buf, PREVIEW.image_quality).encode( + img.as_bytes(), + img.width(), + img.height(), + img.color(), + )?; + Ok::<_, anyhow::Error>(buf) }) - .await? + .await??; + + Ok(tokio::fs::write(cache, buf).await?) } pub(super) async fn downscale(path: &Path, rect: Rect) -> Result { @@ -49,15 +58,19 @@ impl Image { .await??; let (mut w, mut h) = Self::max_size(rect); - tokio::task::spawn_blocking(move || { - if (5..=8).contains(&orientation) { - (w, h) = (h, w); - } + if (5..=8).contains(&orientation) { + (w, h) = (h, w); + } + // Fast path. + if img.width() <= w && img.height() <= h && orientation <= 1 { + return Ok(img); + } + + tokio::task::spawn_blocking(move || { if img.width() > w || img.height() > h { img = img.resize(w, h, FilterType::Triangle) } - Ok(Self::rotate(img, orientation)) }) .await? @@ -73,6 +86,12 @@ impl Image { } async fn orientation(path: &Path) -> Result { + // We don't want to read the orientation of the cached image that has been + // rotated in the `Self::precache()` step. + if path.parent() == Some(&PREVIEW.cache_dir) { + return Ok(0); + } + let path = path.to_owned(); tokio::task::spawn_blocking(move || { let file = std::fs::File::open(path)?; @@ -96,23 +115,16 @@ impl Image { // https://magnushoff.com/articles/jpeg-orientation/ fn rotate(mut img: DynamicImage, orientation: u8) -> DynamicImage { let rgba = img.color().has_alpha(); - if orientation == 2 { - img = DynamicImage::ImageRgba8(imageops::flip_horizontal(&img)); - } else if orientation == 3 { - img = DynamicImage::ImageRgba8(imageops::rotate180(&img)); - } else if orientation == 4 { - img = DynamicImage::ImageRgba8(imageops::flip_horizontal(&img)); - } else if orientation == 5 { - img = DynamicImage::ImageRgba8(imageops::rotate90(&img)); - img = DynamicImage::ImageRgba8(imageops::flip_horizontal(&img)); - } else if orientation == 6 { - img = DynamicImage::ImageRgba8(imageops::rotate90(&img)); - } else if orientation == 7 { - img = DynamicImage::ImageRgba8(imageops::rotate270(&img)); - img = DynamicImage::ImageRgba8(imageops::flip_horizontal(&img)); - } else if orientation == 8 { - img = DynamicImage::ImageRgba8(imageops::rotate270(&img)); - } + img = match orientation { + 2 => DynamicImage::ImageRgba8(imageops::flip_horizontal(&img)), + 3 => DynamicImage::ImageRgba8(imageops::rotate180(&img)), + 4 => DynamicImage::ImageRgba8(imageops::flip_horizontal(&img)), + 5 => DynamicImage::ImageRgba8(imageops::flip_horizontal(&imageops::rotate90(&img))), + 6 => DynamicImage::ImageRgba8(imageops::rotate90(&img)), + 7 => DynamicImage::ImageRgba8(imageops::flip_horizontal(&imageops::rotate270(&img))), + 8 => DynamicImage::ImageRgba8(imageops::rotate270(&img)), + _ => img, + }; if !rgba { img = DynamicImage::ImageRgb8(img.into_rgb8()); } diff --git a/yazi-adaptor/src/sixel.rs b/yazi-adaptor/src/sixel.rs index 8d85c2d6..75bb4fb8 100644 --- a/yazi-adaptor/src/sixel.rs +++ b/yazi-adaptor/src/sixel.rs @@ -4,6 +4,7 @@ use anyhow::{bail, Result}; use color_quant::NeuQuant; use image::DynamicImage; use ratatui::layout::Rect; +use yazi_config::PREVIEW; use yazi_shared::term::Term; use crate::{adaptor::Adaptor, Image, CLOSE, ESCAPE, START}; @@ -44,7 +45,7 @@ impl Sixel { tokio::task::spawn_blocking(move || { let img = img.into_rgba8(); - let nq = NeuQuant::new(10, 256 - alpha as usize, &img); + let nq = NeuQuant::new(PREVIEW.sixel_fraction as i32, 256 - alpha as usize, &img); let mut buf: Vec = Vec::with_capacity(1 << 16); write!(buf, "{}P0;1;8q\"1;1;{};{}", START, img.width(), img.height())?; diff --git a/yazi-config/preset/yazi.toml b/yazi-config/preset/yazi.toml index b4cfd9e6..64f631fc 100644 --- a/yazi-config/preset/yazi.toml +++ b/yazi-config/preset/yazi.toml @@ -17,6 +17,8 @@ tab_size = 2 max_width = 600 max_height = 900 cache_dir = "" +image_quality = 75 +sixel_fraction = 15 ueberzug_scale = 1 ueberzug_offset = [ 0, 0, 0, 0 ] diff --git a/yazi-config/src/preview/preview.rs b/yazi-config/src/preview/preview.rs index 37d68e26..f4c3d4cb 100644 --- a/yazi-config/src/preview/preview.rs +++ b/yazi-config/src/preview/preview.rs @@ -1,9 +1,10 @@ use std::{fs, path::PathBuf, process, time::{self, SystemTime}}; use serde::{Deserialize, Serialize}; +use validator::Validate; use yazi_shared::fs::expand_path; -use crate::{xdg::Xdg, ARGS, MERGED_YAZI}; +use crate::{validation::check_validation, xdg::Xdg, ARGS, MERGED_YAZI}; #[derive(Debug, Serialize)] pub struct Preview { @@ -13,6 +14,9 @@ pub struct Preview { pub cache_dir: PathBuf, + pub image_quality: u8, + pub sixel_fraction: u8, + pub ueberzug_scale: f32, pub ueberzug_offset: (f32, f32, f32, f32), } @@ -23,7 +27,7 @@ impl Default for Preview { struct Outer { preview: Shadow, } - #[derive(Deserialize)] + #[derive(Deserialize, Validate)] struct Shadow { tab_size: u8, max_width: u32, @@ -31,11 +35,18 @@ impl Default for Preview { cache_dir: Option, + #[validate(range(min = 50, max = 90))] + image_quality: u8, + #[validate(range(min = 10, max = 20))] + sixel_fraction: u8, + ueberzug_scale: f32, ueberzug_offset: (f32, f32, f32, f32), } let preview = toml::from_str::(&MERGED_YAZI).unwrap().preview; + check_validation(preview.validate()); + let cache_dir = preview.cache_dir.filter(|p| !p.is_empty()).map_or_else(Xdg::cache_dir, expand_path); @@ -63,6 +74,9 @@ impl Default for Preview { cache_dir, + image_quality: preview.image_quality, + sixel_fraction: preview.sixel_fraction, + ueberzug_scale: preview.ueberzug_scale, ueberzug_offset: preview.ueberzug_offset, }