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.

DXF and IFC Ingestion Routes Converging on a GIS Store Diagram showing two parallel ingestion routes. The DXF route flows from a DXF input through ezdxf-to-shapely extraction. The IFC route flows from an IFC input through ifcopenshell geometry evaluation to footprints. Both routes feed a shared normalisation stage applying unit scale and CRS reprojection, which loads a GIS store such as PostGIS or GeoPackage. DXF input 2D survey / linework IFC input attributed BIM model ezdxf → shapely read entity vertices ifcopenshell.geom mesh → footprint Normalise unit scale CRS reproject GIS store PostGIS / GeoPackage

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 by ifcopenshell.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.