Skip to content
Permalink
c8a1013e7b
Switch branches/tags

Name already in use

A tag already exists with the provided branch name. Many Git commands accept both tag and branch names, so creating this branch may cause unexpected behavior. Are you sure you want to create this branch?
Go to file
 
 
Cannot retrieve contributors at this time
765 lines (662 sloc) 29.2 KB
# Created: 06.2020
# Copyright (c) 2020, Matthew Broadway
# Copyright (c) 2020, Manfred Moitzi
# License: MIT License
from typing import (
TYPE_CHECKING, Dict, Optional, Tuple, Union, List, Set, cast,
)
import re
from ezdxf.entities import Attrib
from ezdxf.lldxf import const
from ezdxf.addons.drawing.type_hints import Color, RGB
from ezdxf.addons.drawing import fonts
from ezdxf.addons import acadctb
from ezdxf.sections.table import table_key as layer_key
from ezdxf.colors import luminance, DXF_DEFAULT_COLORS, int2rgb
from ezdxf.tools.pattern import scale_pattern, HatchPatternType
from ezdxf.entities.ltype import CONTINUOUS_PATTERN
if TYPE_CHECKING:
from ezdxf.eztypes import (
DXFGraphic, Layout, Table, Layer, Linetype, Drawing, Textstyle,
)
__all__ = [
'Properties', 'LayerProperties', 'RenderContext', 'layer_key', 'rgb_to_hex',
'hex_to_rgb', 'MODEL_SPACE_BG_COLOR', 'PAPER_SPACE_BG_COLOR',
'VIEWPORT_COLOR', 'set_color_alpha',
]
table_key = layer_key
MODEL_SPACE_BG_COLOR = '#212830'
PAPER_SPACE_BG_COLOR = '#ffffff'
VIEWPORT_COLOR = '#aaaaaa' # arbitrary choice
SHX_FONTS = {
# See examples in: CADKitSamples/Shapefont.dxf
# Shape file structure is not documented, therefore replace this fonts by
# true type fonts.
# `None` is for: use the default font.
'AMGDT': "amgdt___.ttf", # Tolerance symbols
'AMGDT.SHX': "amgdt___.ttf",
'COMPLEX': "complex_.ttf",
'COMPLEX.SHX': "complex_.ttf",
'ISOCP': "isocp.ttf",
'ISOCP.SHX': "isocp.ttf",
'ITALIC': "italicc_.ttf",
'ITALIC.SHX': "italicc_.ttf",
'GOTHICG': "gothicg_.ttf",
'GOTHICG.SHX': "gothicg_.ttf",
'GREEKC': "greekc.ttf",
'GREEKC.SHX': "greekc.ttf",
'ROMANS': "romans__.ttf",
'ROMANS.SHX': "romans__.ttf",
'SCRIPTS': "scripts_.ttf",
'SCRIPTS.SHX': "scripts_.ttf",
'SCRIPTC': "scriptc_.ttf",
'SCRIPTC.SHX': "scriptc_.ttf",
'SIMPLEX': "simplex_.ttf",
'SIMPLEX.SHX': "simplex_.ttf",
'SYMATH': "symath__.ttf",
'SYMATH.SHX': "symath__.ttf",
'SYMAP': "symap___.ttf",
'SYMAP.SHX': "symap___.ttf",
'SYMETEO': "symeteo_.ttf",
'SYMETEO.SHX': "symeteo_.ttf",
'TXT': "monotxt_.ttf", # Default AutoCAD font
'TXT.SHX': "monotxt_.ttf",
}
def is_dark_color(color: Color, dark: float = 0.2) -> bool:
luma = luminance(hex_to_rgb(color))
return luma <= dark
class Filling:
SOLID = 0
PATTERN = 1
GRADIENT = 2
def __init__(self):
# Solid fill color is stored in Properties.color attribute
self.type = Filling.SOLID
# Gradient- or pattern name
self.name: str = 'SOLID'
# Gradient- or pattern angle
self.angle: float = 0.0 # in degrees
self.gradient_color1: Optional[Color] = None
self.gradient_color2: Optional[Color] = None
self.gradient_centered: float = 0.0 # todo: what's the meaning?
self.pattern_scale: float = 1.0
# Regular HATCH pattern definition:
self.pattern: HatchPatternType = []
class Properties:
""" An implementation agnostic representation of entity properties like
color and linetype.
"""
def __init__(self):
self.color: str = '#ffffff' # format #RRGGBB or #RRGGBBAA
# Color names should be resolved into a actual color value
# Store linetype name for backends which don't have the ability to use
# user-defined linetypes, but have some predefined linetypes, maybe
# matching most common AutoCAD linetypes is possible.
# Store linetype names in UPPERCASE.
self.linetype_name: str = 'CONTINUOUS'
# Linetypes: Complex DXF linetypes are not supported:
# 1. Don't know if there are any backends which can use linetypes
# including text or shapes
# 2. No decoder for SHX files available, which are the source for
# shapes in linetypes
# 3. SHX files are copyrighted - including in ezdxf not possible
#
# Simplified DXF linetype definition:
# all line elements >= 0.0, 0.0 = point
# all gap elements > 0.0
# Usage as alternating line - gap sequence: line-gap-line-gap ....
# (line could be a point 0.0), line-line or gap-gap - makes no sense
# Examples:
# DXF: ("DASHED", "Dashed __ __ __ __ __ __ __ __ __ __ __ __ __ _",
# [0.6, 0.5, -0.1])
# first entry 0.6 is the total pattern length = sum(linetype_pattern)
# linetype_pattern: [0.5, 0.1] = line-gap
# DXF: ("DASHDOTX2", "Dash dot (2x) ____ . ____ . ____ . ____",
# [2.4, 2.0, -0.2, 0.0, -0.2])
# linetype_pattern: [2.0, 0.2, 0.0, 0.2] = line-gap-point-gap
# Stored as tuple, so pattern could be used as key for caching.
# SVG dash-pattern does not support points, so a minimal line length
# (maybe inferred from linewidth?) has to be used, which may alter the
# overall line appearance - but linetype mapping will never be perfect.
# The continuous pattern is an empty tuple ()
self.linetype_pattern: Tuple[float, ...] = CONTINUOUS_PATTERN
self.linetype_scale: float = 1.0
# line weight in mm, todo: default lineweight is 0.25?
self.lineweight: float = 0.25
self.is_visible = True
# The 'layer' attribute stores the resolved layer of an entity:
# Entities inside of a block references get properties from the layer
# of the INSERT entity, if they reside on the layer '0'.
# To get the "real" layer of an entity, you have to use `entity.dxf.layer`
self.layer: str = '0'
# Font definition object for text entities:
# `None` is for the default font
self.font: Optional[fonts.Font] = None
# Filling properties: Solid, Pattern, Gradient
self.filling: Optional[Filling] = None
# default is unit less
self.units = 0
def __str__(self):
return f'({self.color}, {self.linetype_name}, {self.lineweight}, ' \
f'"{self.layer}")'
@property
def rgb(self) -> RGB:
""" Returns color as RGB tuple."""
return hex_to_rgb(self.color[:7]) # ignore alpha if present
@property
def luminance(self) -> float:
""" Returns perceived color luminance in range [0, 1] from dark to light.
"""
return luminance(self.rgb)
class LayerProperties(Properties):
""" Modified attribute meaning:
is_visible: Whether entities belonging to this layer should be drawn
layer: Stores real layer name (mixed case)
"""
def __init__(self):
super().__init__()
self.has_aci_color_7 = False
def get_entity_color_from_layer(self, fg: Color) -> Color:
""" Returns the layer color or if layer color is ACI color 7 the
given layout default foreground color `fg`.
"""
if self.has_aci_color_7:
return fg
else:
return self.color
DEFAULT_LAYER_PROPERTIES = LayerProperties()
class LayoutProperties:
# The LAYOUT, BLOCK and BLOCK_RECORD entities do not have
# explicit graphic properties.
def __init__(self):
self.name: str = 'Model' # tab/display name
self.units = 0 # default is unit less
self._background_color: Color = MODEL_SPACE_BG_COLOR
self._default_color: Color = '#ffffff'
self._has_dark_background: bool = True
@property
def background_color(self) -> Color:
""" Returns the default layout background color. """
return self._background_color
@property
def default_color(self) -> Color:
""" Returns the default layout foreground color. """
return self._default_color
@property
def has_dark_background(self) -> bool:
""" Returns ``True`` if the actual background-color is "dark". """
return self._has_dark_background
def set_layout(self, layout: 'Layout', bg: Optional[Color] = None,
fg: Optional[Color] = None,
units: Optional[int] = None) -> None:
""" Setup default layout properties. """
self.name = layout.name
if bg is None:
if self.name == 'Model':
bg = MODEL_SPACE_BG_COLOR
else:
bg = PAPER_SPACE_BG_COLOR
self.set_colors(bg, fg)
if units is None:
self.units = layout.units
else:
self.units = int(units)
def set_colors(self, bg: Color, fg: Color = None) -> None:
""" Setup default layout colors.
Required color format "#RRGGBB" or including alpha transparency
"#RRGGBBAA".
"""
if not is_valid_color(bg):
raise ValueError(f'Invalid background color: {bg}')
self._background_color = bg
if len(bg) == 9: # including transparency
bg = bg[:7]
self._has_dark_background = is_dark_color(bg)
if fg is not None:
if not is_valid_color(fg):
raise ValueError(f'Invalid foreground color: {fg}')
self._default_color = fg
else:
self._default_color = '#ffffff' if self._has_dark_background \
else '#000000'
class RenderContext:
def __init__(self, doc: Optional['Drawing'] = None, *, ctb: str = '',
export_mode: bool = False):
""" Represents the render context for the DXF document `doc`.
A given `ctb` file (plot style file) overrides the default properties.
Args:
doc: The document that is being drawn
ctb: A path to a plot style table to use
export_mode: Whether to render the document as it would look when
exported (plotted) by a CAD application to a file such as pdf,
or whether to render the document as it would appear inside a
CAD application.
"""
self._saved_states: List[Properties] = []
self.line_pattern = _load_line_pattern(doc.linetypes) if doc else dict()
self.current_layout = LayoutProperties() # default is 'Model'
self.current_block_reference: Optional[Properties] = None
self.plot_styles = self._load_plot_style_table(ctb)
self.export_mode = export_mode
# Always consider: entity layer may not exist
# Layer name as key is normalized, most likely name.lower(), but may
# change in the future.
self.layers: Dict[str, LayerProperties] = dict()
# Text-style -> font mapping
self.fonts: Dict[str, fonts.Font] = dict()
self.units = 0 # store modelspace units as enum, see ezdxf/units.py
self.linetype_scale: float = 1.0 # overall modelspace linetype scaling
self.measurement: int = 0
self.pdsize = 0
self.pdmode = 0
if doc:
self.linetype_scale = doc.header.get('$LTSCALE', 1.0)
self.units = doc.header.get('$INSUNITS', 0)
self.measurement = doc.header.get('$MEASUREMENT', 0)
self.pdsize = doc.header.get('$PDSIZE', 1.0)
self.pdmode = doc.header.get('$PDMODE', 0)
self._setup_layers(doc)
self._setup_text_styles(doc)
if self.units == 0:
# set default units based on measurement system:
# imperial (0) / metric (1)
if self.measurement == 1:
self.units = 6 # 1 m
else:
self.units = 1 # 1 in
self.current_layout.units = self.units
self._hatch_pattern_cache: Dict[str, HatchPatternType] = dict()
def update_backend_configuration(self, backend):
""" Configuration parameters are stored in the backend and may be
changed by the backend at runtime. Some parameters are stored globally
in the header section of the DXF document. This method must be called
if a new DXF document was loaded.
"""
# This DXF document parameters are not accessible by the backend
# in a direct way:
if backend.pdsize is None:
backend.pdsize = self.pdsize
if backend.pdmode is None:
backend.pdmode = self.pdmode
backend.measurement = self.measurement
def _setup_layers(self, doc: 'Drawing'):
for layer in doc.layers: # type: Layer
self.add_layer(layer)
def _setup_text_styles(self, doc: 'Drawing'):
for text_style in doc.styles: # type: Textstyle
self.add_text_style(text_style)
def add_layer(self, layer: 'Layer') -> None:
""" Setup layer properties. """
properties = LayerProperties()
name = layer_key(layer.dxf.name)
# Store real layer name (mixed case):
properties.layer = layer.dxf.name
properties.color = self._true_layer_color(layer)
# Depend layer ACI color from layout background color?
# True color overrides ACI color and layers with only true color set
# have default ACI color 7!
if not layer.has_dxf_attrib('true_color'):
properties.has_aci_color_7 = layer.dxf.color == 7
# Normalize linetype names to UPPERCASE:
properties.linetype_name = str(layer.dxf.linetype).upper()
properties.linetype_pattern = self.line_pattern.get(
properties.linetype_name, CONTINUOUS_PATTERN)
properties.lineweight = self._true_layer_lineweight(
layer.dxf.lineweight)
properties.is_visible = layer.is_on() and not layer.is_frozen()
if self.export_mode:
properties.is_visible &= bool(layer.dxf.plot)
self.layers[name] = properties
def add_text_style(self, text_style: 'Textstyle'):
""" Setup text style properties. """
name = table_key(text_style.dxf.name)
ttf = text_style.dxf.font
# Map SHX fonts to True Type Fonts:
font_upper = ttf.upper()
if font_upper in SHX_FONTS:
ttf = SHX_FONTS[font_upper]
# Only ttf-fonts are supported
elif not font_upper.endswith('.TTF'):
ttf = None # use default font
if ttf:
font = fonts.get(ttf)
else: # default font
font = fonts.Font(
'arial.ttf', 'Arial', 'normal', 'normal', 'normal')
self.fonts[name] = font
def _true_layer_color(self, layer: 'Layer') -> Color:
if layer.dxf.hasattr('true_color'):
return rgb_to_hex(layer.rgb)
else:
# Don't use layer.dxf.color: color < 0 is layer state off
aci = layer.color
# aci: 0=BYBLOCK, 256=BYLAYER, 257=BYOBJECT
if aci < 1 or aci > 255:
aci = 7 # default layer color
return self._aci_to_true_color(aci)
def _true_layer_lineweight(self, lineweight: int) -> float:
if lineweight < 0:
return self.default_lineweight()
else:
return float(lineweight) / 100.0
@staticmethod
def _load_plot_style_table(filename: str):
# Each layout can have a different plot style table stored in
# Layout.dxf.current_style_sheet.
# HEADER var $STYLESHEET stores the default ctb-file name.
try:
ctb = acadctb.load(filename)
except IOError:
ctb = acadctb.new_ctb()
# Colors in CTB files can be RGB colors but don't have to,
# therefore initialize color without RGB values by the
# default AutoCAD palette:
for aci in range(1, 256):
entry = ctb[aci]
if entry.has_object_color():
# initialize with default AutoCAD palette
entry.color = int2rgb(DXF_DEFAULT_COLORS[aci])
return ctb
def set_layers_state(self, layers: Set[str], state=True):
""" Set layer state of `layers` to on/off.
Args:
layers: set of layer names
state: `True` turn this `layers` on and others off,
`False` turn this `layers` off and others on
"""
layers = {layer_key(name) for name in layers}
for name, layer in self.layers.items():
if name in layers:
layer.is_visible = state
else:
layer.is_visible = not state
def set_current_layout(self, layout: 'Layout'):
self.current_layout.set_layout(layout, units=self.units)
@property
def inside_block_reference(self) -> bool:
""" Returns ``True`` if current processing state is inside of a block
reference (INSERT).
"""
return bool(self.current_block_reference)
def push_state(self, block_reference: Properties) -> None:
self._saved_states.append(self.current_block_reference)
self.current_block_reference = block_reference
def pop_state(self) -> None:
self.current_block_reference = self._saved_states.pop()
def resolve_all(self, entity: 'DXFGraphic') -> Properties:
""" Resolve all properties of `entity`. """
p = Properties()
p.layer = self.resolve_layer(entity)
resolved_layer = layer_key(p.layer)
p.units = self.resolve_units()
p.color = self.resolve_color(entity, resolved_layer=resolved_layer)
p.linetype_name, p.linetype_pattern = \
self.resolve_linetype(entity, resolved_layer=resolved_layer)
p.lineweight = self.resolve_lineweight(entity,
resolved_layer=resolved_layer)
p.linetype_scale = self.resolve_linetype_scale(entity)
p.is_visible = self.resolve_visible(entity,
resolved_layer=resolved_layer)
if entity.dxf.hasattr('style'):
p.font = self.resolve_font(entity)
if entity.dxftype() == 'HATCH':
p.filling = self.resolve_filling(entity)
return p
def resolve_units(self) -> int:
return self.current_layout.units
def resolve_linetype_scale(self, entity: 'DXFGraphic') -> float:
return entity.dxf.ltscale * self.linetype_scale
def resolve_visible(self, entity: 'DXFGraphic', *,
resolved_layer: Optional[str] = None) -> bool:
""" Resolve the visibility state of `entity`.
Returns ``True`` if `entity` is visible.
"""
entity_layer = resolved_layer or layer_key(self.resolve_layer(entity))
layer_properties = self.layers.get(entity_layer)
if layer_properties and not layer_properties.is_visible:
return False
elif entity.dxftype() == 'ATTRIB':
return (not bool(entity.dxf.invisible) and
not cast(Attrib, entity).is_invisible)
else:
return not bool(entity.dxf.invisible)
def resolve_layer(self, entity: 'DXFGraphic') -> str:
""" Resolve the layer of `entity`, this is only relevant for entities
inside of block references.
"""
layer = entity.dxf.layer
if layer == '0' and self.inside_block_reference:
layer = self.current_block_reference.layer
return layer
def resolve_color(self, entity: 'DXFGraphic', *,
resolved_layer: Optional[str] = None) -> Color:
""" Resolve the rgb-color of `entity` as hex color string:
"#RRGGBB" or "#RRGGBBAA".
"""
aci = entity.dxf.color # defaults to BYLAYER
if aci == const.BYLAYER:
entity_layer = resolved_layer or layer_key(
self.resolve_layer(entity))
layer = self.layers.get(
entity_layer, DEFAULT_LAYER_PROPERTIES)
color = layer.get_entity_color_from_layer(
self.current_layout.default_color)
elif aci == const.BYBLOCK:
if not self.inside_block_reference:
color = self.current_layout.default_color
else:
color = self.current_block_reference.color
else: # BYOBJECT
color = self._true_entity_color(entity.rgb, aci)
alpha = int(round((1.0 - entity.transparency) * 255))
if alpha == 255:
return color
else:
return set_color_alpha(color, alpha)
def _true_entity_color(self,
true_color: Optional[Tuple[int, int, int]],
aci: int) -> Color:
""" Returns rgb color in hex format: "#RRGGBB".
`true_color` has higher priority than `aci`.
"""
if true_color is not None:
return rgb_to_hex(true_color)
elif 0 < aci < 256:
return self._aci_to_true_color(aci)
else:
return self.current_layout.default_color # unknown / invalid
def _aci_to_true_color(self, aci: int) -> Color:
""" Returns the `aci` value (AutoCAD Color Index) as rgb value in
hex format: "#RRGGBB".
"""
if aci == 7: # black/white; todo: this bypasses the plot style table
if self.current_layout.has_dark_background:
return '#ffffff'
else:
return '#000000'
else:
return rgb_to_hex(self.plot_styles[aci].color)
def resolve_linetype(self, entity: 'DXFGraphic', *,
resolved_layer: str = None
) -> Tuple[str, Tuple[float, ...]]:
""" Resolve the linetype of `entity`. Returns a tuple of the linetype
name as upper-case string and the simplified linetype pattern as tuple
of floats.
"""
aci = entity.dxf.color
# Not sure if plotstyle table overrides actual entity setting?
if (0 < aci < 256) and \
self.plot_styles[aci].linetype != acadctb.OBJECT_LINETYPE:
# todo: return special line types - overriding linetypes by
# plotstyle table
pass
name = entity.dxf.linetype.upper() # default is 'BYLAYER'
if name == 'BYLAYER':
entity_layer = resolved_layer or layer_key(
self.resolve_layer(entity))
layer = self.layers.get(entity_layer, DEFAULT_LAYER_PROPERTIES)
name = layer.linetype_name
pattern = layer.linetype_pattern
elif name == 'BYBLOCK':
if self.inside_block_reference:
name = self.current_block_reference.linetype_name
pattern = self.current_block_reference.linetype_pattern
else:
# There is no default layout linetype
name = 'STANDARD'
pattern = CONTINUOUS_PATTERN
else:
pattern = self.line_pattern.get(name, CONTINUOUS_PATTERN)
return name, pattern
def resolve_lineweight(self, entity: 'DXFGraphic', *,
resolved_layer: str = None) -> float:
""" Resolve the lineweight of `entity` in mm.
DXF stores the lineweight in mm times 100 (e.g. 0.13mm = 13).
The smallest line weight is 0 and the biggest line weight is 211.
The DXF/DWG format is limited to a fixed value table,
see: :attr:`ezdxf.lldxf.const.VALID_DXF_LINEWEIGHTS`
CAD applications draw lineweight 0mm as an undefined small value, to
prevent backends to draw nothing for lineweight 0mm the smallest
return value is 0.01mm.
"""
def lineweight():
aci = entity.dxf.color
# Not sure if plotstyle table overrides actual entity setting?
if (0 < aci < 256) and self.plot_styles[
aci].lineweight != acadctb.OBJECT_LINEWEIGHT:
# overriding lineweight by plotstyle table
return self.plot_styles.get_lineweight(aci)
lineweight = entity.dxf.lineweight # default is BYLAYER
if lineweight == const.LINEWEIGHT_BYLAYER:
entity_layer = resolved_layer or layer_key(
self.resolve_layer(entity))
return self.layers.get(entity_layer,
DEFAULT_LAYER_PROPERTIES).lineweight
elif lineweight == const.LINEWEIGHT_BYBLOCK:
if self.inside_block_reference:
return self.current_block_reference.lineweight
else:
# There is no default layout lineweight
return self.default_lineweight()
elif lineweight == const.LINEWEIGHT_DEFAULT:
return self.default_lineweight()
else:
return float(lineweight) / 100.0
return max(0.01, lineweight())
def default_lineweight(self):
""" Returns the default lineweight of the document. """
# todo: is this value stored anywhere (e.g. HEADER section)?
return 0.25
def resolve_font(self, entity: 'DXFGraphic') -> Optional[fonts.Font]:
""" Resolve the text style of `entity` to a font name.
Returns ``None`` for the default font.
"""
if entity.dxf.hasattr('style'):
# todo: extended font data
return self.fonts.get(table_key(entity.dxf.style))
else:
return None
def resolve_filling(self, entity: 'DXFGraphic') -> Optional[Filling]:
""" Resolve filling properties (SOLID, GRADIENT, PATTERN) of `entity`.
"""
def setup_gradient():
filling.type = Filling.GRADIENT
filling.name = gradient.name.upper()
# todo: no idea when to use aci1 and aci2
filling.color1 = rgb_to_hex(gradient.color1)
if gradient.one_color:
c = round(gradient.tint * 255) # channel value
filling.color2 = rgb_to_hex((c, c, c))
else:
filling.color2 = rgb_to_hex(gradient.color2)
filling.angle = gradient.rotation
filling.gradient_centered = gradient.centered
def setup_pattern():
filling.type = Filling.PATTERN
filling.name = hatch.dxf.pattern_name.upper()
filling.pattern_scale = hatch.dxf.pattern_scale
filling.angle = hatch.dxf.pattern_angle
if hatch.dxf.pattern_double:
# This value is not editable by CAD-App-GUI:
filling.pattern_scale *= 2 # todo: is this correct?
filling.pattern = self._hatch_pattern_cache.get(filling.name)
if filling.pattern:
return
pattern = hatch.pattern
if not pattern:
return
# DXF stores the hatch pattern already rotated and scaled,
# pattern_scale and pattern_rotation are just hints for the CAD
# application to modify the pattern if required.
# It's better to revert the scaling and rotation, because in general
# back-ends do not handle pattern that way, they need a base-pattern
# and separated scaling and rotation attributes and these
# base-pattern could be cached by their name.
#
# There is no advantage of simplifying the hatch line pattern and
# this format is required by the PatternAnalyser():
filling.pattern = scale_pattern(
pattern.as_list(),
1.0 / filling.pattern_scale,
-filling.angle
)
self._hatch_pattern_cache[filling.name] = filling.pattern
if entity.dxftype() != 'HATCH':
return None
hatch = cast('Hatch', entity)
filling = Filling()
if hatch.dxf.solid_fill:
gradient = hatch.gradient
if gradient is None:
filling.type = Filling.SOLID
else:
if gradient.kind == 0: # Solid
filling.type = Filling.SOLID
filling.color1 = rgb_to_hex(gradient.color1)
else:
setup_gradient()
else:
setup_pattern()
return filling
COLOR_PATTERN = re.compile('#[0-9A-Fa-f]{6,8}')
def is_valid_color(color: Color) -> bool:
if type(color) is not Color:
raise TypeError(f'Invalid argument type: {type(color)}.')
if len(color) in (7, 9):
return bool(COLOR_PATTERN.fullmatch(color))
return False
def rgb_to_hex(
rgb: Union[Tuple[int, int, int], Tuple[float, float, float]]) -> Color:
""" Returns color in hex format: "#RRGGBB". """
assert all(0 <= x <= 255 for x in rgb), f'invalid RGB color: {rgb}'
r, g, b = rgb
return f'#{r:02x}{g:02x}{b:02x}'
def hex_to_rgb(hex_string: Color) -> RGB:
""" Returns hex string color as (r, g, b) tuple. """
hex_string = hex_string.lstrip('#')
assert len(hex_string) == 6
r = int(hex_string[0:2], 16)
g = int(hex_string[2:4], 16)
b = int(hex_string[4:6], 16)
return r, g, b
def set_color_alpha(color: Color, alpha: int) -> Color:
""" Returns `color` including the new `alpha` channel in hex format:
"#RRGGBBAA".
Args:
color: may be an RGB or RGBA hex color string
alpha: the new alpha value (0-255)
"""
assert color.startswith('#') and len(color) in (
7, 9), f'invalid RGB color: "{color}"'
assert 0 <= alpha < 256, f'alpha out of range: {alpha}'
return f'{color[:7]}{alpha:02x}'
def _load_line_pattern(linetypes: 'Table') -> Dict[str, Tuple]:
""" Load linetypes defined in a DXF document into as dictionary,
key is the upper case linetype name, value is the simplified line pattern,
see :func:`compile_line_pattern`.
"""
pattern = dict()
for linetype in linetypes: # type: Linetype
name = linetype.dxf.name.upper()
pattern[name] = linetype.pattern_tags.compile()
return pattern