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++
diff --git a/lua/image/processors/magick_cli.lua b/lua/image/processors/magick_cli.lua index 640a0de..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" @@ -28,13 +29,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 +55,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 +115,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 +142,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 @@ -228,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) diff --git a/lua/image/utils/dimensions.lua b/lua/image/utils/dimensions.lua index b011c46..2425fff 100644 --- a/lua/image/utils/dimensions.lua +++ b/lua/image/utils/dimensions.lua @@ -273,6 +273,45 @@ 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 + + -- 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 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*[\"']([^\"']+)[\"']")) + 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 +335,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