简介:这份资源是一款基于C#编写的Gerber查看器源码,面向电子工程师、PCB设计师以及希望学习图形解析与渲染的C#开发者,用于解决PCB制造中Gerber文件读取、解析与可视化查看的问题。压缩包共35个文件,约76KB,以cs源码文件为主,辅以resx与resources资源文件、exe可执行程序、config配置、pdb调试符号及sln解决方案等,结构完整,便于直接编译运行与二次开发。项目围绕Gerber文本指令的逐行解析展开,涉及坐标、功能代码、单位转换与绝对/增量坐标处理,并通过WPF或WinForms构建可缩放、平移的图形界面,同时支持层叠加、颜色编码与测量等CAM查看器常见能力。目前已有3641人学习下载,适合作为文本解析与图形渲染的实战参考,也可在此基础上扩展导出图像、规则检查等功能。
1. 拆开一个 C# 写的 Gerber 查看器:它到底能替你把哪段活干了
如果你在 PCB 厂、CAM 工程或者硬件团队待过,大概率遇到过这种场景:板厂发来一包 Gerber,你想在 Windows 上快速确认一下线路层、阻焊层、钻孔层有没有叠错,结果手头只有 Altium 或 CAM350,装一个动辄几个 G,授权还卡得死。这时候一个用 C# 写的 Gerber 查看器就很有价值了——它不追求替代 Genesis 那种全流程 CAM 系统,而是把「读入 RS-274X、解析光圈和坐标、渲染成可缩放图形」这条链路用托管代码跑通,源码直接给你,改起来没有黑匣子。
这份资源的核心是 C# Gerber 读入源码加一个可运行的查看器,覆盖 PCB Gerber 文件的解析与显示。适合三类人:想给自家测试工装加个轻量预览的嵌入式工程师、要二次开发 CAM 工具链的软件工程师、以及想搞懂 Gerber 格式到底怎么被程序吃进去的学生和爱好者。下面我按「格式原理 → 解析实现 → 渲染交互 → 避坑 → 进阶」的顺序,把这份源码拆开讲透。
2. Gerber RS-274X 的坐标系与光圈:解析器必须先立住的三个概念
2.1 为什么不能按文本行直接画线
很多人第一次写 Gerber 解析,直觉是逐行读、遇到 D01 就画线、遇到 D02 就抬笔。跑简单文件没问题,一遇到带 Step and Repeat、带多边形填充、带宏光圈的文件立刻翻车。原因是 Gerber 不是绘图指令流,而是一套带状态机的坐标描述语言。解析器必须维护当前光圈(Aperture)、当前插补模式(G01 线性 / G02 顺时针 / G03 逆时针)、当前坐标格式(前导零还是后导零、整数位和小数位各几位),这三个状态任何一个没跟上,后面所有坐标都会整体偏移。
常见做法是先把整个文件按行读入,做一次预处理:去掉注释 G04、识别 RS-274X 扩展属性(%...% 包裹的块),再进入主解析循环。C# 里用StreamReader逐行读比一次性ReadToEnd再Split更稳,因为大板子的 Gerber 动辄几十万行,一次性读入容易触发大对象堆分配,GC 一抖界面就卡。
2.2 坐标格式:FS 指令决定小数点在哪
Gerber 的坐标是整数,小数点位置由%FSLAX36Y36*%这类格式说明决定。X36表示 X 轴整数 3 位、小数 6 位,也就是实际值 = 原始整数 / 10^6。如果解析时把格式读错,图形会整体放大或缩小 10 倍、100 倍。下面这段是格式解析的典型写法:
// 解析 %FSLAX36Y36*% 这类格式说明 // 返回 (整数位数, 小数位数) private (int intDigits, int decDigits) ParseFormatSpec(string line) { // 形如 FSLAX36Y36,取 X 后面的两位数字 int xIndex = line.IndexOf('X'); if (xIndex < 0 || xIndex + 2 >= line.Length) throw new FormatException("FS 指令缺少 X 格式定义"); // 第一位是整数位,第二位是小数位 int intPart = line[xIndex + 1] - '0'; int decPart = line[xIndex + 2] - '0'; // 常见范围校验:整数 2~6 位,小数 3~7 位 if (intPart < 2 || intPart > 6 || decPart < 3 || decPart > 7) throw new FormatException($"坐标格式异常: {intPart}.{decPart}"); return (intPart, decPart); }逻辑说明:Gerber 的 FS 指令里,X 和 Y 的格式通常一致,取 X 即可。参数上,整数位决定坐标上限,小数位决定分辨率。校验范围是为了防止把%FSLAX36Y36*%误读成别的指令。实际项目中我一般会把解析出的格式存成类字段,后续每个坐标都用同一个Math.Pow(10, decDigits)去缩放,绝不每行重算。
2.3 光圈定义:C 指令、AD 指令和宏光圈
光圈分三类:标准光圈(C 圆形、R 矩形、O 椭圆)、自定义光圈(通过%ADD定义)、宏光圈(%AM定义,可含多边形、热焊盘等复杂形状)。解析器要维护一个Dictionary<int, Aperture>,键是 D 码(D10 以上),值是光圈对象。遇到D10这种选择指令就切换当前光圈,遇到D01就用当前光圈在起点到终点之间「画」出对应形状。
这里有个容易忽略的点:光圈尺寸也受坐标格式影响。%ADD10C,0.5*%里的 0.5 是实际毫米值,不需要再缩放;但如果是宏光圈里的参数,有些文件会用原始整数表示,需要按格式换算。我一般会在光圈类里加一个IsMacro标志,宏光圈参数走单独的换算分支。
3. 用 C# 把 Gerber 读进来:解析器分层与关键代码
3.1 分层设计:Lexer、Parser、Model 三层
直接在一个while循环里既做词法又做语义,代码超过 500 行就没法维护。我一般分三层:Lexer 负责把一行拆成指令码和参数;Parser 负责维护状态机、生成图元;Model 层是纯数据对象(Line、Arc、Flash、Region),不依赖任何解析逻辑。这样渲染层只认 Model,换渲染后端(GDI+ 换 SkiaSharp)不用动解析代码。
// 图元基类,所有可绘制对象都继承它 public abstract class GerberPrimitive { public Aperture Aperture { get; set; } public abstract void Render(Graphics g, Matrix transform); } // 线段图元:D01 在 G01 模式下产生 public class GerberLine : GerberPrimitive { public PointF Start { get; set; } public PointF End { get; set; } public override void Render(Graphics g, Matrix transform) { // 把 Gerber 坐标经变换矩阵映射到屏幕坐标 var p1 = transform.TransformPoint(Start); var p2 = transform.TransformPoint(End); using var pen = new Pen(Color.Black, Aperture.GetScreenWidth(transform)); g.DrawLine(pen, p1, p2); } }逻辑说明:GerberPrimitive抽象出Render,渲染层不需要知道图元来自哪种指令。transform矩阵承担缩放和平移,这样缩放查看时不用重建图元。参数上,Aperture.GetScreenWidth把光圈实际尺寸换算成屏幕像素宽度,保证放大后线宽跟着变。
3.2 状态机主循环:D01/D02/D03 与 G01/G02/G03
主循环要处理的核心指令就几个:D01画、D02移动、D03闪绘(Flash,用于焊盘);G01线性、G02/G03圆弧。圆弧解析是最容易出 bug 的地方,因为 Gerber 的圆弧用 I、J 表示圆心相对起点的偏移,还要判断是优弧还是劣弧。
// 处理 D01 指令:根据当前插补模式生成线段或圆弧 private void HandleD01(PointF current, PointF target, PointF? offset) { switch (_interpolationMode) { case InterpolationMode.Linear: _primitives.Add(new GerberLine { Start = current, End = target, Aperture = _currentAperture }); break; case InterpolationMode.Clockwise: case InterpolationMode.CounterClockwise: if (offset == null) throw new InvalidOperationException("圆弧指令缺少 I/J 偏移"); // 圆心 = 起点 + (I, J) var center = new PointF(current.X + offset.Value.X, current.Y + offset.Value.Y); _primitives.Add(new GerberArc { Center = center, Start = current, End = target, Clockwise = _interpolationMode == InterpolationMode.Clockwise, Aperture = _currentAperture }); break; } }逻辑说明:offset就是 I、J 值,圆心由起点加偏移得到。Clockwise标志决定渲染时扫过的方向。参数上,圆弧的半径由圆心到起点距离决定,终点如果不在圆上,说明文件有问题,我一般会记一条警告而不是直接抛异常,因为有些老文件确实存在微小偏差。
3.3 多图层与极性:LPD/LPC 不能忽略
一块板子通常有多个 Gerber 文件(顶层线路、底层线路、阻焊、字符等),查看器要支持同时加载并叠加显示。每个文件内部还有极性指令%LPD*%(正片)和%LPC*%(负片)。负片区域在渲染时要「挖空」已有图形,GDI+ 里可以用GraphicsPath加FillMode.Alternate实现,或者用Region.Exclude。忽略极性会让阻焊层看起来完全错误,这是新手最常见的翻车点之一。
4. 渲染与交互:GDI+ 还是 SkiaSharp,缩放平移怎么不卡
4.1 渲染后端选型:GDI+ 够用,SkiaSharp 更稳
这份源码用的是 GDI+(System.Drawing),优点是 Windows 上零依赖、上手快;缺点是图元数量上万后DrawLine逐条调用会明显卡顿。如果板子复杂,常见做法是先把所有图元按光圈分组,用GraphicsPath批量构建路径再一次绘制,能减少大量 GDI 调用。跨平台需求明显的话,换 SkiaSharp 是更长远的选择,SKCanvas.DrawPath在几十万图元下依然流畅。
| 方案 | 依赖 | 万级图元表现 | 跨平台 |
|---|---|---|---|
| GDI+ | .NET 自带 | 需路径合并优化 | 仅 Windows |
| SkiaSharp | NuGet 包 | 原生流畅 | Windows/Linux/macOS |
| SharpDX | NuGet 包 | 需显卡支持 | 仅 Windows |
4.2 缩放平移:矩阵变换与鼠标锚点
缩放的核心是维护一个Matrix,鼠标滚轮时以光标位置为锚点做缩放,而不是以画布中心。否则用户放大时图形会往一个方向跑,体验很差。平移用鼠标中键或右键拖拽,直接修改矩阵的平移分量。
// 以鼠标位置为锚点缩放,scaleFactor > 1 放大 private void ZoomAt(Point mousePos, float scaleFactor) { // 先把鼠标点反变换回 Gerber 坐标 var inverse = _transform.Clone(); inverse.Invert(); var worldPoint = inverse.TransformPoint(mousePos); // 应用缩放 _transform.Scale(scaleFactor, scaleFactor, MatrixOrder.Append); // 再把同一个 Gerber 点变换回屏幕,计算需要补偿的平移量 var newScreenPoint = _transform.TransformPoint(worldPoint); _transform.Translate(mousePos.X - newScreenPoint.X, mousePos.Y - newScreenPoint.Y, MatrixOrder.Append); _canvas.Invalidate(); // 触发重绘 }逻辑说明:先反变换求出鼠标下的 Gerber 坐标,缩放后再正变换,两次屏幕位置的差值就是需要补偿的平移。参数上,scaleFactor一般取 1.1 或 0.9,太大缩放会跳。MatrixOrder.Append保证变换按调用顺序叠加,顺序错了结果完全不同。
4.3 测量与图层开关:查看器的实用功能
一个能用的查看器至少要有:图层显示/隐藏复选框、测量两点距离、坐标显示。测量功能实现简单——鼠标点击两次,把屏幕坐标反变换回 Gerber 坐标,算欧氏距离再按格式换算成毫米或密耳。图层开关则是给每个加载的文件维护一个Visible标志,渲染时跳过不可见图层。这些功能代码量不大,但决定了这个工具是「能看」还是「好用」。
5. 避坑与排查:Gerber 解析里最容易翻车的五件事
5.1 现象:图形整体偏移或放大 10 倍 → 原因:FS 格式没解析或解析错 → 解决:先打印格式再解析坐标
这是最高频的问题。表现是整块板子跑到画布外面,或者尺寸明显不对。根因几乎都是%FSLAX...%没被正确识别,导致坐标缩放系数用错。解决方法是解析前先把 FS 指令打印出来,确认整数位和小数位,再统一用同一个系数换算。我习惯在解析器里加一个FormatSpec属性,任何坐标换算都走它,杜绝各处手算。
5.2 现象:圆弧画成直线或方向反了 → 原因:G02/G03 方向判断或 I/J 偏移理解错 → 解决:用已知圆弧文件做单元测试
Gerber 的 G02 是顺时针、G03 是逆时针,但屏幕坐标系 Y 轴向下,和数学坐标系相反,直接套公式会画反。另外 I、J 是圆心相对起点的偏移,不是绝对坐标。建议单独准备一个只含一段圆弧的测试文件,断言渲染出的圆弧中点坐标,跑通再处理复杂文件。
5.3 现象:阻焊层显示成全黑或全空 → 原因:LPC 负极性没处理 → 解决:用 Region.Exclude 或 FillMode.Alternate
负片区域要从已有图形里挖掉。GDI+ 里如果只是简单叠加,负片会被当成正片画上去,阻焊层就全错了。正确做法是把负片图元构建成GraphicsPath,用FillMode.Alternate填充,或者用Region做布尔减。注意负片的作用范围是「当前已绘制的所有内容」,所以渲染顺序必须严格按文件内指令顺序来。
5.4 现象:大文件加载卡死或内存暴涨 → 原因:一次性读入加逐条 DrawLine → 解决:流式解析加路径合并
几十万行的 Gerber 一次性ReadToEnd会分配巨大字符串,逐条DrawLine会触发海量 GDI 调用。改成StreamReader逐行解析,渲染时按光圈分组构建GraphicsPath,一次DrawPath画一组。内存上,图元对象能用结构体就别用类,减少 GC 压力。
5.5 现象:不同软件导出的 Gerber 有的能读有的报错 → 原因:扩展属性或宏光圈兼容性不足 → 解决:未知指令记警告而非抛异常
不同 CAM 工具导出的 Gerber 在扩展属性(%TF、%TA等)和宏光圈上差异很大。遇到不认识的指令直接抛异常会让整个文件读不进来,正确做法是记录一条警告、跳过该指令,继续解析。查看器底部可以放一个日志面板,把跳过的指令列出来,方便判断是文件问题还是解析器问题。
6. 进阶:把查看器改成能算面积的 CAM 小工具
源码跑通之后,最有价值的扩展是让它从「看」变成「算」。PCB 工程里经常要估算铜面积、阻焊开窗面积,用来做蚀刻补偿或成本核算。基于已有的图元模型,加一个面积计算模块并不难:线段按光圈宽度算矩形面积,圆弧按扇形加矩形近似,Flash 按光圈形状算。多个图元重叠区域严格算很复杂,工程上一般用栅格化近似——把图形渲染到一张高分辨率位图,统计非背景像素占比再乘以实际面积。
// 用栅格化近似计算指定图层的铜面积(单位:平方毫米) private double EstimateArea(Bitmap raster, double mmPerPixel) { int count = 0; // 锁定像素数据,比 GetPixel 快两个数量级 var data = raster.LockBits(new Rectangle(0, 0, raster.Width, raster.Height), ImageLockMode.ReadOnly, PixelFormat.Format32bppArgb); try { unsafe { byte* ptr = (byte*)data.Scan0; int total = data.Width * data.Height; for (int i = 0; i < total; i++) { // 判断该像素是否被绘制(非背景色) if (ptr[i * 4] != 0 || ptr[i * 4 + 1] != 0 || ptr[i * 4 + 2] != 0) count++; } } } finally { raster.UnlockBits(data); } // 像素数 × 单像素面积 return count * mmPerPixel * mmPerPixel; }逻辑说明:LockBits拿到像素首地址后用指针遍历,避免GetPixel的逐次封送开销。参数mmPerPixel由渲染时的缩放比例反推,必须和实际板子尺寸对应。这个方法算出来的是近似值,精度取决于位图分辨率,一般取板子长边的 4000 到 8000 像素,误差能控制在 1% 以内,足够工程估算。
另一个实用扩展是导出 PNG 或 SVG。PNG 直接Bitmap.Save即可;SVG 需要把图元转成<path>或<line>标签,好处是矢量、可被其他工具二次编辑。我一般会先做 PNG 导出,因为实现成本低,SVG 留到有明确需求再做。
最后说个血泪经验:Gerber 解析这行,永远不要相信「这个文件格式很标准」。我现在的习惯是,每接入一批新来源的 Gerber,先拿三五个文件跑一遍,把跳过的指令和警告日志过一遍,确认没有系统性偏差再批量处理。从那以后我每次改解析器,都强制走一遍「格式打印 → 单图元测试 → 整板对比」三步,再也没出现过整板偏移这种低级翻车。希望帮到你。
本文还有配套的精品资源,点击获取