DXF vs IFC for GIS Ingestion: Choosing the Interchange Format
Choosing between DXF and IFC as the interchange format that feeds a GIS pipeline is one of the first architectural decisions in any CAD/BIM-to-GIS integration, and it determines how much semantic and spatial fidelity survives ingestion.
This guide sits within the Interoperability Decision Guides section and frames the choice as an engineering trade-off, not a format preference. DXF is a geometry-centric interchange format: it carries entities, layers, and coordinates but no building semantics and no coordinate reference system. IFC is a semantic, parametric model: it carries typed building elements, property sets, and — when authored correctly — embedded georeferencing, at the cost of a heavier file and an expensive geometry-evaluation step. The right answer depends on what your source actually is and what your GIS store needs to hold. Survey deliverables and 2D linework almost always arrive as DXF and belong on the DXF route; attributed BIM assets that must land in GIS with their identity and properties intact belong on the IFC route.
Both routes converge on the same destination — a spatially indexed GIS store — but they diverge sharply in how they extract geometry, how they recover attributes, and how much CPU they burn doing it.
Prerequisites
Before building either ingestion route, confirm the following:
- Python 3.9+ — type hints,
pathlib, and dataclasses are used throughout.# python>=3.9 - ezdxf ≥ 1.1.0 — install with
pip install "ezdxf>=1.1.0"for the DXF route. - ifcopenshell ≥ 0.8.0 — install with
pip install "ifcopenshell>=0.8.0"for the IFC route; it bundles the geometry kernel used byifcopenshell.geom. - shapely ≥ 2.0 — install with
pip install "shapely>=2.0"for geometry construction and validation. - pyproj ≥ 3.4 — required for the shared CRS reprojection stage.
- Knowledge of the source data model — you need to know whether your input is discrete 2D linework or a semantically typed building model. The DXF Entity Structure Breakdown and the IFC4X3 Schema Mapping document the two data models this guide compares.
Architectural Overview
The two formats differ at the level of what a “feature” even is. In DXF, a feature is a geometric entity — a polyline, a line, a circle — tagged with a layer name and, optionally, block attributes or XDATA. There is no notion of a wall, a door, or a parcel; those meanings live only in layer-naming conventions that vary by drafter. In IFC, a feature is a typed object — IfcWall, IfcSlab, IfcSpace — with a stable global identifier, a class, and property sets, whose geometry is a parametric representation that must be evaluated before you can read a single coordinate.
That distinction cascades into every downstream concern:
| Dimension | DXF | IFC |
|---|---|---|
| Data model | Geometry entities + layers | Typed semantic objects + relationships |
| Semantic richness | None (layer-name conventions only) | High (IfcWall, property sets, classifications) |
| Georeferencing | None; $INSUNITS gives units only |
IfcMapConversion + IfcProjectedCRS when authored |
| Geometry fidelity | Exact stored coordinates, 2D-centric | Exact parametric B-Rep, full 3D |
| Parse cost | Very low — direct coordinate read | High — kernel evaluates each representation |
| File size | Compact for equivalent linework | Large; carries semantics + geometry |
| Stable IDs | Entity handles (per-file, volatile) | GlobalId GUIDs (stable across exports) |
| Tooling maturity | Mature: ezdxf, GDAL DXF driver |
Mature: ifcopenshell; heavier dependency |
| Typical source | Survey, cadastral, 2D CAD | Architectural / MEP / structural BIM |
Version compatibility for the two routes:
| Component | Supported range | Notes |
|---|---|---|
ezdxf |
>=1.1.0 |
Reads DXF R12–R2018; direct vertex access. |
ifcopenshell |
>=0.8.0 |
Reads IFC2X3, IFC4, IFC4X3; bundles geometry kernel. |
shapely |
>=2.0 |
GEOS-backed; vectorised geometry ops. |
pyproj |
>=3.4 |
PROJ bindings for the shared reprojection stage. |
| IFC georeferencing | IFC4 / IFC4X3 | IfcMapConversion requires IFC4+; IFC2X3 lacks it. |
ezdxf reads exactly what the DXF file stores and evaluates nothing — deterministic and fast. ifcopenshell.geom evaluates parametric definitions into meshes on demand, which is where both the value (true 3D geometry with semantics) and the cost (CPU-bound triangulation) of the IFC route come from.
Step-by-Step Implementation
1. Classify the source and pick the route
Before writing extraction code, classify the input. A survey or cadastral deliverable is 2D linework with meaning encoded in layer names — route it through DXF. A building model exported from Revit, ArchiCAD, or Tekla carries typed elements and properties you want to preserve — route it through IFC. The decision is about information content, not file extension.
# python>=3.9
from pathlib import Path
def choose_route(path: Path) -> str:
"""Return 'dxf' or 'ifc' based on suffix; the semantic decision
is made upstream by whoever classifies the deliverable."""
suffix = path.suffix.lower()
if suffix == ".dxf":
return "dxf" # geometry-centric: survey / 2D linework
if suffix == ".ifc":
return "ifc" # semantic: attributed BIM assets
raise ValueError(f"Unsupported interchange format: {suffix!r}")
2. Ingest DXF geometry with ezdxf and shapely
On the DXF route, iterate the modelspace, read vertices directly, and build shapely geometries. There is no geometry evaluation — vertices are read as stored. Carry the layer name across as the only available attribute.
# ezdxf>=1.1.0 | shapely>=2.0 | python>=3.9
import ezdxf
from shapely.geometry import LineString, Polygon, mapping
def dxf_to_features(dxf_path: str, unit_scale: float = 1.0) -> list[dict]:
"""Extract LWPOLYLINE/LINE entities as GeoJSON-like features.
unit_scale converts drawing units to metres (from $INSUNITS)."""
doc = ezdxf.readfile(dxf_path)
msp = doc.modelspace()
features: list[dict] = []
for e in msp.query("LWPOLYLINE"):
pts = [(x * unit_scale, y * unit_scale) for x, y, *_ in e.get_points()]
if e.closed and len(pts) >= 3:
geom = Polygon(pts)
else:
geom = LineString(pts)
features.append({
"geometry": mapping(geom),
"properties": {"layer": e.dxf.layer, "handle": e.dxf.handle},
})
for e in msp.query("LINE"):
start = (e.dxf.start.x * unit_scale, e.dxf.start.y * unit_scale)
end = (e.dxf.end.x * unit_scale, e.dxf.end.y * unit_scale)
features.append({
"geometry": mapping(LineString([start, end])),
"properties": {"layer": e.dxf.layer, "handle": e.dxf.handle},
})
return features
The $INSUNITS header is the only unit signal DXF gives you; there is no CRS. The full traversal and block-flattening machinery — critical for real survey files with nested INSERT references — is documented in the ezdxf Deep Dive, and the conversion of raw polylines into RFC 7946 GeoJSON is covered in Converting CAD Polylines to GeoJSON.
3. Ingest IFC geometry with ifcopenshell
On the IFC route, evaluate each product’s representation into a mesh, then project the mesh to a 2D footprint for the GIS store. This is where semantics survive: every feature keeps its GlobalId, class, and name.
# ifcopenshell>=0.8.0 | shapely>=2.0 | python>=3.9
import ifcopenshell
import ifcopenshell.geom
import numpy as np
from shapely.geometry import Polygon, MultiPoint
def ifc_to_footprints(ifc_path: str, ifc_class: str = "IfcBuildingElement"):
"""Yield (GlobalId, ifc_class, name, footprint_polygon) for each product.
The footprint is the 2D convex hull of the world-coordinate mesh vertices."""
model = ifcopenshell.open(ifc_path)
settings = ifcopenshell.geom.settings()
settings.set(settings.USE_WORLD_COORDS, True)
for product in model.by_type(ifc_class):
if not product.Representation:
continue
try:
shape = ifcopenshell.geom.create_shape(settings, product)
except RuntimeError:
continue # unsupported representation — log and skip
verts = np.array(shape.geometry.verts).reshape(-1, 3)
xy = verts[:, :2]
footprint = MultiPoint(xy).convex_hull
if isinstance(footprint, Polygon):
yield (
product.GlobalId,
product.is_a(),
getattr(product, "Name", None),
footprint,
)
The convex hull is a deliberate simplification for footprint extraction; for accurate wall outlines that respect concavity, evaluate the representation and slice at a Z plane, as shown in Extracting IFC Wall Geometries to Shapely. The complete geometry-settings and property-set traversal lives in the ifcopenshell Workflow.
4. Normalise units and CRS, then load
Both routes feed a shared normalisation stage. DXF geometry arrives in scaled local units with no CRS and must be georeferenced from external control. IFC geometry arrives in metres and — if IfcMapConversion is populated — already carries the offset and rotation needed to place it in a projected CRS.
# pyproj>=3.4 | shapely>=2.0 | python>=3.9
from pyproj import Transformer
from shapely.ops import transform as shp_transform
def reproject(geom, src_epsg: int, dst_epsg: int):
"""Reproject a shapely geometry between CRSs. always_xy avoids
lon/lat axis-order surprises."""
tf = Transformer.from_crs(src_epsg, dst_epsg, always_xy=True)
return shp_transform(lambda xs, ys, zs=None: tf.transform(xs, ys), geom)
Deriving the source CRS for DXF — including datum shifts and control-point alignment — is the subject of the CRS Normalization Workflows section. Whichever route produced the geometry, the loaded output is a spatially indexed GIS store.
Edge Cases & Gotchas
DXF has no CRS — never assume one
A DXF file’s $INSUNITS records the base unit (millimetres, metres, feet) but nothing about projection or datum. Ingesting DXF coordinates directly into a GIS layer tagged EPSG:4326 places every feature near the origin off the coast of West Africa. Always attach the CRS from an external source before reprojection:
if src_epsg is None:
raise ValueError("DXF carries no CRS; supply src_epsg from survey control.")
IFC georeferencing may be absent or partial
IfcMapConversion is optional and frequently omitted by exporters. Check for it before trusting IFC coordinates as georeferenced:
# ifcopenshell>=0.8.0
conversions = model.by_type("IfcMapConversion")
if not conversions:
logging.warning("No IfcMapConversion; treat IFC coords as local, not projected.")
Unsupported IFC representations silently vanish
ifcopenshell.geom.create_shape() raises RuntimeError on representations the kernel cannot evaluate. Without a guard, those elements disappear from the output with no error. Catch, log the GlobalId, and count skips so a CI gate can fail on excessive loss.
DXF layer names are the only semantics you get
Because DXF has no object typing, a wall and a property boundary can share a layer if the drafter was careless. Do not infer feature class from geometry; map it from layer names using an explicit, reviewed lookup. This mapping problem is the subject of Layer Mapping Logic.
Z coordinates behave differently across routes
IFC geometry is natively 3D; footprint extraction discards Z deliberately. DXF may store 2D LWPOLYLINE (no Z) or 3D entities. Decide the target dimension early — a mixed-dimension GIS layer rejects inserts in strict PostGIS configurations.
Validation & Testing
Validate feature counts and geometry validity for both routes against known-good fixtures before committing to storage:
# ezdxf>=1.1.0 | ifcopenshell>=0.8.0 | shapely>=2.0 | python>=3.9
from shapely.geometry import shape
def test_dxf_route_produces_valid_geometry():
feats = dxf_to_features("tests/fixtures/survey.dxf", unit_scale=0.001)
assert len(feats) == 42, "Expected 42 features from survey fixture"
for f in feats:
assert shape(f["geometry"]).is_valid
def test_ifc_route_preserves_globalids():
ids = [gid for gid, *_ in ifc_to_footprints("tests/fixtures/building.ifc")]
assert len(ids) == len(set(ids)), "GlobalIds must be unique per product"
assert all(len(gid) == 22 for gid in ids), "IFC GUIDs are 22-char base64"
Curate fixtures that exercise the failure modes above: a DXF with $INSUNITS=0, an IFC without IfcMapConversion, and an IFC containing at least one representation the kernel cannot evaluate.
Performance & Scale
The parse-cost gap between the two routes dominates capacity planning. DXF extraction is I/O-bound and reads vertices directly; a 200 MB survey DXF streams through ezdxf in seconds using generator traversal. IFC extraction is CPU-bound: every create_shape() call triangulates a parametric representation, and a large federated model with hundreds of thousands of elements can take minutes to hours.
For the DXF route, use generator-based iteration (for e in msp.query(...)) rather than list(msp) to keep memory flat on large files. For the IFC route, use ifcopenshell.geom.iterator with multiple worker threads to parallelise mesh evaluation, and filter by ifc_class before evaluating so you never triangulate elements you will discard:
# ifcopenshell>=0.8.0 — parallel geometry evaluation
import multiprocessing
import ifcopenshell.geom
settings = ifcopenshell.geom.settings()
settings.set(settings.USE_WORLD_COORDS, True)
it = ifcopenshell.geom.iterator(
settings, model, multiprocessing.cpu_count(),
include=model.by_type("IfcSlab"),
)
if it.initialize():
while True:
shape = it.get()
# process shape.geometry.verts here
if not it.next():
break
For repeated ingestion of the same federated model, cache evaluated footprints keyed by GlobalId so unchanged elements skip re-evaluation. Batch converted features into the GIS store in transactions of a few thousand rows to amortise index maintenance. When the downstream toolchain cannot run an IFC parser at all, the fallback is to pre-convert — covered in Converting IFC to DXF as a GIS Fallback.
FAQ
Does DXF carry a coordinate reference system?
No. A DXF file stores coordinates in local drawing units with no embedded CRS. The $INSUNITS header records the base unit but not the projection or datum. You must supply the CRS out of band — from a survey control sheet, a sidecar file, or a georeferencing block — before reprojecting into a GIS coordinate system.
Can IFC store georeferencing?
Yes. IFC4 and IFC4X3 define IfcMapConversion and IfcProjectedCRS, which record the EPSG code, eastings/northings offset, scale, and rotation to true north. When these entities are populated, an IFC model carries enough information to place its geometry directly into a projected CRS without external control data.
Which format is faster to parse for a GIS pipeline?
DXF is dramatically cheaper. Reading DXF vertices is a direct memory access with no geometry evaluation. IFC requires evaluating parametric representations — swept solids, boolean operations, B-Reps — into explicit meshes through a geometry kernel, which is CPU-bound and can be one to two orders of magnitude slower per element.
When should I convert IFC to DXF instead of ingesting IFC directly?
Convert IFC to DXF only when the downstream GIS toolchain accepts DXF exclusively and you cannot run a Python IFC parser in the pipeline. The conversion discards semantic attributes unless you carry them into layer names or XDATA, so ingest IFC directly whenever the toolchain supports it.
Related Pages
- Interoperability Decision Guides — the section overview framing format and library trade-offs for CAD/BIM-to-GIS pipelines
- Converting IFC to DXF as a GIS Fallback — when the toolchain only accepts DXF, evaluate IFC geometry and write entities with ezdxf
- Choosing ezdxf, pydwg, or ODA for Production — selecting the DXF/DWG parsing stack that feeds the DXF route
- GeoPackage vs PostGIS for CAD Output — choosing the storage target both routes converge on
- DXF Entity Structure Breakdown — the group-code data model behind the DXF route
- IFC4X3 Schema Mapping — the semantic schema behind the IFC route
- ifcopenshell Workflow — geometry evaluation and property-set extraction for IFC ingestion