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
return true
end
-- stylua: ignore function M:spot(job)
local cmd = require("magick").with_env():args { local rows = self:spot_base(job)
"-size", string.format("%dx%d^", rt.preview.max_width, rt.preview.max_height), rows[#rows + 1] = ui.Row {}
string.format("rsvg:%s", job.file.url), "-strip",
"-quality", rt.preview.image_quality,
string.format("JPG:%s", cache),
}
local status, err = cmd:status() ya.spot_table(
if status then job,
return status.success ui.Table(ya.list_merge(rows, require("file"):spot_base(job)))
else :area(ui.Pos { "center", w = 60, h = 20 })
return true, Err("Failed to start `magick`, error: %s", err) :row(job.skip)
end :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
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 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)?,