Batch Converting DWG to DXF with the ODA File Converter
DWG is Autodesk’s closed, version-fragmented binary format, and no pure-Python reader parses it reliably across releases — so the production route into ezdxf is to convert DWG to DXF first with the free ODA File Converter, then parse the DXF. The converter is a Qt GUI application driven through a seven-argument command line, so on a headless server you run it under xvfb-run -a, wrap it in subprocess with a timeout, and verify every output before loading it. This page is part of the DWG-to-Python Integration workflow within the broader Python Parsing & Geometry Extraction pipeline, and it feeds directly into Parsing DWG Layers with Python Scripts, which consumes the DXF this conversion produces.
How the ODA File Converter Handles DWG Batches
The ODA File Converter is a command-line-capable desktop application. Its CLI takes seven positional arguments, all quoted:
ODAFileConverter "<inDir>" "<outDir>" "<outVer>" "<outFormat>" "<recurse>" "<audit>" "<filter>"
inDir— the folder containing source drawings (the converter works on folders, not single files).outDir— the destination folder for converted output.outVer— the output version string, for exampleACAD2018orACAD2013.outFormat—DXForDWG(orDXB).recurse—1to descend into subfolders,0for the top level only.audit—1to run the recover/audit pass on each file,0to skip it.filter— an input glob such as*.DWG.
Because it drives a Qt interface, the process needs an X display even when it produces no visible window. On a headless Linux worker there is no display, so the converter either exits immediately or blocks. xvfb-run -a allocates a throwaway virtual framebuffer and picks a free display number, letting the tool initialize normally. What the converter does not do well is report per-file failures: it frequently returns exit code 0 even when a specific drawing failed. Treat the process exit code as advisory only, and verify output existence yourself.
The two-step DWG to DXF to ezdxf path also absorbs version differences. The converter reads everything from R14 through the latest AutoCAD release and writes a single normalized DXF version you choose, so your parser only ever sees one schema. That normalization is the entire reason this indirection exists — parsing DWG directly would mean tracking every AC10xx binary revision yourself.
Production-Ready Script
The wrapper converts a directory of DWG files, runs the converter headlessly with a timeout, then verifies and loads each DXF, returning a structured manifest.
# ezdxf>=1.1.0, Python 3.9+ ; requires ODA File Converter + xvfb on the host
from __future__ import annotations
import shutil
import subprocess
from pathlib import Path
import ezdxf
from ezdxf import recover
def batch_dwg_to_dxf(
in_dir: str,
out_dir: str,
out_version: str = "ACAD2018",
audit: bool = True,
recurse: bool = False,
timeout_s: int = 900,
oda_binary: str = "ODAFileConverter",
) -> dict:
"""Batch-convert a folder of DWG files to DXF, then validate each with ezdxf.
Runs the ODA File Converter headlessly under xvfb-run. Returns a manifest
dict with 'succeeded' and 'failed' lists. Raises RuntimeError if the
converter binary or xvfb-run is missing, or if the process times out.
"""
in_path = Path(in_dir).resolve()
out_path = Path(out_dir).resolve()
out_path.mkdir(parents=True, exist_ok=True)
if shutil.which(oda_binary) is None:
raise RuntimeError(f"{oda_binary} not found on PATH.")
if shutil.which("xvfb-run") is None:
raise RuntimeError("xvfb-run not found; install xvfb for headless runs.")
# ODA CLI positional args:
# inDir outDir outVer outFormat recurse(0|1) audit(1|0) filter
cmd = [
"xvfb-run", "-a",
oda_binary,
str(in_path),
str(out_path),
out_version,
"DXF",
"1" if recurse else "0",
"1" if audit else "0",
"*.DWG",
]
try:
proc = subprocess.run(
cmd,
capture_output=True,
text=True,
timeout=timeout_s,
)
except subprocess.TimeoutExpired as exc:
raise RuntimeError(
f"Conversion timed out after {timeout_s}s — likely a corrupt DWG."
) from exc
# Exit code is unreliable; verify outputs per file instead.
pattern = "**/*.DWG" if recurse else "*.DWG"
dwg_files = sorted(
p for p in in_path.glob(pattern) if p.suffix.lower() == ".dwg"
)
manifest: dict = {
"returncode": proc.returncode,
"succeeded": [],
"failed": [],
}
for dwg in dwg_files:
dxf = out_path / (dwg.stem + ".dxf")
if not dxf.exists():
manifest["failed"].append(
{"source": dwg.name, "reason": "no DXF produced"}
)
continue
try:
# Fast path; fall back to recover for structurally damaged output.
try:
doc = ezdxf.readfile(str(dxf))
except ezdxf.DXFStructureError:
doc, auditor = recover.readfile(str(dxf))
if auditor.has_errors:
raise ezdxf.DXFStructureError(
f"{len(auditor.errors)} unrecovered errors"
)
entity_count = sum(1 for _ in doc.modelspace())
manifest["succeeded"].append(
{"source": dwg.name, "dxf": dxf.name, "entities": entity_count}
)
except Exception as exc: # noqa: BLE001 — record any load failure
manifest["failed"].append(
{"source": dwg.name, "reason": f"ezdxf load failed: {exc}"}
)
return manifest
if __name__ == "__main__":
import json
import sys
if len(sys.argv) < 3:
print("Usage: python batch_dwg_to_dxf.py <in_dir> <out_dir> [version]")
sys.exit(1)
version = sys.argv[3] if len(sys.argv) > 3 else "ACAD2018"
result = batch_dwg_to_dxf(sys.argv[1], sys.argv[2], out_version=version)
print(json.dumps(result, indent=2))
if result["failed"]:
sys.exit(2) # non-zero exit so CI catches partial batches
Key implementation notes:
xvfb-run -ais mandatory on headless hosts. The-aflag auto-selects a free display number, avoiding collisions when several conversion workers run concurrently. Without it the Qt front end fails to initialize and the process exits before writing anything.- Verify outputs, never trust the exit code. The converter returns
0on partial failures, so the script enumerates source DWGs and checks that a matching DXF exists and opens. A missing or unreadable DXF is recorded as a failure with a reason. ezdxf.recover.readfile()is the fallback loader. Whenreadfile()raisesDXFStructureErroron an audited-but-imperfect output,recover.readfile()returns the document plus anauditor; inspectauditor.has_errorsbefore trusting the geometry.- The timeout protects the batch. A single corrupt DWG can make the converter spin forever.
subprocess.run(..., timeout=...)bounds the whole run; for large folders, convert in smaller batches so one timeout does not discard hours of work. - A non-zero process exit on failures lets CI treat a partial batch as a build failure rather than silently ingesting incomplete data downstream.
Compatibility Matrix
| Component | Supported range | Notes |
|---|---|---|
| Python | 3.9 – 3.12 | Uses subprocess, pathlib, shutil.which; no version-specific syntax beyond type hints. |
| ODA File Converter | 2023+ | Use 2024 for R2021/R2024 sources; the seven-argument CLI signature is stable across recent releases. |
| Output version | ACAD2013 (AC1027) – ACAD2018 (AC1032) |
ACAD2018 recommended for widest ezdxf support; drop to ACAD2013 for proxy/legacy consumers. |
ezdxf |
≥ 1.1.0 | recover.readfile() returns (doc, auditor); DXFStructureError guards the fast path. |
xvfb |
any recent | Provides the virtual display the Qt-based converter needs on headless Linux. |
| OS | Linux (server), Windows, macOS | xvfb-run is Linux-only; on Windows/macOS the GUI display already exists and xvfb-run is omitted. |
The converter is a free download from the Open Design Alliance; its version support governs which DWG revisions you can ingest.
Fallback Strategies
DWG batch conversion fails in five recurring ways. Handle them in order.
1. Output version target. Default to ACAD2018 (AC1032) for the broadest ezdxf coverage. Choose ACAD2013 (AC1027) when a downstream tool rejects AC1032, or when proxy entities from a vertical product round-trip more cleanly through the older schema. The version fragmentation behind these codes is documented in Understanding DWG Version Compatibility.
2. Audit repair. Set audit=True so the converter runs its recover pass on each drawing, fixing broken handles and dangling references that would otherwise crash ezdxf. Auditing is slower, so for known-clean exports you can disable it — but keep it on for any drawings of unknown provenance.
3. Proxy entities. Custom objects from Civil 3D, Plant 3D, or third-party applications convert to ACAD_PROXY_ENTITY placeholders that carry a cached graphic but no editable geometry. ezdxf reads them without error, yet their true geometry is unavailable. Detect them with msp.query("ACAD_PROXY_ENTITY") and route them per Handling DWG Proxy Entities During Conversion rather than assuming the DXF is complete.
4. XREF resolution. External references are not embedded by the converter; a drawing that relies on XREFs converts to a DXF whose referenced geometry is missing. Bind XREFs at the CAD authoring stage before conversion, or ensure the referenced DWGs sit in the same input folder so their content is available.
5. Headless display errors. Messages like could not connect to display or QXcbConnection mean the process ran without xvfb-run, or xvfb is not installed. Confirm xvfb-run is on PATH and prefix the command with xvfb-run -a; the script’s shutil.which guard fails fast with a clear message when it is absent.
FAQ
Why does the ODA File Converter need xvfb on a server?
The ODA File Converter is a Qt GUI application even in command-line mode, so it requires an X display to initialize. On a headless Linux server there is no display, and the process exits immediately or hangs. Running it under xvfb-run -a provides a virtual framebuffer so it starts without a physical screen, and -a auto-picks a free display so parallel workers do not collide.
Should I target ACAD2018 or ACAD2013 as the output version?
Target ACAD2018 (AC1032) for the widest ezdxf support and modern entity coverage. Drop to ACAD2013 (AC1027) only when a downstream tool cannot read AC1032, or when proxy entities from a vertical product survive more cleanly in the older schema. Both are stable ezdxf read targets.
Does the converter report per-file failures in its exit code?
Not reliably. The ODA File Converter often returns exit code 0 even when individual files fail to convert. The robust check is to verify that an output DXF exists and opens for every input DWG, treating a missing or unreadable output as a failure regardless of the process exit code — which is exactly what the manifest loop in the script does.
How do I stop a hung conversion from blocking the batch?
Wrap the subprocess call with a timeout and catch subprocess.TimeoutExpired. A single corrupt DWG can make the converter spin indefinitely, so a per-batch timeout — or converting one file at a time by pointing the input folder at a single drawing — keeps one bad file from stalling the whole run.
Related Pages
- DWG-to-Python Integration — parent workflow covering version detection, headless conversion, and the full DWG parsing landscape
- Parsing DWG Layers with Python Scripts — sibling guide that reads the LAYER table from the DXF this conversion produces
- Python Parsing & Geometry Extraction — top-level pipeline covering ingestion, extraction, and serialization across CAD, BIM, and GIS
- Understanding DWG Version Compatibility — cross-topic reference for the AC10xx version codes that decide your output target
- Handling DWG Proxy Entities During Conversion — cross-topic guide for the proxy placeholders that survive DWG to DXF conversion