- 图形学
【免费下载链接】ezdxf
Python interface to DXF
<导读> 本文以 ezdxf 官方参考文档 xref.rst 为主体,系统讲解ezdxf.xref模块(自 ezdxf 1.1 起引入)如何创建、检测、分离与嵌入外部引用(XREF),并深入其附带的数据导入能力——用load_modelspace、load_paperspace、write_block与低层Loader接口把外部 DXF 文件的实体与资源(图层、线型、文字样式等)搬进当前文档。读完本文,你将掌握 XREF 的定义结构、三种冲突处理策略(ConflictPolicy)的取舍,以及用几行代码完成"合并多个 DXF 文件""块定义外置为 XREF""XREF 内嵌为普通块"等高频实战任务。
</导读>
XREF 的基本概念:附着、嵌套与覆盖
在 DXF 语境中,附着式外部引用(Attached XREF)是指向某个绘图文件模型空间(modelspace)的链接:对被引用文件所做的修改,会在宿主文件下次打开或 XREF 重新加载时自动反映到当前图纸中。
XREF 支持三种典型的组合用法:
- 嵌套:一个 XREF 内部可以再附着另一个 XREF,即 XREF 可以层层套叠;
- 多次引用:同一个 XREF 可以被一个或多个 INSERT 实体反复引用,每次引用都可以拥有不同的位置(insert)、缩放(scale)与旋转(rotation);
- 覆盖(Overlay):与附着不同,覆盖式 XREF 在该文件被其他图纸作为 XREF 附着/覆盖时不会被传递。也就是说,overlay 只影响当前文档,不会随文档的再次引用而递归带入,适用于避免"环形引用"或控制引用传播链的场合。
关于 DXF 文件作为附着 XREF 的兼容性提醒
关联文档在开头就给出一个重要警告:
AutoCAD 只能把 DWG 文件显示为附着 XREF,而 ezdxf 只能生成 DXF 文件。因此,任何作为 XREF 附着到 DXF 文档的 DXF 文件,都必须先转换为 DWG 才能在 AutoCAD 中正常显示。
相比之下,BricsCAD 等 CAD 应用则宽松得多:BricsCAD 可以直接把 DXF 文件当作 XREF 显示(尽管 BricsCAD 应用本身不允许用户在界面里直接附着 DXF 文件)。源码注释也印证了这一点(见 src/ezdxf/xref.py 第 157-171 行):AutoCAD 甚至对 ezdxf 生成的 DXF R2000+ 文件也不予解析,而 "BricsCAD accepts any DXF/DWG file as XREF"。示例 examples/xref/attach_dxf_dwg_xref.py 中的export_dwg_xref_document()正是演示了通过odafc.export_dwg()把 DXF 导出为 DWG 后再附着,这是面向 AutoCAD 场景的推荐做法。
XREF 的文档结构:带标志的普通块定义
从 DXF 结构看,一个 XREF 本质上就是BLOCKS 段中的一个普通块定义(block definition),只是它带有特殊的标志位(flags)和一个指向外部文件的路径(xref_path),且块内没有内容——块的"内容"就是被引用文件的模型空间。它可以被一个或多个 INSERT 实体引用。
对应的标志位定义在 src/ezdxf/lldxf/const.py:
| 常量 | 值 | 含义 |
|---|---|---|
BLK_XREF | 4 | 附着式外部引用 |
BLK_XREF_OVERLAY | 8 | 覆盖式外部引用 |
BLK_EXTERNAL | 16 | 外部引用(外部文件)标志 |
ezdxf.xref内部的make_xref_flags()(src/ezdxf/xref.py 第 242-246 行)会按 overlay 与否组合出BLK_XREF | BLK_EXTERNAL或BLK_XREF_OVERLAY | BLK_EXTERNAL作为块定义标志。
检测既有文档中的 XREF
关联文档给出了两种查找模式。第一种是在 BLOCKS 段遍历块定义:
for block_layout in doc.blocks: block = block_layout.block # 即 BLOCK 实体 if block.is_xref: handle_xref(block_layout) elif block.is_xref_overlay: handle_xref_overlay(block_layout)Block.is_xref与Block.is_xref_overlay属性实现在 src/ezdxf/entities/block.py 第 181-189 行,二者分别检查块的BLK_XREF/BLK_XREF_OVERLAY标志位。
第二种是在模型空间中查找引用 XREF 的 INSERT 实体:
for insert in msp.query("INSERT"): if insert.is_xref: handle_xref_reference(insert) # ... 或者获取 XREF 的定义 block_layout = insert.block() if block_layout is not None: handle_xref_definition(block_layout)Insert.is_xref()(src/ezdxf/entities/insert.py 第 280-285 行)内部通过self.block()取回关联的BlockLayout,再委托给block_record.is_xref判断,因此它同时覆盖 XREF 与 XREF_OVERLAY 两种情形。
如果要在宿主文档中手动创建自己的 XREF 定义,可以使用xref.define():它以指定名称在 BLOCKS 段新建一个带 XREF 标志与xref_path的块定义;当同名块已存在时抛出XrefDefinitionError。而attach()会自动调用define()完成定义创建,因此直接使用attach()时若块已存在同样会抛出该异常。
高层函数实战
ezdxf.xref模块对外提供以下高层函数(全部实现在 src/ezdxf/xref.py):
| 函数 | 用途 |
|---|---|
attach | 把 DXF/DWG 文件作为 XREF 附着到宿主文档 |
define | 创建 XREF 块定义(不创建块引用) |
detach | 把块定义分离为 XREF(反向操作见embed) |
embed | 把 XREF 内容内嵌为一个普通块定义 |
dxf_info | 扫描 DXF 文件的基本设置与属性 |
load_modelspace | 从另一份 DXF 文档加载模型空间内容 |
load_paperspace | 从另一份 DXF 文档加载某个图纸空间布局 |
write_block | 把实体写入一个新 DXF 文档的模型空间 |
加载 DWG 文件内容需要一个能把 DWG 读成Drawing文档的加载函数,官方推荐使用ezdxf.addons.odafc插件提供的odafc.readfile(详见 odafc 插件)。
attach:附着外部引用
attach的完整签名(src/ezdxf/xref.py 第 249-305 行):
def attach( doc: Drawing, *, block_name: str, filename: str, insert: UVec = (0, 0, 0), scale: float = 1.0, rotation: float = 0.0, overlay=False, ) -> Insert:doc:宿主 DXF 文档;block_name:XREF 定义块的名称;filename:被引用文件路径;insert:默认块引用的插入点,默认(0, 0, 0);scale:统一缩放因子,默认1.0(非 1.0 时同时设置xscale/yscale/zscale);rotation:旋转角(度),默认0.0;overlay:为True时创建覆盖式 XREF,否则为附着式。
该函数在模型空间中自动创建默认块引用并返回该Insert实体。若要为同一个 XREF 追加更多引用,直接复用块引用方法即可:
msp.add_blockref(block_name, insert=another_location)来自 docs/source/tutorials/xref_module.rst 的典型调用:
host_doc = ezdxf.new(DXFVERSION, units=units.M) xref.attach(host_doc, block_name="dxf_xref", insert=(0, 0), filename="attached_xref.dxf") host_doc.set_modelspace_vport(height=10, center=(0, 0)) host_doc.saveas("attach_host_dxf.dxf")完整的可运行示例见 examples/xref/attach_dxf_dwg_xref.py,它同时演示了新旧两种 API:v1.1 之前用doc.add_xref_def()+msp.add_blockref(),v1.1 之后统一由xref.attach()一站式完成。
单位换算提醒(重要):如果 XREF 与宿主文档的绘图单位不同,必须在块引用上以统一缩放因子补上两种单位之间的换算比例。attach()不会自动做这件事,因为单位只能通过dxf_info()扫描被引用文件的 HEADER 段得知。官方建议:同一项目内所有图纸尽量使用同一单位。
dxf_info:扫描 DXF 文件基本信息
dxf_info(filename)只扫描 HEADER 段(不加载整个文档),返回一个DXFInfo对象,包含 DXF 版本、文本编码、绘图单位与插入基点($INSBASE)。二进制 DXF 与文本 DXF 分别由validator.binary_dxf_info()与validator.dxf_info()处理;若文件不是 DXF,抛出IOError。集成测试 integration_tests/test_dxf_info_scanning.py 中有现成的调用范例。
detach:把块定义分离为 XREF
detach(block, *, xref_filename, overlay=False)(src/ezdxf/xref.py 第 413-449 行)执行的是与附着相反的操作:把指定块定义的内容写入一个新 DXF 文档的模型空间,然后把原块转换为外部引用。新文档需要由调用方自行saveas()写出;如需 DWG 可以交给 odafc 转换:
xref_doc = xref.detach(my_block, "my_block.dwg") odafc.export_dwg(xref_doc, "my_block.dwg")几点关键细节:
- 分离操作不会创建任何块引用——因为块与 XREF 的引用方式相同,已存在的引用无需改动;
- 新文档的 DXF 版本、单位、$INSBASE(基点为原块基点)都会与源块保持一致;
- 分离后建议清理宿主文档的实体数据库:
doc.entitydb.purge(); - 分离后宿主文档被修改,务必保存宿主文档;
- 若要把结果喂给 AutoCAD,应保证
xref_filename与最终导出的 DWG 文件名一致。
完整的 DXF/DWG 双路径示例见 examples/xref/detach_block_as_xref.py,集成测试 integration_tests/test_xref_detach.py 验证了分离后新文档的 DXF 版本、单位、$INSBASE 以及全部块实体与资源(图层、ATTRIB 模板等)均正确保留。
embed:把 XREF 内嵌为普通块
embed(xref, *, load_fn=None, search_paths=(), conflict_policy=ConflictPolicy.XREF_PREFIX)(src/ezdxf/xref.py 第 340-410 行)把 XREF 引用的文件内容加载进该块定义,是detach的逆操作:
- 默认用
ezdxf.readfile加载 DXF;加载 DWG 需传load_fn=odafc.readfile;对于有错误的 DXF 文件,可用load_fn=lambda f: recover.readfile(f)[0]; - 当
xref_path是相对路径时,会依次在宿主文档所在目录及search_paths指定的目录中查找(查找逻辑见find_xref(),src/ezdxf/xref.py 第 308-337 行); - 若被引用文件的 DXF 版本高于宿主文档,抛出
DXFVersionError(可先用 odafc 降级 XREF,或升级宿主文档); - 加载成功后块定义会清除 XREF 标志(
BLK_XREF | BLK_EXTERNAL),并把块的基点更新为源文档的$INSBASE,使内嵌后的插入行为与原 XREF 一致。
示例用法(摘自 docs/source/tutorials/xref_module.rst):
import ezdxf from ezdxf.addons import odafc doc = ezdxf.readfile("attach_host_dwg.dxf") gear_xref = doc.blocks.get("GEAR") try: xref.embed(gear_xref, load_fn=odafc.readfile) except FileNotFoundError as e: print(str(e))完整示例见 examples/xref/embed_dxf_dwg_xref.py。
write_block:写出可作 XREF 的独立文档
write_block(entities, *, origin=(0, 0, 0))(src/ezdxf/xref.py 第 466-498 行)把一组实体写入一个新 DXF 文档的模型空间,功能类似 CAD 中的WBLOCK命令:新文档可以(但不必须)当作外部引用块使用。origin定义块基点——即模型空间中的哪个点会被对齐到块引用的插入点。注意:虚拟实体(virtual entity)不被支持,因为每个实体都需要真实的数据库与所有者句柄;传入空列表时返回一个空的新文档。
支持的实体范围
当前实现只支持可复制、可变换的 DXF 实体,即 LINE、CIRCLE 等全部基础实体类型,以及块引用和与之关联的必需表项、OBJECTS 段对象。来自教程 docs/source/tutorials/xref_module.rst 的补充说明指出,所有在布局与 XREF 之间搬运实体的操作本质上是复制,因此以下不可复制实体无法传输:
- DXF 参考手册未记载的实体;
- ACAD_TABLE;
- ACAD_PROXY_ENTITY;
- OLE2FRAME;
- 早期版本中的 ACIS 实体(BODY、3DSOLID、REGION 等);
- 基于 AutoCAD 垂直产品(Map 3D、Civil 3D、Architecture 等)的自定义实体。
关联文档还明确指出:不支持被包装在DXFTagStorage中的保留未知实体(如代理实体与对象);不支持的实体一律忽略且不抛异常。大多数存放在 HEADER 与 OBJECTS 段的文档特性也不受支持,例如 GROUPS、LAYER_FILTER、GEODATA、SUN。
需要特别留意的是版本演进:ezdxf 1.3.0 起已加入对 ACIS 实体的支持,对应集成测试见 integration_tests/test_xref_load_acis.py,示例见 examples/xref/load_and_transform_acis_entities.py。
从外部文件导入数据与资源
ezdxf.xref模块的另一个重要使命是替代旧的ezdxf.addons.importer.Importer插件。用户经常需要从其他 DXF 文件把数据导入当前文档,而Importer插件能力有限、会丢弃大量资源;ezdxf.xref则尽力保留尽可能多的信息。
load_modelspace(sdoc, tdoc, filter_fn=None, conflict_policy=KEEP):把源文档模型空间内容加载进目标文档模型空间,filter_fn可对每个源实体返回True(加载)或False(跳过);load_paperspace(psp, tdoc, filter_fn=None, conflict_policy=KEEP):把源文档的某个图纸空间布局作为新布局加载进目标文档;同名布局会被自动改名为<名称> (2)、<名称> (3)……;只加载图纸空间内容,不加载模型空间内容,因此加载进来的 VIEWPORT 显示的是目标文档的模型空间;write_block:见上文。
合并多个 DXF 文件的经典场景(docs/source/tutorials/src/xref/merge_dxf_files.py):
import ezdxf from ezdxf import transform, xref from ezdxf.math import Matrix44 merged_doc = ezdxf.new() for index in range(3): sdoc = ezdxf.readfile("gear.dxf") # 可以是不同的 DXF 文件 transform.inplace(sdoc.modelspace(), Matrix44.translate(index * 10, 0, 0)) xref.load_modelspace(sdoc, merged_doc) merged_doc.saveas("merged.dxf")教程提示:如果目标只是往既有文档追加新实体,更推荐直接ezdxf.readfile把源文档作为模板读取、追加内容后saveas另存;load_modelspace更适合"组合多个已有 DXF 文件"这类场景。
ConflictPolicy:资源名冲突处理策略
这里的"资源"指图层、线型、文字样式、标注样式、MLINE 样式、MLEADER 样式、材质与块等定义。当源文档与目标文档存在同名资源时即发生冲突。ConflictPolicy枚举(src/ezdxf/xref.py 第 101-121 行)提供三种策略:
| 策略 | 行为 | 适用场景 |
|---|---|---|
ConflictPolicy.KEEP | 保留目标文档中的现有资源,忽略源文档的同名资源;加载的实体改用目标文档中的资源(外观可能因资源不同而变化) | 默认策略,保守、稳定 |
ConflictPolicy.XREF_PREFIX | 总是把加载的资源重命名为<xref>$0$<name>,xref为源文档名,$0$为生成唯一名的数字,<name>为原名 | 模拟 CAD 应用的导入行为;即使无冲突也强制重命名 |
ConflictPolicy.NUM_PREFIX | 仅当资源名在目标文档已存在时,重命名为$0$<name> | 只在冲突时改名,最大限度保留原名 |
以下系统级资源名永远被 KEEP(保留),不参与重命名(见 src/ezdxf/xref.py 第 147-155 行注释及_Transfer的实现):
- 资源
STANDARD; - 材质
GLOBAL、BYLAYER、BYBLOCK; - 打印样式
NORMAL; - 图层
0、DEFPOINTS及 Autodesk 特殊图层(以*开头); - 线型
CONTINUOUS、BYLAYER、BYBLOCK; - 箭头等特殊块;
- 匿名块会被赋予符合匿名块命名规则的新名字(如
*D01→*D0815)。
冲突策略的逐项行为在测试 tests/test_05_tools/test_536_xref_conflict.py 中有系统验证;集成测试 integration_tests/test_issue_1359_xref_keep.py 则验证了 KEEP 策略下合并文档不产生冗余资源。
Loader:低层加载接口
Loader类是构建一切加载操作的基础积木(src/ezdxf/xref.py 第 742-1002 行)。它管理一组加载命令(loading commands),命令通过loader.execute()一次性执行:
loader = xref.Loader(sdoc, tdoc) # 默认冲突策略为 KEEP loader.load_modelspace() # 或 load_paperspace_layout / load_block_layout ... loader.execute()为什么必须一次性执行?因为资源名映射(重命名)必须全局一致:如果分多次执行,即使资源来自同一源文档,每次加载也会生成不同的唯一名,导致同名资源出现多个副本。execute(xref_prefix="")中的xref_prefix正是XREF_PREFIX策略用于生成<xref>$0$<name>前缀的源文档名。
Loader支持下列加载命令方法:
| 方法 | 行为 |
|---|---|
load_modelspace(target_layout=None, filter_fn=None) | 把源模型空间内容加载进目标布局(默认目标文档模型空间,也可以是图纸空间或块布局) |
load_paperspace_layout(psp, filter_fn=None) | 把源图纸空间布局作为新布局加载,同名自动改名为<名称> (2)等 |
load_paperspace_layout_into(psp, target_layout, filter_fn=None) | 把源图纸空间内容合并进目标文档的某个既有布局 |
load_block_layout(block_layout) | 把源块定义作为新块定义加载,冲突时应用冲突策略 |
load_block_layout_into(block_layout, target_layout) | 把源块定义内容合并进目标文档的某个既有布局 |
load_layers(names) | 按名称列表加载图层表项 |
load_linetypes(names) | 按名称列表加载线型表项 |
load_text_styles(names) | 按名称列表加载文字样式表项 |
load_dim_styles(names) | 按名称列表加载标注样式表项(会连带加载其依赖的文字样式) |
load_mline_styles(names) | 按名称列表加载 MLINE 样式 |
load_mleader_styles(names) | 按名称列表加载 MLEADER 样式 |
load_materials(names) | 按名称列表加载材质 |
以 examples/xref/load_table_resources.py 为例,加载表资源的完整流程:
loader = xref.Loader(source_doc, target_doc) # 加载全部图层:默认图层 "0" 和 "DEFPOINTS" 不会被加载,它们总是存在于目标文档! loader.load_layers([layer.dxf.name for layer in source_doc.layers]) # 加载指定线型:"GAS" 会因被图层 "Layer0" 引用而自动加载,额外指定也无害 loader.load_linetypes(["CENTER", "DASHED", "DASHDOT"]) # 加载指定文字样式 loader.load_text_styles(["OpenSans", "LiberationMono"]) # 加载全部标注样式,会连带加载依赖的文字样式 loader.load_dim_styles([dimstyle.dxf.name for dimstyle in source_doc.dimstyles]) # 一次性执行全部加载命令 loader.execute()一个值得注意的语义:加载图层并不会加载引用该图层的实体——图层只是 DXF 属性,不是实体容器。
底层执行流程(Loader.execute()内部)依次为:注册命令资源 → 用CopyMachine复制块与对象 → 通过_Transfer注册表资源与对象资源 → 重定向句柄映射(把复制实体的句柄重定向到目标文档既有资源)→ 映射实体/对象的资源引用 → 复制文档设置 → 依次执行各命令 →finalize()。同时,Loader构造时若目标文档 DXF 版本低于源文档会发出警告日志;源与目标不能是同一文档(断言)。
异常体系
ezdxf.xref定义了以下异常(src/ezdxf/xref.py 第 79-98 行):
| 异常 | 触发条件 |
|---|---|
XrefError | 模块基础异常 |
XrefDefinitionError | 同名块定义已存在(define/attach)、参数不是 XREF 定义或未定义xref_path(embed) |
EntityError | 传入虚拟实体(write_block)等实体级错误 |
LayoutError | 目标/源布局类型非法、布局不属于对应文档(如load_paperspace的源布局来自目标文档)等 |
此外还会向上抛出标准的FileNotFoundError(XREF 文件找不到)、DXFVersionError(XREF 版本高于宿主文档)与IOError(dxf_info遇到非 DXF 文件)。错误处理测试集中在 tests/test_05_tools/test_535_xref_basic.py 与 tests/test_05_tools/test_536_xref_conflict.py。
小结:XREF 工作流的四类典型任务
| 任务 | 推荐 API |
|---|---|
| 把外部 DXF/DWG 附着为 XREF | xref.attach(doc, block_name=..., filename=...) |
| 把块定义外置为独立 DXF/DWG 再引用 | xref.detach(block, xref_filename=...)+odafc.export_dwg() |
| 把 XREF 内容固化进当前文档 | xref.embed(block, load_fn=...) |
| 跨文档搬运实体与资源 | xref.load_modelspace/load_paperspace/write_block,或低层xref.Loader组合各load_*命令 |
所有高层函数均基于Loader构建,统一由execute()一次性执行以保证资源重命名的一致性;冲突处理由ConflictPolicy统一收口。进一步深入学习可参阅官方教程 docs/source/tutorials/xref_module.rst、xref 参考文档、examples/xref 示例目录 以及源码 src/ezdxf/xref.py。
- 图形学
【免费下载链接】ezdxf
Python interface to DXF
相关推荐
ezdxf Importer 插件使用指南:跨 DXF 文件实体导入与资源迁移实战
ezdxf Importer 插件使用指南:跨 DXF 文件实体导入与资源迁移实战 导读 本文聚焦 ezdxf 的 Importer 插件( ezdxf.add
图形学gridstack.js React 封装设计决策:双组件架构、静态回调与 Portal 渲染方案详解
gridstack.js React 封装设计决策:双组件架构、静态回调与 Portal 渲染方案详解 导读 本文档基于 react/DESIGN DECISI
图形学Weave Net 动态挂载与卸载容器:weave attach / detach 完整实战指南
Weave Net 动态挂载与卸载容器:weave attach / detach 完整实战指南 Weave Net 支持在容器已经运行之后,动态地将其挂载(a
网络云原生
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考