# """## Imports""" import sys import math import random import string import zlib import base64 import datetime import uuid import re from io import BytesIO from ctypes import sizeof from collections import Counter from typing import NewType import xml.etree.ElementTree as ET from xml.etree.ElementTree import Element, SubElement, tostring, ElementTree from xml.dom.minidom import parseString import numpy as np import cv2 from matplotlib import pyplot as plt from matplotlib.patches import Polygon from shapely.geometry import Point, Polygon as ShapelyPolygon from shapely.ops import unary_union from PIL import Image, ImageDraw, ImageFont, ImageColor import fitz import ezdxf from ezdxf import units, bbox from ezdxf.colors import aci2rgb from ezdxf.math import OCS, Matrix44, Vec3, Vec2 import pandas as pd # import google_sheet_Legend # import tsadropboxretrieval from PyPDF2 import PdfReader, PdfWriter from PyPDF2.generic import ( NameObject, TextStringObject, DictionaryObject, ArrayObject, FloatObject, NumberObject, ) from math import sin, cos, radians, isclose def aci_to_rgb(aci): aci_rgb_map = { 0: (0, 0, 0), 1: (255, 0, 0), 2: (255, 255, 0), 3: (0, 255, 0), 4: (0, 255, 255), 5: (0, 0, 255), 6: (255, 0, 255), 7: (255, 255, 255), 8: (65, 65, 65), 9: (128, 128, 128), 10: (255, 0, 0), 11: (255, 170, 170), 12: (189, 0, 0), 13: (189, 126, 126), 14: (129, 0, 0), 15: (129, 86, 86), 16: (104, 0, 0), 17: (104, 69, 69), 18: (79, 0, 0), 19: (79, 53, 53), 20: (255, 63, 0), 21: (255, 191, 170), 22: (189, 46, 0), 23: (189, 141, 126), 24: (129, 31, 0), 25: (129, 96, 86), 26: (104, 25, 0), 27: (104, 78, 69), 28: (79, 19, 0), 29: (79, 59, 53), 30: (255, 127, 0), 31: (255, 212, 170), 32: (189, 94, 0), 33: (189, 157, 126), 34: (129, 64, 0), 35: (129, 107, 86), 36: (104, 52, 0), 37: (104, 86, 69), 38: (79, 39, 0), 39: (79, 66, 53), 40: (255, 191, 0), 41: (255, 234, 170), 42: (189, 141, 0), 43: (189, 173, 126), 44: (129, 96, 0), 45: (129, 118, 86), 46: (104, 78, 0), 47: (104, 95, 69), 48: (79, 59, 0), 49: (79, 73, 53), 50: (255, 255, 0), 51: (255, 255, 170), 52: (189, 189, 0), 53: (189, 189, 126), 54: (129, 129, 0), 55: (129, 129, 86), 56: (104, 104, 0), 57: (104, 104, 69), 58: (79, 79, 0), 59: (79, 79, 53), 60: (191, 255, 0), 61: (234, 255, 170), 62: (141, 189, 0), 63: (173, 189, 126), 64: (96, 129, 0), 65: (118, 129, 86), 66: (78, 104, 0), 67: (95, 104, 69), 68: (59, 79, 0), 69: (73, 79, 53), 70: (127, 255, 0), 71: (212, 255, 170), 72: (94, 189, 0), 73: (157, 189, 126), 74: (64, 129, 0), 75: (107, 129, 86), 76: (52, 104, 0), 77: (86, 104, 69), 78: (39, 79, 0), 79: (66, 79, 53), 80: (63, 255, 0), 81: (191, 255, 170), 82: (46, 189, 0), 83: (141, 189, 126), 84: (31, 129, 0), 85: (96, 129, 86), 86: (25, 104, 0), 87: (78, 104, 69), 88: (19, 79, 0), 89: (59, 79, 53), 90: (0, 255, 0), 91: (170, 255, 170), 92: (0, 189, 0), 93: (126, 189, 126), 94: (0, 129, 0), 95: (86, 129, 86), 96: (0, 104, 0), 97: (69, 104, 69), 98: (0, 79, 0), 99: (53, 79, 53), 100: (0, 255, 63), 101: (170, 255, 191), 102: (0, 189, 46), 103: (126, 189, 141), 104: (0, 129, 31), 105: (86, 129, 96), 106: (0, 104, 25), 107: (69, 104, 78), 108: (0, 79, 19), 109: (53, 79, 59), 110: (0, 255, 127), 111: (170, 255, 212), 112: (0, 189, 94), 113: (126, 189, 157), 114: (0, 129, 64), 115: (86, 129, 107), 116: (0, 104, 52), 117: (69, 104, 86), 118: (0, 79, 39), 119: (53, 79, 66), 120: (0, 255, 191), 121: (170, 255, 234), 122: (0, 189, 141), 123: (126, 189, 173), 124: (0, 129, 96), 125: (86, 129, 118), 126: (0, 104, 78), 127: (69, 104, 95), 128: (0, 79, 59), 129: (53, 79, 73), 130: (0, 255, 255), 131: (170, 255, 255), 132: (0, 189, 189), 133: (126, 189, 189), 134: (0, 129, 129), 135: (86, 129, 129), 136: (0, 104, 104), 137: (69, 104, 104), 138: (0, 79, 79), 139: (53, 79, 79), 140: (0, 191, 255), 141: (170, 234, 255), 142: (0, 141, 189), 143: (126, 173, 189), 144: (0, 96, 129), 145: (86, 118, 129), 146: (0, 78, 104), 147: (69, 95, 104), 148: (0, 59, 79), 149: (53, 73, 79), 150: (0, 127, 255), 151: (170, 212, 255), 152: (0, 94, 189), 153: (126, 157, 189), 154: (0, 64, 129), 155: (86, 107, 129), 156: (0, 52, 104), 157: (69, 86, 104), 158: (0, 39, 79), 159: (53, 66, 79), 160: (0, 63, 255), 161: (170, 191, 255), 162: (0, 46, 189), 163: (126, 141, 189), 164: (0, 31, 129), 165: (86, 96, 129), 166: (0, 25, 104), 167: (69, 78, 104), 168: (0, 19, 79), 169: (53, 59, 79), 170: (0, 0, 255), 171: (170, 170, 255), 172: (0, 0, 189), 173: (126, 126, 189), 174: (0, 0, 129), 175: (86, 86, 129), 176: (0, 0, 104), 177: (69, 69, 104), 178: (0, 0, 79), 179: (53, 53, 79), 180: (63, 0, 255), 181: (191, 170, 255), 182: (46, 0, 189), 183: (141, 126, 189), 184: (31, 0, 129), 185: (96, 86, 129), 186: (25, 0, 104), 187: (78, 69, 104), 188: (19, 0, 79), 189: (59, 53, 79), 190: (127, 0, 255), 191: (212, 170, 255), 192: (94, 0, 189), 193: (157, 126, 189), 194: (64, 0, 129), 195: (107, 86, 129), 196: (52, 0, 104), 197: (86, 69, 104), 198: (39, 0, 79), 199: (66, 53, 79), 200: (191, 0, 255), 201: (234, 170, 255), 202: (141, 0, 189), 203: (173, 126, 189), 204: (96, 0, 129), 205: (118, 86, 129), 206: (78, 0, 104), 207: (95, 69, 104), 208: (59, 0, 79), 209: (73, 53, 79), 210: (255, 0, 255), 211: (255, 170, 255), 212: (189, 0, 189), 213: (189, 126, 189), 214: (129, 0, 129), 215: (129, 86, 129), 216: (104, 0, 104), 217: (104, 69, 104), 218: (79, 0, 79), 219: (79, 53, 79), 220: (255, 0, 191), 221: (255, 170, 234), 222: (189, 0, 141), 223: (189, 126, 173), 224: (129, 0, 96), 225: (129, 86, 118), 226: (104, 0, 78), 227: (104, 69, 95), 228: (79, 0, 59), 229: (79, 53, 73), 230: (255, 0, 127), 231: (255, 170, 212), 232: (189, 0, 94), 233: (189, 126, 157), 234: (129, 0, 64), 235: (129, 86, 107), 236: (104, 0, 52), 237: (104, 69, 86), 238: (79, 0, 39), 239: (79, 53, 66), 240: (255, 0, 63), 241: (255, 170, 191), 242: (189, 0, 46), 243: (189, 126, 141), 244: (129, 0, 31), 245: (129, 86, 96), 246: (104, 0, 25), 247: (104, 69, 78), 248: (79, 0, 19), 249: (79, 53, 59), 250: (51, 51, 51), 251: (80, 80, 80), 252: (105, 105, 105), 253: (130, 130, 130), 254: (190, 190, 190), 255: (255, 255, 255) } # Default to white if index is invalid or not found return aci_rgb_map.get(aci, (255, 255, 255)) def int_to_rgb(color_int): """Convert an integer to an (R, G, B) tuple.""" r = (color_int >> 16) & 255 g = (color_int >> 8) & 255 b = color_int & 255 return (r, g, b) def get_hatch_color(entity): """Extract hatch color with detailed debugging.""" if not entity: # print("No entity provided for color extraction.") return (255, 255, 255),'default' # Check for true color if entity.dxf.hasattr('true_color'): true_color = entity.dxf.true_color rgb_color = int_to_rgb(true_color) # Convert integer to (R, G, B) # print(f"True color detected (RGB): {rgb_color}") if(rgb_color == (255, 255, 255)): return rgb_color,'White true_color' else: return rgb_color,'true_color' # Check for color index color_index = entity.dxf.color # print(f"Entity color index: {color_index}") if 1 <= color_index <= 255: rgb_color = aci_to_rgb(color_index) # Convert ACI to RGB # print(f"Converted ACI to RGB: {rgb_color}") return rgb_color,'aci' # Handle ByLayer or ByBlock if color_index == 0: # ByLayer layer_name = entity.dxf.layer layer = entity.doc.layers.get(layer_name) # print(f"ByLayer detected for layer '{layer_name}'.") if layer: layer_color_index = layer.dxf.color # print(layer_color_index) rgb_color = aci_to_rgb(layer_color_index) # print(f"Layer '{layer_name}' color index {layer_color_index} converted to RGB: {rgb_color}") return rgb_color,'bylayer' else: # print(f"Layer '{layer_name}' not found. Defaulting to white.") return (255, 255, 255),'default' # Default # print("Unhandled color case. Defaulting to white.") return (255, 255, 255),'default' def calculate_distance(pt1, pt2): dx = pt2[0] - pt1[0] dy = pt2[1] - pt1[1] return math.hypot(dx, dy) def dedupe_colors_preserve_order(hatchcolor): seen = set() unique = [] for item in hatchcolor: # normalize to a tuple (handles [(r,g,b)], [r,g,b], or (r,g,b)) if isinstance(item, (list, tuple)) and len(item) == 1 and isinstance(item[0], (list, tuple)): color = tuple(item[0]) else: color = tuple(item) if not isinstance(item, tuple) else item if color not in seen: seen.add(color) unique.append(color) return unique def remove_existing_colors(unique_colors, filtered_items): # extract normalized colors from filtered_items (assumes color is last element) filtered_set = set() for row in filtered_items: if not row: continue color = row[-1] if color is None: continue # normalize: make tuple if isinstance(color, (list, tuple)): filtered_set.add(tuple(color)) else: # unexpected type: try to convert try: filtered_set.add(tuple(color)) except Exception: pass # build new list preserving order, excluding any color that appears in filtered_set result = [] for c in unique_colors: # normalize unique color to tuple in case it is list-like color_t = tuple(c) if not isinstance(c, tuple) else c if color_t not in filtered_set: result.append(color_t) return result def Legend_Detection(datadoc,dxfile,SearchArray,pdf_content=0): hatchColors=[] FinalColors=[] doc = ezdxf.readfile(dxfile) doc.header['$MEASUREMENT'] = 1 msp = doc.modelspace() text_with_positions = [] # if pdf_content: # doc = fitz.open(stream=pdf_content, filetype="pdf") # else: # doc = fitz.open('pdf',datadoc) if(SearchArray): for i in range(len(SearchArray)): print("SearchArray[i][0] = ",SearchArray[i][0]) print("SearchArray[i][1] = ",SearchArray[i][1]) print("SearchArray[i][2] = ",SearchArray[i][2]) if (SearchArray[i][0] and SearchArray[i][1] and SearchArray[i][2]): print("First IF") print("SearchArray[i][1] = ",SearchArray[i][0]) print("SearchArray[i][2] = ",SearchArray[i][1]) print("SearchArray[i][3] = ",SearchArray[i][2]) for text_entity in doc.modelspace().query('TEXT MTEXT'): text = text_entity.text.strip() if hasattr(text_entity, 'text') else "" # if (text.startswith("P") and len(text) == 3) or (text.startswith("I") and len(text) == 3): # Filter for "Wall" if(text.startswith(SearchArray[i][0]) and len(text)==int(SearchArray[i][2])): # print("text = ",text) position = text_entity.dxf.insert # Extract text position x, y = position.x, position.y for text_entity in doc.modelspace().query('TEXT MTEXT'): NBS = text_entity.text.strip() if hasattr(text_entity, 'text') else "" # textNBS = None if (NBS.startswith(SearchArray[i][1])): positionNBS = text_entity.dxf.insert # Extract text position xNBS, yNBS = positionNBS.x, positionNBS.y if(x == xNBS or y == yNBS): textNBS=NBS # print("textNBS = ",textNBS) break else: textNBS = None nearest_hatch = None min_distance = float('inf') # Initialize with a very large value detected_color = (255, 255, 255) # Default to white # Search for the nearest hatch for hatch in doc.modelspace().query('HATCH'): # Query only hatches if hatch.paths: for path in hatch.paths: if path.type == 1: # PolylinePath vertices = [v[:2] for v in path.vertices] # Calculate the centroid of the hatch centroid_x = sum(v[0] for v in vertices) / len(vertices) centroid_y = sum(v[1] for v in vertices) / len(vertices) centroid = (centroid_x, centroid_y) # Calculate the distance between the text and the hatch centroid distance = calculate_distance((x, y), centroid) # Update the nearest hatch if a closer one is found if distance < min_distance: min_distance = distance nearest_hatch = hatch # Get the color of this hatch current_color,color_index = get_hatch_color(hatch) if current_color != (255, 255, 255): # Valid color found detected_color = current_color break # Stop checking further paths for this hatch # Append the detected result only once text_with_positions.append([text, textNBS, (x, y), detected_color]) elif (SearchArray[i][0] and SearchArray[i][2]): print("Second IF") for text_entity in doc.modelspace().query('TEXT MTEXT'): text = text_entity.text.strip() if hasattr(text_entity, 'text') else "" # if (text.startswith("P") and len(text) == 3) or (text.startswith("I") and len(text) == 3): # Filter for "Wall" if(text.startswith(SearchArray[i][0]) and len(text)==int(SearchArray[i][2])): position = text_entity.dxf.insert # Extract text position x, y = position.x, position.y textNBS = None nearest_hatch = None min_distance = float('inf') # Initialize with a very large value detected_color = (255, 255, 255) # Default to white # Search for the nearest hatch for hatch in doc.modelspace().query('HATCH'): # Query only hatches if hatch.paths: for path in hatch.paths: if path.type == 1: # PolylinePath vertices = [v[:2] for v in path.vertices] # Calculate the centroid of the hatch centroid_x = sum(v[0] for v in vertices) / len(vertices) centroid_y = sum(v[1] for v in vertices) / len(vertices) centroid = (centroid_x, centroid_y) # Calculate the distance between the text and the hatch centroid distance = calculate_distance((x, y), centroid) # Update the nearest hatch if a closer one is found if distance < min_distance: min_distance = distance nearest_hatch = hatch # Get the color of this hatch current_color,color_index = get_hatch_color(hatch) if current_color != (255, 255, 255): # Valid color found detected_color = current_color break # Stop checking further paths for this hatch # Append the detected result only once text_with_positions.append([text, textNBS, (x, y), detected_color]) # print("text_with_positions=",text_with_positions) elif(SearchArray[i][0]): print("Third IF") for text_entity in doc.modelspace().query('TEXT MTEXT'): text = text_entity.text.strip() if hasattr(text_entity, 'text') else "" # if (text.startswith("P") and len(text) == 3) or (text.startswith("I") and len(text) == 3): # Filter for "Wall" if(text.startswith(SearchArray[i][0])): position = text_entity.dxf.insert # Extract text position x, y = position.x, position.y textNBS = None nearest_hatch = None min_distance = float('inf') # Initialize with a very large value detected_color = (255, 255, 255) # Default to white # Search for the nearest hatch for hatch in doc.modelspace().query('HATCH'): # Query only hatches if hatch.paths: for path in hatch.paths: if path.type == 1: # PolylinePath vertices = [v[:2] for v in path.vertices] # Calculate the centroid of the hatch centroid_x = sum(v[0] for v in vertices) / len(vertices) centroid_y = sum(v[1] for v in vertices) / len(vertices) centroid = (centroid_x, centroid_y) # Calculate the distance between the text and the hatch centroid distance = calculate_distance((x, y), centroid) # Update the nearest hatch if a closer one is found if distance < min_distance: min_distance = distance nearest_hatch = hatch # Get the color of this hatch current_color,color_index = get_hatch_color(hatch) if current_color != (255, 255, 255): # Valid color found detected_color = current_color break # Stop checking further paths for this hatch # Append the detected result only once text_with_positions.append([text, textNBS, (x, y), detected_color]) # Group entries by the text value (first element) print("Grouping") grouped = {} for entry in text_with_positions: key = entry[0] grouped.setdefault(key, []).append(entry) # Filter the groups: if any entry in a group has a non-None Text Nbs, keep only one of those filtered_results = [] for key, entries in grouped.items(): # Find the first entry with a valid textNBS (non-None) complete = next((entry for entry in entries if entry[1] is not None), None) if complete: filtered_results.append(complete) else: # If none are complete, you can choose to keep just one entry filtered_results.append(entries[0]) # Print the filtered results for text, textNbs, position, detected_color in filtered_results: print(f"Text: {text}, Text Nbs: {textNbs}, Position: {position}, Nearest Hatch Color: {detected_color}") text_with_positions=filtered_results for entity in msp: if entity.dxftype() == 'HATCH': for path in entity.paths: rgb_color,index = get_hatch_color(entity) hatchColors.append([rgb_color]) unique_colors = dedupe_colors_preserve_order(hatchColors) unique_new = remove_existing_colors(unique_colors, text_with_positions) for item in unique_new: FinalColors.append(['Hatch',None,None,item]) text_with_positions.append(FinalColors) flat = [] for item in text_with_positions: if isinstance(item, list) and len(item) > 0 and isinstance(item[0], list): flat.extend(item) # nested → expand it else: flat.append(item) text_with_positions = flat print(text_with_positions) return text_with_positions