DXF Entity Structure Breakdown

The Drawing Exchange Format (DXF) is a tagged-text interchange standard that exposes every CAD object as a sequence of integer-keyed group codes and typed values — a schema-transparent design that makes it the lowest-common-denominator for geometry transfer across heterogeneous AEC, GIS, and infrastructure platforms. This page is part of the Core Format Fundamentals & Schema Mapping reference, which covers the file-level architecture knowledge required to build reliable Python conversion, validation, and spatial ingestion pipelines.

Getting the entity structure wrong has measurable costs: OCS-to-WCS transformation errors invert geometry in rendering engines; misread $INSUNITS values cause 12× scale drift in spatial databases; unhandled proxy entities silently truncate ingestion batches. The sections below dissect the format’s internal layout, provide production-tested parsing workflows, and document the failure modes that catch engineering teams by surprise.

Prerequisites

  • Python 3.9+ with pip package management
  • ezdxf 1.1.0+ — install with pip install "ezdxf>=1.1.0"
  • Familiarity with CAD coordinate systems (WCS, OCS) and spatial reference concepts
  • Understanding of ASCII vs. binary DXF encoding differences
  • Representative DXF exports in the R2013–R2024 range for modern entity support
  • Basic knowledge of group code taxonomy: integer identifier paired with a typed value

Architectural Overview

DXF is organized into six strictly ordered sections. Every drawing object is serialized as a flat sequence of group code / value pairs; the integer code determines both the data type and the semantic role of the following value. The canonical section order is:

  1. HEADER — global drawing variables ($INSUNITS, $EXTMIN, $EXTMAX, $HANDSEED, $ACADVER)
  2. CLASSES — custom object class definitions (rarely present in standard CAD exports)
  3. TABLES — symbol tables for reusable definitions (LAYER, LTYPE, STYLE, DIMSTYLE, UCS, BLOCK_RECORD, VIEWPORT)
  4. BLOCKS — reusable geometry containers with attribute definitions
  5. ENTITIES — primary drawable objects (LINE, CIRCLE, LWPOLYLINE, SPLINE, TEXT, INSERT, HATCH, 3DSOLID)
  6. OBJECTS — non-graphical data structures: layouts, dictionaries, XRECORD, MATERIAL

Unlike proprietary formats covered in DWG Proprietary Limitations & Binary Format Constraints, DXF exposes its schema transparently. This transparency comes at the cost of file size and parsing overhead, but it eliminates reverse-engineered binary offsets and makes DXF highly suitable for deterministic parsing in CI/CD validation pipelines.

The diagram below shows how the six sections relate and where parsing branches occur during ingestion:

DXF Section Layout and Ingestion Pipeline Flow diagram showing the six DXF sections (HEADER, CLASSES, TABLES, BLOCKS, ENTITIES, OBJECTS) and how a Python ingestion pipeline reads them in order, branches on entity type, transforms OCS to WCS, then routes output to GIS or BIM targets. HEADER CLASSES TABLES BLOCKS ENTITIES OBJECTS Dispatch by dxftype() LINE · CIRCLE · LWPOLYLINE OCS → WCS Extrusion vector 210 / 220 / 230 Proxy / Civil 3D → Quarantine queue GIS / BIM export $INSUNITS · $ACADVER · $HANDSEED → read first, normalize units LAYER · LTYPE · STYLE → build symbol index

Group Code Taxonomy

Group codes are the atomic units of DXF serialization. They are categorized by numeric ranges that enforce strict typing:

Group Code Range Data Type Typical Usage
0–9 String Entity/class names, text values, handles
10–59 Real / Double Primary coordinates, scale factors, angles
60–79 Integer Visibility flags, color indices, line weights
90–99 32-bit Integer Counters, custom object IDs
100–109 String Subclass markers (AcDbLine, AcDbCircle, AcDbEntity)
140–149 Real Dimension variables, system constants
210–239 Real Extrusion direction vectors (OCS alignment)

A single entity in the ENTITIES section typically spans 10–40 lines of ASCII text. A LINE entity begins with 0 LINE, followed by subclass marker 100 AcDbEntity, layer assignment 8 <layer_name>, and coordinate pairs 10/20/30 (start point) and 11/21/31 (end point). The Autodesk DXF Reference maintains the definitive mapping of codes across AutoCAD releases.

Version Compatibility Table

DXF Version String AutoCAD Release Key Changes
AC1009 R12 No subclass markers; limited entity set
AC1015 R2000 Subclass markers introduced; LWPOLYLINE added
AC1021 R2007 Binary encoding option; VISUALSTYLE
AC1027 R2013 SURFACE, MESH entities
AC1032 R2018–2026 GEOGRAPHICLOCATION; current baseline

Reject AC1009 or earlier unless a legacy conversion pass is explicitly enabled in your pipeline configuration.

Step-by-Step Implementation

Step 1 — Read HEADER Variables First

Reading HEADER variables before entity iteration allows you to normalize units and reject unsupported versions early, preventing scale drift in downstream spatial databases.

# ezdxf>=1.1.0
import ezdxf

INSUNITS_MAP = {
    0: None,   # unitless — cannot normalize safely
    1: 0.0254, # inches → metres
    2: 0.3048, # feet → metres
    4: 0.001,  # millimetres → metres
    6: 1.0,    # metres — identity
}

SUPPORTED_VERSIONS = {"AC1015", "AC1021", "AC1024", "AC1027", "AC1032"}

def open_and_validate(filepath: str):
    doc = ezdxf.readfile(filepath)
    version = doc.header.get("$ACADVER", "UNKNOWN")
    if version not in SUPPORTED_VERSIONS:
        raise ValueError(f"Unsupported DXF version: {version}. Minimum: AC1015 (R2000).")
    units_code = doc.header.get("$INSUNITS", 0)
    scale = INSUNITS_MAP.get(units_code)
    if scale is None:
        raise ValueError(f"$INSUNITS={units_code} is unitless — assign a real-world CRS before ingestion.")
    return doc, scale

Step 2 — Build a Layer and Symbol Index from TABLES

Before iterating entities, construct a layer index from the TABLES section so you can resolve layer properties without repeated lookups during the entity pass.

# ezdxf>=1.1.0
def build_layer_index(doc) -> dict:
    """Return {layer_name: {'color': int, 'linetype': str}} for all defined layers."""
    index = {}
    for layer in doc.layers:
        index[layer.dxf.name] = {
            "color": layer.dxf.get("color", 7),
            "linetype": layer.dxf.get("linetype", "CONTINUOUS"),
        }
    return index

Step 3 — Iterate ENTITIES and Dispatch by Type

The ezdxf library abstracts low-level group code iteration into a typed object model. Dispatch on dxftype() and isolate failures at the entity level so a single corrupted object does not abort the entire ingestion job.

# ezdxf>=1.1.0
import logging
from typing import Any

logging.basicConfig(level=logging.WARNING)

SUPPORTED_TYPES = {"LINE", "CIRCLE", "LWPOLYLINE", "ARC", "SPLINE", "INSERT", "HATCH"}

def extract_entities(doc, scale: float) -> list[dict[str, Any]]:
    """Parse supported entity types with error isolation and unit normalization."""
    msp = doc.modelspace()
    results = []

    for entity in msp:
        etype = entity.dxftype()
        if etype not in SUPPORTED_TYPES:
            if etype in ("ACAD_PROXY_ENTITY",) or etype.startswith("AECC_"):
                logging.warning("Proxy/Civil3D entity %s (handle %s) routed to quarantine.",
                                etype, entity.dxf.get("handle", "?"))
            continue
        try:
            record = _parse_entity(entity, scale)
            if record:
                results.append(record)
        except AttributeError as exc:
            logging.warning("Malformed %s (handle %s): %s",
                            etype, entity.dxf.get("handle", "?"), exc)
    return results


def _parse_entity(entity, scale: float) -> dict | None:
    etype = entity.dxftype()
    base = {"type": etype, "handle": entity.dxf.handle, "layer": entity.dxf.layer}

    if etype == "LINE":
        s, e = entity.dxf.start, entity.dxf.end
        base["start"] = (s.x * scale, s.y * scale, s.z * scale)
        base["end"]   = (e.x * scale, e.y * scale, e.z * scale)

    elif etype == "CIRCLE":
        c = entity.dxf.center
        base["center"] = (c.x * scale, c.y * scale, c.z * scale)
        base["radius"] = entity.dxf.radius * scale

    elif etype == "LWPOLYLINE":
        # get_points("xy") yields (x, y); pad Z for 3-D pipelines
        base["vertices"] = [(v[0] * scale, v[1] * scale, 0.0)
                            for v in entity.get_points("xy")]
        base["closed"] = bool(entity.closed)
    else:
        return None  # extend per pipeline requirements

    return base

Step 4 — Transform OCS to WCS for Planar Entities

HATCH, LWPOLYLINE, SOLID, and TRACE entities store their coordinates in the Object Coordinate System (OCS). Before GIS or BIM export, apply the arbitrary axis algorithm using the extrusion vector from group codes 210/220/230 to convert to World Coordinate System (WCS).

# ezdxf>=1.1.0 — use ezdxf's built-in OCS helper rather than rolling your own
from ezdxf.math import OCS, Vec3

def ocs_to_wcs(point_ocs: tuple, extrusion: tuple) -> tuple:
    """Convert an OCS (x, y, z) point to WCS using the entity extrusion vector."""
    ocs = OCS(Vec3(extrusion))
    wcs_point = ocs.to_wcs(Vec3(point_ocs))
    return (wcs_point.x, wcs_point.y, wcs_point.z)

# Usage for a HATCH entity:
# extrusion = entity.dxf.extrusion  # Vec3, defaults to (0, 0, 1) for flat drawings
# wcs_boundary = [ocs_to_wcs(pt, extrusion) for pt in boundary_points]

Misaligned OCS extrusion vectors are a frequent source of inverted geometry or flipped normals in downstream rendering engines. This is especially critical when aligning DXF geometry with IFC4x3 Schema Mapping workflows, where spatial consistency governs clash detection accuracy and quantity takeoff reliability.

Step 5 — Extract Metadata from HEADER

For the complete header extraction implementation with fallback logic for missing variables, see the dedicated guide on How to Parse DXF Headers with Python.

Key HEADER variables for pipeline automation:

  • $INSUNITS — drawing unit definition: 0=Unitless, 1=Inches, 2=Feet, 4=Millimetres, 6=Metres. Note that 2 is Feet, not Inches — the most common misread, causing a 12× scale error.
  • $MEASUREMENT0=Imperial, 1=Metric. Use as a secondary cross-check against $INSUNITS when they disagree.
  • $HANDSEED — next available entity handle; useful for incremental update and deduplication strategies.
  • $ACADVER — AutoCAD release string (AC1032 for R2018–2026, the current baseline).

Edge Cases & Gotchas

1. $INSUNITS=2 Is Feet, Not Inches

$INSUNITS=1 is inches; $INSUNITS=2 is feet. The off-by-one numbering catches teams that skim the spec. Always use a lookup table keyed on the integer value rather than comparing against assumed strings.

# Safe lookup — never guess; always validate
SCALE_TO_METRES = {1: 0.0254, 2: 0.3048, 4: 0.001, 5: 0.01, 6: 1.0}
scale = SCALE_TO_METRES.get(doc.header.get("$INSUNITS", 0))
if scale is None:
    raise ValueError("Cannot determine drawing units; aborting ingestion.")

2. LWPOLYLINE Bulge Values Ignored

LWPOLYLINE vertices can carry a bulge factor (group code 42) that encodes arc segments between straight segments. get_points("xy") silently ignores bulge — you receive straight-line vertices only. For pipelines that must preserve curved geometry, use get_points("xyb") and convert bulge to arc parameters explicitly.

# ezdxf>=1.1.0 — preserve arc curvature
for v in entity.get_points("xyb"):
    x, y, bulge = v[0], v[1], v[2]
    # bulge != 0 signals an arc segment; compute sagitta and arc centre as needed

3. INSERT Entities Do Not Expand Blocks

An INSERT entity is a block reference, not the resolved geometry. Iterating doc.modelspace() yields the INSERT object, not the constituent LINE / CIRCLE entities inside the block definition. Resolve block references explicitly using doc.blocks[entity.dxf.name] before spatial analysis.

# ezdxf>=1.1.0 — explode a block reference into constituent entities
def resolve_insert(entity, doc):
    block = doc.blocks.get(entity.dxf.name)
    if block is None:
        return []
    return list(block)  # yields the block's own entity list

4. OCS Extrusion Vector Defaults and Silent Errors

When group codes 210/220/230 are absent, the extrusion vector defaults to (0, 0, 1) — the flat XY plane. For drawings authored entirely in the XY plane this is correct. For 3-D drawings with tilted work planes, absent extrusion codes mean silent identity transforms and geometry that lands in the wrong location. Always verify extrusion presence with entity.dxf.hasattr("extrusion") before transforming.

5. ACAD_PROXY_ENTITY Payloads Are Opaque Blobs

Civil 3D, Map 3D, and third-party vertical applications embed custom objects as ACAD_PROXY_ENTITY records. The group code 310 carries a hex-encoded binary payload that ezdxf cannot decode. Do not silently drop these — they may represent alignment elements, parcels, or pipe networks. Route them to a quarantine queue with their raw dump attached.

# ezdxf>=1.1.0 — capture raw proxy data for audit
import json

def quarantine_proxy(entity) -> dict:
    raw_codes = [(code, value) for code, value in entity.raw_dxf_attribs()]
    return {
        "handle": entity.dxf.get("handle", "unknown"),
        "type": entity.dxftype(),
        "raw_group_codes": raw_codes,
    }

6. Binary DXF (DXFB) Has No Human-Readable Group Codes

Binary DXF files (DXFB) encode group codes as 2-byte integers rather than ASCII text. ezdxf handles both transparently via ezdxf.readfile(). However, legacy tools that inspect files with a text editor or regex-based parsers will misinterpret binary DXF as corrupt. Always use a library-level parser; never grep or stream-read DXF headers manually.

Validation & Testing

Verify parsing correctness by asserting coordinate bounds against known reference extents from $EXTMIN and $EXTMAX, and by comparing the encountered entity type manifest against a baseline schema to detect upstream CAD template drift.

# ezdxf>=1.1.0
import pytest
from pathlib import Path

def validate_entity_output(entities: list, doc) -> None:
    """Assert spatial bounds and entity count minimums."""
    ext_min = doc.header.get("$EXTMIN")
    ext_max = doc.header.get("$EXTMAX")

    for record in entities:
        if record["type"] == "LINE":
            for coord_set in (record["start"], record["end"]):
                x, y, _ = coord_set
                assert ext_min.x <= x <= ext_max.x, f"X out of drawing extents: {x}"
                assert ext_min.y <= y <= ext_max.y, f"Y out of drawing extents: {y}"

    type_counts = {}
    for e in entities:
        type_counts[e["type"]] = type_counts.get(e["type"], 0) + 1

    return type_counts


def test_dxf_parse_round_trip(tmp_path):
    """Smoke test: parse a minimal DXF and verify entity extraction."""
    import ezdxf
    doc = ezdxf.new("R2018")
    msp = doc.modelspace()
    msp.add_line((0, 0, 0), (10, 0, 0), dxfattribs={"layer": "TEST"})
    test_file = tmp_path / "test.dxf"
    doc.saveas(str(test_file))

    loaded_doc, scale = open_and_validate(str(test_file))
    entities = extract_entities(loaded_doc, scale)
    assert len(entities) == 1
    assert entities[0]["type"] == "LINE"
    assert entities[0]["layer"] == "TEST"

Schema diff logging should compare encountered entity types against a baseline manifest after each ingestion run. Store the manifest as a JSON artifact in your CI/CD pipeline alongside the converted output.

# ezdxf>=1.1.0
import json, datetime

def log_schema_manifest(entities: list, source_path: str, output_path: str) -> None:
    type_counts = {}
    for e in entities:
        type_counts[e["type"]] = type_counts.get(e["type"], 0) + 1
    manifest = {
        "source": source_path,
        "timestamp": datetime.datetime.utcnow().isoformat(),
        "entity_type_counts": type_counts,
        "total": len(entities),
    }
    with open(output_path, "w") as f:
        json.dump(manifest, f, indent=2)

Performance & Scale

For DXF files exceeding 500 MB, avoid loading the full document into memory. Use ezdxf’s lazy iterator and filter by type before materialization:

# ezdxf>=1.1.0 — stream large files without full in-memory load
import ezdxf
from ezdxf.recover import readfile as recover_readfile

def stream_large_dxf(filepath: str, target_types: set[str]):
    """Yield parsed entity dicts from large DXF files without full memory load."""
    # recover.readfile tolerates partial/corrupted files and streams section by section
    doc, auditor = recover_readfile(filepath)
    if auditor.has_errors:
        for error in auditor.errors:
            logging.warning("Audit error: %s", error)

    msp = doc.modelspace()
    # query() filters by type before full entity materialization
    for etype in target_types:
        for entity in msp.query(etype):
            yield entity

Memory budgets for typical file sizes on a 16 GB host:

File Size Approach Peak RSS
< 50 MB ezdxf.readfile() full load ~300 MB
50–500 MB Full load + msp.query() filtering ~1.5 GB
> 500 MB recover.readfile() + streaming iterator ~500 MB steady-state
> 2 GB Pre-split with external DXF splitter; process per section < 400 MB per chunk

Vectorize coordinate transformation across large vertex arrays using NumPy rather than Python loops when processing LWPOLYLINE or SPLINE entities with thousands of control points:

# ezdxf>=1.1.0, numpy>=1.24.0
import numpy as np

def transform_vertices_numpy(vertices: list[tuple], scale: float) -> np.ndarray:
    """Apply unit scaling to a vertex array in one vectorized operation."""
    arr = np.array(vertices, dtype=np.float64)  # shape (N, 3)
    return arr * scale

FAQ

What does $INSUNITS=2 mean in a DXF file?

$INSUNITS=2 means the drawing unit is Feet. Inches is $INSUNITS=1. The numbering is off-by-one from what most engineers expect, and misreading it produces a 12× scale error in downstream spatial databases. Always cross-check against $MEASUREMENT (0=Imperial, 1=Metric) — when the two fields disagree, $INSUNITS takes precedence for geometry scaling.

Does ezdxf reconstruct B-Rep topology from 3DSOLID entities?

No. ezdxf exposes the raw ACIS or ShapeManager SAT/SAB blob embedded in the 3DSOLID entity but does not parse B-Rep topology. You need an ODA-compatible kernel (ODA Drawings SDK or Teigha) or a dedicated ACIS parser to reconstruct faces, edges, and vertices. For details on working with 3D solid geometry, see Reading 3D Solids with ezdxf Python.

What is the difference between WCS and OCS in DXF?

WCS (World Coordinate System) is the global Cartesian frame for the entire drawing. OCS (Object Coordinate System) is a local frame defined by an extrusion vector stored in group codes 210/220/230, used for planar entities like HATCH, LWPOLYLINE, and SOLID. OCS coordinates must be transformed to WCS via the arbitrary axis algorithm before GIS export. ezdxf.math.OCS implements this transform; use it rather than hand-rolling the matrix multiplication.

How do I handle ACAD_PROXY_ENTITY objects in ezdxf?

ezdxf cannot decode ACAD_PROXY_ENTITY payloads — they are application-specific binary blobs written by Civil 3D, Map 3D, or third-party verticals. Route them to a quarantine queue with their raw group code dumps (including group code 310 hex data) attached, then trigger a secondary conversion pass using the originating application’s SDK or ODA Drawings SDK. Never silently discard proxy entities — they may represent critical alignment or infrastructure elements.

What AC version string corresponds to AutoCAD 2018 and later?

$ACADVER = AC1032 corresponds to AutoCAD R2018. AutoCAD 2019 through 2026 retain the AC1032 schema — the version string has not advanced. Use this as your minimum baseline for modern DXF support. Reject AC1009 (R12) or earlier unless a legacy conversion path is explicitly configured, because R12 lacks subclass markers, making entity parsing significantly more fragile.