---
# System prepended metadata

title: 'DWG to DXF & DXF 版本對比 '

---

## DWG to DXF & DXF 版本對比

因為 dwg 檔什麼都封閉，所以要先轉成 dxf 檔比較好進行後續

轉換的話需要先下載 **ODA FILE CONVERTER** (免費)

## 1\. Python 函式庫

  * `subprocess`：內建函式庫，負責執行外部系統命令（呼叫 ODA)
  * `os`：Python 內建函式庫，負責處理檔案路徑、目錄操作
  * `ezdxf`：關鍵函式庫，負責一切跟 dxf 檔有關的操作

-----

### 函式拆解：`convert_dwg_to_dxf`

先將單個 DWG 檔案轉換為 DXF 檔案。

#### 函式

```python
def convert_dwg_to_dxf(dwg_path, output_dir):
```

| 參數 | 類型 | 說明 |
| :--- | :--- | :--- |
| `dwg_path` | str | 來源 `.dwg` 檔案的完整路徑。 |
| `output_dir` | str | 轉換後 `.dxf` 檔案的目標輸出資料夾。 |
| **回傳** | str / None | 成功時回傳新生成的 `.dxf` 檔案路徑；失敗時回傳 `None`。 |

#### 程式解析

| 行為 | 程式碼片段 | 說明 |
| :--- | :--- | :--- |
| **路徑準備** | `input_dir = os.path.dirname(os.path.abspath(dwg_path))` | 提取輸入 DWG 檔案的**所在資料夾**，作為 ODA Converter 的輸入目錄。 |
| **參數建構** | `command = [...]` | 建立傳遞給 ODA Converter 的位置參數。必須按照 ODA 要求的順序：<br>1. 執行檔路徑 (被引號包圍)<br>2. 輸入目錄 (`input_dir`)<br>3. 輸出目錄 (`output_dir`)<br>4. 輸出版本 (`ACAD2018`)<br>5. 輸出格式 (`DXF`)<br>6. 遞迴選項 (`0` = 否)<br>7. 檔案稽核 (`0` = 否) |
| **命令組合** | `full_command = " ".join(command)` | 將參數列表用空格連接，形成單一命令字串，準備給 Shell 執行。 |
| **命令執行** | `result = subprocess.run(..., check=True, shell=True)` | call 外部程式。`shell=True` 用於正確解析帶有引號和空格的路徑。<br>`check=True` 確保有問題時有辦法 debug |
| **結果輸出** | `print("DWG 轉換成功")` | 轉換成功，並根據輸入檔名重新命名 DXF ，然後回傳路徑。 |
| **錯誤處理** | `except subprocess.CalledProcessError as e:` | Debug 用。 |

-----

###  範例

```python
# 替換成本機 DWG 檔案路徑
DWG_FILE_PATH = r"C:\path\to\your\drawing.dwg" 

dxf_output_path = convert_dwg_to_dxf(DWG_FILE_PATH, OUTPUT_FOLDER)

if dxf_output_path:
    print(f"檔案已轉換並儲存於: {dxf_output_path}")
```
----
## 2\. 從 DXF 檔裡抓資訊

CAD 圖紙的物件通常都被打包在 **Blocks** 或 **XREFs** 中。
所以第二步要透過函式從 DXF 裡面把這些資訊抓出來

### 函式拆解：`extract_block_features`

因為這個函式是遞迴執行的，因此它不直接返回結果，而是修改傳入的容器（`entity_type_counts` 和 `text_content_list`）。

```python
def extract_block_features(block, entity_type_counts, text_content_list, max_text=40):
```

| 參數 | 類型 | 說明 |
| :--- | :--- | :--- |
| `block` | ezdxf Block/Layout | 目前的迭代容器 |
| `entity_type_counts` | defaultdict | 累計各類型物件數量 |
| `text_content_list` | list | 收集檔案中文字註記的列表|
| `max_text` | int | 限制文字條數，避免列表過長 |

### 程式解析

| 行為 | 程式碼片段 | 說明 |
| :--- | :--- | :--- |
| **圖元計數** | `entity_type_counts[entity_type] += 1` | 記錄所有物件數量，後續提供給 LLM 提供圖元類型分佈的數據 |
| **處理 `INSERT`** | `if entity_type == 'INSERT':` | 檢查圖元是否為塊的引用（Block Reference）。用來觸發遞迴。 |
| **獲取 Block 定義** | `block_layout = entity.doc.blocks.get(block_name)` | 獲取被引用塊的內容 (`BlockLayout`)。<br>`block_record = entity.doc.block_records.get(block_name)` | 獲取被引用塊的定義資訊 (`BlockRecord`)。 |
| **XREF 檢查** | `if block_record.dxf.flags & 2 or block_record.dxf.flags & 16:` | 透查 `BlockRecord` 的 `.dxf.flags` 屬性，判斷該塊是否為外部參考 (XREF)。<br>值 `2` 或 `16` 表示是 XREF。<br>如果是 XREF，跳過以避免處理外部檔案。 |
| **遞迴呼叫** | `extract_block_features(block_layout, ...)` | 如果不是 XREF，則將 `block_layout`（塊的內容）傳遞給自己，進入下一層 Block 繼續解析。 |
| **文字提取** | `elif entity_type in ('TEXT', 'MTEXT', 'ATTRIB'):` | 針對文字、多行文字和屬性定義，提取其內容、圖層名稱，並格式化儲存到 `text_content_list` 中供 LLM 分析。 |
| **錯誤處理** | `try...except ValueError` / `except Exception` | 捕捉在獲取 Block 定義時可能發生的錯誤（例如：名稱不存在），防止程式在複雜或損壞的檔案結構中崩潰。 |

好的，這是關於 `generate_entity_hash` 函式的 HackMD 筆記草稿，它詳述了幾何比對的核心機制。

-----
## 3\. 提取 Hash String 
因為物件數量太多，一一辨識前後差異不太可行，所以透過 Hash String 來進行對比會精確的多

### 函式拆解：`generate_entity_hash`

```python
def generate_entity_hash(entity, precision=6):
```

| 參數 | 類型 | 說明 |
| :--- | :--- | :--- |
| `entity` | ezdxf Entity | 要生成 Hash 的單個幾何圖元。 |
| `precision` | int | **浮點數精度**。這是確保 Hash 穩定的，避免因浮點數計算誤差（例如 `1.000000001` vs `1.000000002`）而誤將相同線條判定為不同。 |
| **回傳** | str | 該圖元的唯一 Hash 字串。 |

### 程式碼核心邏輯解析

| 行為 | 程式碼片段 | 說明 |
| :--- | :--- | :--- |
| **基礎屬性** | `key_attrs = [entity_type, layer]` | 每個物件的 Hash 都包含其類型 (`LINE`, `TEXT` 等) 和所在圖層 (`layer`)。最基本的差異維度比較 |
| **確保坐標四捨五入** | `round_coord(coord)` | **Hash 穩定性**：由於 CAD 坐標是浮點數，此函式強制將所有坐標四捨五入到指定的 `precision` (例如 6 位小數)，避免微小的計算誤差破壞 Hash 穩定性。 |
| **LINE 處理** | `key_attrs.append(round_coord(...)); key_attrs.sort(key=str)` | 針對直線：擷取**起點**和**終點**的圓整坐標。**`key_attrs.sort()`** 是必要的，因為 `(start, end)` 和 `(end, start)` 應該是同一條線，排序能確保兩者產生相同的 Hash。 |
| **LWPOLYLINE 處理** | `key_attrs.append(len(entity.get_points()))` | 針對複雜的輕量多段線：出於 PoC 簡化目的，我們只使用**點的數量**和**圖層**作為 Hash 因子。在生產環境中，需要將所有頂點的圓整坐標都包含進去。 |
| **TEXT 處理** | `key_attrs.append(entity.dxf.text.strip())` | 針對文字：擷取**文字內容**和**插入點坐標**。文字內容必須去除空白 (`.strip()`) 以防止 Hash 被無關的格式變更影響。 |
| **最終 Hash** | `return str(hash(tuple(key_attrs)))` | 將所有收集到的關鍵屬性轉換為一個不可變的 `tuple`，然後使用 Python 內建的 `hash()` 函式生成最終的唯一 Hash 字串。 |

### 幾何比對原理

在生成了每個圖元的 Hash 後，比較兩個版本（V1 和 V2）的過程如下：

1.  **轉換為集合 (Set)：** 將 V1 和 V2 的 Hash 列表轉換為集合或字典。
2.  **新增 (綠色)：** Hash 存在於 V2 集合，但不存在於 V1 集合。
3.  **刪除 (紅色)：** Hash 存在於 V1 集合，但不存在於 V2 集合。
4.  **修改 (Modifications)：** 任何圖元的**單一屬性**（例如，一條線的坐標或圖層）發生變化，都會產生一個新的 Hash 值。這會被比對邏輯視為 **V1 刪除 (紅)** 和 **V2 新增 (綠)** 的組合。

好的，這是關於 `_recursive_hash_extraction` 函式的 HackMD 筆記草稿，它接續了前面的筆記，並著重於解釋其遞迴和穩健性處理。

-----
## 4\.  遞迴抓取物件 Hash 值

**核心函式**。透過遞迴（自動追蹤和進入 Block 內部），全面提取 CAD 文件中所有**需要比對**的物件，並為每個物件生成穩定 Hash 值。

### 函式詳解：`_recursive_hash_extraction`

遞迴輔助函式，為了解決 CAD 的巢狀（Nested）結構問題。

```python
def _recursive_hash_extraction(block, all_entities):
```

| 參數 | 類型 | 說明 |
| :--- | :--- | :--- |
| `block` | ezdxf Block/Layout | 當前的迭代容器（Modelspace, Paperspace, 或 Block 定義）。 |
| `all_entities` | list | 收集所有提取到的物件（包含 Hash）列表。 |

### 程式碼核心邏輯解析

| 行為 | 程式碼片段 | 說明 |
| :--- | :--- | :--- |
| **類型檢查** | `if not isinstance(block, (...)): return` | **終止條件** 檢查傳入的 `block` 是否為有效的 CAD 容器類型 (`Modelspace`, `Paperspace`, `BlockLayout`)。若不是，則立即停止遞迴，防止程式進入無效或無限迴圈。 |
| **選擇迭代器** | `iterator = block.entities if hasattr(block, 'entities') else block` | **兼容性修復** <br>在 `ezdxf` 中，Modelspace 和 BlockLayout 的迭代方式不同。此行自動選擇：<br>• 如果是 Block 定義，則使用 `.entities` 屬性（更穩定的迭代）。<br>• 如果是 Modelspace/Paperspace，則直接使用 `block` 物件本身進行迭代。 |
| **處理 `INSERT`** | `if entity_type == 'INSERT': ...` | 遇到 Blocks 引用時，嘗試獲取其定義 (`block_def`)。這是遞迴的核心，**移除 XREF 檢查** (簡化和排除錯誤)。 |
| **遞迴呼叫** | `_recursive_hash_extraction(block_def, all_entities)` | 程式呼叫自身，進入 Block 內部，繼續提取下一層的物件。 |
| **核心圖元過濾** | `elif entity_type in [...]` | 僅處理需要進行幾何比對的物件：`LINE`, `LWPOLYLINE`, `ARC`, `CIRCLE`, `TEXT`, `MTEXT`。忽略不需要比對的物件（如 `VIEW`, `DIMSTYLE` 等）。 |
| **Hash 與收集** | `entity_hash = generate_entity_hash(entity)` | 呼叫前面的函式，為物件生成 Hash。<br>`entity_object': entity.copy()` | 儲存圖元的一個**副本**。這是絕對必要的，因為我們稍後需要在新文檔中添加差異圖元，且不能修改舊文檔的原始圖元。 |
| **錯誤容錯** | `try...except Exception:` | 每個遞迴步驟和 Hash 生成都被 `try...except` 包圍。這樣在遇到單個**損壞的 Block** 或**錯誤的圖元數據**時，不會全部 crash |

-----

## 5\. 深度特徵提取

`extract_deep_features` 函式是 CAD 解析流程中的**啟動器**。它的職責是：

1.  讀取指定的 DXF 檔案。
2.  找到檔案的起點：**Modelspace**（模型空間，即 CAD 圖紙的主要繪圖區域）。
3.  呼叫遞迴提取引擎 (`_recursive_hash_extraction`)，開始全面解析圖元。

### 函式詳解：`extract_deep_features`

此函式是整個數據提取工作的**門面**，它封裝了 `ezdxf` 的檔案讀取和啟動邏輯。

```python
def extract_deep_features(dxf_path):
```

| 參數 | 類型 | 說明 |
| :--- | :--- | :--- |
| `dxf_path` | str | 要分析的 DXF 檔案的完整路徑。 |
| **回傳** | list | 包含所有提取到的幾何圖元數據（包括其 Hash、類型、圖層等）的列表。 |

### 程式碼核心邏輯解析

| 行為 | 程式碼片段 | 說明 |
| :--- | :--- | :--- |
| **讀取檔案** | `doc = ezdxf.readfile(dxf_path)` | 使用 `ezdxf` 庫讀取並解析指定的 DXF 檔案，將整個檔案結構載入到 `doc` (Document) 物件中。 |
| **定位模型空間** | `msp = doc.modelspace()` | 獲取 CAD 檔案中最重要的繪圖空間：**模型空間 (Modelspace)**。這是所有主要幾何圖元、線條和 Block 引用的所在之處，是遞迴的起點。 |
| **準備容器** | `all_entities = []` | 初始化一個空的列表，這個列表將會被傳遞給遞迴函式，最終用來存放所有被提取出來的圖元數據。 |
| **啟動遞迴** | `_recursive_hash_extraction(msp, all_entities)` | 將 Modelspace 作為起點，呼叫模組四中定義的遞迴函式。該函式會自動遍歷 Modelspace 中的所有圖元和巢狀 Blocks，並將結果填充到 `all_entities` 列表中。 |
| **回傳結果** | `return all_entities` | 返回已填充完畢的列表。這個列表包含了 V1 或 V2 版本中所有需要比對的幾何圖元的 Hash 數據，供下一步的 `compare_hashes` 函式使用。 |

### 流程定位

此函式在整個比對流程中的位置：

1.  **`extract_deep_features(V1_Path)`** → 得到 `entities_v_old` 列表。
2.  **`extract_deep_features(V2_Path)`** → 得到 `entities_v_new` 列表。
3.  `compare_hashes(entities_v_old, entities_v_new)`

好的，這是關於 `export_dxf_to_png` 函式的 HackMD 筆記草稿，用於解釋如何將生成的差異 DXF 檔案轉換為圖像。

-----
## 6\. 差異視圖生成 (`export_dxf_to_png`)

 PoC 流程的最後一步，負責實現用可視化方式呈現版本差異
 此函式使用 `ezdxf` 的 `drawing` 附加組件將 DXF 向量數據渲染成 PNG 檔。

### 函式詳解：`export_dxf_to_png`

此函式將前面生成的 **差異 DXF** 轉換為 PNG 圖像。

```python
def export_dxf_to_png(dxf_path, png_output_path):
```

| 參數 | 類型 | 說明 |
| :--- | :--- | :--- |
| `dxf_path` | str | 來源 `.dxf` 檔案（通常是 `DIFF_... .dxf`）的路徑。 |
| `png_output_path` | str | 輸出 `.png` 圖像的路徑。 |

### 程式碼核心邏輯解析

| 行為 | 程式碼片段 | 說明 |
| :--- | :--- | :--- |
| **讀取與定位** | `doc = ezdxf.readfile(dxf_path)`<br>`msp = doc.modelspace()` | 讀取 DXF 檔案，並定位到包含差異圖元的**模型空間** (`msp`)。 |
| **初始化 Matplotlib** | `fig = plt.figure(figsize=(12, 9))` | 建立畫布 |
| **隱藏坐標軸** | `ax.axis('off')` | 這樣看起來比較像 CAD 的圖 |
| **建立渲染管道** | `ctx = RenderContext(doc)`<br>`out = MatplotlibBackend(ax)` | **渲染設定：**<br>• `RenderContext (ctx)`：包含圖紙的顏色、線型、字體等全局定義。<br>• `MatplotlibBackend (out)`：定義渲染的目標為 Matplotlib 軸。 |
| **執行繪製** | `Frontend(ctx, out).draw_layout(msp, finalize=True)` | 使用 `Frontend` 將 DXF 物件數據 (`ctx`) 轉換為 Matplotlib 的圖形繪製指令 (`out`)。<br>`finalize=True` 確保繪製範圍正確，並包含所有物件 |
| **匯出與清理** | `fig.savefig(png_output_path, dpi=300, ...)`<br>`plt.close(fig)` | 存檔； `bbox_inches='tight'`：移除圖像周圍多餘的空白邊界。<br>**清理：** 關閉圖形，釋放記憶體。 |
| **錯誤處理** | `except Exception as e:` | Debug 用 |

-----
## 7\. Hash 幾何比對 (`compare_hashes`)

此函式接收兩個版本物件 Hash 列表，透過集合運算快速找出**新增**或**刪除**的物件。

### 函式拆解：`compare_hashes`

```python
def compare_hashes(v1_entities, v2_entities):
```

| 參數 | 類型 | 說明 |
| :--- | :--- | :--- |
| `v1_entities` | list | 舊版圖元的 Hash 列表（包含物件數據）。 |
| `v2_entities` | list | 新版圖元的 Hash 列表（包含物件數據）。 |
| **回傳** | tuple (list, list) | `(added, deleted)` 兩個列表，分別包含新增（綠色）和刪除（紅色）的圖元物件數據。 |

### 程式碼核心邏輯解析

| 行為 | 程式碼片段 | 說明 |
| :--- | :--- | :--- |
| **優化比對** | `v1_hashes = {e['hash']: e for e in v1_entities}` | 將列表轉換為**字典**，以 Hash 字符串作為 Key。將比對複雜度從 O(N²) 降低到 O(N) |
| **初始化差異** | `added = []`<br>`deleted = []` | 收集比對結果。 |
| **尋找新增 (綠色)** | `if h not in v1_hashes: added.append(e2)` | 迭代 V2 的所有 Hash。如果 V2 的 Hash 不存在於 V1 的 Hash 字典中，則表示這是 V2 **新增**的圖元。 |
| **尋找刪除 (紅色)** | `if h not in v2_hashes: deleted.append(e1)` | 迭代 V1 的所有 Hash。如果 V1 的 Hash 不存在於 V2 的 Hash 字典中，則表示這是 V1 **刪除**的圖元。 |
| **處理修改** | *註解說明* |在 Hash 比對中，圖元屬性的任何變動（如坐標、文字內容）都會產生新的 Hash。因此，一個「被修改」的圖元會被計為：**舊版刪除 (紅) + 新版新增 (綠)**。 |

-----

## 8\. 差異 DXF 文件生成 (`create_diff_dxf`)

接收比對結果，並創建只包含差異圖元（紅/綠）的新 DXF 文件，實現可視化。

### 函式詳解：`create_diff_dxf`

```python
def create_diff_dxf(v1_doc, diff_added, diff_deleted, output_path):
```

| 參數 | 類型 | 說明 |
| :--- | :--- | :--- |
| `v1_doc` | ezdxf Document | 讀取 V1 檔案後的 Document 物件（用於繼承圖層、線型等定義）。 |
| `diff_added` | list | 新增的圖元列表（來自 `compare_hashes`）。 |
| `diff_deleted` | list | 刪除的圖元列表（來自 `compare_hashes`）。 |
| `output_path` | str | 差異 DXF 檔案的輸出路徑。 |

### 程式碼核心邏輯解析

| 行為 | 程式碼片段 | 說明 |
| :--- | :--- | :--- |
| **創建新文檔** | `new_doc = ezdxf.new('AC1015')`<br>`msp = new_doc.modelspace()` | 建立一個新的 DXF 文件。使用 `AC1015` (DXF R2000) 版本以確保兼容性。 |
| **定義差異圖層** | `new_doc.layers.new('DIFF_ADDED', ...)`<br>`new_doc.layers.new('DIFF_DELETED', ...)` | 創建兩個專門用於顯示差異的圖層。將顏色硬編碼為：<br>• `GREEN` (顏色代碼 3)：用於新增圖元。<br>• `RED` (顏色代碼 1)：用於刪除圖元。 |
| **寫入新增圖元** | `for entity in diff_added:`<br>`new_entity.dxf.layer = 'DIFF_ADDED'` | 遍歷新增圖元列表。將每個圖元的圖層屬性強制設置為 `DIFF_ADDED`，然後添加到新文檔的模型空間中。 |
| **寫入刪除圖元** | `for entity in diff_deleted:`<br>`new_entity.dxf.layer = 'DIFF_DELETED'` | 遍歷刪除圖元列表。將每個圖元的圖層屬性強制設置為 `DIFF_DELETED`，然後添加到新文檔的模型空間中。 |
| **保存文件** | `new_doc.saveas(output_path)` | 將只包含差異圖元的新文檔保存到指定路徑，完成差異可視化檔案的創建。 |