大概两个月前,我接手了一个旧项目的数据迁移需求。车间里积压了一大批DWG图纸,需要把里面的设备信息、尺寸和备注提取出来,整理成结构化数据,方便后续的管理系统调用。一开始我天真地以为DXF是纯文本,写个解析脚本就行,结果花了三四天死在版本差异和块嵌套上。后来换成了Teigha,一个跨平台的CAD文件解析内核,终于把这条路走通了。这篇文章就是记录这次选型、开发到落地验收的完整过程,尤其那些容易反复踩的坑,希望给同样要和DWG打交道的朋友一点参考。
Teigha这个库在CAD开发圈子里其实很有名,它封装了DWG和DXF的底层二进制结构,你不需要关心一个圆在文件里到底存成几个字节,只需要调用接口就能拿到圆心、半径、图层这些常规属性。它也能把老版本图纸转换成新版本,或者反过来降级保存。对于做数据提取、批量转换、图纸预览的开发者来说,这套东西比从零开始解析二进制靠谱得多。
下面按我的实际使用顺序来写,先讲为什么选型,再讲怎么搭环境,接着是核心读写的实现,最后是批量处理时遇到的坑和性能优化方法。
1. 项目背景与核心需求解析
1.1 为什么绕不开Teigha
DWG是CAD行业的事实标准,但它不是完全开放的格式。它的二进制布局在不同年代版本里变化很大,光版本代号就有十几种。每个版本里又包含对象图、词典、扩展记录、代理实体、外部参照这些复杂结构。直接解析最可怕的是遇到嵌套块:一个设备可能是由几百个基础实体组成的块,块里还能包含另一个块,而且块引用还带着变换矩阵,坐标得一层层换算回世界坐标系。如果全靠自己写算法,很容易在一个边界情况下翻车。
我最早试过用某个开源解析库,普通的直线、圆、文本能读出来,但遇到有动态块或者扩展字典的图纸,数据就缺失了。更麻烦的是,很多图纸是十几年前的老文件,字符编码五花八门,用错了会导致文本乱码。Teigha把这些历史包袱都接住了,它内置了对多版本DWG、DXF的支持,也提供了统一的对象模型。所以你只需要学会一套接口,就可以兼容不同年代的图纸,这是它最大的价值。
1.2 项目里真正要解决的问题
这个项目不只是把图纸读出来那么简单,具体有三个硬性要求:
- 批量处理,每天可能要解析几百个DWG文件,不能人工手动处理。
- 提取的内容包括设备编号、尺寸标注、文字备注,以及图元所在的图层。
- 最终结果要存入数据库,供后台管理系统做检索和展示。
为了满足第三点,我需要把每个实体的几何信息转换成JSON数据,再批量写入数据库。这里就牵扯到坐标系的处理、单位的换算、特殊字符的转义。用Teigha之后,这些问题都被框在了合理的范围内:坐标系可以直接通过接口获取,单位和图纸的设置相关,转换逻辑也相对固定。
我当时列了一个技术选型对比表,归根到底就是三条:支持老版本格式、能在Linux服务器上跑、可以嵌入到现有的C++服务里。Teigha正好对应上了。
2. 开发环境搭建与工程配置
2.1 版本选择和SDK获取
Teigha的版本迭代有点特殊,早期叫Teigha,后来整体平台化了,但目录结构和大部分API依然沿用老名字。下载SDK后,一般能看到Include、Lib、Docs、Examples这些目录。选版本的时候,一定要看它支持哪一代DWG文件格式。比如R18系列对应比较老的一批DWG,R21覆盖中间几年,R24以上支持更新的图纸。高版本SDK通常能读低版本文件,但反过来就不行。所以做通用工具时,我建议直接选支持代号最高的SDK,同时保留降级保存到老版本的能力。
这里有个容易忽略的地方:SDK的许可文件。Teigha初始化时会检查许可证,许可文件一般是一个自定义格式的授权文件,缺少它的话程序虽然能编译,但运行时会在初始化阶段报错。最开始我在测试环境把许可文件放错目录,查了很久才发现是授权文件没被加载。
2.2 编译链接时的三个常见坑
第一个坑是字符集设置。Teigha的接口要支持中文路径和中文内容,最好把工程设置成Unicode,在Windows下使用宽字符入口,在Linux下确保文件路径按UTF-8传递。有一个很典型的场景:图纸名包含中文,用窄字符打开会失败,改成宽字符就好了。
第二个坑是动态库依赖。Teigha的库不是只有一个静态库,而是一组动态库和对应的导入库。编译能通过,运行时容易提示找不到某个动态库。检查一下执行目录是不是包含了所有依赖,最好能在CMake里写一个自动拷贝依赖的步骤。
第三个坑是运行库的匹配。Teigha的库通常是使用动态运行时编译的,你的工程必须也使用相同的运行时选项,否则会出现堆损坏或者偶发崩溃。在Windows上,尽量统一使用Release版本的动态运行库,不要混着Debugg和Release。
下面是一个常用的CMake配置片段:
cmake_minimum_required(VERSION 3.16) project(CadParser) set(CMAKE_CXX_STANDARD 14) include_directories(${TEIGHA_ROOT}/Include) link_directories(${TEIGHA_ROOT}/Lib) add_executable(CadParser main.cpp) target_link_libraries(CadParser TeighaCore TeighaDb TeighaGeometry )这里的TeighaCore、TeighaDb等库名会随具体SDK版本变化,以你拿到的实际导入库为准。
2.3 初始化和许可证验证
无论你的程序多简单,使用Teigha前都必须先初始化。初始化和释放成对出现,一般长这样:
#include <OdaCommon.h> int main(int argc, char* argv[]) { ::odInitialize(); // 在这里处理你的图纸文件 // 不要在任何数据库对象创建前跳过了初始化 ::odUninitialize(); return 0; }初始化时它会读取系统环境里的许可证,也可能需要你在代码里显式指定许可路径。如果初始化失败,后续调用大多会直接抛异常。建议在初始化代码周围加上日志,方便排查问题。
我在项目里还会判断许可的剩余时间,许可证快到期时提前邮件告警,避免半夜批量任务突然失败。这个细节虽然简单,但很实用。
3. 核心功能实现:从打开到遍历
3.1 打开DWG文件并选择空间
读取一个DWG文件最直接的方式是调用静态方法打开:
#include <OdaCommon.h> #include <DbDatabase.h> OdDbDatabasePtr openDrawing(const std::string& path) { return OdDbDatabase::readFile(path.c_str()); }这样返回的是一个数据库对象,它代表整张图纸。注意路径在Linux下是UTF-8,在Windows下可能需要转成宽字符。如果你用QT的QString或者其他框架,记得先统一编码。
打开后要读取图形数据,通常从模型空间开始。模型空间就是图纸上所有图形实体所在的逻辑空间,对应的接口是:
OdDbBlockTableRecordPtr pModel = pDb->getModelSpaceId().openObject();打开对象时要注意打开模式。Teigha里对象有只读和写两种模式,如果只是遍历,用只读模式就够了;要修改实体,再改成写模式。
3.2 遍历模型空间实体
拿到模型空间后,遍历里面所有实体:
void traverseEntities(OdDbBlockTableRecordPtr pModel) { OdDbObjectIteratorPtr pIter = pModel->newIterator(); int count = 0; for (; !pIter->done(); pIter->step()) { OdDbEntityPtr pEntity = pIter->entity(); if (pEntity.isNull()) continue; ++count; } }这里pIter->entity()返回的是基类实体指针。Teigha几乎把所有的图形元素都抽象成了实体,比如直线、圆、圆弧、多段线、标注、文字,都继承自OdDbEntity。要想拿到具体类型,需要用isKindOf或运行时类型转换。
在遍历时,强烈建议把所有指针都用智能指针保存,比如OdDbEntityPtr。Teigha的引用计数机制和常见的shared_ptr不太一样,但思路类似。如果你用裸指针保存,很容易在批量任务里造成内存泄漏或悬空指针。
3.3 处理常见实体类型
对于一个数据提取工具,最常用到的几种实体要熟悉:
- OdDbLine:直线,主要属性是startPoint和endPoint。
- OdDbCircle:圆,主要属性是center和radius。
- OdDbArc:圆弧,除了圆心和半径,还有起始角度、终止角度。
- OdDbText和OdDbMText:单行文本和多行文本,读取内容用textString。
- OdDbPolyline:多段线,可以拿到顶点数组和宽度。
举个例子,判断一个实体是圆,可以这样:
if (pEntity->isKindOf(OdDbCircle::desc())) { OdDbCirclePtr pCircle = OdDbCircle::cast(pEntity); OdGePoint3d center = pCircle->center(); double radius = pCircle->radius(); }文本实体的处理稍微特殊。有些多行文本的内容存储在扩展数据里,直接用textString可能拿到的是带格式控制的字符串,需要清理一下控制字符。比如“\A1;”这种格式标记,提取时得去掉。
坐标都是从世界坐标系里取出来的,单位是图纸单位,一般是毫米。如果你最后要转成米,记得统一乘以系数。这个很容易忘,我之前就因为这个,把几百个设备的坐标直接放大了一百倍。
3.4 块引用的递归处理
图纸里的设备很多是以块的形式存在的。块引用的本质是一次变换矩阵:基础实体定义在块自己的坐标系里,通过块引用放到图纸的某个位置、缩放和旋转。Teigha可以直接拿到块变换矩阵,但如果你要把里面的子实体坐标转换成图纸坐标系,需要自己乘一下变换矩阵。
处理块引用的代码大致是这样:
OdDbBlockReferencePtr pRef = OdDbBlockReference::cast(pEntity); if (!pRef.isNull()) { OdDbObjectId blockId = pRef->blockTableRecordId(); OdDbBlockTableRecordPtr pBlockTable = blockId.openObject(); // 递归遍历子实体 }这里一定要小心循环引用。虽然理论上块定义不会直接包含自己,但某些复杂的图纸确实会出现深层嵌套。建议遍历时记录一下深度,超过比如20层就直接跳过,避免程序卡死。
嵌套块很快成了一个高频需求。我写了一个递归函数,接收当前实体和当前的变换矩阵,每层块引用就把矩阵乘进去,最后把子实体坐标转换到世界坐标系。这样提取出来的数据才有实际意义。
4. 写入、转换和系统级问题
4.1 修改内容并另存
某些场景下,不只要读,还要把处理后的结果另存成新文件。例如给一个零件批量添加编号文本。这个过程分几步:
- 打开源数据库。
- 把需要修改的实体打开成写模式。
- 修改属性。
- 调用saveAs保存。
保存时可以指定文件版本:
pDb->saveAs(_T("output.dxf"), OdDb::kDxf); pDb->saveAs(_T("output.dwg"), OdDb::kDwg);这里要注意,保存前所有打开的对象都应该关闭或者释放写锁。如果你在遍历过程中修改实体,不能直接saveAs,需要先完成迭代,再保存。否则有可能出现数据库状态不一致。
还有一个坑:如果要另存成老版本,有些特性在新版本中才有,比如某些自定义实体、新的标注样式。这些在老版本格式里可能会变成代理实体,甚至直接丢掉。所以降级保存前,先确认一下你的图纸里有没有特殊对象。
4.2 中文乱码与字体缺失
中文乱码是数据处理工具最容易遇到的问题。原因主要有两个:一是老图纸的代码页设置不正确,二是系统里缺少对应的CAD字体文件。
Teigha读文字时,会尝试把文字内容转换成Unicode。但对于老的有码页的图纸,如果字体定义有问题,转换结果会有偏差。我遇到的情况是:所有中文文本都能读出来,但个别字符变成问号或者空字符,比如钢构的“构”字在某些字体下会丢。
解决办法有两个方向。第一,在打开数据库前,设置代码页:
pDb->setCodePage(_T("ANSI_936"));第二,如果文字内容本身没问题,只是显示宽度不对,那多半是缺少字体文件。Teigha在Linux服务器上常见缺少中文字体,导致多行文本的内容正常,但字体替代错误。这种情况不算严重,至少数据还在。
4.3 代理实体和外部参照
DWG图纸里可以携带第三方软件的自定义实体。Teigha不是CAD原生产品,所以无法百分之百还原这些自定义对象,只能把它包装成一个代理实体。它能拿到的信息非常有限,只有可视化快照或者部分属性。
如果你的业务恰好需要读这些自定义实体的业务字段,就比较难受了。解决办法是让提供方导出成标准实体,或者通过扩展数据把必要信息提前写好。我在项目里就碰到过这种,最后是跟设计师沟通,让他们把设备编号统一写入到实体的扩展字典里,Teigha可以正常读取扩展字典。
外部参照是另一个常见的坑。Teigha打开主文件时,并不会自动递归加载外部参照文件。你需要自己根据主文件里的外部参照路径,去逐一打开被参照的文件,然后把它们的内容合并进来。路径可能是相对路径,也可能是绝对路径,处理的时候注意拼接。
4.4 内存泄漏与批量处理建议
批量处理文件时,最怕内存只涨不降。Teigha的对象模型是引用计数管理,理论上所有智能指针释放后内存会回收,但如果你在循环里长期持有某些数据库对象的引用,内存就可能一直占着。
我整理了两条经验:
- 每个文件处理完,显式释放顶层数据库指针,并调用一次::odClearAll()。
- 不要在循环外缓存大量的实体指针,只保留必要的基础数据。
另外,批量处理的程序最好是做成一个常驻进程,每处理完一个文件就退出当前线程或者清空局部作用域。这样即使某个文件内部状态异常,也不会影响下一个文件。
5. 性能调优与结果验证
5.1 用只读模式减少解析
读取文件时,可以指定只读模式,避免把一些编辑用的状态加载到内存里。Teigha的readFile有重载,传入打开模式参数时,可以控制是否以只读方式打开。只读模式除了更安全,速度上也会快一点,因为不需要建立可回滚的编辑记录。
如果只是做数据提取,强烈建议用只读模式。很多时候你只是拿坐标和文本,根本不需要写回原文件。
5.2 在实体层做过滤
遍历时,可以先用实体的通用类型判断过滤掉不关心的数据。比如你只关心文本和块引用,就可以对其它实体直接跳过。快速过滤可以用:
if (!pEntity->isKindOf(OdDbText::desc()) && !pEntity->isKindOf(OdDbMText::desc()) && !pEntity->isKindOf(OdDbBlockReference::desc())) continue;这样能避免对大量不需要的对象做类型转换,性能提升非常明显。有一次我处理一个超大图纸,有几万个多段线,但只需要其中的设备编号文本。过滤前用时几十秒,过滤后不到一秒。
5.3 多线程处理:不稳定但可用
Teigha不是完全线程安全的。我在项目里没有直接多线程解析同一个文件,而是用了最简单的策略:多个进程,每个进程处理一份文件。如果是多个线程同时解析不同的文件,有可能会在同一模块内部产生冲突。建议每个线程独立初始化和清理Teigha环境,避免共享全局状态。
曲线救国的方案是,把解析任务拆成多个独立进程,由主进程统一调度。每个子进程处理一份文件,把结果写入一个临时JSON文件,主进程收集后入库。这个方案虽然重一点,但稳定性和并发度都有保障。
5.4 结果验证
功能开发完之后,结果验证最重要。我做了两层校验:
- 抽了10张典型图纸,让设计师人工核对坐标和文本,确认提取结果和原图一致。
- 在数据库里做批量统计,比如每个图层的实体数量、文本总条数,和历史人工记录对比。
这一步发现了一个隐藏问题:有部分图纸里的圆,实际上是退化后的圆弧,半径是零。Teigha读取时也能正常返回,但后续计算时除零,导致整个任务崩溃。后来我在校验逻辑里额外加了一条规则,半径小于0.001的圆直接忽略。
6. 实际踩坑记录与经验心得
6.1 三个印象深刻的坑
第一个是版本不兼容。最开始用的旧版SDK,打开一个较新的DWG时直接报“无效的文件版本”。后来统一升级到新版,并把版本代号写进配置文件,才彻底解决。以后凡是遇到打不开的图纸,先确认SDK版本。
第二个是许可证。Teigha的许可证是绑定机器信息的。我在一台测试服务器上生成的授权文件,换到另一台机器就失效了。这个不是代码问题,但要提前规划好服务器,避免临时换机器导致整个服务不可用。
第三个是自定义实体丢失。某个设备图纸用得是第三方插件绘制的专用对象,用Teigha读取时变成了代理实体,业务属性全部丢失。后来让设计人员在原始图纸里把这些专用对象全部文字化成标准文本,才解决了问题。这个属于工作流程问题,单靠代码绕不过去。
6.2 给后来人的建议
如果从零开始接触Teigha,建议先把SDK自带的Examples编译一遍。每个示例对应一个核心功能,比如自定义实体、文件转换、渲染,都跑通之后再写自己的业务逻辑。这些示例代码本身是很好的参考,能少走很多弯路。
另外,项目里所有依赖Teigha的功能都做成了独立的命令行工具,方便单独调试。功能上线后,我们把解析工具的版本号也写进了数据库,方便追踪哪批数据是用哪个版本生成的。数据出问题时,能很快定位到是不是解析逻辑变了。
6.3 最后的经验技巧
再分享一个小技巧。Teigha读多行文本时,内容里经常混有格式控制符。在入库前,我写了一个清洗函数,把所有控制符去掉,只保留纯文本。这个函数很不起眼,但效果很好,直接解决了后台搜索时关键词匹配不到的问题。
还有一个心得是在批量处理时给每个图纸都加一个超时机制。某些巨大或损坏的图纸可能让解析进程卡住,没有超时的话,整个队列都会被拖死。我用的办法是:每个解析子进程最长运行5分钟,超过就杀掉重启,并记录失败日志。这样即使遇到几个顽固文件,也能保证整体进度不中断。
这次项目做完之后,我对“不要从零解析复杂格式”这个原则有了更深的体会。Teigha虽然学起来有点陡峭,但一旦把环境搭好、接口摸熟,后续的开发效率会高很多。如果你也正在被DWG文件折磨,希望这篇使用记录能帮你省一点时间。