
简介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_factorylist) # (x0, y0, x1, y1, ap) arcs: list field(default_factorylist) # (x0, y0, x1, y1, i, j, cw, ap) flashes: list field(default_factorylist) # (x, y, ap, negative) regions: list field(default_factorylist) # [[(x, y), ...], ...] bbox: list field(default_factorylambda: [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(rX(\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(rADD(\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( rX(-?\d)?Y(-?\d)?I(-?\d)?J(-?\d)?D(\d) ) def build_geometry(path): text open(path, r, encodingutf-8, errorsignore).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(rADD(\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(rD(\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语句别猜。格式串零省略方式对齐方向X12346 位小数解出FSLAX46Y46前导零省略右对齐0.001234 mmFSTAX46Y46后导零省略左对齐12.34 mmFSLIX46Y46前导零保留右对齐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, margin24): 返回 (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建议 2030 像素太小会让板框贴边交互时不好点太大则有效绘图区被压缩小尺寸板比如 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, steps48): 把一段 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, margin24): 屏幕像素反算成 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(errorsignore)[: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都强制先跑一遍层属性自检再打开看图形把这个动作当成开箱流程的一部分省下来的返工时间远比写脚本的时间多。希望帮到你。本文还有配套的精品资源点击获取