diff --git a/.github/workflows/check.yml b/.github/workflows/check.yml index 5601b3c7..e8ca53f9 100644 --- a/.github/workflows/check.yml +++ b/.github/workflows/check.yml @@ -7,7 +7,6 @@ on: branches: [main] env: - SCCACHE_GHA_ENABLED: true RUSTC_WRAPPER: sccache jobs: diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 801d4579..a1967c46 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -7,7 +7,6 @@ on: branches: [main] env: - SCCACHE_GHA_ENABLED: true RUSTC_WRAPPER: sccache CARGO_TERM_COLOR: always diff --git a/Cargo.lock b/Cargo.lock index 8d8ac2ac..fe312a12 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -145,6 +145,12 @@ dependencies = [ "syn", ] +[[package]] +name = "arrayref" +version = "0.3.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "76a2e8124351fda1ef8aaaa3bbd7ebbcb486bbcd4225aca0aa0d84bb2db8fecb" + [[package]] name = "arrayvec" version = "0.7.6" @@ -505,6 +511,15 @@ version = "0.8.7" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "773648b94d0e5d620f64f280777445740e61fe701025087ec8b57f45c791888b" +[[package]] +name = "core_maths" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "77745e017f5edba1a9c1d854f6f3a52dac8a12dd5af5d2f54aecf61e43d80d30" +dependencies = [ + "libm", +] + [[package]] name = "crc32fast" version = "1.4.2" @@ -645,6 +660,12 @@ dependencies = [ "syn", ] +[[package]] +name = "data-url" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5c297a1c74b71ae29df00c3e22dd9534821d60eb9af5a0192823fa2acea70c2a" + [[package]] name = "deranged" version = "0.4.0" @@ -874,6 +895,12 @@ dependencies = [ "miniz_oxide", ] +[[package]] +name = "float-cmp" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "98de4bbd547a563b716d8dfa9aad1cb19bfab00f4fa09a6a4ed21dbcf44ce9c4" + [[package]] name = "fnv" version = "1.0.7" @@ -886,6 +913,29 @@ version = "0.1.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d9c4f5dac5e15c24eb999c26181a6ca40b39fe946cbe4c263c7209467bc83af2" +[[package]] +name = "fontconfig-parser" +version = "0.5.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c1fcfcd44ca6e90c921fee9fa665d530b21ef1327a4c1a6c5250ea44b776ada7" +dependencies = [ + "roxmltree", +] + +[[package]] +name = "fontdb" +version = "0.23.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "457e789b3d1202543297a350643cf459f836cade38934e7a4cf6a39e7cde2905" +dependencies = [ + "fontconfig-parser", + "log", + "memmap2", + "slotmap", + "tinyvec", + "ttf-parser", +] + [[package]] name = "fsevent-sys" version = "4.1.0" @@ -1134,6 +1184,12 @@ dependencies = [ "quick-error", ] +[[package]] +name = "imagesize" +version = "0.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "edcd27d72f2f071c64249075f42e205ff93c9a4c5f6c6da53e79ed9f9832c285" + [[package]] name = "imgref" version = "1.11.0" @@ -1271,6 +1327,16 @@ dependencies = [ "libc", ] +[[package]] +name = "kurbo" +version = "0.11.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "89234b2cc610a7dd927ebde6b41dd1a5d4214cffaef4cf1fb2195d592f92518f" +dependencies = [ + "arrayvec", + "smallvec", +] + [[package]] name = "lazy_static" version = "1.5.0" @@ -1299,6 +1365,12 @@ dependencies = [ "cc", ] +[[package]] +name = "libm" +version = "0.2.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8355be11b20d696c8f18f6cc018c4e372165b1fa8126cef092399c9951984ffa" + [[package]] name = "libredox" version = "0.1.3" @@ -1433,6 +1505,15 @@ version = "2.7.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "78ca9ab1a0babb1e7d5695e3530886289c18cf2f87ec19a575a0abdce112e3a3" +[[package]] +name = "memmap2" +version = "0.9.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fd3f7eed9d3848f8b98834af67102b720745c4ec028fcd0aa0239277e7de374f" +dependencies = [ + "libc", +] + [[package]] name = "minimal-lexical" version = "0.2.1" @@ -1773,6 +1854,12 @@ version = "2.3.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e3148f5046208a5d56bcfc03053e3ca6334e51da8dfb19b6cdc8b306fae3283e" +[[package]] +name = "pico-args" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5be167a7af36ee22fe3115051bc51f6e6c7054c9348e28deb4f49bd6f705a315" + [[package]] name = "pin-project-lite" version = "0.2.16" @@ -2095,11 +2182,37 @@ version = "0.8.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2b15c43186be67a4fd63bee50d0303afffcef381492ebe2c5d87f324e1b8815c" +[[package]] +name = "resvg" +version = "0.45.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dd43d1c474e9dadf09a8fdf22d713ba668b499b5117b9b9079500224e26b5b29" +dependencies = [ + "gif", + "image-webp", + "log", + "pico-args", + "rgb", + "svgtypes", + "tiny-skia", + "usvg", + "zune-jpeg", +] + [[package]] name = "rgb" version = "0.8.50" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "57397d16646700483b67d2dd6511d79318f9d057fdbd21a4066aeac8b41d310a" +dependencies = [ + "bytemuck", +] + +[[package]] +name = "roxmltree" +version = "0.20.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6c20b6793b5c2fa6553b250154b78d6d0db37e72700ae35fad9387a46f487c97" [[package]] name = "rustc-demangle" @@ -2154,6 +2267,24 @@ version = "1.0.20" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "eded382c5f5f786b989652c49544c4877d9f015cc22e145a5ea8ea66c2921cd2" +[[package]] +name = "rustybuzz" +version = "0.20.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fd3c7c96f8a08ee34eff8857b11b49b07d71d1c3f4e88f8a88d4c9e9f90b1702" +dependencies = [ + "bitflags 2.9.0", + "bytemuck", + "core_maths", + "log", + "smallvec", + "ttf-parser", + "unicode-bidi-mirroring", + "unicode-ccc", + "unicode-properties", + "unicode-script", +] + [[package]] name = "ryu" version = "1.0.20" @@ -2310,6 +2441,21 @@ version = "0.1.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e3a9fe34e3e7a50316060351f37187a3f546bce95496156754b601a5fa71b76e" +[[package]] +name = "simplecss" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7a9c6883ca9c3c7c90e888de77b7a5c849c779d25d74a1269b0218b14e8b136c" +dependencies = [ + "log", +] + +[[package]] +name = "siphasher" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "56199f7ddabf13fe5074ce809e7d3f42b42ae711800501b5b16ea82ad029c39d" + [[package]] name = "slab" version = "0.4.9" @@ -2319,6 +2465,15 @@ dependencies = [ "autocfg", ] +[[package]] +name = "slotmap" +version = "1.0.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dbff4acf519f630b3a3ddcfaea6c06b42174d9a44bc70c620e9ed1649d58b82a" +dependencies = [ + "version_check", +] + [[package]] name = "smallvec" version = "1.15.0" @@ -2347,6 +2502,15 @@ version = "1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a2eb9349b6444b326872e140eb1cf5e7c522154d69e7a0ffb0fb81c06b37543f" +[[package]] +name = "strict-num" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6637bab7722d379c8b41ba849228d680cc12d0a45ba1fa2b48f2a30577a06731" +dependencies = [ + "float-cmp", +] + [[package]] name = "strsim" version = "0.11.1" @@ -2375,6 +2539,16 @@ dependencies = [ "syn", ] +[[package]] +name = "svgtypes" +version = "0.15.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "68c7541fff44b35860c1a7a47a7cadf3e4a304c457b58f9870d9706ece028afc" +dependencies = [ + "kurbo", + "siphasher", +] + [[package]] name = "syn" version = "2.0.101" @@ -2540,6 +2714,47 @@ dependencies = [ "time-core", ] +[[package]] +name = "tiny-skia" +version = "0.11.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "83d13394d44dae3207b52a326c0c85a8bf87f1541f23b0d143811088497b09ab" +dependencies = [ + "arrayref", + "arrayvec", + "bytemuck", + "cfg-if", + "log", + "png", + "tiny-skia-path", +] + +[[package]] +name = "tiny-skia-path" +version = "0.11.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9c9e7fc0c2e86a30b117d0462aa261b72b7a99b7ebd7deb3a14ceda95c5bdc93" +dependencies = [ + "arrayref", + "bytemuck", + "strict-num", +] + +[[package]] +name = "tinyvec" +version = "1.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "09b3661f17e86524eccd4371ab0429194e0d7c008abb45f7a7495b1719463c71" +dependencies = [ + "tinyvec_macros", +] + +[[package]] +name = "tinyvec_macros" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1f3ccbac311fea05f86f61904b462b55fb3df8837a366dfc601a0161d0532f20" + [[package]] name = "tokio" version = "1.44.2" @@ -2725,6 +2940,15 @@ dependencies = [ "windows", ] +[[package]] +name = "ttf-parser" +version = "0.25.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d2df906b07856748fa3f6e0ad0cbaa047052d4a7dd609e231c4f72cee8c36f31" +dependencies = [ + "core_maths", +] + [[package]] name = "twox-hash" version = "2.1.0" @@ -2746,6 +2970,24 @@ version = "1.18.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1dccffe3ce07af9386bfd29e80c0ab1a8205a2fc34e4bcd40364df902cfa8f3f" +[[package]] +name = "unicode-bidi" +version = "0.3.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5c1cb5db39152898a79168971543b1cb5020dff7fe43c8dc468b0885f5e29df5" + +[[package]] +name = "unicode-bidi-mirroring" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5dfa6e8c60bb66d49db113e0125ee8711b7647b5579dc7f5f19c42357ed039fe" + +[[package]] +name = "unicode-ccc" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ce61d488bcdc9bc8b5d1772c404828b17fc481c0a582b5581e95fb233aef503e" + [[package]] name = "unicode-ident" version = "1.0.18" @@ -2758,6 +3000,18 @@ version = "0.1.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3b09c83c3c29d37506a3e260c08c03743a6bb66a9cd432c6934ab501a190571f" +[[package]] +name = "unicode-properties" +version = "0.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e70f2a8b45122e719eb623c01822704c4e0907e7e426a05927e1a1cfff5b75d0" + +[[package]] +name = "unicode-script" +version = "0.5.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9fb421b350c9aff471779e262955939f565ec18b86c15364e6bdf0d662ca7c1f" + [[package]] name = "unicode-segmentation" version = "1.12.0" @@ -2775,6 +3029,12 @@ dependencies = [ "unicode-width 0.1.14", ] +[[package]] +name = "unicode-vo" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b1d386ff53b415b7fe27b50bb44679e2cc4660272694b7b6f3326d8480823a94" + [[package]] name = "unicode-width" version = "0.1.14" @@ -2793,6 +3053,33 @@ version = "2.1.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "daf8dba3b7eb870caf1ddeed7bc9d2a049f3cfdfae7cb521b087cc33ae4c49da" +[[package]] +name = "usvg" +version = "0.45.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2ac8e0e3e4696253dc06167990b3fe9a2668ab66270adf949a464db4088cb354" +dependencies = [ + "base64", + "data-url", + "flate2", + "fontdb", + "imagesize", + "kurbo", + "log", + "pico-args", + "roxmltree", + "rustybuzz", + "simplecss", + "siphasher", + "strict-num", + "svgtypes", + "tiny-skia-path", + "unicode-bidi", + "unicode-script", + "unicode-vo", + "xmlwriter", +] + [[package]] name = "utf8parse" version = "0.2.2" @@ -3227,6 +3514,12 @@ dependencies = [ "bitflags 2.9.0", ] +[[package]] +name = "xmlwriter" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ec7a2a501ed189703dba8b08142f057e887dfc4b2cc4db2d343ac6376ba3e0b9" + [[package]] name = "yazi-adapter" version = "25.4.8" @@ -3238,6 +3531,7 @@ dependencies = [ "crossterm 0.29.0", "image", "ratatui", + "resvg", "scopeguard", "tokio", "tracing", @@ -3475,12 +3769,12 @@ dependencies = [ "crossterm 0.29.0", "futures", "globset", - "libc", "md-5", "mlua", "parking_lot", "paste", "ratatui", + "resvg", "serde_json", "syntect", "tokio", @@ -3490,7 +3784,6 @@ dependencies = [ "twox-hash", "unicode-width 0.2.0", "uzers", - "windows-sys 0.59.0", "yazi-adapter", "yazi-binding", "yazi-boot", diff --git a/Cargo.toml b/Cargo.toml index 996b8227..c37f071c 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -41,6 +41,7 @@ parking_lot = "0.12.3" paste = "1.0.15" ratatui = { version = "0.29.0", features = [ "unstable-rendered-line-info" ] } regex = "1.11.1" +resvg = { version = "0.45.0" } scopeguard = "1.2.0" serde = { version = "1.0.219", features = [ "derive" ] } serde_json = "1.0.140" diff --git a/yazi-adapter/Cargo.toml b/yazi-adapter/Cargo.toml index 0b544e4a..00971a50 100644 --- a/yazi-adapter/Cargo.toml +++ b/yazi-adapter/Cargo.toml @@ -21,6 +21,7 @@ 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" ] } ratatui = { workspace = true } +resvg = { workspace = true } scopeguard = { workspace = true } tokio = { workspace = true } tracing = { workspace = true } diff --git a/yazi-adapter/src/image.rs b/yazi-adapter/src/image.rs index d9b56593..e98bfb21 100644 --- a/yazi-adapter/src/image.rs +++ b/yazi-adapter/src/image.rs @@ -1,27 +1,24 @@ -use std::path::{Path, PathBuf}; +use std::{path::{Path, PathBuf}, sync::{LazyLock, RwLock}}; use anyhow::Result; -use image::{DynamicImage, ExtendedColorType, ImageDecoder, ImageEncoder, ImageError, ImageReader, ImageResult, Limits, codecs::{jpeg::JpegEncoder, png::PngEncoder}, imageops::FilterType, metadata::Orientation}; +use image::{DynamicImage, ExtendedColorType, ImageBuffer, ImageDecoder, ImageEncoder, ImageError, ImageReader, ImageResult, Limits, codecs::{jpeg::JpegEncoder, png::PngEncoder}, error::UnsupportedErrorKind, imageops::FilterType, metadata::Orientation}; use ratatui::layout::Rect; +use resvg::{tiny_skia::{Pixmap, Transform}, usvg::{Options, Tree}}; use yazi_config::YAZI; use crate::Dimension; +pub static GLOBAL_OPTIONS: LazyLock>> = + LazyLock::new(|| RwLock::new(Options::default())); + pub struct Image; impl Image { pub async fn precache(path: &Path, cache: PathBuf) -> Result<()> { - let (mut img, orientation, icc) = Self::decode_from(path).await?; - let (w, h) = Self::flip_size(orientation, (YAZI.preview.max_width, YAZI.preview.max_height)); + let (img, icc) = + Self::decode_to_fit_from(path, YAZI.preview.max_width, YAZI.preview.max_height).await?; let buf = tokio::task::spawn_blocking(move || { - if img.width() > w || img.height() > h { - img = img.resize(w, h, Self::filter()); - } - if orientation != Orientation::NoTransforms { - img.apply_orientation(orientation); - } - let mut buf = Vec::new(); if img.color().has_alpha() { let rgba = img.into_rgba8(); @@ -42,25 +39,8 @@ impl Image { } pub(super) async fn downscale(path: &Path, rect: Rect) -> Result { - let (mut img, orientation, _) = Self::decode_from(path).await?; - let (w, h) = Self::flip_size(orientation, Self::max_pixel(rect)); - - // Fast path. - if img.width() <= w && img.height() <= h && orientation == Orientation::NoTransforms { - return Ok(img); - } - - let img = tokio::task::spawn_blocking(move || { - if img.width() > w || img.height() > h { - img = img.resize(w, h, Self::filter()) - } - if orientation != Orientation::NoTransforms { - img.apply_orientation(orientation); - } - img - }) - .await?; - + let (width, height) = Self::max_pixel(rect); + let (img, _) = Self::decode_to_fit_from(path, width, height).await?; Ok(img) } @@ -96,7 +76,54 @@ impl Image { } } - async fn decode_from(path: &Path) -> ImageResult<(DynamicImage, Orientation, Option>)> { + async fn decode_to_fit_from( + path: &Path, + width: u32, + height: u32, + ) -> Result<(DynamicImage, Option>)> { + let path = path.to_owned(); + + tokio::task::spawn_blocking(move || { + Self::try_decode_raster(&path, width, height).or_else(|err| match err { + ImageError::Unsupported(ref unsupported) => match unsupported.kind() { + UnsupportedErrorKind::Format(_) => Self::try_decode_svg(&path, width, height), + _ => Err(err.into()), + }, + _ => Err(err.into()), + }) + }) + .await + .map_err(|e| ImageError::IoError(e.into()))? + } + + #[inline] + fn try_decode_raster( + path: &Path, + width: u32, + height: u32, + ) -> ImageResult<(DynamicImage, Option>)> { + let limits = Self::build_limits(); + let mut reader = ImageReader::open(path)?; + reader.limits(limits); + let mut decoder = reader.with_guessed_format()?.into_decoder()?; + let orientation = decoder.orientation().unwrap_or(Orientation::NoTransforms); + let icc = decoder.icc_profile().unwrap_or_default(); + + let mut img = DynamicImage::from_decoder(decoder)?; + let (w, h) = Self::flip_size(orientation, (width, height)); + + if img.width() > w || img.height() > h { + img = img.resize(w, h, Self::filter()); + } + if orientation != Orientation::NoTransforms { + img.apply_orientation(orientation); + } + + Ok((img, icc)) + } + + #[inline] + fn build_limits() -> Limits { let mut limits = Limits::no_limits(); if YAZI.tasks.image_alloc > 0 { limits.max_alloc = Some(YAZI.tasks.image_alloc as u64); @@ -107,20 +134,76 @@ impl Image { if YAZI.tasks.image_bound[1] > 0 { limits.max_image_height = Some(YAZI.tasks.image_bound[1] as u32); } + limits + } - let path = path.to_owned(); - tokio::task::spawn_blocking(move || { - let mut reader = ImageReader::open(path)?; - reader.limits(limits); + #[inline] + fn try_decode_svg( + path: &Path, + width: u32, + height: u32, + ) -> Result<(DynamicImage, Option>)> { + let pixmap = Self::render_svg_to_fit_from(path, width, height)?; + let (width, height) = (pixmap.width(), pixmap.height()); + let mut container = pixmap.take(); - let mut decoder = reader.with_guessed_format()?.into_decoder()?; - let orientation = decoder.orientation().unwrap_or(Orientation::NoTransforms); - let icc = decoder.icc_profile().unwrap_or_default(); + for rgba in container.chunks_exact_mut(4) { + let alpha = rgba[3]; + if alpha != 0xff { + let pixel: &mut [u8; 4] = unsafe { rgba.try_into().unwrap_unchecked() }; + let a = alpha as f64 / 255.0; + pixel[0] = (pixel[0] as f64 / a + 0.5) as u8; + pixel[1] = (pixel[1] as f64 / a + 0.5) as u8; + pixel[2] = (pixel[2] as f64 / a + 0.5) as u8; + } + } - Ok((DynamicImage::from_decoder(decoder)?, orientation, icc)) - }) - .await - .map_err(|e| ImageError::IoError(e.into()))? + let img = ImageBuffer::from_raw(width, height, container) + .ok_or_else(|| anyhow::anyhow!("Failed to create image buffer"))?; + + Ok((DynamicImage::ImageRgba8(img), None)) + } + + /// Helper function to rasterize an SVG to a pixmap fitting within max_width + /// and max_height. + fn render_svg_to_fit_from(path: &Path, max_width: u32, max_height: u32) -> Result { + let svg = std::fs::read(path)?; + let options_guard = + GLOBAL_OPTIONS.read().map_err(|e| anyhow::anyhow!("RwLock poisoned: {}", e))?; + let tree = Tree::from_data(&svg, &options_guard)?; + let (width, height, transform) = Self::svg_size_and_scale(&tree, max_width, max_height); + + let mut pixmap = + Pixmap::new(width, height).ok_or_else(|| anyhow::anyhow!("Cannot create pixmap"))?; + + resvg::render(&tree, transform, &mut pixmap.as_mut()); + + Ok(pixmap) + } + + fn svg_size_and_scale(tree: &Tree, max_width: u32, max_height: u32) -> (u32, u32, Transform) { + // It is Ok. The max_width and max_height could not be larger then monitor + // dimensions which much less then f32::MAX + let max_width = max_width as f32; + let max_height = max_height as f32; + let mut width = tree.size().width(); + let mut height = tree.size().height(); + + for node in tree.root().children() { + if let Some(bounding_box) = node.abs_layer_bounding_box() { + width = width.max(bounding_box.width()); + height = height.max(bounding_box.height()); + } + } + if width <= max_width && height <= max_height { + return (width.floor() as u32, height.floor() as u32, Transform::from_scale(1.0, 1.0)); + } + let ratio = f32::min(max_width / width, max_height / height); + ( + (width * ratio).floor() as u32, + (height * ratio).floor() as u32, + Transform::from_scale(ratio, ratio), + ) } fn flip_size(orientation: Orientation, (w, h): (u32, u32)) -> (u32, u32) { diff --git a/yazi-adapter/src/info.rs b/yazi-adapter/src/info.rs index cdc1d60f..21bf9cf4 100644 --- a/yazi-adapter/src/info.rs +++ b/yazi-adapter/src/info.rs @@ -1,6 +1,9 @@ -use std::path::Path; +use std::{collections::HashSet, path::Path}; use image::{ImageDecoder, ImageError}; +use resvg::usvg::{Font, Group, ImageKind, Node, Tree}; + +use super::image::GLOBAL_OPTIONS; pub type ImageFormat = image::ImageFormat; pub type ImageColor = image::ColorType; @@ -42,3 +45,77 @@ impl ImageInfo { .map_err(|e| ImageError::IoError(e.into()))? } } + +#[derive(Debug, Clone)] +pub struct SvgInfo { + pub width: f32, + pub height: f32, + pub fonts: HashSet, + pub layers: Vec, +} + +impl SvgInfo { + pub async fn new(path: &Path) -> anyhow::Result { + let path = path.to_owned(); + tokio::task::spawn_blocking(move || { + let svg = std::fs::read(path)?; + let options_guard = + GLOBAL_OPTIONS.read().map_err(|e| anyhow::anyhow!("RwLock poisoned: {}", e))?; + let tree = Tree::from_data(&svg, &options_guard)?; + + let mut info = SvgInfo { + width: tree.size().width().round(), + height: tree.size().height().round(), + fonts: HashSet::new(), + layers: Vec::new(), + }; + + Self::collect_group(tree.root(), &mut info); + + Ok(info) + }) + .await? + } + + fn collect_group(group: &Group, info: &mut SvgInfo) { + if !group.id().is_empty() { + info.layers.push(group.id().to_string()); + } + + for node in group.children() { + Self::collect_node(node, info); + } + } + + fn collect_node(node: &Node, info: &mut SvgInfo) { + if let Some(bounding_box) = node.abs_layer_bounding_box() { + info.width = info.width.max(bounding_box.width()); + info.height = info.height.max(bounding_box.height()); + } + match node { + Node::Group(g) => Self::collect_group(g, info), + + Node::Path(_) => {} + + Node::Text(t) => { + for chunk in t.chunks() { + for span in chunk.spans() { + if !info.fonts.contains(span.font()) { + info.fonts.insert(span.font().clone()); + } + } + } + } + + Node::Image(i) => { + if let ImageKind::SVG(sub_tree) = &i.kind() { + Self::collect_group(sub_tree.root(), info); + } + } + } + + node.subroots(|subroot: &Group| { + Self::collect_group(subroot, info); + }); + } +} diff --git a/yazi-plugin/Cargo.toml b/yazi-plugin/Cargo.toml index 16fb1787..dcaaa5d3 100644 --- a/yazi-plugin/Cargo.toml +++ b/yazi-plugin/Cargo.toml @@ -35,6 +35,7 @@ mlua = { workspace = true } parking_lot = { workspace = true } paste = { workspace = true } ratatui = { workspace = true } +resvg = { workspace = true } serde_json = { workspace = true } syntect = { workspace = true } tokio = { workspace = true } @@ -46,12 +47,10 @@ unicode-width = { workspace = true } yazi-prebuilt = "0.1.0" [target."cfg(unix)".dependencies] -libc = { workspace = true } uzers = { workspace = true } [target."cfg(windows)".dependencies] clipboard-win = "5.4.0" -windows-sys = { version = "0.59.0", features = [ "Win32_Security", "Win32_System_JobObjects", "Win32_System_Threading" ] } [target.'cfg(target_os = "macos")'.dependencies] crossterm = { workspace = true, features = [ "use-dev-tty", "libc" ] } diff --git a/yazi-plugin/preset/plugins/svg.lua b/yazi-plugin/preset/plugins/svg.lua index 7d046b17..92f40d7f 100644 --- a/yazi-plugin/preset/plugins/svg.lua +++ b/yazi-plugin/preset/plugins/svg.lua @@ -1,71 +1,57 @@ local M = {} function M:peek(job) - local start, cache = os.clock(), ya.file_cache(job) - if not cache then - return - end - - local ok, err = self:preload(job) - if not ok or err then - return - end - + local start = os.clock() ya.sleep(math.max(0, rt.preview.image_delay / 1000 + start - os.clock())) - ya.image_show(cache, job.area) + ya.image_show(job.file.url, job.area) ya.preview_widgets(job, {}) end function M:seek() end -function M:preload(job) - local cache = ya.file_cache(job) - if not cache or fs.cha(cache) then - return true - end +function M:preload(job) end - -- stylua: ignore - local cmd = Command("resvg"):args { - "-w", rt.preview.max_width, "-h", rt.preview.max_height, - "--image-rendering", "optimizeSpeed", - tostring(job.file.url), tostring(cache) - } - if rt.tasks.image_alloc > 0 then - cmd = cmd:memory(rt.tasks.image_alloc) - end +function M:spot(job) + local rows = self:spot_base(job) + rows[#rows + 1] = ui.Row {} - local child, err = cmd:spawn() - if not child then - return true, Err("Failed to start `resvg`, error: %s", err) - end - - local status, err - while true do - ya.sleep(0.2) - - status, err = child:try_wait() - if status or err then - break - end - - local id, mem = child:id(), nil - if id then - mem = ya.proc_info(id).mem_resident - end - if mem and mem > rt.tasks.image_alloc then - child:start_kill() - err = Err("memory limit exceeded, pid: %s, memory: %s", id, mem) - break - end - end - - if status then - return status.success - else - return true, Err("Error while running `resvg`: %s", err) - end + ya.spot_table( + job, + ui.Table(ya.list_merge(rows, require("file"):spot_base(job))) + :area(ui.Pos { "center", w = 60, h = 20 }) + :row(job.skip) + :row(1) + :col(1) + :col_style(th.spot.tbl_col) + :cell_style(th.spot.tbl_cell) + :widths { ui.Constraint.Length(14), ui.Constraint.Fill(1) } + ) end -function M:spot(job) require("file"):spot(job) end +function M:spot_base(job) + local info = ya.svg_info(job.file.url) + if not info then + return {} + end + + local rows = { + ui.Row({ "SVG Image" }):style(ui.Style():fg("green")), + ui.Row { " Size:", string.format("%.0fx%.0f", info.w, info.h) }, + } + + if #info.layers > 0 then + rows[#rows + 1] = ui.Row { " Layers:", table.concat(info.layers, ", ") } + end + + if #info.fonts > 0 then + rows[#rows + 1] = ui.Row { " Fonts:", tostring(info.fonts[1]) } + + for i = 2, #info.fonts do + rows[#rows + 1] = ui.Row { "", tostring(info.fonts[i]) } + end + end + + return rows +end return M diff --git a/yazi-plugin/src/bindings/image.rs b/yazi-plugin/src/bindings/image.rs index 3d2a7b10..482511bd 100644 --- a/yazi-plugin/src/bindings/image.rs +++ b/yazi-plugin/src/bindings/image.rs @@ -1,6 +1,7 @@ -use std::ops::Deref; +use std::{fmt::Write, ops::Deref}; use mlua::{MetaMethod, UserData}; +use resvg::usvg; pub struct ImageInfo(yazi_adapter::ImageInfo); @@ -81,3 +82,115 @@ impl UserData for ImageColor { }); } } + +pub struct SvgInfo(yazi_adapter::SvgInfo); + +impl Deref for SvgInfo { + type Target = yazi_adapter::SvgInfo; + + fn deref(&self) -> &Self::Target { &self.0 } +} + +impl From for SvgInfo { + fn from(value: yazi_adapter::SvgInfo) -> Self { Self(value) } +} + +impl UserData for SvgInfo { + fn add_fields>(fields: &mut F) { + fields.add_field_method_get("w", |_, this| Ok(this.width)); + fields.add_field_method_get("h", |_, this| Ok(this.height)); + + fields.add_field_method_get("layers", |lua, this| { + let table = lua.create_table()?; + for (i, layer) in this.layers.iter().enumerate() { + table.set(i + 1, layer.clone())?; + } + Ok(table) + }); + + fields.add_field_method_get("fonts", |lua, this| { + let table = lua.create_table()?; + for (i, font) in this.fonts.iter().enumerate() { + table.set(i + 1, Font(font.clone()))?; + } + Ok(table) + }); + } +} + +pub struct Font(usvg::Font); + +impl Deref for Font { + type Target = usvg::Font; + + fn deref(&self) -> &Self::Target { &self.0 } +} + +trait ToStaticStr { + fn as_str(&self) -> &'static str; +} + +impl ToStaticStr for usvg::FontStyle { + fn as_str(&self) -> &'static str { + match self { + Self::Normal => "Normal", + Self::Italic => "Italic", + Self::Oblique => "Oblique", + } + } +} + +impl ToStaticStr for usvg::FontStretch { + fn as_str(&self) -> &'static str { + match self { + Self::UltraCondensed => "UltraCondensed", + Self::ExtraCondensed => "ExtraCondensed", + Self::Condensed => "Condensed", + Self::SemiCondensed => "SemiCondensed", + Self::Normal => "Normal", + Self::SemiExpanded => "SemiExpanded", + Self::Expanded => "Expanded", + Self::ExtraExpanded => "ExtraExpanded", + Self::UltraExpanded => "UltraExpanded", + } + } +} + +impl UserData for Font { + fn add_fields>(fields: &mut F) { + fields.add_field_method_get("families", |_, this| { + let mut families = String::new(); + for (i, family) in this.families().iter().enumerate() { + if i > 0 { + families.push_str(", "); + } + write!(&mut families, "{}", family).unwrap(); + } + Ok(families) + }); + fields.add_field_method_get("style", |_, this| Ok(this.style().as_str())); + fields.add_field_method_get("stretch", |_, this| Ok(this.stretch().as_str())); + + fields.add_field_method_get("weight", |_, this| Ok(this.weight())); + } + + fn add_methods>(methods: &mut M) { + methods.add_meta_method(MetaMethod::ToString, |_, this, ()| { + let mut families = String::new(); + for (i, family) in this.families().iter().enumerate() { + if i > 0 { + families.push_str(", "); + } + write!(&mut families, "{}", family).unwrap(); + } + + Ok(format!( + "{}, style: {}, stretch: {}, weight: {}", + families, + this.style().as_str(), + this.stretch().as_str(), + this.weight() + )) + }); + } +} diff --git a/yazi-plugin/src/process/child.rs b/yazi-plugin/src/process/child.rs index f9f9bd3a..7e53e765 100644 --- a/yazi-plugin/src/process/child.rs +++ b/yazi-plugin/src/process/child.rs @@ -1,7 +1,7 @@ -use std::{ops::DerefMut, process::ExitStatus, time::Duration}; +use std::{ops::DerefMut, time::Duration}; use futures::future::try_join3; -use mlua::{AnyUserData, ExternalError, IntoLua, IntoLuaMulti, Table, UserData, UserDataMethods, Value}; +use mlua::{AnyUserData, ExternalError, IntoLua, IntoLuaMulti, Table, UserData, Value}; use tokio::{io::{self, AsyncBufReadExt, AsyncReadExt, AsyncWriteExt, BufReader, BufWriter}, process::{ChildStderr, ChildStdin, ChildStdout}, select}; use yazi_binding::Error; @@ -9,94 +9,39 @@ use super::Status; use crate::process::Output; pub struct Child { - inner: tokio::process::Child, - stdin: Option>, - stdout: Option>, - stderr: Option>, - #[cfg(windows)] - job_handle: Option, -} - -#[cfg(windows)] -impl Drop for Child { - fn drop(&mut self) { - if let Some(h) = self.job_handle.take() { - unsafe { windows_sys::Win32::Foundation::CloseHandle(h) }; - } - } + inner: tokio::process::Child, + stdin: Option>, + stdout: Option>, + stderr: Option>, } impl Child { - pub fn new( - mut inner: tokio::process::Child, - #[cfg(windows)] job_handle: Option, - ) -> Self { + pub fn new(mut inner: tokio::process::Child) -> Self { let stdin = inner.stdin.take().map(BufWriter::new); let stdout = inner.stdout.take().map(BufReader::new); let stderr = inner.stderr.take().map(BufReader::new); - Self { - inner, - stdin, - stdout, - stderr, - #[cfg(windows)] - job_handle, - } - } - - pub(super) async fn wait(&mut self) -> io::Result { - drop(self.stdin.take()); - self.inner.wait().await - } - - pub(super) async fn status(&mut self) -> io::Result { - drop(self.stdin.take()); - drop(self.stdout.take()); - drop(self.stderr.take()); - self.inner.wait().await - } - - async fn read_line(&mut self) -> (Option>, u8) { - async fn read(r: Option) -> Option> { - let mut buf = Vec::new(); - match r?.read_until(b'\n', &mut buf).await { - Ok(0) | Err(_) => None, - Ok(_) => Some(buf), - } - } - - select! { - r @ Some(_) = read(self.stdout.as_mut()) => (r, 0u8), - r @ Some(_) = read(self.stderr.as_mut()) => (r, 1u8), - else => (None, 2u8), - } - } - - pub(super) async fn wait_with_output(mut self) -> io::Result { - async fn read(r: &mut Option) -> io::Result> { - let mut vec = Vec::new(); - if let Some(r) = r.as_mut() { - r.read_to_end(&mut vec).await?; - } - Ok(vec) - } - - // Ensure stdin is closed so the child isn't stuck waiting on input while the - // parent is waiting for it to exit. - drop(self.stdin.take()); - - // Drop happens after `try_join` due to - let mut stdout = self.stdout.take(); - let mut stderr = self.stderr.take(); - - let result = try_join3(self.inner.wait(), read(&mut stdout), read(&mut stderr)).await?; - Ok(std::process::Output { status: result.0, stdout: result.1, stderr: result.2 }) + Self { inner, stdin, stdout, stderr } } } impl UserData for Child { - fn add_methods>(methods: &mut M) { - methods.add_method("id", |_, me, ()| Ok(me.inner.id())); + fn add_methods>(methods: &mut M) { + #[inline] + async fn read_line(me: &mut Child) -> (Option>, u8) { + async fn read(r: Option) -> Option> { + let mut buf = Vec::new(); + match r?.read_until(b'\n', &mut buf).await { + Ok(0) | Err(_) => None, + Ok(_) => Some(buf), + } + } + + select! { + r @ Some(_) = read(me.stdout.as_mut()) => (r, 0u8), + r @ Some(_) = read(me.stderr.as_mut()) => (r, 1u8), + else => (None, 2u8), + } + } methods.add_async_method_mut("read", |_, mut me, len: usize| async move { async fn read(r: Option, len: usize) -> Option> { @@ -117,15 +62,14 @@ impl UserData for Child { }) }); methods.add_async_method_mut("read_line", |lua, mut me, ()| async move { - match me.read_line().await { + match read_line(&mut me).await { (Some(b), event) => (lua.create_string(b)?, event).into_lua_multi(&lua), (None, event) => (Value::Nil, event).into_lua_multi(&lua), } }); - // TODO: deprecate this method methods.add_async_method_mut("read_line_with", |lua, mut me, options: Table| async move { let timeout = Duration::from_millis(options.raw_get("timeout")?); - let Ok(result) = tokio::time::timeout(timeout, me.read_line()).await else { + let Ok(result) = tokio::time::timeout(timeout, read_line(&mut me)).await else { return (Value::Nil, 3u8).into_lua_multi(&lua); }; match result { @@ -154,21 +98,38 @@ impl UserData for Child { }); methods.add_async_method_mut("wait", |lua, mut me, ()| async move { - match me.wait().await { + drop(me.stdin.take()); + match me.inner.wait().await { Ok(status) => (Status::new(status), Value::Nil).into_lua_multi(&lua), Err(e) => (Value::Nil, Error::Io(e)).into_lua_multi(&lua), } }); methods.add_async_function("wait_with_output", |lua, ud: AnyUserData| async move { - match ud.take::()?.wait_with_output().await { - Ok(output) => (Output::new(output), Value::Nil).into_lua_multi(&lua), - Err(e) => (Value::Nil, Error::Io(e)).into_lua_multi(&lua), + async fn read_to_end(r: &mut Option) -> io::Result> { + let mut vec = Vec::new(); + if let Some(r) = r.as_mut() { + r.read_to_end(&mut vec).await?; + } + Ok(vec) } - }); - methods.add_async_method_mut("try_wait", |lua, mut me, ()| async move { - match me.inner.try_wait() { - Ok(Some(status)) => (Status::new(status), Value::Nil).into_lua_multi(&lua), - Ok(None) => (Value::Nil, Value::Nil).into_lua_multi(&lua), + + let mut me = ud.take::()?; + let mut stdout_pipe = me.stdout.take(); + let mut stderr_pipe = me.stderr.take(); + + let stdout_fut = read_to_end(&mut stdout_pipe); + let stderr_fut = read_to_end(&mut stderr_pipe); + + drop(me.stdin.take()); + let result = try_join3(me.inner.wait(), stdout_fut, stderr_fut).await; + drop(stdout_pipe); + drop(stderr_pipe); + + match result { + Ok((status, stdout, stderr)) => { + (Output::new(std::process::Output { status, stdout, stderr }), Value::Nil) + .into_lua_multi(&lua) + } Err(e) => (Value::Nil, Error::Io(e)).into_lua_multi(&lua), } }); diff --git a/yazi-plugin/src/process/command.rs b/yazi-plugin/src/process/command.rs index 49f99f56..1f5c55ec 100644 --- a/yazi-plugin/src/process/command.rs +++ b/yazi-plugin/src/process/command.rs @@ -1,4 +1,4 @@ -use std::{io, process::Stdio}; +use std::process::Stdio; use mlua::{AnyUserData, ExternalError, IntoLuaMulti, Lua, MetaMethod, Table, UserData, Value}; use tokio::process::{ChildStderr, ChildStdin, ChildStdout}; @@ -8,8 +8,7 @@ use super::{Child, output::Output}; use crate::process::Status; pub struct Command { - inner: tokio::process::Command, - memory: Option, + inner: tokio::process::Command, } const NULL: u8 = 0; @@ -22,7 +21,7 @@ impl Command { let mut inner = tokio::process::Command::new(program); inner.kill_on_drop(true).stdin(Stdio::null()).stdout(Stdio::null()).stderr(Stdio::null()); - Ok(Self { inner, memory: None }) + Ok(Self { inner }) })?; let command = lua.create_table_from([ @@ -36,74 +35,6 @@ impl Command { lua.globals().raw_set("Command", command) } - - #[cfg(unix)] - fn spawn(&mut self) -> io::Result { - if let Some(max) = self.memory { - unsafe { - self.inner.pre_exec(move || { - let rlp = libc::rlimit { rlim_cur: max as _, rlim_max: max as _ }; - libc::setrlimit(libc::RLIMIT_AS, &rlp); - Ok(()) - }); - } - } - self.inner.spawn().map(Child::new) - } - - #[cfg(windows)] - fn spawn(&mut self) -> io::Result { - use std::os::windows::io::RawHandle; - - use windows_sys::Win32::System::JobObjects::{AssignProcessToJobObject, CreateJobObjectW, JOB_OBJECT_LIMIT_PROCESS_MEMORY, JOBOBJECT_EXTENDED_LIMIT_INFORMATION, JobObjectExtendedLimitInformation, SetInformationJobObject}; - - fn assign_job(max: usize, handle: RawHandle) -> io::Result { - unsafe { - let job = CreateJobObjectW(std::ptr::null_mut(), std::ptr::null()); - if job.is_null() { - return Err(io::Error::last_os_error()); - } - - let mut info: JOBOBJECT_EXTENDED_LIMIT_INFORMATION = std::mem::zeroed(); - info.BasicLimitInformation.LimitFlags = JOB_OBJECT_LIMIT_PROCESS_MEMORY; - info.ProcessMemoryLimit = max; - - let result = SetInformationJobObject( - job, - JobObjectExtendedLimitInformation, - &mut info as *mut _ as *mut _, - std::mem::size_of_val(&info) as u32, - ); - - if result == 0 { - Err(io::Error::last_os_error()) - } else if AssignProcessToJobObject(job, handle) == 0 { - Err(io::Error::last_os_error()) - } else { - Ok(job) - } - } - } - - let child = self.inner.spawn()?; - if let (Some(max), Some(handle)) = (self.memory, child.raw_handle()) { - if let Ok(job) = assign_job(max, handle) { - return Ok(Child::new(child, Some(job))); - } - } - - Ok(Child::new(child, None)) - } - - async fn output(&mut self) -> io::Result { - self.inner.stdin(Stdio::piped()); - self.inner.stdout(Stdio::piped()); - self.spawn()?.wait_with_output().await - } - - async fn status(&mut self) -> io::Result { - self.spawn()?.status().await - } } impl UserData for Command { @@ -171,22 +102,18 @@ impl UserData for Command { ud.borrow_mut::()?.inner.stderr(make_stdio(stdio)?); Ok(ud) }); - methods.add_function_mut("memory", |_, (ud, max): (AnyUserData, usize)| { - ud.borrow_mut::()?.memory = Some(max); - Ok(ud) - }); - methods.add_method_mut("spawn", |lua, me, ()| match me.spawn() { - Ok(child) => (child, Value::Nil).into_lua_multi(lua), + methods.add_method_mut("spawn", |lua, me, ()| match me.inner.spawn() { + Ok(child) => (Child::new(child), Value::Nil).into_lua_multi(lua), Err(e) => (Value::Nil, Error::Io(e)).into_lua_multi(lua), }); methods.add_async_method_mut("output", |lua, mut me, ()| async move { - match me.output().await { + match me.inner.output().await { Ok(output) => (Output::new(output), Value::Nil).into_lua_multi(&lua), Err(e) => (Value::Nil, Error::Io(e)).into_lua_multi(&lua), } }); methods.add_async_method_mut("status", |lua, mut me, ()| async move { - match me.status().await { + match me.inner.status().await { Ok(status) => (Status::new(status), Value::Nil).into_lua_multi(&lua), Err(e) => (Value::Nil, Error::Io(e)).into_lua_multi(&lua), } diff --git a/yazi-plugin/src/utils/mod.rs b/yazi-plugin/src/utils/mod.rs index c1f3a585..93a9273a 100644 --- a/yazi-plugin/src/utils/mod.rs +++ b/yazi-plugin/src/utils/mod.rs @@ -1,5 +1,5 @@ #![allow(clippy::module_inception)] yazi_macro::mod_flat!( - app cache call image json layer log preview process spot sync target text time user utils + app cache call image json layer log preview spot svg sync target text time user utils ); diff --git a/yazi-plugin/src/utils/process.rs b/yazi-plugin/src/utils/process.rs deleted file mode 100644 index 9d8d1008..00000000 --- a/yazi-plugin/src/utils/process.rs +++ /dev/null @@ -1,29 +0,0 @@ -use mlua::{Function, Lua}; - -use super::Utils; - -impl Utils { - #[cfg(target_os = "macos")] - pub(super) fn proc_info(lua: &Lua) -> mlua::Result { - lua.create_function(|lua, pid: usize| { - let info = unsafe { - let mut info: libc::proc_taskinfo = std::mem::zeroed(); - libc::proc_pidinfo( - pid as _, - libc::PROC_PIDTASKINFO, - 0, - &mut info as *mut _ as *mut _, - std::mem::size_of_val(&info) as _, - ); - info - }; - - lua.create_table_from([("mem_resident", info.pti_resident_size)]) - }) - } - - #[cfg(not(target_os = "macos"))] - pub(super) fn proc_info(lua: &Lua) -> mlua::Result { - lua.create_function(|lua, ()| lua.create_table()) - } -} diff --git a/yazi-plugin/src/utils/svg.rs b/yazi-plugin/src/utils/svg.rs new file mode 100644 index 00000000..fc69e972 --- /dev/null +++ b/yazi-plugin/src/utils/svg.rs @@ -0,0 +1,76 @@ +use std::sync::Arc; + +use mlua::{Function, IntoLua, Lua, Value}; +use resvg::usvg::fontdb::Database; +use yazi_adapter::GLOBAL_OPTIONS; +use yazi_binding::UrlRef; + +use super::Utils; +use crate::bindings::SvgInfo; + +impl Utils { + pub(super) fn svg_info(lua: &Lua) -> mlua::Result { + lua.create_async_function(|lua, url: UrlRef| async move { + if let Ok(info) = yazi_adapter::SvgInfo::new(&url).await { + SvgInfo::from(info).into_lua(&lua) + } else { + Value::Nil.into_lua(&lua) + } + }) + } + + pub(super) fn set_svg_font_family(lua: &Lua) -> mlua::Result { + lua.create_function(|_, (family, font_name): (mlua::String, mlua::String)| { + let font = font_name.to_str()?; + + Self::with_mut_fontdb(|db| { + match family.to_str()?.as_ref() { + "serif" => db.set_serif_family(&*font), + "sans-serif" => db.set_sans_serif_family(&*font), + "cursive" => db.set_cursive_family(&*font), + "fantasy" => db.set_fantasy_family(&*font), + "monospace" => db.set_monospace_family(&*font), + _ => return Err(mlua::Error::external("Invalid font family")), + } + Ok(()) + }) + }) + } + + pub(super) fn load_system_fonts(lua: &Lua) -> mlua::Result { + lua.create_function(|_, ()| { + Self::with_mut_fontdb(|db| { + db.load_system_fonts(); + Ok(()) + }) + }) + } + + pub(super) fn load_font_file(lua: &Lua) -> mlua::Result { + lua.create_function(|_, path: mlua::String| { + Self::with_mut_fontdb(|db| { + db.load_font_file(&*path.to_str()?) + .map_err(|e| mlua::Error::external(format!("Failed to load font file: {e}"))) + }) + }) + } + + pub(super) fn load_fonts_dir(lua: &Lua) -> mlua::Result { + lua.create_function(|_, dir: mlua::String| { + Self::with_mut_fontdb(|db| { + db.load_fonts_dir(&*dir.to_str()?); + Ok(()) + }) + }) + } + + fn with_mut_fontdb(f: F) -> mlua::Result<()> + where + F: FnOnce(&mut Database) -> mlua::Result<()>, + { + let mut options = GLOBAL_OPTIONS + .write() + .map_err(|e| mlua::Error::external(format!("RwLock poisoned: {}", e)))?; + f(Arc::make_mut(&mut options.fontdb)) + } +} diff --git a/yazi-plugin/src/utils/utils.rs b/yazi-plugin/src/utils/utils.rs index 41100a70..09993f1b 100644 --- a/yazi-plugin/src/utils/utils.rs +++ b/yazi-plugin/src/utils/utils.rs @@ -46,13 +46,17 @@ pub fn compose(lua: &Lua, isolate: bool) -> mlua::Result { b"preview_code" => Utils::preview_code(lua)?, b"preview_widgets" => Utils::preview_widgets(lua)?, - // Process - b"proc_info" => Utils::proc_info(lua)?, - // Spot b"spot_table" => Utils::spot_table(lua)?, b"spot_widgets" => Utils::spot_widgets(lua)?, + // Svg + b"load_font_file" => Utils::load_font_file(lua)?, + b"load_fonts_dir" => Utils::load_fonts_dir(lua)?, + b"load_system_fonts" => Utils::load_system_fonts(lua)?, + b"svg_info" => Utils::svg_info(lua)?, + b"set_svg_font_family" => Utils::set_svg_font_family(lua)?, + // Sync b"sync" => Utils::sync(lua, isolate)?, b"chan" => Utils::chan(lua)?,