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 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 raisesKeyError; it returns the supplied default, so everyget()call is safe even on minimal or incomplete DXF exports.Vec3objects from$EXTMIN/$EXTMAXare not JSON-serialisable. Always calllist()on them before storing or transmitting.units_to_metresisNonewhen$INSUNITSis0(Unitless). TreatNoneas 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$INSUNITSwhen both are present, because$MEASUREMENTdoes 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.
Related Pages
- DXF Entity Structure Breakdown — parent page covering the full group-code taxonomy, section layout, and entity hierarchy
- Core Format Fundamentals & Schema Mapping — the broader framework for unit normalization, schema alignment, and format-version routing across CAD, GIS, and BIM pipelines
- Metadata Extraction Strategies — covers block attribute extraction, XDATA parsing, and dynamic extent computation
- Converting CAD Local Coordinates to EPSG:4326 — applies the unit and extent values extracted here to full coordinate reprojection with pyproj
- Understanding DWG Version Compatibility — sibling reference for version-routing decisions when your pipeline handles both DXF and DWG inputs