Parsing DWG Layers with Python Scripts

Extracting DWG layer metadata requires an intermediate normalization step. Because Autodesk’s DWG format is proprietary and version-fragmented, direct binary parsing is unstable in production pipelines. The reliable approach converts DWG to DXF using the Open Design Alliance File Converter or libredwg, then queries layer names, visibility states, color indices, and entity counts via ezdxf. This is covered in depth in the DWG-to-Python Integration guide, which this page extends with a focused implementation for layer extraction.

DWG Layer Extraction Pipeline Data flows left to right: a DWG file is converted to DXF by ODA/libredwg, parsed by ezdxf to read the LAYER table, then serialized to a JSON manifest for downstream GIS or BIM systems. .dwg file R13 – R2024 ODA / libredwg DWG → DXF ezdxf doc.layers + modelspace() JSON manifest GIS / BIM ingestion

How ezdxf Handles the DWG LAYER Table

When the ODA File Converter or libredwg converts a DWG to DXF, it translates the DWG LAYER object store into a DXF LAYER table section. Every layer entry carries:

  • Name — the string identifier used as entity.dxf.layer on all entities in that layer.
  • Color index (ACI) — an integer 1–255 referencing AutoCAD Color Index. A value of 256 means “by layer” (inherits from the drawing default). A negative value signals the layer is turned off.
  • Linetype reference — a string name pointing to a LTYPE table entry.
  • Lineweight — an integer code (e.g., 25 = 0.25 mm); -1 means “default”.
  • Flags — frozen, locked, plot-suppressed states encoded in the 70 and 290 group codes.

ezdxf exposes these through the Layer entity object. In ezdxf >= 1.0, the visibility state methods (is_frozen(), is_locked(), is_off()) are callable methods, not plain attribute properties. Calling them without parentheses always returns the bound method object (truthy), which silently masks frozen/off states — a common bug when upgrading from pre-1.0 code.

ezdxf does not reconstruct XREF-inherited layer states. When a DWG uses external references whose layers override visibility, the converted DXF usually flattens or drops those overrides. If layer inheritance matters for your pipeline, bind all XREFs before conversion at the CAD authoring stage.

Production-Ready Script

The script below reads a converted DXF, extracts layer properties in a single modelspace pass, filters system layers, and writes a JSON manifest sorted by entity count. It handles missing attributes with safe fallbacks and logs warnings for non-standard states.

# parse_layers.py — requires ezdxf>=1.1.0, Python 3.9+
import sys
import json
import logging
from pathlib import Path
from typing import Dict, List, Any
from collections import defaultdict

import ezdxf
from ezdxf.entities import Layer

logging.basicConfig(level=logging.INFO, format="%(levelname)s: %(message)s")
logger = logging.getLogger(__name__)

# Layers present in every DWG/DXF; rarely contain meaningful geometry
SYSTEM_LAYERS = {"0", "defpoints"}


def parse_dwg_layers(
    dxf_path: str,
    output_json: str = "layer_manifest.json",
) -> Dict[str, Any]:
    """
    Extract layer metadata, states, and entity counts from a DWG-converted DXF.

    Args:
        dxf_path:    Path to the converted .dxf file.
        output_json: Destination for the JSON manifest.

    Returns:
        The manifest dict (also written to output_json).
    """
    path = Path(dxf_path)
    if not path.exists():
        raise FileNotFoundError(f"DXF not found: {path}")

    try:
        doc = ezdxf.readfile(str(path))
    except ezdxf.DXFStructureError as exc:
        logger.error("Corrupted or unsupported DXF: %s", exc)
        sys.exit(1)

    layers_db = doc.layers
    entity_counts: Dict[str, int] = defaultdict(int)

    # Single-pass entity count — avoids materialising the full modelspace list
    for entity in doc.modelspace():
        try:
            name = entity.dxf.layer
            if name:
                entity_counts[name] += 1
        except AttributeError:
            continue

    layer_manifest: List[Dict[str, Any]] = []

    for layer_name in layers_db.keys():
        layer: Layer = layers_db.get(layer_name)
        if layer is None:
            continue

        # Skip AutoCAD system and annotation-only layers
        if layer_name.startswith("*") or layer_name.lower() in SYSTEM_LAYERS:
            logger.debug("Skipping system layer: %s", layer_name)
            continue

        # is_frozen/is_locked/is_off are METHOD CALLS in ezdxf>=1.0
        try:
            is_frozen = layer.is_frozen()
            is_locked = layer.is_locked()
            is_off    = layer.is_off()
        except TypeError:
            # Fallback for ezdxf<1.0 where these are plain bool properties
            is_frozen = bool(layer.is_frozen)  # type: ignore[attr-defined]
            is_locked = bool(layer.is_locked)  # type: ignore[attr-defined]
            is_off    = bool(layer.is_off)      # type: ignore[attr-defined]
            logger.warning(
                "Layer %s: is_frozen/locked/off are properties, not methods — "
                "upgrade to ezdxf>=1.0 for reliable state detection.",
                layer_name,
            )

        props = {
            "name":         layer_name,
            "color_index":  layer.dxf.get("color", 256),
            "linetype":     layer.dxf.get("linetype", "Continuous"),
            "lineweight":   layer.dxf.get("lineweight", -1),
            "frozen":       is_frozen,
            "locked":       is_locked,
            "off":          is_off,
            "plot":         bool(layer.dxf.get("plot", True)),
            "entity_count": entity_counts.get(layer_name, 0),
        }
        layer_manifest.append(props)

    # Most-populated layers first — useful for quick visual scanning
    layer_manifest.sort(key=lambda x: x["entity_count"], reverse=True)

    manifest = {
        "source_file":  str(path),
        "total_layers": len(layer_manifest),
        "layers":       layer_manifest,
    }

    # Validation gate: warn if the manifest is empty (likely a converter failure)
    if manifest["total_layers"] == 0:
        logger.warning(
            "No user layers found in %s — verify the DWG→DXF conversion succeeded "
            "and that the file contains model-space content.",
            path,
        )

    try:
        with open(output_json, "w", encoding="utf-8") as f:
            json.dump(manifest, f, indent=2, ensure_ascii=False)
        logger.info("Manifest written: %s (%d layers)", output_json, manifest["total_layers"])
    except OSError as exc:
        logger.error("Failed to write JSON: %s", exc)
        sys.exit(1)

    return manifest


if __name__ == "__main__":
    if len(sys.argv) < 2:
        print("Usage: python parse_layers.py <input.dxf> [output.json]")
        sys.exit(1)
    dxf_input   = sys.argv[1]
    json_output = sys.argv[2] if len(sys.argv) > 2 else "layer_manifest.json"
    parse_dwg_layers(dxf_input, json_output)

Key implementation notes:

  • is_frozen(), is_locked(), and is_off() are method calls in ezdxf >= 1.0. Pre-1.0 versions exposed them as plain boolean properties. The try/except block in the script handles both without crashing — but check the ezdxf changelog if you see TypeError: 'bool' object is not callable.
  • color_index == 256 means “by layer” (inherits the drawing default). A negative ACI value (e.g., -1) signals the layer is turned off in some older DXF versions — normalize both before comparison.
  • lineweight == -1 is the DXF sentinel for “default lineweight”. Map it to your target thickness (e.g., 0.25 mm) during downstream processing rather than passing -1 into a GIS renderer.
  • For files exceeding 500 MB, iterating doc.modelspace() as a generator (as done above) avoids loading all entities into memory at once. Never replace this with list(doc.modelspace()) in production.
  • The total_layers == 0 warning catches silent converter failures. An empty manifest that proceeds silently into a GIS pipeline will produce empty feature classes with no error trace.

Compatibility Matrix

Component Supported Range Notes
Python 3.9 – 3.12 3.13+ may require wheel rebuilds for C-extension dependencies
ezdxf >= 1.1.0 is_frozen() / is_locked() / is_off() are methods from 1.0; color attribute semantics stabilised in 1.1
DWG format R13 – R2024 (AC1012 – AC1032) R2021+ files require ODA 2023+ or libredwg 0.12.5+; no public spec beyond AC1032 as of mid-2026
ODA File Converter 2023+ Use 2024 for R2021/R2024 source files; earlier versions silently omit some layer flag group codes
libredwg >= 0.12.5 GPL-licensed; lags 6–12 months on newest format versions; adequate for R2018 and earlier
OS Windows 10/11, Ubuntu 20.04+, macOS 12+ ODA CLI ships native Win/Linux binaries; macOS requires ARM builds or Rosetta 2

Fallback Strategies and Troubleshooting

1. Manifest is empty (total_layers == 0) after conversion

The converter likely wrote an empty or structurally broken DXF. Re-run ODA with audit enabled (audit=1 in the CLI arguments) and check its log for group-code errors. If libredwg was used, try the same file through ODA — newer DWG revisions often trip libredwg’s parser before ODA does.

2. DXFStructureError on ezdxf.readfile()

The DXF was written with a DXF version string the installed ezdxf does not recognise (e.g., a future AC103x code). Pin ODA output to ACAD2018 (AC1032) to produce a stable target version. Pass dxf_version="ACAD2018" to the converter subprocess call documented in the DWG-to-Python Integration workflow.

3. Layer states all read as False (never frozen/off)

You are running ezdxf < 1.0 and calling layer.is_frozen() as a method, but in that version it is a plain bool property. Upgrade to ezdxf >= 1.1.0. Alternatively, read the raw group-code flags with layer.dxf.get("flags", 0) and test bit 1 (frozen = flags & 1), bit 4 (frozen in new viewport = flags & 4).

4. entity.dxf.layer raises AttributeError

Some non-graphic entities (e.g., ACAD_PROXY_ENTITY, dimension overrides) do not carry a layer attribute in their DXF group codes. The try/except AttributeError guard in the script handles these silently. If you need to audit which entity types are skipped, log entity.dxftype() inside the except block.

5. Color index is negative in the output JSON

AutoCAD encodes a layer-off state by negating the ACI color value in older DXF versions (group code 62). Wrap the color read as abs(layer.dxf.get("color", 256)) if you only need the display color, and use is_off() separately to record visibility state. Do not conflate them — both signals carry independent meaning in Civil 3D files.