Detecting and Routing DWG Version Compatibility in Python Pipelines
Detecting DWG version compatibility requires mapping the 6-byte ACADxxxx binary header at file offset 0x00 to a schema revision, then routing the file through a version-aware converter before any GIS or BIM ingestion stage runs. The .dwg extension alone carries no schema guarantee — each major AutoCAD release introduces new compression algorithms, object-ID widths, or cloud metadata blocks that silently corrupt naive parsers. As part of the DWG Proprietary Limitations workflow, header-first routing is the minimal safeguard that keeps interoperability pipelines deterministic.
How the 6-Byte Header Controls Parsing
Every DWG file stores a 6-byte ASCII version string starting at file offset 0x00. This code is the sole reliable signal of the binary schema in use — no reliable fallback exists once parsing has started. Autodesk introduced LZ77 section compression at AC1018 (2004), widened object IDs from 32-bit to 64-bit at AC1027 (2013), and embedded cloud-sync metadata blocks at AC1032 (2018). Each change breaks parsers that were not written against that schema.
Because the DWG specification is proprietary, reverse-engineered parsers — including the Open Design Alliance (ODA) libraries — sometimes fail silently on unsupported codes rather than raising exceptions. This means the failure mode you must guard against is not a crash but a silently truncated geometry set that passes downstream validation while missing entire layer groups or 3D solids. The Core Format Fundamentals & Schema Mapping layer in your pipeline is the right place to gate files by version before any geometry or attribute extraction runs.
ACAD Version Registry
The table below maps every ACAD code encountered in production to its AutoCAD release year, a routing compatibility tier, and the architectural changes that affect parser behaviour.
| ACAD Code | Release | Routing Tier | Key Schema Changes |
|---|---|---|---|
AC1009 |
R11 / R12 | Legacy — universal | Early binary/ASCII hybrid; all ODA versions accept this |
AC1012 |
R13 | Stable | First fully binary DWG; baseline for legacy GIS ingestion |
AC1014 |
R14 | Stable | Standardised entity dictionaries; widely interoperable |
AC1015 |
2000 | High | Object Enablers and proxy objects introduced |
AC1018 |
2004 | High | LZ77 section compression; 3D solid kernel update |
AC1021 |
2007 | Moderate | ACIS 7.0 kernel; extended XREF handling |
AC1024 |
2010 | Moderate | Dynamic blocks; parametric constraints added |
AC1027 |
2013 | Low | 64-bit object IDs; new hash-table structures |
AC1032 |
2018–2026 | Low | PDF underlay enhancement; cloud-sync metadata; current schema — AutoCAD 2019–2026 all use R2018 |
| Unknown | — | Reject / warn | No public DWG schema beyond AC1032 has shipped as of mid-2026; treat unrecognised codes as unconfirmed and apply the AC1032 fallback |
Files whose ACAD code exceeds your converter’s maximum supported version will either raise a parsing exception or — more dangerously — silently drop entities. The three-stage routing strategy is:
- Header inspection: read the first 6 bytes synchronously before loading the full binary into memory.
- Code lookup: match the header against the registry; emit a structured warning and apply the fallback version for any code not in the map.
- Authorised conversion: downgrade to a stable baseline (
AC1015orAC1018for most GIS consumers) using the ODA File Converter CLI — see the Open Design Alliance File Converter documentation for supported flags.
Production-Ready Script
The module below detects the DWG version, resolves the export target from the registry, calls odafileconverter, and enforces all four fallback paths. It requires Python 3.9+ and odafileconverter on $PATH.
# dwg_version_router.py
# Requires: Python >= 3.9, odafileconverter installed from Open Design Alliance
import os
import subprocess
import logging
from pathlib import Path
from typing import Optional
logging.basicConfig(level=logging.INFO, format="%(levelname)s: %(message)s")
logger = logging.getLogger(__name__)
# Maps 6-byte ACAD header bytes to the ODA File Converter target-version string.
# AC1032 is the current DWG schema; AutoCAD 2019–2026 all write this version.
VERSION_MAP: dict[bytes, str] = {
b"AC1009": "R12",
b"AC1012": "R13",
b"AC1014": "R14",
b"AC1015": "2000",
b"AC1018": "2004",
b"AC1021": "2007",
b"AC1024": "2010",
b"AC1027": "2013",
b"AC1032": "2018",
}
# Fallback target for any unrecognised ACAD code (e.g. future releases).
FALLBACK_VERSION = "2018"
# Minimum plausible DWG file size; rejects stubs and zero-byte placeholders.
MIN_DWG_BYTES = 1024
def read_dwg_version(file_path: Path) -> Optional[bytes]:
"""
Extract the 6-byte ACAD version header from a DWG file.
Returns None on I/O error or if the file is smaller than MIN_DWG_BYTES.
Does NOT raise — callers must treat None as a hard rejection signal.
"""
if not file_path.exists():
logger.error("File not found: %s", file_path)
return None
if file_path.stat().st_size < MIN_DWG_BYTES:
logger.error(
"File too small (%d bytes); rejecting before subprocess invocation: %s",
file_path.stat().st_size,
file_path,
)
return None
try:
with open(file_path, "rb") as fh:
header = fh.read(6)
if not header.startswith(b"AC"):
logger.error(
"Missing ACAD prefix in first 6 bytes of %s — not a DWG file",
file_path,
)
return None
return header
except OSError as exc:
logger.error("Failed to read header from %s: %s", file_path, exc)
return None
def convert_dwg_to_dxf(
input_path: Path,
output_dir: Path,
target_version: str = FALLBACK_VERSION,
timeout_seconds: int = 120,
) -> Path:
"""
Convert a DWG file to DXF at the given target version using odafileconverter.
Raises RuntimeError on non-zero exit, FileNotFoundError if the CLI is absent,
and TimeoutExpired if conversion exceeds timeout_seconds.
"""
output_dir.mkdir(parents=True, exist_ok=True)
safe_version = target_version.replace(" ", "")
output_path = output_dir / f"{input_path.stem}_v{safe_version}.dxf"
cmd = [
"odafileconverter",
str(input_path.parent), # ODA takes an input directory
str(output_dir),
"DXF",
target_version,
"0", # recurse flag (0 = no recursion)
"1", # audit flag (1 = audit on read)
]
logger.info(
"Converting %s → DXF (target schema: %s)", input_path.name, target_version
)
try:
result = subprocess.run(
cmd,
check=True,
capture_output=True,
text=True,
timeout=timeout_seconds,
)
if result.stdout:
logger.debug("odafileconverter stdout: %s", result.stdout.strip())
logger.info("Conversion complete: %s", output_path)
return output_path
except subprocess.CalledProcessError as exc:
logger.error(
"odafileconverter exited %d: %s", exc.returncode, exc.stderr.strip()
)
raise RuntimeError(
f"ODA converter failed with exit code {exc.returncode}"
) from exc
except FileNotFoundError:
logger.error(
"odafileconverter not found in PATH — install from Open Design Alliance"
)
raise
except subprocess.TimeoutExpired:
logger.error("Conversion timed out after %ds for %s", timeout_seconds, input_path)
raise
def process_dwg(file_path: Path, output_dir: Path) -> Path:
"""
End-to-end DWG version detection and DXF conversion pipeline.
Returns the Path to the converted DXF file ready for GIS or BIM ingestion.
"""
header = read_dwg_version(file_path)
if header is None:
raise ValueError(f"Cannot process {file_path}: invalid or unreadable DWG")
if header not in VERSION_MAP:
logger.warning(
"Unrecognised ACAD code %r — applying fallback target %s",
header.decode("ascii", errors="replace"),
FALLBACK_VERSION,
)
target = FALLBACK_VERSION
else:
target = VERSION_MAP[header]
logger.info(
"Detected %s → AutoCAD %s",
header.decode("ascii"),
target,
)
return convert_dwg_to_dxf(file_path, output_dir, target)
# ---------------------------------------------------------------------------
# CLI usage example
# ---------------------------------------------------------------------------
if __name__ == "__main__":
import sys
if len(sys.argv) != 3:
print("Usage: python dwg_version_router.py <input.dwg> <output_dir>")
sys.exit(1)
dxf_out = process_dwg(Path(sys.argv[1]), Path(sys.argv[2]))
print(f"Output: {dxf_out}")
Key implementation notes:
odafileconvertertakes an input directory, not an individual file path. The script passesinput_path.parentso only the target file is in scope; isolate files in a temporary directory if you process batches to avoid converting siblings.- The audit flag (
1) instructs ODA to run an internal consistency check on read, which surfaces corrupted entity references that would otherwise produce silent geometry gaps. capture_output=Trueprevents ODA’s verbose progress output from polluting the calling process’s stdout while still making stderr available for structured error logging.- Cache converted DXF outputs by hashing
input_path+target_versionto avoid redundant conversions in high-throughput pipelines. A simple{hash}.dxfnaming convention in a shared cache directory is sufficient.
Compatibility Matrix
| Component | Supported Range | Notes |
|---|---|---|
| Python | 3.9 – 3.13 | Uses dict[bytes, str] PEP 585 syntax; requires 3.9+ |
| odafileconverter | ODA 25.x – 26.x | Free download from Open Design Alliance; no Python binding — subprocess only |
| Input DWG schema | AC1009 – AC1032 |
AC1032 is the current ceiling; codes beyond it are treated as unknown |
| Output DXF target | R12 – 2018 | R2018 (AC1032) is the recommended target for ezdxf and most GIS consumers |
| Operating system | Linux, Windows, macOS | odafileconverter ships as a native binary per platform; PATH setup differs |
| ezdxf downstream | ezdxf >= 1.1.0 | Use ezdxf to parse the converted DXF for geometry extraction and layer filtering |
Fallback Strategies and Troubleshooting
1. FileNotFoundError: odafileconverter
The ODA binary is not on $PATH. On Linux, install the .run bundle from the Open Design Alliance and add the install directory to ~/.bashrc. On Docker-based pipelines, bake the binary into the image and verify with which odafileconverter at container startup.
2. Unrecognised ACAD code in the registry
If read_dwg_version returns a header not in VERSION_MAP (e.g. a future AC1035), the pipeline logs a warning and attempts AC1032 as the conversion target. If ODA also rejects that code, the only option is a read-only metadata pass: log the raw header bytes, the file size, and any layer names extractable from the ASCII sections of the DWG header, then quarantine the file for manual review.
3. Silent entity loss after conversion
Compare the layer count in the input DWG (readable via odafileconverter audit output or a lightweight binary scan for the LAYER section marker) against the layer count in the converted DXF parsed with ezdxf. A mismatch signals proxy objects that were silently dropped. Re-run with a lower target version (AC1015) to see if ODA can reconstruct the geometry without the proprietary extensions.
4. RuntimeError: ODA converter failed with exit code 1
ODA exit code 1 usually indicates an encrypted or password-protected DWG. There is no programmatic bypass — request an unprotected export from the source. Log the file hash and notify the upstream data provider.
5. Conversion timeout on large DWG files
Increase timeout_seconds in convert_dwg_to_dxf. For files above 500 MB, pre-split the DWG into model-space and paper-space portions using an authorised tool before invoking the converter; monolithic files with thousands of xrefs frequently exceed 120-second limits on commodity hardware.
Related Pages
- DWG Proprietary Limitations — parent: routing strategies, ODA environment setup, and licensing boundaries
- Core Format Fundamentals & Schema Mapping — section overview: format normalisation, schema mapping, and pipeline architecture
- DXF Entity Structure Breakdown — sibling guide: group code taxonomy and entity parsing after DWG-to-DXF conversion
- How to Parse DXF Headers with Python — downstream task: extracting
$ACADVER,$INSUNITS, and variable section values from the converted DXF output - pydwg Integration — related: alternative DWG parsing approach using pydwg without an ODA converter dependency