mtext.py 9.3 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271
  1. # Created: 24.05.2015
  2. # Copyright (c) 2015-2018, Manfred Moitzi
  3. # License: MIT License
  4. from typing import TYPE_CHECKING, List, Union, Tuple
  5. from contextlib import contextmanager
  6. import math
  7. from ezdxf.math.vector import Vector
  8. from ezdxf.lldxf.attributes import DXFAttr, DXFAttributes, DefSubclass, XType
  9. from ezdxf.lldxf.tags import DXFTag
  10. from ezdxf.lldxf.extendedtags import ExtendedTags
  11. from ezdxf.lldxf import const
  12. from ezdxf.lldxf.const import DXFValueError
  13. from ezdxf import rgb2int
  14. from .graphics import none_subclass, entity_subclass, ModernGraphicEntity
  15. if TYPE_CHECKING:
  16. from ezdxf.eztypes import Vertex
  17. _MTEXT_TPL = """0
  18. MTEXT
  19. 5
  20. 0
  21. 330
  22. 0
  23. 100
  24. AcDbEntity
  25. 8
  26. 0
  27. 100
  28. AcDbMText
  29. 40
  30. 1.0
  31. 71
  32. 1
  33. 73
  34. 1
  35. 1
  36. """
  37. mtext_subclass = DefSubclass('AcDbMText', {
  38. 'insert': DXFAttr(10, xtype=XType.point3d),
  39. 'char_height': DXFAttr(40), # nominal (initial) text height
  40. 'width': DXFAttr(41), # reference column width
  41. 'attachment_point': DXFAttr(71),
  42. # 1 = Top left; 2 = Top center; 3 = Top right
  43. # 4 = Middle left; 5 = Middle center; 6 = Middle right
  44. # 7 = Bottom left; 8 = Bottom center; 9 = Bottom right
  45. 'flow_direction': DXFAttr(72),
  46. # 1 = Left to right
  47. # 3 = Top to bottom
  48. # 5 = By style (the flow direction is inherited from the associated text style)
  49. 'style': DXFAttr(7, default='STANDARD'), # text style name
  50. 'extrusion': DXFAttr(210, xtype=XType.point3d, default=(0.0, 0.0, 1.0)),
  51. 'text_direction': DXFAttr(11, xtype=XType.point3d), # x-axis direction vector (in WCS)
  52. # If *rotation* and *text_direction* are present, *text_direction* wins
  53. 'rect_width': DXFAttr(42), # Horizontal width of the characters that make up the mtext entity.
  54. # This value will always be equal to or less than the value of *width*, (read-only, ignored if supplied)
  55. 'rect_height': DXFAttr(43), # vertical height of the mtext entity (read-only, ignored if supplied)
  56. 'rotation': DXFAttr(50, default=0.0), # in degrees (circle=360 deg) - error in DXF reference, which says radians
  57. 'line_spacing_style': DXFAttr(73), # line spacing style (optional):
  58. # 1 = At least (taller characters will override)
  59. # 2 = Exact (taller characters will not override)
  60. 'line_spacing_factor': DXFAttr(44), # line spacing factor (optional):
  61. # Percentage of default (3-on-5) line spacing to be applied. Valid values
  62. # range from 0.25 to 4.00
  63. 'box_fill_scale': DXFAttr(45, dxfversion='AC1021'),
  64. # Determines how much border there is around the text.
  65. # (45) + (90) + (63) all three required, if one of them is used
  66. 'bg_fill': DXFAttr(90, dxfversion='AC1021'), # background fill type:
  67. # 0=off;
  68. # 1=color -> (63) < (421) or (431);
  69. # 2=drawing window color
  70. # 3=use background color
  71. 'bg_fill_color': DXFAttr(63, dxfversion='AC1021'), # background fill color as ACI, required even true color is used
  72. 'bg_fill_true_color': DXFAttr(421, dxfversion='AC1021'), # background fill color as true color value, (63) also required but ignored
  73. 'bg_fill_color_name': DXFAttr(431, dxfversion='AC1021'), # background fill color as color name ???, (63) also required but ignored
  74. 'bg_fill_transparency': DXFAttr(441, dxfversion='AC1021'), # background fill color transparency - not used by AutoCAD/BricsCAD
  75. })
  76. class MText(ModernGraphicEntity): # MTEXT will be extended in DXF version AC1021 (ACAD 2007)
  77. __slots__ = ()
  78. TEMPLATE = ExtendedTags.from_text(_MTEXT_TPL)
  79. DXFATTRIBS = DXFAttributes(none_subclass, entity_subclass, mtext_subclass)
  80. def get_text(self) -> str:
  81. tags = self.tags.get_subclass('AcDbMText')
  82. tail = ""
  83. parts = []
  84. for tag in tags:
  85. if tag.code == 1:
  86. tail = tag.value
  87. if tag.code == 3:
  88. parts.append(tag.value)
  89. parts.append(tail)
  90. return "".join(parts)
  91. def set_text(self, text: str) -> 'MText':
  92. tags = self.tags.get_subclass('AcDbMText')
  93. tags.remove_tags(codes=(1, 3))
  94. str_chunks = split_string_in_chunks(text, size=250)
  95. if len(str_chunks) == 0:
  96. str_chunks.append("")
  97. while len(str_chunks) > 1:
  98. tags.append(DXFTag(3, str_chunks.pop(0)))
  99. tags.append(DXFTag(1, str_chunks[0]))
  100. return self
  101. def get_rotation(self) -> float:
  102. try:
  103. vector = self.dxf.text_direction
  104. except DXFValueError:
  105. rotation = self.get_dxf_attrib('rotation', 0.0)
  106. else:
  107. radians = math.atan2(vector[1], vector[0]) # ignores z-axis
  108. rotation = math.degrees(radians)
  109. return rotation
  110. def set_rotation(self, angle: float) -> 'MText':
  111. del self.dxf.text_direction # *text_direction* has higher priority than *rotation*, therefore delete it
  112. self.dxf.rotation = angle
  113. return self
  114. def set_location(self, insert: 'Vertex', rotation: float = None, attachment_point: int = None) -> 'MText':
  115. self.dxf.insert = Vector(insert)
  116. if rotation is not None:
  117. self.set_rotation(rotation)
  118. if attachment_point is not None:
  119. self.dxf.attachment_point = attachment_point
  120. return self
  121. def set_bg_color(self, color: Union[int, str, Tuple[int, int, int], None], scale: float = 1.5):
  122. self.dxf.box_fill_scale = scale
  123. if color is None:
  124. self.del_dxf_attrib('bg_fill')
  125. self.del_dxf_attrib('box_fill_scale')
  126. self.del_dxf_attrib('bg_fill_color')
  127. self.del_dxf_attrib('bg_fill_true_color')
  128. self.del_dxf_attrib('bg_fill_color_name')
  129. elif color == 'canvas': # special case for use background color
  130. self.dxf.bg_fill = const.MTEXT_BG_CANVAS_COLOR
  131. self.dxf.bg_fill_color = 0 # required but ignored
  132. else:
  133. self.dxf.bg_fill = const.MTEXT_BG_COLOR
  134. if isinstance(color, int):
  135. self.dxf.bg_fill_color = color
  136. elif isinstance(color, str):
  137. self.dxf.bg_fill_color = 0 # required but ignored
  138. self.dxf.bg_fill_color_name = color
  139. elif isinstance(color, tuple):
  140. self.dxf.bg_fill_color = 0 # required but ignored
  141. self.dxf.bg_fill_true_color = rgb2int(color)
  142. return self # fluent interface
  143. @contextmanager
  144. def edit_data(self) -> 'MTextData':
  145. buffer = MTextData(self.get_text())
  146. yield buffer
  147. self.set_text(buffer.text)
  148. buffer = edit_data # alias
  149. ##################################################
  150. # MTEXT inline codes
  151. # \L Start underline
  152. # \l Stop underline
  153. # \O Start overstrike
  154. # \o Stop overstrike
  155. # \K Start strike-through
  156. # \k Stop strike-through
  157. # \P New paragraph (new line)
  158. # \pxi Control codes for bullets, numbered paragraphs and columns
  159. # \X Paragraph wrap on the dimension line (only in dimensions)
  160. # \Q Slanting (obliquing) text by angle - e.g. \Q30;
  161. # \H Text height - e.g. \H3x;
  162. # \W Text width - e.g. \W0.8x;
  163. # \F Font selection
  164. #
  165. # e.g. \Fgdt;o - GDT-tolerance
  166. # e.g. \Fkroeger|b0|i0|c238|p10 - font Kroeger, non-bold, non-italic, codepage 238, pitch 10
  167. #
  168. # \S Stacking, fractions
  169. #
  170. # e.g. \SA^B:
  171. # A
  172. # B
  173. # e.g. \SX/Y:
  174. # X
  175. # -
  176. # Y
  177. # e.g. \S1#4:
  178. # 1/4
  179. #
  180. # \A Alignment
  181. #
  182. # \A0; = bottom
  183. # \A1; = center
  184. # \A2; = top
  185. #
  186. # \C Color change
  187. #
  188. # \C1; = red
  189. # \C2; = yellow
  190. # \C3; = green
  191. # \C4; = cyan
  192. # \C5; = blue
  193. # \C6; = magenta
  194. # \C7; = white
  195. #
  196. # \T Tracking, char.spacing - e.g. \T2;
  197. # \~ Non-wrapping space, hard space
  198. # {} Braces - define the text area influenced by the code
  199. # \ Escape character - e.g. \\ = "\", \{ = "{"
  200. #
  201. # Codes and braces can be nested up to 8 levels deep
  202. class MTextData:
  203. UNDERLINE_START = '\\L;'
  204. UNDERLINE_STOP = '\\l;'
  205. UNDERLINE = UNDERLINE_START + '%s' + UNDERLINE_STOP
  206. OVERSTRIKE_START = '\\O;'
  207. OVERSTRIKE_STOP = '\\o;'
  208. OVERSTRIKE = OVERSTRIKE_START + '%s' + OVERSTRIKE_STOP
  209. STRIKE_START = '\\K;'
  210. STRIKE_STOP = '\\k;'
  211. STRIKE = STRIKE_START + '%s' + STRIKE_STOP
  212. NEW_LINE = '\\P;'
  213. GROUP_START = '{'
  214. GROUP_END = '}'
  215. GROUP = GROUP_START + '%s' + GROUP_END
  216. NBSP = '\\~' # none breaking space
  217. def __init__(self, text: str):
  218. self.text = text
  219. def __iadd__(self, text: str) -> 'MTextData':
  220. self.text += text
  221. return self
  222. append = __iadd__
  223. def set_font(self, name: str, bold: bool = False, italic: bool = False, codepage: int = 1252,
  224. pitch: int = 0) -> None:
  225. bold_flag = 1 if bold else 0
  226. italic_flag = 1 if italic else 0
  227. s = "\\F{}|b{}|i{}|c{}|p{};".format(name, bold_flag, italic_flag, codepage, pitch)
  228. self.append(s)
  229. def set_color(self, color_name: str) -> None:
  230. self.append("\\C%d" % const.MTEXT_COLOR_INDEX[color_name.lower()])
  231. def split_string_in_chunks(s: str, size: int = 250) -> List[str]:
  232. chunks = []
  233. pos = 0
  234. while True:
  235. chunk = s[pos:pos + size]
  236. chunk_len = len(chunk)
  237. if chunk_len:
  238. chunks.append(chunk)
  239. if chunk_len < size:
  240. return chunks
  241. pos += size
  242. else:
  243. return chunks