C#手写Gerber查看器:从RS-274X解析到SkiaSharp渲染完整实践
简介面向电子工程师与PCB设计师这份资源提供基于C#的Gerber文件查看器完整工程可导入并查看导电路径、孔位、丝印等PCB图层信息适合希望学习C#解析文本指令、构建图形界面的开发者。整个压缩包共35个文件以cs源码、resx资源文件、exe可执行程序及config配置为主体积仅76KB代码量精简、结构清晰便于直接阅读与二次开发。目前已有3627人学习下载。通过源码可了解System.IO逐行读取、正则解析、WPF或WinForms渲染等核心环节并掌握层叠加、缩放平移、颜色编码等查看器常见功能的实现思路是入门EDA工具开发的实用参考。 大概半年前我接了个不算复杂、但很磨人的活儿客户发来一批Gerber文件想让我们在打样前快速确认几个关键尺寸可公司电脑上只装了Altium Designer打开Gerber还得另配组件客户那边更是连EDA软件都没有。折腾了一圈要么是付费商业软件要么是界面老旧的绿色版体验一言难尽。那段时间我正在做C#上位机工具链索性做了个决定用C#手写一个轻量级Gerber查看器能打开、能缩放、能测距、能导出图片就够了。这篇博文就是把整个开发过程复盘一遍从Gerber格式底层原理到解析器设计、渲染选型再到交互实现和性能优化适合想自己写PCB预览工具、或者想深度理解Gerber文件的工程师参考。1. 为什么我决定用C#手写一个Gerber查看器而不是直接用现成工具1.1 摆在我面前的三条路一开始我也没想自己造轮子先调研了一圈现成方案。第一类是商业CAM工具比如业内常见的GC-Prevue、ViewMate等。功能确实完整能看钻孔、能叠层、能做DFM检查但问题也很明显授权费用不低而且界面停留在十年前的设计风格集成到我们内部流程里非常费劲。客户临时要看图总不能先教他装一个几百兆的客户端。第二类是自己写个批量转换脚本把Gerber转成图片再发给客户。可Gerber文件不是一张位图它是矢量指令转图片之前还得先解析。而且每次改版都要重新跑一遍坐标、图层、D码差异稍微多一点脚本就开始失控。第三类就是直接写一个查看器。当时我的判断是这个工具的核心工作其实就两件事——把Gerber指令流解析成可绘制图元再把图元渲染到屏幕上。听起来工程量不小但如果你理解了RS-274X格式的本质这事儿真没那么神秘。加上公司后续还有拼板预览、DFM自动检查、AOI设备导入这些需求一个能自研的查看器能省掉无数外包沟通成本。1.2 为什么技术栈选C#我在团队里常年写C#上位机公司现有的产线软件、设备通讯模块全是.NET体系所以这个选择几乎是顺理成章的。首先C#在桌面端UI开发上效率确实高。WPF的MVVM模式做工具栏、图层面板、状态栏这种交互非常顺手不需要像C那样花大量时间在内存管理和消息循环上。其次生态里有SkiaSharp这种跨平台2D渲染库图形质量比传统GDI好一个量级后面我会细讲。第三如果以后要对接数据库、WebAPI或者把查看器嵌到MES系统里C#的服务端和桌面端可以共用一套解析核心代码这种便利性在项目后期会体现得很明显。多说一句如果你只是为了看Gerber确实没必要走这条路但如果你跟我一样需要把它变成自有工具链的一环那自研的ROI就完全不一样了。2. RS-274X是怎么通过指令流描述一块PCB板的2.1 不要被.GTL、.GBL这些扩展名吓到很多人第一次接触Gerber看到一堆.GTL、.GBL、.GTO文件就头大。其实Gerber文件就是纯文本打开编辑器就能看到内容。每一行或者每一条记录就是一条绘图指令告诉解析器现在把笔移动到哪个坐标当前用什么形状的画刷画一段线还是闪一个点。它跟SVG、HPGL这类矢量格式的思路非常像只不过Gerber是PCB行业自定义的一套标准。扩展名只是约定俗成的图层后缀比如.GTL通常是顶层走线.GBL是底层走线.GTO是顶层丝印.GTS是顶层阻焊不同EDA软件会有自己的命名习惯但文件内部格式基本是RS-274X标准。我现在打开一个实际文件给你看这段内容基本代表了RS-274X的核心%FSLAX44Y44*% %MOMM*% %ADD10C,1.200*% %ADD11R,1.000X0.500*% D10* X10000Y20000D02* X30000Y40000D01* X50000Y40000D01* M02*不要慌逐条来看。以%开头和结尾的是参数指令不属于绘图动作。FSLAX44Y44定义坐标格式表示绝对坐标、前导零省略、X轴整数4位小数4位、Y轴同样。MOMM表示单位是毫米。ADD10C,1.200定义D码10为一个圆孔焊盘直径1.2mmADD11R,1.000X0.500定义D码11是一个矩形孔径宽1.0、高0.5。D10*表示当前选中的是D码10号孔径。然后X10000Y20000D02*是移动到坐标(1.0000, 2.0000)mm不画线相当于把笔抬起来走过去。接着X30000Y40000D01*是从当前点画到(3.0000, 4.0000)mmD01表示落笔画线。最后M02*是文件结束。2.2 理解画刷和运动指令就理解了80%的GerberGerber的绘图模型非常像真实世界的印刷过程一支可更换笔头的笔在纸上移动。笔头就是孔径Aperture笔头形状决定了画出来的线宽和焊盘形状落笔和抬笔则对应D01和D02。我把最常见的指令整理成了一张速查表开发时放在手边非常有用指令/参数含义典型示例%MOMM*%/%MOIN*%设置单位毫米/英寸文件开头必须有%FSLAX44Y44*%坐标格式绝对/增量、整数位/小数位、前导零规则最关键的解析参数%ADD10C,1.2*%定义D码10为圆形孔径直径1.2mmC圆R矩形O椭圆G01*线性插值模式默认画直线G02*/G03*顺时针/逆时针圆弧插值配合I、J中心坐标使用D01*落笔/曝光打开从当前点画到下一个坐标走线、画焊盘轮廓D02*抬笔/曝光关闭只移动不画换位置D03*闪光在当前点画一个当前孔径图形贴片焊盘、过孔焊盘M02*文件结束最后一行理解了这个模型之后Gerber查看器的解析核心就不是绘图而是状态机维护一个当前坐标、当前D码、当前插值模式逐条指令更新状态同时把中间产生的线段、圆弧、焊盘记录下来。3. 渲染引擎与坐标变换最影响体验的架构决策3.1 GDI、SkiaSharp还是直接上GPU解析出来的是真实的物理坐标和尺寸但屏幕显示是一个像素矩阵中间这层坐标变换和渲染引擎的选型决定了整个工具用起来顺不顺手。我最早用GDI写过一版直线和圆都能画但在高DPI显示器上一放大锯齿和模糊问题很扎眼。后来对比了几个方案方案优点缺点适合场景GDI入门快系统自带抗锯齿一般缩放重绘慢简单原型验证SkiaSharp跨平台抗锯齿优秀2D性能好需要引入NuGet包桌面/移动端通用查看器OpenGL/Vulkan性能上限最高开发量大绘制简单几何有点浪费超大PCB、GPU实时渲染最终我选了SkiaSharp。它在Windows/Linux/macOS都能跑后续如果要出个跨平台版本或者做Linux工控机的展示终端代码不用重写。SkiaSharp内部封装了Skia图形引擎也就是Chrome和Flutter在用的那一套2D渲染画直线、圆弧、圆焊盘的曲线很平滑抗锯齿质量非常稳。3.2 世界坐标与屏幕坐标的矩阵映射Gerber文件的坐标原点一般在左下角Y轴向上屏幕坐标系原点在左上角Y轴向下。这个方向差异如果不处理好画出来的板子是上下颠倒的。我看到过不少初版实现解析一条指令就做一次坐标换算screenX (worldX - minX) * scale offsetX; screenY (maxY - worldY) * scale offsetY;这种方式在数据量小的时候没问题但Gerber文件动辄几万条线段每条都做乘加运算加上鼠标拖拽、滚轮缩放时全量重算UI线程很容易卡顿。我的做法是把坐标变换抽象成一个矩阵渲染时让SkiaSharp直接使用这个矩阵var worldToScreen SKMatrix.CreateScale(scale, -scale) * SKMatrix.CreateTranslation(offsetX, offsetY); canvas.SetMatrix(worldToScreen);这里用-scale做Y轴翻转offsetX/offsetY表示平移量。缩放和平移操作只需要修改scale和offset两个变量再刷新界面不需要重新遍历任何一个图元坐标。实测下来即便是几十万条线段的大文件拖拽时依然能保持流畅。3.3 不要一条一条画用SKPath批量构建刚开始我的绘制逻辑是每个图元就是一个独立的SKPaint调用一次DrawLine或者DrawCircle。结果打开一块四层板顶层走线加底层走线两万多个图元界面就开始掉帧。后来改成按渲染层批量构建SKPath把同一层的所有直线段合并到一个SKPath中大圆形焊盘和矩形焊盘按孔径类型分类分别走批量绘制一层一个SKPaint同一种线宽不再切换画笔。这样GPU和CPU的压力都小了很多画面层级也更清晰。而且SkiaSharp对统一画笔的路径绘制有优化批量DrawPath的性能比逐条DrawLine高一个数量级。4. 解析层解构从文本到内存中可绘制的图形对象4.1 解析器的分层设计我第一版解析器是边读边画看到D01就立刻画一段线到界面上。虽然代码简单但缩放、导出图片、图层隐藏这些操作全都变得很痛苦因为你没有保留原始数据。所以第二版我把解析器改成了纯数据解析读取文本按照Gerber命令规则生成一组内存中的图元对象完全不管渲染。渲染层只依赖这组对象。职责分清楚之后整个架构就稳定了读取层StreamReader逐行读取文件词法层把每一条记录按字母和数字拆成指令片段语法层识别D01/D02/D03、G01/G02/G03、X/Y/I/J参数对象层把识别结果转换成LineSegment、ArcSegment、FlashDot等图元对象。4.2 FS坐标格式解析是最容易错的环节Gerber格式里最坑的一个点就是坐标值的前导零省略。举个实际例子FSLAX44Y44表示整数4位、小数4位。如果坐标值是X1.0000mm理论上要写成X00010000但很多EDA导出时会把前导零省略变成X10000。如果你按固定8位去截取字符串就会把后面Y坐标的一部分吞掉。我的处理方式是不依赖固定长度而是按足够解析完X后再解析Y的方式逐个坐标读取。具体做法是先根据FS声明的整数位小数位计算出单个坐标的理论长度然后读取坐标字符串如果不足理论长度在左侧补零再按小数点位置还原数值。private static double ParseGerberCoordinate(string coordData, int intDigits, int decDigits) { int totalDigits intDigits decDigits; if (coordData.Length totalDigits) coordData coordData.PadLeft(totalDigits, 0); double value double.Parse(coordData.PadLeft(totalDigits, 0)) / Math.Pow(10, decDigits); return value; }当时我在这块吃过大亏文件里某个小焊盘坐标位置在版图上偏了0.7mm查了半天才发现是前导零补位逻辑写错了位置。所以真心建议写完解析先把各种EDA导出的文件都跑一遍Altium、PADS、Cadence各来一份专门测坐标偏移。4.3 孔径管理焊盘形状和闪光点Gerber里的D码是孔径编号它规定了当前用什么样的画笔画图。圆孔最简单矩形和椭圆孔径也常见更复杂的是宏孔径Aperture Macro这种可以画异形焊盘处理起来比较麻烦但基础版可以先跳过或者用多个几何体近似。我在内存里维护了一个孔径表class Aperture { public int Code; public string Shape; // Circle/Rectangle/Oval/Macro public double XSize; public double YSize; public double HoleDiameter; // 过孔内径 }解析到D10*就直接从孔径表里取当前孔径之后所有D01/D03都按当前孔径来创建图元D01生成带线宽的线段D03在指定坐标生成一个与孔径形状匹配的焊盘对象。4.4 圆弧解析需要注意的方向问题走线拐弯处特别是差分对、圆弧倒角EDA会导出G02/G03圆弧插补指令。这里的关键是G02是顺时针圆弧G03是逆时针圆弧而且圆弧方向的定义和当前单位、坐标象限有关不能想当然。圆弧指令中通常用I、J表示圆心相对于起点的偏移量。要生成可绘制的圆弧需要把起点、终点和圆心之间的关系算清楚然后决定是画优弧还是劣弧。SkiaSharp里的ArcTo方法能接收圆弧边界矩形、起始角度和扫描角度我把圆心计算好之后往里面传即可。5. 查看器的交互应用缩放、拾取、测量这些日常刚需功能5.1 围绕鼠标位置进行缩放用户看Gerber最频繁的操作就是滚轮缩放。很多初版实现的缩放是以文件原点为中心的鼠标在右上角放大结果想看的位置滚出了屏幕体验非常割裂。正确的做法是屏幕缩放锚定在鼠标当前位置。实现逻辑很简单记录下鼠标的世界坐标然后在缩放前后保证这个点映射到屏幕的位置不变。void OnMouseWheel(double mouseScreenX, double mouseScreenY, double zoomFactor) { // 记录鼠标位置对应的世界坐标 double worldBeforeX (mouseScreenX - offsetX) / scale; double worldBeforeY (mouseScreenY - offsetY) / -scale; scale * zoomFactor; // 让缩放后的该世界坐标重新对应到鼠标位置 offsetX mouseScreenX - worldBeforeX * scale; offsetY mouseScreenY - worldBeforeY * -scale; }这个公式写起来很简单但它让整个浏览体验立刻变得跟专业CAM软件一样自然。5.2 鼠标拾取和状态栏信息显示Gerber文件看着是一堆线条但你真正看图的时候经常想知道某个焊盘是哪一层、孔径多大、坐标多少。我做了个简单拾取功能鼠标移动时遍历当前可见图层里的图元找距离鼠标位置最近、且小于一定像素阈值的对象在状态栏显示它的坐标、所在图层和所属D码。性能方面需要注意不要每帧都遍历全部图元。我用了一个空间索引思路——先按图层过滤再在图层内部按线段的包围盒做快速排除。PCB图元的分布有很强局部性大部分线段很容易被包围盒测试直接排除掉。5.3 测量工具两分钟写一个实用的测距功能客户最常见的问题是这里间距多少两个焊盘中心距多少。测量功能本质上就是两点间的世界坐标距离难度不高但要做几个细节才能好用鼠标吸附测量第二点时自动吸附到最近的焊盘中心或线段端点单位切换内部统一用毫米计算状态栏同时显示mm和mil基准线显示画一条半透明的测量线并在线中央标注距离。对PCB行业来说mil和mm的换算是高频操作如果你做单位切换记得遵守1mm 39.3701mil这个精确值不要用近似值否则测大板子时误差会被放大。5.4 图层管理和PNG导出查看器如果没有图层控制四层板、六层板叠在一起根本没法看。我的方案是从文件扩展名推测图层类型再用颜色表分配固定颜色扩展名图层含义默认显示颜色.GTL顶层走线红色.GBL底层走线蓝色.GTO顶层丝印黄色.GBO底层丝印褐色.GTS顶层阻焊紫色.GBS底层阻焊绿色.TXT / .DRL钻孔白色图层面板上提供复选框勾选哪些层就渲染哪些层。导出PNG时只要把当前界面内容绘制到一张离屏Bitmap上就行SkiaSharp支持直接渲染到SKBitmap不需要借助屏幕截图。6. 实测中踩过的解析坑和性能优化手记6.1 前导零问题引发的坐标漂移前面提到了前导零但我还想再说一次因为它实在坑了太多人。很多自研工具第一版在验证时只跑一个文件刚好那个文件是Altium导出的坐标格式规规矩矩没暴露问题换成PADS或者老版本Protel导出的文件前导零省略规则一变坐标立刻偏移。我的建议是在解析器里写一个自检测逻辑文件开头如果出现FSLAX45Y45这类声明后面所有坐标都按这个格式解析如果坐标字符串长度和声明不符宁可抛异常也不要猜这样至少能尽早暴露问题。6.2 圆弧模式G74/G75的历史遗留坑部分老文件或者某些第三方工具导出的Gerber会用到G74单象限圆弧和G75多象限圆弧它们的圆心计算方式不同解析结果也完全不同。单象限圆弧要求圆心角不超过90度圆心位置只需要根据起点、终点和I/J推算多象限圆弧允许跨多个象限圆心位置仍然用同样的I/J推算但弧的方向和终点落在哪个象限会影响生成路径。我第一版只处理了G75的公共情况结果一个客户发来的老文件里有几段圆弧方向画反了。后来查资料才意识到G74/G75对圆心和象限的判定是有严格区分的。所以我建议在解析圆弧时保留原始参数调试时可以直接打印对比。6.3 大文件卡顿解析线程与增量加载一块12层的高密度板Gerber文件加起来可能超过100MB顶层走线文件单层就有30万条线段。如果所有层一次性解析完再画启动等待时间会非常长。我的优化方案分了两步第一步把文件解析放到后台线程。用Task.Run解析解析过程中生成图元到临时列表每解析完一层就发布到UI线程让用户看到图层列表逐渐出现而不是无脑转圈。第二步对大文件做分层懒加载。默认打开时只解析当前激活的视图范围内需要的层其他层等用户勾选后再加载。由于Gerber文件是纯文本顺序读取这个按需解析实现起来比想象中简单解析器记录每层文件的字节流偏移位置需要时从指定位置继续读就行。实测下来一个60MB的走线文件初始加载从6秒降到1.2秒用户基本感知不到等待。6.4 缩放级别的简化渲染当用户缩到整个板子都显示在屏幕上的时候所有线段其实都小于一个像素这时逐条绘制完全是在浪费CPU。我加了一个LOD策略当缩放比例小于某个阈值时不绘制细线段只绘制焊盘和过孔的大致轮廓当缩放比例恢复到一定程度再切回完整绘制。这个细节分层的策略虽然不算复杂但对大板子浏览体验的提升是肉眼可见的。后来我做DFM预览时直接把这套思路搬过去了效果也很好。最后说一点个人体会。这个项目最终只用了不到两千行核心代码却解决了整个工具链里最麻烦的看图问题。做完之后最大的感受是Gerber格式没你想的那么玄但也没你想的那么随便。真正花时间的不是解析而是把各种EDA工具的导出差异、坐标格式的细节、圆弧方向的判定这些脏活处理干净。如果你也打算做一个类似的工具我的建议是先拿三五家主流EDA软件导出的文件做回归测试把解析稳定性做扎实再考虑界面和交互。数据稳了后面所有功能都是水到渠成的事。本文还有配套的精品资源点击获取