Permalink
Cannot retrieve contributors at this time
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?
ezdxf/src/ezdxf/addons/drawing/properties.py
Go to fileThis commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
765 lines (662 sloc)
29.2 KB
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
# 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 |