Extracting LWPOLYLINE Vertices with ezdxf
To extract LWPOLYLINE vertices with ezdxf, query the entities from modelspace and call entity.get_points(format="xyb") for raw (x, y, bulge) tuples, or entity.flattening(distance) when you need every arc segment resolved into straight line segments. LWPOLYLINE is a lightweight 2D polyline: all its vertices lie on a single plane defined by dxf.elevation and the extrusion vector, and curved segments are encoded not as extra points but as bulge values on the preceding vertex. Getting a correct coordinate list therefore means deciding whether you want the raw control points or a flattened approximation, and whether you need to lift the 2D coordinates back into world space. This page is part of the ezdxf Deep Dive reference on production-grade DXF parsing.
How ezdxf Handles LWPOLYLINE Vertices
An LWPOLYLINE (group code entity type LWPOLYLINE) stores its vertices as a compact array rather than as individual VERTEX sub-entities the way the legacy heavyweight POLYLINE does. Each vertex carries up to five values: x, y, start_width, end_width, and bulge. ezdxf exposes these through entity.get_points(format=...), where the format string selects which fields you receive per point — "xy" for plain coordinates, "xyb" for coordinates plus bulge, or "xyseb" for the full record.
The bulge is the mechanism that makes LWPOLYLINE able to represent arcs without additional entities. A bulge of 0 denotes a straight segment to the next vertex; a non-zero bulge is the tangent of one quarter of the arc’s included angle. Because the arc geometry is implicit, a naive extraction that reads only x and y will silently convert every arc into a chord — a common and hard-to-spot source of area and length error in downstream GIS.
ezdxf gives you two ways to resolve bulges. For a single segment, ezdxf.math.bulge_to_arc(start_point, end_point, bulge) returns (center, start_angle, end_angle, radius), letting you reconstruct the arc analytically. For the whole entity, entity.flattening(distance) yields Vec3 points where each arc has been subdivided so that the maximum deviation between the true arc and the approximating chords never exceeds distance (the sagitta, in drawing units). The diagram below shows the two routes.
What ezdxf does not do automatically is project the 2D coordinates into world coordinates. LWPOLYLINE is stored in the Object Coordinate System (OCS): its x/y values are planar, its single Z is dxf.elevation, and its plane orientation is the dxf.extrusion vector. When the extrusion is the default (0, 0, 1), OCS and WCS coincide and you can use the 2D coordinates directly. When it is anything else — common for polylines drawn on a rotated UCS — you must lift each point with entity.ocs().to_wcs((x, y, elevation)) to get true world coordinates. It also does not apply dxf.const_width to geometry; that value is a rendering width, not a vertex offset.
Production-Ready Script
The script below queries every LWPOLYLINE, chooses raw or flattened extraction based on whether the entity actually contains arcs, lifts coordinates through the OCS when needed, and returns a list of coordinate lists ready for conversion of CAD polylines to GeoJSON or Shapely.
# ezdxf>=1.1.0, Python 3.9+
import ezdxf
from ezdxf.math import Vec3
from typing import List, Dict, Any
def extract_lwpolylines(
dxf_path: str,
flatten_distance: float = 0.01,
) -> List[Dict[str, Any]]:
"""Extract LWPOLYLINE vertices from a DXF file.
Arc segments (non-zero bulge) are flattened to line segments so that the
chord-to-arc deviation never exceeds ``flatten_distance`` (drawing units).
Straight-only polylines skip flattening to keep exact control points.
Coordinates are returned in world coordinates (WCS).
"""
doc = ezdxf.readfile(dxf_path)
msp = doc.modelspace()
results: List[Dict[str, Any]] = []
for pl in msp.query("LWPOLYLINE"):
elevation = pl.dxf.elevation # single Z for all vertices
ocs = pl.ocs() # OCS -> WCS helper
# (x, y, bulge) per vertex; bulge encodes the arc to the NEXT vertex.
raw_points = pl.get_points(format="xyb")
has_arc = any(abs(bulge) > 1e-12 for _, _, bulge in raw_points)
if has_arc:
# flattening() yields Vec3 in the entity's own plane (OCS space),
# resolving every arc at the requested max deviation (sagitta).
planar_pts = list(pl.flattening(distance=flatten_distance))
else:
planar_pts = [Vec3(x, y, elevation) for x, y, _ in raw_points]
# Lift planar OCS points into world coordinates. When the extrusion is
# the default (0, 0, 1) this is an identity transform.
wcs_pts = [tuple(ocs.to_wcs(p)) for p in planar_pts]
# A closed LWPOLYLINE does not repeat its first point; close it here
# so the ring is explicit for polygon consumers.
if pl.closed and len(wcs_pts) >= 3 and wcs_pts[0] != wcs_pts[-1]:
wcs_pts.append(wcs_pts[0])
results.append({
"handle": pl.dxf.handle,
"layer": pl.dxf.layer,
"closed": bool(pl.closed),
"elevation": elevation,
"vertex_count": len(wcs_pts),
"coordinates": wcs_pts, # list of (x, y, z) tuples in WCS
})
return results
if __name__ == "__main__":
for poly in extract_lwpolylines("input.dxf", flatten_distance=0.02):
kind = "ring" if poly["closed"] else "path"
print(f"{poly['handle']}: {poly['vertex_count']} pts "
f"({kind}) on layer {poly['layer']}")
Key implementation notes:
get_points(format="xyb")returns(x, y, bulge)tuples. The bulge belongs to the segment leading to the next vertex, so the final vertex of an open polyline always has a bulge of0.flattening(distance)yieldsVec3and is the correct tool when any bulge is non-zero. Skipping it on straight polylines preserves exact control points and avoids inserting redundant collinear vertices.pl.ocs().to_wcs(point)lifts planar OCS coordinates into world space. Omitting this step corrupts geometry wheneverdxf.extrusionis not(0, 0, 1).- A closed polyline is closed by the
closedflag, not by a duplicated final vertex. Append the first coordinate explicitly before building aPolygonor GeoJSON ring. dxf.const_widthand per-vertexstart_width/end_widthare display widths; they never move a vertex and should not be added to coordinates.
Compatibility Matrix
| Component | Supported Range | Notes |
|---|---|---|
ezdxf version |
>=1.0.0 |
flattening() and get_points() stable since 0.16; >=1.1.0 recommended. |
| Python | 3.9+ |
Uses typing generics and f-strings only. |
| DXF format | R2000 (AC1015) — R2018 (AC1032) |
LWPOLYLINE introduced in R14; fully supported across this range. |
| Bulge decoding | All arcs | ezdxf.math.bulge_to_arc and flattening() cover circular arc segments; no elliptical bulges exist. |
| OCS handling | Any extrusion | Use entity.ocs().to_wcs(); identity when extrusion is (0, 0, 1). |
| Elevation | Single Z | All vertices share dxf.elevation; there is no per-vertex Z. |
For the storage-level view of how these vertices are encoded as group codes 10/20/42, see the DXF Entity Structure Breakdown.
Fallback Strategies
Real-world drawings break naive extractors in predictable ways. Handle these scenarios in order.
1. Mixed line and arc segments in one polyline
A single LWPOLYLINE frequently interleaves straight and curved segments — for example a rounded property boundary. Do not branch per segment by hand. Test whether any bulge is non-zero and, if so, route the whole entity through flattening(). This keeps straight runs as exact two-point segments while subdividing only the arcs, because flattening() emits a single segment where the bulge is zero.
2. Self-intersecting or degenerate rings
Closed polylines exported from CAD are not guaranteed to be simple (non-self-intersecting) polygons. Before treating a closed ring as a Polygon, validate it with Shapely and repair if necessary:
# shapely>=2.0
from shapely.geometry import Polygon
from shapely.validation import make_valid
ring = Polygon([(x, y) for x, y, *_ in poly["coordinates"]])
if not ring.is_valid:
ring = make_valid(ring) # may return a MultiPolygon
3. Explosive vertex counts from tight tolerances
A flatten_distance far smaller than the coordinate units — for example 0.001 on a drawing measured in millimetres with metre-scale arcs — can turn one arc into tens of thousands of points. Tune flatten_distance relative to the drawing’s $INSUNITS scale, and log any entity whose flattened vertex count exceeds a ceiling (say 5,000) so oversized rings are reviewed rather than silently written.
4. Closed versus open ambiguity
Never infer closure from coincident first and last coordinates; a legitimately open polyline can end where it began. Read entity.closed as the single source of truth, and only then decide whether to build a Polygon (closed) or a LineString (open).
5. Non-default OCS on rotated drawings
Polylines drawn on a rotated UCS store an extrusion vector other than (0, 0, 1). If your output geometry appears mirrored or rotated, confirm you are calling entity.ocs().to_wcs() on every point rather than reading raw x/y. This is the single most common cause of “the shape is right but placed wrong” bugs when moving CAD polylines into a mapping stack.
FAQ
What is a bulge value in an LWPOLYLINE?
The bulge is the tangent of one quarter of the included angle of the arc segment between two vertices. A bulge of 0 means a straight segment; a positive bulge curves counter-clockwise and a negative bulge clockwise. Convert a bulge to an explicit arc with ezdxf.math.bulge_to_arc(start, end, bulge), or flatten the whole polyline with entity.flattening(distance).
Does LWPOLYLINE store 3D coordinates?
No. LWPOLYLINE is a lightweight 2D entity. All vertices share a single Z value stored in dxf.elevation, and the plane orientation is defined by the extrusion vector (OCS). Use entity.ocs().to_wcs() to convert elevation-plus-2D coordinates into world coordinates when the extrusion is not the default (0, 0, 1).
How do I control the vertex count when flattening arcs?
entity.flattening(distance) takes a maximum chord-to-arc deviation (sagitta) in drawing units. A smaller distance produces more vertices and a closer approximation; a larger distance produces fewer vertices. Tune this value against your coordinate units so that a circular arc does not explode into hundreds of thousands of points on large survey drawings.
Why is the last vertex missing from a closed LWPOLYLINE?
A closed LWPOLYLINE does not repeat its first point as a final vertex; the closing segment is implied by entity.closed being True. When building a Shapely Polygon or a GeoJSON ring, append a copy of the first coordinate so the ring is explicitly closed.
Related Pages
- ezdxf Deep Dive: Production-Grade DXF Parsing — parent reference covering entity traversal, block resolution, and memory-efficient DXF processing
- Tessellating SPLINE Entities with ezdxf — the same flattening tolerance approach applied to NURBS curves
- Reading 3D Solids with ezdxf Python — sibling workflow for
3DSOLIDACIS payloads that polylines cannot represent - Converting CAD Polylines to GeoJSON — turning the extracted coordinate lists into GIS-ready features
- DXF Entity Structure Breakdown — group code taxonomy that governs how
LWPOLYLINEvertices and bulges are stored