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