做芯片厂的内部业务系统,有一类需求看起来很简单,实际一碰全是坑:工程师遇到设备异常或者想更新一份工艺说明时,需要在TinyMCE这套富文本编辑器里插入一张CAD图纸,并且要求图纸不能糊、能放大看细节、打印出来线条还要清楚。我刚接手这类需求时,也以为“粘贴”一下就搞定了,结果发现从CAD软件里直接Ctrl+C再切到浏览器里Ctrl+V,得到的只是一张静态图,根本谈不上矢量输出。这篇文章就把我折腾下来的完整思路和实现过程整理一下,尤其是芯片制造企业里最关心的矢量保留问题。
1. 为什么芯片制造场景里,把CAD图纸贴进TinyMCE会成为一个真问题
1.1 先看典型业务:FAB内网文档系统里为什么会用到CAD图纸
很多人印象中芯片制造企业用的都是版图设计工具,跟AutoCAD这类CAD软件关系不大。但实际情况不是这样,芯片厂里跑的设备布局图、净化车间改造图、气体管路走向图、水电气配套图,甚至设备备件安装图,很多都是用CAD格式维护的。设备工程师在做保养记录、异常报告或者工程变更申请时,经常需要把相关图纸贴进说明文档里,告诉下一环节的人“问题出在哪根管路”“这次改造影响哪个区域”。
这类业务系统的前端,十有八九用的是现成的富文本编辑器,TinyMCE是比较常见的选型。工程师的理想操作流程是:在CAD软件里选中图纸内容,复制,回到网页编辑器里粘贴,再补充几句文字说明,保存提交。这个流程听起来顺畅,但问题恰恰出在“复制粘贴”这个动作上。
1.2 直接从CAD复制粘贴到TinyMCE会发生什么
从Windows系统剪贴板的底层行为说起。CAD软件复制对象时,通常会向剪贴板同时写入多种格式的数据,比如原生CAD对象、图元文件EMF、位图PNG/BMP、纯文本等。浏览器收到粘贴事件时,并不能直接识别CAD原生对象,只能读取它认识的那些标准格式。于是Chrome、Edge这些浏览器最终在编辑器里插入的,几乎都是来自剪贴板的PNG位图,或者一张被转换过的EMF渲染图。
位图意味着什么?意味着这张图的分辨率在复制那一刻就固定了。CAD图形里大量细线条、小尺寸标注、文字注释,只要一缩放,要么变模糊,要么出现锯齿。如果是A0或者A1的大图,转成PNG后文件体积还特别大,上传、保存、预览全都会被拖慢。更糟糕的是,如果文档后续要打印归档,位图的清晰度完全达不到工程档案要求。
还有个容易被忽略的问题:位图是无法选中和检索的。工程师想把图纸里的某个坐标、某个图号复制出来,或者想在文档系统里搜关键词,对着位图完全没法操作。这跟芯片制造企业重视可追溯性的习惯是冲突的。
1.3 我们要的“矢量输出”到底指的是什么
这个需求里的“矢量输出”,我理解下来其实包含三层意思。第一层是显示层的矢量,也就是图纸进入浏览器后,不是一张固定像素的位图,而是可以任意缩放不模糊的矢量图形,通常落地成SVG;第二层是编辑层的矢量,即图形元素在HTML文本中是真实存在的DOM节点或者可引用的资源对象,而不是一个死图片;第三层是业务层的矢量,图纸里的图形还能和图号、版本、责任人这些元数据挂勾,后续可以追踪来源。
三层都满足,才能算真正解决了问题。只做到第一层,比如想办法插入一张超高分辨率的长图,本质上还是没有解决矢量化。所以整个方案的核心,就是设计一条从CAD文件到浏览器端SVG的安全可控链路。
需要模型API调用? 免费领10W Token,多模型网关一键接入 Claude、DeepSeek 等主流模型。
2. 实现方案选型:从DWG到浏览器可显示矢量的可行路线
2.1 主要路线横向对比
为解决“矢量输出”,我先后评估过几条路线。直接粘贴剪贴板位图,第一轮就被否掉,原因上面已经说了,它压根没有矢量信息。接下来说几条真正有可能保住矢量的路线:
第一种是前端解析DWG/DXF。思路是用浏览器里的JavaScript库直接把CAD文件解析成SVG,常见库有dxf-parser、three-dxf等。这套路对DXF格式还有一定可行性,因为DXF本质是文本格式,但遇到真正的DWG就比较吃力,尤其是高版本DWG,文件结构复杂且带压缩,纯前端解析很容易出现图元丢失、文字乱码的情况,开发量很大。
第二种是服务端转换,用专门工具把DWG/DXF转成SVG后回传前端。这条路线的转换质量稳定,能处理复杂图纸,而且代码只在服务端维护,前端只要上传和展示。对芯片厂内部系统来说,图纸文件本身就在内网流转,服务端处理也更安全可控。
第三种是所谓“伪矢量”方案,把CAD图纸先打印成高精度PDF,再在页面里嵌入PDF。PDF可以缩放,但它不是小型TinyMCE文档可以灵活使用的格式,编辑器的内容导出、字数统计、全文检索都很难处理,所以只能算旁路方案,不能作为主流程。
2.2 为什么选择“上传图纸+服务端转SVG”的路线
我最终定的是第二种,上传原始图纸文件到服务端,由服务端完成DWG到DXF再到SVG的转换,然后把SVG内容的访问地址返回给前端。这里有两个关键判断。
第一个判断是,不要把希望寄托在“自动读取系统剪贴板里的CAD矢量数据”上。浏览器安全模型决定了网页程序拿不到CAD软件写入剪贴板的私有格式,即使能拿到,跨浏览器兼容性也非常差。让用户通过一个显式的“上传CAD图纸”按钮提交文件,是现有技术条件下最可靠、最不依赖客户端环境的方式。
第二个判断是,矢量输出需要一个稳定、可扩展的中间格式。SVG是目前和HTML集成最好、浏览器原生支持的矢量格式,后续做缩放、打印、二次标注都有现成生态。把转换放在服务端,也让CAD图纸这类重资源的处理不会阻塞编辑器的输入操作。整体数据流可以概括为:用户上传DWG/DXF文件,服务端生成SVG文件,前端在TinyMCE里嵌入一个指向该SVG的占位对象,查看详情时再按需加载。
3. TinyMCE集成与核心实现细节
3.1 TinyMCE侧的用户入口设计
这里需要先做一个操作层面的决定:不建议继续让用户“直接在CAD里复制、到TinyMCE里粘贴”,而是自己加一个上传按钮。原因不是开发人员偷懒,而是为了保住矢量。我在编辑器工具栏上增加了一个“CAD图纸”按钮,点击后弹出文件选择框,只允许选DWG和DXF文件。这样用户的操作路径就变成了从“复制粘贴”到“上传文件”,虽然多了一步,但图纸信息是一条完整的文件链路,后端能真正解析出矢量数据。
TinyMCE 6的自定义按钮代码大致是这种结构:
javascript复制tinymce.init({
selector: '#contentEditor',
plugins: 'lists link table code',
toolbar: 'blocks bold italic bullist numlist | cadupload | code',
setup: function (editor) {
editor.ui.registry.addButton('cadupload', {
text: 'CAD图纸',
onAction: function () {
let input = document.createElement('input');
input.type = 'file';
input.accept = '.dwg,.dxf';
input.onchange = function () {
let file = input.files[0];
if (!file) return;
uploadAndInsertCad(editor, file);
};
input.click();
}
});
}
});
注意,TinyMCE的按钮回调里不能直接调用系统文件框,因为回调上下文不是用户点击事件直接触发链,一些浏览器会拦截。解决方案就是动态创建一个隐藏的input元素,再调用它的click方法,这是富文本插件里比较通用的做法。
3.2 上传、转换与回填的完整实现
前端拿到文件后,通过FormData上传到后端接口。以下是一个简化但可以运行的上传和回填函数:
javascript复制async function uploadAndInsertCad(editor, file) {
const formData = new FormData();
formData.append('file', file);
formData.append('docId', document.querySelector('#docId').value);
try {
const resp = await fetch('/api/cad/vectorize', {
method: 'POST',
body: formData
});
const data = await resp.json();
if (!data.ok) {
editor.windowManager.alert('图纸转换失败:' + data.message);
return;
}
// 优先插入 svg 占位容器,真正的 svg 通过懒加载或预览时再渲染
// 避免因为 svg 过大导致编辑器卡顿
const placeholder = [
'<div class="cad-vector" data-cad-svg-url="' + data.svgUrl + '"',
' data-cad-name="' + escapeHtml(data.fileName) + '">',
'<div class="cad-vector-title">矢量图纸:' + escapeHtml(data.fileName) + '</div>',
'<object data="' + data.svgUrl + '" type="image/svg+xml"',
' class="cad-vector-object"',
' style="width:' + data.viewWidth + 'px;height:' + data.viewHeight + 'px;">',
'当前浏览器不支持 SVG 预览',
'</object>',
'</div>'
].join('');
editor.insertContent(placeholder);
} catch (err) {
editor.windowManager.alert('请求异常:' + err.message);
}
}
function escapeHtml(text) {
return text.replace(/&/g, '&').replace(/</g, '<').replace(/>/g, '>');
}
这里有个细节值得多说一句:上传成功后,我们返回的是svgUrl而不是直接把SVG源码拼进HTML。之所以这么设计,是因为DWG转出来的SVG往往体积较大,而且可能包含外部字体、图片引用,直接塞进编辑器正文会造成两个问题,一个是TinyMCE的HTML解析器在处理大量SVG节点时可能误伤代码结构,另一个是内容保存时会把SVG全部塞进正文字段,导致数据库文本巨大、全文检索效率下降。用占位方式保存,SVG本体走文件接口,正文只保存一个引用标记,这样既保证了编辑器里的显示效果,也让后端保存的数据干净很多。
3.3 服务端转换的具体步骤
服务端的技术栈是Python,所以我设计了一条组合转换链路。第一步,判断上传文件的后缀。如果是DWG,先用ODA File Converter进行格式转换,把它转成DXF。ODA File Converter是一个常见的CAD文件格式转换工具,支持命令行批量处理,适合在Linux服务器上使用。命令大致是这样:
bash复制ODAFileConverter /data/cad_in /data/cad_out ACAD2018 DXF 0 1 "*.dwg"
命令参数含义依次是输入目录、输出目录、目标CAD版本、输出格式、是否递归处理子目录、是否进行图形审核、文件过滤规则。这一步能把新老版本的DWG统一转换成一个相对稳定的DXF版本,方便后续处理。
第二步,对DXF做预处理。DXF文件里可能包含了不需要的布局空间、参考底图、超大的外部参照路径,这些如果不清理,后续转出的SVG会有大量空白区域或异常图形。我会用ezdxf这个Python库清理一下:
python复制import ezdxf
doc = ezdxf.readfile("source.dxf")
# 只保留模型空间里的图元
msp = doc.modelspace()
# 如果原图有外部参照,按需解除或忽略
doc.discard_block("DELETED_BLOCK")
target_svg_path = "source_prepared.dxf"
doc.saveas(target_svg_path)
第三步,DXF转SVG。经测试,直接用dxf2svg开源库处理中等复杂度图纸是可用的。核心逻辑大体如下:
python复制from dxf2svg import DXF2SVG
converter = DXF2SVG("source_prepared.dxf")
converter.write_svg("output.svg")
如果图纸里有大量椭圆、样条曲线、填充快,dxf2svg处理效果会不稳定,这时我会改用后端渲染管线,比如基于ODA的绘图内核转成SVG,或者把DXF拆成基础图元后逐类转换,保证最终SVG中只保留path、circle、line、text、polyline这些常见矢量节点。
转换完成后,SVG文件落到独立文件服务里,同时生成一个缩略预览图,用来在列表页快速展示。返回给前端的JSON结构是这样的:
json复制{
"ok": true,
"svgUrl": "/file/cad/20250612/abc123.svg",
"thumbUrl": "/file/cad/20250612/abc123_thumb.png",
"fileName": "etching_area_layout.dwg",
"viewWidth": 1280,
"viewHeight": 720
}
3.4 在HTML里保存和渲染SVG时的防丢方案
如果你打算不采用占位方式,而是把SVG标签真正放到TinyMCE编辑器内容里,那就必须处理TinyMCE的标签过滤机制。默认配置下,TinyMCE会按白名单方式清理HTML,SVG这种不在schema标准里的节点可能会被剥掉或打散。遇到过类似问题的同学应该有印象,辛辛苦苦插入的SVG,源代码里看着还在,切回可视化模式再切回来,图就没了。
我测试下来,需要在初始化时保留SVG相关节点:
javascript复制tinymce.init({
// 让svg及常用子节点作为允许元素保留
custom_elements: 'svg,path,circle,rect,line,polyline,polygon,g,defs,text,tspan,use,image,stop,linearGradient,title,desc',
extended_valid_elements: 'svg[*],path[*],circle[*],rect[*],line[*],polyline[*],polygon[*],g[*],defs[*],text[*],tspan[*],use[*],image[*],stop[*],linearGradient[*],title[*],desc[*]',
valid_children: '+body[svg],+p[svg],+div[svg],+svg[g],+svg[path],+svg[rect],+svg[circle],+svg[text]',
});
不过从实际项目稳定性考虑,我还是建议走占位方案。业务上完全可以把SVG的查看做成“点击后在弹窗里放大预览”,不需要在编辑器视口内直接缩放。这样TinyMCE保存的只是那个容器div,不会被解析器反复处理,打印和导出再做一次特殊渲染即可。
4. 实操过程中的参数调整与效果验证
4.1 坐标范围和显示比例的处理
初版接口做完后,我拿一张典型的车间设备布置图测试,发现SVG插入编辑器后比例不对,图纸中的设备框缩成一团,旁边出现一大片空白。排查后发现,DXF转SVG时默认会保留图纸的原始坐标范围,但DWG里很多图形的作图原点离图形本体非常远,甚至到了几千、几万毫米之外。如果直接渲染,浏览器需要在一个巨大的画布上找那个设备,自然看到的图形就很小。
解决办法是在后端转换时做归一化处理。读取DXF图形实体的最大包围盒,算出图形中心点,然后整体平移到坐标原点附近,再按输出宽度换算缩放比例。ezdxf里可以遍历实体边界:
python复制import ezdxf
from ezdxf import bbox
doc = ezdxf.readfile("source_prepared.dxf")
msp = doc.modelspace()
extents = bbox.extents(msp)
min_x, min_y = extents.min.x, extents.min.y
max_x, max_y = extents.max.x, extents.max.y
# 输出SVG默认宽度设为1600px,按包围盒等比计算高度
output_width = 1600
draw_width = max_x - min_x
draw_height = max_y - min_y
scale = output_width / draw_width
output_height = int(draw_height * scale)
# 通过平移向量把包围盒左下角移到(0,0)附近
# 具体动作可以在dxf2svg前对dxf做偏移,也可以在SVG根节点上做transform
这样处理后的SVG,viewBox是一个整数范围的矩形,图形自动居中,网页展示效果稳定很多。
4.2 线宽、字体、图层颜色的保留规则
CAD图纸变成SVG后,线宽、颜色、文字是最容易“变味”的三个点。DWG里默认各种颜色是索引色,比如1号红色、7号白色/黑色,如果直接转成SVG时不做映射,白色图元在浏览器白底上就会看不清。我最后在后端加了一个颜色映射表,把CAD索引色和当前文档主题绑定,保证深色线画在白底上可读。
字体上,DWG里很多汉字用的是SHX形文件字体,这类字体本身不是TrueType轮廓,转换SVG后可能出现文字位置跑偏或变为线条。在服务端转换阶段,我会优先尝试转为普通TEXT实体,并在输出SVG时指定一个Web安全字体栈。芯片厂环境有时不允许外链字体,稳妥办法是只保留图形,把这类容易出问题的文字统一转成path曲线,这样看起来不是文本,但图形显示是完整的,不依赖客户端字体库。
4.3 和CAD看图软件对照验证
功能上线前,我们需要做一轮完整的效果对照。我在内网找了一台装了常用CAD看图软件的客户端,把同一张图纸分别用“看图软件导出图片”和“服务端转换SVG”两种方式生成结果,打印成PDF后对比。重点关注三块:细线是否出现断线、文字是否出现乱码、填充区域是否有漏白。经过两轮调整,细线断线的问题通过设置SVG形状渲染的stroke-width最小值解决,文字乱码则通过把SHX文字统一转为path解决。
对照下来,SVG方式在线条锐度上明显优于位图方式。位图在200%缩放时,0.5mm线宽的边缘已经出现模糊;SVG在任意缩放比例下轮廓都保持清晰。而且SVG文件大小通常比同等可视范围的高清PNG还要小,对文档系统存储来说也比较友好。
5. 上线后遇到的典型问题和排查清单
5.1 用户问“我在CAD里复制了,为什么要点按钮上传”
业务方最初会质疑,为什么不能像Office那样直接粘贴。这个问题的本质不在编辑器,而是CAD软件没有像Office文档那样把可编辑对象暴露给系统剪贴板的标准格式。我们在操作说明里写清楚了原因,同时做了一个优化:在文本编辑区内增加一个粘贴识别逻辑,如果监测到用户粘贴的是一张来自CAD剪贴板的位图,就弹出一个提示框,引导用户“如需矢量图纸,请用上传CAD文件按钮上传原始DWG”。
判断是否是CAD软件复制的位图,没有百分百准确的方案。我采用的是识别图片宽高比和文件名特征,同时结合编辑器的paste事件做粗判断。这个体验层面的优化不完美,但能减少大量误操作。
5.2 上传后转换成功,但图纸细节缺失
这类问题出现得最多,通常不是整张图没转出来,而是某几类特殊对象丢失,比如代理实体、自定义对象、光栅图像、OLE嵌入对象。DWG里的自定义对象如果缺少对应的ObjectARX应用,很多转换工具都无法解出几何数据。处理办法是回到源头:在图纸管理制度上要求提交图纸时尽量输出标准DXF,或把特殊对象炸开成基础图元。
代码层面,我会在转换前记录日志,统计DXF里出现了哪些类型的图元,一旦发现无法识别的类型,立刻返回一个明确错误码。这样至少用户知道是哪一类对象有问题,而不是面对一个“转换失败”的笼统提示。
5.3 SVG能访问但显示空白
排查这类问题时,先把SVG下载到本地用浏览器直接打开,确认图形文件本身是否正常。如果文件正常,那就是TinyMCE里object标签的高度或宽度不对。我遇到过一次情况是SVG文件的viewBox比例和object标签的width不一致,导致图形被挤到可视区域外。解决办法是后端返回viewWidth和viewHeight,前端创建容器时就按这个尺寸设定高度,同时给object容器加上min-height。
另外,某些低版本浏览器和TinyMCE的组合对object标签支持不好。为了稳妥,我最终在前端实现了一个小的渲染函数,读取svgUrl后通过fetch拿到文本,再通过innerHTML注入到容器中。这样做的好处是SVG成为文档的一部分,打印时也能正常输出。
5.4 文档导出Word/PDF时SVG显示异常
内网文档常常需要导出成PDF或者Word归档。TinyMCE保存的编辑器内容里如果放的是object标签,导出组件一概不认。我的解决方案是导出前做一次预处理:在后端遍历整个文档HTML,遇到带有data-cad-svg-url属性的div,就读取对应的SVG文件,把SVG内容替换进去,再用专门渲染HTML到PDF的组件处理。Word导出则更简单一些,因为Word对图片支持比较好,我在导出时临时把SVG转成高分辨率PNG,插到Word文档里,保证归档文件清晰度达标。
6. 这个方案在芯片制造场景还能继续扩展的地方
当前整套能力已经稳定支持了设备异常工单里的图纸插入。下一步我计划把它延伸到两个方向。第一个方向是图纸审批流,当工程师上传CAD图纸时,自动解析出文件里的图纸名称、图号、版本信息,填入审批单的元数据字段;第二个方向是和缺陷定位配合,上传图纸后,工程师可以在标注层画红框标明问题区域,这样后道工序收到文档时,看到的不是一整张大图,而是带着标注的精确位置。
芯片制造企业对文档的版本管理要求很严。CAD图纸经常会更新,SVG本质上只是某一时刻的渲染快照,所以在SVG的周边信息里必须维护原始DWG的文件版本号和哈希值。我们当前的后端会保存每个上传文件的MD5值,如果同一个图号重复上传且MD5一致,就判断为相同版本,避免文档系统里积累大量重复的矢量副本。
从实际体会来说,做这种编辑器功能,最难的不是技术本身,而是如何让业务方理解“复制粘贴得到位图”和“上传文件得到矢量图”之间的差异。技术人员如果一开始就把入口做成上传按钮,把SVG的保存、展示、导出整条链路打通,后面返工成本会小很多。工程图纸这种东西,只有真正矢量化了,在芯片制造这种要求严格的行业里才经得起放大、追溯和归档。
