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.
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")
Key implementation notes:
recover.readfilerather thanreadfile. 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.
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.
Related Pages
- DWG Proprietary Limitations — parent reference on why converted files need auditing at all
- Detecting and Routing DWG Version Compatibility in Python Pipelines — the routing step that precedes this audit
- Batch Converting DWG to DXF with the ODA File Converter — the conversion whose output this audit gates