1# Copyright (c) 2021 Manfred Moitzi
2# License: MIT License
3from pathlib import Path
4import ezdxf
5from ezdxf.tools.text import (
6    MTextEditor, ParagraphProperties, MTextParagraphAlignment,
7)
8from ezdxf.tools.text_layout import lorem_ipsum
9
10OUTBOX = Path("~/Desktop/Outbox").expanduser()
11ATTRIBS = {
12    "char_height": 0.7,
13    "style": "OpenSans",
14}
15
16# use constants defined in MTextEditor:
17NP = MTextEditor.NEW_PARAGRAPH
18
19
20def recreate_mtext_py_example(msp, location):
21    # replicate example "mtext.py":
22    attribs = dict(ATTRIBS)
23    attribs["width"] = 15.0
24    editor = MTextEditor(f"recreate mtext.py result:{NP}normal ").overline(
25        "over line").append(" normal" + NP + "normal ").strike_through(
26        "strike through").append(" normal" + NP).underline(
27        "under line").append(" normal")
28    msp.add_mtext(str(editor), attribs).set_location(insert=location)
29
30
31def using_colors(msp, location):
32    attribs = dict(ATTRIBS)
33    attribs["width"] = 10.0
34    editor = MTextEditor("using colors:" + NP)
35    # Change colors by name: red, green, blue, yellow, cyan, magenta, white
36    editor.color("red").append("RED" + NP)
37    # The color stays the same until changed
38    editor.append("also RED" + NP)
39    # Change color by ACI (AutoCAD Color Index)
40    editor.aci(3).append("GREEN" + NP)
41    # Change color by RGB tuples
42    editor.rgb((0, 0, 255)).append("BLUE" + NP)
43    msp.add_mtext(str(editor), attribs).set_location(insert=location)
44
45
46def changing_text_height_absolute(msp, location):
47    attribs = dict(ATTRIBS)
48    attribs["width"] = 40.0  # need mor space to avoid text wrapping
49    editor = MTextEditor(
50        "changing text height absolute: default height is 0.7" + NP)
51    # doubling the default height = 1.4
52    editor.height(1.4)
53    editor.append("text height: 1.4" + NP)
54    editor.height(3.5).append("text height: 3.5" + NP)
55    editor.height(0.7).append("back to default height: 0.7" + NP)
56    msp.add_mtext(str(editor), attribs).set_location(insert=location)
57
58
59def changing_text_height_relative(msp, location):
60    attribs = dict(ATTRIBS)
61    attribs["width"] = 40.0  # need mor space to avoid text wrapping
62    editor = MTextEditor(
63        "changing text height relative: default height is 0.7" + NP)
64    # this is the default text height in the beginning:
65    current_height = attribs["char_height"]
66    # The text height can only be changed by a factor:
67    editor.scale_height(2)  # scale by 2 = 1.4
68    # keep track of the actual height:
69    current_height *= 2
70    editor.append("text height: 1.4" + NP)
71    # to set an absolute height, calculate the required factor:
72    desired_height = 3.5
73    factor = desired_height / current_height
74    editor.scale_height(factor).append("text height: 3.5" + NP)
75    current_height = desired_height
76    # and back to 0.7
77    editor.scale_height(0.7 / current_height).append(
78        "back to default height: 0.7" + NP)
79    msp.add_mtext(str(editor), attribs).set_location(insert=location)
80
81
82def changing_fonts(msp, location):
83    attribs = dict(ATTRIBS)
84    attribs["width"] = 15.0
85    editor = MTextEditor("changing fonts:" + NP)
86    editor.append("Default: Hello World!" + NP)
87    editor.append("SimSun: ")
88    # The font name for changing MTEXT fonts inline is the font family name!
89    # The font family name is the name shown in font selection widgets in
90    # desktop applications: "Arial", "Times New Roman", "Comic Sans MS"
91    #
92    # change font in a group to revert back to the default font at the end:
93    simsun_editor = MTextEditor().font("SimSun").append("你好,世界" + NP)
94    # reverts the font back at the end of the group:
95    editor.group(str(simsun_editor))
96    # back to default font OpenSans:
97    editor.append("Times New Roman: ")
98    # change font outside of a group until next font change:
99    editor.font("Times New Roman").append("Привет мир!" + NP)
100    # If the font does not exist, a replacement font will be used:
101    editor.font("Does not exist").append("This is the replacement font!")
102    msp.add_mtext(str(editor), attribs).set_location(insert=location)
103
104
105def indent_first_line(msp, location):
106    # Indentation is a multiple of the default text height (MTEXT char_height)
107    attribs = dict(ATTRIBS)
108    attribs["char_height"] = 0.25
109    attribs["width"] = 7.5
110    editor = MTextEditor("Indent the first line:" + NP)
111    props = ParagraphProperties(
112        indent=1,  # indent first line = 1x0.25 drawing units
113        align=MTextParagraphAlignment.JUSTIFIED
114    )
115    editor.paragraph(props)
116    editor.append(" ".join(lorem_ipsum(100)))
117    msp.add_mtext(str(editor), attribs).set_location(insert=location)
118
119
120def indent_except_fist_line(msp, location):
121    # Indentation is a multiple of the default text height (MTEXT char_height)
122    attribs = dict(ATTRIBS)
123    attribs["char_height"] = 0.25
124    attribs["width"] = 7.5
125    editor = MTextEditor("Indent left paragraph side:" + NP)
126    indent = 0.7  # 0.7 * 0.25 = 0.175 drawing units
127    props = ParagraphProperties(
128        # first line indentation is relative to "left", this reverses the
129        # left indentation:
130        indent=-indent,  # first line
131        # indent left paragraph side:
132        left=indent,
133        align=MTextParagraphAlignment.JUSTIFIED
134    )
135    editor.paragraph(props)
136    editor.append(" ".join(lorem_ipsum(100)))
137    msp.add_mtext(str(editor), attribs).set_location(insert=location)
138
139
140def bullet_list(msp, location):
141    attribs = dict(ATTRIBS)
142    attribs["char_height"] = 0.25
143    attribs["width"] = 7.5
144    # There are no special commands to build bullet list, the list is build of
145    # indentation and a tabulator stop. Each list item needs a marker as an
146    # arbitrary string.
147    bullet = "•"  # alt + numpad 7
148    editor = MTextEditor("Bullet List:" + NP)
149    editor.bullet_list(
150        indent=1,
151        bullets=[bullet] * 3,  # each list item needs a marker
152        content=[
153            "First item",
154            "Second item",
155            " ".join(lorem_ipsum(30)),
156        ])
157    msp.add_mtext(str(editor), attribs).set_location(insert=location)
158
159
160def numbered_list(msp, location):
161    attribs = dict(ATTRIBS)
162    attribs["char_height"] = 0.25
163    attribs["width"] = 7.5
164    # There are no special commands to build numbered list, the list is build of
165    # indentation and a tabulator stop. There is no automatic numbering,
166    # but therefore the absolute freedom for using any string as list marker:
167    editor = MTextEditor("Numbered List:" + NP)
168    editor.bullet_list(
169        indent=1,
170        bullets=["1.", "2.", "3."],
171        content=[
172            "First item",
173            "Second item",
174            " ".join(lorem_ipsum(30)),
175        ])
176    # Indentation and tab stops are multiples of the default text height (MTEXT
177    # char_height)!
178    msp.add_mtext(str(editor), attribs).set_location(insert=location)
179
180
181def stacking(msp, location):
182    attribs = dict(ATTRIBS)
183    attribs["char_height"] = 0.25
184    attribs["width"] = 4
185    editor = MTextEditor("Stacked text:" + NP)
186
187    # place fraction with down scaled text height in a group:
188    stack = MTextEditor().scale_height(0.6).stack("1", "2", "^")
189    editor.append("over: ").group(str(stack)).append(NP)
190
191    stack = MTextEditor().scale_height(0.6).stack("1", "2", "/")
192    editor.append("fraction: ").group(str(stack)).append(NP)
193
194    stack = MTextEditor().scale_height(0.6).stack("1", "2", "#")
195    editor.append("slanted: ").group(str(stack)).append(NP)
196
197    # additional formatting in numerator and denominator is not supported
198    # by AutoCAD or BricsCAD.
199    # switching colors inside the fraction to red does not work:
200    numerator = MTextEditor().color("red").append("1")
201    stack = MTextEditor().scale_height(0.6).stack(str(numerator), "2", "#")
202    editor.append("color red: ").group(str(stack)).append(NP)
203    msp.add_mtext(str(editor), attribs).set_location(insert=location)
204
205
206def create(dxfversion):
207    """
208    Important:
209
210        MTEXT FORMATTING IS NOT PORTABLE ACROSS CAD APPLICATIONS!
211
212    Inline MTEXT codes are not supported by every CAD application and even
213    if inline codes are supported the final rendering may vary.
214    Inline codes are very well supported by AutoCAD (of course!) and BricsCAD,
215    but don't expect the same rendering in other CAD applications.
216
217    The drawing add-on of ezdxf may support some features in the future,
218    but very likely with a different rendering result than AutoCAD/BricsCAD.
219
220    """
221    doc = ezdxf.new(dxfversion, setup=True)
222    msp = doc.modelspace()
223    recreate_mtext_py_example(msp, location=(0, 0))
224    using_colors(msp, location=(0, 10))
225    changing_text_height_absolute(msp, location=(0, 25))
226    changing_text_height_relative(msp, location=(0, 40))
227    changing_fonts(msp, location=(15, 14))
228    indent_first_line(msp, location=(15, 6))
229    indent_except_fist_line(msp, location=(24, 6))
230    bullet_list(msp, location=(33, 6))
231    numbered_list(msp, location=(33, 2))
232    stacking(msp, location=(33, 14))
233    doc.set_modelspace_vport(height=60, center=(15, 15))
234    return doc
235
236
237for dxfversion in ["R2000", "R2004", "R2007", "R2010", "R2013", "R2018"]:
238    doc = create(dxfversion)
239    filename = f"mtext_editor_{dxfversion}.dxf"
240    doc.saveas(OUTBOX / filename)
241    print(f"saved {filename}")
242