bluer-objects 6.3.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 bluer-objects might be problematic. Click here for more details.

Files changed (149) hide show
  1. bluer_objects/.abcli/abcli.sh +9 -0
  2. bluer_objects/.abcli/actions.sh +11 -0
  3. bluer_objects/.abcli/aka.sh +3 -0
  4. bluer_objects/.abcli/alias.sh +36 -0
  5. bluer_objects/.abcli/blue_objects.sh +11 -0
  6. bluer_objects/.abcli/cache.sh +5 -0
  7. bluer_objects/.abcli/clone.sh +94 -0
  8. bluer_objects/.abcli/download.sh +53 -0
  9. bluer_objects/.abcli/file.sh +8 -0
  10. bluer_objects/.abcli/gif.sh +27 -0
  11. bluer_objects/.abcli/host.sh +29 -0
  12. bluer_objects/.abcli/ls.sh +24 -0
  13. bluer_objects/.abcli/metadata/get.sh +24 -0
  14. bluer_objects/.abcli/metadata/post.sh +22 -0
  15. bluer_objects/.abcli/metadata.sh +16 -0
  16. bluer_objects/.abcli/mlflow/browse.sh +36 -0
  17. bluer_objects/.abcli/mlflow/cache.sh +31 -0
  18. bluer_objects/.abcli/mlflow/list_registered_models.sh +9 -0
  19. bluer_objects/.abcli/mlflow/log_artifacts.sh +10 -0
  20. bluer_objects/.abcli/mlflow/log_run.sh +10 -0
  21. bluer_objects/.abcli/mlflow/run.sh +11 -0
  22. bluer_objects/.abcli/mlflow/tags/clone.sh +15 -0
  23. bluer_objects/.abcli/mlflow/tags/get.sh +10 -0
  24. bluer_objects/.abcli/mlflow/tags/search.sh +12 -0
  25. bluer_objects/.abcli/mlflow/tags/set.sh +13 -0
  26. bluer_objects/.abcli/mlflow/tags.sh +16 -0
  27. bluer_objects/.abcli/mlflow/test.sh +11 -0
  28. bluer_objects/.abcli/mlflow/transition.sh +20 -0
  29. bluer_objects/.abcli/mlflow.sh +29 -0
  30. bluer_objects/.abcli/mysql/cache.sh +65 -0
  31. bluer_objects/.abcli/mysql/relations.sh +83 -0
  32. bluer_objects/.abcli/mysql/tags.sh +85 -0
  33. bluer_objects/.abcli/mysql.sh +16 -0
  34. bluer_objects/.abcli/object.sh +54 -0
  35. bluer_objects/.abcli/publish.sh +58 -0
  36. bluer_objects/.abcli/select.sh +34 -0
  37. bluer_objects/.abcli/storage/clear.sh +45 -0
  38. bluer_objects/.abcli/storage/download_file.sh +9 -0
  39. bluer_objects/.abcli/storage/exists.sh +8 -0
  40. bluer_objects/.abcli/storage/list.sh +8 -0
  41. bluer_objects/.abcli/storage/rm.sh +11 -0
  42. bluer_objects/.abcli/storage/status.sh +11 -0
  43. bluer_objects/.abcli/storage.sh +15 -0
  44. bluer_objects/.abcli/tags.sh +5 -0
  45. bluer_objects/.abcli/tests/README.sh +8 -0
  46. bluer_objects/.abcli/tests/clone.sh +32 -0
  47. bluer_objects/.abcli/tests/help.sh +85 -0
  48. bluer_objects/.abcli/tests/host.sh +7 -0
  49. bluer_objects/.abcli/tests/ls.sh +13 -0
  50. bluer_objects/.abcli/tests/metadata.sh +53 -0
  51. bluer_objects/.abcli/tests/mlflow_cache.sh +14 -0
  52. bluer_objects/.abcli/tests/mlflow_logging.sh +12 -0
  53. bluer_objects/.abcli/tests/mlflow_tags.sh +29 -0
  54. bluer_objects/.abcli/tests/mlflow_test.sh +7 -0
  55. bluer_objects/.abcli/tests/mysql_cache.sh +15 -0
  56. bluer_objects/.abcli/tests/mysql_relations.sh +20 -0
  57. bluer_objects/.abcli/tests/mysql_tags.sh +16 -0
  58. bluer_objects/.abcli/tests/test_gif.sh +13 -0
  59. bluer_objects/.abcli/tests/version.sh +10 -0
  60. bluer_objects/.abcli/upload.sh +73 -0
  61. bluer_objects/README/__init__.py +29 -0
  62. bluer_objects/README/functions.py +285 -0
  63. bluer_objects/README/items.py +30 -0
  64. bluer_objects/__init__.py +19 -0
  65. bluer_objects/__main__.py +16 -0
  66. bluer_objects/config.env +22 -0
  67. bluer_objects/env.py +72 -0
  68. bluer_objects/file/__init__.py +41 -0
  69. bluer_objects/file/__main__.py +51 -0
  70. bluer_objects/file/classes.py +38 -0
  71. bluer_objects/file/functions.py +290 -0
  72. bluer_objects/file/load.py +219 -0
  73. bluer_objects/file/save.py +280 -0
  74. bluer_objects/graphics/__init__.py +4 -0
  75. bluer_objects/graphics/__main__.py +84 -0
  76. bluer_objects/graphics/frame.py +15 -0
  77. bluer_objects/graphics/gif.py +86 -0
  78. bluer_objects/graphics/screen.py +63 -0
  79. bluer_objects/graphics/signature.py +97 -0
  80. bluer_objects/graphics/text.py +165 -0
  81. bluer_objects/help/__init__.py +0 -0
  82. bluer_objects/help/__main__.py +10 -0
  83. bluer_objects/help/functions.py +5 -0
  84. bluer_objects/host/__init__.py +1 -0
  85. bluer_objects/host/__main__.py +84 -0
  86. bluer_objects/host/functions.py +66 -0
  87. bluer_objects/logger/__init__.py +4 -0
  88. bluer_objects/logger/matrix.py +209 -0
  89. bluer_objects/markdown.py +43 -0
  90. bluer_objects/metadata/__init__.py +8 -0
  91. bluer_objects/metadata/__main__.py +110 -0
  92. bluer_objects/metadata/enums.py +29 -0
  93. bluer_objects/metadata/get.py +89 -0
  94. bluer_objects/metadata/post.py +101 -0
  95. bluer_objects/mlflow/__init__.py +28 -0
  96. bluer_objects/mlflow/__main__.py +271 -0
  97. bluer_objects/mlflow/cache.py +13 -0
  98. bluer_objects/mlflow/logging.py +81 -0
  99. bluer_objects/mlflow/models.py +57 -0
  100. bluer_objects/mlflow/objects.py +76 -0
  101. bluer_objects/mlflow/runs.py +100 -0
  102. bluer_objects/mlflow/tags.py +90 -0
  103. bluer_objects/mlflow/testing.py +39 -0
  104. bluer_objects/mysql/cache/__init__.py +8 -0
  105. bluer_objects/mysql/cache/__main__.py +91 -0
  106. bluer_objects/mysql/cache/functions.py +181 -0
  107. bluer_objects/mysql/relations/__init__.py +9 -0
  108. bluer_objects/mysql/relations/__main__.py +138 -0
  109. bluer_objects/mysql/relations/functions.py +180 -0
  110. bluer_objects/mysql/table.py +144 -0
  111. bluer_objects/mysql/tags/__init__.py +1 -0
  112. bluer_objects/mysql/tags/__main__.py +130 -0
  113. bluer_objects/mysql/tags/functions.py +203 -0
  114. bluer_objects/objects.py +167 -0
  115. bluer_objects/path.py +194 -0
  116. bluer_objects/sample.env +16 -0
  117. bluer_objects/storage/__init__.py +3 -0
  118. bluer_objects/storage/__main__.py +114 -0
  119. bluer_objects/storage/classes.py +237 -0
  120. bluer_objects/tests/__init__.py +0 -0
  121. bluer_objects/tests/test_README.py +5 -0
  122. bluer_objects/tests/test_env.py +27 -0
  123. bluer_objects/tests/test_file_load_save.py +105 -0
  124. bluer_objects/tests/test_fullname.py +5 -0
  125. bluer_objects/tests/test_graphics.py +28 -0
  126. bluer_objects/tests/test_graphics_frame.py +11 -0
  127. bluer_objects/tests/test_graphics_gif.py +29 -0
  128. bluer_objects/tests/test_graphics_screen.py +8 -0
  129. bluer_objects/tests/test_graphics_signature.py +80 -0
  130. bluer_objects/tests/test_graphics_text.py +14 -0
  131. bluer_objects/tests/test_logger.py +5 -0
  132. bluer_objects/tests/test_logger_matrix.py +73 -0
  133. bluer_objects/tests/test_markdown.py +10 -0
  134. bluer_objects/tests/test_metadata.py +204 -0
  135. bluer_objects/tests/test_mlflow.py +60 -0
  136. bluer_objects/tests/test_mysql_cache.py +14 -0
  137. bluer_objects/tests/test_mysql_relations.py +16 -0
  138. bluer_objects/tests/test_mysql_table.py +9 -0
  139. bluer_objects/tests/test_mysql_tags.py +13 -0
  140. bluer_objects/tests/test_objects.py +180 -0
  141. bluer_objects/tests/test_path.py +7 -0
  142. bluer_objects/tests/test_storage.py +7 -0
  143. bluer_objects/tests/test_version.py +5 -0
  144. bluer_objects/urls.py +3 -0
  145. bluer_objects-6.3.1.dist-info/METADATA +57 -0
  146. bluer_objects-6.3.1.dist-info/RECORD +149 -0
  147. bluer_objects-6.3.1.dist-info/WHEEL +5 -0
  148. bluer_objects-6.3.1.dist-info/licenses/LICENSE +121 -0
  149. bluer_objects-6.3.1.dist-info/top_level.txt +1 -0
@@ -0,0 +1,280 @@
1
+ from typing import Any, Dict, List
2
+ import yaml
3
+ import numpy as np
4
+ import json
5
+ import dill
6
+ import pandas as pd
7
+
8
+ from blueness import module
9
+ from blue_options.logger import crash_report
10
+ from blue_options import string
11
+ from blue_options.host import is_jupyter
12
+
13
+ from bluer_objects import NAME
14
+ from bluer_objects.file.classes import JsonEncoder
15
+ from bluer_objects.file.functions import path as file_path
16
+ from bluer_objects.file.load import load_text
17
+ from bluer_objects.path import create as path_create
18
+ from bluer_objects.logger import logger
19
+
20
+
21
+ NAME = module.name(__file__, NAME)
22
+
23
+
24
+ def prepare_for_saving(
25
+ filename: str,
26
+ ) -> bool:
27
+ return path_create(file_path(filename))
28
+
29
+
30
+ def finish_saving(
31
+ success: bool,
32
+ message: str,
33
+ log: bool = True,
34
+ ) -> bool:
35
+ if not success:
36
+ crash_report(f"{message}: failed.")
37
+ elif log:
38
+ logger.info(message)
39
+
40
+ return success
41
+
42
+
43
+ def save(
44
+ filename: str,
45
+ data: Any,
46
+ log: bool = False,
47
+ ) -> bool:
48
+ if not prepare_for_saving(filename):
49
+ return False
50
+
51
+ success = True
52
+ try:
53
+ with open(filename, "wb") as fp:
54
+ dill.dump(data, fp)
55
+ except:
56
+ success = False
57
+
58
+ return finish_saving(
59
+ success,
60
+ "{}.save: {} -> {}".format(
61
+ NAME,
62
+ type(data),
63
+ filename,
64
+ ),
65
+ log,
66
+ )
67
+
68
+
69
+ def save_csv(
70
+ filename: str,
71
+ df: pd.DataFrame,
72
+ log: bool = False,
73
+ ):
74
+ if not prepare_for_saving(filename):
75
+ return False
76
+
77
+ success = True
78
+ # https://stackoverflow.com/a/10250924/10917551
79
+ try:
80
+ df.to_csv(filename)
81
+ except:
82
+ success = False
83
+
84
+ return finish_saving(
85
+ success,
86
+ "{}.save_csv: {:,}X[{}] -> {}".format(
87
+ NAME,
88
+ len(df),
89
+ ",".join(list(df.columns)),
90
+ filename,
91
+ ),
92
+ log,
93
+ )
94
+
95
+
96
+ def save_fig(
97
+ filename: str,
98
+ log: bool = False,
99
+ ):
100
+ if not prepare_for_saving(filename):
101
+ return False
102
+
103
+ success = True
104
+ # https://stackoverflow.com/a/10250924/10917551
105
+ try:
106
+ import matplotlib.pyplot as plt
107
+
108
+ if is_jupyter():
109
+ plt.show()
110
+ plt.savefig(filename, bbox_inches="tight")
111
+ plt.close()
112
+ except:
113
+ success = False
114
+
115
+ return finish_saving(
116
+ success,
117
+ f"{NAME}.save_fig -> {filename}",
118
+ log,
119
+ )
120
+
121
+
122
+ def save_image(
123
+ filename: str,
124
+ image: np.ndarray,
125
+ log: bool = False,
126
+ ):
127
+ import cv2
128
+
129
+ if not prepare_for_saving(filename):
130
+ return False
131
+
132
+ success = True
133
+ try:
134
+ data = image.copy()
135
+
136
+ if len(data.shape) == 3:
137
+ data = np.flip(data, axis=2)
138
+
139
+ cv2.imwrite(filename, data)
140
+ except:
141
+ success = False
142
+
143
+ return finish_saving(
144
+ success,
145
+ "{}.save_image: {} -> {}".format(
146
+ NAME,
147
+ string.pretty_shape_of_matrix(image),
148
+ filename,
149
+ ),
150
+ log,
151
+ )
152
+
153
+
154
+ def save_json(
155
+ filename: str,
156
+ data: Any,
157
+ log: bool = False,
158
+ ):
159
+ if not prepare_for_saving(filename):
160
+ return False
161
+
162
+ success = True
163
+ try:
164
+ if hasattr(data, "to_json"):
165
+ data = data.to_json()
166
+
167
+ with open(filename, "w") as fh:
168
+ json.dump(
169
+ data,
170
+ fh,
171
+ sort_keys=True,
172
+ cls=JsonEncoder,
173
+ indent=4,
174
+ ensure_ascii=False,
175
+ )
176
+ except:
177
+ success = False
178
+
179
+ return finish_saving(
180
+ success,
181
+ "{}.save_json -> {}".format(
182
+ NAME,
183
+ filename,
184
+ ),
185
+ log,
186
+ )
187
+
188
+
189
+ def save_matrix(
190
+ filename: str,
191
+ matrix: np.ndarray,
192
+ log: bool = True,
193
+ ) -> bool:
194
+ if not prepare_for_saving(filename):
195
+ return False
196
+
197
+ success = True
198
+ try:
199
+ np.save(filename, matrix)
200
+ except:
201
+ success = False
202
+
203
+ return finish_saving(
204
+ success,
205
+ "{}.save_matrix({}) -> {}".format(
206
+ NAME,
207
+ string.pretty_shape_of_matrix(matrix),
208
+ filename,
209
+ ),
210
+ log,
211
+ )
212
+
213
+
214
+ def save_text(
215
+ filename: str,
216
+ text: List[str],
217
+ if_different: bool = False,
218
+ log: bool = False,
219
+ remove_empty_lines: bool = False,
220
+ ) -> bool:
221
+ if remove_empty_lines:
222
+ text = [
223
+ line
224
+ for line, next_line in zip(text, text[1:] + ["x"])
225
+ if line.strip() or next_line.strip()
226
+ ]
227
+
228
+ if if_different:
229
+ _, content = load_text(filename, ignore_error=True)
230
+
231
+ if "|".join([line for line in content if line]) == "|".join(
232
+ [line for line in text if line]
233
+ ):
234
+ return True
235
+
236
+ if not prepare_for_saving(filename):
237
+ return False
238
+
239
+ success = True
240
+ try:
241
+ with open(filename, "w") as fp:
242
+ fp.writelines([string + "\n" for string in text])
243
+ except:
244
+ success = False
245
+
246
+ return finish_saving(
247
+ success,
248
+ "{}.save_text: {:,} line(s) -> {}".format(
249
+ NAME,
250
+ len(text),
251
+ filename,
252
+ ),
253
+ log,
254
+ )
255
+
256
+
257
+ def save_yaml(
258
+ filename: str,
259
+ data: Dict,
260
+ log=True,
261
+ ):
262
+ if not prepare_for_saving(filename):
263
+ return False
264
+
265
+ success = True
266
+ try:
267
+ with open(filename, "w") as f:
268
+ yaml.dump(data, f)
269
+ except:
270
+ success = False
271
+
272
+ return finish_saving(
273
+ success,
274
+ "{}.save_yaml: {} -> {}.".format(
275
+ NAME,
276
+ ", ".join(data.keys()),
277
+ filename,
278
+ ),
279
+ log,
280
+ )
@@ -0,0 +1,4 @@
1
+ from bluer_objects.graphics.frame import add_frame
2
+ from bluer_objects.graphics.screen import get_size
3
+ from bluer_objects.graphics.signature import add_signature
4
+ from bluer_objects.graphics.text import render_text
@@ -0,0 +1,84 @@
1
+ import argparse
2
+ import glob
3
+
4
+ from blueness import module
5
+ from blueness.argparse.generic import sys_exit
6
+
7
+ from bluer_objects import NAME, objects
8
+ from bluer_objects.graphics.gif import generate_animated_gif
9
+ from bluer_objects.graphics import screen
10
+ from bluer_objects.logger import logger
11
+
12
+ NAME = module.name(__file__, NAME)
13
+
14
+
15
+ parser = argparse.ArgumentParser(NAME)
16
+ parser.add_argument(
17
+ "task",
18
+ type=str,
19
+ default="",
20
+ help="generate_animated_gif|get_screen_size",
21
+ )
22
+ parser.add_argument(
23
+ "--object_name",
24
+ type=str,
25
+ )
26
+ parser.add_argument(
27
+ "--suffix",
28
+ default=".png",
29
+ type=str,
30
+ )
31
+ parser.add_argument(
32
+ "--output_filename",
33
+ default="",
34
+ type=str,
35
+ help="blank: <object-name>.gif",
36
+ )
37
+ parser.add_argument(
38
+ "--frame_duration",
39
+ default=150,
40
+ type=int,
41
+ help="ms",
42
+ )
43
+ parser.add_argument(
44
+ "--scale",
45
+ default=1,
46
+ type=int,
47
+ )
48
+
49
+ args = parser.parse_args()
50
+
51
+ success = False
52
+ if args.task == "generate_animated_gif":
53
+ success = generate_animated_gif(
54
+ list_of_images=sorted(
55
+ list(
56
+ glob.glob(
57
+ objects.path_of(
58
+ f"*{args.suffix}",
59
+ args.object_name,
60
+ )
61
+ )
62
+ )
63
+ ),
64
+ output_filename=objects.path_of(
65
+ filename=(
66
+ args.output_filename
67
+ if args.output_filename
68
+ else "{}{}.gif".format(
69
+ args.object_name,
70
+ f"-{args.scale}X" if args.scale != 1 else "",
71
+ )
72
+ ),
73
+ object_name=args.object_name,
74
+ ),
75
+ frame_duration=args.frame_duration,
76
+ scale=args.scale,
77
+ )
78
+ elif args.task == "get_screen_size":
79
+ success = True
80
+ print("x".join([str(value) for value in screen.get_size()]))
81
+ else:
82
+ success = None
83
+
84
+ sys_exit(logger, NAME, args.task, success)
@@ -0,0 +1,15 @@
1
+ import numpy as np
2
+
3
+
4
+ def add_frame(
5
+ martrix: np.ndarray,
6
+ width: int,
7
+ ) -> np.ndarray:
8
+ output = np.zeros(
9
+ (martrix.shape[0] + 2 * width, martrix.shape[1] + 2 * width, martrix.shape[2]),
10
+ dtype=martrix.dtype,
11
+ )
12
+
13
+ output[width:-width, width:-width, :] = martrix[:, :, :]
14
+
15
+ return output
@@ -0,0 +1,86 @@
1
+ from PIL import Image
2
+ from tqdm import tqdm
3
+ from typing import List
4
+
5
+ from blueness import module
6
+ from blue_options.logger import crash_report
7
+
8
+ from bluer_objects import NAME
9
+ from bluer_objects.logger import logger
10
+
11
+
12
+ NAME = module.name(__file__, NAME)
13
+
14
+
15
+ def generate_animated_gif(
16
+ list_of_images: List[str],
17
+ output_filename: str,
18
+ frame_duration: int = 150,
19
+ scale: int = 1,
20
+ log: bool = True,
21
+ ) -> bool:
22
+ if not list_of_images:
23
+ return True
24
+
25
+ max_width = 0
26
+ max_height = 0
27
+ frames = []
28
+ for filename in tqdm(list_of_images):
29
+ image = Image.open(filename)
30
+
31
+ frames.append(image)
32
+
33
+ width, height = image.size
34
+ max_width = max(max_width, width)
35
+ max_height = max(max_height, height)
36
+
37
+ padded_frames = []
38
+ for image in frames:
39
+ padded_image = Image.new(
40
+ "RGB",
41
+ (max_width, max_height),
42
+ (255, 255, 255),
43
+ )
44
+
45
+ width, height = image.size
46
+ left_pad = (max_width - width) // 2
47
+ top_pad = (max_height - height) // 2
48
+ padded_image.paste(image, (left_pad, top_pad))
49
+
50
+ if scale != 1:
51
+ padded_image = padded_image.resize(
52
+ (max_width // scale, max_height // scale),
53
+ Image.Resampling.LANCZOS,
54
+ )
55
+
56
+ padded_frames.append(padded_image)
57
+
58
+ success = True
59
+ try:
60
+ padded_frames[0].save(
61
+ output_filename,
62
+ save_all=True,
63
+ append_images=padded_frames[1:],
64
+ duration=frame_duration,
65
+ loop=0, # 0 means infinite loop
66
+ )
67
+ except Exception:
68
+ success = False
69
+
70
+ message = "{}.generate_animated_gif({}x{}x{}) -scale={}-> {} @ {:.2f}ms".format(
71
+ NAME,
72
+ len(list_of_images),
73
+ height,
74
+ width,
75
+ scale,
76
+ output_filename,
77
+ frame_duration,
78
+ )
79
+
80
+ if success:
81
+ if log:
82
+ logger.info(message)
83
+ return True
84
+
85
+ crash_report(message)
86
+ return False
@@ -0,0 +1,63 @@
1
+ import os
2
+ from typing import Tuple
3
+
4
+ from blueness import module
5
+ from blue_options.host.functions import (
6
+ is_rpi,
7
+ is_headless,
8
+ is_mac,
9
+ is_ec2,
10
+ is_docker,
11
+ is_github_workflow,
12
+ is_jupyter,
13
+ is_aws_batch,
14
+ )
15
+
16
+ from bluer_objects import NAME
17
+ from bluer_objects.host import shell
18
+ from bluer_objects.logger import logger
19
+
20
+ NAME = module.name(__file__, NAME)
21
+
22
+
23
+ def get_size() -> Tuple[int, int]:
24
+ screen_height = 480
25
+ screen_width = 640
26
+
27
+ if is_rpi() and not is_headless():
28
+ try:
29
+ # https://stackoverflow.com/a/14124257
30
+ screen = os.popen("xrandr -q -d :0").readlines()[0]
31
+ screen_width = int(screen.split()[7])
32
+ screen_height = int(screen.split()[9][:-1])
33
+ except Exception as e:
34
+ logger.error(f"{NAME}: Failed: {e}.")
35
+ elif is_mac():
36
+ success, output = shell(
37
+ "system_profiler SPDisplaysDataType | grep Resolution",
38
+ clean_after=True,
39
+ return_output=True,
40
+ )
41
+ output = [thing for thing in output if thing]
42
+ if success and output:
43
+ screen_width, screen_height = [
44
+ int(thing) for thing in output[-1].split() if thing.isnumeric()
45
+ ]
46
+ elif (
47
+ not is_ec2()
48
+ and not is_docker()
49
+ and not is_github_workflow()
50
+ and not is_jupyter()
51
+ and not is_aws_batch()
52
+ ):
53
+ try:
54
+ from gi.repository import Gdk # type: ignore
55
+
56
+ screen = Gdk.Screen.get_default()
57
+ geo = screen.get_monitor_geometry(screen.get_primary_monitor())
58
+ screen_width = geo.width
59
+ screen_height = geo.height
60
+ except Exception as e:
61
+ logger.error(f"{NAME}: Failed: {e}.")
62
+
63
+ return screen_height, screen_width
@@ -0,0 +1,97 @@
1
+ from typing import List, Union
2
+ import numpy as np
3
+ from functools import reduce
4
+ import textwrap
5
+
6
+ from blueness import module
7
+
8
+ from bluer_objects import NAME
9
+ from bluer_objects import file
10
+ from bluer_objects.graphics.text import render_text
11
+ from bluer_objects.logger import logger
12
+
13
+ NAME = module.name(__file__, NAME)
14
+
15
+
16
+ def justify_text(
17
+ text: Union[List[str], str],
18
+ line_width: int = 80,
19
+ return_str: bool = False,
20
+ ) -> Union[List[str], str]:
21
+ output = reduce(
22
+ lambda x, y: x + y,
23
+ [
24
+ textwrap.wrap(line, width=line_width)
25
+ for line in (text if isinstance(text, list) else [text])
26
+ ],
27
+ )
28
+
29
+ return "\n".join(output) if return_str else output
30
+
31
+
32
+ def add_signature(
33
+ image: np.ndarray,
34
+ header: List[str],
35
+ footer: List[str] = [],
36
+ word_wrap: bool = True,
37
+ line_width: int = 80,
38
+ ) -> np.ndarray:
39
+ if image is None or not image.shape:
40
+ return image
41
+
42
+ if word_wrap:
43
+ header = justify_text(header, line_width=line_width)
44
+ footer = justify_text(footer, line_width=line_width)
45
+
46
+ justify_line = lambda line: (
47
+ line if len(line) >= line_width else line + (line_width - len(line)) * " "
48
+ )
49
+
50
+ color_depth = image.shape[2] if len(image.shape) >= 3 else 1
51
+
52
+ return np.concatenate(
53
+ [
54
+ render_text(
55
+ text=justify_line(line),
56
+ image_width=image.shape[1],
57
+ color_depth=color_depth,
58
+ )
59
+ for line in header
60
+ ]
61
+ + [image]
62
+ + [
63
+ render_text(
64
+ text=justify_line(line),
65
+ image_width=image.shape[1],
66
+ color_depth=color_depth,
67
+ )
68
+ for line in footer
69
+ ],
70
+ axis=0,
71
+ )
72
+
73
+
74
+ def sign_filename(
75
+ filename: str,
76
+ header: List[str],
77
+ footer: List[str],
78
+ line_width: int = 80,
79
+ ) -> bool:
80
+ success, image = file.load_image(filename)
81
+ if not success:
82
+ return success
83
+
84
+ if not file.save_image(
85
+ filename,
86
+ add_signature(
87
+ image,
88
+ header=[" | ".join(header)],
89
+ footer=[" | ".join(footer)],
90
+ line_width=line_width,
91
+ ),
92
+ ):
93
+ return False
94
+
95
+ logger.info("-> {}".format(filename))
96
+
97
+ return True