From c59aa5e4c918983e60b2bd6b71a0f11d98e82f2f Mon Sep 17 00:00:00 2001 From: Alisher Galiev Date: Tue, 8 Apr 2025 21:13:01 +0500 Subject: [PATCH] Use resvg as library --- Cargo.lock | 295 +++++++++++++++++++++++++++++ Cargo.toml | 1 + yazi-adapter/Cargo.toml | 1 + yazi-adapter/src/image.rs | 167 ++++++++++++---- yazi-adapter/src/info.rs | 79 +++++++- yazi-plugin/Cargo.toml | 1 + yazi-plugin/preset/plugins/svg.lua | 72 ++++--- yazi-plugin/src/bindings/image.rs | 115 ++++++++++- yazi-plugin/src/utils/mod.rs | 2 +- yazi-plugin/src/utils/svg.rs | 76 ++++++++ yazi-plugin/src/utils/utils.rs | 7 + 11 files changed, 741 insertions(+), 75 deletions(-) create mode 100644 yazi-plugin/src/utils/svg.rs diff --git a/Cargo.lock b/Cargo.lock index 2bb429b8..d60429f7 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" @@ -496,6 +502,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" @@ -618,6 +633,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" @@ -817,6 +838,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" @@ -829,6 +856,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" @@ -1077,6 +1127,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" @@ -1214,6 +1270,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" @@ -1242,6 +1308,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" @@ -1364,6 +1436,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" @@ -1704,6 +1785,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" @@ -2026,11 +2113,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" @@ -2072,6 +2185,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" @@ -2228,6 +2359,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" @@ -2237,6 +2383,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" @@ -2265,6 +2420,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" @@ -2293,6 +2457,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.100" @@ -2458,6 +2632,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" @@ -2636,6 +2851,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" @@ -2657,6 +2881,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" @@ -2669,6 +2911,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" @@ -2686,6 +2940,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" @@ -2704,6 +2964,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" @@ -3138,6 +3425,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" @@ -3149,6 +3442,7 @@ dependencies = [ "crossterm", "image", "ratatui", + "resvg", "scopeguard", "tokio", "tracing", @@ -3391,6 +3685,7 @@ dependencies = [ "parking_lot", "paste", "ratatui", + "resvg", "serde_json", "syntect", "tokio", diff --git a/Cargo.toml b/Cargo.toml index 3115d4af..979b5022 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -35,6 +35,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 90095dff..f9f1e8b7 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 efa4a0b6..77b1c7af 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 1ff767dc..da5aeab4 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 } diff --git a/yazi-plugin/preset/plugins/svg.lua b/yazi-plugin/preset/plugins/svg.lua index 20fefc8c..0747d547 100644 --- a/yazi-plugin/preset/plugins/svg.lua +++ b/yazi-plugin/preset/plugins/svg.lua @@ -1,45 +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 = require("magick").with_env():args { - "-size", string.format("%dx%d^", rt.preview.max_width, rt.preview.max_height), - string.format("rsvg:%s", job.file.url), "-strip", - "-quality", rt.preview.image_quality, - string.format("JPG:%s", cache), - } +function M:spot(job) + local rows = self:spot_base(job) + rows[#rows + 1] = ui.Row {} - local status, err = cmd:status() - if status then - return status.success - else - return true, Err("Failed to start `magick`, error: %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/utils/mod.rs b/yazi-plugin/src/utils/mod.rs index 5a320167..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 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/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 839e3e46..f21e91f9 100644 --- a/yazi-plugin/src/utils/utils.rs +++ b/yazi-plugin/src/utils/utils.rs @@ -49,6 +49,13 @@ pub fn compose(lua: &Lua, isolate: bool) -> mlua::Result { 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)?,