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.

Files changed (52) hide show
  1. drawlib/.pylintrc +2 -0
  2. drawlib/__init__.py +33 -0
  3. drawlib/__main__.py +21 -0
  4. drawlib/apis.py +22 -0
  5. drawlib/assets/__init__.py +8 -0
  6. drawlib/assets/v0_1/__init__.py +8 -0
  7. drawlib/assets/v0_1/fonticons/__init__.py +8 -0
  8. drawlib/assets/v0_1/fonts/__init__.py +8 -0
  9. drawlib/v0_1/__init__.py +31 -0
  10. drawlib/v0_1/__main__.py +20 -0
  11. drawlib/v0_1/apis.py +127 -0
  12. drawlib/v0_1/private/__init__.py +20 -0
  13. drawlib/v0_1/private/arg_validator.py +163 -0
  14. drawlib/v0_1/private/command.py +204 -0
  15. drawlib/v0_1/private/core/__init__.py +15 -0
  16. drawlib/v0_1/private/core/colors.py +285 -0
  17. drawlib/v0_1/private/core/dimage.py +510 -0
  18. drawlib/v0_1/private/core/fonts.py +324 -0
  19. drawlib/v0_1/private/core/model.py +1082 -0
  20. drawlib/v0_1/private/core/model_validator.py +175 -0
  21. drawlib/v0_1/private/core/theme.py +1042 -0
  22. drawlib/v0_1/private/core/util.py +809 -0
  23. drawlib/v0_1/private/core_canvas/__init__.py +15 -0
  24. drawlib/v0_1/private/core_canvas/base.py +653 -0
  25. drawlib/v0_1/private/core_canvas/canvas.py +88 -0
  26. drawlib/v0_1/private/core_canvas/image.py +154 -0
  27. drawlib/v0_1/private/core_canvas/line.py +263 -0
  28. drawlib/v0_1/private/core_canvas/original_arrow.py +139 -0
  29. drawlib/v0_1/private/core_canvas/original_polygon.py +584 -0
  30. drawlib/v0_1/private/core_canvas/patches.py +547 -0
  31. drawlib/v0_1/private/core_canvas/text.py +136 -0
  32. drawlib/v0_1/private/download.py +58 -0
  33. drawlib/v0_1/private/dutil.py +28 -0
  34. drawlib/v0_1/private/icons/__init__.py +15 -0
  35. drawlib/v0_1/private/icons/phosphor.py +35275 -0
  36. drawlib/v0_1/private/icons/util.py +82 -0
  37. drawlib/v0_1/private/logging.py +38 -0
  38. drawlib/v0_1/private/settings.py +233 -0
  39. drawlib/v0_1/private/smartarts/__init__.py +15 -0
  40. drawlib/v0_1/private/smartarts/cycle.py +11 -0
  41. drawlib/v0_1/private/smartarts/dsart.py +17 -0
  42. drawlib/v0_1/private/smartarts/groups.py +20 -0
  43. drawlib/v0_1/private/smartarts/pyramid.py +18 -0
  44. drawlib/v0_1/private/smartarts/sourcecode.py +216 -0
  45. drawlib/v0_1/private/smartarts/table.py +11 -0
  46. drawlib/v0_1/private/smartarts/tree.py +18 -0
  47. drawlib/v0_1/private/tools.py +161 -0
  48. drawlib/v0_1/private/util.py +353 -0
  49. drawlib-0.1.1.dist-info/LICENSE.txt +202 -0
  50. drawlib-0.1.1.dist-info/METADATA +36 -0
  51. drawlib-0.1.1.dist-info/RECORD +52 -0
  52. 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}