drawlib 0.1.1__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Potentially problematic release.
This version of drawlib might be problematic. Click here for more details.
- drawlib/.pylintrc +2 -0
- drawlib/__init__.py +33 -0
- drawlib/__main__.py +21 -0
- drawlib/apis.py +22 -0
- drawlib/assets/__init__.py +8 -0
- drawlib/assets/v0_1/__init__.py +8 -0
- drawlib/assets/v0_1/fonticons/__init__.py +8 -0
- drawlib/assets/v0_1/fonts/__init__.py +8 -0
- drawlib/v0_1/__init__.py +31 -0
- drawlib/v0_1/__main__.py +20 -0
- drawlib/v0_1/apis.py +127 -0
- drawlib/v0_1/private/__init__.py +20 -0
- drawlib/v0_1/private/arg_validator.py +163 -0
- drawlib/v0_1/private/command.py +204 -0
- drawlib/v0_1/private/core/__init__.py +15 -0
- drawlib/v0_1/private/core/colors.py +285 -0
- drawlib/v0_1/private/core/dimage.py +510 -0
- drawlib/v0_1/private/core/fonts.py +324 -0
- drawlib/v0_1/private/core/model.py +1082 -0
- drawlib/v0_1/private/core/model_validator.py +175 -0
- drawlib/v0_1/private/core/theme.py +1042 -0
- drawlib/v0_1/private/core/util.py +809 -0
- drawlib/v0_1/private/core_canvas/__init__.py +15 -0
- drawlib/v0_1/private/core_canvas/base.py +653 -0
- drawlib/v0_1/private/core_canvas/canvas.py +88 -0
- drawlib/v0_1/private/core_canvas/image.py +154 -0
- drawlib/v0_1/private/core_canvas/line.py +263 -0
- drawlib/v0_1/private/core_canvas/original_arrow.py +139 -0
- drawlib/v0_1/private/core_canvas/original_polygon.py +584 -0
- drawlib/v0_1/private/core_canvas/patches.py +547 -0
- drawlib/v0_1/private/core_canvas/text.py +136 -0
- drawlib/v0_1/private/download.py +58 -0
- drawlib/v0_1/private/dutil.py +28 -0
- drawlib/v0_1/private/icons/__init__.py +15 -0
- drawlib/v0_1/private/icons/phosphor.py +35275 -0
- drawlib/v0_1/private/icons/util.py +82 -0
- drawlib/v0_1/private/logging.py +38 -0
- drawlib/v0_1/private/settings.py +233 -0
- drawlib/v0_1/private/smartarts/__init__.py +15 -0
- drawlib/v0_1/private/smartarts/cycle.py +11 -0
- drawlib/v0_1/private/smartarts/dsart.py +17 -0
- drawlib/v0_1/private/smartarts/groups.py +20 -0
- drawlib/v0_1/private/smartarts/pyramid.py +18 -0
- drawlib/v0_1/private/smartarts/sourcecode.py +216 -0
- drawlib/v0_1/private/smartarts/table.py +11 -0
- drawlib/v0_1/private/smartarts/tree.py +18 -0
- drawlib/v0_1/private/tools.py +161 -0
- drawlib/v0_1/private/util.py +353 -0
- drawlib-0.1.1.dist-info/LICENSE.txt +202 -0
- drawlib-0.1.1.dist-info/METADATA +36 -0
- drawlib-0.1.1.dist-info/RECORD +52 -0
- drawlib-0.1.1.dist-info/WHEEL +4 -0
|
@@ -0,0 +1,809 @@
|
|
|
1
|
+
# Copyright (c) 2024 Yuichi Ito (yuichi@yuichi.com)
|
|
2
|
+
#
|
|
3
|
+
# This software is licensed under the Apache License, Version 2.0.
|
|
4
|
+
# For more information, please visit: https://github.com/yuichi110/drawlib
|
|
5
|
+
#
|
|
6
|
+
# This software is provided "as is", without warranty of any kind,
|
|
7
|
+
# express or implied, including but not limited to the warranties of
|
|
8
|
+
# merchantability, fitness for a particular purpose and noninfringement.
|
|
9
|
+
|
|
10
|
+
"""Internal utility functions for drawing matplotlib objects
|
|
11
|
+
|
|
12
|
+
drawlib's style are different to matplotlib's style.
|
|
13
|
+
This module provides format converter utilities.
|
|
14
|
+
It is not designed to be used by library users.
|
|
15
|
+
|
|
16
|
+
"""
|
|
17
|
+
|
|
18
|
+
import math
|
|
19
|
+
from typing import Optional, Dict, Any, Union, Tuple
|
|
20
|
+
from copy import deepcopy
|
|
21
|
+
from matplotlib.font_manager import FontProperties
|
|
22
|
+
from matplotlib.text import Text
|
|
23
|
+
|
|
24
|
+
from drawlib.v0_1.private.download import download_if_not_exist
|
|
25
|
+
from drawlib.v0_1.private.core.colors import Colors
|
|
26
|
+
from drawlib.v0_1.private.core.model import (
|
|
27
|
+
IconStyle,
|
|
28
|
+
IconStyleDefault,
|
|
29
|
+
ImageStyle,
|
|
30
|
+
ImageStyleDefault,
|
|
31
|
+
LineStyle,
|
|
32
|
+
LineStyleDefault,
|
|
33
|
+
LineArrowStyle,
|
|
34
|
+
LineArrowStyleDefault,
|
|
35
|
+
ShapeStyle,
|
|
36
|
+
ShapeStyleDefault,
|
|
37
|
+
ShapeTextStyle,
|
|
38
|
+
ShapeTextStyleDefault,
|
|
39
|
+
TextStyle,
|
|
40
|
+
TextStyleDefault,
|
|
41
|
+
)
|
|
42
|
+
from drawlib.v0_1.private.core.fonts import (
|
|
43
|
+
FontBase,
|
|
44
|
+
FontFile,
|
|
45
|
+
)
|
|
46
|
+
from drawlib.v0_1.private.core.theme import dtheme
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
class IconUtil:
|
|
50
|
+
|
|
51
|
+
@staticmethod
|
|
52
|
+
def get_merged_style(
|
|
53
|
+
primary: Optional[IconStyle],
|
|
54
|
+
secondary: IconStyle,
|
|
55
|
+
) -> IconStyle:
|
|
56
|
+
if primary is None:
|
|
57
|
+
primary = IconStyle()
|
|
58
|
+
else:
|
|
59
|
+
primary = deepcopy(primary)
|
|
60
|
+
|
|
61
|
+
# apply secondary (theme)
|
|
62
|
+
if primary.style is None:
|
|
63
|
+
primary.style = secondary.style
|
|
64
|
+
if primary.color is None:
|
|
65
|
+
primary.color = secondary.color
|
|
66
|
+
if primary.alpha is None:
|
|
67
|
+
primary.alpha = secondary.alpha
|
|
68
|
+
if primary.halign is None:
|
|
69
|
+
primary.halign = secondary.halign
|
|
70
|
+
if primary.valign is None:
|
|
71
|
+
primary.valign = secondary.valign
|
|
72
|
+
|
|
73
|
+
# apply system default
|
|
74
|
+
if primary.style is None:
|
|
75
|
+
primary.style = IconStyleDefault.style
|
|
76
|
+
if primary.color is None:
|
|
77
|
+
primary.color = IconStyleDefault.color
|
|
78
|
+
if primary.alpha is None:
|
|
79
|
+
primary.alpha = IconStyleDefault.alpha
|
|
80
|
+
if primary.halign is None:
|
|
81
|
+
primary.halign = IconStyleDefault.haligh
|
|
82
|
+
if primary.valign is None:
|
|
83
|
+
primary.valign = IconStyleDefault.valign
|
|
84
|
+
|
|
85
|
+
return primary
|
|
86
|
+
|
|
87
|
+
|
|
88
|
+
class ImageUtil:
|
|
89
|
+
|
|
90
|
+
@staticmethod
|
|
91
|
+
def get_merged_style(
|
|
92
|
+
primary: Optional[ImageStyle],
|
|
93
|
+
secondary: ImageStyle,
|
|
94
|
+
) -> IconStyle:
|
|
95
|
+
if primary is None:
|
|
96
|
+
primary = ImageStyle()
|
|
97
|
+
else:
|
|
98
|
+
primary = deepcopy(primary)
|
|
99
|
+
|
|
100
|
+
# apply secondary (theme)
|
|
101
|
+
if primary.lstyle is None:
|
|
102
|
+
primary.lstyle = secondary.lstyle
|
|
103
|
+
if primary.lcolor is None:
|
|
104
|
+
primary.lcolor = secondary.lcolor
|
|
105
|
+
if primary.lwidth is None:
|
|
106
|
+
primary.lwidth = secondary.lwidth
|
|
107
|
+
if primary.halign is None:
|
|
108
|
+
primary.halign = secondary.halign
|
|
109
|
+
if primary.valign is None:
|
|
110
|
+
primary.valign = secondary.valign
|
|
111
|
+
|
|
112
|
+
# apply system default
|
|
113
|
+
if primary.lstyle is None:
|
|
114
|
+
primary.lstyle = ImageStyleDefault.lstyle
|
|
115
|
+
if primary.lcolor is None:
|
|
116
|
+
primary.lcolor = ImageStyleDefault.lcolor
|
|
117
|
+
if primary.lwidth is None:
|
|
118
|
+
primary.lwidth = ImageStyleDefault.lwidth
|
|
119
|
+
if primary.halign is None:
|
|
120
|
+
primary.halign = ImageStyleDefault.halign
|
|
121
|
+
if primary.valign is None:
|
|
122
|
+
primary.valign = ImageStyleDefault.valign
|
|
123
|
+
|
|
124
|
+
return primary
|
|
125
|
+
|
|
126
|
+
|
|
127
|
+
class ColorUtil:
|
|
128
|
+
|
|
129
|
+
@staticmethod
|
|
130
|
+
def get_mplot_rgba(
|
|
131
|
+
rgb_or_rgba: Union[Tuple[int, int, int], Tuple[int, int, int, float]],
|
|
132
|
+
alpha: Optional[float] = None,
|
|
133
|
+
) -> Tuple[float, float, float, float]:
|
|
134
|
+
"""Convert 0~255 RGB/RGBA to 0.0 ~ 1.0 RGBA
|
|
135
|
+
|
|
136
|
+
drawlib prefere 0~255 RGB/RGBA.
|
|
137
|
+
matplotlib uses 0.0~1.0 RGB/RGBA.
|
|
138
|
+
|
|
139
|
+
This function provides converter from drawlib format to matplotlib format.
|
|
140
|
+
Alpha is determined this way.
|
|
141
|
+
|
|
142
|
+
1. If having alpha arg, use it
|
|
143
|
+
2. If original data is RGBA, use its alpha value
|
|
144
|
+
3. Set alpha 1.0
|
|
145
|
+
|
|
146
|
+
Args:
|
|
147
|
+
rgb_or_rgba: 0~255 for RGB. 0.0 ~ 1.0 for A if having alpha.
|
|
148
|
+
alpha: Over write alpha value
|
|
149
|
+
|
|
150
|
+
Returns:
|
|
151
|
+
Tuple[float, float, float, float]: matplotlib's RGBA format.
|
|
152
|
+
|
|
153
|
+
"""
|
|
154
|
+
|
|
155
|
+
r = round(rgb_or_rgba[0] / 255, 5)
|
|
156
|
+
g = round(rgb_or_rgba[1] / 255, 5)
|
|
157
|
+
b = round(rgb_or_rgba[2] / 255, 5)
|
|
158
|
+
|
|
159
|
+
if alpha is not None:
|
|
160
|
+
a = alpha
|
|
161
|
+
elif len(rgb_or_rgba) == 3:
|
|
162
|
+
a = 1.0
|
|
163
|
+
else:
|
|
164
|
+
a = rgb_or_rgba[3]
|
|
165
|
+
|
|
166
|
+
return (r, g, b, a)
|
|
167
|
+
|
|
168
|
+
@staticmethod
|
|
169
|
+
def get_hexrgb(
|
|
170
|
+
rgb_or_rgba: Union[Tuple[int, int, int], Tuple[int, int, int, float]],
|
|
171
|
+
) -> str:
|
|
172
|
+
r = rgb_or_rgba[0]
|
|
173
|
+
g = rgb_or_rgba[1]
|
|
174
|
+
b = rgb_or_rgba[2]
|
|
175
|
+
if not (0 <= r <= 255 and 0 <= g <= 255 and 0 <= b <= 255):
|
|
176
|
+
raise ValueError("RGB values must be in the range 0 to 255.")
|
|
177
|
+
hex_color = f"#{r:02x}{g:02x}{b:02x}"
|
|
178
|
+
return hex_color
|
|
179
|
+
|
|
180
|
+
@staticmethod
|
|
181
|
+
def get_rgba_from_hex(hex_color: str) -> Tuple[int, int, int, float]:
|
|
182
|
+
"""
|
|
183
|
+
Convert a hexadecimal color code to RGBA values.
|
|
184
|
+
|
|
185
|
+
Args:
|
|
186
|
+
hex_color (str): The hexadecimal color code (e.g., "#FF5733" or "#FFF").
|
|
187
|
+
|
|
188
|
+
Returns:
|
|
189
|
+
tuple[int, int, int, float]: A tuple containing the RGBA values (0-255 for R, G, B and 0.0-1.0 for A).
|
|
190
|
+
"""
|
|
191
|
+
|
|
192
|
+
# Remove the '#' prefix if present
|
|
193
|
+
hex_color = hex_color.lstrip("#")
|
|
194
|
+
|
|
195
|
+
# Determine the length of the hex color code
|
|
196
|
+
hex_length = len(hex_color)
|
|
197
|
+
|
|
198
|
+
# Convert the hex code to RGB values
|
|
199
|
+
if hex_length == 3: # Short hex format (#RGB)
|
|
200
|
+
r = int(hex_color[0] * 2, 16)
|
|
201
|
+
g = int(hex_color[1] * 2, 16)
|
|
202
|
+
b = int(hex_color[2] * 2, 16)
|
|
203
|
+
a = 1.0
|
|
204
|
+
elif hex_length in (6, 8): # Full hex format (#RRGGBB)
|
|
205
|
+
r = int(hex_color[0:2], 16)
|
|
206
|
+
g = int(hex_color[2:4], 16)
|
|
207
|
+
b = int(hex_color[4:6], 16)
|
|
208
|
+
if hex_length == 8: # With alpha
|
|
209
|
+
a = int(hex_color[6:8], 16)
|
|
210
|
+
else:
|
|
211
|
+
a = 1.0
|
|
212
|
+
else:
|
|
213
|
+
raise ValueError("Invalid hex color code format")
|
|
214
|
+
|
|
215
|
+
return (r, g, b, a)
|
|
216
|
+
|
|
217
|
+
|
|
218
|
+
class LineUtil:
|
|
219
|
+
|
|
220
|
+
@staticmethod
|
|
221
|
+
def get_merged_style(
|
|
222
|
+
primary: Union[LineStyle, LineArrowStyle],
|
|
223
|
+
secondary_linestyle: LineStyle,
|
|
224
|
+
secondary_linearrowstyle: LineArrowStyle,
|
|
225
|
+
) -> Union[LineStyle, LineArrowStyle]:
|
|
226
|
+
if primary is None:
|
|
227
|
+
primary = LineStyle()
|
|
228
|
+
else:
|
|
229
|
+
primary = deepcopy(primary)
|
|
230
|
+
|
|
231
|
+
if isinstance(primary, LineStyle):
|
|
232
|
+
secondary = secondary_linestyle
|
|
233
|
+
|
|
234
|
+
# apply secondary (theme)
|
|
235
|
+
if primary.width is None:
|
|
236
|
+
primary.width = secondary.width
|
|
237
|
+
if primary.color is None:
|
|
238
|
+
primary.color = secondary.color
|
|
239
|
+
if primary.style is None:
|
|
240
|
+
primary.style = secondary.style
|
|
241
|
+
if primary.alpha is None:
|
|
242
|
+
primary.alpha = secondary.alpha
|
|
243
|
+
|
|
244
|
+
# apply system default
|
|
245
|
+
if primary.width is None:
|
|
246
|
+
primary.width = LineStyleDefault.width
|
|
247
|
+
if primary.color is None:
|
|
248
|
+
primary.color = LineStyleDefault.color
|
|
249
|
+
if primary.style is None:
|
|
250
|
+
primary.style = LineStyleDefault.style
|
|
251
|
+
if primary.alpha is None:
|
|
252
|
+
primary.alpha = LineStyleDefault.alpha
|
|
253
|
+
|
|
254
|
+
return primary
|
|
255
|
+
|
|
256
|
+
# apply secondary (theme)
|
|
257
|
+
secondary = secondary_linearrowstyle
|
|
258
|
+
if primary.lstyle is None:
|
|
259
|
+
primary.lstyle = secondary.lstyle
|
|
260
|
+
if primary.lwidth is None:
|
|
261
|
+
primary.lwidth = secondary.lwidth
|
|
262
|
+
if primary.hstyle is None:
|
|
263
|
+
primary.hstyle = secondary.hstyle
|
|
264
|
+
if primary.hscale is None:
|
|
265
|
+
primary.hscale = secondary.hscale
|
|
266
|
+
if primary.color is None:
|
|
267
|
+
primary.color = secondary.color
|
|
268
|
+
if primary.alpha is None:
|
|
269
|
+
primary.alpha = secondary.alpha
|
|
270
|
+
|
|
271
|
+
# apply system default
|
|
272
|
+
if primary.lstyle is None:
|
|
273
|
+
primary.lstyle = LineArrowStyleDefault.lstyle
|
|
274
|
+
if primary.lwidth is None:
|
|
275
|
+
primary.lwidth = LineArrowStyleDefault.lwidth
|
|
276
|
+
if primary.hstyle is None:
|
|
277
|
+
primary.hstyle = LineArrowStyleDefault.hstyle
|
|
278
|
+
if primary.hscale is None:
|
|
279
|
+
primary.hscale = LineArrowStyleDefault.hscale
|
|
280
|
+
if primary.color is None:
|
|
281
|
+
primary.color = LineArrowStyleDefault.color
|
|
282
|
+
if primary.alpha is None:
|
|
283
|
+
primary.alpha = LineArrowStyleDefault.alpha
|
|
284
|
+
|
|
285
|
+
return primary
|
|
286
|
+
|
|
287
|
+
@staticmethod
|
|
288
|
+
def get_fancyarrowpatch_options(
|
|
289
|
+
style: Union[LineStyle, LineArrowStyle, None] = None,
|
|
290
|
+
) -> Dict[str, Any]:
|
|
291
|
+
"""Convert drawlib's LineStyle to matplotlib's line options
|
|
292
|
+
|
|
293
|
+
Matplotlib handles line style at its args.
|
|
294
|
+
This module convert drawlib's LineStyle to dict
|
|
295
|
+
which fit to matplotlib's function.
|
|
296
|
+
|
|
297
|
+
At caller side, please apply options(return value) as
|
|
298
|
+
``Line2D(arg1, ..., **options)``.
|
|
299
|
+
It will apply LineStyle's style to matplotlib's function.
|
|
300
|
+
|
|
301
|
+
Args:
|
|
302
|
+
style: LineStyle or None
|
|
303
|
+
|
|
304
|
+
Returns:
|
|
305
|
+
dict: arg None makes {}. LineStyle makes appropriate options dict.
|
|
306
|
+
|
|
307
|
+
"""
|
|
308
|
+
|
|
309
|
+
if style is None:
|
|
310
|
+
return {"arrowstyle": "-"}
|
|
311
|
+
|
|
312
|
+
color = None if style.color is None else ColorUtil.get_mplot_rgba(style.color)
|
|
313
|
+
|
|
314
|
+
if isinstance(style, LineStyle):
|
|
315
|
+
options = {
|
|
316
|
+
"linewidth": style.width,
|
|
317
|
+
"linestyle": style.style,
|
|
318
|
+
"color": color,
|
|
319
|
+
"alpha": style.alpha,
|
|
320
|
+
"arrowstyle": "-",
|
|
321
|
+
}
|
|
322
|
+
elif isinstance(style, LineArrowStyle):
|
|
323
|
+
options = {
|
|
324
|
+
"linewidth": style.lwidth,
|
|
325
|
+
"linestyle": style.lstyle,
|
|
326
|
+
"color": color,
|
|
327
|
+
"alpha": style.alpha,
|
|
328
|
+
"arrowstyle": style.hstyle,
|
|
329
|
+
"mutation_scale": style.hscale,
|
|
330
|
+
}
|
|
331
|
+
else:
|
|
332
|
+
raise ValueError()
|
|
333
|
+
|
|
334
|
+
return _get_dict_value_none_keys_removed(options)
|
|
335
|
+
|
|
336
|
+
|
|
337
|
+
class ShapeUtil:
|
|
338
|
+
@staticmethod
|
|
339
|
+
def apply_alignment(
|
|
340
|
+
xy: Tuple[float, float],
|
|
341
|
+
width: float,
|
|
342
|
+
height: float,
|
|
343
|
+
angle: Optional[float],
|
|
344
|
+
style: ShapeStyle,
|
|
345
|
+
is_default_center: bool = False,
|
|
346
|
+
) -> Tuple[Tuple[float, float], ShapeStyle]:
|
|
347
|
+
|
|
348
|
+
x, y = xy
|
|
349
|
+
if angle is None:
|
|
350
|
+
if is_default_center:
|
|
351
|
+
if style.halign is None:
|
|
352
|
+
style.halign = "center"
|
|
353
|
+
if style.valign is None:
|
|
354
|
+
style.valign = "center"
|
|
355
|
+
else:
|
|
356
|
+
if style.halign is None:
|
|
357
|
+
style.halign = "left"
|
|
358
|
+
if style.valign is None:
|
|
359
|
+
style.valign = "bottom"
|
|
360
|
+
else:
|
|
361
|
+
if style.halign is None:
|
|
362
|
+
style.halign = "center"
|
|
363
|
+
if style.valign is None:
|
|
364
|
+
style.valign = "center"
|
|
365
|
+
|
|
366
|
+
if is_default_center:
|
|
367
|
+
if style.halign == "left":
|
|
368
|
+
x = x + width / 2
|
|
369
|
+
if style.halign == "right":
|
|
370
|
+
x = x - width / 2
|
|
371
|
+
if style.valign == "bottom":
|
|
372
|
+
y = y + height / 2
|
|
373
|
+
if style.valign == "top":
|
|
374
|
+
y = y - height / 2
|
|
375
|
+
else:
|
|
376
|
+
if style.halign == "center":
|
|
377
|
+
x = x - width / 2
|
|
378
|
+
if style.halign == "right":
|
|
379
|
+
x = x - width
|
|
380
|
+
if style.valign == "center":
|
|
381
|
+
y = y - height / 2
|
|
382
|
+
if style.valign == "top":
|
|
383
|
+
y = y - height
|
|
384
|
+
|
|
385
|
+
return (x, y), style
|
|
386
|
+
|
|
387
|
+
@staticmethod
|
|
388
|
+
def get_shape_text(
|
|
389
|
+
xy: Tuple[float, float],
|
|
390
|
+
angle: Optional[float],
|
|
391
|
+
text: str,
|
|
392
|
+
style: Optional[ShapeTextStyle] = None,
|
|
393
|
+
) -> Text:
|
|
394
|
+
"""Get text object which is drawn inside shape.
|
|
395
|
+
|
|
396
|
+
Few shape objects can have text in its center.
|
|
397
|
+
This function helps creating text object inside shape.
|
|
398
|
+
Specifically, try to align to center of shapes.
|
|
399
|
+
|
|
400
|
+
Args:
|
|
401
|
+
x: center of shape. center of text.
|
|
402
|
+
y: center of shape. center of text.
|
|
403
|
+
text: shown text
|
|
404
|
+
angle(option): angle of shape. angle of text.
|
|
405
|
+
style(option): style of text
|
|
406
|
+
|
|
407
|
+
Returns:
|
|
408
|
+
matplotlib.text.Text: shape center text
|
|
409
|
+
|
|
410
|
+
"""
|
|
411
|
+
|
|
412
|
+
if style is None:
|
|
413
|
+
style = ShapeTextStyle()
|
|
414
|
+
|
|
415
|
+
shape_angle = angle
|
|
416
|
+
if shape_angle is None:
|
|
417
|
+
shape_angle = 0.0
|
|
418
|
+
|
|
419
|
+
if style.angle is not None:
|
|
420
|
+
text_angle = style.angle
|
|
421
|
+
else:
|
|
422
|
+
text_angle = shape_angle
|
|
423
|
+
if style.flip is not None and style.flip:
|
|
424
|
+
text_angle = (text_angle + 180) % 360
|
|
425
|
+
|
|
426
|
+
x, y = xy
|
|
427
|
+
if style.xy_shift is not None:
|
|
428
|
+
x_shift, y_shift = style.xy_shift
|
|
429
|
+
if shape_angle == 0:
|
|
430
|
+
x += x_shift
|
|
431
|
+
y += y_shift
|
|
432
|
+
else:
|
|
433
|
+
angle_rad = math.radians(shape_angle)
|
|
434
|
+
rotated_x_shift = x_shift * math.cos(angle_rad) - y_shift * math.sin(angle_rad)
|
|
435
|
+
rotated_y_shift = x_shift * math.sin(angle_rad) + y_shift * math.cos(angle_rad)
|
|
436
|
+
x += rotated_x_shift
|
|
437
|
+
y += rotated_y_shift
|
|
438
|
+
|
|
439
|
+
# only check color. ignore alignment
|
|
440
|
+
options = TextUtil.get_text_options(style)
|
|
441
|
+
if "horizontalalignment" in options:
|
|
442
|
+
del options["horizontalalignment"]
|
|
443
|
+
if "verticalalignment" in options:
|
|
444
|
+
del options["verticalalignment"]
|
|
445
|
+
|
|
446
|
+
return Text(
|
|
447
|
+
x,
|
|
448
|
+
y,
|
|
449
|
+
text,
|
|
450
|
+
rotation=text_angle,
|
|
451
|
+
rotation_mode="anchor",
|
|
452
|
+
horizontalalignment="center",
|
|
453
|
+
verticalalignment="center",
|
|
454
|
+
fontproperties=TextUtil.get_font_properties(style),
|
|
455
|
+
**options,
|
|
456
|
+
)
|
|
457
|
+
|
|
458
|
+
@staticmethod
|
|
459
|
+
def get_merged_shapestyle(
|
|
460
|
+
primary: Optional[ShapeStyle],
|
|
461
|
+
secondary: ShapeStyle,
|
|
462
|
+
) -> ShapeStyle:
|
|
463
|
+
if primary is None:
|
|
464
|
+
primary = ShapeStyle()
|
|
465
|
+
else:
|
|
466
|
+
primary = deepcopy(primary)
|
|
467
|
+
|
|
468
|
+
if primary.halign is None:
|
|
469
|
+
primary.halign = secondary.halign
|
|
470
|
+
if primary.valign is None:
|
|
471
|
+
primary.valign = secondary.valign
|
|
472
|
+
if primary.lwidth is None:
|
|
473
|
+
primary.lwidth = secondary.lwidth
|
|
474
|
+
if primary.lcolor is None:
|
|
475
|
+
primary.lcolor = secondary.lcolor
|
|
476
|
+
if primary.lstyle is None:
|
|
477
|
+
primary.lstyle = secondary.lstyle
|
|
478
|
+
if primary.fcolor is None:
|
|
479
|
+
primary.fcolor = secondary.fcolor
|
|
480
|
+
if primary.alpha is None:
|
|
481
|
+
primary.alpha = secondary.alpha
|
|
482
|
+
|
|
483
|
+
if primary.halign is None:
|
|
484
|
+
primary.halign = ShapeStyleDefault.halign
|
|
485
|
+
if primary.valign is None:
|
|
486
|
+
primary.valign = ShapeStyleDefault.valign
|
|
487
|
+
if primary.lwidth is None:
|
|
488
|
+
primary.lwidth = ShapeStyleDefault.lwidth
|
|
489
|
+
if primary.lcolor is None:
|
|
490
|
+
primary.lcolor = ShapeStyleDefault.lcolor
|
|
491
|
+
if primary.lstyle is None:
|
|
492
|
+
primary.lstyle = ShapeStyleDefault.lstyle
|
|
493
|
+
if primary.fcolor is None:
|
|
494
|
+
primary.fcolor = ShapeStyleDefault.fcolor
|
|
495
|
+
if primary.alpha is None:
|
|
496
|
+
primary.alpha = ShapeStyleDefault.alpha
|
|
497
|
+
|
|
498
|
+
return primary
|
|
499
|
+
|
|
500
|
+
@staticmethod
|
|
501
|
+
def get_merged_shapetextstyle(
|
|
502
|
+
primary: Optional[ShapeTextStyle],
|
|
503
|
+
secondary: ShapeTextStyle,
|
|
504
|
+
) -> TextStyle:
|
|
505
|
+
if primary is None:
|
|
506
|
+
primary = ShapeTextStyle()
|
|
507
|
+
else:
|
|
508
|
+
primary = deepcopy(primary)
|
|
509
|
+
|
|
510
|
+
if primary.color is None:
|
|
511
|
+
primary.color = secondary.color
|
|
512
|
+
if primary.size is None:
|
|
513
|
+
primary.size = secondary.size
|
|
514
|
+
if primary.halign is None:
|
|
515
|
+
primary.halign = secondary.halign
|
|
516
|
+
if primary.valign is None:
|
|
517
|
+
primary.valign = secondary.valign
|
|
518
|
+
if primary.font is None:
|
|
519
|
+
primary.font = secondary.font
|
|
520
|
+
if primary.alpha is None:
|
|
521
|
+
primary.alpha = secondary.alpha
|
|
522
|
+
|
|
523
|
+
if primary.color is None:
|
|
524
|
+
primary.color = ShapeTextStyleDefault.color
|
|
525
|
+
if primary.size is None:
|
|
526
|
+
primary.size = ShapeTextStyleDefault.size
|
|
527
|
+
if primary.halign is None:
|
|
528
|
+
primary.halign = ShapeTextStyleDefault.halign
|
|
529
|
+
if primary.valign is None:
|
|
530
|
+
primary.valign = ShapeTextStyleDefault.valign
|
|
531
|
+
if primary.font is None:
|
|
532
|
+
primary.font = ShapeTextStyleDefault.font
|
|
533
|
+
if primary.alpha is None:
|
|
534
|
+
primary.alpha = ShapeTextStyleDefault.alpha
|
|
535
|
+
|
|
536
|
+
return primary
|
|
537
|
+
|
|
538
|
+
@staticmethod
|
|
539
|
+
def get_shape_options(
|
|
540
|
+
style: Optional[ShapeStyle] = None,
|
|
541
|
+
default_no_line: bool = True,
|
|
542
|
+
) -> Dict[str, Any]:
|
|
543
|
+
"""Convert drawlib's ShapeStyle to matplotlib's patches(shape) options
|
|
544
|
+
|
|
545
|
+
Matplotlib handles shape style at its args.
|
|
546
|
+
This module convert drawlib's ShapeStyle to dict
|
|
547
|
+
which fit to matplotlib's function.
|
|
548
|
+
|
|
549
|
+
At caller side, please apply options(return value) as
|
|
550
|
+
``Circle(arg1, arg2, **options)``.
|
|
551
|
+
It will apply TextStyle's style to matplotlib's function.
|
|
552
|
+
|
|
553
|
+
Args:
|
|
554
|
+
style: ShapeStyle or None
|
|
555
|
+
|
|
556
|
+
Returns:
|
|
557
|
+
dict: arg None makes {}. ShapeStyle makes appropriate options dict.
|
|
558
|
+
|
|
559
|
+
"""
|
|
560
|
+
|
|
561
|
+
if style is None:
|
|
562
|
+
if default_no_line:
|
|
563
|
+
return {"linewidth": 0}
|
|
564
|
+
return {}
|
|
565
|
+
|
|
566
|
+
lcolor = None if style.lcolor is None else ColorUtil.get_mplot_rgba(style.lcolor)
|
|
567
|
+
fcolor = None if style.fcolor is None else ColorUtil.get_mplot_rgba(style.fcolor)
|
|
568
|
+
|
|
569
|
+
# halign, valign will be used on shape. They are not in options.
|
|
570
|
+
options = {
|
|
571
|
+
"facecolor": fcolor,
|
|
572
|
+
"edgecolor": lcolor,
|
|
573
|
+
"linestyle": style.lstyle,
|
|
574
|
+
"linewidth": style.lwidth,
|
|
575
|
+
"alpha": style.alpha,
|
|
576
|
+
}
|
|
577
|
+
|
|
578
|
+
if options["linewidth"] is None:
|
|
579
|
+
if default_no_line:
|
|
580
|
+
options["linewidth"] = 0
|
|
581
|
+
|
|
582
|
+
return _get_dict_value_none_keys_removed(options)
|
|
583
|
+
|
|
584
|
+
|
|
585
|
+
class TextUtil:
|
|
586
|
+
|
|
587
|
+
@staticmethod
|
|
588
|
+
def get_merged_textstyle(
|
|
589
|
+
primary: Optional[TextStyle],
|
|
590
|
+
secondary: TextStyle,
|
|
591
|
+
) -> TextStyle:
|
|
592
|
+
if primary is None:
|
|
593
|
+
primary = TextStyle()
|
|
594
|
+
else:
|
|
595
|
+
primary = deepcopy(primary)
|
|
596
|
+
|
|
597
|
+
if primary.color is None:
|
|
598
|
+
primary.color = secondary.color
|
|
599
|
+
if primary.size is None:
|
|
600
|
+
primary.size = secondary.size
|
|
601
|
+
if primary.halign is None:
|
|
602
|
+
primary.halign = secondary.halign
|
|
603
|
+
if primary.valign is None:
|
|
604
|
+
primary.valign = secondary.valign
|
|
605
|
+
if primary.font is None:
|
|
606
|
+
primary.font = secondary.font
|
|
607
|
+
if primary.alpha is None:
|
|
608
|
+
primary.alpha = secondary.alpha
|
|
609
|
+
if primary.bgalpha is None:
|
|
610
|
+
primary.bgalpha = secondary.bgalpha
|
|
611
|
+
if primary.bglcolor is None:
|
|
612
|
+
primary.bglcolor = secondary.bglcolor
|
|
613
|
+
if primary.bglstyle is None:
|
|
614
|
+
primary.bglstyle = secondary.bglstyle
|
|
615
|
+
if primary.bglwidth is None:
|
|
616
|
+
primary.bglwidth = secondary.bglwidth
|
|
617
|
+
if primary.bgfcolor is None:
|
|
618
|
+
primary.bgfcolor = secondary.bgfcolor
|
|
619
|
+
|
|
620
|
+
if primary.color is None:
|
|
621
|
+
primary.color = TextStyleDefault.color
|
|
622
|
+
if primary.size is None:
|
|
623
|
+
primary.size = TextStyleDefault.size
|
|
624
|
+
if primary.halign is None:
|
|
625
|
+
primary.halign = TextStyleDefault.halign
|
|
626
|
+
if primary.valign is None:
|
|
627
|
+
primary.valign = TextStyleDefault.valign
|
|
628
|
+
if primary.font is None:
|
|
629
|
+
primary.font = TextStyleDefault.font
|
|
630
|
+
if primary.alpha is None:
|
|
631
|
+
primary.alpha = TextStyleDefault.alpha
|
|
632
|
+
if primary.bgalpha is None:
|
|
633
|
+
primary.bgalpha = TextStyleDefault.bgalpha
|
|
634
|
+
if primary.bgfcolor is None:
|
|
635
|
+
primary.bgfcolor = TextStyleDefault.bgfcolor
|
|
636
|
+
if primary.bglcolor is not None or primary.bglstyle is not None or primary.bglwidth is not None:
|
|
637
|
+
if primary.bglcolor is None:
|
|
638
|
+
primary.bglcolor = TextStyleDefault.bglcolor
|
|
639
|
+
if primary.bglstyle is None:
|
|
640
|
+
primary.bglstyle = TextStyleDefault.bglstyle
|
|
641
|
+
if primary.bglwidth is None:
|
|
642
|
+
primary.bglwidth = TextStyleDefault.bglwidth
|
|
643
|
+
|
|
644
|
+
return primary
|
|
645
|
+
|
|
646
|
+
@staticmethod
|
|
647
|
+
def get_text_options(
|
|
648
|
+
style: Union[TextStyle, ShapeTextStyle, None],
|
|
649
|
+
) -> Dict[str, Any]:
|
|
650
|
+
"""Convert drawlib's TextStyle to matplotlib's text options
|
|
651
|
+
|
|
652
|
+
Matplotlib handles text style at its args.
|
|
653
|
+
This module convert drawlib's TextStyle to dict
|
|
654
|
+
which fit to matplotlib's function.
|
|
655
|
+
|
|
656
|
+
At caller side, please apply options(return value) as
|
|
657
|
+
``Text(arg1, ..., **options)``.
|
|
658
|
+
It will apply TextStyle's style to matplotlib's function.
|
|
659
|
+
|
|
660
|
+
Args:
|
|
661
|
+
style: TextStyle or None
|
|
662
|
+
|
|
663
|
+
Returns:
|
|
664
|
+
dict: arg None makes {}. TextStyle makes appropriate options dict.
|
|
665
|
+
|
|
666
|
+
"""
|
|
667
|
+
|
|
668
|
+
if style is None:
|
|
669
|
+
return {}
|
|
670
|
+
|
|
671
|
+
# convert rgb -> matplot rgb
|
|
672
|
+
color = None if style.color is None else ColorUtil.get_mplot_rgba(style.color)
|
|
673
|
+
|
|
674
|
+
options = {
|
|
675
|
+
"color": color,
|
|
676
|
+
"horizontalalignment": style.halign,
|
|
677
|
+
"verticalalignment": style.valign,
|
|
678
|
+
}
|
|
679
|
+
|
|
680
|
+
return _get_dict_value_none_keys_removed(options)
|
|
681
|
+
|
|
682
|
+
@staticmethod
|
|
683
|
+
def get_font_properties(
|
|
684
|
+
style: Union[TextStyle, ShapeTextStyle, None],
|
|
685
|
+
) -> Optional[FontProperties]:
|
|
686
|
+
"""Create matplotlib's FontProperties from TextStyle
|
|
687
|
+
|
|
688
|
+
Arg:
|
|
689
|
+
style: TextStyle or None
|
|
690
|
+
|
|
691
|
+
Returns:
|
|
692
|
+
Optional[FontProperties]: return None if arg is None.
|
|
693
|
+
|
|
694
|
+
"""
|
|
695
|
+
|
|
696
|
+
if style is None or isinstance(style, TextStyle):
|
|
697
|
+
default = dtheme.textstyle_get()
|
|
698
|
+
if default.size is None:
|
|
699
|
+
default.size = TextStyleDefault.size
|
|
700
|
+
if default.font is None:
|
|
701
|
+
default.font = TextStyleDefault.font
|
|
702
|
+
|
|
703
|
+
if isinstance(style, ShapeTextStyle):
|
|
704
|
+
default = dtheme.shapetextstyle_get()
|
|
705
|
+
if default.size is None:
|
|
706
|
+
default.size = ShapeTextStyleDefault.size
|
|
707
|
+
if default.font is None:
|
|
708
|
+
default.font = ShapeTextStyleDefault.font
|
|
709
|
+
|
|
710
|
+
DEFAULT_SIZE = default.size
|
|
711
|
+
DEFAULT_FONT = default.font
|
|
712
|
+
|
|
713
|
+
# use user font file
|
|
714
|
+
|
|
715
|
+
if isinstance(style.font, FontFile):
|
|
716
|
+
if style.size is None:
|
|
717
|
+
style.size = DEFAULT_SIZE
|
|
718
|
+
return FontProperties(size=style.size, fname=style.font.file)
|
|
719
|
+
|
|
720
|
+
# use default font
|
|
721
|
+
|
|
722
|
+
file_path, download_url, md5_hash = DEFAULT_FONT.value
|
|
723
|
+
download_if_not_exist(file_path=file_path, download_url=download_url, md5_hash=md5_hash)
|
|
724
|
+
|
|
725
|
+
if style is None:
|
|
726
|
+
return FontProperties(size=DEFAULT_SIZE, fname=file_path)
|
|
727
|
+
|
|
728
|
+
if style.font is None:
|
|
729
|
+
if style.size is None:
|
|
730
|
+
style.size = DEFAULT_SIZE
|
|
731
|
+
return FontProperties(size=style.size, fname=file_path)
|
|
732
|
+
|
|
733
|
+
# use specified font
|
|
734
|
+
|
|
735
|
+
if isinstance(style.font, FontBase):
|
|
736
|
+
# get True for all BaseFont subclasses
|
|
737
|
+
if style.size is None:
|
|
738
|
+
style.size = DEFAULT_SIZE
|
|
739
|
+
|
|
740
|
+
file_path, download_url, md5_hash = style.font.value
|
|
741
|
+
download_if_not_exist(file_path=file_path, download_url=download_url, md5_hash=md5_hash)
|
|
742
|
+
return FontProperties(size=style.size, fname=file_path)
|
|
743
|
+
|
|
744
|
+
raise ValueError(f"font type {type(style.font)} is not supported")
|
|
745
|
+
|
|
746
|
+
@staticmethod
|
|
747
|
+
def get_bbox_dict(
|
|
748
|
+
style: Optional[TextStyle] = None,
|
|
749
|
+
) -> Optional[Dict[str, Any]]:
|
|
750
|
+
"""Convert drawlib's TextStyle to matplotlib's text background options.
|
|
751
|
+
|
|
752
|
+
Matplotlib handles text background style at its arg ``bbox`` as dict.
|
|
753
|
+
This module convert drawlib's TextBackgroundStyle to dict
|
|
754
|
+
which fit to matplotlib's function.
|
|
755
|
+
|
|
756
|
+
Note:
|
|
757
|
+
``{}`` doesn't mean no style at matplotlib's bbox.
|
|
758
|
+
If you want have no style, need to apply ``None``.
|
|
759
|
+
|
|
760
|
+
At caller side please provide return value to arg ``bbox``.
|
|
761
|
+
It is not same to other style ways. Please be careful.
|
|
762
|
+
|
|
763
|
+
Args:
|
|
764
|
+
style: TextBackgroundStyle or None
|
|
765
|
+
|
|
766
|
+
Returns:
|
|
767
|
+
Optional[dict]: arg None makes None. TextBackgroundStyle makes appropriate bbox dict.
|
|
768
|
+
|
|
769
|
+
"""
|
|
770
|
+
|
|
771
|
+
# {} doesn't mean no style.
|
|
772
|
+
# requires returning None when no style.
|
|
773
|
+
|
|
774
|
+
if style is None:
|
|
775
|
+
return None
|
|
776
|
+
|
|
777
|
+
all_none = True
|
|
778
|
+
for e in [style.bgfcolor, style.bglcolor, style.bglstyle]:
|
|
779
|
+
if e is not None:
|
|
780
|
+
all_none = False
|
|
781
|
+
if all_none:
|
|
782
|
+
return None
|
|
783
|
+
|
|
784
|
+
# background exist
|
|
785
|
+
|
|
786
|
+
lcolor = None if style.bglcolor is None else ColorUtil.get_mplot_rgba(style.bglcolor)
|
|
787
|
+
fcolor = None if style.bgfcolor is None else ColorUtil.get_mplot_rgba(style.bgfcolor)
|
|
788
|
+
|
|
789
|
+
if lcolor is None:
|
|
790
|
+
lcolor = Colors.Transparent
|
|
791
|
+
if fcolor is None:
|
|
792
|
+
fcolor = Colors.Transparent
|
|
793
|
+
|
|
794
|
+
bbox_dict = {
|
|
795
|
+
"boxstyle": "square",
|
|
796
|
+
"facecolor": fcolor,
|
|
797
|
+
"edgecolor": lcolor,
|
|
798
|
+
"linestyle": style.bglstyle,
|
|
799
|
+
"linewidth": style.bglwidth,
|
|
800
|
+
"alpha": style.bgalpha,
|
|
801
|
+
}
|
|
802
|
+
if bbox_dict["linewidth"] is None:
|
|
803
|
+
bbox_dict["linewidth"] = 0
|
|
804
|
+
|
|
805
|
+
return _get_dict_value_none_keys_removed(bbox_dict)
|
|
806
|
+
|
|
807
|
+
|
|
808
|
+
def _get_dict_value_none_keys_removed(options: Dict[str, Any]) -> Dict[str, Any]:
|
|
809
|
+
return {key: value for key, value in options.items() if value is not None}
|