- Published on
在浏览器中解析和渲染 XMind 文件
- Authors
- Name
- 文森
- Github
- @leungwensen
前言:思维导图的在线渲染
思维导图软件可以帮助我们完成非常多的事情,包括不限于项目任务管理、知识管理、计划管理、思路脑爆和整理、结构架构梳理、快速演示等等。国产老牌思维导图软件 XMind 和 2017 年的新秀 MindMaster 基本上是这个领域普及程度市场占有率最高的软件,所以这算是难得的国产软件领先的领域。
但除了幕布、ProcessOn、钉钉脑图之类的新型思维导图服务以外,传统的思维导图软件是桌面软件,有非常多的存量文件,必须要先下载安装对应软件才能打开,这对于很多用户而言是有负担的。在线的、免安装打开思维导图软件是一个现实的、还在增长的需求。
本文以 XMind 文件为例,讲解如何在 Web 前端解析和渲染思维导图文件。
方案 1 文件格式解析 + 思维导图布局 + 2D 渲染
要在 Web 前端合理合法地打开一个 XMind 文件,首选的方案是自行解析其文件格式,并且自行布局渲染。具体来说每个步骤可以这么去拆解:
1.1 文件格式解析
XMind 的文件格式遵循 OpenDocument Format(ODF)规范,是一种基于 XML 并使用 ZIP 压缩的文件格式。
这个说法准确地说适用于 XMind 8 update3 之前的版本,在这次升级之后,.xmind 文件依然是一个 ZIP 压缩包,但内容由 content.xml 变成了 content.json,个人猜测有两个原因,第一个是 JSON 格式已经可以获得事实上的最好性能和最佳平台兼容性;第二个是从 2017 年开始,XMind 的团队转投 JavaScript 阵营,核心的思维导图渲染引擎(Snowbrush)完全由 JavaScript 开发
新版 JSON 格式无需多言,从压缩包里拿到 content.json 的文件内容之后,直接 JSON.parse 即可。难点在于老版本的、严格遵循 ODF 规范的 XMind 文档。按照 ODF 的定义,我们解压文件可以得到以下产出:
.
├── META-INF
│ └── manifest.xml
├── Thumbnails
│ └── thumbnail.png
├── attachments
│ ├── 1jgveohr7kcd0htgupa6cqp89q.png
│ └── 6u3kani0lrd1f828qe2l5k5prm.png
├── content.xml
├── meta.xml
└── styles.xml
这是非常规范的 ODF 格式,包含有附件、缩略图的定义,并且文件的内容、元信息和样式分别在不同的 XML 文件中定义。不过在实际操作中,脑图并不像 Excel、PPT 那样,需要去扩展强大的宏定义、脚本和排版系统等能力,慢慢地 XMind 发展产品功能的过程中,绝大部分和样式相关的定义都耦合到了 content.xml 文件中,meta.xml 更是名存实亡。最后新版本中只保留了 content.js 也就不足为奇了。需要解析出最核心的节点树信息,我们去解析 content.xml 就已经满足需求。
XML 的解析在 Node.js 端和浏览器端分别有成熟的方案。浏览器中就是成熟的DOMParser
和XMLSerializer
API,Node.js 里是使用三方包xmldom
实现的等同的 API 能力(完整支持 DOM level2 规范,并且支持部分 DOM level3 规范)。因此解析和生成 XMind 文件可以实现跨平台的统一接口。
这两个项目,xml-lite
实现了一套浏览器和 Node.js 端通用的 XML 文件格式处理库:
xml-lite 的能力基本上是以上 XML 格式解析、序列化的能力加上可以很方便地维护 XML DOM 内容的各种 API,譬如遍历、CRUD、格式化等等
而xmind-sdk-javascript
则是基于xml-lite
的 XML DOM 操作能力高效地维护 XMind 文件,并且抽象出 Workbook、Sheet、Topic、Relationship 等思维导图相关的基础实体,为 XMind 文件提供好用的 CRUD 接口。这个库在 XMind 公司更新了 .xmind 文件格式,并且正式接手这个 npm 包之前,一直就是 npm 包 xmind
背后持续更新发布的库,“民间的官方库”。
// loading the `xmind` package
var workbook = new xmind.Workbook(); // 也可以用 load 接口加载一个已有的文件
var primarySheet = workbook.getPrimarySheet();
var rootTopic = primarySheet.getRootTopic();
rootTopic.setTitle('test another title');
var secondTopic = rootTopic.addChild({
id: 'secondTopic',
title: 'second topic'
});
xmind.save(workbook, 'test.xmind');
1.2 思维导图布局
思维导图布局是非常典型的二维平面直角坐标系下树的布局。前面我们分享过,确保树结构在 x、y 两个维度同时紧凑的布局,自从 2014 的一篇论文问世后,其复杂度已经降低到 O(n) 线性时间复杂度,而思维导图的布局甚至不需要确保 y 维度的紧凑、x 维度的分层,因此我们可以轻而易举实现比 XMind 官方更好的布局性能。伪代码如下:
function xmindLayout(data, options) {
const {left, right} = separateRoot(data, options); // 分离左右子树
nonLayeredUntidyLayout(left); // 采用非分层、非紧凑型布局
nonLayeredUntidyLayout(right);
left.right2left();
const root = combineSubTrees(left, right);
// 思路:left 的根节点调用 translate 使得坐标和 right 的根节点一致
return root;
}
function separateRoot(data, options) { // 分离左右树
const left = new TreeNode(data, options, true);
const right = new TreeNode(data, options, true);
const root = new TreeNode(data, options);
for (let child in root.children) {
const side = options.getSide(child.data);
if (side === 'left') left.children.push(child);
if (side === 'right') right.children.push(child);
}
return {left, right};
}
function calcTotalHeight(node) { // 统计子树 y 轴占高
if (node.isLeaf()) return node.totalHeight = node.height;
let totalHeight = 0;
node.children.forEach(c => { totalHeight += calcTotalHeight(c); });
return node.totalHeight = Math.max(node.height, totalHeight);
}
function nonLayeredUntidyLayout(root) { // 非分层非紧凑树布局(水平向右)
// first walk: x
root.BFTraverse(node => {
node.x = node.parent.x + node.parent.width;
});
// y
// second walk
calcTotalHeight(root);
// third walk: 根据子树 y 轴占高(totalHeight)分配 y 坐标,逻辑和缩进树类似,不再赘述
separateSubTreeYCoord(root);
// forth walk: y 坐标取的是子节点节点本身的包围盒高度来计算,从而使得局部看起来更加的对称
adjustAncestor(root);
}
function adjustAncestor(node) {
if (node.isLeaf()) return;
node.children.forEach(adjustAncestor);
node.y = (node.children[0].y + node.children[node.children.length - 1].y) / 2
}
1.3 2D 渲染
完成上一步之后,就剩最后一个动作,就是在浏览器中把带包围盒信息(尺寸)、带坐标的节点绘制出来,并继续绘制连线。最后这一步反而是最简单的,使用 SVG 来完成就可以了。创建一个 SVG Doc 节点,插入两个图层(g),分别存储节点和边。从根节点(RootTopic)开始遍历 Workbook 当前 Sheet 的内容,把每个 Topic 绘制出来,并设置其坐标为布局后坐标,插入节点图层。然后再一次遍历,把节点和子节点之间连上连线,插入边图层。最后再一次遍历 Relationships 数组,把非树结构关联的节点之间的关系画出来,插入边图层。节点简单点用矩形 Rect 来绘制;边和关系都是 Path,锚点和关键点的信息参考 XMind 软件即可。
最后基本上就可以造出这样的轮子了:
补上交互,基本可以再造一个简易版本的 ZMind 出来。不过目前为止的工作,最大的槽点就是“我想要打开 XMind,最后你打开长得不像 XMind”。这是选择“合理合法”路径最大的问题。如果不满足于此,可以尝试以下邪门歪道方案 2 和方案 3。
方案 2 BS 架构 + AppleScript
这个方案是国内某在线文档产品采取的方案。事实上是违法的。原理异常简单,并且具备非常强的成本优势和通用性(违法成本较低)。
2.1 异常简单的作案工具
MacOS 有一套脚本语言 AppleScript,它可以操作 GUI 软件。遵从 MacOS 软件设计规范、有相对完善的应用 Menu 的软件都可以复用这个方法。使用 AppleScript,把文件放到指定路径(也可以运行时输入),就可以用脚本唤起 XMind.app,然后把 .xmind 文件导出为 pdf(或者 svg)了:
tell application "XMind"
-- 打开 XMind 文档
set doc to open "路径到你的 XMind 文档.xmind"
-- 导出为 PDF
export doc to PDF file format PDF file name "导出的 PDF 文件路径.pdf"
-- 关闭 XMind 文档
close doc saving no
end tell
2.2 异常简单的 BS 架构
一台 Mac Mini,一根网线,结合 AppleScript + Node.js 接口,你就拥有了非常多只有 GUI 软件界面操作才能完成的在线功能服务。以 XMind 文件转换导出为例,Node.js 接口只需要接受 upload 文件,把文件转存到某个特定文件夹,然后使用类似shelljs
之类的库,从命令行执行 AppleScript 的转换脚本即可获得高还原、高清版本的 pdf 或者 svg 文件,就可以直接在浏览器端展示了。
osascript /path/to/xmind2pdf.scpt
至于要不要把转换任务接入 MQ 之类的,要不要把转换后的文件存入 CDN,这些都是毫不起眼的细节,不值一提。用这个方法,全文档预览支持不在话下,成本比租用云服务器、购买 Office365 Web API 服务诸如此类,低了不止一个档次。就是不宜扩散不宜推广不宜高调。
方案 2 虽然很刑,但对于个人服务而言,实在太香了。有法外狂徒某在线文档产品在前面顶着,我们大可以各种姿势个人项目里尽情探索这个方案的可能性。
方案 3 Snowbrush 破解
最后这个方法更隐秘,但难度更高,风险相当大,并且不具备通用性。
XMind 从 2017 年起倾力打造的新一代 XMind 渲染引擎,库名称叫 Snowbrush。这个库相比原来 Native 桌面应用的版本有一些槽点,譬如性能有所下降,譬如 JavaScript 源码有一股 Java 社区的味道(有可能主导开发引擎的技术团队是 Java 出身,换技术栈没换人)等等,但不可否认,这是官方唯一未来持续支持的渲染引擎,XMind 文件长什么样是由这个引擎决定的,并且一直在成长发展中,交互视觉体验各方面也都领先于同行。
这个团队做云服务比较晚,原本想像幕布一样,把思维导图做成云存储的、跨端的,可惜 beta 阶段有一些问题和客诉,最终暂停(最近他们注册了 xmind.ai 站点开始做在线版 XMind 了,恭喜 XMind 团队)。而之前他们在线分享脑图模板的服务 xmind.net 有个无伤大雅的问题,把 Snowbrush 的源码通过 sourcemap 文件暴露了。拿到这份破解版的源码(或者就混淆后的文件),开发者就可以几乎 100% 还原 XMind.app 的体验。
方案 3 除技术研究以外,任何用途均不推荐。
相关链接
- openfiles.online 可以在线打开各种文件,支持 XMind 文件格式