Parsing DWG Files with Python ezdxf
This page covers the specific technique of reading CAD floor plans with the ezdxf library and turning their raw entities into clean, unit-aligned geometry, and it sits within the SVG/DWG Parsing Workflows collection of the floor-plan parsing reference. It is the native-CAD branch that has to converge on the same polygon contract as the SVG room-boundary extractor before any routing work begins.
What “parsing with ezdxf” actually means
ezdxf is a pure-Python reader/writer for the DXF interchange format — the ASCII (or binary) text dialect that CAD tools emit alongside their proprietary .dwg. Parsing here means loading a drawing into an in-memory document tree, querying its modelspace for the entities that carry navigable meaning (walls, doors, room labels), and decomposing each entity into plain coordinate primitives. It is not the same as opening the file in AutoCAD: there is no rendering engine, no COM automation, and no implicit unit handling. You get the exact geometry the drafter stored, quirks included.
That distinction matters because ezdxf.readfile() accepts DXF, not the binary .dwg container. A true .dwg must first pass through the ODA File Converter (or libredwg) to become DXF. Everything below assumes that conversion has already happened and you hold a .dxf on disk. The output you are steering toward is the GeoJSON FeatureCollection envelope the rest of the pipeline consumes, so absolute, metric, right-side-up coordinates are the non-negotiable contract — the same one earned for floor-level (Z-axis) logic and for projection into an indoor coordinate reference system downstream.
Minimal working example
The snippet below is the whole core technique in one self-contained block: open a DXF defensively, pull every line-like entity off the wall floor-levels, and emit a flat list of (start, end) segment tuples ready for Shapely. It handles the two failures you will actually hit first — a structurally broken file and a binary .dwg mistakenly passed to a text reader.
import logging
from typing import List, Tuple
import ezdxf
from ezdxf import DXFStructureError
logging.basicConfig(level=logging.INFO)
logger = logging.getLogger("dwg_parse")
Segment = Tuple[Tuple[float, float], Tuple[float, float]]
def extract_wall_segments(path: str) -> List[Segment]:
"""Load a DXF and return wall LINE/LWPOLYLINE segments as coordinate pairs."""
try:
doc = ezdxf.readfile(path)
except DXFStructureError as exc:
logger.error("Corrupt DXF structure in %s: %s", path, exc)
raise
except UnicodeDecodeError as exc:
raise RuntimeError("Binary DWG detected — convert to DXF first") from exc
msp = doc.modelspace()
segments: List[Segment] = []
for e in msp.query('LINE LWPOLYLINE[layer=="A-WALL"]'):
if e.dxftype() == "LINE":
segments.append(((e.dxf.start.x, e.dxf.start.y), (e.dxf.end.x, e.dxf.end.y)))
else: # LWPOLYLINE: get_points() yields (x, y, start_w, end_w, bulge)
pts = [(p[0], p[1]) for p in e.get_points()]
segments += list(zip(pts, pts[1:]))
logger.info("Extracted %d wall segments from %s", len(segments), path)
return segments
The msp.query() string is ezdxf’s entity DSL: a space-separated list of DXF types, each optionally followed by an attribute predicate in brackets. From here the segments flow into Shapely for unioning and into the wall-and-door logic that classifies passable edges.
Parameter and entity reference
The fields you touch most when extracting navigable geometry, with the types ezdxf exposes them as:
| Field / argument | Type | Default | Notes |
|---|---|---|---|
ezdxf.readfile(filename) |
str / path |
— | Reads DXF text only. Binary .dwg raises UnicodeDecodeError; convert via ODA/libredwg first. |
doc.modelspace() |
Modelspace |
— | The drawing’s primary entity space. Paperspace layouts are separate and usually annotation-only. |
entity.dxf.layer |
str |
"0" |
The floor-level/category name. Naming is unstandardised (A-WALL, WALLS, S-WALL); match case-insensitively. |
entity.dxftype() |
str |
— | LINE, LWPOLYLINE, POLYLINE, INSERT, TEXT, MTEXT, ARC, SPLINE. |
LWPOLYLINE.get_points() |
list[tuple] |
— | Yields (x, y, start_width, end_width, bulge); a non-zero bulge means the segment is an arc. |
INSERT.attribs |
iterable | — | Block attribute tags (door IDs, room numbers); read attr.dxf.tag / attr.dxf.text. |
doc.header["$INSUNITS"] |
int |
0 |
Unit code — 4=mm, 6=m, 1=inch. 0 means unitless; you must assume a scale. |
query('TYPE[attr=="x"]') |
str |
— | Entity DSL. Combine types with spaces; predicates support ==, !=, <, >, and ? regex. |
A bulge value on an LWPOLYLINE vertex is the single most overlooked field: ignoring it silently turns curved wall runs and door swings into straight chords. Use ezdxf.math.bulge_to_arc() to expand each bulged segment into sampled points before building geometry.
Common errors and fixes
1. UnicodeDecodeError on readfile(). Symptom: the loader throws before any entity is read. Root cause is a real binary .dwg (or a binary-DXF) handed to the text reader. Detect the format up front and route binaries to a converter rather than crashing the batch:
from ezdxf.recover import readfile as recover_readfile
def load_resilient(path: str):
"""Fall back to ezdxf.recover for files with minor structural damage."""
try:
return ezdxf.readfile(path)
except (UnicodeDecodeError, DXFStructureError) as exc:
logger.warning("Standard read failed (%s); attempting recover mode", exc)
doc, auditor = recover_readfile(path) # repairs many malformed exports
if auditor.has_errors:
logger.error("Unrecoverable: %d errors", len(auditor.errors))
return doc
2. Proxy objects yield no geometry. Symptom: AutoCAD Architecture/MEP exports show ACAD_PROXY_ENTITY types whose walls and ducts never appear in your segment list. ezdxf cannot decompose proxies into primitives. Detect them, log a topology-affecting gap, and degrade to a bounding reference so the floor level is not silently incomplete:
def flag_proxies(entities) -> list:
"""Log proxy entities ezdxf cannot decompose so they aren't lost silently."""
proxies = [e for e in entities if e.dxftype() in ("ACAD_PROXY_ENTITY", "PROXY_ENTITY")]
for p in proxies:
logger.warning("Proxy %s on %s — route to ODA/libredwg for full geometry",
p.dxf.handle, p.dxf.layer)
return proxies
3. Geometry is mirrored or 1000x off scale. Symptom: rooms render flipped, or door widths read as “900 metres”. Root cause is an unhandled $INSUNITS mismatch — millimetre drawings treated as metres — or a Y-axis assumption that does not hold. Read the unit code explicitly and scale to metres before emitting anything:
_TO_METRES = {1: 0.0254, 4: 0.001, 5: 0.01, 6: 1.0} # in, mm, cm, m
def metre_scale(doc) -> float:
code = doc.header.get("$INSUNITS", 0)
if code == 0:
logger.warning("Unitless drawing; assuming millimetres")
return 0.001
return _TO_METRES.get(code, 1.0)
How this feeds the pipeline
The segments and attributes produced here are the CAD-sourced substrate the rest of the floor-plan pipeline binds to. Once scaled to metres they are unioned and snapped into closed rings, classified into passable edges by the wall and door detection algorithms, enriched with the labels and block attributes covered in attribute mapping from blueprints, and then emitted inside the same GeoJSON FeatureCollection envelope defined by the JSON schema for indoor map APIs. When a portfolio of buildings has to be processed at once, this loader is the unit of work driven by the async batch processing pipelines, and the SVG branch converges on the identical polygon contract so CAD-sourced and web-sourced floors are indistinguishable to the routing graph.
Related
- Extracting Room Boundaries from SVG Floor Plans
- Automating Wall and Door Detection in CAD
- Building Async Pipelines for Batch Floor-Plan Processing
This page belongs to the SVG/DWG Parsing Workflows collection, part of the Automated Floor Plan Parsing & Vectorization reference.