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 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<DynamicImage> {
@ -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<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();
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());
}

View file

@ -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<u8> = Vec::with_capacity(1 << 16);
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_height = 900
cache_dir = ""
image_quality = 75
sixel_fraction = 15
ueberzug_scale = 1
ueberzug_offset = [ 0, 0, 0, 0 ]

View file

@ -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<String>,
#[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::<Outer>(&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,
}