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,510 @@
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
+ """drawlib image class
11
+
12
+ drawlib depends on ``PIL.Image.Image`` for drawing image.
13
+ It is very useful and easy to use.
14
+ However, drawlib want to let user handle image **very very** easy.
15
+ This module and its class ``Pimage`` achieves it.
16
+
17
+ """
18
+
19
+ from __future__ import annotations
20
+
21
+ import os
22
+ from typing import Union, Tuple, Dict, cast
23
+ from PIL import (
24
+ Image,
25
+ ImageFilter,
26
+ ImageOps,
27
+ ImageEnhance,
28
+ ImageChops,
29
+ )
30
+ from drawlib.v0_1.private.util import (
31
+ error_handler,
32
+ get_script_relative_path,
33
+ )
34
+
35
+
36
+ class Dimage:
37
+ """drawlib image class
38
+
39
+ Providing very easy methods for read/write and applying effects to image.
40
+ This class is wrapper of ``PIL.Image.Image``.
41
+ So, you can get/set PIL image from this class.
42
+ If you needs apply advanced effects, use PIL Image instead.
43
+
44
+ """
45
+
46
+ _cache: Dict[str, Dimage] = {}
47
+
48
+ @error_handler
49
+ def __init__(
50
+ self,
51
+ image: Union[str, Image.Image, Dimage],
52
+ copy: bool = False,
53
+ ):
54
+ """Initialize Pimage from file, PIL Image, Pimage.
55
+
56
+ Initialize Pimage from file path, PIL Image, Pimage.
57
+
58
+ Note:
59
+ File path is relative to user code which calls ``Pimage()``.
60
+ Not from where you call python (default path behavior).
61
+ If you want to have normal path behavior,
62
+ convert relative path to absolute path.
63
+ And then, pass to this class.
64
+
65
+ """
66
+
67
+ # create new PIL.Image instance
68
+ if isinstance(image, str):
69
+ image_str = cast(str, image) # for mypy check
70
+ image_str = get_script_relative_path(image_str)
71
+ if not os.path.exists(image_str):
72
+ raise FileNotFoundError(f'file "{image_str}" does not exist.')
73
+ self._pilimg = Image.open(image_str)
74
+
75
+ elif isinstance(image, Image.Image):
76
+ if copy:
77
+ self._pilimg = image.copy()
78
+ else:
79
+ self._pilimg = image
80
+
81
+ elif isinstance(image, Dimage):
82
+ if copy:
83
+ self._pilimg = image._pilimg.copy()
84
+ else:
85
+ self._pilimg = image._pilimg
86
+
87
+ else:
88
+ raise ValueError(f'Pimage does not support type "{type(image)}".')
89
+
90
+ @error_handler
91
+ def get_pil_image(self) -> Image.Image:
92
+ """Get **copied** PIL Image.
93
+
94
+ Get **copied** PIL Image which Pimage instance holds.
95
+ Modification to the PIL Image has no effects to Pimage.
96
+
97
+ Returns:
98
+ PIL.Image.Image: copy of PIL Image.
99
+
100
+ """
101
+
102
+ return self._pilimg.copy()
103
+
104
+ @error_handler
105
+ def get_image_size(self) -> Tuple[int, int]:
106
+ width, height = self._pilimg.size
107
+ return (width, height)
108
+
109
+ @error_handler
110
+ def copy(self) -> Dimage:
111
+ """Get copied Pimage
112
+
113
+ Create another Pimage instance which has same content.
114
+
115
+ Returns:
116
+ Pimage: deep copied image
117
+
118
+ """
119
+
120
+ return Dimage(self)
121
+
122
+ @error_handler
123
+ def save(self, file: str) -> None:
124
+ """Save Pimage data to file
125
+
126
+ Save to specified file path.
127
+ Path is relative to script file which calls this method.
128
+
129
+ Args:
130
+ file: write file path. relative to user script file.
131
+
132
+ Returns:
133
+ None
134
+
135
+ """
136
+
137
+ if not isinstance(file, str):
138
+ raise ValueError('arg "file" must be str.')
139
+
140
+ abspath = get_script_relative_path(file)
141
+ directory = os.path.dirname(abspath)
142
+ os.makedirs(directory, exist_ok=True)
143
+ self._pilimg.save(abspath, quality=95)
144
+
145
+ @error_handler
146
+ def border(
147
+ self,
148
+ width: int,
149
+ color: Union[Tuple[int, int, int], Tuple[int, int, int, float]],
150
+ ) -> Dimage:
151
+ print(color)
152
+
153
+ new_img = ImageOps.expand(
154
+ self._pilimg,
155
+ border=width,
156
+ # pillow supports (R, G, B) and (R, G, B, A)
157
+ fill=(color[0], color[1], color[2]),
158
+ )
159
+ return Dimage(new_img)
160
+
161
+ @error_handler
162
+ def rotate(self, angle: float) -> Dimage:
163
+ """Get rotated new Pimage. Original Pimage is keeped.
164
+
165
+ Get new Pimage which has rotated.
166
+
167
+ Args:
168
+ angle: between 0.0 ~ 360. pixel size can be changed. New area becomes transparent.
169
+
170
+ Returns:
171
+ Pimage: new rotated image.
172
+
173
+ """
174
+
175
+ newimg = self._pilimg.rotate(
176
+ angle,
177
+ resample=Image.Resampling.BICUBIC,
178
+ expand=True,
179
+ )
180
+ return Dimage(newimg)
181
+
182
+ @error_handler
183
+ def resize(self, width: int, height: int) -> Dimage:
184
+ """Get resized new Pimage. Original Pimage is keeped.
185
+
186
+ Get new Pimage which has resized.
187
+
188
+ Args:
189
+ width: new image width
190
+ height: new image height
191
+
192
+ Returns:
193
+ Pimage: new rotated image.
194
+
195
+ """
196
+
197
+ newimg = self._pilimg.resize(
198
+ (width, height),
199
+ resample=Image.LANCZOS, # pylint: disable=no-member
200
+ )
201
+ return Dimage(newimg)
202
+
203
+ @error_handler
204
+ def flip(self) -> Dimage:
205
+ """Get flipped new Pimage. Original Pimage is keeped.
206
+
207
+ Get new Pimage which has flip effect.
208
+
209
+ Returns:
210
+ Pimage: new flipped image.
211
+
212
+ """
213
+
214
+ newimg = ImageOps.flip(self._pilimg)
215
+ return Dimage(newimg)
216
+
217
+ @error_handler
218
+ def mirror(self) -> Dimage:
219
+ """Get mirrored new Pimage. Original Pimage is keeped.
220
+
221
+ Get new Pimage which has mirror effect.
222
+
223
+ Returns:
224
+ Pimage: new mirror image.
225
+
226
+ """
227
+
228
+ newimg = ImageOps.mirror(self._pilimg)
229
+ return Dimage(newimg)
230
+
231
+ @error_handler
232
+ def invert(self) -> Dimage:
233
+ """Get inverted new Pimage. Original Pimage is keeped.
234
+
235
+ Get new Pimage which has invert effect.
236
+
237
+ Returns:
238
+ Pimage: new inverted image.
239
+
240
+ """
241
+
242
+ if "A" not in self._pilimg.mode:
243
+ # has no tranceparency. use function
244
+ newimg = ImageOps.invert(self._pilimg)
245
+ return Dimage(newimg)
246
+
247
+ # Invert RGB channels
248
+ r, g, b, a = self._pilimg.split()
249
+ r = Image.eval(r, lambda x: 255 - x)
250
+ g = Image.eval(g, lambda x: 255 - x)
251
+ b = Image.eval(b, lambda x: 255 - x)
252
+
253
+ # Merge inverted RGB channels with original alpha channel
254
+ inverted_image = Image.merge("RGBA", (r, g, b, a))
255
+ return Dimage(inverted_image)
256
+
257
+ @error_handler
258
+ def grayscale(self) -> Dimage:
259
+ """Get grayscaled new Pimage. Original Pimage is keeped.
260
+
261
+ Get new Pimage which has grayscale effect.
262
+
263
+ Returns:
264
+ Pimage: new grayscaled image.
265
+
266
+ """
267
+
268
+ newimg = self._pilimg.convert("LA")
269
+ return Dimage(newimg)
270
+
271
+ @error_handler
272
+ def brightness(self, brightness_: float = 0.5) -> Dimage:
273
+ """Get brightness changed new Pimage. Original Pimage is keeped.
274
+
275
+ Get new Pimage which has change brightness effect.
276
+
277
+ Returns:
278
+ Pimage: new brightness changed image.
279
+
280
+ """
281
+
282
+ enhancer = ImageEnhance.Brightness(self._pilimg)
283
+ newimg = enhancer.enhance(brightness_)
284
+ return Dimage(newimg)
285
+
286
+ @error_handler
287
+ def sepia(self) -> Dimage:
288
+ """Get sepia color new Pimage. Original Pimage is keeped.
289
+
290
+ Get new Pimage which has sepia effect.
291
+
292
+ Returns:
293
+ Pimage: new sepia color image.
294
+
295
+ """
296
+
297
+ gray = self._pilimg.convert("L")
298
+ sepia_image = Image.merge(
299
+ "RGB",
300
+ (
301
+ gray.point(lambda x: x * 240 / 255),
302
+ gray.point(lambda x: x * 200 / 255),
303
+ gray.point(lambda x: x * 145 / 255),
304
+ ),
305
+ )
306
+ if "A" not in self._pilimg.mode:
307
+ return Dimage(sepia_image)
308
+
309
+ # add alpha from original
310
+ alpha_mask = self._pilimg.split()[3]
311
+ sepia_image.putalpha(alpha_mask)
312
+ return Dimage(sepia_image)
313
+
314
+ @error_handler
315
+ def colorize(
316
+ self,
317
+ black: Union[str, int],
318
+ white: Union[str, int],
319
+ # black: Union[str, Tuple[int, int, int]],
320
+ # white: Union[str, Tuple[int, int, int]],
321
+ ) -> Dimage:
322
+ """Get colorized new Pimage. Original Pimage is keeped.
323
+
324
+ Get new Pimage which has colorize effect.
325
+
326
+ Returns:
327
+ Pimage: new colorized image.
328
+
329
+ """
330
+
331
+ gray = self._pilimg.convert("L")
332
+ colorized_image = ImageOps.colorize(gray, black=black, white=white)
333
+ if "A" not in self._pilimg.mode:
334
+ return Dimage(colorized_image)
335
+
336
+ # add alpha from original
337
+ alpha_mask = self._pilimg.split()[3]
338
+ colorized_image.putalpha(alpha_mask)
339
+ return Dimage(colorized_image)
340
+
341
+ @error_handler
342
+ def posterize(self, num_colors: int = 4) -> Dimage:
343
+ """Get posterized new Pimage. Original Pimage is keeped.
344
+
345
+ Get new Pimage which has posterized effect.
346
+
347
+ Returns:
348
+ Pimage: new posterized image.
349
+
350
+ """
351
+
352
+ if "A" not in self._pilimg.mode:
353
+ newimg = ImageOps.posterize(self._pilimg, num_colors)
354
+ return Dimage(newimg)
355
+
356
+ r, g, b, a = self._pilimg.split()
357
+ r = ImageOps.posterize(r, num_colors)
358
+ g = ImageOps.posterize(g, num_colors)
359
+ b = ImageOps.posterize(b, num_colors)
360
+ newimg = Image.merge("RGBA", (r, g, b, a))
361
+ return Dimage(newimg)
362
+
363
+ @error_handler
364
+ def mosaic(self) -> Dimage:
365
+ """Get mosiac new Pimage. Original Pimage is keeped.
366
+
367
+ Get new Pimage which has mosaic effect.
368
+
369
+ Returns:
370
+ Pimage: new mosaic image.
371
+
372
+ """
373
+
374
+ gimg = self._pilimg.filter(ImageFilter.GaussianBlur(4))
375
+ newimg = gimg.resize(
376
+ (self._pilimg.size[0] // 8, self._pilimg.size[1] // 8),
377
+ # [x // 8 for x in self._pilimg.size],
378
+ ).resize(self._pilimg.size)
379
+ return Dimage(newimg)
380
+
381
+ @error_handler
382
+ def blur(self) -> Dimage:
383
+ """Get blur new Pimage. Original Pimage is keeped.
384
+
385
+ Get new Pimage which has blur effect.
386
+
387
+ Returns:
388
+ Pimage: new blur image.
389
+
390
+ """
391
+
392
+ newimg = self._pilimg.filter(ImageFilter.BLUR)
393
+ return Dimage(newimg)
394
+
395
+ @error_handler
396
+ def line_extraction(self) -> Dimage:
397
+ """Get line extracted new Pimage. Original Pimage is keeped.
398
+
399
+ Get new Pimage which has line extraction effect.
400
+
401
+ Returns:
402
+ Pimage: new line extracted image.
403
+
404
+ """
405
+
406
+ gray = self._pilimg.convert("L")
407
+ gray2 = gray.filter(ImageFilter.MaxFilter(5))
408
+ senga_inv = ImageChops.difference(gray, gray2)
409
+ newimg = ImageOps.invert(senga_inv)
410
+ return Dimage(newimg)
411
+
412
+ @error_handler
413
+ def remove_margin(
414
+ self,
415
+ margin_color: Union[None, str, Tuple[int, int, int]],
416
+ ) -> Dimage:
417
+ """Get margin removed new Pimage. Original Pimage is keeped.
418
+
419
+ Get new Pimage which has margin removed.
420
+
421
+ Returns:
422
+ Pimage: new margin removed image.
423
+
424
+ """
425
+
426
+ if margin_color is None:
427
+ if "A" not in self._pilimg.mode:
428
+ message = "Can't remove transparent margin from RGB image."
429
+ raise ValueError(message)
430
+ crop = self._pilimg.split()[-1].getbbox()
431
+ new_image = self._pilimg.crop(crop)
432
+ return Dimage(new_image)
433
+
434
+ raise NotImplementedError("Implement later")
435
+
436
+ #############
437
+ ### Cache ###
438
+ #############
439
+
440
+ @classmethod
441
+ def cache_exist(cls, name: str) -> bool:
442
+ """check whether having Pimage cache or not.
443
+
444
+ Args:
445
+ name: cache name
446
+
447
+ Returns:
448
+ bool: whether cache exist or not.
449
+
450
+ """
451
+
452
+ return name in cls._cache
453
+
454
+ @classmethod
455
+ def cache_set(cls, name: str, image: Dimage) -> None:
456
+ """Set **copied** Pimage cache with provided name key.
457
+
458
+ This method copies original object and set copied one.
459
+ It means you can modify original object after caching.
460
+ It doesn't modify cached object.
461
+
462
+ Args:
463
+ name: cache key.
464
+ style: Pimage object which you want to cache.
465
+
466
+ Returns:
467
+ None
468
+
469
+ """
470
+
471
+ cls._cache[name] = image.copy()
472
+
473
+ @classmethod
474
+ def cache_get(cls, name: str) -> Dimage:
475
+ """Get **copied** Pimage cache via name(key).
476
+
477
+ This method doesn't return original object.
478
+ But **copied** object.
479
+ So, you can modify returned Pimage object.
480
+ It doesn't harm original cached Pimage object.
481
+
482
+ Args:
483
+ name: cache key.
484
+
485
+ Returns:
486
+ Pimage: copy of original cached object.
487
+
488
+ """
489
+
490
+ if not cls.cache_exist(name):
491
+ raise ValueError(f'Pimage "{name}" is not cached.')
492
+ return cls._cache[name].copy()
493
+
494
+ @classmethod
495
+ def cache_delete(cls, name: str) -> None:
496
+ """Delete Pimage cache
497
+
498
+ Delete Pimage cache if exist.
499
+ If not exist, just ignore deletion request.
500
+
501
+ Args:
502
+ name: cache key.
503
+
504
+ Returns:
505
+ None
506
+
507
+ """
508
+
509
+ if cls.cache_exist(name):
510
+ del cls._cache[name]