1# Copyright (c) 2019-2021 Manfred Moitzi 2# License: MIT License 3import enum 4import math 5import logging 6from typing import ( 7 TYPE_CHECKING, Union, Tuple, List, Iterable, Optional, Callable, cast, 8) 9 10from ezdxf.lldxf import const, validator 11from ezdxf.lldxf.attributes import ( 12 DXFAttr, DXFAttributes, DefSubclass, XType, RETURN_DEFAULT, 13 group_code_mapping, 14) 15from ezdxf.lldxf.const import SUBCLASS_MARKER, DXF2000, DXF2018 16from ezdxf.lldxf.types import DXFTag, dxftag 17from ezdxf.lldxf.tags import ( 18 Tags, find_begin_and_end_of_encoded_xdata_tags, NotFoundException, 19) 20 21from ezdxf.math import Vec3, Matrix44, OCS, UCS, NULLVEC, Z_AXIS, X_AXIS 22from ezdxf.math.transformtools import transform_extrusion 23from ezdxf.colors import rgb2int 24from ezdxf.tools.text import ( 25 split_mtext_string, escape_dxf_line_endings, plain_mtext, 26) 27from . import factory 28from .dxfentity import base_class, SubclassProcessor 29from .dxfgfx import DXFGraphic, acdb_entity 30from .xdata import XData 31 32if TYPE_CHECKING: 33 from ezdxf.eztypes import ( 34 TagWriter, DXFNamespace, DXFEntity, Vertex, Auditor, Drawing, EntityDB, 35 ) 36 37__all__ = ['MText', 'MTextColumns', 'ColumnType'] 38 39logger = logging.getLogger('ezdxf') 40 41BG_FILL_MASK = 1 + 2 + 16 42 43acdb_mtext = DefSubclass('AcDbMText', { 44 'insert': DXFAttr(10, xtype=XType.point3d, default=NULLVEC), 45 46 # Nominal (initial) text height 47 'char_height': DXFAttr( 48 40, default=2.5, 49 validator=validator.is_greater_zero, 50 fixer=RETURN_DEFAULT, 51 ), 52 53 # Reference column width 54 'width': DXFAttr(41, optional=True), 55 56 # Found in BricsCAD export: 57 'defined_height': DXFAttr(46, dxfversion='AC1021'), 58 59 # Attachment point: enum const.MTextEntityAlignment 60 # 1 = Top left 61 # 2 = Top center 62 # 3 = Top right 63 # 4 = Middle left 64 # 5 = Middle center 65 # 6 = Middle right 66 # 7 = Bottom left 67 # 8 = Bottom center 68 # 9 = Bottom right 69 'attachment_point': DXFAttr( 70 71, default=1, 71 validator=validator.is_in_integer_range(1, 10), 72 fixer=RETURN_DEFAULT, 73 ), 74 75 # Flow direction: enum MTextFlowDirection 76 # 1 = Left to right 77 # 3 = Top to bottom 78 # 5 = By style (the flow direction is inherited from the associated 79 # text style) 80 'flow_direction': DXFAttr( 81 72, default=1, optional=True, 82 validator=validator.is_one_of({1, 3, 5}), 83 fixer=RETURN_DEFAULT, 84 ), 85 86 # Content text: 87 # group code 1: text 88 # group code 3: additional text (optional) 89 90 # Text style name: 91 'style': DXFAttr( 92 7, default='Standard', optional=True, 93 validator=validator.is_valid_table_name, # do not fix! 94 ), 95 'extrusion': DXFAttr( 96 210, xtype=XType.point3d, default=Z_AXIS, optional=True, 97 validator=validator.is_not_null_vector, 98 fixer=RETURN_DEFAULT, 99 ), 100 101 # x-axis direction vector (in WCS) 102 # If rotation and text_direction are present, text_direction wins 103 'text_direction': DXFAttr( 104 11, xtype=XType.point3d, default=X_AXIS, optional=True, 105 validator=validator.is_not_null_vector, 106 fixer=RETURN_DEFAULT, 107 ), 108 109 # Horizontal width of the characters that make up the mtext entity. 110 # This value will always be equal to or less than the value of *width*, 111 # (read-only, ignored if supplied) 112 'rect_width': DXFAttr(42, optional=True), 113 114 # Vertical height of the mtext entity (read-only, ignored if supplied) 115 'rect_height': DXFAttr(43, optional=True), 116 117 # Text rotation in degrees - Error in DXF reference, which claims radians 118 'rotation': DXFAttr(50, default=0, optional=True), 119 120 # Line spacing style (optional): enum const.MTextLineSpacing 121 # 1 = At least (taller characters will override) 122 # 2 = Exact (taller characters will not override) 123 'line_spacing_style': DXFAttr( 124 73, default=1, optional=True, 125 validator=validator.is_one_of({1, 2}), 126 fixer=RETURN_DEFAULT, 127 ), 128 129 # Line spacing factor (optional): Percentage of default (3-on-5) line 130 # spacing to be applied. Valid values range from 0.25 to 4.00 131 'line_spacing_factor': DXFAttr( 132 44, default=1, optional=True, 133 validator=validator.is_in_float_range(0.25, 4.00), 134 fixer=validator.fit_into_float_range(0.25, 4.00), 135 ), 136 137 # Determines how much border there is around the text. 138 # (45) + (90) + (63) all three required, if one of them is used 139 'box_fill_scale': DXFAttr(45, dxfversion='AC1021'), 140 141 # background fill type flags: enum const.MTextBackgroundColor 142 # 0 = off 143 # 1 = color -> (63) < (421) or (431) 144 # 2 = drawing window color 145 # 3 = use background color (1 & 2) 146 # 16 = text frame ODA specification 20.4.46 147 # 2021-05-14: text frame only is supported bg_fill = 16, 148 # but scaling is always 1.5 and tags 45 + 63 are not present 149 'bg_fill': DXFAttr( 150 90, dxfversion='AC1021', 151 validator=validator.is_valid_bitmask(BG_FILL_MASK), 152 fixer=validator.fix_bitmask(BG_FILL_MASK) 153 ), 154 155 # background fill color as ACI, required even true color is used 156 'bg_fill_color': DXFAttr( 157 63, dxfversion='AC1021', 158 validator=validator.is_valid_aci_color, 159 ), 160 161 # 420-429? : background fill color as true color value, (63) also required 162 # but ignored 163 'bg_fill_true_color': DXFAttr(421, dxfversion='AC1021'), 164 165 # 430-439? : background fill color as color name ???, (63) also required 166 # but ignored 167 'bg_fill_color_name': DXFAttr(431, dxfversion='AC1021'), 168 169 # background fill color transparency - not used by AutoCAD/BricsCAD 170 'bg_fill_transparency': DXFAttr(441, dxfversion='AC1021'), 171 172}) 173acdb_mtext_group_codes = group_code_mapping(acdb_mtext) 174 175 176# ----------------------------------------------------------------------- 177# For more information go to docs/source/dxfinternals/entities/mtext.rst 178# ----------------------------------------------------------------------- 179 180# MTEXT column support: 181# MTEXT columns have the same appearance and handling for all DXF versions 182# as a single MTEXT entity like in DXF R2018. 183 184 185class ColumnType(enum.IntEnum): 186 NONE = 0 187 STATIC = 1 188 DYNAMIC = 2 189 190 191class MTextColumns: 192 """The column count is not stored explicit in the columns definition for 193 DXF versions R2018+. 194 195 If column_type is DYNAMIC and auto_height is True the column 196 count is defined by the content. The exact calculation of the column count 197 requires an accurate rendering of the MTEXT content like AutoCAD does! 198 199 If the column count is not defined, ezdxf tries to calculate the column 200 count from total_width, width and gutter_width, if these attributes are set 201 properly. 202 203 """ 204 205 def __init__(self): 206 self.column_type: ColumnType = ColumnType.STATIC 207 # The embedded object in R2018 does not store the column count for 208 # column type DYNAMIC and auto_height is True! 209 self.count: int = 1 210 self.auto_height: bool = False 211 self.reversed_column_flow: bool = False 212 self.defined_height: float = 0.0 213 self.width: float = 0.0 214 self.gutter_width: float = 0.0 215 self.total_width: float = 0.0 216 self.total_height: float = 0.0 217 # Storage for handles of linked MTEXT entities at loading stage: 218 self.linked_handles: Optional[List[str]] = None 219 # Storage for linked MTEXT entities for DXF versions < R2018: 220 self.linked_columns: List['MText'] = [] 221 # R2018+: heights of all columns if auto_height is False 222 self.heights: List[float] = [] 223 224 def deep_copy(self) -> 'MTextColumns': 225 columns = self.shallow_copy() 226 columns.linked_columns = [mtext.copy() for mtext in self.linked_columns] 227 return columns 228 229 def shallow_copy(self) -> 'MTextColumns': 230 columns = MTextColumns() 231 columns.count = self.count 232 columns.column_type = self.column_type 233 columns.auto_height = self.auto_height 234 columns.reversed_column_flow = self.reversed_column_flow 235 columns.defined_height = self.defined_height 236 columns.width = self.width 237 columns.gutter_width = self.gutter_width 238 columns.total_width = self.total_width 239 columns.total_height = self.total_height 240 columns.linked_columns = list(self.linked_columns) 241 columns.heights = list(self.heights) 242 return columns 243 244 @classmethod 245 def new_static_columns(cls, count: int, width: float, gutter_width: float, 246 height: float) -> 'MTextColumns': 247 columns = cls() 248 columns.column_type = ColumnType.STATIC 249 columns.count = int(count) 250 columns.width = float(width) 251 columns.gutter_width = float(gutter_width) 252 columns.defined_height = float(height) 253 columns.update_total_width() 254 columns.update_total_height() 255 return columns 256 257 @classmethod 258 def new_dynamic_auto_height_columns( 259 cls, count: int, width: float, gutter_width: float, 260 height: float) -> 'MTextColumns': 261 columns = cls() 262 columns.column_type = ColumnType.DYNAMIC 263 columns.auto_height = True 264 columns.count = int(count) 265 columns.width = float(width) 266 columns.gutter_width = float(gutter_width) 267 columns.defined_height = float(height) 268 columns.update_total_width() 269 columns.update_total_height() 270 return columns 271 272 @classmethod 273 def new_dynamic_manual_height_columns( 274 cls, width: float, gutter_width: float, 275 heights: Iterable[float]) -> 'MTextColumns': 276 columns = cls() 277 columns.column_type = ColumnType.DYNAMIC 278 columns.auto_height = False 279 columns.width = float(width) 280 columns.gutter_width = float(gutter_width) 281 columns.defined_height = 0.0 282 columns.heights = list(heights) 283 columns.count = len(columns.heights) 284 columns.update_total_width() 285 columns.update_total_height() 286 return columns 287 288 def update_total_width(self): 289 count = self.count 290 if count > 0: 291 self.total_width = count * self.width + \ 292 (count - 1) * self.gutter_width 293 else: 294 self.total_width = 0.0 295 296 def update_total_height(self): 297 if self.has_dynamic_manual_height: 298 self.total_height = max(self.heights) 299 else: 300 self.total_height = self.defined_height 301 302 @property 303 def has_dynamic_auto_height(self) -> bool: 304 return self.column_type == ColumnType.DYNAMIC and self.auto_height 305 306 @property 307 def has_dynamic_manual_height(self) -> bool: 308 return self.column_type == ColumnType.DYNAMIC and not self.auto_height 309 310 def link_columns(self, doc: 'Drawing'): 311 # DXF R2018+ has no linked MTEXT entities. 312 if doc.dxfversion >= DXF2018 or not self.linked_handles: 313 return 314 db = doc.entitydb 315 assert db is not None, "entity database not initialized" 316 linked_columns = [] 317 for handle in self.linked_handles: 318 mtext = cast('MText', db.get(handle)) 319 if mtext: 320 linked_columns.append(mtext) 321 else: 322 logger.debug(f"Linked MTEXT column #{handle} does not exist.") 323 self.linked_handles = None 324 self.linked_columns = linked_columns 325 326 def transform(self, m: Matrix44, hscale: float = 1, vscale: float = 1): 327 self.width *= hscale 328 self.gutter_width *= hscale 329 self.total_width *= hscale 330 self.total_height *= vscale 331 self.defined_height *= vscale 332 self.heights = [h * vscale for h in self.heights] 333 for mtext in self.linked_columns: 334 mtext.transform(m) 335 336 def acad_mtext_column_info_xdata(self) -> Tags: 337 tags = Tags([ 338 DXFTag(1000, "ACAD_MTEXT_COLUMN_INFO_BEGIN"), 339 DXFTag(1070, 75), DXFTag(1070, int(self.column_type)), 340 DXFTag(1070, 79), DXFTag(1070, int(self.auto_height)), 341 DXFTag(1070, 76), DXFTag(1070, self.count), 342 DXFTag(1070, 78), DXFTag(1070, int(self.reversed_column_flow)), 343 DXFTag(1070, 48), DXFTag(1040, self.width), 344 DXFTag(1070, 49), DXFTag(1040, self.gutter_width), 345 ]) 346 if self.has_dynamic_manual_height: 347 tags.extend([DXFTag(1070, 50), DXFTag(1070, len(self.heights))]) 348 tags.extend(DXFTag(1040, height) for height in self.heights) 349 tags.append(DXFTag(1000, "ACAD_MTEXT_COLUMN_INFO_END")) 350 return tags 351 352 def acad_mtext_columns_xdata(self) -> Tags: 353 tags = Tags([ 354 DXFTag(1000, "ACAD_MTEXT_COLUMNS_BEGIN"), 355 DXFTag(1070, 47), DXFTag(1070, self.count), # incl. main MTEXT 356 ]) 357 tags.extend( # writes only (count - 1) handles! 358 DXFTag(1005, handle) for handle in self.mtext_handles()) 359 tags.append(DXFTag(1000, "ACAD_MTEXT_COLUMNS_END")) 360 return tags 361 362 def mtext_handles(self) -> List[str]: 363 """ Returns a list of all linked MTEXT handles. """ 364 if self.linked_handles: 365 return self.linked_handles 366 handles = [] 367 for column in self.linked_columns: 368 if column.is_alive: 369 handle = column.dxf.handle 370 if handle is None: 371 raise const.DXFStructureError( 372 "Linked MTEXT column has no handle.") 373 handles.append(handle) 374 else: 375 raise const.DXFStructureError("Linked MTEXT column deleted!") 376 return handles 377 378 def acad_mtext_defined_height_xdata(self) -> Tags: 379 return Tags([ 380 DXFTag(1000, "ACAD_MTEXT_DEFINED_HEIGHT_BEGIN"), 381 DXFTag(1070, 46), DXFTag(1040, self.defined_height), 382 DXFTag(1000, "ACAD_MTEXT_DEFINED_HEIGHT_END"), 383 ]) 384 385 386def load_columns_from_embedded_object( 387 dxf: 'DXFNamespace', embedded_obj: Tags) -> MTextColumns: 388 columns = MTextColumns() 389 insert = dxf.get('insert') # mandatory attribute, but what if ... 390 text_direction = dxf.get('text_direction') # optional attribute 391 reference_column_width = dxf.get('width') # optional attribute 392 for code, value in embedded_obj: 393 # Update duplicated attributes if MTEXT attributes are not set: 394 if code == 10 and text_direction is None: 395 dxf.text_direction = Vec3(value) 396 # rotation is not needed anymore: 397 dxf.discard('rotation') 398 elif code == 11 and insert is None: 399 dxf.insert = Vec3(value) 400 elif code == 40 and reference_column_width is None: 401 dxf.width = value 402 elif code == 41: 403 # Column height if auto height is True. 404 columns.defined_height = value 405 # Keep in sync with DXF attribute: 406 dxf.defined_height = value 407 elif code == 42: 408 columns.total_width = value 409 elif code == 43: 410 columns.total_height = value 411 elif code == 44: 412 # All columns have the same width. 413 columns.width = value 414 elif code == 45: 415 # All columns have the same gutter width = space between columns. 416 columns.gutter_width = value 417 elif code == 71: 418 columns.column_type = ColumnType(value) 419 elif code == 72: # column height count 420 # The column height count can be 0 in some cases (dynamic & auto 421 # height) in DXF version R2018+. 422 columns.count = value 423 elif code == 73: 424 columns.auto_height = bool(value) 425 elif code == 74: 426 columns.reversed_column_flow = bool(value) 427 elif code == 46: # column heights 428 # The last column height is 0; takes the rest? 429 columns.heights.append(value) 430 431 # The column count is not defined explicit: 432 if columns.count == 0: 433 if columns.heights: # very unlikely 434 columns.count = len(columns.heights) 435 elif columns.total_width > 0: 436 # calculate column count from total_width 437 g = columns.gutter_width 438 wg = abs(columns.width + g) 439 if wg > 1e-6: 440 columns.count = int(round((columns.total_width + g) / wg)) 441 return columns 442 443 444def load_mtext_column_info(tags: Tags) -> Optional[MTextColumns]: 445 try: # has column info? 446 start, end = find_begin_and_end_of_encoded_xdata_tags( 447 "ACAD_MTEXT_COLUMN_INFO", tags) 448 except NotFoundException: 449 return None 450 columns = MTextColumns() 451 height_count = 0 452 group_code = None 453 for code, value in tags[start + 1: end]: 454 if height_count: 455 if code == 1040: 456 columns.heights.append(value) 457 height_count -= 1 458 continue 459 else: # error 460 logger.error("missing column heights in MTEXT entity") 461 height_count = 0 462 463 if group_code is None: 464 group_code = value 465 continue 466 467 if group_code == 75: 468 columns.column_type = ColumnType(value) 469 elif group_code == 79: 470 columns.auto_height = bool(value) 471 elif group_code == 76: 472 columns.count = int(value) 473 elif group_code == 78: 474 columns.reversed_column_flow = bool(value) 475 elif group_code == 48: 476 columns.width = value 477 elif group_code == 49: 478 columns.gutter_width = value 479 elif group_code == 50: 480 height_count = int(value) 481 group_code = None 482 return columns 483 484 485def load_mtext_linked_column_handles(tags: Tags) -> List[str]: 486 handles = [] 487 try: 488 start, end = find_begin_and_end_of_encoded_xdata_tags( 489 "ACAD_MTEXT_COLUMNS", tags) 490 except NotFoundException: 491 return handles 492 for code, value in tags[start:end]: 493 if code == 1005: 494 handles.append(value) 495 return handles 496 497 498def load_mtext_defined_height(tags: Tags) -> float: 499 # The defined height stored in the linked MTEXT entities, is not required: 500 # 501 # If all columns have the same height (static & dynamic auto height), the 502 # "defined_height" is stored in the main MTEXT, but the linked MTEXT entities 503 # also have a "ACAD_MTEXT_DEFINED_HEIGHT" group in the ACAD section of XDATA. 504 # 505 # If the columns have different heights (dynamic manual height), these 506 # height values are only stored in the main MTEXT. The linked MTEXT 507 # entities do not have an ACAD section at all. 508 509 height = 0.0 510 try: 511 start, end = find_begin_and_end_of_encoded_xdata_tags( 512 "ACAD_MTEXT_DEFINED_HEIGHT", tags) 513 except NotFoundException: 514 return height 515 516 for code, value in tags[start:end]: 517 if code == 1040: 518 height = value 519 return height 520 521 522def load_columns_from_xdata(dxf: 'DXFNamespace', 523 xdata: XData) -> Optional[MTextColumns]: 524 # The ACAD section in XDATA of the main MTEXT entity stores all column 525 # related information: 526 if "ACAD" in xdata: 527 acad = xdata.get("ACAD") 528 else: 529 return None 530 531 name = f"MTEXT(#{dxf.get('handle')})" 532 try: 533 columns = load_mtext_column_info(acad) 534 except const.DXFStructureError: 535 logger.error(f"Invalid ACAD_MTEXT_COLUMN_INFO in {name}") 536 return None 537 538 if columns is None: # no columns defined 539 return None 540 541 try: 542 columns.linked_handles = load_mtext_linked_column_handles(acad) 543 except const.DXFStructureError: 544 logger.error(f"Invalid ACAD_MTEXT_COLUMNS in {name}") 545 546 columns.update_total_width() 547 if columns.heights: # dynamic columns, manual heights 548 # This is correct even if the last column is the tallest, which height 549 # is not known. The height of last column is always stored as 0. 550 columns.total_height = max(columns.heights) 551 else: # all columns have the same "defined" height 552 try: 553 columns.defined_height = load_mtext_defined_height(acad) 554 except const.DXFStructureError: 555 logger.error(f"Invalid ACAD_MTEXT_DEFINED_HEIGHT in {name}") 556 columns.total_height = columns.defined_height 557 558 return columns 559 560 561def extract_mtext_text_frame_handles(xdata: XData) -> List[str]: 562 # Stores information about the text frame until DXF R2018. 563 # Newer CAD applications do not need that information nor the separated 564 # LWPOLYLINE as text frame entity. 565 handles = [] 566 if "ACAD" in xdata: 567 acad = xdata.get("ACAD") 568 else: 569 return handles 570 571 try: 572 start, end = find_begin_and_end_of_encoded_xdata_tags( 573 "ACAD_MTEXT_TEXT_BORDERS", acad) 574 except NotFoundException: 575 return handles 576 577 for code, value in acad[start:end]: 578 # multiple handles to a LWPOLYLINE entity could be present: 579 if code == 1005: 580 handles.append(value) 581 582 # remove MTEXT_TEXT_BORDERS data 583 del acad[start:end] 584 if len(acad) < 2: 585 xdata.discard("ACAD") 586 return handles 587 588 589@factory.register_entity 590class MText(DXFGraphic): 591 """ DXF MTEXT entity """ 592 DXFTYPE = 'MTEXT' 593 DXFATTRIBS = DXFAttributes(base_class, acdb_entity, acdb_mtext) 594 MIN_DXF_VERSION_FOR_EXPORT = DXF2000 595 596 def __init__(self): 597 super().__init__() 598 self.text: str = "" 599 # Linked MText columns do not have a MTextColumns() object! 600 self._columns: Optional[MTextColumns] = None 601 602 @property 603 def columns(self) -> Optional[MTextColumns]: 604 """ Returns a copy of the column configuration. """ 605 # The column configuration is deliberately not editable. 606 # Can't prevent access to _columns, but you are on your own if do this! 607 return self._columns.shallow_copy() if self._columns else None 608 609 @property 610 def has_columns(self) -> bool: 611 return self._columns is not None 612 613 def _copy_data(self, entity: 'MText') -> None: 614 entity.text = self.text 615 if self.has_columns: 616 # copies also the linked MTEXT column entities! 617 entity._columns = self._columns.deep_copy() 618 619 def load_dxf_attribs( 620 self, processor: SubclassProcessor = None) -> 'DXFNamespace': 621 dxf = super().load_dxf_attribs(processor) 622 if processor: 623 tags = Tags(self.load_mtext_content(processor.subclass_by_index(2))) 624 processor.fast_load_dxfattribs( 625 dxf, acdb_mtext_group_codes, subclass=tags, recover=True) 626 if processor.embedded_objects: 627 obj = processor.embedded_objects[0] 628 self._columns = load_columns_from_embedded_object(dxf, obj) 629 elif self.xdata: 630 self._columns = load_columns_from_xdata(dxf, self.xdata) 631 self.embedded_objects = None # todo: remove 632 return dxf 633 634 def post_load_hook(self, doc: 'Drawing') -> Optional[Callable]: 635 def destroy_text_frame_entity(): 636 entitydb = doc.entitydb 637 if entitydb: 638 for handle in extract_mtext_text_frame_handles(self.xdata): 639 text_frame = entitydb.get(handle) 640 if text_frame: 641 text_frame.destroy() 642 643 def unlink_mtext_columns_from_layout(): 644 """ Unlinked MTEXT entities from layout entity space. """ 645 layout = self.get_layout() 646 if layout is not None: 647 for mtext in self._columns.linked_columns: 648 layout.unlink_entity(mtext) 649 else: 650 for mtext in self._columns.linked_columns: 651 mtext.dxf.owner = None 652 653 super().post_load_hook(doc) 654 if self.xdata: 655 destroy_text_frame_entity() 656 657 if self.has_columns: 658 # Link columns, one MTEXT entity for each column, to the main MTEXT 659 # entity (DXF version <R2018). 660 self._columns.link_columns(doc) 661 return unlink_mtext_columns_from_layout 662 return None 663 664 def preprocess_export(self, tagwriter: 'TagWriter') -> bool: 665 """ Pre requirement check and pre processing for export. 666 667 Returns False if MTEXT should not be exported at all. 668 669 (internal API) 670 """ 671 columns = self._columns 672 if columns and tagwriter.dxfversion < const.DXF2018: 673 if columns.count != len(columns.linked_columns) + 1: 674 logger.debug( 675 f"{str(self)}: column count does not match linked columns") 676 return False 677 if not all(column.is_alive for column in columns.linked_columns): 678 logger.debug( 679 f"{str(self)}: contains destroyed linked columns") 680 return False 681 self.sync_common_attribs_of_linked_columns() 682 return True 683 684 def export_dxf(self, tagwriter: 'TagWriter') -> None: 685 super().export_dxf(tagwriter) 686 # Linked MTEXT entities are not stored in the layout entity space! 687 if self.has_columns and tagwriter.dxfversion < const.DXF2018: 688 self.export_linked_entities(tagwriter) 689 690 def export_entity(self, tagwriter: 'TagWriter') -> None: 691 """ Export entity specific data as DXF tags. """ 692 super().export_entity(tagwriter) 693 tagwriter.write_tag2(SUBCLASS_MARKER, acdb_mtext.name) 694 self.dxf.export_dxf_attribs(tagwriter, [ 695 'insert', 'char_height', 'width', 'defined_height', 696 'attachment_point', 'flow_direction', 697 ]) 698 self.export_mtext_content(tagwriter) 699 self.dxf.export_dxf_attribs(tagwriter, [ 700 'style', 'extrusion', 'text_direction', 'rect_width', 'rect_height', 701 'rotation', 'line_spacing_style', 'line_spacing_factor', 702 'box_fill_scale', 'bg_fill', 'bg_fill_color', 'bg_fill_true_color', 703 'bg_fill_color_name', 'bg_fill_transparency', 704 ]) 705 columns = self._columns 706 if columns is None or columns.column_type == ColumnType.NONE: 707 return 708 709 if tagwriter.dxfversion >= DXF2018: 710 self.export_embedded_object(tagwriter) 711 else: 712 self.set_column_xdata() 713 self.set_linked_columns_xdata() 714 715 def load_mtext_content(self, tags: Tags) -> Iterable['DXFTag']: 716 tail = "" 717 parts = [] 718 for tag in tags: 719 if tag.code == 1: 720 tail = tag.value 721 elif tag.code == 3: 722 parts.append(tag.value) 723 else: 724 yield tag 725 parts.append(tail) 726 self.text = escape_dxf_line_endings("".join(parts)) 727 728 def export_mtext_content(self, tagwriter: 'TagWriter') -> None: 729 txt = escape_dxf_line_endings(self.text) 730 str_chunks = split_mtext_string(txt, size=250) 731 if len(str_chunks) == 0: 732 str_chunks.append("") 733 while len(str_chunks) > 1: 734 tagwriter.write_tag2(3, str_chunks.pop(0)) 735 tagwriter.write_tag2(1, str_chunks[0]) 736 737 def export_embedded_object(self, tagwriter: 'TagWriter'): 738 dxf = self.dxf 739 cols = self._columns 740 741 tagwriter.write_tag2(101, 'Embedded Object') 742 tagwriter.write_tag2(70, 1) # unknown meaning 743 tagwriter.write_tag(dxftag(10, dxf.text_direction)) 744 tagwriter.write_tag(dxftag(11, dxf.insert)) 745 tagwriter.write_tag2(40, dxf.width) # repeated reference column width 746 tagwriter.write_tag2(41, cols.defined_height) 747 tagwriter.write_tag2(42, cols.total_width) 748 tagwriter.write_tag2(43, cols.total_height) 749 tagwriter.write_tag2(71, int(cols.column_type)) 750 751 if cols.has_dynamic_auto_height: 752 count = 0 753 else: 754 count = cols.count 755 tagwriter.write_tag2(72, count) 756 757 tagwriter.write_tag2(44, cols.width) 758 tagwriter.write_tag2(45, cols.gutter_width) 759 tagwriter.write_tag2(73, int(cols.auto_height)) 760 tagwriter.write_tag2(74, int(cols.reversed_column_flow)) 761 for height in cols.heights: 762 tagwriter.write_tag2(46, height) 763 764 def export_linked_entities(self, tagwriter: 'TagWriter'): 765 for mtext in self._columns.linked_columns: 766 if mtext.dxf.handle is None: 767 raise const.DXFStructureError( 768 "Linked MTEXT column has no handle.") 769 # Export linked columns as separated DXF entities: 770 mtext.export_dxf(tagwriter) 771 772 def sync_common_attribs_of_linked_columns(self): 773 common_attribs = self.dxfattribs(drop={ 774 'handle', 'insert', 'rect_width', 'rect_height'}) 775 for mtext in self._columns.linked_columns: 776 mtext.update_dxf_attribs(common_attribs) 777 778 def set_column_xdata(self): 779 if self.xdata is None: 780 self.xdata = XData() 781 cols = self._columns 782 acad = cols.acad_mtext_column_info_xdata() 783 acad.extend(cols.acad_mtext_columns_xdata()) 784 if not cols.has_dynamic_manual_height: 785 acad.extend(cols.acad_mtext_defined_height_xdata()) 786 787 xdata = self.xdata 788 # Replace existing column data and therefore also removes 789 # ACAD_MTEXT_TEXT_BORDERS information! 790 xdata.discard('ACAD') 791 xdata.add('ACAD', acad) 792 793 def set_linked_columns_xdata(self): 794 cols = self._columns 795 for column in cols.linked_columns: 796 column.discard_xdata('ACAD') 797 if not cols.has_dynamic_manual_height: 798 tags = cols.acad_mtext_defined_height_xdata() 799 for column in cols.linked_columns: 800 column.set_xdata('ACAD', tags) 801 802 def get_rotation(self) -> float: 803 """ Get text rotation in degrees, independent if it is defined by 804 :attr:`dxf.rotation` or :attr:`dxf.text_direction`. 805 806 """ 807 if self.dxf.hasattr('text_direction'): 808 vector = self.dxf.text_direction 809 radians = math.atan2(vector[1], vector[0]) # ignores z-axis 810 rotation = math.degrees(radians) 811 else: 812 rotation = self.dxf.get('rotation', 0) 813 return rotation 814 815 def set_rotation(self, angle: float) -> 'MText': 816 """ Set attribute :attr:`rotation` to `angle` (in degrees) and deletes 817 :attr:`dxf.text_direction` if present. 818 819 """ 820 # text_direction has higher priority than rotation, therefore delete it 821 self.dxf.discard('text_direction') 822 self.dxf.rotation = angle 823 return self # fluent interface 824 825 def set_location(self, insert: 'Vertex', rotation: float = None, 826 attachment_point: int = None) -> 'MText': 827 """ Set attributes :attr:`dxf.insert`, :attr:`dxf.rotation` and 828 :attr:`dxf.attachment_point`, ``None`` for :attr:`dxf.rotation` or 829 :attr:`dxf.attachment_point` preserves the existing value. 830 831 """ 832 self.dxf.insert = Vec3(insert) 833 if rotation is not None: 834 self.set_rotation(rotation) 835 if attachment_point is not None: 836 self.dxf.attachment_point = attachment_point 837 return self # fluent interface 838 839 def set_bg_color(self, color: Union[int, str, Tuple[int, int, int], None], 840 scale: float = 1.5, text_frame=False): 841 """ Set background color as :ref:`ACI` value or as name string or as RGB 842 tuple ``(r, g, b)``. 843 844 Use special color name ``canvas``, to set background color to canvas 845 background color. 846 847 Use `color` = ``None`` to remove the background filling. 848 849 Setting only a text border is supported (`color`=``None``), but in this 850 case the scaling is always 1.5. 851 852 Args: 853 color: color as :ref:`ACI`, string, RGB tuple or ``None`` 854 scale: determines how much border there is around the text, the 855 value is based on the text height, and should be in the range 856 of [1, 5], where 1 fits exact the MText entity. 857 text_frame: draw a text frame in text color if ``True`` 858 859 """ 860 if 1 <= scale <= 5: 861 self.dxf.box_fill_scale = scale 862 else: 863 raise ValueError('argument scale has to be in range from 1 to 5.') 864 865 text_frame = const.MTEXT_TEXT_FRAME if text_frame else 0 866 if color is None: 867 self.dxf.discard('bg_fill') 868 self.dxf.discard('box_fill_scale') 869 self.dxf.discard('bg_fill_color') 870 self.dxf.discard('bg_fill_true_color') 871 self.dxf.discard('bg_fill_color_name') 872 if text_frame: 873 # special case, text frame only with scaling factor = 1.5 874 self.dxf.bg_fill = 16 875 elif color == 'canvas': # special case for use background color 876 self.dxf.bg_fill = const.MTEXT_BG_CANVAS_COLOR | text_frame 877 self.dxf.bg_fill_color = 0 # required but ignored 878 else: 879 self.dxf.bg_fill = const.MTEXT_BG_COLOR | text_frame 880 if isinstance(color, int): 881 self.dxf.bg_fill_color = color 882 elif isinstance(color, str): 883 self.dxf.bg_fill_color = 0 # required but ignored 884 self.dxf.bg_fill_color_name = color 885 elif isinstance(color, tuple): 886 self.dxf.bg_fill_color = 0 # required but ignored 887 self.dxf.bg_fill_true_color = rgb2int(color) 888 return self # fluent interface 889 890 def __iadd__(self, text: str) -> 'MText': 891 """ Append `text` to existing content (:attr:`text` attribute). """ 892 self.text += text 893 return self 894 895 append = __iadd__ 896 897 def get_text_direction(self) -> Vec3: 898 """ Returns the horizontal text direction as :class:`~ezdxf.math.Vec3` 899 object, even if only the text rotation is defined. 900 901 """ 902 dxf = self.dxf 903 # "text_direction" has higher priority than "rotation" 904 if dxf.hasattr("text_direction"): 905 return dxf.text_direction 906 if dxf.hasattr("rotation"): 907 # MTEXT is not an OCS entity, but I don't know how else to convert 908 # a rotation angle for an entity just defined by an extrusion vector. 909 # It's correct for the most common case: extrusion=(0, 0, 1) 910 return OCS(dxf.extrusion).to_wcs(Vec3.from_deg_angle(dxf.rotation)) 911 return X_AXIS 912 913 def convert_rotation_to_text_direction(self): 914 """ Convert text rotation into text direction and discard text rotation. 915 """ 916 dxf = self.dxf 917 if dxf.hasattr('rotation'): 918 if not dxf.hasattr('text_direction'): 919 dxf.text_direction = self.get_text_direction() 920 dxf.discard('rotation') 921 922 def ucs(self) -> UCS: 923 """ Returns the :class:`~ezdxf.math.UCS` of the :class:`MText` entity, 924 defined by the insert location (origin), the text direction or rotation 925 (x-axis) and the extrusion vector (z-axis). 926 927 """ 928 dxf = self.dxf 929 return UCS( 930 origin=dxf.insert, 931 ux=self.get_text_direction(), 932 uz=dxf.extrusion, 933 ) 934 935 def transform(self, m: Matrix44) -> 'MText': 936 """ Transform the MTEXT entity by transformation matrix `m` inplace. """ 937 dxf = self.dxf 938 old_extrusion = Vec3(dxf.extrusion) 939 new_extrusion, _ = transform_extrusion(old_extrusion, m) 940 self.convert_rotation_to_text_direction() 941 942 old_text_direction = Vec3(dxf.text_direction) 943 new_text_direction = m.transform_direction(old_text_direction) 944 945 old_vertical_direction = old_extrusion.cross(old_text_direction) 946 old_char_height_vec = old_vertical_direction.normalize(dxf.char_height) 947 new_char_height_vec = m.transform_direction(old_char_height_vec) 948 oblique = new_text_direction.angle_between(new_char_height_vec) 949 dxf.char_height = new_char_height_vec.magnitude * math.sin(oblique) 950 951 if dxf.hasattr('width'): 952 width_vec = old_text_direction.normalize(dxf.width) 953 dxf.width = m.transform_direction(width_vec).magnitude 954 955 dxf.insert = m.transform(dxf.insert) 956 dxf.text_direction = new_text_direction 957 dxf.extrusion = new_extrusion 958 if self.has_columns: 959 hscale = m.transform_direction( 960 old_text_direction.normalize()).magnitude 961 vscale = m.transform_direction( 962 old_vertical_direction.normalize()).magnitude 963 self._columns.transform(m, hscale, vscale) 964 return self 965 966 def plain_text(self, split=False) -> Union[List[str], str]: 967 """ Returns the text content without inline formatting codes. 968 969 Args: 970 split: split content text at line breaks if ``True`` and 971 returns a list of strings without line endings 972 973 """ 974 return plain_mtext(self.text, split=split) 975 976 def all_columns_plain_text(self, split=False) -> Union[List[str], str]: 977 """ Returns the text content of all columns without inline formatting 978 codes. 979 980 Args: 981 split: split content text at line breaks if ``True`` and 982 returns a list of strings without line endings 983 984 .. versionadded: 0.17 985 986 """ 987 988 def merged_content(): 989 content = [plain_mtext(self.text, split=False)] 990 if self.has_columns: 991 for c in self._columns.linked_columns: 992 content.append(c.plain_text(split=False)) 993 return "".join(content) 994 995 def split_content(): 996 content = plain_mtext(self.text, split=True) 997 if self.has_columns: 998 if content and content[-1] == "": 999 content.pop() 1000 for c in self._columns.linked_columns: 1001 content.extend(c.plain_text(split=True)) 1002 if content and content[-1] == "": 1003 content.pop() 1004 return content 1005 1006 if split: 1007 return split_content() 1008 else: 1009 return merged_content() 1010 1011 def all_columns_raw_content(self) -> str: 1012 """ Returns the text content of all columns as a single string 1013 including the inline formatting codes. 1014 1015 .. versionadded: 0.17 1016 1017 """ 1018 content = [self.text] 1019 if self.has_columns: 1020 for column in self._columns.linked_columns: 1021 content.append(column.text) 1022 return "".join(content) 1023 1024 def audit(self, auditor: 'Auditor'): 1025 """ Validity check. """ 1026 if not self.is_alive: 1027 return 1028 if self.dxf.owner is not None: 1029 # Kills linked columns, because owner (None) does not exist! 1030 super().audit(auditor) 1031 else: # linked columns: owner is None 1032 # TODO: special audit for linked columns 1033 pass 1034 auditor.check_text_style(self) 1035 # TODO: audit column structure 1036 1037 def destroy(self) -> None: 1038 if not self.is_alive: 1039 return 1040 1041 if self.has_columns: 1042 for column in self._columns.linked_columns: 1043 column.destroy() 1044 1045 del self._columns 1046 super().destroy() 1047 1048 # Linked MTEXT columns are not the same structure as 1049 # POLYLINE & INSERT with sub-entities and SEQEND :( 1050 def add_sub_entities_to_entitydb(self, db: 'EntityDB') -> None: 1051 """ Add linked columns (MTEXT) entities to entity database `db`, 1052 called from EntityDB. (internal API) 1053 1054 """ 1055 if self.is_alive and self._columns: 1056 doc = self.doc 1057 for column in self._columns.linked_columns: 1058 if column.is_alive and column.is_virtual: 1059 column.doc = doc 1060 db.add(column) 1061 1062 def process_sub_entities(self, func: Callable[['DXFEntity'], None]): 1063 """ Call `func` for linked columns. (internal API) 1064 """ 1065 if self.is_alive and self._columns: 1066 for entity in self._columns.linked_columns: 1067 if entity.is_alive: 1068 func(entity) 1069 1070 def setup_columns(self, columns: MTextColumns, 1071 linked: bool = False) -> None: 1072 assert columns.column_type != ColumnType.NONE 1073 assert columns.count > 0, "one or more columns required" 1074 assert columns.width > 0, "column width has to be > 0" 1075 assert columns.gutter_width >= 0, "gutter width has to be >= 0" 1076 1077 if self.has_columns: 1078 raise const.DXFStructureError('Column setup already exist.') 1079 self._columns = columns 1080 self.dxf.width = columns.width 1081 self.dxf.defined_height = columns.defined_height 1082 if columns.total_height < 1e-6: 1083 columns.total_height = columns.defined_height 1084 if columns.total_width < 1e-6: 1085 columns.update_total_width() 1086 if linked: 1087 self._create_linked_columns() 1088 1089 def _create_linked_columns(self) -> None: 1090 """ Create linked MTEXT columns for DXF versions before R2018. """ 1091 # creates virtual MTEXT entities 1092 dxf = self.dxf 1093 attribs = self.dxfattribs(drop={'handle', 'owner'}) 1094 doc = self.doc 1095 cols = self._columns 1096 1097 insert = dxf.get('insert', Vec3()) 1098 default_direction = Vec3.from_deg_angle(dxf.get('rotation', 0)) 1099 text_direction = Vec3(dxf.get('text_direction', default_direction)) 1100 offset = text_direction.normalize(cols.width + cols.gutter_width) 1101 linked_columns = cols.linked_columns 1102 for _ in range(cols.count - 1): 1103 insert += offset 1104 column = MText.new(dxfattribs=attribs, doc=doc) 1105 column.dxf.insert = insert 1106 linked_columns.append(column) 1107 1108 def remove_dependencies(self, other: 'Drawing' = None) -> None: 1109 if not self.is_alive: 1110 return 1111 1112 super().remove_dependencies() 1113 has_style = (bool(other) and (self.dxf.style in other.styles)) 1114 if not has_style: 1115 self.dxf.style = 'Standard' 1116 1117 if self.has_columns: 1118 for column in self._columns.linked_columns: 1119 column.remove_dependencies(other) 1120