简介:OwlVision GDSII Viewer 是一款基于 Java 开发的开源版图查看工具,面向集成电路设计、版图验证及半导体相关专业的工程师与学生,用于高效浏览和分析 GDSII 格式的芯片几何布局数据。资源包共 255 个文件,以 131 个 class 与 104 个 java 源码为主体,辅以 7 个 bat 构建脚本、若干 txt 说明及 gdsii 示例文件,并附 pdf 文档与 jar 包,整体约 1.56MB,结构完整便于二次开发。项目强调易用性与效率,支持缩放、平移、旋转、测量等常用操作,并可能通过分块加载与按需渲染应对大容量版图,降低内存占用、提升响应速度。开源特性使代码可自由查看、修改与分享,便于理解解析与渲染机制并参与社区协作。目前已有 388 人学习下载,适合需要轻量级 GDSII 查看方案或研究 Java 图形解析实现的读者参考借鉴。
1. 从一张打不开的版图说起:这个查看器到底解决什么问题
如果你做过芯片后端或者版图相关工作,大概率遇到过这种场景:同事甩过来一个几个 GB 的 GDSII 文件,你手头只有一台普通办公机,装个商业版图工具要排 license、要等审批,甚至压根没买对应模块。这时候你只想干一件事——把版图打开看一眼,确认某个 cell 的位置、某层金属的走向,或者单纯检查一下导出文件有没有缺层。OwlVision GDSII Viewer 就是冲着这个需求来的开源工具,它把 GDSII 版图文件的解析和可视化做成了一套能独立跑起来的桌面程序,不依赖任何商业 EDA 套件。
它适合三类人:一是需要快速预览版图但不想开重型工具的版图工程师;二是想学习 GDSII 二进制格式怎么解析、图形怎么渲染的开发者;三是手里有历史版图归档、需要一个轻量查看方案做长期维护的团队。项目本身是开源软件,意味着你可以直接读源码、改渲染逻辑、加自定义图层颜色映射,甚至把它嵌进自己的流程里当预览组件。接下来我会按「它怎么组织 → 怎么跑起来 → 核心解析和渲染怎么实现 → 怎么改」的顺序,把这份源码拆开讲。
2. 项目结构与技术选型:从目录看它怎么分层
2.1 顶层目录的职责划分
拿到一份源码,我习惯先看目录结构,因为它比任何文档都诚实。OwlVision GDSII Viewer 这类桌面版图查看器,常见的组织方式是按「数据解析 / 渲染 / 界面 / 工具」四块切分。典型结构大致是这样:
OwlVision-GDSII-Viewer/ ├── src/ │ ├── parser/ # GDSII 二进制解析 │ │ ├── gds_reader.py │ │ ├── record_types.py │ │ └── element_builder.py │ ├── render/ # 图形渲染与坐标变换 │ │ ├── canvas.py │ │ ├── layer_style.py │ │ └── viewport.py │ ├── ui/ # 主窗口、菜单、图层面板 │ │ ├── main_window.py │ │ └── layer_panel.py │ └── utils/ # 单位换算、日志、配置 │ ├── units.py │ └── config.py ├── tests/ # 解析与渲染的单元测试 ├── examples/ # 示例 GDSII 文件 ├── requirements.txt └── README.md这个划分的好处是解析和渲染彻底解耦:parser 只负责把二进制流变成内存里的多边形和路径对象,render 只负责把这些对象画到画布上。你想换渲染后端(比如从 Qt 换成 OpenGL),不用动解析代码;你想支持 OASIS 格式,也只需要在 parser 旁边加一个 reader。
2.2 技术栈与依赖选择
从项目定位看,它走的是 Python + Qt 的路线。Python 负责解析和业务逻辑,Qt(PySide6 或 PyQt5)负责窗口、事件和 2D 绘制。为什么不用 C++?因为 GDSII 解析本身是 IO 密集型而不是计算密集型,Python 的 struct 模块处理二进制足够快,而开发效率高出一大截。渲染部分如果多边形数量极大,常见做法是用 QGraphicsScene 做图元管理,或者把大批多边形合并成 QPainterPath 一次性绘制,避免逐个图元走事件循环。
依赖清单通常很克制:
| 依赖 | 用途 | 备注 |
|---|---|---|
| PySide6 / PyQt5 | 窗口与 2D 渲染 | 二选一,看项目实际绑定 |
| numpy | 坐标数组批量运算 | 加速多边形变换 |
| shapely(可选) | 多边形布尔运算 | 只在需要裁剪时引入 |
我一般会先确认 requirements.txt 里有没有锁版本。版图工具最怕的就是 Qt 版本漂移导致绘制 API 行为不一致,所以如果项目里写了PySide6==6.5.*这种,别手贱升级。
2.3 数据流:一个 GDSII 文件从磁盘到屏幕
理解这个项目,关键是理解数据流。GDSII 是二进制格式,内部是一连串「记录(record)」,每条记录有类型、长度和负载。解析器顺序读取,遇到结构体(STRUCT)就建一个 cell,遇到边界(BOUNDARY)就收集多边形顶点,遇到路径(PATH)就记录宽度和端点。全部读完后,内存里是一棵 cell 树,每个 cell 挂着若干几何元素。
渲染时,视口(viewport)决定当前显示哪块区域、缩放多少。坐标变换把 GDSII 的数据库单位(通常是 1nm 或 0.1nm)映射到屏幕像素。图层样式表决定每一层用什么颜色、是否填充、线宽多少。最后画布把这些多边形画出来。这条链路里任何一环出问题,你看到的就是空白或者错位,所以后面排查章节会重点讲。
3. 跑起来:环境准备与首次加载版图
3.1 环境搭建与依赖安装
先确认 Python 版本。这类项目一般要求 3.9 以上,因为用到了较新的类型注解和 dataclass。我习惯用虚拟环境隔离,避免和系统里其他 Qt 绑定打架:
# 创建并激活虚拟环境 python -m venv venv source venv/bin/activate # Windows 用 venv\Scripts\activate # 安装依赖,建议先看 requirements.txt 再执行 pip install -r requirements.txt # 如果项目没有打包入口,通常这样启动 python -m src.ui.main_window这里有个血泪经验:如果你系统里同时装过 PyQt5 和 PySide6,import 时可能拿到错误的绑定,报一些莫名其妙的QWidget: Must construct a QApplication before a QWidget。解决办法是在虚拟环境里只装一种,或者用pip list | grep -i qt确认干净。
3.2 加载第一个 GDSII 文件
启动后主窗口一般有「File → Open」菜单。选一个 examples 目录里的示例文件,或者你自己的版图。加载过程分两步:解析和渲染。解析大文件时界面可能卡住,好的实现会把解析放到 QThread 里,通过信号把进度传回主线程。如果你读源码时看到QThread和Signal,那就是在做异步加载。
加载完成后,视口默认会 fit 到整个版图范围。如果看到的是全黑或者全白,先别慌,大概率是图层颜色没配或者视口没算对边界。可以试着滚动滚轮缩放、按住中键平移,确认是不是只是显示区域不对。
3.3 图层控制与显示参数
图层面板是这类工具的核心交互。GDSII 里每个元素都有 layer 和 datatype 两个编号,面板通常按 layer 分组,每组可以单独开关、改颜色、调透明度。常见做法是用一个字典维护 layer → style 的映射:
# layer_style.py 里常见的样式表结构 DEFAULT_STYLES = { (1, 0): {"color": "#FF0000", "filled": True, "alpha": 180}, (2, 0): {"color": "#00FF00", "filled": False, "alpha": 255}, (3, 0): {"color": "#0000FF", "filled": True, "alpha": 120}, } def get_style(layer, datatype): # 没配过的层给一个默认灰色,避免直接崩溃 return DEFAULT_STYLES.get((layer, datatype), {"color": "#888888", "filled": False, "alpha": 255})参数说明:color是十六进制颜色,filled决定多边形是否填充,alpha是透明度(0 全透,255 不透)。改这里就能定制你习惯的层色方案。注意 alpha 在填充模式下才明显,线框模式基本看不出透明效果。
4. 核心解析与渲染:GDSII 二进制怎么变成屏幕上的多边形
4.1 GDSII 记录读取:struct 解包的正确姿势
GDSII 文件由记录组成,每条记录开头是 2 字节长度(大端)和 1 字节记录类型,然后是 1 字节数据类型,最后是负载。解析的核心就是循环读、按类型分发。下面是一个简化但可运行的读取骨架:
import struct def read_records(filepath): """顺序读取 GDSII 记录,yield (rec_type, data_type, payload)""" with open(filepath, "rb") as f: while True: header = f.read(4) if len(header) < 4: break # 文件结束 length, rec_type, data_type = struct.unpack(">HBB", header) # 记录长度包含这 4 字节头,所以负载要减掉 payload = f.read(length - 4) if length > 4 else b"" yield rec_type, data_type, payload def parse_int2(payload): """两个 2 字节整数,常用于 layer/datatype""" return struct.unpack(">hh", payload) def parse_int4(payload): """一个 4 字节整数,常用于坐标""" return struct.unpack(">i", payload)[0]逻辑说明:>HBB里的>表示大端,GDSII 全程大端,写错字节序会读出天文数字坐标。length - 4是因为长度字段把自己也算进去了,这是新手最容易翻车的地方。parse_int2返回两个有符号短整型,正好对应 layer 和 datatype。坐标用 4 字节有符号整数,单位由文件头里的 UNITS 记录决定。
4.2 从 BOUNDARY 到多边形:顶点收集与闭合
BOUNDARY 记录后面跟着若干 XY 记录,每个 XY 负载是一串 4 字节坐标对。最后一个点通常和第一个点重合,表示闭合。收集逻辑:
def build_polygon(xy_payload): """把 XY 负载转成 [(x, y), ...] 顶点列表""" count = len(xy_payload) // 8 # 每对坐标 8 字节 coords = struct.unpack(f">{count * 2}i", xy_payload) points = [(coords[i], coords[i + 1]) for i in range(0, len(coords), 2)] # 去掉重复的闭合点,渲染时自己闭合 if len(points) > 1 and points[0] == points[-1]: points.pop() return points参数说明:count * 2是因为每个点有 x、y 两个整数。f">..."动态构造格式串,比循环 unpack 快很多。去掉闭合点是常见优化,因为 Qt 的drawPolygon会自动闭合,留着重复点反而可能触发某些后端的退化判断。
4.3 坐标变换与视口:别让版图跑到屏幕外
GDSII 坐标是数据库单位,可能小到 0.001 微米。直接画到屏幕上要么是一个点,要么超出范围。视口要做的是:先算版图包围盒,再根据窗口大小算缩放比和平移量。
class Viewport: def __init__(self): self.scale = 1.0 self.offset_x = 0.0 self.offset_y = 0.0 def fit(self, bbox, win_w, win_h): """bbox = (min_x, min_y, max_x, max_y)""" min_x, min_y, max_x, max_y = bbox w = max(max_x - min_x, 1) h = max(max_y - min_y, 1) # 留 5% 边距,避免贴边 self.scale = min(win_w / w, win_h / h) * 0.95 self.offset_x = -min_x * self.scale + (win_w - w * self.scale) / 2 # Y 轴翻转:GDSII 的 Y 向上,屏幕 Y 向下 self.offset_y = max_y * self.scale + (win_h - h * self.scale) / 2 def to_screen(self, x, y): return (x * self.scale + self.offset_x, -y * self.scale + self.offset_y)逻辑说明:fit算缩放时取宽高比例的较小值,保证整个版图都能塞进窗口。Y 轴翻转是必须的,否则版图上下颠倒,你检查层对齐时会怀疑人生。to_screen里对 y 取负再乘 scale,配合 offset 完成翻转和平移。这套变换如果写错,典型现象就是版图镜像或者偏移半个屏幕。
4.4 渲染性能:多边形多了怎么不卡
小版图随便画,大版图几百万个多边形,逐个drawPolygon会卡到没法交互。常见优化有三条:一是按图层合并成 QPainterPath,减少绘制调用;二是视口裁剪,只画包围盒和当前视口相交的多边形;三是用 numpy 批量做坐标变换,避免 Python 循环。
import numpy as np def transform_batch(points, scale, ox, oy): """points: Nx2 的 numpy 数组,批量变换到屏幕坐标""" arr = np.asarray(points, dtype=np.float64) arr[:, 0] = arr[:, 0] * scale + ox arr[:, 1] = -arr[:, 1] * scale + oy return arr参数说明:points是 N 行 2 列,scale、ox、oy来自视口。批量变换比逐个点算快一到两个数量级,尤其是几十万点的时候。注意 dtype 用 float64,避免大坐标下精度丢失导致图形抖动。
5. 避坑与排查:加载空白、坐标错位、性能卡顿
5.1 打开文件后一片空白
现象:选完文件,窗口没有任何图形,缩放平移也没反应。原因通常有三个:一是解析时字节序读错,坐标变成极大或极小值,视口 fit 到了错误范围;二是图层颜色和背景色相同,比如白底白线;三是解析中途抛异常被吞掉,几何列表其实是空的。解决:先在解析入口加日志,打印读到的 cell 数量和元素数量;再把背景改成深灰、默认线色改成亮绿,排除颜色问题;最后检查 UNITS 记录有没有正确解析,单位错了坐标会差几个数量级。
5.2 版图镜像或上下颠倒
现象:图形能显示,但和预期方向相反,或者层与层之间的相对位置对不上。原因基本是 Y 轴没翻转,或者翻转了两次。GDSII 的坐标系 Y 向上,屏幕 Y 向下,变换时必须翻一次。解决:在to_screen里确认 y 是取负的,同时检查fit里算 offset 时有没有重复翻转。我一般会在视口里加一个flip_y开关,调试时快速对比。
5.3 大文件加载卡死界面
现象:打开几百 MB 的 GDSII,界面直接无响应,进度条也不动。原因是解析和渲染都在主线程跑,Qt 事件循环被堵死。解决:把解析放到 QThread,通过 signal 发进度;渲染如果也慢,可以先用低细节预览(只画包围盒或抽样点),等交互停止后再补全。另一个常见坑是一次性把所有多边形塞进 QGraphicsScene,图元数量爆炸。改成按需创建、视口外销毁,内存和帧率都会好很多。
5.4 图层颜色改了不生效
现象:在面板里改了某层颜色,画布没变化。原因通常是样式表改了但没触发重绘,或者渲染时缓存了旧的 QPen/QBrush。解决:改完样式后调用viewport()或update()强制重绘;如果用了图元缓存,要清掉对应图层的缓存再重建。还有一种情况是 layer 和 datatype 组合没匹配上,面板按 layer 分组但实际元素带 datatype,查样式时要用完整键。
5.5 坐标精度导致的图形抖动
现象:放大到很高倍率时,图形边缘抖动或者顶点对不齐。原因是坐标在 float32 和 float64 之间转换丢了精度,或者数据库单位换算时除了又乘。解决:全程用 float64 存坐标,单位换算只在最终映射到屏幕时做一次。如果项目里用了 numpy,确认数组 dtype 不是默认的 float32。
6. 二次开发:加自定义图层映射和导出功能
6.1 扩展图层样式:从配置文件到运行时热更新
最直接的改造是让图层样式可配置。我一般会在utils/config.py里加一个 JSON 加载函数,启动时读layer_styles.json,运行时改完写回。这样不同工艺的层色方案可以存成不同文件,切换项目时不用改代码。
import json def load_styles(path="layer_styles.json"): try: with open(path, "r", encoding="utf-8") as f: raw = json.load(f) # JSON 的键是字符串,转回 (layer, datatype) 元组 return {tuple(map(int, k.split(","))): v for k, v in raw.items()} except FileNotFoundError: return dict(DEFAULT_STYLES) def save_styles(styles, path="layer_styles.json"): serializable = {f"{k[0]},{k[1]}": v for k, v in styles.items()} with open(path, "w", encoding="utf-8") as f: json.dump(serializable, f, indent=2)逻辑说明:JSON 不支持元组键,所以存的时候拼成"layer,datatype"字符串,读的时候拆回元组。save_styles配合面板的「保存配色」按钮,就能把当前方案固化下来。注意异常处理只抓FileNotFoundError,格式错误应该让它抛出来,否则你会拿到一个静默的空样式表。
6.2 加一个导出 PNG 的功能
查看器经常需要把当前视图导出成图片,贴到报告里。Qt 的QWidget.grab()能直接抓取控件渲染结果:
def export_png(canvas_widget, filepath): """把画布当前内容存成 PNG""" pixmap = canvas_widget.grab() if not pixmap.save(filepath, "PNG"): raise IOError(f"保存失败: {filepath}")参数说明:canvas_widget是实际绘图的控件,不是主窗口,否则会把菜单栏也抓进去。grab()是同步的,大画布可能卡一下,可以配合QApplication.processEvents()或者放到线程里。导出前记得把视口 fit 到你想导出的范围,否则导出的就是当前屏幕那一块。
6.3 验证改造是否正确的笨办法
改完解析或渲染,别急着上大版图。我习惯准备三个测试文件:一个只有单个矩形的最小文件,一个带多层重叠的中等文件,一个包含路径和阵列引用的大文件。每次改完先跑最小文件,确认坐标和颜色;再跑中等文件,确认层间关系;最后跑大文件,看性能和内存。这个顺序能帮你快速定位是解析错了、变换错了还是渲染错了。如果项目自带 tests 目录,先跑一遍单元测试,再手动验证,双保险。
6.4 一个容易忽略的细节:cell 引用与阵列
GDSII 支持 SREF(cell 引用)和 AREF(阵列引用),一个 cell 可以被多次实例化并带旋转、镜像、放大。很多简易查看器只画顶层几何,遇到引用就漏画。改造时要在解析阶段展开引用树,或者渲染时递归遍历并叠加变换矩阵。展开引用要注意循环引用,GDSII 理论上不允许,但损坏文件里出现过,递归前加个访问集合能防止栈溢出。我一般会在 element_builder 里维护一个visited集合,遇到重复 cell 就跳过并打日志。
从那以后我每次改解析器,都强制先跑最小矩形文件确认坐标,再跑带引用的文件确认展开逻辑,最后才碰真实版图。这个习惯帮我省了无数次对着空白画布发呆的时间。希望帮到你。
本文还有配套的精品资源,点击获取