1# Purpose: validate DXF tag structures 2# Created: 03.01.2018 3# Copyright (C) 2018-2020, Manfred Moitzi 4# License: MIT License 5import logging 6import io 7import bisect 8import math 9from typing import TextIO, Iterable, List, Optional, Set 10 11from .const import ( 12 DXFStructureError, DXFError, DXFValueError, DXFAppDataError, DXFXDataError, 13 APP_DATA_MARKER, HEADER_VAR_MARKER, XDATA_MARKER, 14 INVALID_LAYER_NAME_CHARACTERS, acad_release, VALID_DXF_LINEWEIGHT_VALUES, 15 VALID_DXF_LINEWEIGHTS, LINEWEIGHT_BYLAYER, 16) 17 18from .tagger import ascii_tags_loader 19from .types import is_embedded_object_marker, DXFTag, NONE_TAG 20from ezdxf.tools.codepage import toencoding 21from ezdxf.math import NULLVEC 22 23logger = logging.getLogger('ezdxf') 24 25 26class DXFInfo: 27 def __init__(self): 28 self.release = 'R12' 29 self.version = 'AC1009' 30 self.encoding = 'cp1252' 31 self.handseed = '0' 32 33 def set_header_var(self, name: str, value: str) -> int: 34 if name == '$ACADVER': 35 self.version = value 36 self.release = acad_release.get(value, 'R12') 37 elif name == '$DWGCODEPAGE': 38 self.encoding = toencoding(value) 39 elif name == '$HANDSEED': 40 self.handseed = value 41 else: 42 return 0 43 return 1 44 45 46def dxf_info(stream: TextIO) -> DXFInfo: 47 info = DXFInfo() 48 tagger = ascii_tags_loader(stream) 49 # comments already removed 50 if next(tagger) != (0, 'SECTION'): 51 # maybe a DXF structure error, handled by later processing 52 return info 53 if next(tagger) != (2, 'HEADER'): 54 # no leading HEADER section like DXF R12 with only ENTITIES section 55 return info 56 tag = NONE_TAG 57 found = 0 58 while tag != (0, 'ENDSEC'): # until end of HEADER section 59 tag = next(tagger) 60 if tag.code != HEADER_VAR_MARKER: 61 continue 62 name = tag.value 63 value = next(tagger).value 64 found += info.set_header_var(name, value) 65 if found > 2: # all expected values collected 66 break 67 return info 68 69 70def header_validator(tagger: Iterable[DXFTag]) -> Iterable[DXFTag]: 71 """ Checks the tag structure of the content of the header section. 72 73 Do not feed (0, 'SECTION') (2, 'HEADER') and (0, 'ENDSEC') tags! 74 75 Args: 76 tagger: generator/iterator of low level tags or compiled tags 77 78 Raises: 79 DXFStructureError() -> invalid group codes 80 DXFValueError() -> invalid header variable name 81 82 """ 83 variable_name_tag = True 84 for tag in tagger: 85 code, value = tag 86 if variable_name_tag: 87 if code != HEADER_VAR_MARKER: 88 raise DXFStructureError( 89 f'Invalid header variable tag {code}, {value}).' 90 ) 91 if not value.startswith('$'): 92 raise DXFValueError( 93 f'Invalid header variable name "{value}", missing leading "$".' 94 ) 95 variable_name_tag = False 96 else: 97 variable_name_tag = True 98 yield tag 99 100 101def entity_structure_validator(tags: List[DXFTag]) -> Iterable[DXFTag]: 102 """ Checks for valid DXF entity tag structure. 103 104 - APP DATA can not be nested and every opening tag (102, '{...') needs a 105 closing tag (102, '}') 106 - extended group codes (>=1000) allowed before XDATA section 107 - XDATA section starts with (1001, APPID) and is always at the end of an 108 entity 109 - XDATA section: only group code >= 1000 is allowed 110 - XDATA control strings (1002, '{') and (1002, '}') have to be balanced 111 - embedded objects may follow XDATA 112 113 XRECORD entities will not be checked. 114 115 Args: 116 tags: list of DXFTag() 117 118 Raises: 119 DXFAppDataError: for invalid APP DATA 120 DXFXDataError: for invalid XDATA 121 122 """ 123 assert isinstance(tags, list) 124 dxftype = tags[0].value # type: str 125 is_xrecord = dxftype == 'XRECORD' 126 handle = '???' 127 app_data = False 128 xdata = False 129 xdata_list_level = 0 130 app_data_closing_tag = '}' 131 embedded_object = False 132 for tag in tags: 133 if tag.code == 5 and handle == '???': 134 handle = tag.value 135 136 if is_embedded_object_marker(tag): 137 embedded_object = True 138 139 if embedded_object: # no further validation 140 yield tag 141 continue # with next tag 142 143 if xdata and not embedded_object: 144 if tag.code < 1000: 145 dxftype = tags[0].value 146 raise DXFXDataError( 147 f'Invalid XDATA structure in entity {dxftype}(#{handle}), ' 148 f'only group code >=1000 allowed in XDATA section' 149 ) 150 if tag.code == 1002: 151 value = tag.value 152 if value == '{': 153 xdata_list_level += 1 154 elif value == '}': 155 xdata_list_level -= 1 156 else: 157 raise DXFXDataError( 158 f'Invalid XDATA control string (1002, "{value}") entity' 159 f' {dxftype}(#{handle}).' 160 ) 161 if xdata_list_level < 0: # more closing than opening tags 162 raise DXFXDataError( 163 f'Invalid XDATA structure in entity {dxftype}(#{handle}), ' 164 f'unbalanced list markers, missing (1002, "{{").' 165 ) 166 167 if tag.code == APP_DATA_MARKER and not is_xrecord: 168 # Ignore control tags (102, ...) tags in XRECORD 169 value = tag.value 170 if value.startswith('{'): 171 if app_data: # already in app data mode 172 raise DXFAppDataError( 173 f'Invalid APP DATA structure in entity {dxftype}' 174 f'(#{handle}), APP DATA can not be nested.' 175 ) 176 app_data = True 177 # 'APPID}' is also a valid closing tag 178 app_data_closing_tag = value[1:] + '}' 179 elif value == '}' or value == app_data_closing_tag: 180 if not app_data: 181 raise DXFAppDataError( 182 f'Invalid APP DATA structure in entity {dxftype}' 183 f'(#{handle}), found (102, "}}") tag without opening tag.' 184 ) 185 app_data = False 186 app_data_closing_tag = '}' 187 else: 188 raise DXFAppDataError( 189 f'Invalid APP DATA structure tag (102, "{value}") in ' 190 f'entity {dxftype}(#{handle}).' 191 ) 192 193 # XDATA section starts with (1001, APPID) and is always at the end of 194 # an entity. 195 if tag.code == XDATA_MARKER and xdata is False: 196 xdata = True 197 if app_data: 198 raise DXFAppDataError( 199 f'Invalid APP DATA structure in entity {dxftype}' 200 f'(#{handle}), missing closing tag (102, "}}").' 201 ) 202 yield tag 203 204 if app_data: 205 raise DXFAppDataError( 206 f'Invalid APP DATA structure in entity {dxftype}(#{handle}), ' 207 f'missing closing tag (102, "}}").' 208 ) 209 if xdata: 210 if xdata_list_level < 0: 211 raise DXFXDataError( 212 f'Invalid XDATA structure in entity {dxftype}(#{handle}), ' 213 f'unbalanced list markers, missing (1002, "{{").' 214 ) 215 elif xdata_list_level > 0: 216 raise DXFXDataError( 217 f'Invalid XDATA structure in entity {dxftype}(#{handle}), ' 218 f'unbalanced list markers, missing (1002, "}}").') 219 220 221def is_dxf_file(filename: str) -> bool: 222 """ Returns ``True`` if `filename` is an ASCII DXF file. """ 223 with io.open(filename, errors='ignore') as fp: 224 return is_dxf_stream(fp) 225 226 227def is_binary_dxf_file(filename: str) -> bool: 228 """ Returns ``True`` if `filename` is a binary DXF file. """ 229 with open(filename, 'rb') as fp: 230 sentinel = fp.read(22) 231 return sentinel == b'AutoCAD Binary DXF\r\n\x1a\x00' 232 233 234def is_dwg_file(filename: str) -> bool: 235 """ Returns ``True`` if `filename` is a DWG file. """ 236 return dwg_version(filename) is not None 237 238 239def dwg_version(filename: str) -> Optional[str]: 240 """ Returns DWG version of `filename` as string or ``None``. """ 241 with open(str(filename), 'rb') as fp: 242 try: 243 version = fp.read(6).decode(errors='ignore') 244 except IOError: 245 return None 246 if version not in acad_release: 247 return None 248 return version 249 250 251def is_dxf_stream(stream: TextIO) -> bool: 252 try: 253 reader = ascii_tags_loader(stream) 254 except DXFError: 255 return False 256 try: 257 for tag in reader: 258 # The common case for well formed DXF files 259 if tag == (0, 'SECTION'): 260 return True 261 # Accept/Ignore tags in front of first SECTION - like AutoCAD and 262 # BricsCAD, but group code should be < 1000, until reality proofs 263 # otherwise. 264 if tag.code > 999: 265 return False 266 except DXFStructureError: 267 pass 268 return False 269 270 271# Names used in symbol table records and in dictionaries must follow these rules: 272# 273# - Names can be any length in ObjectARX, but symbol names entered by users in 274# AutoCAD are limited to 255 characters. 275# - AutoCAD preserves the case of names but does not use the case in 276# comparisons. For example, AutoCAD considers "Floor" to be the same symbol 277# as "FLOOR." 278# - Names can be composed of all characters allowed by Windows or Mac OS for 279# filenames, except comma (,), backquote (‘), semi-colon (;), and equal 280# sign (=). 281# http://help.autodesk.com/view/OARX/2018/ENU/?guid=GUID-83ABF20A-57D4-4AB3-8A49-D91E0F70DBFF 282 283def is_valid_table_name(name: str) -> bool: 284 return not bool(INVALID_LAYER_NAME_CHARACTERS.intersection(set(name))) 285 286 287def is_valid_layer_name(name: str) -> bool: 288 if is_adsk_special_layer(name): 289 return True 290 else: 291 return is_valid_table_name(name) 292 293 294def is_adsk_special_layer(name: str) -> bool: 295 if name.startswith('*'): 296 # special Autodesk layers starts with invalid character * 297 name = name.upper() 298 if name.startswith('*ADSK_'): 299 return True 300 if name.startswith('*ACMAP'): 301 return True 302 if name.startswith('*TEMPORARY'): 303 return True 304 return False 305 306 307def is_valid_block_name(name: str) -> bool: 308 if name.startswith('*'): 309 return is_valid_table_name(name[1:]) 310 else: 311 return is_valid_table_name(name) 312 313 314def is_valid_vport_name(name: str) -> bool: 315 if name.startswith('*'): 316 return name.upper() == '*ACTIVE' 317 else: 318 return is_valid_table_name(name) 319 320 321def is_valid_lineweight(lineweight: int) -> bool: 322 return lineweight in VALID_DXF_LINEWEIGHT_VALUES 323 324 325def fix_lineweight(lineweight: int) -> int: 326 if lineweight in VALID_DXF_LINEWEIGHT_VALUES: 327 return lineweight 328 if lineweight < -3: 329 return LINEWEIGHT_BYLAYER 330 if lineweight > 211: 331 return 211 332 index = bisect.bisect(VALID_DXF_LINEWEIGHTS, lineweight) 333 return VALID_DXF_LINEWEIGHTS[index] 334 335 336def is_valid_aci_color(aci: int) -> bool: 337 return 0 <= aci <= 257 338 339 340def is_in_integer_range(start: int, end: int): 341 """ Range of integer values, excluding the `end` value. """ 342 343 def _validator(value: int) -> bool: 344 return start <= value < end 345 346 return _validator 347 348 349def fit_into_integer_range(start: int, end: int): 350 def _fixer(value: int) -> int: 351 return min(max(value, start), end - 1) 352 353 return _fixer 354 355 356def fit_into_float_range(start: float, end: float): 357 def _fixer(value: float) -> float: 358 return min(max(value, start), end) 359 360 return _fixer 361 362 363def is_in_float_range(start: float, end: float): 364 """ Range of float values, including the `end` value. """ 365 366 def _validator(value: float) -> bool: 367 return start <= value <= end 368 369 return _validator 370 371 372def is_not_null_vector(v) -> bool: 373 return not NULLVEC.isclose(v) 374 375 376def is_not_zero(v: float) -> bool: 377 return not math.isclose(v, 0.0, abs_tol=1e-12) 378 379 380def is_not_negative(v) -> bool: 381 return v >= 0 382 383 384is_greater_or_equal_zero = is_not_negative 385 386 387def is_positive(v) -> bool: 388 return v > 0 389 390 391is_greater_zero = is_positive 392 393 394def is_valid_bitmask(mask: int): 395 def _validator(value: int) -> bool: 396 return not bool(~mask & value) 397 398 return _validator 399 400 401def fix_bitmask(mask: int): 402 def _fixer(value: int) -> int: 403 return mask & value 404 405 return _fixer 406 407 408def is_integer_bool(v) -> bool: 409 return v in (0, 1) 410 411 412def fix_integer_bool(v) -> int: 413 return 1 if v else 0 414 415 416def is_one_of(values: Set): 417 def _validator(v) -> bool: 418 return v in values 419 420 return _validator 421 422 423def is_valid_one_line_text(text: str) -> bool: 424 has_line_breaks = bool(set(text).intersection({'\n', '\r'})) 425 return not has_line_breaks and not text.endswith('^') 426 427 428def fix_one_line_text(text: str) -> str: 429 return text.replace('\n', '').replace('\r', '').rstrip('^') 430 431 432def is_valid_attrib_tag(tag: str) -> bool: 433 return is_valid_one_line_text(tag) 434 435 436def fix_attrib_tag(tag: str) -> str: 437 return fix_one_line_text(tag) 438