ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

跨平台编辑器公式兼容与手写识别:用统一AST打通Word OMML

跨平台编辑器公式兼容与手写识别:用统一AST打通Word OMML 很长时间里我都把跨平台编辑器怎么兼容Word公式和手写公式识别当成两条独立的技术路线来处理。直到有一次用户把论文里十几页的Word公式贴进我们的Web编辑器格式全部乱掉紧接着另一位同事又丢来一张手写公式草稿希望识别成可编辑公式后直接放进Word文档。两个需求碰到一起我才真正意识到它们其实是同一套公式内核的两条入口分开做只会让系统变得又脆又难维护。这篇文章就把我过去大半年折腾跨平台编辑器、Word公式兼容、手写公式识别三者协同的经验完整拆开讲透适合做文档类产品、在线编辑器、科研工具的同学参考也适合想弄明白公式在Word里到底以什么形态存在的人。1. 先搞清楚Word公式到底是个什么东西1.1 OMML2007年之后的公式心脏从Word 2007开始Word里的公式就不再是普通图片也不再是简单的文本拼接而是用一种名为OMMLOffice Math Markup Language的XML方言来存储。它在OOXMLOffice Open XML体系之下命名空间是m:。你会发现一个简单的分数m:oMath m:f m:num m:rm:tab/m:t/m:r /m:num m:den m:rm:tc/m:t/m:r /m:den /m:f /m:oMath在Word里显示成(ab)/c那样一个带分数线的数学对象。OMML的厉害之处在于它的结构是高度语义化的m:f表示分数m:sSup表示上标m:sSub表示下标m:sSubSup表示同时有上下标m:d表示带分隔符的括号结构。这种东西天生就是为可编辑的数学结构设计的而不是只保存看起来像什么样子。这个特性和普通文本的差异很大。普通文本在XML里就是一串字符样式靠run属性而公式是嵌套的树形结构。如果你做跨平台编辑器时依然用字符串样式的思维去处理公式那你注定会踩坑。我见过不少编辑器把公式转成Base64图片或SVG存起来Word原始公式导入后看起来没丢实际上用户双击图片根本没法编辑这显然不是兼容。1.2 老古董Equation 3.0、MathType和AxMath的OLE包袱Word的兼容性包袱不在于2007年的新公式而在于那些老公式对象。Equation 3.0是上世纪90年代的组件本质是一个OLE对象嵌入到Word文档后存的是一段二进制数据存放在docx包内的word/embeddings/目录里。MathType和国产的AxMath也一样它们通过OLE嵌入Word文档里留下的是一个w:object节点指向一个.bin文件。这意味着如果你只是按OMML解析公式遇到老文档或者第三方插件生成的公式多半会解析失败。Mac上打开Windows发的旧Word时老公式显示异常就是因为目标机器上没有对应的OLE服务器。跨平台编辑器要处理这类对象不能指望本地注册表里有MathType只能把二进制数据提取出来用专门的解析库去识别。有些库能应付但很多情况下只能降级为图片兜底。这个兜底策略很重要下面第5节我会再展开。1.3 跨平台对公式兼容的直接影响跨平台带来的第一个麻烦是字体和渲染差异。Windows上Word渲染公式用Cambria MathMac上的Pages和Word则可能回退到别的数学字体到了Linux LibreOffice又是一套字体度量。同一个OMML在不同平台的布局结果会有细微差别。第二个麻烦是编辑器的宿主环境。Web里渲染公式靠HTML/CSS桌面端靠原生控件移动端靠Canvas或WebView。手写公式识别又依赖画布上的墨迹采集坐标。你没法在每个平台各写一套公式引擎所以公式内核统一、渲染层分平台适配就成了唯一靠谱的路线。这也是我后面坚持用统一中间格式的根本原因。2. 三种公式语言之间搭一座共同中转站而不是单向桥2.1 OMML、MathML、LaTeX各自擅长的东西做公式兼容绕不开三种公式语言OMML是Word的原生格式MathML是W3C标准、浏览器可读LaTeX是学术圈通用、人类可写的文本格式。它们各有各的用途公式语言优势短板典型场景OMMLWord原生结构语义化完整公式编号和文档样式一体可读性差非Office生态几乎不用Word文档内的公式MathMLW3C标准语义稳定浏览器可直接渲染手写繁琐人类几乎不会直接编写网页展示、无障碍阅读LaTeX文本可读生态工具多转换库成熟字符串表达损失结构语义解析依赖上下文论文排版、学术交流那么问题是编辑器在处理公式时到底应该把OMML直接转成LaTeX还是转成MathML还是说三条路都保留2.2 字符串往返转换的无损假象一开始我的想法很简单Word转成LaTeX用户编辑再转回OMML导出Word。这条单向桥初期跑得通但没多久就暴露了问题。首先是花括号和组的问题。LaTeX里{ab}的空组在OMML里没有对应物OMML里一个分子有多个项时LaTeX写作\frac{ab}{c}但更复杂的嵌套结构比如“左侧有上下标、右侧有条件的方程”转换容易丢嵌套层级。更麻烦的是分隔符。Word公式里的括号可以自动拉伸OMML用m:d来标记LaTeX里可以是\left(但如果用户写的是普通(转换器可能把它们当成不同的结构。字符串层面往返一次还勉强能看往返三次之后结构就漂移了。比如带大括号的分段函数、矩阵的列对齐、多行公式的对齐点只要中间有一步做的是文本替换而不是结构映射总能漏掉一些信息。2.3 统一公式AST让所有输入法说同一种方言踩了这些坑之后我把架构改成了统一公式ASTAbstract Syntax Tree抽象语法树。不管是Word导入、手写识别、公式图片OCR还是用户直接在编辑器里敲LaTeX第一件事就是解析成同一套AST节点分数节点Frac上下标节点Sup、Sub、SupSub根式节点Sqrt、Root分隔符节点Delimiter记录左括号、右括号、是否拉伸矩阵节点Matrix记录行列数和对齐方式多行节点MultiLine记录每个公式行的对齐点有了AST这个共同中转站OMML、MathML、LaTeX都只是AST的表示形式。解析器负责把各格式翻译成AST渲染器从AST生成HTML/SVG导出器从AST生成OMML或LaTeX。这样手写识别出的LaTeX、Word里的OMML、网页里的MathML进入编辑器后都先归一化再按用户需要导出。整个系统的思路就从单向桥变成了巴别塔翻译中心兼容性一下清晰了很多。3. 手写公式识别从哪里接入才不会把架构搞乱3.1 手写识别的完整管线拆解手写公式识别很多人以为就是一个OCR模型的事实际拆开是四段管线第一段是墨迹采集。用户在画布上滑动时前端要记录的不是一张图片而是笔画的轨迹点序列每个点至少包含x、y和时间戳t。时间戳尤其重要因为笔画速度和停顿位置可以影响候选切分。第二段是预处理。原始轨迹有手抖和噪声要做去抖、降采样、坐标归一化。坐标归一化这一步很关键因为写字大小和位置会影响识别结果归一化能显著提高稳定性。第三段是符号切分与分类。神经网络判断每一段轨迹对应什么符号比如x、1、、∫同时还要判断哪些轨迹组成同一个符号哪些只是连续笔画。第四段是结构分析。识别出符号之后要判断符号之间的二维空间关系谁在谁的右上方、谁在谁的下标位置、分母是哪一串符号。这一步常常用基于上下文的序列模型或规则引擎来做。最终模型输出的结果最好是LaTeX字符串或MathML而不是一个扁平的符号列表。原因很简单下一站就是统一ASTLaTeX可以无损解析进AST而符号列表还需要重建结构。3.2 Web端部署识别模型的取舍既然要做跨平台手写识别引擎就不能只跑在Windows桌面端。我的方案是优先保证Web端可运行用的方式是ONNX Runtime Web把训练好的识别模型转成ONNX格式在浏览器里通过WebAssembly或WebGL推理。桌面端则可以直接复用同一个模型文件不需要额外维护。模型选型上体量和精度要平衡。一个几十MB的模型在Web端还能接受但如果是几百MB的大模型页面首屏体验会非常糟糕。我的实测经验是手写公式识别不像语音识别那样要求超大规模模型重点在结构分析一个中等规模的CNNTransformer组合模型配合好的后处理规则准确率就能比较高。真正需要花心思的是后处理里的候选排序比如1和l、0和O、乘号和字母x都要靠上下文语法做约束让存在歧义的识别结果进入候选列表而不是直接给一个死答案。3.3 识别结果进编辑器后的结构归一与纠错识别结果不直接进入Word而是先进AST这算是架构上的一个安全阀。比如手写时经常出现多行公式里号没有对齐AST归一化阶段可以按用户选择自动对齐再比如手写frac的分数线画得太短AST里没有这个概念只有分子分母的区域划分渲染时自然会拉长分数线。我还建议做一个低置信度标记机制。识别模型对某些符号没有把握时输出一个置信度分数。低于阈值时编辑器在对应符号下方画一个下划线提示用户可以直接点击候选词进行替换。这个功能在真实使用中特别重要因为手写体的个体差异是模型难以穷尽的。与其追求模型一次识别百分百准确不如让用户能在十秒内完成纠错。4. 把Word公式完整走通的关键链路4.1 从docx包中找到公式的三种情况docx本质上是一个ZIP文件。先用解压工具打开定位到word/document.xml然后遍历XML节点遇到m:oMath节点就是Word原生公式走OMML解析器遇到m:oMathPara这是独立的公式段落通常承载块级公式遇到w:object节点就要去检查word/embeddings/里有没有对应的.bin文件这可能是老公式或MathType/AxMath生成的OLE对象。写这个解析器的时候最怕的是XML命名空间写错。document.xml里混着w:和m:两套命名空间你按标签名搜oMath时必须带上命名空间判断。很多人在这里栽跟头直接用字符串查找m:oMath遇到格式换行或属性插入就匹配失败了。正确做法是解析成DOM树或流式XML再按命名空间和标签名双重匹配。4.2 渲染层的MathJax与KaTeX选择AST拿到手之后要渲染到网页上。市面上有两套主流方案MathJax和KaTeX。我的选择逻辑很简单——短文档、交互编辑场景用KaTeX长文档批量渲染优先KaTeX带复杂MathML需求时考虑MathJax。对比维度MathJax 3KaTeX渲染速度慢首屏有延迟快尤其适合批量公式支持的输入格式LaTeX、MathML、ASCIIMath以LaTeX为主MathML支持有限布局精度高对复杂结构支持充分足够好个别极端结构有差异输出方式HTMLCSS、SVGHTMLCSS体积较大更小编辑器场景里用户频繁改写公式渲染速度直接决定手感。KaTeX的缓存机制做得好同一个公式重复渲染时几乎不耗时。MathJax胜在兼容性如果你的文档包含复杂MathML特性或者需要屏幕阅读器无障碍支持MathJax更稳妥。4.3 AST到OMML导出Word的序列化要领导出回Word才是完整闭环的最后一步。AST到OMML的序列化有几个细节特别容易出错。矩阵结构。OMML里矩阵的定义不叫array而是m:m矩阵单元格是m:e行是m:mr。转换AST里的Matrix节点时每个单元格内容不管多复杂都要完整套上m:e标签并且要处理单元格内部可能是空的情况。分隔符拉伸。Word里括号能否自动拉伸取决于m:d节点的属性。AST里Delimiter节点要记录stretchy标志导出的OMML才能保持括号随内容变高的行为。还有一个让很多人忽略的问题Word里的行内公式和块级公式。行内公式直接嵌在文本段落的m:oMath里块级公式则用m:oMathPara包一层。AST里要给公式标记inline还是block导出时才能放进正确的位置否则排出来的公式全部挤在同一行。5. 我在实际项目里踩过的一批坑5.1 公式编号和Word域代码的蒸发问题Word里的公式编号看着像1-1这样的普通字符但它往往藏着域代码Field。Word的自动编号是基于SEQ域的在XML里表现为w:fldSimple或一段fldChar结构。你如果只解析公式而不解析域导到非Word环境再导回去编号很可能消失或变成静态文本。我的处理方式是解析阶段遇到域代码时把它转成AST里的FieldNode保留域类型和显示结果导出到Word时再重新生成对应的域结构。如果实在不想碰域代码至少要把显示结果转成普通文本保证用户看到的东西没丢只是失去自动更新能力。5.2 行内公式与文字的基线错位公式与文字不对齐这个坑我在处理Word导入的文档时遇到过很多次。原因是多方面的MathType生成的字体基线计算和Word原生字体不一致中文字体与Cambria Math默认行高差异很大行内公式里含有分式时整体高度会超出普通文字行高。我的解决方案分两层。解析时从OMML的m:oMath节点读取基线信息记录到AST节点属性里渲染时用CSS的vertical-align配合动态计算的行高做微调。实测下来设置vertical-align: middle只对简单公式有效遇到分式、上下标还是要计算公式的实际bounding box。所以更稳的做法是渲染后测量公式元素的高度再手动设置一个基于字号的偏移量。这个偏移量没有万能公式要在你选择的数学字体下做一批样本回归测试。5.3 被MathType/AxMath包过的公式识别不出来用户的Word文档里大量存在MathType公式甚至两边都装了AxMath和MathType插入公式时弹出的却是另一个工具的界面。这类文档的公式对象是OLE二进制解析OMML的代码根本没用。我试过两条路一是用开源的OLE解析库尝试解出MathType的专用二进制格式这条路能处理一部分简单的公式但遇到嵌套结构、字体包嵌入时经常解析失败二是渲染兜底——把OLE对象区域截成图片用公式OCR模型识别成LaTeX再进AST。后者准确率不一定100%但至少能保住文本内容而且配合低置信度标记让用户确认实际运营效果还不错。5.4 上万条公式在大文档里的换行与性能问题有用户导入过一个包含几万条行内公式和大量数据表格的Word文档页面直接卡到无法滚动。问题出在两个地方一次性渲染了所有公式以及重复解析公式字符串。我的修法是懒渲染公式进入视口才渲染用IntersectionObserver监听离开视口后保留结果但不重排渲染缓存同一个公式的LaTeX字符串作为key渲染结果放在Map里可能复用数十次KaTeX替代MathJax同样一批公式KaTeX的渲染耗时要低一个数量级。这几个改动组合起来长文档的滚动流畅度提升非常明显。6. 一个最小可行的落地顺序6.1 第一阶段先跑通Word进入-编辑-导出Word闭环不要一上来就同时搞手写识别、OCR、MathJax先把最核心的闭环打通解压docx、定位m:oMath、转AST、网页渲染、用户编辑、AST转OMML、打包回docx。这个闭环至少需要准备10个测试docx覆盖简单行内公式、分数根式、上下标、矩阵、多行公式对齐、公式编号、嵌套分隔符、MathType对象。每通过一轮手动比对导入导出前后的公式结构记录差异。6.2 第二阶段手写识别叠加进这个闭环手写识别模块可以分两步走先接入现有识别引擎的API或本地推理输出LaTeX然后走LaTeX解析器进统一AST。前期的重点不是提高识别准确率而是让手写→LaTeX→AST→OMML导出这一整条链路跑通。不要急着把识别模块和Word导入模块耦合在一起。我吃过这个亏一开始想让手写识别直接生成OMML绕过了AST结果两边结构对不上最后不得不推翻重来。现在回想让手写识别和Word解析都走AST是一种典型的优先后端归一、前端解耦的工程决策虽然初期多写一点转换代码后期省下的调试时间却是成倍的。6.3 测试集设计的黄金分法不管你做公式转换还是手写识别测试集都是保证质量的基石。我的做法是按公式类别分组线性公式abc无上下标分数根式\frac{a}{b}、\sqrt{x^2y^2}上下标组合e^{i\pi}10矩阵行列式多行公式\begin{aligned} ...分段函数带\left\{的分支结构特殊符号求和、积分、极限、希腊字母。每组准备50个典型公式标注好LaTeX、MathML和Word里的等价形式跑回归的时候按组统计通过率。手写识别再加一组真实路测集让不同人用不同书写习惯写同一批公式用来评估模型在不同字迹上的稳定性。真实路测集不在于大而在于多样性——同样是x有人一笔写完有人两笔写完识别管线都得接得住。我自己在这个项目里最大的体会是跨平台编辑器的公式兼容从来不是把A格式转成B格式的翻译问题而是如何让不同来源的公式真正住进同一个结构里的架构问题。手写识别、Word导入、OCR、键盘输入各自只是这个结构的一条入口。先把结构定扎实别怕多写转换层后面的路会越走越顺。至于具体的模型选型、渲染方案都可以在实际项目中边跑边调因为真正决定产品体验的永远是那条完整的链路而不是某一个点的精度。
返回列表