drawing-analyzer
Analyze construction drawings to extract dimensions, annotations, symbols, and metadata. Support quantity takeoff and design review automation.
Install / Use
npx skills add datadrivenconstruction/DDC_Skills_for_AI_Agents_in_Construction --skill drawing-analyzerInstalls into whichever agent you are using.
SKILL.md
Installable skill definition
Quality Score
Category
AutomationSupported Platforms
Our assessment of drawing-analyzer
drawing-analyzer scores 89/100 on our quality scale, 1474th of 2,863 Automation skills we index.
Its SKILL.md is 16 KB long, well organised into 12 sections with 3 code examples: a thorough specification that gives an agent plenty to work with.
It has 333 GitHub stars, a meaningful sign that others use it.
Maintenance, license and trust
- The repository was last updated about 2 months ago, so drawing-analyzer is actively maintained.
- It is released under the MIT license, a permissive license that allows use, modification and commercial use with attribution.
- Its trust signals score 100/100, with no cautions. These come from repository metadata, not a code audit — read the skill file before letting an agent act on it.
drawing-analyzer compared with similar skills
All 4 of these similar skills score higher than drawing-analyzer; compare them before choosing.
| Skill | Score | Stars | Updated | Format |
|---|---|---|---|---|
| drawing-analyzer (this skill)by datadrivenconstruction | 89 | 333 | 2mo ago | SKILL.md |
| Agent-Reachby Panniantong | 100 | 106.4k | 5d ago | CLAUDE.md |
| taste-skillby Leonxlnx | 100 | 105.7k | today | CLAUDE.md |
| headroomby headroomlabs-ai | 100 | 78.3k | today | CLAUDE.md |
| Scraplingby D4Vinci | 100 | 90.4k | today | MCP Server |
Frequently asked questions
- How do I install drawing-analyzer?
- Run
npx skills add datadrivenconstruction/DDC_Skills_for_AI_Agents_in_Construction --skill drawing-analyzer. The install tabs above show the steps for each supported agent. - Which AI agents does drawing-analyzer work with?
- It is written for Universal, as a SKILL.md file. Other agents that read the same format can often use it too.
- Is drawing-analyzer safe to use?
- It is MIT-licensed and scores 100/100 on trust signals. Skills are instructions an agent will follow, so read the file before installing it and do not approve commands you do not understand.
- Is drawing-analyzer still maintained?
- The repository was last updated about 2 months ago, so drawing-analyzer is actively maintained.
Skill content
View source on GitHubname: "drawing-analyzer" description: "Analyze construction drawings to extract dimensions, annotations, symbols, and metadata. Support quantity takeoff and design review automation." homepage: "https://datadrivenconstruction.io" metadata: {"openclaw": {"emoji": "📑", "os": ["darwin", "linux", "win32"], "homepage": "https://datadrivenconstruction.io", "requires": {"bins": ["python3"]}}}
Drawing Analyzer for Construction
Overview
Analyze construction drawings (PDF, DWG) to extract dimensions, annotations, symbols, title block data, and support automated quantity takeoff and design review.
Business Case
Drawing analysis automation enables:
- Faster Takeoffs: Extract quantities from drawings
- Quality Control: Verify drawing completeness
- Data Extraction: Pull metadata for project systems
- Design Review: Automated checking against standards
Technical Implementation
from dataclasses import dataclass, field
from typing import List, Dict, Any, Optional, Tuple
import re
import pdfplumber
from pathlib import Path
@dataclass
class TitleBlockData:
project_name: str
project_number: str
sheet_number: str
sheet_title: str
discipline: str
scale: str
date: str
revision: str
drawn_by: str
checked_by: str
approved_by: str
@dataclass
class Dimension:
value: float
unit: str
dimension_type: str # linear, angular, radial
location: Tuple[float, float]
associated_text: str
@dataclass
class Annotation:
text: str
annotation_type: str # note, callout, tag, keynote
location: Tuple[float, float]
references: List[str]
@dataclass
class Symbol:
symbol_type: str # door, window, equipment, etc.
tag: str
location: Tuple[float, float]
properties: Dict[str, Any]
@dataclass
class DrawingAnalysisResult:
file_name: str
title_block: Optional[TitleBlockData]
dimensions: List[Dimension]
annotations: List[Annotation]
symbols: List[Symbol]
scale_factor: float
drawing_area: Tuple[float, float]
quality_issues: List[str]
class DrawingAnalyzer:
"""Analyze construction drawings for data extraction."""
# Common dimension patterns
DIMENSION_PATTERNS = [
r"(\d+'-\s*\d+(?:\s*\d+/\d+)?\"?)", # Feet-inches: 10'-6", 10' - 6 1/2"
r"(\d+(?:\.\d+)?)\s*(?:mm|cm|m|ft|in)", # Metric/imperial with unit
r"(\d+'-\d+\")", # Compact feet-inches
r"(\d+)\s*(?:SF|LF|CY|EA)", # Quantity dimensions
]
# Common annotation patterns
ANNOTATION_PATTERNS = {
'keynote': r'^\d{1,2}[A-Z]?$', # 1A, 12, 5B
'room_tag': r'^(?:RM|ROOM)\s*\d+',
'door_tag': r'^[A-Z]?\d{2,3}[A-Z]?$',
'grid_line': r'^[A-Z]$|^\d+$',
'elevation': r'^(?:EL|ELEV)\.?\s*\d+',
'detail_ref': r'^\d+/[A-Z]\d+',
}
# Scale patterns
SCALE_PATTERNS = [
r"SCALE:\s*(\d+(?:/\d+)?)\s*[\"']\s*=\s*(\d+)\s*['\-]", # 1/4" = 1'-0"
r"(\d+):(\d+)", # 1:100
r"NTS|NOT TO SCALE",
]
def __init__(self):
self.results: Dict[str, DrawingAnalysisResult] = {}
def analyze_pdf_drawing(self, pdf_path: str) -> DrawingAnalysisResult:
"""Analyze a PDF drawing."""
path = Path(pdf_path)
all_text = ""
dimensions = []
annotations = []
symbols = []
quality_issues = []
with pdfplumber.open(pdf_path) as pdf:
for page in pdf.pages:
# Extract text
text = page.extract_text() or ""
all_text += text + "\n"
# Extract dimensions
page_dims = self._extract_dimensions(text)
dimensions.extend(page_dims)
# Extract annotations
page_annots = self._extract_annotations(text)
annotations.extend(page_annots)
# Extract from tables (often contain schedules)
tables = page.extract_tables()
for table in tables:
symbols.extend(self._parse_schedule_table(table))
# Parse title block
title_block = self._extract_title_block(all_text)
# Determine scale
scale_factor = self._determine_scale(all_text)
# Quality checks
quality_issues = self._check_drawing_quality(
title_block, dimensions, annotations
)
result = DrawingAnalysisResult(
file_name=path.name,
title_block=title_block,
dimensions=dimensions,
annotations=annotations,
symbols=symbols,
scale_factor=scale_factor,
drawing_area=(0, 0), # Would need image analysis
quality_issues=quality_issues
)
self.results[path.name] = result
return result
def _extract_dimensions(self, text: str) -> List[Dimension]:
"""Extract dimensions from text."""
dimensions = []
for pattern in self.DIMENSION_PATTERNS:
matches = re.findall(pattern, text)
for match in matches:
value, unit = self._parse_dimension_value(match)
if value > 0:
dimensions.append(Dimension(
value=value,
unit=unit,
dimension_type='linear',
location=(0, 0),
associated_text=match
))
return dimensions
def _parse_dimension_value(self, dim_text: str) -> Tuple[float, str]:
"""Parse dimension text to value and unit."""
dim_text = dim_text.strip()
# Feet and inches: 10'-6"
ft_in_match = re.match(r"(\d+)'[-\s]*(\d+)?(?:\s*(\d+)/(\d+))?\"?", dim_text)
if ft_in_match:
feet = int(ft_in_match.group(1))
inches = int(ft_in_match.group(2) or 0)
if ft_in_match.group(3) and ft_in_match.group(4):
inches += int(ft_in_match.group(3)) / int(ft_in_match.group(4))
return feet * 12 + inches, 'in'
# Metric with unit
metric_match = re.match(r"(\d+(?:\.\d+)?)\s*(mm|cm|m)", dim_text)
if metric_match:
return float(metric_match.group(1)), metric_match.group(2)
# Just a number
num_match = re.match(r"(\d+(?:\.\d+)?)", dim_text)
if num_match:
return float(num_match.group(1)), ''
return 0, ''
def _extract_annotations(self, text: str) -> List[Annotation]:
"""Extract annotations from text."""
annotations = []
lines = text.split('\n')
for line in lines:
line = line.strip()
if not line:
continue
for annot_type, pattern in self.ANNOTATION_PATTERNS.items():
if re.match(pattern, line, re.IGNORECASE):
annotations.append(Annotation(
text=line,
annotation_type=annot_type,
location=(0, 0),
references=[]
))
break
# General notes
if line.startswith(('NOTE:', 'SEE ', 'REFER TO', 'TYP', 'U.N.O.')):
annotations.append(Annotation(
text=line,
annotation_type='note',
location=(0, 0),
references=[]
))
return annotations
def _extract_title_block(self, text: str) -> Optional[TitleBlockData]:
"""Extract title block information."""
# Common title block patterns
patterns = {
'project_name': r'PROJECT(?:\s*NAME)?:\s*(.+?)(?:\n|$)',
'project_number': r'(?:PROJECT\s*)?(?:NO|NUMBER|#)\.?:\s*(\S+)',
'sheet_number': r'SHEET(?:\s*NO)?\.?:\s*([A-Z]?\d+(?:\.\d+)?)',
'sheet_title': r'SHEET\s*TITLE:\s*(.+?)(?:\n|$)',
'scale': r'SCALE:\s*(.+?)(?:\n|$)',
'date': r'DATE:\s*(\d{1,2}[/-]\d{1,2}[/-]\d{2,4})',
'revision': r'REV(?:ISION)?\.?:\s*(\S+)',
'drawn_by': r'(?:DRAWN|DRN)\s*(?:BY)?:\s*(\S+)',
'checked_by': r'(?:CHECKED|CHK)\s*(?:BY)?:\s*(\S+)',
}
extracted = {}
for field, pattern in patterns.items():
match = re.search(pattern, text, re.IGNORECASE)
extracted[field] = match.group(1).strip() if match else ''
# Determine discipline from sheet number
sheet_num = extracted.get('sheet_number', '')
discipline = ''
if sheet_num:
prefix = sheet_num[0].upper() if sheet_num[0].isalpha() else ''
discipline_map = {
'A': 'Architectural', 'S': 'Structural', 'M': 'Mechanical',
'E': 'Electrical', 'P': 'Plumbing', 'C': 'Civil',
'L': 'Landscape', 'I': 'Interior', 'F': 'Fire Protection'
}
discipline = discipline_map.get(prefix, '')
return TitleBlockData(
project_name=extracted.get('project_name', ''),
project_number=extracted.get('project_number', ''),
sheet_number=sheet_num,
sheet_title=extracted.get('sheet_title', ''),
discipline=discipline,
scale=extracted.get('scale', ''),
date=extracted.get('date', ''),
revision=extracted.get('revision', ''),
drawn_by=extracted.get('drawn_by', ''),
checked_by=extracted.get('checked_by', ''),
approved_by=''
)
def _parse_schedule_table(self, table: List[List]) -> List[Symbol]:
"""Parse schedule table to extract symbols/elements."""
symbols = []
if not table or len(table) < 2:
return symbols
# First row is usually headers
headers = [str(cell).lower() if cell else '' for cell in table[0]]
# Find key columns
tag_col = next((i for i, h in enumerate(headers) if 'tag' in h or 'mark' in h or 'no' in h), 0)
type_col = next((i for i, h in enumerate(headers) if 'type' in h or 'size' in h), -1)
for row in table[1:]:
if len(row) > tag_col and row[tag_col]:
tag = str(row[tag_col]).strip()
symbol_type = str(row[type_col]).strip() if type_col >= 0 and len(row) > type_col else ''
if tag:
props = {}
for i, header in enumerate(headers):
if i < len(row) and row[i]:
props[header] = str(row[i])
symbols.append(Symbol(
symbol_type=symbol_type or 'unknown',
tag=tag,
location=(0, 0),
properties=props
))
return symbols
def _determine_scale(self, text: str) -> float:
"""Determine drawing scale factor."""
for pattern in self.SCALE_PATTERNS:
match = re.search(pattern, text, re.IGNORECASE)
if match:
if 'NTS' in match.group(0).upper():
return 0 # Not to scale
if '=' in match.group(0):
# Imperial: 1/4" = 1'-0"
return self._parse_imperial_scale(match.group(0))
else:
# Metric: 1:100
return 1 / float(match.group(2))
return 1.0 # Default
def _parse_imperial_scale(self, scale_text: str) -> float:
"""Parse imperial scale to factor."""
match = re.search(r'(\d+)(?:/(\d+))?\s*["\']?\s*=\s*(\d+)', scale_text)
if match:
numerator = float(match.group(1))
denominator = float(match.group(2)) if match.group(2) else 1
feet = float(match.group(3))
inches_per_foot = (numerator / denominator)
return inches_per_foot / (feet * 12)
return 1.0
Truncated for display — read the full file on GitHub.
Related Skills
Agent-Reach
106.4kGive your AI agent eyes to see the entire internet. Read & search Twitter, Reddit, YouTube, GitHub, Bilibili, XiaoHongShu — one CLI, zero API fees.
taste-skill
105.7kAgent skills that give coding agents taste: art direction, reference images and a browser check instead of generic AI pages
headroom
78.3kCompress tool outputs, logs, files, and RAG chunks before they reach the LLM. 20% fewer tokens for coding agents, 60-95% fewer tokens for JSON, same answers. Library, proxy, MCP server.
Scrapling
90.4k🕷️ The BEST Web Scraping framework that handles everything from a single request to a full-scale crawl! Don't be shy, join here: https://discord.gg/EMgGbDceNQ and follow here for daily tips and tricks: https://x.com/Scrapling_dev
Languages
Trust signals
From repository metadata: license, adoption, age and documentation. Not a code audit — see the Safety scan above for what the skill file itself contains.
