feat!: 9x faster Sixel image preview (#2707)

This commit is contained in:
三咲雅 · Misaki Masa 2025-05-02 00:47:27 +08:00 committed by GitHub
parent d6a01a63ef
commit 9037f8b8c0
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
7 changed files with 171 additions and 33 deletions

145
Cargo.lock generated
View file

@ -122,6 +122,15 @@ version = "1.0.98"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e16d2d3311acee920a9eb8d33b8cbc1787ce4a264e85f964c2404b969bdcd487"
[[package]]
name = "approx"
version = "0.5.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "cab112f0a86d568ea0e627cc1d6be74a1e9cd55214684db5561995f6dad897c6"
dependencies = [
"num-traits",
]
[[package]]
name = "arbitrary"
version = "1.4.1"
@ -253,6 +262,18 @@ version = "2.6.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6099cdc01846bc367c4e7dd630dc5966dccf36b652fae7a74e17b640411a91b2"
[[package]]
name = "bitvec"
version = "1.0.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1bc2832c24239b0141d5674bb9174f9d68a8b5b3f2753311927c172ca46f7e9c"
dependencies = [
"funty",
"radium",
"tap",
"wyz",
]
[[package]]
name = "block-buffer"
version = "0.10.4"
@ -293,6 +314,12 @@ version = "3.17.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1628fb46dfa0b37568d12e5edd512553eccf6a22a78e8bde00bb4aed84d5bdbf"
[[package]]
name = "by_address"
version = "1.2.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "64fa3c856b712db6612c019f14756e64e4bcea13337a6b33b696333a9eaa2d06"
[[package]]
name = "bytemuck"
version = "1.23.0"
@ -579,7 +606,7 @@ dependencies = [
"libc",
"mio",
"parking_lot",
"rustix 1.0.5",
"rustix 1.0.7",
"signal-hook",
"signal-hook-mio",
"winapi",
@ -822,6 +849,12 @@ dependencies = [
"zune-inflate",
]
[[package]]
name = "fast-srgb8"
version = "1.0.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "dd2e7510819d6fbf51a5545c8f922716ecfb14df168a3242f7d33e0239efe6a1"
[[package]]
name = "fdeflate"
version = "0.3.7"
@ -895,6 +928,12 @@ dependencies = [
"libc",
]
[[package]]
name = "funty"
version = "2.0.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e6d5a32815ae3f33302d95fdcb2ce17862f8c65363dcfd29360480ba1001fc9c"
[[package]]
name = "futures"
version = "0.3.31"
@ -1058,9 +1097,9 @@ dependencies = [
[[package]]
name = "hashbrown"
version = "0.15.2"
version = "0.15.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "bf151400ff0baff5465007dd2f3e717f3fe502074ca563069ce3a6629d07b289"
checksum = "84b26c544d002229e640969970a2e74021aadf6e2f96372b9c58eff97de08eb3"
dependencies = [
"allocator-api2",
"equivalent",
@ -1726,12 +1765,44 @@ dependencies = [
"num-traits",
]
[[package]]
name = "ordered-float"
version = "4.6.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7bb71e1b3fa6ca1c61f383464aaf2bb0e2f8e772a1f01d486832464de363b951"
dependencies = [
"num-traits",
]
[[package]]
name = "overload"
version = "0.1.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b15813163c1d831bf4a13c3610c05c0d03b39feb07f7e09fa234dac9b15aaf39"
[[package]]
name = "palette"
version = "0.7.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4cbf71184cc5ecc2e4e1baccdb21026c20e5fc3dcf63028a086131b3ab00b6e6"
dependencies = [
"approx",
"fast-srgb8",
"palette_derive",
]
[[package]]
name = "palette_derive"
version = "0.7.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f5030daf005bface118c096f510ffb781fc28f9ab6a32ab224d8631be6851d30"
dependencies = [
"by_address",
"proc-macro2",
"quote",
"syn",
]
[[package]]
name = "parking"
version = "2.2.1"
@ -1891,6 +1962,19 @@ dependencies = [
"bytemuck",
]
[[package]]
name = "quantette"
version = "0.3.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5a3ba8b34ab55d43cea1f7c183ef3c124cb96fe935c6ec5c6d97127ee340c96e"
dependencies = [
"bitvec",
"num-traits",
"ordered-float 4.6.0",
"palette",
"wide",
]
[[package]]
name = "quick-error"
version = "2.0.1"
@ -1921,6 +2005,12 @@ version = "5.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "74765f6d916ee2faa39bc8e68e4f3ed8949b48cccdac59983d287a7cb71ce9c5"
[[package]]
name = "radium"
version = "0.7.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "dc33ff2d4973d518d823d61aa239014831e521c75da58e3df4840d3f47749d09"
[[package]]
name = "rand"
version = "0.8.5"
@ -2137,9 +2227,9 @@ dependencies = [
[[package]]
name = "rustix"
version = "1.0.5"
version = "1.0.7"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d97817398dd4bb2e6da002002db259209759911da105da92bec29ccb12cf58bf"
checksum = "c71e83d6afe7ff64890ec6b71d6a69bb8a610ab78ce364b3352876bb4c801266"
dependencies = [
"bitflags 2.9.0",
"errno",
@ -2160,6 +2250,15 @@ version = "1.0.20"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "28d3b2b1366ec20994f1fd18c3c594f05c5dd4bc44d8bb0c1c632c8d6829481f"
[[package]]
name = "safe_arch"
version = "0.7.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "96b02de82ddbe1b636e6170c21be622223aea188ef2e139be0a5b219ec215323"
dependencies = [
"bytemuck",
]
[[package]]
name = "same-file"
version = "1.0.6"
@ -2196,7 +2295,7 @@ version = "0.7.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f3a1a3341211875ef120e117ea7fd5228530ae7e7036a779fdc9117be6b3282c"
dependencies = [
"ordered-float",
"ordered-float 2.10.1",
"serde",
]
@ -2420,6 +2519,12 @@ dependencies = [
"version-compare",
]
[[package]]
name = "tap"
version = "1.0.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "55937e1799185b12863d447f42597ed69d9928686b8d88a1df17376a097d8369"
[[package]]
name = "target-lexicon"
version = "0.12.16"
@ -2974,10 +3079,20 @@ checksum = "24d643ce3fd3e5b54854602a080f34fb10ab75e0b813ee32d00ca2b44fa74762"
dependencies = [
"either",
"env_home",
"rustix 1.0.5",
"rustix 1.0.7",
"winsafe",
]
[[package]]
name = "wide"
version = "0.7.32"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "41b5576b9a81633f3e8df296ce0063042a73507636cbe956c61133dd7034ab22"
dependencies = [
"bytemuck",
"safe_arch",
]
[[package]]
name = "winapi"
version = "0.3.9"
@ -3205,9 +3320,9 @@ checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec"
[[package]]
name = "winnow"
version = "0.7.7"
version = "0.7.8"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6cb8234a863ea0e8cd7284fcdd4f145233eb00fee02bbdd9861aec44e6477bc5"
checksum = "9e27d6ad3dac991091e4d35de9ba2d2d00647c5d0fc26c5496dee55984ae111b"
dependencies = [
"memchr",
]
@ -3227,6 +3342,15 @@ dependencies = [
"bitflags 2.9.0",
]
[[package]]
name = "wyz"
version = "0.5.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "05f360fc0b24296329c78fda852a1e9ae82de9cf7b27dae4b7f62f118f77b9ed"
dependencies = [
"tap",
]
[[package]]
name = "yazi-adapter"
version = "25.4.8"
@ -3234,9 +3358,10 @@ dependencies = [
"ansi-to-tui",
"anyhow",
"base64",
"color_quant",
"crossterm 0.29.0",
"image",
"palette",
"quantette",
"ratatui",
"scopeguard",
"tokio",

View file

@ -18,9 +18,10 @@ yazi-term = { path = "../yazi-term", version = "25.4.8" }
ansi-to-tui = { workspace = true }
anyhow = { workspace = true }
base64 = { workspace = true }
color_quant = "1.1.0"
crossterm = { workspace = true }
image = { version = "0.25.6", default-features = false, features = [ "avif", "bmp", "dds", "exr", "ff", "gif", "hdr", "ico", "jpeg", "png", "pnm", "qoi", "tga", "webp" ] }
palette = { version = "0.7.6", default-features = false }
quantette = { version = "0.3.0", default-features = false }
ratatui = { workspace = true }
scopeguard = { workspace = true }
tokio = { workspace = true }

View file

@ -1,11 +1,11 @@
use std::{io::Write, path::Path};
use anyhow::{Result, bail};
use color_quant::NeuQuant;
use crossterm::{cursor::MoveTo, queue};
use image::DynamicImage;
use image::{DynamicImage, GenericImageView, RgbImage};
use palette::{Srgb, cast::ComponentsAs};
use quantette::{ColorSlice, PaletteSize, QuantizeOutput, wu::UIntBinner};
use ratatui::layout::Rect;
use yazi_config::YAZI;
use crate::{CLOSE, ESCAPE, Emulator, Image, START, adapter::Adapter};
@ -42,22 +42,25 @@ impl Sixel {
bail!("image is empty");
}
tokio::task::spawn_blocking(move || {
let img = img.into_rgba8();
let nq = NeuQuant::new(YAZI.preview.sixel_fraction as i32, 256 - alpha as usize, &img);
let (qo, img) = tokio::task::spawn_blocking(move || match &img {
DynamicImage::ImageRgb8(rgb) => Self::quantify(rgb, false).map(|q| (q, img)),
_ => Self::quantify(&img.to_rgb8(), alpha).map(|q| (q, img)),
})
.await??;
let mut buf: Vec<u8> = Vec::with_capacity(1 << 16);
tokio::task::spawn_blocking(move || {
let mut buf = vec![];
write!(buf, "{START}P0;1;8q\"1;1;{};{}", img.width(), img.height())?;
// Palette
for (i, c) in nq.color_map_rgba().chunks(4).enumerate() {
for (i, c) in qo.palette.iter().enumerate() {
write!(
buf,
"#{};2;{};{};{}",
i + alpha as usize,
c[0] as u16 * 100 / 255,
c[1] as u16 * 100 / 255,
c[2] as u16 * 100 / 255
c.red as u16 * 100 / 255,
c.green as u16 * 100 / 255,
c.blue as u16 * 100 / 255
)?;
}
@ -67,8 +70,11 @@ impl Sixel {
let mut last = 0;
let mut repeat = 0usize;
for x in 0..img.width() {
let pixel = img.get_pixel(x, y).0;
let idx = if pixel[3] == 0 { 0 } else { nq.index_of(&pixel) as u8 + alpha as u8 };
let idx = if img.get_pixel(x, y)[3] == 0 {
0
} else {
qo.indices[y as usize * img.width() as usize + x as usize] + alpha as u8
};
if idx == last || repeat == 0 {
(last, repeat) = (idx, repeat + 1);
@ -101,4 +107,15 @@ impl Sixel {
})
.await?
}
fn quantify(rgb: &RgbImage, alpha: bool) -> Result<QuantizeOutput<Srgb<u8>>> {
let buf = &rgb.as_raw()[..(rgb.pixels().len() * 3)];
let slice: ColorSlice<Srgb<u8>> = buf.components_as().try_into()?;
Ok(quantette::wu::indexed_palette(
&slice,
PaletteSize::try_from(256u16 - alpha as u16)?,
&UIntBinner::<32>,
))
}
}

View file

@ -25,7 +25,6 @@ cache_dir = ""
image_delay = 30
image_filter = "triangle"
image_quality = 75
sixel_fraction = 20
ueberzug_scale = 1
ueberzug_offset = [ 0, 0, 0, 0 ]

View file

@ -20,10 +20,9 @@ pub struct Preview {
pub cache_dir: PathBuf,
pub image_delay: u8,
pub image_filter: String,
pub image_quality: u8,
pub sixel_fraction: u8,
pub image_delay: u8,
pub image_filter: String,
pub image_quality: u8,
pub ueberzug_scale: f32,
pub ueberzug_offset: (f32, f32, f32, f32),
@ -50,8 +49,6 @@ impl Preview {
bail!("[preview].image_delay must be between 0 and 100.");
} else if self.image_quality < 50 || self.image_quality > 90 {
bail!("[preview].image_quality must be between 50 and 90.");
} else if self.sixel_fraction < 10 || self.sixel_fraction > 30 {
bail!("[preview].sixel_fraction must be between 10 and 30.");
}
self.cache_dir = if self.cache_dir.as_os_str().is_empty() {

View file

@ -78,7 +78,6 @@ impl Runtime {
b"image_delay" => lua.to_value_with(&p.image_delay, OPTS)?,
b"image_filter" => lua.to_value_with(&p.image_filter, OPTS)?,
b"image_quality" => lua.to_value_with(&p.image_quality, OPTS)?,
b"sixel_fraction" => lua.to_value_with(&p.sixel_fraction, OPTS)?,
b"ueberzug_scale" => lua.to_value_with(&p.ueberzug_scale, OPTS)?,
b"ueberzug_offset" => lua.to_value_with(&p.ueberzug_offset, OPTS)?,

View file

@ -1,7 +1,7 @@
use std::mem;
use ansi_to_tui::IntoText;
use mlua::{ExternalError, ExternalResult, IntoLua, Lua, MetaMethod, ObjectLike, Table, UserData, Value};
use mlua::{ExternalError, ExternalResult, IntoLua, Lua, MetaMethod, Table, UserData, Value};
use ratatui::widgets::Widget;
use super::{Area, Line, Span};