Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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++
<details>
Expand Down
54 changes: 45 additions & 9 deletions lua/image/processors/magick_cli.lua
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand All @@ -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)
Expand All @@ -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
Expand Down Expand Up @@ -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)
Expand All @@ -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
Expand Down Expand Up @@ -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)
Expand Down
43 changes: 41 additions & 2 deletions lua/image/utils/dimensions.lua
Original file line number Diff line number Diff line change
Expand Up @@ -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 <svg ...> tag
local tag = content:match("<svg[^>]*>") 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
Expand All @@ -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
Expand Down