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

View file

@ -46,10 +46,12 @@ unicode-width = { workspace = true }
yazi-prebuilt = "0.1.0"
[target."cfg(unix)".dependencies]
libc = { workspace = true }
uzers = { workspace = true }
[target."cfg(windows)".dependencies]
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]
crossterm = { workspace = true, features = [ "use-dev-tty", "libc" ] }

View file

@ -25,18 +25,44 @@ 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),
local cmd = Command("resvg"):args {
"-w", rt.preview.max_width, "-h", rt.preview.max_height,
"--image-rendering", "optimizeSpeed",
tostring(job.file.url), tostring(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
return status.success
else
return true, Err("Failed to start `magick`, error: %s", err)
return true, Err("Error while running `resvg`: %s", err)
end
end

View file

@ -58,7 +58,7 @@ function M:preload(job)
"-skip_frame", "nokey", "-ss", ss,
"-an", "-sn", "-dn",
"-i", tostring(job.file.url),
"-vframes", 1,
"-map", "0:v", "-vframes", 1,
"-q:v", qv,
"-vf", string.format("scale=-1:'min(%d,ih)':flags=fast_bilinear", rt.preview.max_height),
"-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 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 yazi_binding::Error;
@ -9,39 +9,94 @@ use super::Status;
use crate::process::Output;
pub struct Child {
inner: tokio::process::Child,
stdin: Option<BufWriter<ChildStdin>>,
stdout: Option<BufReader<ChildStdout>>,
stderr: Option<BufReader<ChildStderr>>,
inner: tokio::process::Child,
stdin: Option<BufWriter<ChildStdin>>,
stdout: Option<BufReader<ChildStdout>>,
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 {
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 stdout = inner.stdout.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 {
fn add_methods<M: mlua::UserDataMethods<Self>>(methods: &mut M) {
#[inline]
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),
}
}
fn add_methods<M: UserDataMethods<Self>>(methods: &mut M) {
methods.add_method("id", |_, me, ()| Ok(me.inner.id()));
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>> {
@ -62,14 +117,15 @@ impl UserData for Child {
})
});
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),
(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 {
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);
};
match result {
@ -98,38 +154,21 @@ impl UserData for Child {
});
methods.add_async_method_mut("wait", |lua, mut me, ()| async move {
drop(me.stdin.take());
match me.inner.wait().await {
match me.wait().await {
Ok(status) => (Status::new(status), Value::Nil).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 {
async fn read_to_end(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)
match ud.take::<Self>()?.wait_with_output().await {
Ok(output) => (Output::new(output), Value::Nil).into_lua_multi(&lua),
Err(e) => (Value::Nil, Error::Io(e)).into_lua_multi(&lua),
}
let mut me = ud.take::<Self>()?;
let mut stdout_pipe = me.stdout.take();
let mut stderr_pipe = me.stderr.take();
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)
}
});
methods.add_async_method_mut("try_wait", |lua, mut me, ()| async move {
match me.inner.try_wait() {
Ok(Some(status)) => (Status::new(status), Value::Nil).into_lua_multi(&lua),
Ok(None) => (Value::Nil, Value::Nil).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 tokio::process::{ChildStderr, ChildStdin, ChildStdout};
@ -8,7 +8,8 @@ use super::{Child, output::Output};
use crate::process::Status;
pub struct Command {
inner: tokio::process::Command,
inner: tokio::process::Command,
memory: Option<usize>,
}
const NULL: u8 = 0;
@ -21,7 +22,7 @@ impl Command {
let mut inner = tokio::process::Command::new(program);
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([
@ -35,6 +36,74 @@ impl 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 {
@ -102,18 +171,22 @@ impl UserData for Command {
ud.borrow_mut::<Self>()?.inner.stderr(make_stdio(stdio)?);
Ok(ud)
});
methods.add_method_mut("spawn", |lua, me, ()| match me.inner.spawn() {
Ok(child) => (Child::new(child), Value::Nil).into_lua_multi(lua),
methods.add_function_mut("memory", |_, (ud, max): (AnyUserData, usize)| {
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),
});
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),
Err(e) => (Value::Nil, Error::Io(e)).into_lua_multi(&lua),
}
});
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),
Err(e) => (Value::Nil, Error::Io(e)).into_lua_multi(&lua),
}

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 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_widgets" => Utils::preview_widgets(lua)?,
// Process
b"proc_info" => Utils::proc_info(lua)?,
// Spot
b"spot_table" => Utils::spot_table(lua)?,
b"spot_widgets" => Utils::spot_widgets(lua)?,