How to Parse DXF Headers with Python

To parse DXF headers with Python, use ezdxf (>=1.1.0) and its doc.header dictionary interface. This interface resolves raw DXF group codes into named variables automatically, eliminating manual string parsing. The HEADER section functions as the drawing-wide configuration block, storing coordinate bounds, unit definitions, version identifiers, and layer defaults. For a complete map of how the HEADER fits inside the full format layout, see the DXF Entity Structure Breakdown. For AEC and GIS pipelines, extracting these values before geometry ingestion prevents unit mismatches, coordinate drift, and schema validation failures that are otherwise extremely difficult to trace.

How ezdxf Handles the HEADER Section

The DXF HEADER section is a flat list of variable definitions, each expressed as a pair of group codes: a string name (e.g., $INSUNITS) followed by one or more typed value codes. ezdxf indexes these pairs at parse time and exposes them through doc.header, a dictionary-like object. You call header.get("$VARNAME", default) and receive the decoded Python type — integers, floats, or Vec3 objects — rather than raw text lines.

The diagram below shows how a DXF file’s sections relate to each other and where the HEADER sits in relation to the geometry you ultimately want.

DXF Section Layout and Pipeline Entry Point A diagram showing the five DXF sections (HEADER, CLASSES, TABLES, BLOCKS, ENTITIES) arranged left to right, with an arrow from the HEADER section to a pipeline routing box, which then feeds into geometry extraction. HEADER variables CLASSES custom objs TABLES layers/styles BLOCKS definitions ENTITIES geometry Pipeline Routing unit norm · version gate · extent check Parse HEADER first — it controls how all subsequent geometry is interpreted

DXF files carry no embedded coordinate reference system. They depend on implicit drawing units, version-specific entity behaviors, and origin offsets. When CAD exports enter automated ingestion workflows, unvalidated headers cause silent spatial distortions. A file drawn in architectural units (1 unit = 1 inch) imported into a metric GIS pipeline will scale incorrectly by a factor of 25.4. No error is thrown; the geometry simply arrives at the wrong coordinates.

Reading the header before any geometry gives your pipeline three things:

  • a unit multiplier to apply before coordinate transformation
  • a version identifier to route legacy files to compatibility shims
  • a bounding box for spatial indexing and sanity-checking extent validity

This connects directly to the Core Format Fundamentals & Schema Mapping framework: consistent field naming, type coercion, and validation rules across CAD, GIS, and BIM endpoints all depend on an agreed unit and version baseline set at ingestion.

Production-Ready Script

The script below demonstrates a defensive, pipeline-ready approach. It handles missing files, malformed DXF structures, and Vec3 serialization while mapping raw codes to human-readable pipeline values. Install the dependency first:

pip install "ezdxf>=1.1.0"
# ezdxf>=1.1.0  |  Python 3.9+
import json
import ezdxf
from pathlib import Path
from typing import Any, Dict, Optional

# $ACADVER string → AutoCAD release label
# Source: Autodesk DXF Reference + Open Design Alliance version matrix
ACAD_VERSION_MAP: Dict[str, str] = {
    "AC1009": "R12",
    "AC1012": "R13",
    "AC1014": "R14",
    "AC1015": "2000",
    "AC1018": "2004",
    "AC1021": "2007",
    "AC1024": "2010",
    "AC1027": "2013",
    "AC1032": "2018",  # current on-disk format as of AutoCAD 2019–2026
}

# $INSUNITS integer code → unit name
# Full list: DXF spec §HEADER Variables, group code 70 for $INSUNITS
UNIT_MAP: Dict[int, str] = {
    0: "Unitless",       1: "Inches",          2: "Feet",
    3: "Miles",          4: "Millimeters",      5: "Centimeters",
    6: "Meters",         7: "Kilometers",       8: "Microinches",
    9: "Mils",          10: "Yards",           11: "Angstroms",
   12: "Nanometers",    13: "Microns",         14: "Decimeters",
   15: "Decameters",    16: "Hectometers",     17: "Gigameters",
   18: "Astronomical Units",                   19: "Light Years",
   20: "Parsecs",
}

# Conversion factor to metres for each $INSUNITS code (0 = unknown, handle separately)
TO_METRES: Dict[int, float] = {
    1: 0.0254,    2: 0.3048,     3: 1609.344,   4: 0.001,
    5: 0.01,      6: 1.0,        7: 1000.0,     8: 2.54e-8,
    9: 2.54e-5,  10: 0.9144,    11: 1e-10,      12: 1e-9,
   13: 1e-6,     14: 0.1,       15: 10.0,       16: 100.0,
   17: 1e9,      18: 1.496e11,  19: 9.461e15,   20: 3.086e16,
}


def parse_dxf_header(filepath: str) -> Dict[str, Any]:
    """Extract and normalise critical DXF header variables for pipeline routing.

    Returns a dict with typed, serialisable values ready for JSON output or
    downstream schema validation. Raises FileNotFoundError or RuntimeError on
    unrecoverable failures; never returns partial data silently.
    """
    path = Path(filepath)
    if not path.exists():
        raise FileNotFoundError(f"DXF file not found: {filepath}")

    try:
        doc = ezdxf.readfile(str(path))
    except ezdxf.DXFError as exc:
        raise RuntimeError(f"Failed to parse DXF structure: {exc}") from exc

    header = doc.header
    acad_ver: str = header.get("$ACADVER", "Unknown")
    ins_units: int = header.get("$INSUNITS", 0)

    # ezdxf returns Vec3 objects for point variables — convert to plain lists
    # before JSON serialisation; Vec3 is not JSON-serialisable by default.
    ext_min: Optional[Any] = header.get("$EXTMIN", None)
    ext_max: Optional[Any] = header.get("$EXTMAX", None)

    extents_min = list(ext_min) if ext_min is not None else None
    extents_max = list(ext_max) if ext_max is not None else None

    # Flag suspiciously degenerate extents (empty or corrupted drawing)
    degenerate_extents = (
        extents_min is not None
        and extents_max is not None
        and extents_min == extents_max
    )

    return {
        "source_file": str(path),
        "acad_version_raw": acad_ver,
        "acad_version_label": ACAD_VERSION_MAP.get(acad_ver, "Unknown"),
        "units_code": ins_units,
        "units_label": UNIT_MAP.get(ins_units, "Unknown"),
        "units_to_metres": TO_METRES.get(ins_units),   # None when unitless
        "extents_min": extents_min,
        "extents_max": extents_max,
        "degenerate_extents": degenerate_extents,
        # Linear unit display format (2 = decimal, 4 = architectural, etc.)
        "lunits": header.get("$LUNITS", 2),
        # Angular precision (decimal places); important for survey exports
        "auprec": header.get("$AUPREC", 4),
        # $MEASUREMENT is the older fallback: 0 = Imperial, 1 = Metric
        "measurement_fallback": header.get("$MEASUREMENT", None),
    }


if __name__ == "__main__":
    import sys
    target = sys.argv[1] if len(sys.argv) > 1 else "sample.dxf"
    try:
        result = parse_dxf_header(target)
        print(json.dumps(result, indent=2))
    except (FileNotFoundError, RuntimeError) as exc:
        print(f"Pipeline ingestion failed: {exc}", file=sys.stderr)
        sys.exit(1)

Key implementation notes:

  • doc.header.get() never raises KeyError; it returns the supplied default, so every get() call is safe even on minimal or incomplete DXF exports.
  • Vec3 objects from $EXTMIN/$EXTMAX are not JSON-serialisable. Always call list() on them before storing or transmitting.
  • units_to_metres is None when $INSUNITS is 0 (Unitless). Treat None as a hard gate: reject the file, apply a config-driven default, or cross-reference external metadata — but never assume metric.
  • $MEASUREMENT (the older Imperial/Metric flag) is read as a fallback only. Prefer $INSUNITS when both are present, because $MEASUREMENT does not distinguish between millimetres and metres.

Compatibility Matrix

Component Supported Range Notes
Python 3.9 – 3.13 dict type hints require 3.9+; no walrus operator used
ezdxf 1.1.0 – 1.3.x doc.header API stable since 0.18; Vec3 always returned for point vars
DXF version AC1009 (R12) – AC1032 (R2018) AC1009 files may lack $INSUNITS; default to 0 (Unitless)
OS Linux, macOS, Windows pathlib.Path normalises separators; no platform-specific code
Binary DXF Supported with caveats ezdxf.readfile() detects binary encoding automatically; R12 binary files occasionally omit extended header vars

Files saved by non-Autodesk CAD tools (BricsCAD, DraftSight, LibreCAD) generally conform to the specification but may set $ACADVER to values not in ACAD_VERSION_MAP. Use .startswith("AC1") as a guard before treating an unknown string as a fatal error.

Fallback Strategies and Troubleshooting

1. $INSUNITS is 0 (Unitless)

The file was exported without a declared unit. Possible recoveries in priority order: (a) check a sidecar metadata file if your ingest workflow supports one; (b) read $MEASUREMENT — if it equals 1, metric is likely, but you still need to guess between mm/cm/m; © apply the unit declared in a pipeline config for that project or data source; (d) reject the file and log it for manual review. Never silently assume metres.

2. $EXTMIN equals $EXTMAX or both are None

The drawing is either empty or the exporting application did not regenerate extents before saving (REGEN / REGEN ALL in AutoCAD). Compute extents dynamically by iterating doc.modelspace() entity coordinates. Flag the file for verification before triggering expensive spatial transforms. The Metadata Extraction Strategies page covers programmatic extent computation in more detail.

3. Unknown $ACADVER string

Third-party CAD tools sometimes append build suffixes (e.g., "AC1032_BETA"). Fall back to prefix matching:

known = next(
    (label for ver, label in ACAD_VERSION_MAP.items() if acad_ver.startswith(ver)),
    "Unknown"
)

4. ezdxf.DXFError on readfile()

The file is malformed, truncated, or uses a version newer than ezdxf supports. Log the exception message, move the file to a quarantine path, and continue processing the batch. Do not let a single bad file halt the pipeline.

5. $ACADVER predates AC1015 (R2000)

Files older than R2000 lack modern XDATA, ACAD_PROXY_ENTITY, and extended block attribute support. Route them through a legacy compatibility shim that strips or transforms unsupported entity types before passing geometry to the main parser. Connecting this routing decision to version-aware branching in your DXF Entity Structure Breakdown parser prevents runtime errors on downstream entity reads.