feat!: switch to resvg as the SVG renderer (#2581)

This commit is contained in:
三咲雅 · Misaki Masa 2025-04-07 23:01:58 +08:00 committed by sxyazi
parent be00881403
commit 0082d1181a
No known key found for this signature in database
9 changed files with 243 additions and 69 deletions

2
Cargo.lock generated
View file

@ -3386,6 +3386,7 @@ dependencies = [
"crossterm", "crossterm",
"futures", "futures",
"globset", "globset",
"libc",
"md-5", "md-5",
"mlua", "mlua",
"parking_lot", "parking_lot",
@ -3400,6 +3401,7 @@ dependencies = [
"twox-hash", "twox-hash",
"unicode-width 0.2.0", "unicode-width 0.2.0",
"uzers", "uzers",
"windows-sys 0.59.0",
"yazi-adapter", "yazi-adapter",
"yazi-binding", "yazi-binding",
"yazi-boot", "yazi-boot",

View file

@ -46,10 +46,12 @@ unicode-width = { workspace = true }
yazi-prebuilt = "0.1.0" yazi-prebuilt = "0.1.0"
[target."cfg(unix)".dependencies] [target."cfg(unix)".dependencies]
libc = { workspace = true }
uzers = { workspace = true } uzers = { workspace = true }
[target."cfg(windows)".dependencies] [target."cfg(windows)".dependencies]
clipboard-win = "5.4.0" clipboard-win = "5.4.0"
windows-sys = { version = "0.59.0", features = [ "Win32_Security", "Win32_System_JobObjects", "Win32_System_Threading" ] }
[target.'cfg(target_os = "macos")'.dependencies] [target.'cfg(target_os = "macos")'.dependencies]
crossterm = { workspace = true, features = [ "use-dev-tty", "libc" ] } crossterm = { workspace = true, features = [ "use-dev-tty", "libc" ] }

View file

@ -25,18 +25,44 @@ function M:preload(job)
end end
-- stylua: ignore -- stylua: ignore
local cmd = require("magick").with_env():args { local cmd = Command("resvg"):args {
"-size", string.format("%dx%d^", rt.preview.max_width, rt.preview.max_height), "-w", rt.preview.max_width, "-h", rt.preview.max_height,
string.format("rsvg:%s", job.file.url), "-strip", "--image-rendering", "optimizeSpeed",
"-quality", rt.preview.image_quality, tostring(job.file.url), tostring(cache)
string.format("JPG:%s", cache),
} }
if rt.tasks.image_alloc > 0 then
cmd = cmd:memory(rt.tasks.image_alloc)
end
local child, err = cmd:spawn()
if not child then
return true, Err("Failed to start `resvg`, error: %s", err)
end
local status, err
while true do
ya.sleep(0.2)
status, err = child:try_wait()
if status or err then
break
end
local id, mem = child:id(), nil
if id then
mem = ya.proc_info(id).mem_resident
end
if mem and mem > rt.tasks.image_alloc then
child:start_kill()
err = Err("memory limit exceeded, pid: %s, memory: %s", id, mem)
break
end
end
local status, err = cmd:status()
if status then if status then
return status.success return status.success
else else
return true, Err("Failed to start `magick`, error: %s", err) return true, Err("Error while running `resvg`: %s", err)
end end
end end

View file

@ -58,7 +58,7 @@ function M:preload(job)
"-skip_frame", "nokey", "-ss", ss, "-skip_frame", "nokey", "-ss", ss,
"-an", "-sn", "-dn", "-an", "-sn", "-dn",
"-i", tostring(job.file.url), "-i", tostring(job.file.url),
"-vframes", 1, "-map", "0:v", "-vframes", 1,
"-q:v", qv, "-q:v", qv,
"-vf", string.format("scale=-1:'min(%d,ih)':flags=fast_bilinear", rt.preview.max_height), "-vf", string.format("scale=-1:'min(%d,ih)':flags=fast_bilinear", rt.preview.max_height),
"-f", "image2", "-f", "image2",

View file

@ -1,7 +1,7 @@
use std::{ops::DerefMut, time::Duration}; use std::{ops::DerefMut, process::ExitStatus, time::Duration};
use futures::future::try_join3; use futures::future::try_join3;
use mlua::{AnyUserData, ExternalError, IntoLua, IntoLuaMulti, Table, UserData, Value}; use mlua::{AnyUserData, ExternalError, IntoLua, IntoLuaMulti, Table, UserData, UserDataFields, UserDataMethods, Value};
use tokio::{io::{self, AsyncBufReadExt, AsyncReadExt, AsyncWriteExt, BufReader, BufWriter}, process::{ChildStderr, ChildStdin, ChildStdout}, select}; use tokio::{io::{self, AsyncBufReadExt, AsyncReadExt, AsyncWriteExt, BufReader, BufWriter}, process::{ChildStderr, ChildStdin, ChildStdout}, select};
use yazi_binding::Error; use yazi_binding::Error;
@ -9,39 +9,94 @@ use super::Status;
use crate::process::Output; use crate::process::Output;
pub struct Child { pub struct Child {
inner: tokio::process::Child, inner: tokio::process::Child,
stdin: Option<BufWriter<ChildStdin>>, stdin: Option<BufWriter<ChildStdin>>,
stdout: Option<BufReader<ChildStdout>>, stdout: Option<BufReader<ChildStdout>>,
stderr: Option<BufReader<ChildStderr>>, stderr: Option<BufReader<ChildStderr>>,
#[cfg(windows)]
job_handle: Option<std::os::windows::io::RawHandle>,
}
#[cfg(windows)]
impl Drop for Child {
fn drop(&mut self) {
if let Some(h) = self.job_handle.take() {
unsafe { windows_sys::Win32::Foundation::CloseHandle(h) };
}
}
} }
impl Child { impl Child {
pub fn new(mut inner: tokio::process::Child) -> Self { pub fn new(
mut inner: tokio::process::Child,
#[cfg(windows)] job_handle: Option<std::os::windows::io::RawHandle>,
) -> Self {
let stdin = inner.stdin.take().map(BufWriter::new); let stdin = inner.stdin.take().map(BufWriter::new);
let stdout = inner.stdout.take().map(BufReader::new); let stdout = inner.stdout.take().map(BufReader::new);
let stderr = inner.stderr.take().map(BufReader::new); let stderr = inner.stderr.take().map(BufReader::new);
Self { inner, stdin, stdout, stderr } Self {
inner,
stdin,
stdout,
stderr,
#[cfg(windows)]
job_handle,
}
}
pub(super) async fn wait(&mut self) -> io::Result<ExitStatus> {
drop(self.stdin.take());
self.inner.wait().await
}
pub(super) async fn status(&mut self) -> io::Result<ExitStatus> {
drop(self.stdin.take());
drop(self.stdout.take());
drop(self.stderr.take());
self.inner.wait().await
}
async fn read_line(&mut self) -> (Option<Vec<u8>>, u8) {
async fn read(r: Option<impl AsyncBufReadExt + Unpin>) -> Option<Vec<u8>> {
let mut buf = Vec::new();
match r?.read_until(b'\n', &mut buf).await {
Ok(0) | Err(_) => None,
Ok(_) => Some(buf),
}
}
select! {
r @ Some(_) = read(self.stdout.as_mut()) => (r, 0u8),
r @ Some(_) = read(self.stderr.as_mut()) => (r, 1u8),
else => (None, 2u8),
}
}
pub(super) async fn wait_with_output(mut self) -> io::Result<std::process::Output> {
async fn read(r: &mut Option<impl AsyncBufReadExt + Unpin>) -> io::Result<Vec<u8>> {
let mut vec = Vec::new();
if let Some(r) = r.as_mut() {
r.read_to_end(&mut vec).await?;
}
Ok(vec)
}
// Ensure stdin is closed so the child isn't stuck waiting on input while the
// parent is waiting for it to exit.
drop(self.stdin.take());
// Drop happens after `try_join` due to <https://github.com/tokio-rs/tokio/issues/4309>
let mut stdout = self.stdout.take();
let mut stderr = self.stderr.take();
let result = try_join3(self.inner.wait(), read(&mut stdout), read(&mut stderr)).await?;
Ok(std::process::Output { status: result.0, stdout: result.1, stderr: result.2 })
} }
} }
impl UserData for Child { impl UserData for Child {
fn add_methods<M: mlua::UserDataMethods<Self>>(methods: &mut M) { fn add_methods<M: UserDataMethods<Self>>(methods: &mut M) {
#[inline] methods.add_method("id", |_, me, ()| Ok(me.inner.id()));
async fn read_line(me: &mut Child) -> (Option<Vec<u8>>, u8) {
async fn read(r: Option<impl AsyncBufReadExt + Unpin>) -> Option<Vec<u8>> {
let mut buf = Vec::new();
match r?.read_until(b'\n', &mut buf).await {
Ok(0) | Err(_) => None,
Ok(_) => Some(buf),
}
}
select! {
r @ Some(_) = read(me.stdout.as_mut()) => (r, 0u8),
r @ Some(_) = read(me.stderr.as_mut()) => (r, 1u8),
else => (None, 2u8),
}
}
methods.add_async_method_mut("read", |_, mut me, len: usize| async move { methods.add_async_method_mut("read", |_, mut me, len: usize| async move {
async fn read(r: Option<impl AsyncBufReadExt + Unpin>, len: usize) -> Option<Vec<u8>> { async fn read(r: Option<impl AsyncBufReadExt + Unpin>, len: usize) -> Option<Vec<u8>> {
@ -62,14 +117,15 @@ impl UserData for Child {
}) })
}); });
methods.add_async_method_mut("read_line", |lua, mut me, ()| async move { methods.add_async_method_mut("read_line", |lua, mut me, ()| async move {
match read_line(&mut me).await { match me.read_line().await {
(Some(b), event) => (lua.create_string(b)?, event).into_lua_multi(&lua), (Some(b), event) => (lua.create_string(b)?, event).into_lua_multi(&lua),
(None, event) => (Value::Nil, event).into_lua_multi(&lua), (None, event) => (Value::Nil, event).into_lua_multi(&lua),
} }
}); });
// TODO: deprecate this method
methods.add_async_method_mut("read_line_with", |lua, mut me, options: Table| async move { methods.add_async_method_mut("read_line_with", |lua, mut me, options: Table| async move {
let timeout = Duration::from_millis(options.raw_get("timeout")?); let timeout = Duration::from_millis(options.raw_get("timeout")?);
let Ok(result) = tokio::time::timeout(timeout, read_line(&mut me)).await else { let Ok(result) = tokio::time::timeout(timeout, me.read_line()).await else {
return (Value::Nil, 3u8).into_lua_multi(&lua); return (Value::Nil, 3u8).into_lua_multi(&lua);
}; };
match result { match result {
@ -98,38 +154,21 @@ impl UserData for Child {
}); });
methods.add_async_method_mut("wait", |lua, mut me, ()| async move { methods.add_async_method_mut("wait", |lua, mut me, ()| async move {
drop(me.stdin.take()); match me.wait().await {
match me.inner.wait().await {
Ok(status) => (Status::new(status), Value::Nil).into_lua_multi(&lua), Ok(status) => (Status::new(status), Value::Nil).into_lua_multi(&lua),
Err(e) => (Value::Nil, Error::Io(e)).into_lua_multi(&lua), Err(e) => (Value::Nil, Error::Io(e)).into_lua_multi(&lua),
} }
}); });
methods.add_async_function("wait_with_output", |lua, ud: AnyUserData| async move { methods.add_async_function("wait_with_output", |lua, ud: AnyUserData| async move {
async fn read_to_end(r: &mut Option<impl AsyncBufReadExt + Unpin>) -> io::Result<Vec<u8>> { match ud.take::<Self>()?.wait_with_output().await {
let mut vec = Vec::new(); Ok(output) => (Output::new(output), Value::Nil).into_lua_multi(&lua),
if let Some(r) = r.as_mut() { Err(e) => (Value::Nil, Error::Io(e)).into_lua_multi(&lua),
r.read_to_end(&mut vec).await?;
}
Ok(vec)
} }
});
let mut me = ud.take::<Self>()?; methods.add_async_method_mut("try_wait", |lua, mut me, ()| async move {
let mut stdout_pipe = me.stdout.take(); match me.inner.try_wait() {
let mut stderr_pipe = me.stderr.take(); Ok(Some(status)) => (Status::new(status), Value::Nil).into_lua_multi(&lua),
Ok(None) => (Value::Nil, Value::Nil).into_lua_multi(&lua),
let stdout_fut = read_to_end(&mut stdout_pipe);
let stderr_fut = read_to_end(&mut stderr_pipe);
drop(me.stdin.take());
let result = try_join3(me.inner.wait(), stdout_fut, stderr_fut).await;
drop(stdout_pipe);
drop(stderr_pipe);
match result {
Ok((status, stdout, stderr)) => {
(Output::new(std::process::Output { status, stdout, stderr }), Value::Nil)
.into_lua_multi(&lua)
}
Err(e) => (Value::Nil, Error::Io(e)).into_lua_multi(&lua), Err(e) => (Value::Nil, Error::Io(e)).into_lua_multi(&lua),
} }
}); });

View file

@ -1,4 +1,4 @@
use std::process::Stdio; use std::{io, process::Stdio};
use mlua::{AnyUserData, ExternalError, IntoLuaMulti, Lua, MetaMethod, Table, UserData, Value}; use mlua::{AnyUserData, ExternalError, IntoLuaMulti, Lua, MetaMethod, Table, UserData, Value};
use tokio::process::{ChildStderr, ChildStdin, ChildStdout}; use tokio::process::{ChildStderr, ChildStdin, ChildStdout};
@ -8,7 +8,8 @@ use super::{Child, output::Output};
use crate::process::Status; use crate::process::Status;
pub struct Command { pub struct Command {
inner: tokio::process::Command, inner: tokio::process::Command,
memory: Option<usize>,
} }
const NULL: u8 = 0; const NULL: u8 = 0;
@ -21,7 +22,7 @@ impl Command {
let mut inner = tokio::process::Command::new(program); let mut inner = tokio::process::Command::new(program);
inner.kill_on_drop(true).stdin(Stdio::null()).stdout(Stdio::null()).stderr(Stdio::null()); inner.kill_on_drop(true).stdin(Stdio::null()).stdout(Stdio::null()).stderr(Stdio::null());
Ok(Self { inner }) Ok(Self { inner, memory: None })
})?; })?;
let command = lua.create_table_from([ let command = lua.create_table_from([
@ -35,6 +36,74 @@ impl Command {
lua.globals().raw_set("Command", command) lua.globals().raw_set("Command", command)
} }
#[cfg(unix)]
fn spawn(&mut self) -> io::Result<Child> {
if let Some(max) = self.memory {
unsafe {
self.inner.pre_exec(move || {
let rlp = libc::rlimit { rlim_cur: max as _, rlim_max: max as _ };
libc::setrlimit(libc::RLIMIT_AS, &rlp);
Ok(())
});
}
}
self.inner.spawn().map(Child::new)
}
#[cfg(windows)]
fn spawn(&mut self) -> io::Result<Child> {
use std::os::windows::io::RawHandle;
use windows_sys::Win32::System::JobObjects::{AssignProcessToJobObject, CreateJobObjectW, JOB_OBJECT_LIMIT_PROCESS_MEMORY, JOBOBJECT_EXTENDED_LIMIT_INFORMATION, JobObjectExtendedLimitInformation, SetInformationJobObject};
fn assign_job(max: usize, handle: RawHandle) -> io::Result<RawHandle> {
unsafe {
let job = CreateJobObjectW(std::ptr::null_mut(), std::ptr::null());
if job.is_null() {
return Err(io::Error::last_os_error());
}
let mut info: JOBOBJECT_EXTENDED_LIMIT_INFORMATION = std::mem::zeroed();
info.BasicLimitInformation.LimitFlags = JOB_OBJECT_LIMIT_PROCESS_MEMORY;
info.ProcessMemoryLimit = max;
let result = SetInformationJobObject(
job,
JobObjectExtendedLimitInformation,
&mut info as *mut _ as *mut _,
std::mem::size_of_val(&info) as u32,
);
if result == 0 {
Err(io::Error::last_os_error())
} else if AssignProcessToJobObject(job, handle) == 0 {
Err(io::Error::last_os_error())
} else {
Ok(job)
}
}
}
let child = self.inner.spawn()?;
if let (Some(max), Some(handle)) = (self.memory, child.raw_handle()) {
if let Ok(job) = assign_job(max, handle) {
return Ok(Child::new(child, Some(job)));
}
}
Ok(Child::new(child, None))
}
async fn output(&mut self) -> io::Result<std::process::Output> {
self.inner.stdin(Stdio::piped());
self.inner.stdout(Stdio::piped());
self.spawn()?.wait_with_output().await
}
async fn status(&mut self) -> io::Result<std::process::ExitStatus> {
self.spawn()?.status().await
}
} }
impl UserData for Command { impl UserData for Command {
@ -102,18 +171,22 @@ impl UserData for Command {
ud.borrow_mut::<Self>()?.inner.stderr(make_stdio(stdio)?); ud.borrow_mut::<Self>()?.inner.stderr(make_stdio(stdio)?);
Ok(ud) Ok(ud)
}); });
methods.add_method_mut("spawn", |lua, me, ()| match me.inner.spawn() { methods.add_function_mut("memory", |_, (ud, max): (AnyUserData, usize)| {
Ok(child) => (Child::new(child), Value::Nil).into_lua_multi(lua), ud.borrow_mut::<Self>()?.memory = Some(max);
Ok(ud)
});
methods.add_method_mut("spawn", |lua, me, ()| match me.spawn() {
Ok(child) => (child, Value::Nil).into_lua_multi(lua),
Err(e) => (Value::Nil, Error::Io(e)).into_lua_multi(lua), Err(e) => (Value::Nil, Error::Io(e)).into_lua_multi(lua),
}); });
methods.add_async_method_mut("output", |lua, mut me, ()| async move { methods.add_async_method_mut("output", |lua, mut me, ()| async move {
match me.inner.output().await { match me.output().await {
Ok(output) => (Output::new(output), Value::Nil).into_lua_multi(&lua), Ok(output) => (Output::new(output), Value::Nil).into_lua_multi(&lua),
Err(e) => (Value::Nil, Error::Io(e)).into_lua_multi(&lua), Err(e) => (Value::Nil, Error::Io(e)).into_lua_multi(&lua),
} }
}); });
methods.add_async_method_mut("status", |lua, mut me, ()| async move { methods.add_async_method_mut("status", |lua, mut me, ()| async move {
match me.inner.status().await { match me.status().await {
Ok(status) => (Status::new(status), Value::Nil).into_lua_multi(&lua), Ok(status) => (Status::new(status), Value::Nil).into_lua_multi(&lua),
Err(e) => (Value::Nil, Error::Io(e)).into_lua_multi(&lua), Err(e) => (Value::Nil, Error::Io(e)).into_lua_multi(&lua),
} }

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 process spot sync target text time user utils
); );

View file

@ -0,0 +1,29 @@
use mlua::{Function, Lua};
use super::Utils;
impl Utils {
#[cfg(target_os = "macos")]
pub(super) fn proc_info(lua: &Lua) -> mlua::Result<Function> {
lua.create_function(|lua, pid: usize| {
let info = unsafe {
let mut info: libc::proc_taskinfo = std::mem::zeroed();
libc::proc_pidinfo(
pid as _,
libc::PROC_PIDTASKINFO,
0,
&mut info as *mut _ as *mut _,
std::mem::size_of_val(&info) as _,
);
info
};
lua.create_table_from([("mem_resident", info.pti_resident_size)])
})
}
#[cfg(not(target_os = "macos"))]
pub(super) fn proc_info(lua: &Lua) -> mlua::Result<Function> {
lua.create_function(|lua, ()| lua.create_table())
}
}

View file

@ -45,6 +45,9 @@ pub fn compose(lua: &Lua, isolate: bool) -> mlua::Result<Value> {
b"preview_code" => Utils::preview_code(lua)?, b"preview_code" => Utils::preview_code(lua)?,
b"preview_widgets" => Utils::preview_widgets(lua)?, b"preview_widgets" => Utils::preview_widgets(lua)?,
// Process
b"proc_info" => Utils::proc_info(lua)?,
// Spot // Spot
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)?,