开源GerberView:从RS-274X解析到PCB工程级查看与排错
简介:GerberView 是一套面向 Ubuntu 用户的轻量级开源 Gerber 文件查看工具,适合 PCB 设计、硬件开发与电子工程学习者用于快速检查光绘文件、核对层叠图形与交付前预览,减少对商业软件的依赖。压缩包共 7 个文件,约 1015KB,包含 3 个动态链接库、1 个主程序可执行文件、1 个 Shell 脚本及若干配置与数据文件,体积小巧,便于在 Linux 环境中解压后直接组织运行。目前已有 637 人学习下载。资源保留了开源工具的程序结构与依赖组成,读者可借此了解 Gerber 查看器的基本运行方式、共享库分工及启动脚本的作用,并熟悉其依赖加载过程,对开源软件在 Ubuntu 下的部署与运行机制形成直观认识;也可作为 Ubuntu 下查看和验证 Gerber 文件的实用工具,用于 PCB 打样前核对钻孔层、阻焊层、丝印层与铜层图形,辅助生产文件初检和日常工程排查,具有一定的参考与实践价值。
1. 一块四层板的 12 个文件,暴露了看图软件和工程工具的距离
设计端把板子导出来,落到手上的通常是十来个后缀各异的文件:.gtl、.gbl、.gts、.gto、.drl,没有原理图,没有网络表,只有一个 GerberView-开源 这样的工具能把它们叠起来看一眼。多数人第一次打开它,双击、缩放、拖拽,觉得够用了;真到要判断「这版阻焊开窗是不是比上一版大了 0.1 mm」「丝印有没有压到测试点」的时候,才发现看图软件和能测量的工程工具完全是两回事。GerberView-开源 属于后者:它直接吃 RS-274X 指令流,把光圈定义、正负极性、区域填充还原成矢量几何,再叠成一块可以量、可以查、可以批量跑的板子。接手别人板子的硬件工程师、要核对工厂回传文件的 PCB 工程师、想把 Gerber 解析塞进自己流水线的工具开发者,这三类人用得最多。下面从文件结构一路拆到渲染和排错。
2. Gerber 文件到底长什么样:从一行 D 码拆到坐标整数
Gerber 不是图片格式,它是一段带状态的绘图指令流。文件里没有像素,只有「把光圈移到哪、选多大的笔、画到哪」这些动作,渲染器要做的是按顺序重放这些动作。搞不清这一点,后面所有坐标错位、镜像、全黑的怪现象都无从下手。
2.1 三类指令决定了整个渲染管线
一个 .gtl 文件里能出现的语句,按作用分成三类。
第一类是头部元信息,用%包裹。%FSLAX46Y46*%是格式说明,告诉解析器坐标整数怎么切分;%MOMM*%声明单位是毫米;%ADD10C,0.200000*%定义编号 10 的圆形光圈,直径 0.2;%LPD*%或%LPC*%切换正负极性。这一类的特点是全局生效、只出现一次或少数几次。
第二类是图形操作。G01线性插补、G02顺时针圆弧、G03逆时针圆弧,G36/G37包住一段区域填充,D01画、D02移动不画、D03闪光(相当于拿光圈盖一个章)。后面跟的X、Y是无小数点整数,I、J是圆弧圆心相对当前点的偏移。
第三类是结束符M02*,顺带把当前光圈状态清掉。
一段最小的合法片段长这样:
%FSLAX46Y46*% %MOMM*% %ADD10C,0.200000*% D10* X0Y0D02* X1000000Y0D01* X1000000Y1000000D01* M02*%FSLAX46Y46*%说明 X、Y 都是 4 位整数加 6 位小数,所以X1000000读出来是 1000000 / 10^6 = 1.0 mm。D02先把笔移到原点,两条D01各画一条 1 mm 的边。一条 0.2 mm 粗的线,在 Gerber 里不是「线宽属性」,而是「用一个直径 0.2 的圆沿路径扫过去」,这个心智模型建立起来,后面画圆头线、画焊盘就都顺了。
提示:很多导出工具会省略重复的
G01,也就是一条指令之后连着好几个X..Y..D01*。解析器必须把插补模式当作跨语句保持的状态,不能每条语句重置。
2.2 用 60 行 Python 搭一个最小解析器
先解决头部。这一步的产出是三个东西:X/Y 各自的小数位数、单位换算系数、光圈字典。
import re from dataclasses import dataclass, field TOKEN_RE = re.compile(r'%([^%]*)%|([^*]+)\*', re.S) @dataclass class Aperture: code: int shape: str # C 圆 / R 矩形 / O 椭圆 params: list # 尺寸,已统一换算成毫米 @dataclass class Geometry: segments: list = field(default_factory=list) # (x0, y0, x1, y1, ap) arcs: list = field(default_factory=list) # (x0, y0, x1, y1, i, j, cw, ap) flashes: list = field(default_factory=list) # (x, y, ap, negative) regions: list = field(default_factory=list) # [[(x, y), ...], ...] bbox: list = field(default_factory=lambda: [1e9, 1e9, -1e9, -1e9]) def parse_header(text): """只扫扩展指令,返回 ((xi, xd), (yi, yd), 单位系数, 光圈表)""" fmt = ((4, 6), (4, 6)) unit_scale = 1.0 # 换算到毫米的乘数 apertures = {} for m in TOKEN_RE.finditer(text): body = (m.group(1) or '').strip().rstrip('*') if not body: continue if body.startswith('FS'): mm = re.search(r'X(\d)(\d)Y(\d)(\d)', body) if mm: fmt = ((int(mm.group(1)), int(mm.group(2))), (int(mm.group(3)), int(mm.group(4)))) elif body.startswith('MO'): unit_scale = 25.4 if body[2:4] == 'IN' else 1.0 elif body.startswith('ADD'): mm = re.match(r'ADD(\d+)([A-Z]),([\d.X]+)', body) if mm: code = int(mm.group(1)) params = [float(v) * unit_scale for v in mm.group(3).split('X') if v] apertures[code] = Aperture(code, mm.group(2), params) return fmt, unit_scale, apertures逻辑说明:正则%([^%]*)%|([^*]+)\*把扩展指令和普通指令分成两条路,扩展指令走 group(1),普通指令走 group(2)。FS里面的四位数字按X整数位小数位Y整数位小数位切。MO只在IN时把系数设成 25.4,其余一律按毫米处理。ADD后面的形状参数可能是0.2,也可能是0.06X0.06这种矩形双参数,统一按X拆开再乘单位系数。
参数说明:unit_scale只影响光圈尺寸,不影响坐标——坐标的小数位换算已经隐含了单位,这是最容易搞混的地方,后面 5.1 会单独讲。
再解决坐标解码和主循环。核心是把「当前点」「当前光圈」「当前插补模式」「当前极性」四个状态跨语句保持住。
def decode(raw, digits): """把 '1000000' 或 '-031250' 这类无小数点整数按小数位还原""" if raw is None or raw == '': return None sign = -1 if raw[0] == '-' else 1 body = raw.lstrip('+-') if body == '': return None return sign * int(body) / (10 ** digits) COORD_RE = re.compile( r'X(-?\d+)?Y(-?\d+)?I(-?\d+)?J(-?\d+)?D(\d+)' ) def build_geometry(path): text = open(path, 'r', encoding='utf-8', errors='ignore').read() (xi, xd), (yi, yd), unit_scale, aps = parse_header(text) g = Geometry() cur = (0.0, 0.0) cur_ap = None interp = 1 # 1 线性 2 顺时针 3 逆时针 negative = False in_region = False region_pts = [] for m in TOKEN_RE.finditer(text): ext, word = m.group(1), m.group(2) if ext is not None: body = ext.strip().rstrip('*') if body.startswith('LP'): negative = body[2:3] == 'C' elif body.startswith('ADD'): mm = re.match(r'ADD(\d+)([A-Z]),([\d.X]+)', body) if mm: code = int(mm.group(1)) params = [float(v) * unit_scale for v in mm.group(3).split('X') if v] aps[code] = Aperture(code, mm.group(2), params) continue w = (word or '').strip() if not w: continue md = re.fullmatch(r'D(\d+)', w) if md: cur_ap = aps.get(int(md.group(1))) continue if w in ('G01', 'G1'): interp = 1; continue if w in ('G02', 'G2'): interp = 2; continue if w in ('G03', 'G3'): interp = 3; continue if w == 'G36': in_region, region_pts = True, [] continue if w == 'G37': if len(region_pts) > 2: g.regions.append(region_pts) in_region = False continue mc = COORD_RE.search(w) if not mc: continue nx = decode(mc.group(1), xd) ny = decode(mc.group(2), yd) ni = decode(mc.group(3), xd) nj = decode(mc.group(4), yd) op = int(mc.group(5)) x1 = nx if nx is not None else cur[0] y1 = ny if ny is not None else cur[1] if in_region: region_pts.append((x1, y1)) elif op == 1: # D01 画 if interp == 1: g.segments.append((cur[0], cur[1], x1, y1, cur_ap)) else: g.arcs.append((cur[0], cur[1], x1, y1, ni or 0.0, nj or 0.0, interp == 2, cur_ap)) elif op == 3: # D03 闪光 g.flashes.append((x1, y1, cur_ap, negative)) cur = (x1, y1) for x, y in ((cur[0], cur[1]),): g.bbox[0] = min(g.bbox[0], x); g.bbox[1] = min(g.bbox[1], y) g.bbox[2] = max(g.bbox[2], x); g.bbox[3] = max(g.bbox[3], y) return g逻辑说明:整个循环本质上是一台状态机。D10*这类纯 D 码切换光圈;G01/02/03切换插补;G36进入区域收集模式,此后所有坐标只入栈不入线段表,直到G37收尾。X或Y缺省表示沿用当前点对应坐标,这是合法的,不能当成 0 处理。
参数说明:interp == 2记作顺时针,对应G02,这个布尔值后面算圆弧角度时要单独处理方向。ni、nj用 X 的小数位解码,实际上大多数软件 I/J 和 X/Y 小数位相同,但严谨的解析器最好分别取,5.4 会讲为什么要留这个口子。
2.3 前导零和后导零:一个字符让整块板偏移 10 mm
格式说明里的LA、TA两个字母决定零省略方式。%FSLAX46Y46*%里L是 leading zero omitted,也就是前导零被省掉了,整数部分靠右对齐到 4 位。%FSTAX46Y46*%则相反,是后导零省略、靠左对齐。两种情况下同一个字符串1234代表的数值完全不同:前者是 00001234 取后 6 位小数 = 0.001234,后者是 12340000 取后 6 位小数 = 12.34。
绝大多数现代 EDA 导出的是LA,所以很多简易解析器直接硬编码「整数除以 10^小数位」,跑自己的文件从来没出过问题,一换供应商就翻车。判断方法只有一个:读FS语句,别猜。
| 格式串 | 零省略方式 | 对齐方向 | X1234(6 位小数)解出 |
|---|---|---|---|
FSLAX46Y46 | 前导零省略 | 右对齐 | 0.001234 mm |
FSTAX46Y46 | 后导零省略 | 左对齐 | 12.34 mm |
FSLIX46Y46 | 前导零保留 | 右对齐 | 123.4 mm |
同一张表里三种解出来的值差了两个数量级,落到画布上就是「板子飞出去了」或者「所有线挤成一个点」。我在一个跨平台项目里见过一次,工厂回传的顶层铜用的是保留前导零的写法,本地解析器按 LA 处理,整层图形缩到原点附近变成一团黑点,排查了两个小时才定位到FS里那个I。
3. 把指令流还原成板图:坐标映射、Y 轴翻转与几何重建
解析出来的东西还只是毫米为单位的浮点数,要变成屏幕上能看能点的图形,中间隔着一层坐标变换。这一层写错,表现出来往往不是「报错」,而是「看着不太对」——镜像、错位、比例不对,全靠肉眼比对,最难查。
3.1 从毫米到像素的三步映射
第一步算包围盒。把所有线段端点、圆弧端点和闪光点坐标扫一遍,取 min/max,得到一个(minx, miny, maxx, maxy)。这一步必须在几何收集完之后做,不能边解析边画,否则缩放比例会随文件读到一半而跳动。
第二步定缩放。用画布可用宽高分别除以板子的宽高跨度,取较小值,保证整板都能装下且不变形。这一步用min而不是分别缩放,是为了防止 X、Y 各拉各的导致图形被压扁——这是新手最容易犯的错,画出来的圆焊盘会变成椭圆。
第三步做映射,同时翻转 Y 轴。Gerber 的原点在左下、Y 向上,屏幕画布的原点在左上、Y 向下,不翻转就是上下镜像。
def build_transform(bbox, canvas_w, canvas_h, margin=24): """返回 (tx, ty, scale),scale 单位是 像素/毫米""" minx, miny, maxx, maxy = bbox span_x = max(maxx - minx, 1e-6) span_y = max(maxy - miny, 1e-6) scale = min((canvas_w - 2 * margin) / span_x, (canvas_h - 2 * margin) / span_y) def tx(x): return margin + (x - minx) * scale def ty(y): # 关键:用画布高度减去偏移量,完成 Y 轴翻转 return canvas_h - margin - (y - miny) * scale return tx, ty, scale逻辑说明:scale同时用于两个方向,保证等比例。tx把板子左边界对到margin像素处,ty把板子下边界对到canvas_h - margin处,这样板子在画布里居中偏下,符合 PCB 的视觉习惯。
参数说明:margin建议 20~30 像素,太小会让板框贴边,交互时不好点;太大则有效绘图区被压缩,小尺寸板(比如 10 mm × 10 mm 的模块)会显得很空。
注意:如果只显示单层而不是整叠层,包围盒要用整叠层的并集,不能只算当前层。否则切换层时视图会跳,测量基准也跟着变,量出来的距离没有可比性。
3.2 线段、圆弧、闪光:三种图元三套画法
线段最好处理,把(x0, y0, x1, y1)全部转成像素对,交给批量绘制接口一次性画完。用 matplotlib 的话是LineCollection,用 Canvas 的话可以攒成一条Path2D再stroke()。逐条moveTo/lineTo/stroke在几千条线的板子上就会明显卡,几万条直接假死,这是纯性能问题,不是逻辑问题。
圆弧麻烦一点。Gerber 里I、J是圆心相对起点的偏移量,不是圆心的绝对坐标,也不是相对终点的偏移。算错这一点,圆弧会朝着完全错误的方向甩出去。
import math def arc_polyline(x0, y0, x1, y1, i, j, clockwise, steps=48): """把一段 Gerber 圆弧离散成折线点列,单位毫米""" cx, cy = x0 + i, y0 + j # 圆心 = 起点 + 偏移 r = math.hypot(x0 - cx, y0 - cy) a0 = math.atan2(y0 - cy, x0 - cx) a1 = math.atan2(y1 - cy, x1 - cx) if clockwise: if a1 >= a0: a1 -= 2 * math.pi else: if a1 <= a0: a1 += 2 * math.pi span = a1 - a0 return [(cx + r * math.cos(a0 + span * t / steps), cy + r * math.sin(a0 + span * t / steps)) for t in range(steps + 1)]逻辑说明:先算半径和起终角,再根据顺逆时针把终止角归一到正确的象限方向上。顺时针时让a1 < a0,逆时针时让a1 > a0,这样span的符号自然就带上了方向,插值时不会绕远路。
参数说明:steps是离散精度。0.2 mm 线宽的丝印弧,48 段足够;如果是大半径的板框圆角(半径 5 mm 以上),建议提到 96 甚至 128,否则放大到 400% 能看到明显折线。这个值可以按弧长 / 0.05 mm动态算,比固定值更稳。
闪光(D03)是第三种图元,本质是把当前光圈形状盖在某个坐标上。圆形直接画圆,矩形画矩形,椭圆画椭圆,尺寸都来自光圈参数。这里有个容易被忽略的点:闪光点的尺寸是光圈尺寸,和周围走线的线宽无关,同一层里可以同时存在 0.15 mm 的细线和 2 mm 的大焊盘,因为它们用的是不同编号的光圈。
3.3 负片极性:为什么阻焊层一打开是全黑的
%LPC*%之后的所有图元都是「挖掉」而不是「画上」。阻焊层(.gts/.gbs)几乎全是负片逻辑:整层铺满,开窗处挖空。渲染器如果只按正片画,结果就是整块板被涂成实心,看起来像全黑。
处理办法是给每个图元带上negative标记,绘制时分层处理:正片图元用正常颜色填充,负片图元用背景色或者擦除模式覆盖。在 Canvas 上用globalCompositeOperation = 'destination-out'很直接;在 matplotlib 或者离屏渲染里,更稳的做法是把负片图元单独收集,转成 Path 后做布尔差集。
区域填充(G36/G37)的坑在于顺序。一段区域内部的孔洞,通常是「先画一整块,再用负片挖掉」,挖的顺序错了孔就没了。我一般把同一层内的图元按「正片 → 负片 → 正片」的实际出现顺序严格保留,不做任何重排,因为 Gerber 的语义就是顺序相关。
4. 叠层、配色与测量:让查看器能回答工程问题
渲染出图只是及格线,真正让人愿意天天打开的是「能回答问题」。板子上最常被问的三件事:这层是什么、这两个东西离多远、这个位置坐标多少。
4.1 层顺序与配色:先定语义,再定颜色
Gerber 文件本身不带「我是哪一层」的强语义,除非用了 X2 属性(第 6 章讲)。传统做法靠文件后缀猜,猜错了整叠层的上下关系就乱了。
| 后缀 | 层含义 | 建议颜色 | 默认可见 |
|---|---|---|---|
| .gtl / .cmp | 顶层铜 | 红 | 是 |
| .gbl / .sol | 底层铜 | 蓝 | 是 |
| .gts / .stc | 顶层阻焊 | 深绿半透明 | 是 |
| .gbs / .sts | 底层阻焊 | 浅绿半透明 | 否 |
| .gto / .plc | 顶层丝印 | 白或浅灰 | 是 |
| .gbo / .pls | 底层丝印 | 黄 | 否 |
| .gm1 / .gko | 板框 | 亮黄加粗 | 是 |
| .drl | 钻孔 | 黑或深灰 | 是 |
叠放顺序按「板框 → 铜 → 阻焊 → 丝印 → 钻孔」从下往上。阻焊一定要半透明,不透明的话铜层全被盖住,看不出开窗和焊盘的对位关系,这个对位恰恰是核对工厂文件时最要看的。丝印放最上面是因为它最细,被压住就看不见了。
铜层的正负片也要注意:顶层铜通常是正片(画线),底层铜从顶层视角看是镜像的。查看器一般提供一个「从底层看」的翻转开关,翻转的只是视图,不是数据,导出测量结果时要用未翻转的坐标。
4.2 两点测距与坐标拾取
测距的实现本身很简单,难的是把屏幕坐标正确反算回板子坐标。
def screen_to_board(px, py, bbox, canvas_w, canvas_h, margin=24): """屏幕像素反算成 Gerber 毫米坐标,与 build_transform 严格互逆""" minx, miny, maxx, maxy = bbox span_x = max(maxx - minx, 1e-6) span_y = max(maxy - miny, 1e-6) scale = min((canvas_w - 2 * margin) / span_x, (canvas_h - 2 * margin) / span_y) x = minx + (px - margin) / scale y = miny + (canvas_h - margin - py) / scale return x, y def measure(p1, p2): """两点毫米坐标,返回 (欧氏距离, dx, dy)""" dx = p2[0] - p1[0] dy = p2[1] - p1[1] return math.hypot(dx, dy), dx, dy逻辑说明:反算公式必须和build_transform用同一套scale和margin,任何一处改了另一处没改,量出来的距离就会系统性地偏大或偏小。这也是为什么我把这两个函数放在一起维护,中间不插别的计算。
参数说明:返回值同时给出dx、dy和斜距,是因为工程上经常要的不是直线距离,而是「水平方向差了多少」,比如核对两个连接器引脚间距。
测距之外,坐标拾取还应该带上当前层信息:点一下要知道这是哪一层的什么图元。这需要在渲染时维护一张空间索引,简单做法是按网格把图元分配到桶里,点击时只查所在桶,几万个图元的板子也能做到毫秒级响应。
4.3 大文件的两处性能瓶颈
一块 300 mm × 300 mm 的高密度板,单层线段数可以到十几万。这时候性能瓶颈通常不在解析,而在绘制。解析是纯字符串扫描,Python 也能跑到几十万条每秒;绘制如果逐条建对象,光对象创建就能吃掉几秒。
我一般的处理是:解析阶段就把线段攒成扁平数组(array('f')或者 numpy 的float32),绘制阶段一次性提交。圆弧在解析阶段就离散成折线并入线段数组,不在绘制时现算。闪光点因为数量少,单独走 patch 路径,不参与批量。
另一个容易忽略的是重绘频率。缩放和拖拽时如果每一帧都全量重绘,体验会很差。常见做法是交互过程中画低精度版本(圆弧降采样、细线省略),松手后再补一次高精度渲染。
5. 常见问题与排查:六个让板图「看起来不对」的原因
下面六条基本覆盖了我在实际使用和二次开发里遇到的绝大多数「图形不对」类问题。共同点是都不报错,只能靠比对发现。
5.1 整块板上下镜像
现象:板框和丝印文字是反的,焊盘位置和预期上下颠倒。
原因:Y 轴翻转漏了或者翻了两次。Gerber 原点在左下、Y 向上,屏幕原点在左上、Y 向下。如果解析器里某处做了一次翻转,绘制库又默认翻了一次,结果就是回到原样。
解决:把翻转动作收敛到唯一一个函数里,就是 3.1 那个ty。所有其他位置一律用板子坐标,不做任何 Y 相关处理。翻两次比不翻更难查,因为图形看起来「还算正常」,只有对照实际板子才能发现。
5.2 阻焊层整片全黑
现象:单独打开 .gts 文件,整层是实心的,看不到任何开窗。
原因:负片极性%LPC*%没处理,所有图元都按正片填充了。
解决:渲染时区分正负片,负片图元用擦除模式或者布尔差集。快速验证方法是在文件里搜LPC,如果出现次数很多,基本可以确定是负片层。另一个交叉验证是看文件大小,负片层通常比正片层小很多,因为只描述要挖掉的部分。
5.3 钻孔和铜层对不上,整体偏移一个固定值
现象:钻孔层和铜层的相对位置是对的,但两者整体偏移了同样的距离。
原因:钻孔文件(Excellon 格式)和 Gerber 是两套格式,坐标小数位和单位声明方式都不同。钻孔文件常用METRIC,TZ或者INCH,LZ这类头部声明,如果没有单独解析,直接套用 Gerber 的格式说明,就会得到固定比例的偏移。
解决:钻孔文件单独走一条解析路径,读它自己的头部声明,不要和 Gerber 共用解码函数。验证方法是拿一个已知间距的排针焊盘量一下孔心距,2.54 mm 的排针量出来是 2.54 就对了,是 0.1 或者 25.4 就是单位错了。
5.4 圆弧方向反了,画成一个大圈
现象:本该是小圆角的弧,画出来绕了大半圈,把整个图形包住。
原因:I、J的方向理解错。标准里I、J是圆心相对起点的偏移,如果按相对终点算,圆心位置就跑到另一边,弧的方向和跨度全变。
解决:cx = x0 + i、cy = y0 + j,只用起点。另外补一个自检:算出来的半径和hypot(x1-cx, y1-cy)应该基本相等(误差在 1e-6 量级),不相等说明圆心算错了,直接抛异常比默默画错要好。
5.5 大文件打开卡死
现象:几十兆的 Gerber 打开后界面无响应,进度条不动。
原因:逐条图元创建绘图对象,内存和 GC 压力都上来了。另一个隐形原因是正则finditer配合re.S在大文件上重复扫描,如果实现里对每条语句都重新search全文,复杂度会变成平方级。
解决:解析阶段用一次finditer扫完全文,绝不在循环内重新扫描;绘制阶段批量提交;圆弧在解析期离散化。三板斧下去,十万级图元的板子打开时间能压到一秒内。
5.6 丝印比实际粗一圈
现象:导出的预览图里,丝印线条明显比设计稿粗,文字糊成一片。
原因:线宽用错了。Gerber 里线的宽度由光圈决定,而丝印层的光圈直径往往就是线宽。如果渲染器额外加了一个「最小线宽」保底(比如为了小尺寸下可见强制 1 像素),在缩小时这个保底会盖过真实宽度。
解决:保底线宽只用于交互视图,导出和测量一律用真实光圈尺寸。更稳的做法是把保底做成缩放相关的:缩放比例低于某个阈值时才启用,且明确标注「当前为示意线宽」。
提示:这六条里有四条属于「不报错但结果错」,所以核对流程里必须有一环是拿已知尺寸的参考物去量,比如 2.54 mm 排针、0.5 mm 间距的 FPC 座。纯靠眼睛看,很难发现 5% 以内的比例误差。
6. 进阶:用 X2 属性做自动层识别与批量自检
传统 Gerber 靠后缀猜层,一旦供应商改了命名规则就全乱。加了 X2 属性的文件会在头部写明自己的身份,形如%TF.FileFunction,Copper,L1,Top*%,直接读这个字段比猜后缀可靠得多。
import re, pathlib FILE_FUNC = re.compile(r'%TF\.FileFunction,([^,*]+)') PART = re.compile(r'%TF\.Part,(Single|Array)') def classify(path): """优先读 X2 属性,读不到再退回后缀判断""" head = pathlib.Path(path).read_text(errors='ignore')[:8192] m = FILE_FUNC.search(head) if m: return m.group(1).strip() suffix = pathlib.Path(path).suffix.lower() return { '.gtl': 'Copper,L1,Top', '.gbl': 'Copper,L2,Bot', '.gts': 'Soldermask,Top', '.gbs': 'Soldermask,Bot', '.gto': 'Legend,Top', '.gbo': 'Legend,Bot', '.gm1': 'Profile,NP', }.get(suffix, 'Unknown')逻辑说明:只读文件前 8 KB,因为 X2 属性必须出现在第一个绘图指令之前,读全文纯属浪费。FileFunction的值里带逗号,所以正则用[^,*]+而不是\w+。读不到就退回后缀映射,保证老文件也能处理。
参数说明:Profile,NP里的NP表示非镀铜,判断板框层时这个字段比后缀更准,因为有些导出工具会把板框写成 .gko 而不是 .gm1。
拿到层身份之后,就能做一个批量自检:遍历一个目录下所有板子,检查每块板是否同时具备顶层铜、底层铜、板框三类,缺哪类直接列出来。我在一个模拟项目里用这个脚本扫过两百多份工厂回传文件,查出十几份缺板框或者板框被并进了丝印层的情况——这些如果靠人工一份份打开看,基本不可能覆盖。
除了查缺,还能查重复:同一个FileFunction出现两次,通常意味着导出时选重了层,叠在一起会让铜层看起来莫名其妙地厚。
从那以后我每次拿到一批新回的 Gerber,都强制先跑一遍层属性自检再打开看图形,把这个动作当成开箱流程的一部分,省下来的返工时间远比写脚本的时间多。希望帮到你。
本文还有配套的精品资源,点击获取