feat: new image_quality and sixel_fraction options to allow users to configure the image preview quality (#576)

This commit is contained in:
三咲雅 · Misaki Masa 2024-01-25 15:47:59 +08:00 committed by GitHub
parent 087448fd94
commit ba0c860b2e
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
4 changed files with 65 additions and 36 deletions

View file

@ -2,7 +2,7 @@ use std::{fs::File, io::BufReader, path::{Path, PathBuf}};
use anyhow::Result; use anyhow::Result;
use exif::{In, Tag}; 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 ratatui::layout::Rect;
use yazi_config::{PREVIEW, TASKS}; use yazi_config::{PREVIEW, TASKS};
use yazi_shared::term::Term; use yazi_shared::term::Term;
@ -20,23 +20,32 @@ impl Image {
.await??; .await??;
let (mut w, mut h) = (PREVIEW.max_width, PREVIEW.max_height); let (mut w, mut h) = (PREVIEW.max_width, PREVIEW.max_height);
tokio::task::spawn_blocking(move || { if (5..=8).contains(&orientation) {
if (5..=8).contains(&orientation) { (w, h) = (h, w);
(w, h) = (h, w); }
}
let buf = tokio::task::spawn_blocking(move || {
if img.width() > w || img.height() > h { if img.width() > w || img.height() > h {
img = img.resize(w, h, FilterType::Triangle); img = img.resize(w, h, FilterType::Triangle);
} }
img = Self::rotate(img, orientation); img = Self::rotate(img, orientation);
Ok(match img { if !matches!(img, DynamicImage::ImageRgb8(_) | DynamicImage::ImageRgba8(_)) {
DynamicImage::ImageRgb8(buf) => buf.save_with_format(cache, ImageFormat::Jpeg), img = DynamicImage::ImageRgb8(img.into_rgb8());
DynamicImage::ImageRgba8(buf) => buf.save_with_format(cache, ImageFormat::Jpeg), }
buf => buf.into_rgb8().save_with_format(cache, ImageFormat::Jpeg),
}?) 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<DynamicImage> { pub(super) async fn downscale(path: &Path, rect: Rect) -> Result<DynamicImage> {
@ -49,15 +58,19 @@ impl Image {
.await??; .await??;
let (mut w, mut h) = Self::max_size(rect); let (mut w, mut h) = Self::max_size(rect);
tokio::task::spawn_blocking(move || { if (5..=8).contains(&orientation) {
if (5..=8).contains(&orientation) { (w, h) = (h, w);
(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 { if img.width() > w || img.height() > h {
img = img.resize(w, h, FilterType::Triangle) img = img.resize(w, h, FilterType::Triangle)
} }
Ok(Self::rotate(img, orientation)) Ok(Self::rotate(img, orientation))
}) })
.await? .await?
@ -73,6 +86,12 @@ impl Image {
} }
async fn orientation(path: &Path) -> Result<u8> { async fn orientation(path: &Path) -> Result<u8> {
// 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(); let path = path.to_owned();
tokio::task::spawn_blocking(move || { tokio::task::spawn_blocking(move || {
let file = std::fs::File::open(path)?; let file = std::fs::File::open(path)?;
@ -96,23 +115,16 @@ impl Image {
// https://magnushoff.com/articles/jpeg-orientation/ // https://magnushoff.com/articles/jpeg-orientation/
fn rotate(mut img: DynamicImage, orientation: u8) -> DynamicImage { fn rotate(mut img: DynamicImage, orientation: u8) -> DynamicImage {
let rgba = img.color().has_alpha(); let rgba = img.color().has_alpha();
if orientation == 2 { img = match orientation {
img = DynamicImage::ImageRgba8(imageops::flip_horizontal(&img)); 2 => DynamicImage::ImageRgba8(imageops::flip_horizontal(&img)),
} else if orientation == 3 { 3 => DynamicImage::ImageRgba8(imageops::rotate180(&img)),
img = DynamicImage::ImageRgba8(imageops::rotate180(&img)); 4 => DynamicImage::ImageRgba8(imageops::flip_horizontal(&img)),
} else if orientation == 4 { 5 => DynamicImage::ImageRgba8(imageops::flip_horizontal(&imageops::rotate90(&img))),
img = DynamicImage::ImageRgba8(imageops::flip_horizontal(&img)); 6 => DynamicImage::ImageRgba8(imageops::rotate90(&img)),
} else if orientation == 5 { 7 => DynamicImage::ImageRgba8(imageops::flip_horizontal(&imageops::rotate270(&img))),
img = DynamicImage::ImageRgba8(imageops::rotate90(&img)); 8 => DynamicImage::ImageRgba8(imageops::rotate270(&img)),
img = DynamicImage::ImageRgba8(imageops::flip_horizontal(&img)); _ => 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));
}
if !rgba { if !rgba {
img = DynamicImage::ImageRgb8(img.into_rgb8()); img = DynamicImage::ImageRgb8(img.into_rgb8());
} }

View file

@ -4,6 +4,7 @@ use anyhow::{bail, Result};
use color_quant::NeuQuant; use color_quant::NeuQuant;
use image::DynamicImage; use image::DynamicImage;
use ratatui::layout::Rect; use ratatui::layout::Rect;
use yazi_config::PREVIEW;
use yazi_shared::term::Term; use yazi_shared::term::Term;
use crate::{adaptor::Adaptor, Image, CLOSE, ESCAPE, START}; use crate::{adaptor::Adaptor, Image, CLOSE, ESCAPE, START};
@ -44,7 +45,7 @@ impl Sixel {
tokio::task::spawn_blocking(move || { tokio::task::spawn_blocking(move || {
let img = img.into_rgba8(); 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<u8> = Vec::with_capacity(1 << 16); let mut buf: Vec<u8> = Vec::with_capacity(1 << 16);
write!(buf, "{}P0;1;8q\"1;1;{};{}", START, img.width(), img.height())?; write!(buf, "{}P0;1;8q\"1;1;{};{}", START, img.width(), img.height())?;

View file

@ -17,6 +17,8 @@ tab_size = 2
max_width = 600 max_width = 600
max_height = 900 max_height = 900
cache_dir = "" cache_dir = ""
image_quality = 75
sixel_fraction = 15
ueberzug_scale = 1 ueberzug_scale = 1
ueberzug_offset = [ 0, 0, 0, 0 ] ueberzug_offset = [ 0, 0, 0, 0 ]

View file

@ -1,9 +1,10 @@
use std::{fs, path::PathBuf, process, time::{self, SystemTime}}; use std::{fs, path::PathBuf, process, time::{self, SystemTime}};
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
use validator::Validate;
use yazi_shared::fs::expand_path; 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)] #[derive(Debug, Serialize)]
pub struct Preview { pub struct Preview {
@ -13,6 +14,9 @@ pub struct Preview {
pub cache_dir: PathBuf, pub cache_dir: PathBuf,
pub image_quality: u8,
pub sixel_fraction: u8,
pub ueberzug_scale: f32, pub ueberzug_scale: f32,
pub ueberzug_offset: (f32, f32, f32, f32), pub ueberzug_offset: (f32, f32, f32, f32),
} }
@ -23,7 +27,7 @@ impl Default for Preview {
struct Outer { struct Outer {
preview: Shadow, preview: Shadow,
} }
#[derive(Deserialize)] #[derive(Deserialize, Validate)]
struct Shadow { struct Shadow {
tab_size: u8, tab_size: u8,
max_width: u32, max_width: u32,
@ -31,11 +35,18 @@ impl Default for Preview {
cache_dir: Option<String>, cache_dir: Option<String>,
#[validate(range(min = 50, max = 90))]
image_quality: u8,
#[validate(range(min = 10, max = 20))]
sixel_fraction: u8,
ueberzug_scale: f32, ueberzug_scale: f32,
ueberzug_offset: (f32, f32, f32, f32), ueberzug_offset: (f32, f32, f32, f32),
} }
let preview = toml::from_str::<Outer>(&MERGED_YAZI).unwrap().preview; let preview = toml::from_str::<Outer>(&MERGED_YAZI).unwrap().preview;
check_validation(preview.validate());
let cache_dir = let cache_dir =
preview.cache_dir.filter(|p| !p.is_empty()).map_or_else(Xdg::cache_dir, expand_path); 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, cache_dir,
image_quality: preview.image_quality,
sixel_fraction: preview.sixel_fraction,
ueberzug_scale: preview.ueberzug_scale, ueberzug_scale: preview.ueberzug_scale,
ueberzug_offset: preview.ueberzug_offset, ueberzug_offset: preview.ueberzug_offset,
} }