使用 Sublime Text(以下简称 ST)有很长时间了,一直对其没有 SVG(Scalable Vector Graphics:可缩放矢量图形)文件预览插件而耿耿于怀。因为 SVG 是一种和图像分辨率无关的矢量图形格式,易于编辑修改,在前端项目中被大量应用。ST 不能直接预览打开的 SVG 文件,开发过程中我需要经常切换到浏览器中预览 SVG 图像,一次两次还好,如果有大量的 SVG 文件,那么每次预览都要打开浏览器,实在是繁琐。在 Google 中搜索,发现这样一条 Issue,开发者并没有意图让 ST 原生支持 SVG 的预览,那么我就自力更生,来动手写一个预览插件。

准备工作

首先 ST 的插件开发的基础知识是必需的,我以前写过一篇文章,通读一遍就有大概的了解。然后梳理一下开发思路,可以得知要开发的这个插件实际上就是一条管道,把解析端(SVG 解析软件)与展示端(ST)连接起来,每一个 SVG 文件从这个开发模型通过,就会实现我想要的效果。

               +--------+  PNG   +--------------+
 SVG file  --> | Parser | -----> | Sublime Text | -->  Preview
               +--------+        +--------------+

ST 本身没有解析渲染 SVG 的功能,所以要找一个 SVG 解析器。Goolge 并试用了一圈,发现还是大名鼎鼎的 Inkscape 好使,安装之后 Inkscape 可以通过 CLI 工具 inkscape 调用,文档在这里。展示端就是 ST 本身,ST 3 提供了一个简洁的 HTML/CSS 渲染引擎 minihtml,可以在 ST 视图面板中解析并显示 HTML 内容。遗憾的是这个引擎并不能识别 <svg> 标签,我的办法是先用 Inkscape 将 SVG 文件转码为 PNG 格式,再用 <img> 标签引用,最后在界面渲染出图像。

实际开发

梳理了思路后,进入实际的开发,将重点的代码逻辑说明一下,实际的插件代码在 GitHub 上。

核心代码

只有一句,将转码得到的 PNG 图像在视图面板中渲染出来,可调用 View Classshow_popup 方法。

view.show_popup('<img src="file://{}">'.format(tmp_png_path))

命令行调用

重点功能是调用 Inkscape 将 SVG 转码为 PNG,查阅文档知调用 inkscape --export-type=png my_file.svg 即可做到。但是经过我的测试,发现有些情况下,直接调用此命令并不能生成正确的 PNG 图像(有些 SVG 中的图像信息丢失了),所以要有一个额外的命令 inkscape --query-all 查询当前 SVG 文件中的全部图像信息,此命令可返回 <svg> 标签中所有嵌套元素的信息,例如:

MySvg,0,0,600,600
MyGroup,100,100,300,300
MyTriangle,120,120,100,80
MySquare,150,150,50,50

得到元素信息后,在转码命令中加入最外层的 svg id 就能正确渲染出 PNG 图像。命令更新为 inkscape --export-type=png --export-id=svg_id my_file.svg。运行一次插件,这两个命令(query 和 export)都要调用,所以实际代码中抽象了一个公共方法 run_cmd

def run_cmd(cmd, msg):
    """
    Run custom command and deal with std errors
    """
    stdout = os.popen(cmd)

    result = stdout.buffer.read().decode(encoding='utf8')
    stderr = stdout.close()
    return result if not stderr else active_console(msg, cmd)

在用 os.popen 运行相关命令后,读取 Buffer 即可得到终端的输出,注意读取时用 UTF-8 格式解码,因为在 SVG 的 XML 描述文件中可能有非 ASCII 码,比如说中文。如果命令调用失败,close 方法会输出 1,此时让 ST 的控制台面板弹出,提示用户插件运行失败。

缓存文件

两次调用 Inkscape 命令很耗时间,所以应当将生成的 PNG 图像缓存起来。当用户多次预览同一 SVG 文件时,只有第一次才生成 PNG 图像,后面每次预览就返回第一次生成的图像。当用户预览新的 SVG 文件或者编辑了当前的 SVG 文件时,重新生成缓存。

def check_cached_file(name, basename, origin_name):
    """
    Check SVG file in tmp folder and current folder
    """
    tmp_svg_path = os.path.join(TMP_DIR, basename)
    if not os.path.exists(tmp_svg_path):
        return False

    # It means original SVG file was modified
    if not filecmp.cmp(tmp_svg_path, name):
        return False

    # Find special png name like : {filename}_svg{id}.png
    # 'svg{id}' is generated by command 'inkscape --query-all'
    name_format = '{}_svg'.format(origin_name)

    # If file is cached, just returns it's png file
    cached_file_name = next(
        (x for x in os.listdir(TMP_DIR) if x.startswith(name_format)), None)

    return cached_file_name

后记

因为 太懒 沉迷于 JS/TS 语言,三天不练手生,再次捡起 Python 感觉很陌生,一路上有些磕磕绊绊的,不过,总归是完成了。现在 Visual Studio Code 的强势崛起,让越来越多的人抛弃了 ST,不过我还是一如既往,深深地喜欢着她。当今社会,很多人在抱怨,却不能主动去解决问题,须知牢骚太盛防肠断,何不自己成为一个栽树的人呢?