1. 先搞清楚:CAD图粘贴到网页后,到底丢了什么?
1.1 你粘贴进TinyMCE的只是“一张照片”
半导体工厂里,工艺工程师最怕的事不是设备宕机,是辛辛苦苦画好的CAD图纸,贴进MES、质量系统或BPM流程里的TinyMCE编辑器后,变成一张“马赛克”。
这不是个例。芯片制造企业的日常文档流里,工程变更单(ECN)、不合格品处理单(MRB)、设备异常报告、工艺参数评审记录,几乎每一条都要贴图纸。过去大家习惯了“Ctrl+C从CAD复制,Ctrl+V到Word/Outlook”,到了Web系统的富文本编辑器里,这个习惯直接翻车。
问题出在剪贴板协议上。当你从AutoCAD、中望CAD或Cadence这类专业工具里复制图元时,Windows剪贴板通常会同时提供几种格式:CF_BITMAP(位图)、CF_ENHMETAFILE(Windows增强图元文件)、有时还有CF_HTML或DWG特有的私有格式。浏览器在收到粘贴事件时,绝大多数情况下只认位图这一路,于是TinyMCE拿到的就是一个拍扁的PNG/BMP。
我见过很多工程师现场崩溃:一张版图或者一套洁净室设备布局图,复制粘贴之后想放大看清某个焊盘、某条走线,结果越放大越糊,标注数字全是毛边。更麻烦的是,位图一旦进入TinyMCE,就彻底“死”了——没有图层、没有块、没有属性,后续想在这个图上做标注、测距离、分层显示,全部免谈。
在很多开发者和办公用户眼里,这不过是“图片清晰度”问题。但在芯片制造企业,这个问题被放大了十倍。因为我们的图纸不是给人看的插图,而是要拿来做审核、做追溯、做精度判断的依据。
1.2 芯片制造场景对矢量图的硬性要求
芯片行业的图纸和普通机械图有本质区别。以我经常经手的版图(Mask Layout)为例,金属层、有源区、通孔阵列这些图元动辄几万到几十万个,最小线宽和间距已经到了纳米级。即便只是设备布局图、封装基板设计图,也涉及大量精细尺寸标注。
这种图纸放到TinyMCE里,业务上至少要满足三件事:
第一,放大不失真。审核工程师需要把局部放大到能看清最小线宽、间距、轮廓细节,位图做不到,但矢量图可以无限缩放。
第二,图层属性要能追溯。哪个图形是Metal 1,哪个是Via层,哪个是标注文本,CAD原始文件里区分得清清楚楚,转化为网页内嵌内容后这些信息不能丢。
第三,与原始DWG版本可对应。质量体系审核时,页面里看到的图和DWG源文件必须能对应上,否则审计时说不清楚。
所以表面上是“粘贴后清晰度不理想”的问题,实际上是“如何把CAD的矢量信息无损搬进TinyMCE”的问题。这篇内容就是把我实际跑通的一套方案完整复盘:工具链选型、转换链路、TinyMCE插件写法、还有那些不跑一遍绝对不会发现的坑。
2. 四条技术路线,我最后为什么选了SVG?
2.1 路线一:把图纸转成SVG内嵌页面
SVG是浏览器原生支持的矢量格式,等于“网页里的CAD”。它的核心优势是:无限缩放不糊、图层结构可以保留、文本可以被搜索,而且DOM结构暴露在前端,后续做交互(点选图元、高亮、加批注)都有天然基础。
实现路径是把DWG先转成DXF,再用Python的ezdxf库把DXF渲染成SVG,最后通过TinyMCE插件把SVG内容嵌入编辑器。
这条路线有什么代价?CAD图纸本身极其复杂,块定义(Block)、外部参照(Xref)、线型填充、SHX字体、代理对象,任何一个处理不好都会在SVG里“降级”。所以需要转换前后做一轮清洗和验证,不能只靠库一把梭。
2.2 路线二:用PDF承载,浏览器直接渲染
把CAD打印导出为PDF,然后在TinyMCE里嵌入一个iframe指向PDF,或用pdf.js渲染。这也是很常见的思路。
优点是保真度极高。CAD的线宽、字体、填充、颜色映射全都由打印引擎处理过,基本不丢东西。缺点是交互性几乎为零,你不能在页面上单独选中某个图层,不能做元素级批注,而且PDF文件往往比SVG大不少。如果业务只是“审核归档”,PDF这条线完全够用;如果业务还要做在线批注、局部放大、版本对比,PDF就显得笨重。
2.3 路线三:WebGL/Canvas自研解析渲染
用开源解析器(比如dxf-parser)读DXF的几何图元,然后自己用Canvas或WebGL画出来,甚至可以做几十万图元的流畅缩放和平移。
说实话这是能力最强的方案,也是成本最可怕的方案。且不说DXF各种版本兼容问题,光是线型管理、文字对齐、填充图案、坐标系转换、碰撞拾取,就够一个团队干半年。除非你的场景是超大版图在线查看器,否则芯片厂内部系统的投入产出比完全不成正比。
2.4 路线四:根子上不粘贴,改走附件
这是最容易被忽略的“兜底方案”。既然粘贴不可靠,那就不要让用户粘贴——在TinyMCE里做一个自定义按钮,用户点击后上传DWG文件本体,页面里显示一个“图纸附件”卡片,点击可下载原文件或调用在线看图服务。
这个方案在涉及知识产权保护和版本管控的场景反而最实用。但产品经理们普遍不接受:因为审核流程里需要在正文中直接看到图,而不是下载后另开软件看。所以附件方案最终被我定位为“保底”,SVG方案为主。
2.5 选型结论:按场景组合使用
对芯片制造企业,我的最终建议是三线并存:
- 工序内快速记录、审核说明:用SVG内嵌方案,最灵活,可批注、可缩放;
- 正式签核、外发归档:用PDF方案,保真度最高,不可篡改;
- 原件追踪:附件方案兜底,确保随时能拿回原始DWG。
这个组合的核心理由是安全与可控。芯片厂普遍有内网隔离,图纸基因里就带着保密属性,第三方在线转换网站基本不用考虑,数据绝对不能出内网。所以整套链路必须跑在企业内部环境里,用自建服务完成。
3. 完整实操:DWG图纸如何一步步进入TinyMCE
3.1 第零步:把DWG统一转为DXF
DWG是闭源私有格式,而且AutoCAD每年都在更新内部结构,直接解析DWG不现实。业内通用做法是先转成DXF,DXF是公开的文本格式,几乎所有开源库都能处理。
转换工具我用的是ODA File Converter,它从Version 6.0到R2018的DWG都能转,而且是命令行工具,可以批量跑。这对企业特别重要,工程师丢过来一堆DWG文件,总不能手动一个一个打开再另存。
批量转换的脚本大体是这样:
# Windows环境下,把某个目录下所有DWG转成DXF 2013格式 for /r D:\drawings %i in (*.dwg) do ( "C:\Program Files\ODA\ODAFileConverter\ODAFileConverter.exe" "%i" D:\dxf_out ACAD2013 DXF 0 1 "最后一版.dxf" )有人说可以用Python的ezdxf直接读取DWG,这里必须澄清:ezdxf.opens底层读的是DXF,它对DWG的支持非常有限。所以转换这一步省不了,老老实实过一遍ODA。
参数里有几个坑要注意,比如输出DXF版本。新版图纸如果业务上下游用的CAD版本较低,建议统一转成ACAD2013,兼容性最稳。另外输出文件名的处理,ODA默认会保留原文件名,如果你一次批量转同名不同目录的文件,会出现覆盖,建议在脚本里先做一次文件重命名映射。
3.2 第一步:用Python把DXF渲染成SVG
DXF转SVG,我用的方案是ezdxf的绘图后端。ezdxf维护得很好,自带的drawing插件能处理绝大多数图元类型:LINE、LWPOLYLINE、CIRCLE、ARC、SPLINE、HATCH、TEXT、MTEXT、INSERT都支持。
先安装依赖:
pip install ezdxf matplotlib然后写一个最简转换脚本:
import ezdxf from ezdxf.addons.drawing import RenderContext, Frontend from ezdxf.addons.drawing import matplotlib as mpl_backend import matplotlib.pyplot as plt def dxf_to_svg(dxf_path, svg_path, figure_width=20, figure_height=12): doc = ezdxf.readfile(dxf_path) msp = doc.modelspace() fig = plt.figure(figsize=(figure_width, figure_height)) ax = fig.add_axes([0, 0, 1, 1]) ctx = RenderContext(doc) backend = mpl_backend.MatplotlibBackend(ax) frontend = Frontend(ctx, backend) frontend.draw_layout(msp) fig.savefig(svg_path, format="svg", bbox_inches="tight", pad_inches=0) plt.close(fig) if __name__ == "__main__": dxf_to_svg("input.dxf", "output.svg")这段代码在图形不复杂的场景下直接能用。但芯片厂图纸往往有大量块引用和外部参照,转出来之后常见问题包括:块里的文字全挤在一个角落、填充丢失、图元位置错乱。我后面第4节会专门说排查。
转换输出后,我强烈建议加一个验证环节:重新读一遍生成的SVG,检查它的<svg viewBox>是否符合原始DXF的边界范围。因为很多诡异问题最后都追溯到边界框不对。
3.3 第二步:写一个TinyMCE插件,把SVG“正经地”插进编辑器
很多人以为拿到SVG之后,直接复制粘贴进TinyMCE就完事了。实际情况是:TinyMCE对不安全HTML有过滤机制,内联SVG的几乎所有标签都会被剥掉。你粘贴进去以后,图形消失,只留下一段空白。
解决办法有两个。一个是初始化时给tinymce.init加上一个配置,让编辑器“放行”SVG相关标签:
tinymce.init({ selector: '#editor', plugins: '...', toolbar: '...', extended_valid_elements: 'svg[*],defs[*],g[*],path[*],line[*],rect[*],circle[*],ellipse[*],polygon[*],polyline[*],text[*],tspan[*],use[*]', custom_elements: 'svg,defs,g,path,line,rect,circle,ellipse,polygon,polyline,text,tspan,use' });第二个建议是写一个真正的插件。因为图纸不是随便插入的,它需要有编号、有版本、有缩放控制、有删除清理逻辑。插件里注册一个按钮,点击后打开文件选择框,读取SVG文本,注入编辑器。
一个能落地的最小插件代码如下:
tinymce.PluginManager.add('cad_svg_embed', function(editor) { editor.ui.registry.addButton('cadSvgUpload', { text: '插入CAD图', onAction: function() { var input = document.createElement('input'); input.type = 'file'; input.accept = '.svg'; input.onchange = function(e) { var file = e.target.files[0]; if (!file) return; var reader = new FileReader(); reader.onload = function(ev) { var svgContent = ev.target.result; var designNo = prompt('请输入图纸编号,例如 CAD-DWG-METAL1-001:', ''); if (designNo === null) return; editor.insertContent( '<div class="cad-svg-wrapper" contenteditable="false">tinymce.init({ selector: '#editor', plugins: '... cad_svg_embed', toolbar: '... cadSvgUpload', extended_valid_elements: 'svg[*],...', custom_elements: 'svg,...' });这里有一个非常重要的细节:外层div一定设contenteditable="false",否则编辑器用户在不经意间点进SVG内部,会把某个path或者text删掉,结果整个图纸“缺胳膊少腿”。我在第一版没注意这个问题,结果审核工程师在页面上复制一段文字时,把图纸的子元素带出来一段,愣是找了一下午才发现。
3.4 第三步:单位、比例和viewBox的处理
芯片行业图纸单位五花八门:版图GDSII用纳米,机械制图用毫米,部分封装图用微米,有些老图还混着英寸。转换SVG进Web页面时,单位问题直接决定viewBox和显示比例的正确性。
拿到DXF以后,先读HEADER段的关键变量:
doc = ezdxf.readfile("input.dxf") units = doc.header.get('$INSUNITS', 0) # 0: 无单位, 1: 英寸, 4: 毫米, 6: 米, 9: 微英寸, 10: 密耳, 11: 码, 12: 埃, 13: 纳米如果是毫米,但是图纸要放到网页上显示,我们一般把实际尺寸(毫米)直接作为viewBox的数值范围,然后网页层用CSS控制显示大小。例如图纸宽2000mm、高1500mm,可以设置:
<svg viewBox="0 0 2000 1500" preserveAspectRatio="xMidYMid meet">同时CSS上给.cad-svg-wrapper svg设置max-width: 100%; height: auto;,这样既有原始坐标系,又能随容器宽度缩放。
比例问题我建议不要在转换阶段硬算,把原始单位、比例尺都记录在SVG文件的<metadata>中,前端展示时如果需要“实际测量距离”,直接从metadata里取比例。这一点在第5章会再展开。
4. 踩坑实录与排查速查表
4.1 字体乱码:SHX字形和TrueType必须分开处理
这是最让我头疼的坑。DWG里很多标注用的是CAD特有的SHX形字体(比如txt.shx、hztxt.shx),这些字体本质上不是系统字体,ezdxf渲染时找不到对应字形,就在SVG里输出一个方块或者直接跳过。
当时我们的设备布局图里,一圈几十个设备位号全变成“□□□□”,车间主任看了直接问是不是图纸坏了。
解决思路是转换前先做一次字体替换。遍历模型空间里的TEXT、MTEXT和block里的文本对象,如果发现dxf.style指向的字体是SHX,就改成TrueType字体:
import ezdxf def replace_shx_font(doc, fallback="simfang.ttf"): """将所有使用SHX字体的文本样式替换为TrueType字体""" for text_style in doc.styles: # 如果字体定义里包含.shx,则改为TrueType if text_style.dxf.font and '.shx' in text_style.dxf.font.lower(): text_style.dxf.font = fallback doc = ezdxf.readfile("input.dxf") replace_shx_font(doc) doc.saveas("input_fixed.dxf")替换之后再用前面的脚本转SVG,文字基本能正常输出。如果还要追求观感,建议fallback字体统一用思源黑体或微软雅黑,不要用simfang这种衬线感太强的,工程图标注用黑体最稳。
4.2 图层颜色、线宽全部走样
DXF转SVG时,颜色映射规则其实很容易乱。CAD图层颜色index是从0到255的Aci色,而SVG里用的是RGB/HEX。ezdxf的RenderContext内部做了映射,但实际效果和CAD屏幕显示经常不完全一致。
最典型的坑是“图层打印样式(CTB)”。CAD图纸的显示颜色和打印颜色是两套体系,很多图纸为了出图好看,图层颜色用浅色,打印样式里映射成黑色。直接转SVG取了显示颜色,结果大量浅色图元在白底网页上几乎看不清。
我的处理办法是转换前先做图层归一化。把要的主图层全部遍历一遍,统一指定颜色为黑色或深灰,彻底绕开CAD里“显示色vs打印色”的分歧:
for layer in doc.layers: layer.dxf.color = 7 # 映射为白色/黑色,前端CSS再控制线宽同理。CAD的线宽是打印属性,不是几何属性,SVG里默认所有线都1px。如果图纸里区分粗实线、细实线、虚线,转换后视觉层次会丢。需要在绘图前端或SVG后处理里,按图层或实体属性重新映射stroke-width。
4.3 大图纸把浏览器拖垮怎么办
芯片厂的版图数据量你是没法想象的。我处理过一张金属层版图,DXF文件本身有200多MB,直接转SVG生成了80MB的文本文件,打开编辑器的一瞬间浏览器分分钟卡死。
这种情况下第一条原则是:不要让用户把大SVG直接塞进TinyMCE。重型图纸和正文文档本来就是两种场景。
我的处理策略是“分级显示”:页面里只嵌入一个轻量概览SVG(自动删除重复短线、缩减圆弧分段数、不上传大图元),概览图上提供“查看高清大图”按钮,点击后用独立的看图页面加载全量SVG或WebGL渲染器。概览SVG的数据量控制在2MB以内,正文编辑不会卡,审核需要细节时再跳到高清视图。
轻量化的一个简洁方法是删除多余的图层。芯片图纸里标注层、辅助线层在总览场景经常可以去掉,只保留几何主层。ezdxf里按层名过滤图元很方便:
doc = ezdxf.readfile("large.dxf") msp = doc.modelspace() DROP_LAYERS = {"DIM", "TEXT", "CENTER", "HIDDEN"} for entity in msp: if entity.dxf.layer in DROP_LAYERS: msp.delete_entity(entity) doc.saveas("large_light.dxf")如果删完还是大,那就只能上分块切割了。把图纸按区域切成若干子图,缩放时按需加载对应块。这个方案开发量大,一般只有在做在线版图浏览器时才值得投入。
4.4 方向反了、坐标偏了:坐标系那点事
DXF采用的是世界坐标系WCS,Y轴朝上;而SVG和网页的坐标系Y轴朝下。看似只需要做一个Y轴翻转,但实际操作时会因为图的基点、极轴、模型空间原点位置不同,出现各种“整体偏移”和“旋转”。
ezdxf的绘图后端在MatplotlibBackend内部已经处理了坐标翻转,但如果你的供应商或自己写的解析器没有处理,就会出现整个图上下颠倒的惨案。
手动处理时,公式是这样:
x_svg = x_dxf - min_x y_svg = max_y - y_dxf即先求包围盒,然后以左下角为原点重新定位。反过来如果发现左右翻转,多半是x轴线性变换符号有误。
有一次我们收到的图纸转换后“缺了一半”,查下来才发现DXF里对象的坐标是相对某个用户坐标系(UCS)画出来的,而ezdxf.readfile默认按WCS读取。这种图最稳妥的办法是在AutoCAD里先用UCS命令把它转回世界坐标系,再保存DXF。程序层面处理UCS偏移很痛苦,不值得硬啃。
4.5 常见问题速查表
| 现象 | 根本原因 | 解决办法 |
|---|---|---|
| 粘贴后图片模糊 | 浏览器取了剪贴板位图 | 改用SVG/PDF方案,禁止直接粘贴 |
| SVG插入后空白 | TinyMCE过滤了SVG标签 | 配置extended_valid_elements和custom_elements |
| 文字全是方块 | SHX字体无对应系统字体 | 转换前遍历字体样式,SHX替换为TrueType |
| 图元颜色发白看不清 | 显示色和打印色不一致 | 转换前对图层做颜色归一化 |
| 线宽全部统一 | CAD线宽是打印属性 | 按图层映射stroke-width |
| 大图卡死浏览器 | SVG文件过大 | 图层过滤、删重复图元、分级加载 |
| 图纸上下颠倒 | Y轴方向不一致 | 手动翻转或依赖已处理坐标系的库 |
| 块引用位置错乱 | UCS或外部参照未解析 | 先回CAD转WCS,再导出DXF |
5. 从单点功能到企业级落地
5.1 把转换逻辑封装成后端服务
单靠工程师本地跑Python脚本,只能解决一次性的问题。企业落地必须把转换能力做成服务。
我在项目里用FastAPI封装了一个转换接口,前端TinyMCE插件不再直接读取本地SVG文件,而是上传DWG到后端,后端调用ODA命令行转DXF,再用ezdxf生成SVG,同时返回图纸的元数据(文件名、单位、包围盒、图层清单)。
后端的核心接口伪代码:
from fastapi import FastAPI, UploadFile import subprocess, os import ezdxf from ezdxf.addons.drawing import RenderContext, Frontend from ezdxf.addons.drawing import matplotlib as mpl_backend import matplotlib.pyplot as plt app = FastAPI() @app.post("/convert") async def convert_dwg(file: UploadFile): # 1. 保存上传的DWG dwg_path = f"/tmp/{file.filename}" with open(dwg_path, "wb") as f: f.write(await file.read()) # 2. ODA 转换 DWG -> DXF dxf_path = dwg_path.replace(".dwg", ".dxf") subprocess.run([ "ODAFileConverter", dwg_path, "/tmp", "ACAD2013", "DXF", "0", "1" ], check=True) # 3. 渲染 SVG doc = ezdxf.readfile(dxf_path) msp = doc.modelspace() fig = plt.figure(figsize=(20, 12)) ax = fig.add_axes([0, 0, 1, 1]) ctx = RenderContext(doc) backend = mpl_backend.MatplotlibBackend(ax) Frontend(ctx, backend).draw_layout(msp) svg_path = dwg_path.replace(".dwg", ".svg") fig.savefig(svg_path, format="svg", bbox_inches="tight") # 4. 返回SVG内容 + 元数据 units_code = doc.header.get('$INSUNITS', 0) extents = doc.header.get('$EXTMIN', (0, 0, 0)), doc.header.get('$EXTMAX', (1, 1, 0)) return { "svg": open(svg_path, encoding="utf-8").read(), "units": units_code, "extents": extents }做了服务化之后,TinyMCE插件就变成调用这个接口,把返回值里的SVG插入正文。好处是图纸格式统一、转换规则可版本管理、日志可审计,后端的权限控制和防病毒扫描也能一起接上。
5.2 给图纸加一张“身份证”
企业落地最容易被忽视的就是metadata。图纸进到TinyMCE里以后,它不是一个孤立图片,而是要参与审批流程的工程对象。
所以转换后的SVG,必须在<metadata>里带上这些字段:
| 字段 | 示例值 | 用途 |
|---|---|---|
| design_no | CAD-DWG-METAL1-001 | 唯一编号,关联DWG源文件 |
| revision | A2 | 设计版本,防止新旧混淆 |
| units | mm / um / nm | 单位换算依据 |
| scale | 1:100 | 前端显示比例换算 |
| source_file | mask_metal1_a2.dwg | 原始文件名 |
| designer | ZHANG_WEI | 责任设计 |
| audit_status | PENDING | 审核状态 |
| timestamp | 2024-06-18 14:30:22 | 转换时间,便于审计 |
有了这些字段,前端可以随时弹出“这个图对应的是哪个版本、谁画的、审核到哪一步”的面板。更重要的是,当出现质量问题需要追溯时,后端可以直接根据design_no + revision检索到对应DWG源文件,做到“页面所见”和“原始文件”一一对应。
5.3 权限、审计和版本管理的建议
芯片行业的图纸是核心资产,在线化之后权限控制必须严谨。
我建议至少做到三个层级:普通用户只能查看、工艺工程师可以插入图纸和添加批注、设计主管可以覆盖更新版本。TinyMCE层面的编辑权限是一回事,后端接口也要做同样的校验,因为不能只靠前端按钮隐藏来防人操作。
版本管理方面,最简单的做法是转换服务每次生成SVG时,自动用design_no + revision + 时间戳命名,并把映射关系写进数据库。这样TinyMCE里即使插入了同一个图的不同版本,也能在数据库里被区分开。审核流程中如果发现贴错了版本,可以沿着映射关系直接追溯是哪个人在什么时间贴的。
最后补充一点:整套系统上线时最好先选一个部门试点,不要一上来就铺开。我当时的经验是先用设备工程部当小白鼠,因为他们图纸量适中,问题反馈快,一个月就能把转换规则打磨稳定,再推广到全厂就顺畅很多。
6. 后续还能怎么扩展
这个方案跑稳定之后,扩展空间其实很大。我目前已经在尝试的方向有两个。
一个是把SVG再变成“可批注的图纸”。因为SVG的图元都在DOM里,给每个path节点加>