From 2150f99fe866b1966b937e6880fd67d44d9e0c5e Mon Sep 17 00:00:00 2001 From: Arthur Cahu Date: Sun, 28 Jun 2026 12:23:28 +0200 Subject: [PATCH 1/5] fix: blocking errors inside uv loop errors inside uv escape pcall doesn't fix the underlying issue yet, but prevents the error from blocking user input for a second or so --- lua/image/processors/magick_cli.lua | 28 +++++++++++++++++++++------- 1 file changed, 21 insertions(+), 7 deletions(-) diff --git a/lua/image/processors/magick_cli.lua b/lua/image/processors/magick_cli.lua index 640a0de..36836ee 100644 --- a/lua/image/processors/magick_cli.lua +++ b/lua/image/processors/magick_cli.lua @@ -28,13 +28,19 @@ function MagickCliProcessor.get_format(path) local output = "" local error_output = "" + local failed = nil + vim.loop.spawn(has_magick and "magick" or "identify", { args = has_magick and { "identify", "-format", "%m", path } or { "-format", "%m", path }, stdio = { nil, stdout, stderr }, hide = true, }, function(code) - if code ~= 0 then error(error_output ~= "" and error_output or "Failed to get format") end - result = output:lower():gsub("%s+$", "") + -- can't error() here, it's a libuv callback and escapes the caller's pcall (#370) + if code ~= 0 then + failed = error_output ~= "" and error_output or "Failed to get format" + else + result = output:lower():gsub("%s+$", "") + end end) vim.loop.read_start(stdout, function(err, data) @@ -48,8 +54,9 @@ function MagickCliProcessor.get_format(path) end) local success = vim.wait(5000, function() - return result ~= nil + return result ~= nil or failed ~= nil end, 10) + if failed then error(failed) end if not success then error("identify format detection timed out") end return result end @@ -107,14 +114,20 @@ function MagickCliProcessor.get_dimensions(path) -- GIF if actual_format == "gif" then path = path .. "[0]" end + local failed = nil + vim.loop.spawn(has_magick and "magick" or "identify", { args = has_magick and { "identify", "-format", "%wx%h", path } or { "-format", "%wx%h", path }, stdio = { nil, stdout, stderr }, hide = true, }, function(code) - if code ~= 0 then error(error_output ~= "" and error_output or "Failed to get dimensions") end - local width, height = output:match("(%d+)x(%d+)") - result = { width = tonumber(width), height = tonumber(height) } + -- can't error() here, it's a libuv callback and escapes the caller's pcall (#370) + if code ~= 0 then + failed = error_output ~= "" and error_output or "Failed to get dimensions" + else + local width, height = output:match("(%d+)x(%d+)") + result = { width = tonumber(width), height = tonumber(height) } + end end) vim.loop.read_start(stdout, function(err, data) @@ -128,9 +141,10 @@ function MagickCliProcessor.get_dimensions(path) end) local success = vim.wait(5000, function() - return result ~= nil + return result ~= nil or failed ~= nil end, 10) + if failed then error(failed) end if not success then error("identify dimensions timed out") end return result From fb8accf5b582f9f0f4129c50619ef4925fdf8c35 Mon Sep 17 00:00:00 2001 From: Arthur Cahu Date: Sun, 28 Jun 2026 12:33:16 +0200 Subject: [PATCH 2/5] fix(svg): parse dimensions from lua --- lua/image/utils/dimensions.lua | 39 ++++++++++++++++++++++++++++++++-- 1 file changed, 37 insertions(+), 2 deletions(-) diff --git a/lua/image/utils/dimensions.lua b/lua/image/utils/dimensions.lua index b011c46..82546c9 100644 --- a/lua/image/utils/dimensions.lua +++ b/lua/image/utils/dimensions.lua @@ -273,6 +273,41 @@ handlers.heic = function(file) return read_isobmff_dimensions(file) end +handlers.svg = function(file) + local content = file:read(4096) + if not content then return nil end + + -- isolate the opening tag + local tag = content:match("]*>") or content + + -- ponytail: treats any unit as px; fine for raster-sized SVGs (badges), off for pt/em/% which we skip below + local function parse_len(s) + if not s or s:find("%%") then return nil end + local n = s:match("^%s*([%d%.]+)") + return n and tonumber(n) + end + + local w = parse_len(tag:match("width%s*=%s*[\"']([^\"']+)[\"']")) + local h = parse_len(tag:match("height%s*=%s*[\"']([^\"']+)[\"']")) + + if not (w and h) then + local vb = tag:match("viewBox%s*=%s*[\"']([^\"']+)[\"']") + if vb then + local nums = {} + for n in vb:gmatch("[%-%d%.]+") do + nums[#nums + 1] = tonumber(n) + end + if #nums == 4 then + w = w or nums[3] + h = h or nums[4] + end + end + end + + if not (w and h) then return nil end + return { width = math.floor(w + 0.5), height = math.floor(h + 0.5) } +end + handlers.xpm = function(file) local content = file:read(1024) -- Read enough to get past the header if not content then return nil end @@ -296,8 +331,8 @@ M.get_dimensions = function(path) local format = magic.detect_format(path) if not format then return nil end - -- Skip SVG/XML/PDF as they require more complex parsing - if format == "svg" or format == "xml" or format == "pdf" then return nil end + -- Skip XML/PDF as they require more complex parsing (SVG is handled below) + if format == "xml" or format == "pdf" then return nil end local file = io.open(path, "rb") if not file then return nil end From 2d60a4d23bcf5b2995b6bbac2814d1b62ac1142a Mon Sep 17 00:00:00 2001 From: Arthur Cahu Date: Sun, 28 Jun 2026 12:34:06 +0200 Subject: [PATCH 3/5] fix(svg): render with rsvg-convert --- lua/image/processors/magick_cli.lua | 26 ++++++++++++++++++++++++-- 1 file changed, 24 insertions(+), 2 deletions(-) diff --git a/lua/image/processors/magick_cli.lua b/lua/image/processors/magick_cli.lua index 36836ee..8bb2a5e 100644 --- a/lua/image/processors/magick_cli.lua +++ b/lua/image/processors/magick_cli.lua @@ -3,6 +3,7 @@ local utils = require("image/utils") local has_magick = vim.fn.executable("magick") == 1 local has_convert = vim.fn.executable("convert") == 1 local has_identify = vim.fn.executable("identify") == 1 +local has_rsvg = vim.fn.executable("rsvg-convert") == 1 -- magick v6 + v7 local convert_cmd = has_magick and "magick" or "convert" @@ -242,20 +243,41 @@ local build_transform_args = function(path, request, output_path) return args end +-- rsvg-convert renders SVGs ImageMagick can't (embedded data: URIs, #370). +-- NOTE: ignores request.crop; inline SVG badges don't trigger renderer crop. magick-crop the PNG if that changes. +local build_rsvg_args = function(path, request, output_path) + local args = {} + if request.target_width and request.target_height then + args[#args + 1] = "-w" + args[#args + 1] = tostring(request.target_width) + args[#args + 1] = "-h" + args[#args + 1] = tostring(request.target_height) + end + args[#args + 1] = "-o" + args[#args + 1] = output_path + args[#args + 1] = path + return args +end + function MagickCliProcessor.transform(path, request, output_path, callback) guard() local stderr = vim.loop.new_pipe() local error_output = "" local handle = nil + local use_rsvg = has_rsvg and (request.source_format or ""):lower() == "svg" + local cmd = use_rsvg and "rsvg-convert" or convert_cmd + local args = use_rsvg and build_rsvg_args(path, request, output_path) + or build_transform_args(path, request, output_path) + local close_stderr = function() if not stderr or stderr:is_closing() then return end pcall(vim.loop.read_stop, stderr) stderr:close() end - handle = vim.loop.spawn(convert_cmd, { - args = build_transform_args(path, request, output_path), + handle = vim.loop.spawn(cmd, { + args = args, stdio = { nil, nil, stderr }, hide = true, }, function(code) From 161ca29646c1bc0adbd9ec74c8a9a6fb843951ee Mon Sep 17 00:00:00 2001 From: Arthur Cahu Date: Sun, 28 Jun 2026 12:36:36 +0200 Subject: [PATCH 4/5] fix(svg): support CSS dimensions --- lua/image/utils/dimensions.lua | 12 ++++++++---- 1 file changed, 8 insertions(+), 4 deletions(-) diff --git a/lua/image/utils/dimensions.lua b/lua/image/utils/dimensions.lua index 82546c9..2425fff 100644 --- a/lua/image/utils/dimensions.lua +++ b/lua/image/utils/dimensions.lua @@ -280,11 +280,15 @@ handlers.svg = function(file) -- isolate the opening tag local tag = content:match("]*>") or content - -- ponytail: treats any unit as px; fine for raster-sized SVGs (badges), off for pt/em/% which we skip below + -- CSS absolute units in px (96dpi). em/ex/% are relative -> nil, fall back to viewBox. + local units = { px = 1, pt = 96 / 72, pc = 16, ["in"] = 96, cm = 96 / 2.54, mm = 96 / 25.4, Q = 96 / 101.6 } local function parse_len(s) - if not s or s:find("%%") then return nil end - local n = s:match("^%s*([%d%.]+)") - return n and tonumber(n) + if not s then return nil end + local n, unit = s:match("^%s*([%d%.]+)%s*(%a*)%s*$") + if not n then return nil end + local scale = unit == "" and 1 or units[unit] + if not scale then return nil end + return tonumber(n) * scale end local w = parse_len(tag:match("width%s*=%s*[\"']([^\"']+)[\"']")) From 7c6d92d6f429941590a96c3abc53b1dc5a369695 Mon Sep 17 00:00:00 2001 From: Arthur Cahu Date: Sun, 28 Jun 2026 12:46:31 +0200 Subject: [PATCH 5/5] docs: librsvg dependency --- README.md | 1 + 1 file changed, 1 insertion(+) diff --git a/README.md b/README.md index ac75a1a..76279bd 100644 --- a/README.md +++ b/README.md @@ -176,6 +176,7 @@ This plugin will always have first class support for Tmux, to make it work make #### Other - [cURL](https://github.com/curl/curl) for remote image support +- [librsvg](https://gitlab.gnome.org/GNOME/librsvg) for embedded SVG support #### Extra: Installing Überzug++