1# Copyright (c) 2020, Matthew Broadway 2# License: MIT License 3from abc import ABC, abstractmethod 4from typing import Optional, Tuple, TYPE_CHECKING, Iterable, List, Dict 5 6from ezdxf.addons.drawing.properties import Properties 7from ezdxf.addons.drawing.type_hints import Color 8from ezdxf.entities import DXFGraphic 9from ezdxf.tools.text import replace_non_printable_characters 10from ezdxf.math import Vec3, Matrix44 11from ezdxf.path import Path 12 13if TYPE_CHECKING: 14 from ezdxf.tools.fonts import FontFace, FontMeasurements 15 16# Some params are also used by the Frontend() which has access to the backend 17# attributes: 18# show_defpoints: frontend filters defpoints if option is 0 19# show_hatch: frontend filters all HATCH entities if option is 0 20DEFAULT_PARAMS = { 21 # Updated by Frontend() class, if not set by user: 22 "pdsize": None, 23 # 0 5% of draw area height 24 # <0 Specifies a percentage of the viewport size 25 # >0 Specifies an absolute size 26 27 # See POINT docs: 28 "pdmode": None, 29 30 # Do not show defpoints by default. 31 # Filtering is handled by the Frontend(). 32 "show_defpoints": 0, 33 34 # linetype render: 35 # "internal" or "ezdxf" 36 "linetype_renderer": "internal", 37 38 # overall linetype scaling: None as default is important! 39 # 0.0 = disable line types at all, only supported by PyQt backend yet! 40 "linetype_scaling": 1.0, 41 42 # lineweight_scaling: 0.0 to disable lineweights at all - the current 43 # result is correct, in SVG the line width is 0.7 points for 0.25mm as 44 # required, but it often looks too thick 45 "lineweight_scaling": 1.0, 46 "min_lineweight": 0.24, # 1/300 inch 47 "min_dash_length": 0.1, # just guessing 48 "max_flattening_distance": 0.01, # just guessing 49 50 # 0 = disable HATCH entities 51 # 1 = show HATCH entities 52 # Filtering is handled by the Frontend(). 53 "show_hatch": 1, 54 55 # 0 = disable hatch pattern 56 # 1 = use predefined matplotlib pattern by pattern-name matching 57 # 2 = draw as solid fillings 58 "hatch_pattern": 1, 59} 60 61 62class Backend(ABC): 63 def __init__(self, params: Dict = None): 64 params_ = dict(DEFAULT_PARAMS) 65 if params: 66 err = set(params.keys()) - set(DEFAULT_PARAMS.keys()) 67 if err: 68 raise ValueError(f'Invalid parameter(s): {str(err)}') 69 params_.update(params) 70 self.entity_stack: List[Tuple[DXFGraphic, Properties]] = [] 71 self.pdsize = params_['pdsize'] 72 self.pdmode = params_['pdmode'] 73 self.show_defpoints = params_['show_defpoints'] 74 self.show_hatch = params_['show_hatch'] 75 self.hatch_pattern = params_['hatch_pattern'] 76 self.linetype_renderer = params_['linetype_renderer'].lower() 77 self.linetype_scaling = params_['linetype_scaling'] 78 self.lineweight_scaling = params_['lineweight_scaling'] 79 self.min_lineweight = params_['min_lineweight'] 80 self.min_dash_length = params_['min_dash_length'] 81 82 # Real document measurement value will be updated by the Frontend(): 83 # 0=Imperial (in, ft, yd, ...); 1=ISO meters 84 self.measurement = 0 85 86 # Deprecated: instead use Path.flattening() for approximation 87 self.bezier_approximation_count: int = 32 88 89 # Max flattening distance in drawing units: the backend implementation 90 # should calculate an appropriate value, like 1 screen- or paper pixel 91 # on the output medium, but converted into drawing units. 92 # Set Path() approximation accuracy: 93 self.max_flattening_distance = params_['max_flattening_distance'] 94 95 def enter_entity(self, entity: DXFGraphic, properties: Properties) -> None: 96 self.entity_stack.append((entity, properties)) 97 98 def exit_entity(self, entity: DXFGraphic) -> None: 99 e, p = self.entity_stack.pop() 100 assert e is entity, 'entity stack mismatch' 101 102 @property 103 def current_entity(self) -> Optional[DXFGraphic]: 104 """ Obtain the current entity being drawn """ 105 return self.entity_stack[-1][0] if self.entity_stack else None 106 107 @abstractmethod 108 def set_background(self, color: Color) -> None: 109 raise NotImplementedError 110 111 @abstractmethod 112 def draw_point(self, pos: Vec3, properties: Properties) -> None: 113 """ Draw a real dimensionless point, because not all backends support 114 zero-length lines! 115 """ 116 raise NotImplementedError 117 118 @abstractmethod 119 def draw_line(self, start: Vec3, end: Vec3, 120 properties: Properties) -> None: 121 raise NotImplementedError 122 123 def draw_path(self, path: Path, properties: Properties) -> None: 124 """ Draw an outline path (connected string of line segments and Bezier 125 curves). 126 127 The :meth:`draw_path` implementation is a fall-back implementation 128 which approximates Bezier curves by flattening as line segments. 129 Backends can override this method if better path drawing functionality 130 is available for that backend. 131 132 """ 133 if len(path): 134 vertices = iter( 135 path.flattening(distance=self.max_flattening_distance) 136 ) 137 prev = next(vertices) 138 for vertex in vertices: 139 self.draw_line(prev, vertex, properties) 140 prev = vertex 141 142 def draw_filled_paths(self, paths: Iterable[Path], holes: Iterable[Path], 143 properties: Properties) -> None: 144 """ Draw multiple filled paths (connected string of line segments and 145 Bezier curves) with holes. 146 147 The strategy to draw multiple paths at once was chosen, because a HATCH 148 entity can contain multiple unconnected areas and the holes are not easy 149 to assign to an external path. 150 151 The idea is to put all filled areas into `paths` (counter-clockwise 152 winding) and all holes into `holes` (clockwise winding) and look what 153 the backend does with this information. 154 155 The HATCH fill strategies ("ignore", "outermost", "ignore") are resolved 156 by the frontend e.g. the holes sequence is empty for the "ignore" 157 strategy and for the "outermost" strategy, holes do not contain nested 158 holes. 159 160 The default implementation draws all paths as filled polygon without 161 holes by the :meth:`draw_filled_polygon` method. Backends can override 162 this method if filled polygon with hole support is available. 163 164 Args: 165 paths: sequence of exterior paths (counter-clockwise winding) 166 holes: sequence of holes (clockwise winding) 167 properties: HATCH properties 168 169 """ 170 for path in paths: 171 self.draw_filled_polygon( 172 path.flattening(distance=self.max_flattening_distance), 173 properties 174 ) 175 176 @abstractmethod 177 def draw_filled_polygon(self, points: Iterable[Vec3], 178 properties: Properties) -> None: 179 """ Fill a polygon whose outline is defined by the given points. 180 Used to draw entities with simple outlines where :meth:`draw_path` may 181 be an inefficient way to draw such a polygon. 182 """ 183 raise NotImplementedError 184 185 @abstractmethod 186 def draw_text(self, text: str, transform: Matrix44, properties: Properties, 187 cap_height: float) -> None: 188 """ Draw a single line of text with the anchor point at the baseline 189 left point. 190 """ 191 raise NotImplementedError 192 193 @abstractmethod 194 def get_font_measurements(self, cap_height: float, 195 font: 'FontFace' = None) -> 'FontMeasurements': 196 """ Note: backends might want to cache the results of these calls """ 197 raise NotImplementedError 198 199 @abstractmethod 200 def get_text_line_width(self, text: str, cap_height: float, 201 font: 'FontFace' = None) -> float: 202 """ Get the width of a single line of text. """ 203 # https://stackoverflow.com/questions/32555015/how-to-get-the-visual-length-of-a-text-string-in-python 204 # https://stackoverflow.com/questions/4190667/how-to-get-width-of-a-truetype-font-character-in-1200ths-of-an-inch-with-python 205 raise NotImplementedError 206 207 @abstractmethod 208 def clear(self) -> None: 209 """ Clear the canvas. Does not reset the internal state of the backend. 210 Make sure that the previous drawing is finished before clearing. 211 212 """ 213 raise NotImplementedError 214 215 def finalize(self) -> None: 216 pass 217 218 219def prepare_string_for_rendering(text: str, dxftype: str) -> str: 220 assert '\n' not in text, 'not a single line of text' 221 if dxftype in {'TEXT', 'ATTRIB', 'ATTDEF'}: 222 text = replace_non_printable_characters(text, replacement='?') 223 text = text.replace('\t', '?') 224 elif dxftype == 'MTEXT': 225 text = replace_non_printable_characters(text, replacement='▯') 226 text = text.replace('\t', ' ') 227 else: 228 raise TypeError(dxftype) 229 return text 230