1# Created: 06.2020
2# Copyright (c) 2020, Matthew Broadway
3# Copyright (c) 2020, Manfred Moitzi
4# License: MIT License
5from typing import (
6    TYPE_CHECKING, Dict, Optional, Tuple, Union, List, Set, cast,
7)
8import re
9from ezdxf.entities import Attrib
10from ezdxf.lldxf import const
11from ezdxf.addons.drawing.type_hints import Color, RGB
12from ezdxf.tools import fonts
13from ezdxf.addons import acadctb
14from ezdxf.sections.table import table_key as layer_key
15from ezdxf.colors import luminance, DXF_DEFAULT_COLORS, int2rgb
16from ezdxf.tools.pattern import scale_pattern, HatchPatternType
17from ezdxf.entities.ltype import CONTINUOUS_PATTERN
18
19if TYPE_CHECKING:
20    from ezdxf.eztypes import (
21        DXFGraphic, Layout, Table, Layer, Linetype, Drawing, Textstyle,
22    )
23
24__all__ = [
25    'Properties', 'LayerProperties', 'RenderContext', 'layer_key', 'rgb_to_hex',
26    'hex_to_rgb', 'MODEL_SPACE_BG_COLOR', 'PAPER_SPACE_BG_COLOR',
27    'VIEWPORT_COLOR', 'set_color_alpha',
28]
29
30table_key = layer_key
31MODEL_SPACE_BG_COLOR = '#212830'
32PAPER_SPACE_BG_COLOR = '#ffffff'
33VIEWPORT_COLOR = '#aaaaaa'  # arbitrary choice
34
35
36def is_dark_color(color: Color, dark: float = 0.2) -> bool:
37    luma = luminance(hex_to_rgb(color))
38    return luma <= dark
39
40
41class Filling:
42    SOLID = 0
43    PATTERN = 1
44    GRADIENT = 2
45
46    def __init__(self):
47        # Solid fill color is stored in Properties.color attribute
48        self.type = Filling.SOLID
49        # Gradient- or pattern name
50        self.name: str = 'SOLID'
51        # Gradient- or pattern angle
52        self.angle: float = 0.0  # in degrees
53        self.gradient_color1: Optional[Color] = None
54        self.gradient_color2: Optional[Color] = None
55        self.gradient_centered: float = 0.0  # todo: what's the meaning?
56        self.pattern_scale: float = 1.0
57        # Regular HATCH pattern definition:
58        self.pattern: HatchPatternType = []
59
60
61class Properties:
62    """ An implementation agnostic representation of entity properties like
63    color and linetype.
64    """
65
66    def __init__(self):
67        self.color: str = '#ffffff'  # format #RRGGBB or #RRGGBBAA
68        # Color names should be resolved into a actual color value
69
70        # Store linetype name for backends which don't have the ability to use
71        # user-defined linetypes, but have some predefined linetypes, maybe
72        # matching most common AutoCAD linetypes is possible.
73        # Store linetype names in UPPERCASE.
74        self.linetype_name: str = 'CONTINUOUS'
75
76        # Linetypes: Complex DXF linetypes are not supported:
77        # 1. Don't know if there are any backends which can use linetypes
78        #    including text or shapes
79        # 2. No decoder for SHX files available, which are the source for
80        #    shapes in linetypes
81        # 3. SHX files are copyrighted - including in ezdxf not possible
82        #
83        # Simplified DXF linetype definition:
84        # all line elements >= 0.0, 0.0 = point
85        # all gap elements > 0.0
86        # Usage as alternating line - gap sequence: line-gap-line-gap ....
87        # (line could be a point 0.0), line-line or gap-gap - makes no sense
88        # Examples:
89        # DXF: ("DASHED", "Dashed __ __ __ __ __ __ __ __ __ __ __ __ __ _",
90        #      [0.6, 0.5, -0.1])
91        # first entry 0.6 is the total pattern length = sum(linetype_pattern)
92        # linetype_pattern: [0.5, 0.1] = line-gap
93        # DXF: ("DASHDOTX2", "Dash dot (2x) ____  .  ____  .  ____  .  ____",
94        #      [2.4, 2.0, -0.2, 0.0, -0.2])
95        # linetype_pattern: [2.0, 0.2, 0.0, 0.2] = line-gap-point-gap
96        # Stored as tuple, so pattern could be used as key for caching.
97        # SVG dash-pattern does not support points, so a minimal line length
98        # (maybe inferred from linewidth?) has to be used, which may alter the
99        # overall line appearance - but linetype mapping will never be perfect.
100        # The continuous pattern is an empty tuple ()
101        self.linetype_pattern: Tuple[float, ...] = CONTINUOUS_PATTERN
102        self.linetype_scale: float = 1.0
103        # line weight in mm, todo: default lineweight is 0.25?
104        self.lineweight: float = 0.25
105        self.is_visible = True
106
107        # The 'layer' attribute stores the resolved layer of an entity:
108        # Entities inside of a block references get properties from the layer
109        # of the INSERT entity, if they reside on the layer '0'.
110        # To get the "real" layer of an entity, you have to use `entity.dxf.layer`
111        self.layer: str = '0'
112
113        # Font definition object for text entities:
114        # `None` is for the default font
115        self.font: Optional[fonts.FontFace] = None
116
117        # Filling properties: Solid, Pattern, Gradient
118        self.filling: Optional[Filling] = None
119
120        # default is unit less
121        self.units = 0
122
123    def __str__(self):
124        return f'({self.color}, {self.linetype_name}, {self.lineweight}, ' \
125               f'"{self.layer}")'
126
127    @property
128    def rgb(self) -> RGB:
129        """ Returns color as RGB tuple."""
130        return hex_to_rgb(self.color[:7])  # ignore alpha if present
131
132    @property
133    def luminance(self) -> float:
134        """ Returns perceived color luminance in range [0, 1] from dark to light.
135        """
136        return luminance(self.rgb)
137
138
139class LayerProperties(Properties):
140    """ Modified attribute meaning:
141
142        is_visible: Whether entities belonging to this layer should be drawn
143        layer: Stores real layer name (mixed case)
144
145    """
146
147    def __init__(self):
148        super().__init__()
149        self.has_aci_color_7 = False
150
151    def get_entity_color_from_layer(self, fg: Color) -> Color:
152        """ Returns the layer color or if layer color is ACI color 7 the
153        given layout default foreground color `fg`.
154        """
155        if self.has_aci_color_7:
156            return fg
157        else:
158            return self.color
159
160
161DEFAULT_LAYER_PROPERTIES = LayerProperties()
162
163
164class LayoutProperties:
165    # The LAYOUT, BLOCK and BLOCK_RECORD entities do not have
166    # explicit graphic properties.
167    def __init__(self):
168        self.name: str = 'Model'  # tab/display name
169        self.units = 0  # default is unit less
170        self._background_color: Color = MODEL_SPACE_BG_COLOR
171        self._default_color: Color = '#ffffff'
172        self._has_dark_background: bool = True
173
174    @property
175    def background_color(self) -> Color:
176        """ Returns the default layout background color. """
177        return self._background_color
178
179    @property
180    def default_color(self) -> Color:
181        """ Returns the default layout foreground color. """
182        return self._default_color
183
184    @property
185    def has_dark_background(self) -> bool:
186        """ Returns ``True`` if the actual background-color is "dark". """
187        return self._has_dark_background
188
189    def set_layout(self, layout: 'Layout', bg: Optional[Color] = None,
190                   fg: Optional[Color] = None,
191                   units: Optional[int] = None) -> None:
192        """ Setup default layout properties. """
193        self.name = layout.name
194        if bg is None:
195            if self.name == 'Model':
196                bg = MODEL_SPACE_BG_COLOR
197            else:
198                bg = PAPER_SPACE_BG_COLOR
199        self.set_colors(bg, fg)
200        if units is None:
201            self.units = layout.units
202        else:
203            self.units = int(units)
204
205    def set_colors(self, bg: Color, fg: Color = None) -> None:
206        """ Setup default layout colors.
207
208        Required color format "#RRGGBB" or including alpha transparency
209        "#RRGGBBAA".
210        """
211        if not is_valid_color(bg):
212            raise ValueError(f'Invalid background color: {bg}')
213        self._background_color = bg
214        if len(bg) == 9:  # including transparency
215            bg = bg[:7]
216        self._has_dark_background = is_dark_color(bg)
217        if fg is not None:
218            if not is_valid_color(fg):
219                raise ValueError(f'Invalid foreground color: {fg}')
220            self._default_color = fg
221        else:
222            self._default_color = '#ffffff' if self._has_dark_background \
223                else '#000000'
224
225
226class RenderContext:
227    def __init__(self, doc: Optional['Drawing'] = None, *, ctb: str = '',
228                 export_mode: bool = False):
229        """ Represents the render context for the DXF document `doc`.
230        A given `ctb` file (plot style file)  overrides the default properties.
231
232        Args:
233            doc: The document that is being drawn
234            ctb: A path to a plot style table to use
235            export_mode: Whether to render the document as it would look when
236                exported (plotted) by a CAD application to a file such as pdf,
237                or whether to render the document as it would appear inside a
238                CAD application.
239        """
240        self._saved_states: List[Properties] = []
241        self.line_pattern = _load_line_pattern(doc.linetypes) if doc else dict()
242        self.current_layout = LayoutProperties()  # default is 'Model'
243        self.current_block_reference: Optional[Properties] = None
244        self.plot_styles = self._load_plot_style_table(ctb)
245        self.export_mode = export_mode
246        # Always consider: entity layer may not exist
247        # Layer name as key is normalized, most likely name.lower(), but may
248        # change in the future.
249        self.layers: Dict[str, LayerProperties] = dict()
250        # Text-style -> font mapping
251        self.fonts: Dict[str, fonts.FontFace] = dict()
252        self.units = 0  # store modelspace units as enum, see ezdxf/units.py
253        self.linetype_scale: float = 1.0  # overall modelspace linetype scaling
254        self.measurement: int = 0
255        self.pdsize = 0
256        self.pdmode = 0
257        if doc:
258            self.linetype_scale = doc.header.get('$LTSCALE', 1.0)
259            self.units = doc.header.get('$INSUNITS', 0)
260            self.measurement = doc.header.get('$MEASUREMENT', 0)
261            self.pdsize = doc.header.get('$PDSIZE', 1.0)
262            self.pdmode = doc.header.get('$PDMODE', 0)
263            self._setup_layers(doc)
264            self._setup_text_styles(doc)
265            if self.units == 0:
266                # set default units based on measurement system:
267                # imperial (0) / metric (1)
268                if self.measurement == 1:
269                    self.units = 6  # 1 m
270                else:
271                    self.units = 1  # 1 in
272        self.current_layout.units = self.units
273        self._hatch_pattern_cache: Dict[str, HatchPatternType] = dict()
274
275    def update_backend_configuration(self, backend):
276        """ Configuration parameters are stored in the backend and may be
277        changed by the backend at runtime. Some parameters are stored globally
278        in the header section of the DXF document. This method must be called
279        if a new DXF document was loaded.
280
281        """
282        # This DXF document parameters are not accessible by the backend
283        # in a direct way:
284        if backend.pdsize is None:
285            backend.pdsize = self.pdsize
286        if backend.pdmode is None:
287            backend.pdmode = self.pdmode
288        backend.measurement = self.measurement
289
290    def _setup_layers(self, doc: 'Drawing'):
291        for layer in doc.layers:  # type: Layer
292            self.add_layer(layer)
293
294    def _setup_text_styles(self, doc: 'Drawing'):
295        for text_style in doc.styles:  # type: Textstyle
296            self.add_text_style(text_style)
297
298    def add_layer(self, layer: 'Layer') -> None:
299        """ Setup layer properties. """
300        properties = LayerProperties()
301        name = layer_key(layer.dxf.name)
302        # Store real layer name (mixed case):
303        properties.layer = layer.dxf.name
304        properties.color = self._true_layer_color(layer)
305
306        # Depend layer ACI color from layout background color?
307        # True color overrides ACI color and layers with only true color set
308        # have default ACI color 7!
309        if not layer.has_dxf_attrib('true_color'):
310            properties.has_aci_color_7 = layer.dxf.color == 7
311
312        # Normalize linetype names to UPPERCASE:
313        properties.linetype_name = str(layer.dxf.linetype).upper()
314        properties.linetype_pattern = self.line_pattern.get(
315            properties.linetype_name, CONTINUOUS_PATTERN)
316        properties.lineweight = self._true_layer_lineweight(
317            layer.dxf.lineweight)
318        properties.is_visible = layer.is_on() and not layer.is_frozen()
319        if self.export_mode:
320            properties.is_visible &= bool(layer.dxf.plot)
321        self.layers[name] = properties
322
323    def add_text_style(self, text_style: 'Textstyle'):
324        """ Setup text style properties. """
325        name = table_key(text_style.dxf.name)
326        font_file = text_style.dxf.font
327        font_face = None
328        if font_file == "":  # Font family stored in XDATA?
329            family, italic, bold = text_style.get_extended_font_data()
330            if family:
331                font_face = fonts.find_font_face_by_family(family, italic, bold)
332        else:
333            font_face = fonts.get_font_face(font_file, map_shx=True)
334
335        if font_face is None:  # fall back to default font
336            font_face = fonts.FontFace()
337        self.fonts[name] = font_face
338
339    def _true_layer_color(self, layer: 'Layer') -> Color:
340        if layer.dxf.hasattr('true_color'):
341            return rgb_to_hex(layer.rgb)
342        else:
343            # Don't use layer.dxf.color: color < 0 is layer state off
344            aci = layer.color
345            # aci: 0=BYBLOCK, 256=BYLAYER, 257=BYOBJECT
346            if aci < 1 or aci > 255:
347                aci = 7  # default layer color
348            return self._aci_to_true_color(aci)
349
350    def _true_layer_lineweight(self, lineweight: int) -> float:
351        if lineweight < 0:
352            return self.default_lineweight()
353        else:
354            return float(lineweight) / 100.0
355
356    @staticmethod
357    def _load_plot_style_table(filename: str):
358        # Each layout can have a different plot style table stored in
359        # Layout.dxf.current_style_sheet.
360        # HEADER var $STYLESHEET stores the default ctb-file name.
361        try:
362            ctb = acadctb.load(filename)
363        except IOError:
364            ctb = acadctb.new_ctb()
365
366        # Colors in CTB files can be RGB colors but don't have to,
367        # therefore initialize color without RGB values by the
368        # default AutoCAD palette:
369        for aci in range(1, 256):
370            entry = ctb[aci]
371            if entry.has_object_color():
372                # initialize with default AutoCAD palette
373                entry.color = int2rgb(DXF_DEFAULT_COLORS[aci])
374        return ctb
375
376    def set_layers_state(self, layers: Set[str], state=True):
377        """ Set layer state of `layers` to on/off.
378
379        Args:
380             layers: set of layer names
381             state: `True` turn this `layers` on and others off,
382                    `False` turn this `layers` off and others on
383        """
384        layers = {layer_key(name) for name in layers}
385        for name, layer in self.layers.items():
386            if name in layers:
387                layer.is_visible = state
388            else:
389                layer.is_visible = not state
390
391    def set_current_layout(self, layout: 'Layout'):
392        self.current_layout.set_layout(layout, units=self.units)
393
394    @property
395    def inside_block_reference(self) -> bool:
396        """ Returns ``True`` if current processing state is inside of a block
397        reference (INSERT).
398        """
399        return bool(self.current_block_reference)
400
401    def push_state(self, block_reference: Properties) -> None:
402        self._saved_states.append(self.current_block_reference)
403        self.current_block_reference = block_reference
404
405    def pop_state(self) -> None:
406        self.current_block_reference = self._saved_states.pop()
407
408    def resolve_all(self, entity: 'DXFGraphic') -> Properties:
409        """ Resolve all properties of `entity`. """
410        p = Properties()
411        p.layer = self.resolve_layer(entity)
412        resolved_layer = layer_key(p.layer)
413        p.units = self.resolve_units()
414        p.color = self.resolve_color(entity, resolved_layer=resolved_layer)
415        p.linetype_name, p.linetype_pattern = \
416            self.resolve_linetype(entity, resolved_layer=resolved_layer)
417        p.lineweight = self.resolve_lineweight(entity,
418                                               resolved_layer=resolved_layer)
419        p.linetype_scale = self.resolve_linetype_scale(entity)
420        p.is_visible = self.resolve_visible(entity,
421                                            resolved_layer=resolved_layer)
422        if entity.is_supported_dxf_attrib('style'):
423            p.font = self.resolve_font(entity)
424        if entity.dxftype() == 'HATCH':
425            p.filling = self.resolve_filling(entity)
426        return p
427
428    def resolve_units(self) -> int:
429        return self.current_layout.units
430
431    def resolve_linetype_scale(self, entity: 'DXFGraphic') -> float:
432        return entity.dxf.ltscale * self.linetype_scale
433
434    def resolve_visible(self, entity: 'DXFGraphic', *,
435                        resolved_layer: Optional[str] = None) -> bool:
436        """ Resolve the visibility state of `entity`.
437        Returns ``True`` if `entity` is visible.
438        """
439        entity_layer = resolved_layer or layer_key(self.resolve_layer(entity))
440        layer_properties = self.layers.get(entity_layer)
441        if layer_properties and not layer_properties.is_visible:
442            return False
443        elif entity.dxftype() == 'ATTRIB':
444            return (not bool(entity.dxf.invisible) and
445                    not cast(Attrib, entity).is_invisible)
446        else:
447            return not bool(entity.dxf.invisible)
448
449    def resolve_layer(self, entity: 'DXFGraphic') -> str:
450        """ Resolve the layer of `entity`, this is only relevant for entities
451        inside of block references.
452        """
453        layer = entity.dxf.layer
454        if layer == '0' and self.inside_block_reference:
455            layer = self.current_block_reference.layer
456        return layer
457
458    def resolve_color(self, entity: 'DXFGraphic', *,
459                      resolved_layer: Optional[str] = None) -> Color:
460        """ Resolve the rgb-color of `entity` as hex color string:
461        "#RRGGBB" or "#RRGGBBAA".
462        """
463        if entity.dxf.hasattr('true_color'):
464            # An existing true color value always overrides ACI color!
465            # Do not default to BYLAYER or BYBLOCK, this ACI value is ignored!
466            aci = 7
467        else:
468            aci = entity.dxf.color  # defaults to BYLAYER
469
470        if aci == const.BYLAYER:
471            entity_layer = resolved_layer or layer_key(
472                self.resolve_layer(entity))
473            layer = self.layers.get(
474                entity_layer, DEFAULT_LAYER_PROPERTIES)
475            color = layer.get_entity_color_from_layer(
476                self.current_layout.default_color)
477        elif aci == const.BYBLOCK:
478            if not self.inside_block_reference:
479                color = self.current_layout.default_color
480            else:
481                color = self.current_block_reference.color
482        else:  # BYOBJECT
483            color = self._true_entity_color(entity.rgb, aci)
484
485        alpha = int(round((1.0 - entity.transparency) * 255))
486        if alpha == 255:
487            return color
488        else:
489            return set_color_alpha(color, alpha)
490
491    def _true_entity_color(self,
492                           true_color: Optional[Tuple[int, int, int]],
493                           aci: int) -> Color:
494        """ Returns rgb color in hex format: "#RRGGBB".
495
496        `true_color` has higher priority than `aci`.
497        """
498        if true_color is not None:
499            return rgb_to_hex(true_color)
500        elif 0 < aci < 256:
501            return self._aci_to_true_color(aci)
502        else:
503            return self.current_layout.default_color  # unknown / invalid
504
505    def _aci_to_true_color(self, aci: int) -> Color:
506        """ Returns the `aci` value (AutoCAD Color Index) as rgb value in
507        hex format: "#RRGGBB".
508        """
509        if aci == 7:  # black/white; todo: this bypasses the plot style table
510            if self.current_layout.has_dark_background:
511                return '#ffffff'
512            else:
513                return '#000000'
514        else:
515            return rgb_to_hex(self.plot_styles[aci].color)
516
517    def resolve_linetype(self, entity: 'DXFGraphic', *,
518                         resolved_layer: str = None
519                         ) -> Tuple[str, Tuple[float, ...]]:
520        """ Resolve the linetype of `entity`. Returns a tuple of the linetype
521        name as upper-case string and the simplified linetype pattern as tuple
522        of floats.
523        """
524        aci = entity.dxf.color
525        # Not sure if plotstyle table overrides actual entity setting?
526        if (0 < aci < 256) and \
527                self.plot_styles[aci].linetype != acadctb.OBJECT_LINETYPE:
528            # todo: return special line types - overriding linetypes by
529            #  plotstyle table
530            pass
531        name = entity.dxf.linetype.upper()  # default is 'BYLAYER'
532        if name == 'BYLAYER':
533            entity_layer = resolved_layer or layer_key(
534                self.resolve_layer(entity))
535            layer = self.layers.get(entity_layer, DEFAULT_LAYER_PROPERTIES)
536            name = layer.linetype_name
537            pattern = layer.linetype_pattern
538
539        elif name == 'BYBLOCK':
540            if self.inside_block_reference:
541                name = self.current_block_reference.linetype_name
542                pattern = self.current_block_reference.linetype_pattern
543            else:
544                # There is no default layout linetype
545                name = 'STANDARD'
546                pattern = CONTINUOUS_PATTERN
547        else:
548            pattern = self.line_pattern.get(name, CONTINUOUS_PATTERN)
549        return name, pattern
550
551    def resolve_lineweight(self, entity: 'DXFGraphic', *,
552                           resolved_layer: str = None) -> float:
553        """ Resolve the lineweight of `entity` in mm.
554
555        DXF stores the lineweight in mm times 100 (e.g. 0.13mm = 13).
556        The smallest line weight is 0 and the biggest line weight is 211.
557        The DXF/DWG format is limited to a fixed value table,
558        see: :attr:`ezdxf.lldxf.const.VALID_DXF_LINEWEIGHTS`
559
560        CAD applications draw lineweight 0mm as an undefined small value, to
561        prevent backends to draw nothing for lineweight 0mm the smallest
562        return value is 0.01mm.
563
564        """
565
566        def lineweight():
567            aci = entity.dxf.color
568            # Not sure if plotstyle table overrides actual entity setting?
569            if (0 < aci < 256) and self.plot_styles[
570                aci].lineweight != acadctb.OBJECT_LINEWEIGHT:
571                # overriding lineweight by plotstyle table
572                return self.plot_styles.get_lineweight(aci)
573            lineweight = entity.dxf.lineweight  # default is BYLAYER
574            if lineweight == const.LINEWEIGHT_BYLAYER:
575                entity_layer = resolved_layer or layer_key(
576                    self.resolve_layer(entity))
577                return self.layers.get(entity_layer,
578                                       DEFAULT_LAYER_PROPERTIES).lineweight
579
580            elif lineweight == const.LINEWEIGHT_BYBLOCK:
581                if self.inside_block_reference:
582                    return self.current_block_reference.lineweight
583                else:
584                    # There is no default layout lineweight
585                    return self.default_lineweight()
586            elif lineweight == const.LINEWEIGHT_DEFAULT:
587                return self.default_lineweight()
588            else:
589                return float(lineweight) / 100.0
590
591        return max(0.01, lineweight())
592
593    def default_lineweight(self):
594        """ Returns the default lineweight of the document. """
595        # todo: is this value stored anywhere (e.g. HEADER section)?
596        return 0.25
597
598    def resolve_font(self, entity: 'DXFGraphic') -> Optional[fonts.FontFace]:
599        """ Resolve the text style of `entity` to a font name.
600        Returns ``None`` for the default font.
601        """
602        # todo: extended font data
603        style = entity.dxf.get('style', 'Standard')
604        return self.fonts.get(table_key(style))
605
606    def resolve_filling(self, entity: 'DXFGraphic') -> Optional[Filling]:
607        """ Resolve filling properties (SOLID, GRADIENT, PATTERN) of `entity`.
608        """
609
610        def setup_gradient():
611            filling.type = Filling.GRADIENT
612            filling.name = gradient.name.upper()
613            # todo: no idea when to use aci1 and aci2
614            filling.color1 = rgb_to_hex(gradient.color1)
615            if gradient.one_color:
616                c = round(gradient.tint * 255)  # channel value
617                filling.color2 = rgb_to_hex((c, c, c))
618            else:
619                filling.color2 = rgb_to_hex(gradient.color2)
620
621            filling.angle = gradient.rotation
622            filling.gradient_centered = gradient.centered
623
624        def setup_pattern():
625            filling.type = Filling.PATTERN
626            filling.name = hatch.dxf.pattern_name.upper()
627            filling.pattern_scale = hatch.dxf.pattern_scale
628            filling.angle = hatch.dxf.pattern_angle
629            if hatch.dxf.pattern_double:
630                # This value is not editable by CAD-App-GUI:
631                filling.pattern_scale *= 2  # todo: is this correct?
632
633            filling.pattern = self._hatch_pattern_cache.get(filling.name)
634            if filling.pattern:
635                return
636
637            pattern = hatch.pattern
638            if not pattern:
639                return
640
641            # DXF stores the hatch pattern already rotated and scaled,
642            # pattern_scale and pattern_rotation are just hints for the CAD
643            # application to modify the pattern if required.
644            # It's better to revert the scaling and rotation, because in general
645            # back-ends do not handle pattern that way, they need a base-pattern
646            # and separated scaling and rotation attributes and these
647            # base-pattern could be cached by their name.
648            #
649            # There is no advantage of simplifying the hatch line pattern and
650            # this format is required by the PatternAnalyser():
651            filling.pattern = scale_pattern(
652                pattern.as_list(),
653                1.0 / filling.pattern_scale,
654                -filling.angle
655            )
656            self._hatch_pattern_cache[filling.name] = filling.pattern
657
658        if entity.dxftype() != 'HATCH':
659            return None
660
661        hatch = cast('Hatch', entity)
662        filling = Filling()
663        if hatch.dxf.solid_fill:
664            gradient = hatch.gradient
665            if gradient is None:
666                filling.type = Filling.SOLID
667            else:
668                if gradient.kind == 0:  # Solid
669                    filling.type = Filling.SOLID
670                    filling.color1 = rgb_to_hex(gradient.color1)
671                else:
672                    setup_gradient()
673        else:
674            setup_pattern()
675        return filling
676
677
678COLOR_PATTERN = re.compile('#[0-9A-Fa-f]{6,8}')
679
680
681def is_valid_color(color: Color) -> bool:
682    if type(color) is not Color:
683        raise TypeError(f'Invalid argument type: {type(color)}.')
684    if len(color) in (7, 9):
685        return bool(COLOR_PATTERN.fullmatch(color))
686    return False
687
688
689def rgb_to_hex(
690        rgb: Union[Tuple[int, int, int], Tuple[float, float, float]]) -> Color:
691    """ Returns color in hex format: "#RRGGBB". """
692    assert all(0 <= x <= 255 for x in rgb), f'invalid RGB color: {rgb}'
693    r, g, b = rgb
694    return f'#{r:02x}{g:02x}{b:02x}'
695
696
697def hex_to_rgb(hex_string: Color) -> RGB:
698    """ Returns hex string color as (r, g, b) tuple. """
699    hex_string = hex_string.lstrip('#')
700    assert len(hex_string) == 6
701    r = int(hex_string[0:2], 16)
702    g = int(hex_string[2:4], 16)
703    b = int(hex_string[4:6], 16)
704    return r, g, b
705
706
707def set_color_alpha(color: Color, alpha: int) -> Color:
708    """ Returns `color` including the new `alpha` channel in hex format:
709    "#RRGGBBAA".
710
711    Args:
712        color: may be an RGB or RGBA hex color string
713        alpha: the new alpha value (0-255)
714    """
715    assert color.startswith('#') and len(color) in (
716        7, 9), f'invalid RGB color: "{color}"'
717    assert 0 <= alpha < 256, f'alpha out of range: {alpha}'
718    return f'{color[:7]}{alpha:02x}'
719
720
721def _load_line_pattern(linetypes: 'Table') -> Dict[str, Tuple]:
722    """ Load linetypes defined in a DXF document into  as dictionary,
723    key is the upper case linetype name, value is the simplified line pattern,
724    see :func:`compile_line_pattern`.
725    """
726    pattern = dict()
727    for linetype in linetypes:  # type: Linetype
728        name = linetype.dxf.name.upper()
729        pattern[name] = linetype.pattern_tags.compile()
730    return pattern
731