Auditing and Repairing DXF Files with ezdxf

A DXF produced by a converter is not guaranteed to be structurally sound, and the failures it carries are the kind that raise deep inside a traversal rather than on open. Read it in recovery mode, audit it, separate the errors that were repaired from those that were not, and quarantine on the latter. The audit fixes structure; it says nothing about whether the content survived the conversion. This page is part of DWG Proprietary Limitations.

What Structural Damage Looks Like

A DXF document is a graph. Entities reference layers, linetypes and text styles by name; blocks reference their definitions; everything carries a handle that other objects use to point at it. A converter that stumbles can leave that graph inconsistent in ways the file format cannot express as an error: an entity on a layer that has no table entry, a handle that duplicates another, a block reference naming a definition that is not there.

The references that make a document a graph An entity does not stand alone: it names a layer, a linetype and a text style by name, may name a block definition, and carries a handle other objects point at. A converter that stumbles can leave any of these dangling, and none of it prevents the file being read as text — it surfaces later, deep inside a traversal. An entity references by name and by handle LAYER table LTYPE table BLOCK definition Handle targets

None of these prevent the file from being read as text. They surface when something traverses the graph — an attribute lookup that finds nothing, an iteration that raises on a dangling reference — which is usually several stages into a pipeline and far from the cause.

The recovery reader and the auditor exist for exactly this. Recovery loads a damaged document as far as it can rather than refusing; the auditor then walks the graph, repairs what it can, and reports what it cannot. The distinction between those two outputs is the whole basis of the gate.

Production-Ready Script

# ezdxf>=1.1.0, Python 3.9+
from __future__ import annotations

from dataclasses import dataclass, field
from pathlib import Path

import ezdxf
from ezdxf import recover


class UnrecoverableDXF(RuntimeError):
    pass


@dataclass
class AuditResult:
    path: str
    loaded: bool
    fixed: list[str] = field(default_factory=list)
    unrecoverable: list[str] = field(default_factory=list)
    entities: int = 0
    layers: int = 0

    @property
    def clean(self) -> bool:
        return self.loaded and not self.unrecoverable


def audit(path: str | Path) -> AuditResult:
    """Load defensively, audit, and separate repairs from unrecoverable errors."""
    path = str(path)
    result = AuditResult(path=path, loaded=False)
    try:
        # recover.readfile tolerates structural damage that readfile refuses.
        doc, auditor = recover.readfile(path)
    except ezdxf.DXFStructureError as exc:
        result.unrecoverable.append(f"unreadable: {exc}")
        return result

    result.loaded = True
    result.fixed = [str(e) for e in auditor.fixed_errors]
    result.unrecoverable = [str(e) for e in auditor.errors]

    # A second audit on the recovered document catches problems the recovery
    # reader introduced or could not see on the first pass.
    second = doc.audit()
    result.fixed.extend(str(e) for e in second.fixed_errors)
    result.unrecoverable.extend(str(e) for e in second.errors)

    result.entities = sum(1 for _ in doc.modelspace())
    result.layers = len(doc.layers)
    return result


def gate_batch(paths: list[str], quarantine: Path) -> tuple[list[str], list[AuditResult]]:
    """Accept clean files; quarantine the rest with their audit record."""
    quarantine.mkdir(parents=True, exist_ok=True)
    accepted: list[str] = []
    rejected: list[AuditResult] = []
    for p in paths:
        r = audit(p)
        if r.clean:
            accepted.append(p)
        else:
            rejected.append(r)
            (quarantine / (Path(p).stem + ".audit.txt")).write_text(
                "\n".join(["UNRECOVERABLE:", *r.unrecoverable, "", "FIXED:", *r.fixed])
            )
    return accepted, rejected


if __name__ == "__main__":
    accepted, rejected = gate_batch(["a.dxf", "b.dxf"], Path("./quarantine"))
    print(f"{len(accepted)} accepted, {len(rejected)} quarantined")
    for r in rejected:
        print(f"  {r.path}: {len(r.unrecoverable)} unrecoverable")
Two audit outputs with two different meanings The auditor produces two lists and they call for different responses. A repaired error is information: the document is now traversable and the repair rate across a batch is a signal about the converter. An unrecoverable error is a gate: the file goes to quarantine with its record rather than into the pipeline. Fixed errors — document is now traversable — record the count — a rising rate is a converter signal — not a rejection Unrecoverable errors — quarantine the file — write the record beside it — no partial geometry downstream — a rejection Structure only — content survival is a separate question.

Key implementation notes:

  • recover.readfile rather than readfile. The latter raises on damage the former loads through, and on converted files that difference is the whole point.
  • Two audit passes. The recovery reader repairs as it loads, and a second audit on the resulting document catches what the first pass could not see.
  • Repairs are recorded rather than discarded. A rising repair rate across a batch is a converter signal, and it is only visible if the repairs are counted.
  • Quarantine writes the audit record next to the file, so the rejection is self-explanatory without re-running anything.
  • Entity and layer counts are captured for the round-trip comparison against the source — structure and content are separate questions and both need answering.

Compatibility Matrix

Component Supported range Notes
ezdxf >=1.1.0 recover.readfile, Auditor
DXF revision R12 – R2018 recovery works across the range
Input converted or hand-edited DXF native exports rarely need it
Output audit record per rejected file written to quarantine
Cost small next to a full parse run on every converted file

Fallback Strategies

1. Recovery raises too. The file is not a DXF, or is truncated. Check the magic bytes and the file size before assuming a conversion problem.

What the audit answers and what it cannot Four questions about a converted file and which check answers each. The auditor speaks only to structure; whether the geometry, the units and the layer table survived the conversion are content questions answered by comparing against the source. A file can pass the audit perfectly and have lost most of its content. Question Kind Answered by Can the document be traversed? structure the auditor Did the geometry survive? content entity counts vs source Did the units survive? content header comparison Did the layers survive? content layer table comparison Three of the four need the source; run both kinds of check.

2. Repaired but empty. Structurally sound with no entities. The conversion lost the content; the audit cannot tell you that, which is why the entity count is captured alongside.

3. Errors that repeat across a batch. A converter behaviour rather than a file defect. Aggregate the audit messages across the batch — one recurring message across fifty files is a configuration to change.

4. Auditor fixes something you needed. Rare, but a repaired reference to a missing layer creates a layer that was not in the source. The repair log is what makes that visible.

5. Quarantine grows steadily. Treat the rate as a metric. A stable low rate is normal; a step change means something upstream changed, usually the converter version or a new source of files.

FAQ

What does the auditor actually fix?

Structural problems: invalid handles, entities pointing at tables that do not exist, malformed table entries, entities on layers that were never defined. It repairs the document graph so the file can be traversed. It does not fix content — a wrong coordinate, a missing units header or geometry that was lost during conversion are all perfectly valid structurally.

Should I audit every file or only failing ones?

Every file that came through a conversion. The audit is cheap relative to parsing, and its value is that it tells you a file needed repair — which is a signal about the conversion, not just about the file. A batch where the repair rate suddenly rises is a converter problem worth catching early.

Is a repaired file safe to use?

Structurally, yes: it can be traversed without raising. Whether its content is complete is a separate question the audit cannot answer. Pair the audit with the round-trip checks — entity counts, extents, layer table — described on the parent page, because those measure content and the audit measures structure.