Use resvg as library

This commit is contained in:
Alisher Galiev 2025-04-08 21:13:01 +05:00
parent 00e54f7dbe
commit c59aa5e4c9
11 changed files with 741 additions and 75 deletions

295
Cargo.lock generated
View file

@ -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",

View file

@ -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"

View file

@ -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 }

View file

@ -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<RwLock<Options<'static>>> =
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<DynamicImage> {
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<Vec<u8>>)> {
async fn decode_to_fit_from(
path: &Path,
width: u32,
height: u32,
) -> Result<(DynamicImage, Option<Vec<u8>>)> {
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<Vec<u8>>)> {
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<Vec<u8>>)> {
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<Pixmap> {
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) {

View file

@ -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<Font>,
pub layers: Vec<String>,
}
impl SvgInfo {
pub async fn new(path: &Path) -> anyhow::Result<Self> {
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);
});
}
}

View file

@ -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 }

View file

@ -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

View file

@ -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<yazi_adapter::SvgInfo> for SvgInfo {
fn from(value: yazi_adapter::SvgInfo) -> Self { Self(value) }
}
impl UserData for SvgInfo {
fn add_fields<F: mlua::UserDataFields<Self>>(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<F: mlua::UserDataFields<Self>>(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<M: mlua::UserDataMethods<Self>>(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()
))
});
}
}

View file

@ -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
);

View file

@ -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<Function> {
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<Function> {
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<Function> {
lua.create_function(|_, ()| {
Self::with_mut_fontdb(|db| {
db.load_system_fonts();
Ok(())
})
})
}
pub(super) fn load_font_file(lua: &Lua) -> mlua::Result<Function> {
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<Function> {
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: 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))
}
}

View file

@ -49,6 +49,13 @@ pub fn compose(lua: &Lua, isolate: bool) -> mlua::Result<Value> {
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)?,