1# Copyright (c) 2020, Manfred Moitzi 2# License: MIT License 3from typing import TYPE_CHECKING, Optional, Iterable, Tuple, List, Dict, cast 4import sys 5import struct 6import math 7from enum import IntEnum 8from itertools import repeat 9from ezdxf.lldxf import const 10from ezdxf.tools.binarydata import bytes_to_hexstr, ByteStream, BitStream 11from ezdxf import colors 12from ezdxf.math import ( 13 Vec3, Matrix44, Z_AXIS, ConstructionCircle, 14 ConstructionArc, 15) 16from ezdxf.entities import factory 17import logging 18 19if TYPE_CHECKING: 20 from ezdxf.eztypes import ( 21 Tags, TagWriter, Drawing, Polymesh, Polyface, Polyline, Hatch, 22 ) 23 24logger = logging.getLogger('ezdxf') 25 26CHUNK_SIZE = 127 27 28 29def load_proxy_graphic(tags: 'Tags', length_code: int = 160, 30 data_code: int = 310) -> Optional[bytes]: 31 binary_data = [tag.value for tag in 32 tags.pop_tags(codes=(length_code, data_code)) if 33 tag.code == data_code] 34 return b''.join(binary_data) if len(binary_data) else None 35 36 37def export_proxy_graphic(data: bytes, tagwriter: 'TagWriter', 38 length_code: int = 160, data_code: int = 310) -> None: 39 # Do not export proxy graphic for DXF R12 files 40 assert tagwriter.dxfversion > const.DXF12 41 42 length = len(data) 43 if length == 0: 44 return 45 46 tagwriter.write_tag2(length_code, length) 47 index = 0 48 while index < length: 49 hex_str = bytes_to_hexstr(data[index:index + CHUNK_SIZE]) 50 tagwriter.write_tag2(data_code, hex_str) 51 index += CHUNK_SIZE 52 53 54class ProxyGraphicTypes(IntEnum): 55 EXTENTS = 1 56 CIRCLE = 2 57 CIRCLE_3P = 3 58 CIRCULAR_ARC = 4 59 CIRCULAR_ARC_3P = 5 60 POLYLINE = 6 61 POLYGON = 7 62 MESH = 8 63 SHELL = 9 64 TEXT = 10 65 TEXT2 = 11 66 XLINE = 12 67 RAY = 13 68 ATTRIBUTE_COLOR = 14 69 UNUSED_15 = 15 70 ATTRIBUTE_LAYER = 16 71 UNUSED_17 = 17 72 ATTRIBUTE_LINETYPE = 18 73 ATTRIBUTE_MARKER = 19 74 ATTRIBUTE_FILL = 20 75 UNUSED_21 = 21 76 ATTRIBUTE_TRUE_COLOR = 22 77 ATTRIBUTE_LINEWEIGHT = 23 78 ATTRIBUTE_LTSCALE = 24 79 ATTRIBUTE_THICKNESS = 25 80 ATTRIBUTE_PLOT_STYLE_NAME = 26 81 PUSH_CLIP = 27 82 POP_CLIP = 28 83 PUSH_MATRIX = 29 84 PUSH_MATRIX2 = 30 85 POP_MATRIX = 31 86 POLYLINE_WITH_NORMALS = 32 87 LWPOLYLINE = 33 88 ATTRIBUTE_MATERIAL = 34 89 ATTRIBUTE_MAPPER = 35 90 UNICODE_TEXT = 36 91 UNKNOWN_37 = 37 92 UNICODE_TEXT2 = 38 93 94 95class ProxyGraphic: 96 def __init__(self, data: bytes, doc: 'Drawing' = None): 97 self._doc = doc 98 self._factory = factory.new 99 self._buffer: bytes = data 100 self._index: int = 8 101 self.dxfversion = doc.dxfversion if doc else 'AC1015' 102 self.color: int = const.BYLAYER 103 self.layer: str = '0' 104 self.linetype: str = 'BYLAYER' 105 self.marker_index: int = 0 106 self.fill: bool = False 107 self.true_color: Optional[int] = None 108 self.lineweight: int = const.LINEWEIGHT_DEFAULT 109 self.ltscale: float = 1.0 110 self.thickness: float = 0.0 111 # Layer list in storage order 112 self.layers: List[str] = [] 113 # Linetypes list in storage order 114 self.linetypes: List[str] = [] 115 # List of text styles, with font name as key 116 self.textstyles = dict() 117 self.required_fonts = set() 118 self.matrices = [] 119 120 if self._doc: 121 self.layers = list(layer.dxf.name for layer in self._doc.layers) 122 self.linetypes = list( 123 linetype.dxf.name for linetype in self._doc.linetypes) 124 self.textstyles = {style.dxf.font: style.dxf.name for style in 125 self._doc.styles} 126 127 def info(self) -> Iterable[Tuple[int, int, str]]: 128 index = self._index 129 buffer = self._buffer 130 while index < len(buffer): 131 size, type_ = struct.unpack_from('<2L', self._buffer, offset=index) 132 try: 133 name = ProxyGraphicTypes(type_).name 134 except ValueError: 135 name = f'UNKNOWN_TYPE_{type_}' 136 yield index, size, name 137 index += size 138 139 def virtual_entities(self): 140 def transform(entity): 141 if self.matrices: 142 return entity.transform(self.matrices[-1]) 143 else: 144 return entity 145 146 index = self._index 147 buffer = self._buffer 148 while index < len(buffer): 149 size, type_ = struct.unpack_from('<2L', self._buffer, offset=index) 150 try: 151 name = ProxyGraphicTypes(type_).name.lower() 152 except ValueError: 153 logger.debug(f'Unsupported Type Code: {type_}') 154 index += size 155 continue 156 method = getattr(self, name, None) 157 if method: 158 result = method(self._buffer[index + 8: index + size]) 159 if isinstance(result, tuple): 160 for entity in result: 161 yield transform(entity) 162 elif result: 163 yield transform(result) 164 if result: # reset fill after each graphic entity 165 self.fill = False 166 else: 167 logger.debug(f'Unsupported feature ProxyGraphic.{name}()') 168 index += size 169 170 def push_matrix(self, data: bytes): 171 values = struct.unpack('<16d', data) 172 m = Matrix44(values) 173 m.transpose() 174 self.matrices.append(m) 175 176 def pop_matrix(self, data: bytes): 177 if self.matrices: 178 self.matrices.pop() 179 180 def reset_colors(self): 181 self.color = const.BYLAYER 182 self.true_color = None 183 184 def attribute_color(self, data: bytes): 185 self.reset_colors() 186 self.color = struct.unpack('<L', data)[0] 187 if self.color < 0 or self.color > 256: 188 self.color = const.BYLAYER 189 190 def attribute_layer(self, data: bytes): 191 if self._doc: 192 index = struct.unpack('<L', data)[0] 193 if index < len(self.layers): 194 self.layer = self.layers[index] 195 196 def attribute_linetype(self, data: bytes): 197 if self._doc: 198 index = struct.unpack('<L', data)[0] 199 if index < len(self.linetypes): 200 self.linetype = self.linetypes[index] 201 202 def attribute_marker(self, data: bytes): 203 self.marker_index = struct.unpack('<L', data)[0] 204 205 def attribute_fill(self, data: bytes): 206 self.fill = bool(struct.unpack('<L', data)[0]) 207 208 def attribute_true_color(self, data: bytes): 209 self.reset_colors() 210 code, value = colors.decode_raw_color(struct.unpack('<L', data)[0]) 211 if code == colors.COLOR_TYPE_RGB: 212 self.true_color = colors.rgb2int(value) 213 else: # ACI colors, BYLAYER, BYBLOCK 214 self.color = value 215 216 def attribute_lineweight(self, data: bytes): 217 lw = struct.unpack('<L', data)[0] 218 if lw > const.MAX_VALID_LINEWEIGHT: 219 self.lineweight = max(lw - 0x100000000, const.LINEWEIGHT_DEFAULT) 220 else: 221 self.lineweight = lw 222 223 def attribute_ltscale(self, data: bytes): 224 self.ltscale = struct.unpack('<d', data)[0] 225 226 def attribute_thickness(self, data: bytes): 227 self.thickness = struct.unpack('<d', data)[0] 228 229 def circle(self, data: bytes): 230 bs = ByteStream(data) 231 attribs = self._build_dxf_attribs() 232 attribs['center'] = Vec3(bs.read_vertex()) 233 attribs['radius'] = bs.read_float() 234 attribs['extrusion'] = bs.read_vertex() 235 return self._factory('CIRCLE', dxfattribs=attribs) 236 237 def circle_3p(self, data: bytes): 238 bs = ByteStream(data) 239 attribs = self._build_dxf_attribs() 240 p1 = Vec3(bs.read_vertex()) 241 p2 = Vec3(bs.read_vertex()) 242 p3 = Vec3(bs.read_vertex()) 243 circle = ConstructionCircle.from_3p(p1, p2, p3) 244 attribs['center'] = circle.center 245 attribs['radius'] = circle.radius 246 return self._factory('CIRCLE', dxfattribs=attribs) 247 248 def circular_arc(self, data: bytes): 249 bs = ByteStream(data) 250 attribs = self._build_dxf_attribs() 251 attribs['center'] = Vec3(bs.read_vertex()) 252 attribs['radius'] = bs.read_float() 253 normal = Vec3(bs.read_vertex()) 254 if normal != (0, 0, 1): 255 logger.debug('ProxyGraphic: unsupported 3D ARC.') 256 start_vec = Vec3(bs.read_vertex()) 257 sweep_angle = bs.read_float() 258 # arc_type = bs.read_long() # unused yet 259 # just do 2D for now 260 start_angle = start_vec.angle_deg 261 end_angle = start_angle + math.degrees(sweep_angle) 262 attribs['start_angle'] = start_angle 263 attribs['end_angle'] = end_angle 264 return self._factory('ARC', dxfattribs=attribs) 265 266 def circular_arc_3p(self, data: bytes): 267 bs = ByteStream(data) 268 attribs = self._build_dxf_attribs() 269 p1 = Vec3(bs.read_vertex()) 270 p2 = Vec3(bs.read_vertex()) 271 p3 = Vec3(bs.read_vertex()) 272 # arc_type = bs.read_long() # unused yet 273 arc = ConstructionArc.from_3p(p1, p3, p2) 274 attribs['center'] = arc.center 275 attribs['radius'] = arc.radius 276 attribs['start_angle'] = arc.start_angle 277 attribs['end_angle'] = arc.end_angle 278 return self._factory('ARC', dxfattribs=attribs) 279 280 def _filled_polygon(self, vertices, attribs): 281 hatch = cast('Hatch', self._factory('HATCH', dxfattribs=attribs)) 282 hatch.paths.add_polyline_path(vertices, is_closed=True) 283 return hatch 284 285 def _polyline(self, vertices, *, close=False, normal=Z_AXIS): 286 # Polyline without bulge values! 287 # Current implementation ignores the normal vector! 288 attribs = self._build_dxf_attribs() 289 count = len(vertices) 290 if count == 1 or (count == 2 and vertices[0].isclose(vertices[1])): 291 attribs['location'] = vertices[0] 292 return self._factory('POINT', dxfattribs=attribs) 293 294 if self.fill and count > 2: 295 polyline = self._filled_polygon(vertices, attribs) 296 else: 297 attribs['flags'] = const.POLYLINE_3D_POLYLINE 298 polyline = cast('Polyline', 299 self._factory('POLYLINE', dxfattribs=attribs)) 300 polyline.append_vertices(vertices) 301 if close: 302 polyline.close() 303 return polyline 304 305 def polyline_with_normals(self, data: bytes): 306 # Polyline without bulge values! 307 vertices, normal = self._load_vertices(data, load_normal=True) 308 return self._polyline(vertices, normal=normal) 309 310 def polyline(self, data: bytes): 311 # Polyline without bulge values! 312 vertices, normal = self._load_vertices(data, load_normal=False) 313 return self._polyline(vertices) 314 315 def polygon(self, data: bytes): 316 # Polyline without bulge values! 317 vertices, normal = self._load_vertices(data, load_normal=False) 318 return self._polyline(vertices, close=True) 319 320 def lwpolyline(self, data: bytes): 321 # OpenDesign Specs LWPLINE: 20.4.85 Page 211 322 logger.warning( 323 'Untested proxy graphic entity: LWPOLYLINE - Need examples!') 324 bs = BitStream(data) 325 flag = bs.read_bit_short() 326 attribs = self._build_dxf_attribs() 327 if flag & 4: 328 attribs['const_width'] = bs.read_bit_double() 329 if flag & 8: 330 attribs['elevation'] = bs.read_bit_double() 331 if flag & 2: 332 attribs['thickness'] = bs.read_bit_double() 333 if flag & 1: 334 attribs['extrusion'] = Vec3(bs.read_bit_double(3)) 335 336 num_points = bs.read_bit_long() 337 if flag & 16: 338 num_bulges = bs.read_bit_long() 339 else: 340 num_bulges = 0 341 342 if self.dxfversion >= 'AC1024': # R2010+ 343 vertex_id_count = bs.read_bit_long() 344 else: 345 vertex_id_count = 0 346 347 if flag & 32: 348 num_width = bs.read_bit_long() 349 else: 350 num_width = 0 351 # ignore DXF R13/14 special vertex order 352 353 vertices = [bs.read_raw_double(2)] 354 prev_point = vertices[-1] 355 for _ in range(num_points - 1): 356 x = bs.read_bit_double_default(default=prev_point[0]) 357 y = bs.read_bit_double_default(default=prev_point[1]) 358 prev_point = (x, y) 359 vertices.append(prev_point) 360 bulges = [bs.read_bit_double() for _ in range(num_bulges)] 361 vertex_ids = [bs.read_bit_long() for _ in range(vertex_id_count)] 362 widths = [(bs.read_bit_double(), bs.read_bit_double()) for _ in 363 range(num_width)] 364 if len(bulges) == 0: 365 bulges = list(repeat(0, num_points)) 366 if len(widths) == 0: 367 widths = list(repeat((0, 0), num_points)) 368 points = [] 369 for v, w, b in zip(vertices, widths, bulges): 370 points.append((v[0], v[1], w[0], w[1], b)) 371 lwpolyline = cast('LWPolyline', 372 self._factory('LWPOLYLINE', dxfattribs=attribs)) 373 lwpolyline.set_points(points) 374 return lwpolyline 375 376 def mesh(self, data: bytes): 377 logger.warning('Untested proxy graphic entity: MESH - Need examples!') 378 bs = ByteStream(data) 379 rows, columns = bs.read_struct('<2L') 380 attribs = self._build_dxf_attribs() 381 attribs['m_count'] = rows 382 attribs['n_count'] = columns 383 attribs['flags'] = const.POLYLINE_3D_POLYMESH 384 polymesh = cast('Polymesh', 385 self._factory('POLYLINE', dxfattribs=attribs)) 386 polymesh.append_vertices( 387 Vec3(bs.read_vertex()) for _ in range(rows * columns)) 388 return polymesh 389 390 def shell(self, data: bytes): 391 logger.warning('Untested proxy graphic entity: SHELL - Need examples!') 392 bs = ByteStream(data) 393 attribs = self._build_dxf_attribs() 394 attribs['flags'] = const.POLYLINE_POLYFACE 395 polyface = cast('Polyface', 396 self._factory('POLYLINE', dxfattribs=attribs)) 397 vertex_count = bs.read_long() 398 vertices = [Vec3(bs.read_vertex()) for _ in range(vertex_count)] 399 face_count = bs.read_long() 400 faces = [] 401 for i in range(face_count): 402 vertex_count = abs(bs.read_signed_long()) 403 face_indices = [bs.read_long() for _ in range(vertex_count)] 404 face = [vertices[index] for index in face_indices] 405 faces.append(face) 406 polyface.append_faces(faces) 407 polyface.optimize() 408 # todo: SHELL - read face properties, but requires an example. 409 return polyface 410 411 def text(self, data: bytes): 412 return self._text(data, unicode=False) 413 414 def unicode_text(self, data: bytes): 415 return self._text(data, unicode=True) 416 417 def _text(self, data: bytes, unicode: bool = False): 418 bs = ByteStream(data) 419 start_point = Vec3(bs.read_vertex()) 420 normal = Vec3(bs.read_vertex()) 421 text_direction = Vec3(bs.read_vertex()) 422 height, width_factor, oblique_angle = bs.read_struct('<3d') 423 if unicode: 424 text = bs.read_padded_unicode_string() 425 else: 426 text = bs.read_padded_string() 427 attribs = self._build_dxf_attribs() 428 attribs['insert'] = start_point 429 attribs['text'] = text 430 attribs['height'] = height 431 attribs['width'] = width_factor 432 attribs['rotation'] = text_direction.angle_deg 433 attribs['oblique'] = math.degrees(oblique_angle) 434 attribs['extrusion'] = normal 435 return self._factory('TEXT', dxfattribs=attribs) 436 437 def text2(self, data: bytes): 438 bs = ByteStream(data) 439 start_point = Vec3(bs.read_vertex()) 440 normal = Vec3(bs.read_vertex()) 441 text_direction = Vec3(bs.read_vertex()) 442 text = bs.read_padded_string() 443 ignore_length_of_string, raw = bs.read_struct('<2l') 444 height, width_factor, oblique_angle, tracking_percentage = bs.read_struct( 445 '<4d') 446 is_backwards, is_upside_down, is_vertical, is_underline, is_overline = bs.read_struct( 447 '<5L') 448 font_filename = bs.read_padded_string() 449 big_font_filename = bs.read_padded_string() 450 attribs = self._build_dxf_attribs() 451 attribs['insert'] = start_point 452 attribs['text'] = text 453 attribs['height'] = height 454 attribs['width'] = width_factor 455 attribs['rotation'] = text_direction.angle_deg 456 attribs['oblique'] = math.degrees(oblique_angle) 457 attribs['style'] = self._get_style(font_filename, big_font_filename) 458 attribs['text_generation_flag'] = 2 * is_backwards + 4 * is_upside_down 459 attribs['extrusion'] = normal 460 return self._factory('TEXT', dxfattribs=attribs) 461 462 def unicode_text2(self, data: bytes): 463 bs = ByteStream(data) 464 start_point = Vec3(bs.read_vertex()) 465 normal = Vec3(bs.read_vertex()) 466 text_direction = Vec3(bs.read_vertex()) 467 text = bs.read_padded_unicode_string() 468 ignore_length_of_string, ignore_raw = bs.read_struct('<2l') 469 height, width_factor, oblique_angle, tracking_percentage = bs.read_struct( 470 '<4d') 471 is_backwards, is_upside_down, is_vertical, is_underline, is_overline = bs.read_struct( 472 '<5L') 473 is_bold, is_italic, charset, pitch = bs.read_struct('<4L') 474 type_face = bs.read_padded_unicode_string() 475 font_filename = bs.read_padded_unicode_string() 476 big_font_filename = bs.read_padded_unicode_string() 477 attribs = self._build_dxf_attribs() 478 attribs['insert'] = start_point 479 attribs['text'] = text 480 attribs['height'] = height 481 attribs['width'] = width_factor 482 attribs['rotation'] = text_direction.angle_deg 483 attribs['oblique'] = math.degrees(oblique_angle) 484 attribs['style'] = self._get_style(font_filename, big_font_filename) 485 attribs['text_generation_flag'] = 2 * is_backwards + 4 * is_upside_down 486 attribs['extrusion'] = normal 487 return self._factory('TEXT', dxfattribs=attribs) 488 489 def xline(self, data: bytes): 490 return self._xline(data, 'XLINE') 491 492 def ray(self, data: bytes): 493 return self._xline(data, 'RAY') 494 495 def _xline(self, data: bytes, type_: str): 496 logger.warning( 497 'Untested proxy graphic entity: RAY/XLINE - Need examples!') 498 bs = ByteStream(data) 499 attribs = self._build_dxf_attribs() 500 start_point = Vec3(bs.read_vertex()) 501 other_point = Vec3(bs.read_vertex()) 502 attribs['start'] = start_point 503 attribs['unit_vector'] = (other_point - start_point).normalize() 504 return self._factory(type_, dxfattribs=attribs) 505 506 def _get_style(self, font: str, bigfont: str) -> str: 507 self.required_fonts.add(font) 508 if font in self.textstyles: 509 style = self.textstyles[font] 510 else: 511 style = font 512 if self._doc: 513 self._doc.styles.new(font, dxfattribs={'font': font, 514 'bigfont': bigfont}) 515 return style 516 517 def _load_vertices(self, data: bytes, load_normal=False): 518 normal = Z_AXIS 519 bs = ByteStream(data) 520 count = bs.read_long() 521 if load_normal: 522 count += 1 523 vertices = [] 524 while count > 0: 525 vertices.append(Vec3(bs.read_struct('<3d'))) 526 count -= 1 527 if load_normal: 528 normal = vertices.pop() 529 return vertices, normal 530 531 def _build_dxf_attribs(self) -> Dict: 532 attribs = dict() 533 if self.layer != '0': 534 attribs['layer'] = self.layer 535 if self.color != const.BYLAYER: 536 attribs['color'] = self.color 537 if self.linetype != 'BYLAYER': 538 attribs['linetype'] = self.linetype 539 if self.lineweight != const.LINEWEIGHT_DEFAULT: 540 attribs['lineweight'] = self.lineweight 541 if self.ltscale != 1.0: 542 attribs['ltscale'] = self.ltscale 543 if self.true_color is not None: 544 attribs['true_color'] = self.true_color 545 return attribs 546 547 548class ProxyGraphicDebugger(ProxyGraphic): 549 def __init__(self, data: bytes, doc: 'Drawing' = None, debug_stream=None): 550 super(ProxyGraphicDebugger, self).__init__(data, doc) 551 if debug_stream is None: 552 debug_stream = sys.stdout 553 self._debug_stream = debug_stream 554 555 def log_entities(self): 556 self.log_separator(char='=', newline=False) 557 self.log_message('Create virtual DXF entities:') 558 self.log_separator(newline=False) 559 for entity in self.virtual_entities(): 560 self.log_message(f"\n * {entity.dxftype()}") 561 self.log_message(f" * {entity.graphic_properties()}\n") 562 self.log_separator(char='=') 563 564 def log_commands(self): 565 self.log_separator(char='=', newline=False) 566 self.log_message('Raw proxy commands:') 567 self.log_separator(newline=False) 568 for index, size, cmd in self.info(): 569 self.log_message(f"Command: {cmd} Index: {index} Size: {size}") 570 self.log_separator(char='=') 571 572 def log_separator(self, char='-', newline=True): 573 self.log_message(char * 79) 574 if newline: 575 self.log_message('') 576 577 def log_message(self, msg: str): 578 print(msg, file=self._debug_stream) 579 580 def log_state(self): 581 self.log_message('> ' + self.get_state()) 582 583 def get_state(self) -> str: 584 return f"ly: '{self.layer}', clr: {self.color}, lt: {self.linetype}, " \ 585 f"lw: {self.lineweight}, ltscale: {self.ltscale}, " \ 586 f"rgb: {self.true_color}, fill: {self.fill}" 587 588 def attribute_color(self, data: bytes): 589 self.log_message('Command: set COLOR') 590 super().attribute_color(data) 591 self.log_state() 592 593 def attribute_layer(self, data: bytes): 594 self.log_message('Command: set LAYER') 595 super().attribute_layer(data) 596 self.log_state() 597 598 def attribute_linetype(self, data: bytes): 599 self.log_message('Command: set LINETYPE') 600 super().attribute_linetype(data) 601 self.log_state() 602 603 def attribute_true_color(self, data: bytes): 604 self.log_message('Command: set TRUE-COLOR') 605 super().attribute_true_color(data) 606 self.log_state() 607 608 def attribute_lineweight(self, data: bytes): 609 self.log_message('Command: set LINEWEIGHT') 610 super().attribute_lineweight(data) 611 self.log_state() 612 613 def attribute_ltscale(self, data: bytes): 614 self.log_message('Command: set LTSCALE') 615 super().attribute_ltscale(data) 616 self.log_state() 617 618 def attribute_fill(self, data: bytes): 619 self.log_message('Command: set FILL') 620 super().attribute_fill(data) 621 self.log_state() 622