Converting IFC to DXF as a GIS Fallback in Python

To convert IFC to DXF as a fallback, evaluate each IFC product’s geometry with ifcopenshell.geom, then write the resulting vertices to a new DXF document with ezdxf — a footprint LWPOLYLINE for 2D toolchains, a MESH entity for 3D — and carry the GlobalId and name into layer names or XDATA so identity survives the loss of semantics. This is a deliberate downgrade, appropriate only when the receiving GIS toolchain accepts DXF exclusively and cannot run a Python IFC parser. When you can ingest IFC directly, do so; the trade-offs are laid out in the DXF vs IFC for GIS Ingestion guide.

How ifcopenshell and ezdxf Handle the Conversion

The conversion bridges two libraries with opposite jobs. ifcopenshell.geom.create_shape() reads a parametric IFC representation and evaluates it into an explicit triangulated mesh — a flat verts array of XYZ triples and a faces array of vertex indices, expressed in world coordinates when USE_WORLD_COORDS is set. ezdxf then writes those explicit primitives into DXF entities: add_lwpolyline for a 2D footprint ring, add_polyline3d for a 3D polyline, or add_mesh for a full faceted surface.

Neither library does the semantic bridging for you. IFC’s typed objects, property sets, and relationships have no DXF equivalent, so the conversion is lossy by construction. What ezdxf gives you to preserve identity is the layer name and XDATA — free-form string attachments keyed by a registered application id. Use them deliberately: the layer name is the most portable carrier because every DXF consumer preserves it, while XDATA survives only through XDATA-aware readers.

The value of doing this in code rather than a GUI export is control over exactly which representation you keep. Writing a full mesh for every wall produces a DXF that a 2D GIS tool cannot use and that bloats to hundreds of megabytes; writing a footprint keeps the file small and directly indexable. The ifcopenshell Workflow covers the geometry-settings object in depth.

Production-Ready Script

The following script converts an IFC model to a DXF file. For each building element it evaluates the mesh, writes a 2D footprint LWPOLYLINE on a layer named after the element class and GlobalId, optionally writes a full MESH, and attaches GlobalId and Name as XDATA.

# ifcopenshell>=0.8.0 | ezdxf>=1.1.0 | shapely>=2.0 | python>=3.9
import re
import ifcopenshell
import ifcopenshell.geom
import numpy as np
import ezdxf
from shapely.geometry import MultiPoint, Polygon

APPID = "IFC2DXF"  # registered XDATA application id

def _safe_layer(ifc_class: str, global_id: str) -> str:
    """DXF layer names forbid a few characters; sanitise and tag with the GUID."""
    stem = f"{ifc_class}_{global_id}"
    return re.sub(r'[<>/\\":;?*|=`,]', "_", stem)[:255]

def convert_ifc_to_dxf(
    ifc_path: str,
    dxf_path: str,
    ifc_class: str = "IfcBuildingElement",
    write_mesh: bool = False,
) -> int:
    """Convert IFC products to DXF footprints (+ optional meshes).
    Returns the number of products written."""
    model = ifcopenshell.open(ifc_path)
    settings = ifcopenshell.geom.settings()
    settings.set(settings.USE_WORLD_COORDS, True)

    doc = ezdxf.new("R2018")          # AC1032 — modern DXF for MESH support
    doc.appids.add(APPID)             # register the XDATA application id once
    msp = doc.modelspace()

    written = 0
    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 — skip, do not abort the file

        verts = np.array(shape.geometry.verts, dtype=float).reshape(-1, 3)
        faces = np.array(shape.geometry.faces, dtype=int).reshape(-1, 3)
        if len(verts) == 0:
            continue

        layer = _safe_layer(product.is_a(), product.GlobalId)
        name = getattr(product, "Name", None) or ""

        # 2D footprint: convex hull of the XY projection
        hull = MultiPoint(verts[:, :2]).convex_hull
        if isinstance(hull, Polygon):
            ring = list(hull.exterior.coords)
            lw = msp.add_lwpolyline(ring, close=True, dxfattribs={"layer": layer})
            lw.set_xdata(APPID, [(1000, product.GlobalId), (1000, name)])

        # Optional full 3D mesh
        if write_mesh and len(faces) > 0:
            mesh = msp.add_mesh(dxfattribs={"layer": layer})
            with mesh.edit_data() as data:
                data.vertices = [tuple(v) for v in verts]
                data.faces = [tuple(f) for f in faces]
            mesh.set_xdata(APPID, [(1000, product.GlobalId), (1000, name)])

        written += 1

    doc.saveas(dxf_path)
    return written

if __name__ == "__main__":
    n = convert_ifc_to_dxf("building.ifc", "building.dxf", write_mesh=False)
    print(f"Wrote {n} products to DXF")

Key implementation notes:

  • ezdxf.new("R2018") selects DXF version AC1032; the MESH entity requires R2000 or later, so do not target R12 if you intend to write meshes.
  • USE_WORLD_COORDS places evaluated geometry in model coordinates, so no manual placement-matrix multiplication is needed before writing to DXF.
  • The RuntimeError guard around create_shape() skips representations the kernel cannot evaluate instead of aborting the whole conversion — the same isolation pattern the direct-ingestion route uses.
  • doc.appids.add(APPID) must run once before any set_xdata call; writing XDATA under an unregistered application id raises DXFValueError.
  • XDATA group code 1000 stores strings; keep each value under 255 bytes or split across multiple 1000 records.
  • For accurate (non-convex) footprints, replace the convex hull with a Z-plane slice of the mesh — see the wall-outline technique linked below.

Compatibility Matrix

Component Supported range Notes
ifcopenshell >=0.8.0 Bundles the geometry kernel used by create_shape.
ezdxf >=1.1.0 add_mesh, add_lwpolyline, add_polyline3d, XDATA API.
shapely >=2.0 Convex hull / footprint construction.
DXF target version R2000R2018 MESH needs R2000+; R2018 (AC1032) recommended.
IFC schema IFC2X3 / IFC4 / IFC4X3 Georeferencing carries only from IFC4+.
Units Model native IFC length unit is usually metres; verify before writing.

Fallback Strategies

Convert defensively — the IFC-to-DXF downgrade fails in predictable ways, and each has a concrete mitigation.

1. Choose footprint over mesh by default

Most GIS toolchains that demand DXF are 2D. Writing full meshes produces geometry those tools silently ignore while inflating file size. Default to footprint LWPOLYLINE output and expose mesh export behind an explicit write_mesh=True flag used only when the target consumes 3D DXF.

2. Carry attributes through layers first, XDATA second

Encode the element class and GlobalId in the layer name (IfcWall_1a2B3c...) because every DXF reader preserves layers, and attach the full property payload as XDATA for readers that support it. If you must preserve richer attributes, write them as block attributes (ATTRIB) on an INSERT, which more GIS tools surface than XDATA. The layer-to-feature-class mapping on the receiving side is covered in Layer Mapping Logic.

3. Resolve units and CRS before writing

IFC lengths are usually metres, but always read the project length unit rather than assuming it. DXF has no CRS field, so if the IFC carried IfcMapConversion georeferencing, apply the offset and rotation to the vertices before writing — the DXF will otherwise land in local coordinates with the georeferencing lost. Reprojection into a target CRS is covered in CRS Normalization Workflows.

4. Stream huge models instead of buffering

A federated model can hold hundreds of thousands of elements. Do not accumulate all entities in memory before saving; write footprints as you evaluate, and for very large jobs split output across multiple DXF files by storey or by class. Use ifcopenshell.geom.iterator with multiple worker threads to keep mesh evaluation from becoming the bottleneck.

5. Degrade gracefully on empty geometry

Products with a Representation can still yield zero vertices (annotation-only or void elements). Guard on len(verts) == 0 and skip rather than writing a degenerate zero-area polyline that corrupts the downstream spatial index.

FAQ

What semantics are lost when converting IFC to DXF?

Everything the IFC schema encodes as typed objects and relationships: element classes, property sets, quantity sets, spatial containment, and classification references. DXF has no object model, so these survive only if you serialise them into layer names, XDATA, or block attributes, and even then they become opaque strings rather than queryable structure.

Should I export footprints or full meshes to DXF?

Export footprints when the GIS toolchain works in 2D — parcels, coverage maps, planning overlays — because a 2D LWPOLYLINE is what those tools index. Export full meshes with add_mesh only when the target genuinely consumes 3D DXF geometry, which is rare in GIS. When unsure, write footprints and keep the mesh export behind a flag.

How do I preserve the IFC GlobalId in DXF?

Two mechanisms: encode it in the layer name (for example IfcWall_1a2B3c) so it survives any DXF reader, and attach it as XDATA under a registered application id so it survives round-trips through XDATA-aware tools. Layer names are the more portable of the two because every DXF consumer preserves them.