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", "syn",
] ]
[[package]]
name = "arrayref"
version = "0.3.9"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "76a2e8124351fda1ef8aaaa3bbd7ebbcb486bbcd4225aca0aa0d84bb2db8fecb"
[[package]] [[package]]
name = "arrayvec" name = "arrayvec"
version = "0.7.6" version = "0.7.6"
@ -496,6 +502,15 @@ version = "0.8.7"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "773648b94d0e5d620f64f280777445740e61fe701025087ec8b57f45c791888b" 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]] [[package]]
name = "crc32fast" name = "crc32fast"
version = "1.4.2" version = "1.4.2"
@ -618,6 +633,12 @@ dependencies = [
"syn", "syn",
] ]
[[package]]
name = "data-url"
version = "0.3.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5c297a1c74b71ae29df00c3e22dd9534821d60eb9af5a0192823fa2acea70c2a"
[[package]] [[package]]
name = "deranged" name = "deranged"
version = "0.4.0" version = "0.4.0"
@ -817,6 +838,12 @@ dependencies = [
"miniz_oxide", "miniz_oxide",
] ]
[[package]]
name = "float-cmp"
version = "0.9.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "98de4bbd547a563b716d8dfa9aad1cb19bfab00f4fa09a6a4ed21dbcf44ce9c4"
[[package]] [[package]]
name = "fnv" name = "fnv"
version = "1.0.7" version = "1.0.7"
@ -829,6 +856,29 @@ version = "0.1.5"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d9c4f5dac5e15c24eb999c26181a6ca40b39fe946cbe4c263c7209467bc83af2" 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]] [[package]]
name = "fsevent-sys" name = "fsevent-sys"
version = "4.1.0" version = "4.1.0"
@ -1077,6 +1127,12 @@ dependencies = [
"quick-error", "quick-error",
] ]
[[package]]
name = "imagesize"
version = "0.13.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "edcd27d72f2f071c64249075f42e205ff93c9a4c5f6c6da53e79ed9f9832c285"
[[package]] [[package]]
name = "imgref" name = "imgref"
version = "1.11.0" version = "1.11.0"
@ -1214,6 +1270,16 @@ dependencies = [
"libc", "libc",
] ]
[[package]]
name = "kurbo"
version = "0.11.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "89234b2cc610a7dd927ebde6b41dd1a5d4214cffaef4cf1fb2195d592f92518f"
dependencies = [
"arrayvec",
"smallvec",
]
[[package]] [[package]]
name = "lazy_static" name = "lazy_static"
version = "1.5.0" version = "1.5.0"
@ -1242,6 +1308,12 @@ dependencies = [
"cc", "cc",
] ]
[[package]]
name = "libm"
version = "0.2.11"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8355be11b20d696c8f18f6cc018c4e372165b1fa8126cef092399c9951984ffa"
[[package]] [[package]]
name = "libredox" name = "libredox"
version = "0.1.3" version = "0.1.3"
@ -1364,6 +1436,15 @@ version = "2.7.4"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "78ca9ab1a0babb1e7d5695e3530886289c18cf2f87ec19a575a0abdce112e3a3" checksum = "78ca9ab1a0babb1e7d5695e3530886289c18cf2f87ec19a575a0abdce112e3a3"
[[package]]
name = "memmap2"
version = "0.9.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "fd3f7eed9d3848f8b98834af67102b720745c4ec028fcd0aa0239277e7de374f"
dependencies = [
"libc",
]
[[package]] [[package]]
name = "minimal-lexical" name = "minimal-lexical"
version = "0.2.1" version = "0.2.1"
@ -1704,6 +1785,12 @@ version = "2.3.1"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e3148f5046208a5d56bcfc03053e3ca6334e51da8dfb19b6cdc8b306fae3283e" checksum = "e3148f5046208a5d56bcfc03053e3ca6334e51da8dfb19b6cdc8b306fae3283e"
[[package]]
name = "pico-args"
version = "0.5.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5be167a7af36ee22fe3115051bc51f6e6c7054c9348e28deb4f49bd6f705a315"
[[package]] [[package]]
name = "pin-project-lite" name = "pin-project-lite"
version = "0.2.16" version = "0.2.16"
@ -2026,11 +2113,37 @@ version = "0.8.5"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2b15c43186be67a4fd63bee50d0303afffcef381492ebe2c5d87f324e1b8815c" 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]] [[package]]
name = "rgb" name = "rgb"
version = "0.8.50" version = "0.8.50"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "57397d16646700483b67d2dd6511d79318f9d057fdbd21a4066aeac8b41d310a" checksum = "57397d16646700483b67d2dd6511d79318f9d057fdbd21a4066aeac8b41d310a"
dependencies = [
"bytemuck",
]
[[package]]
name = "roxmltree"
version = "0.20.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6c20b6793b5c2fa6553b250154b78d6d0db37e72700ae35fad9387a46f487c97"
[[package]] [[package]]
name = "rustc-demangle" name = "rustc-demangle"
@ -2072,6 +2185,24 @@ version = "1.0.20"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "eded382c5f5f786b989652c49544c4877d9f015cc22e145a5ea8ea66c2921cd2" 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]] [[package]]
name = "ryu" name = "ryu"
version = "1.0.20" version = "1.0.20"
@ -2228,6 +2359,21 @@ version = "0.1.5"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e3a9fe34e3e7a50316060351f37187a3f546bce95496156754b601a5fa71b76e" 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]] [[package]]
name = "slab" name = "slab"
version = "0.4.9" version = "0.4.9"
@ -2237,6 +2383,15 @@ dependencies = [
"autocfg", "autocfg",
] ]
[[package]]
name = "slotmap"
version = "1.0.7"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "dbff4acf519f630b3a3ddcfaea6c06b42174d9a44bc70c620e9ed1649d58b82a"
dependencies = [
"version_check",
]
[[package]] [[package]]
name = "smallvec" name = "smallvec"
version = "1.15.0" version = "1.15.0"
@ -2265,6 +2420,15 @@ version = "1.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a2eb9349b6444b326872e140eb1cf5e7c522154d69e7a0ffb0fb81c06b37543f" 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]] [[package]]
name = "strsim" name = "strsim"
version = "0.11.1" version = "0.11.1"
@ -2293,6 +2457,16 @@ dependencies = [
"syn", "syn",
] ]
[[package]]
name = "svgtypes"
version = "0.15.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "68c7541fff44b35860c1a7a47a7cadf3e4a304c457b58f9870d9706ece028afc"
dependencies = [
"kurbo",
"siphasher",
]
[[package]] [[package]]
name = "syn" name = "syn"
version = "2.0.100" version = "2.0.100"
@ -2458,6 +2632,47 @@ dependencies = [
"time-core", "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]] [[package]]
name = "tokio" name = "tokio"
version = "1.44.2" version = "1.44.2"
@ -2636,6 +2851,15 @@ dependencies = [
"windows", "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]] [[package]]
name = "twox-hash" name = "twox-hash"
version = "2.1.0" version = "2.1.0"
@ -2657,6 +2881,24 @@ version = "1.18.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1dccffe3ce07af9386bfd29e80c0ab1a8205a2fc34e4bcd40364df902cfa8f3f" 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]] [[package]]
name = "unicode-ident" name = "unicode-ident"
version = "1.0.18" version = "1.0.18"
@ -2669,6 +2911,18 @@ version = "0.1.5"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3b09c83c3c29d37506a3e260c08c03743a6bb66a9cd432c6934ab501a190571f" 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]] [[package]]
name = "unicode-segmentation" name = "unicode-segmentation"
version = "1.12.0" version = "1.12.0"
@ -2686,6 +2940,12 @@ dependencies = [
"unicode-width 0.1.14", "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]] [[package]]
name = "unicode-width" name = "unicode-width"
version = "0.1.14" version = "0.1.14"
@ -2704,6 +2964,33 @@ version = "2.1.3"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "daf8dba3b7eb870caf1ddeed7bc9d2a049f3cfdfae7cb521b087cc33ae4c49da" 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]] [[package]]
name = "utf8parse" name = "utf8parse"
version = "0.2.2" version = "0.2.2"
@ -3138,6 +3425,12 @@ dependencies = [
"bitflags 2.9.0", "bitflags 2.9.0",
] ]
[[package]]
name = "xmlwriter"
version = "0.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ec7a2a501ed189703dba8b08142f057e887dfc4b2cc4db2d343ac6376ba3e0b9"
[[package]] [[package]]
name = "yazi-adapter" name = "yazi-adapter"
version = "25.4.8" version = "25.4.8"
@ -3149,6 +3442,7 @@ dependencies = [
"crossterm", "crossterm",
"image", "image",
"ratatui", "ratatui",
"resvg",
"scopeguard", "scopeguard",
"tokio", "tokio",
"tracing", "tracing",
@ -3391,6 +3685,7 @@ dependencies = [
"parking_lot", "parking_lot",
"paste", "paste",
"ratatui", "ratatui",
"resvg",
"serde_json", "serde_json",
"syntect", "syntect",
"tokio", "tokio",

View file

@ -35,6 +35,7 @@ parking_lot = "0.12.3"
paste = "1.0.15" paste = "1.0.15"
ratatui = { version = "0.29.0", features = [ "unstable-rendered-line-info" ] } ratatui = { version = "0.29.0", features = [ "unstable-rendered-line-info" ] }
regex = "1.11.1" regex = "1.11.1"
resvg = { version = "0.45.0" }
scopeguard = "1.2.0" scopeguard = "1.2.0"
serde = { version = "1.0.219", features = [ "derive" ] } serde = { version = "1.0.219", features = [ "derive" ] }
serde_json = "1.0.140" serde_json = "1.0.140"

View file

@ -21,6 +21,7 @@ color_quant = "1.1.0"
crossterm = { workspace = true } 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" ] } 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 } ratatui = { workspace = true }
resvg = { workspace = true }
scopeguard = { workspace = true } scopeguard = { workspace = true }
tokio = { workspace = true } tokio = { workspace = true }
tracing = { 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 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 ratatui::layout::Rect;
use resvg::{tiny_skia::{Pixmap, Transform}, usvg::{Options, Tree}};
use yazi_config::YAZI; use yazi_config::YAZI;
use crate::Dimension; use crate::Dimension;
pub static GLOBAL_OPTIONS: LazyLock<RwLock<Options<'static>>> =
LazyLock::new(|| RwLock::new(Options::default()));
pub struct Image; pub struct Image;
impl Image { impl Image {
pub async fn precache(path: &Path, cache: PathBuf) -> Result<()> { pub async fn precache(path: &Path, cache: PathBuf) -> Result<()> {
let (mut img, orientation, icc) = Self::decode_from(path).await?; let (img, icc) =
let (w, h) = Self::flip_size(orientation, (YAZI.preview.max_width, YAZI.preview.max_height)); Self::decode_to_fit_from(path, YAZI.preview.max_width, YAZI.preview.max_height).await?;
let buf = tokio::task::spawn_blocking(move || { 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(); let mut buf = Vec::new();
if img.color().has_alpha() { if img.color().has_alpha() {
let rgba = img.into_rgba8(); let rgba = img.into_rgba8();
@ -42,25 +39,8 @@ impl Image {
} }
pub(super) async fn downscale(path: &Path, rect: Rect) -> Result<DynamicImage> { pub(super) async fn downscale(path: &Path, rect: Rect) -> Result<DynamicImage> {
let (mut img, orientation, _) = Self::decode_from(path).await?; let (width, height) = Self::max_pixel(rect);
let (w, h) = Self::flip_size(orientation, Self::max_pixel(rect)); let (img, _) = Self::decode_to_fit_from(path, width, height).await?;
// 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?;
Ok(img) 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(); let mut limits = Limits::no_limits();
if YAZI.tasks.image_alloc > 0 { if YAZI.tasks.image_alloc > 0 {
limits.max_alloc = Some(YAZI.tasks.image_alloc as u64); limits.max_alloc = Some(YAZI.tasks.image_alloc as u64);
@ -107,20 +134,76 @@ impl Image {
if YAZI.tasks.image_bound[1] > 0 { if YAZI.tasks.image_bound[1] > 0 {
limits.max_image_height = Some(YAZI.tasks.image_bound[1] as u32); limits.max_image_height = Some(YAZI.tasks.image_bound[1] as u32);
} }
limits
}
let path = path.to_owned(); #[inline]
tokio::task::spawn_blocking(move || { fn try_decode_svg(
let mut reader = ImageReader::open(path)?; path: &Path,
reader.limits(limits); 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()?; for rgba in container.chunks_exact_mut(4) {
let orientation = decoder.orientation().unwrap_or(Orientation::NoTransforms); let alpha = rgba[3];
let icc = decoder.icc_profile().unwrap_or_default(); 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)) let img = ImageBuffer::from_raw(width, height, container)
}) .ok_or_else(|| anyhow::anyhow!("Failed to create image buffer"))?;
.await
.map_err(|e| ImageError::IoError(e.into()))? 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) { 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 image::{ImageDecoder, ImageError};
use resvg::usvg::{Font, Group, ImageKind, Node, Tree};
use super::image::GLOBAL_OPTIONS;
pub type ImageFormat = image::ImageFormat; pub type ImageFormat = image::ImageFormat;
pub type ImageColor = image::ColorType; pub type ImageColor = image::ColorType;
@ -42,3 +45,77 @@ impl ImageInfo {
.map_err(|e| ImageError::IoError(e.into()))? .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 } parking_lot = { workspace = true }
paste = { workspace = true } paste = { workspace = true }
ratatui = { workspace = true } ratatui = { workspace = true }
resvg = { workspace = true }
serde_json = { workspace = true } serde_json = { workspace = true }
syntect = { workspace = true } syntect = { workspace = true }
tokio = { workspace = true } tokio = { workspace = true }

View file

@ -1,45 +1,57 @@
local M = {} local M = {}
function M:peek(job) function M:peek(job)
local start, cache = os.clock(), ya.file_cache(job) local start = os.clock()
if not cache then
return
end
local ok, err = self:preload(job)
if not ok or err then
return
end
ya.sleep(math.max(0, rt.preview.image_delay / 1000 + 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, {}) ya.preview_widgets(job, {})
end end
function M:seek() end function M:seek() end
function M:preload(job) function M:preload(job) end
local cache = ya.file_cache(job)
if not cache or fs.cha(cache) then function M:spot(job)
return true local rows = self:spot_base(job)
rows[#rows + 1] = ui.Row {}
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 end
-- stylua: ignore function M:spot_base(job)
local cmd = require("magick").with_env():args { local info = ya.svg_info(job.file.url)
"-size", string.format("%dx%d^", rt.preview.max_width, rt.preview.max_height), if not info then
string.format("rsvg:%s", job.file.url), "-strip", return {}
"-quality", rt.preview.image_quality, end
string.format("JPG:%s", cache),
local rows = {
ui.Row({ "SVG Image" }):style(ui.Style():fg("green")),
ui.Row { " Size:", string.format("%.0fx%.0f", info.w, info.h) },
} }
local status, err = cmd:status() if #info.layers > 0 then
if status then rows[#rows + 1] = ui.Row { " Layers:", table.concat(info.layers, ", ") }
return status.success end
else
return true, Err("Failed to start `magick`, error: %s", err) 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
end end
function M:spot(job) require("file"):spot(job) end return rows
end
return M return M

View file

@ -1,6 +1,7 @@
use std::ops::Deref; use std::{fmt::Write, ops::Deref};
use mlua::{MetaMethod, UserData}; use mlua::{MetaMethod, UserData};
use resvg::usvg;
pub struct ImageInfo(yazi_adapter::ImageInfo); 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)] #![allow(clippy::module_inception)]
yazi_macro::mod_flat!( 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_table" => Utils::spot_table(lua)?,
b"spot_widgets" => Utils::spot_widgets(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 // Sync
b"sync" => Utils::sync(lua, isolate)?, b"sync" => Utils::sync(lua, isolate)?,
b"chan" => Utils::chan(lua)?, b"chan" => Utils::chan(lua)?,