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