DXFのTABLESセクションを読む|TABLE・ENDTAB・シンボルテーブルの構造

「DXFのTABLESセクションを読む|TABLE・ENDTAB・シンボルテーブルの構造」の内容を表す技術イラスト

DXFの図形を解析するとき、LINEやCIRCLEだけを読めても、図面を正しく復元できるとは限りません。

エンティティが参照するレイヤー、線種、文字スタイル、寸法スタイル、ブロック名などは、主にTABLESセクションで定義されます。

たとえばLINEに、

8
OUTLINE
6
DASHED

と記録されていても、この部分だけでOUTLINEレイヤーの色やDASHED線種のパターンまでは分かりません。参照先となるLAYERテーブルやLTYPEテーブルを解決する必要があります。

この記事では、DXFのTABLESセクションを、

  • TABLEとENDTABの境界
  • コード2・5・70・100・330
  • シンボルテーブルとテーブルレコードの違い
  • コード70を実件数として扱えない理由
  • Pythonでの安全な抽出方法

に分けて解説します。

目次

TABLESセクションの役割

TABLESセクションには、図面内で名前を付けて共有される設定が保存されます。

AutodeskのDXF Referenceが示す代表的なシンボルテーブルは次のとおりです。images.autodesk.com

テーブル主な役割
APPIDXDATAで使う登録アプリケーション名
BLOCK_RECORDブロック定義を管理するレコード
DIMSTYLE寸法スタイル
LAYERレイヤー
LTYPE線種
STYLE文字スタイル
UCSユーザー座標系
VIEW名前付きビュー
VPORTビューポート設定

これらは画面へ直接描画される図形ではありません。複数のエンティティから名前やハンドルで参照される、図面データベース上の共有定義です。

ENTITIES
    LINE ── layer name ──→ LAYER record
    TEXT ── style name ──→ STYLE record
    INSERT ─ block name ─→ BLOCK_RECORD

同じ定義を各エンティティへ繰り返し保存しないため、設定の一元管理と参照が可能になります。

TABLESセクション全体の構造

TABLESセクションは、通常のセクション境界で囲まれます。

0
SECTION
2
TABLES

0
TABLE
2
LTYPE
...
0
ENDTAB

0
TABLE
2
LAYER
...
0
ENDTAB

0
ENDSEC

階層として整理すると、次の構造です。

TABLES section
├── TABLE: LTYPE
│   ├── LTYPE record
│   └── LTYPE record
└── TABLE: LAYER
    ├── LAYER record
    └── LAYER record

TABLEはテーブル本体の開始、ENDTABはそのテーブルの終了を表します。セクション全体はENDSECで終了します。

Autodesk仕様ではテーブルの並び順は変化し得ます。ただし、LTYPEテーブルはLAYERテーブルより前に出力されます。独自パーサーでは、それ以外の順序を固定せず、コード2のテーブル名で識別します。help.autodesk.com

テーブルヘッダーの主要コード

テーブル本体には、すべてのシンボルテーブルに共通するコードがあります。

コード意味
0オブジェクト種類TABLE
2テーブル名
5テーブル自身のハンドル
330所有元へのsoft-pointer参照
100AcDbSymbolTableサブクラスマーカー
70テーブルに続く可能性がある最大エントリ数

これらの共通コードはAutodeskのシンボルテーブル仕様に定義されています。help.autodesk.com

LAYERテーブルのヘッダー例は次のようになります。

0
TABLE
2
LAYER
5
2
330
0
100
AcDbSymbolTable
70
4

ここで重要なのは、コード70の4を「必ず4件のLAYERレコードが続く」と解釈しないことです。

コード70は実際のレコード数ではない

Autodesk仕様では、テーブル内に削除済み項目が存在しても、その項目はDXFへ書き出されません。そのため、コード70が示す数より少ないレコードしか続かない場合があります。

コード70は、読み込み側が配列領域を確保するための最大数として用意された値です。help.autodesk.com

したがって、次の読み方は危険です。

for _ in range(table_header[70]):    read_record()

実際の境界は、コード0と値ENDTABの組です。

コード70を読む
    ↓
容量確保や検査の参考にする
    ↓
コード0 / ENDTABまでレコードを読む

実際に読めた件数とコード70が異なる場合は、即座に破損と判定せず、診断情報として記録します。

テーブルレコードの共通構造

各テーブルの中には、そのテーブル名と同じ種類のレコードが並びます。

LAYERテーブルならLAYERレコード、STYLEテーブルならSTYLEレコードです。

シンボルテーブルレコードに共通する主なコードは次のとおりです。

コード意味
0レコード種類。通常はテーブル名と同じ
5レコード自身のハンドル
105DIMSTYLEだけが使用する自己ハンドル
330所有するテーブルへの参照
100AcDbSymbolTableRecordサブクラスマーカー
2レコード名
70レコード種類ごとに定義されるフラグ

共通部分に加えて、各レコード固有のコードが続きます。help.autodesk.com

簡略化したLAYERレコードは次のようになります。

0
LAYER
5
10
330
2
100
AcDbSymbolTableRecord
100
AcDbLayerTableRecord
2
OUTLINE
70
0
62
7
6
CONTINUOUS

この例では、

  • 自己ハンドル:10
  • 所有するテーブル:ハンドル2
  • レイヤー名:OUTLINE
  • 色番号:7
  • 線種名:CONTINUOUS

と解釈できます。

コード70はテーブルヘッダーとレコードで意味が異なります。ヘッダーでは最大エントリ数ですが、LAYERレコードではレイヤー状態を表すビットフラグです。

グループコードだけで意味を決めず、出現したオブジェクト種類とサブクラスを組み合わせて解釈します。

DIMSTYLEだけはコード105を使う

通常のシンボルテーブルレコードは、自己ハンドルにコード5を使用します。

例外はDIMSTYLEです。DIMSTYLEレコードだけはコード105が自己ハンドルになります。

0
DIMSTYLE
105
27
2
ISO-25

すべてのレコードからコード5だけを探す実装では、DIMSTYLEの識別子を取得できません。help.autodesk.com

def handle_code(record_type: str) -> int:
    return 105 if record_type == "DIMSTYLE" else 5

この例外は、ハンドル索引や参照グラフを構築するときに重要です。

名前参照とハンドル参照を分ける

TABLESセクションの定義は、名前またはハンドルで参照されます。

たとえば一般的なエンティティでは、次のような名前参照が使われます。

エンティティ側のコード参照対象
8レイヤー名
6線種名
7文字スタイル名
2INSERTが参照するブロック名

一方、コード330や340などはハンドル参照です。

パーサー内部では、次の二つの索引を分けて作ると扱いやすくなります。

records_by_name = {
    ("LAYER", "OUTLINE"): layer_record,
    ("STYLE", "STANDARD"): style_record,
}

objects_by_handle = {
    "10": layer_record,
    "27": dimstyle_record,
}

名前はテーブル内で解決し、ハンドルは図面全体の索引で解決します。

PythonでTABLESを分割する

次のコードは、TABLESセクションから各テーブルを抽出する最小例です。

from dataclasses import dataclass
from typing import Any


Tag = tuple[int, Any]


@dataclass
class SymbolTable:
    name: str
    header: list[Tag]
    records: list[list[Tag]]


def get_first(tags: list[Tag], code: int) -> Any | None:
    for tag_code, value in tags:
        if tag_code == code:
            return value
    return None


def parse_tables(tags: list[Tag]) -> list[SymbolTable]:
    tables: list[SymbolTable] = []
    index = 0

    while index < len(tags):
        code, value = tags[index]

        if (code, value) != (0, "TABLE"):
            index += 1
            continue

        index += 1
        header: list[Tag] = []

        while index < len(tags) and tags[index][0] != 0:
            header.append(tags[index])
            index += 1

        table_name = get_first(header, 2)

        if not isinstance(table_name, str):
            raise ValueError("TABLE has no valid code 2 name")

        records: list[list[Tag]] = []

        while index < len(tags):
            code, value = tags[index]

            if (code, value) == (0, "ENDTAB"):
                index += 1
                break

            if code != 0:
                raise ValueError(
                    f"unexpected tag before record: {(code, value)!r}"
                )

            record_type = str(value)
            index += 1
            record: list[Tag] = [(0, record_type)]

            while index < len(tags) and tags[index][0] != 0:
                record.append(tags[index])
                index += 1

            if record_type != table_name:
                raise ValueError(
                    f"{table_name} table contains {record_type}"
                )

            records.append(record)
        else:
            raise ValueError(f"{table_name} table has no ENDTAB")

        tables.append(
            SymbolTable(
                name=table_name,
                header=header,
                records=records,
            )
        )

    return tables

この関数は、コード0を次のオブジェクト境界として扱い、ENDTABまでを一つのテーブルとして読みます。

コード70をループ回数に使用していないため、ヘッダー上の最大数と実際のレコード数が異なるDXFも解析できます。

読み取り後に検査する項目

抽出後は、構造と参照を分けて検査します。

  • TABLEにコード2の名前があるか
  • ENDTABで正しく閉じられているか
  • レコード種類がテーブル名と一致するか
  • レコード名が同一テーブル内で重複していないか
  • 自己ハンドルが図面全体で重複していないか
  • コード330の所有先が対象テーブルか
  • DIMSTYLEのコード105を取得できているか
  • エンティティが参照する名前を解決できるか
  • コード70と実際の件数に差があるか

コード70との差は警告候補ですが、ENDTAB欠落や異なる種類のレコード混入は、構造エラーとして扱う方が安全です。

実装で起こりやすい失敗

テーブルの出現順を固定する。
テーブル順は変化する可能性があります。コード2の名前で識別します。

コード70の回数だけレコードを読む。
コード70は実件数を保証しません。ENDTABを終端にします。

テーブルとレコードを同じオブジェクトとして扱う。
TABLEはコンテナ、LAYERなどはその要素です。ハンドルと所有関係も別に保持します。

同じコードを常に同じ意味で解釈する。
コード70は出現位置によって最大数にもフラグにもなります。

DIMSTYLEのコード105を見落とす。
DIMSTYLEだけは自己ハンドルのコードが異なります。

名前だけ保存してRawタグを捨てる。
テーブルごとに追加コードが異なります。未知のタグも順序付きで保持します。

まとめ

DXFのTABLESセクションには、レイヤー、線種、文字スタイル、寸法スタイル、ブロックレコードなど、図面内で共有される名前付き定義が保存されます。

基本構造は次のとおりです。

TABLES
    TABLE
        table header
        symbol table record
        symbol table record
    ENDTAB
ENDSEC

実装で特に重要なのは、コード70を実件数として読まないことです。コード70は最大エントリ数であり、実際の終端はコード0とENDTABの組です。

また、テーブル本体とレコードを分離し、

  • テーブル名
  • レコード名
  • 自己ハンドル
  • 所有先ハンドル
  • テーブル固有の属性
  • 未解釈のRawタグ

を保持します。

TABLESを正しく解析できれば、ENTITIESに書かれたレイヤー名や線種名を共有定義へ接続できます。

これは、DXFを単なる図形配列ではなく、参照関係を持つ図面データベースとして復元するための基盤になります。

参考情報

参考になったらシェアしてください
  • URLをコピーしました!
  • URLをコピーしました!

この記事を書いた人

機械設計・油圧・CAD・Python・AIなど、ものづくりに関わる技術を扱っています。工学知識を整理・構造化し、設計や自動化に再利用できる形へ変えていくことを目指しています。

目次