ascii-art-python 2.0.2__tar.gz → 2.1.1__tar.gz
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.
- {ascii_art_python-2.0.2/src/ascii_art_python.egg-info → ascii_art_python-2.1.1}/PKG-INFO +1 -1
- {ascii_art_python-2.0.2 → ascii_art_python-2.1.1}/pyproject.toml +2 -1
- {ascii_art_python-2.0.2 → ascii_art_python-2.1.1}/src/ascii_art_python/__init__.py +1 -1
- {ascii_art_python-2.0.2 → ascii_art_python-2.1.1}/src/ascii_art_python/ascii_base.py +122 -105
- ascii_art_python-2.1.1/src/ascii_art_python/cli.py +182 -0
- {ascii_art_python-2.0.2 → ascii_art_python-2.1.1}/src/ascii_art_python/full_mode.py +18 -9
- {ascii_art_python-2.0.2 → ascii_art_python-2.1.1}/src/ascii_art_python/new_skool.py +13 -8
- {ascii_art_python-2.0.2 → ascii_art_python-2.1.1}/src/ascii_art_python/old_skool.py +15 -8
- {ascii_art_python-2.0.2 → ascii_art_python-2.1.1/src/ascii_art_python.egg-info}/PKG-INFO +1 -1
- {ascii_art_python-2.0.2 → ascii_art_python-2.1.1}/tests/test_ascii_api.py +15 -9
- ascii_art_python-2.0.2/src/ascii_art_python/cli.py +0 -107
- {ascii_art_python-2.0.2 → ascii_art_python-2.1.1}/LICENSE +0 -0
- {ascii_art_python-2.0.2 → ascii_art_python-2.1.1}/MANIFEST.in +0 -0
- {ascii_art_python-2.0.2 → ascii_art_python-2.1.1}/README.md +0 -0
- {ascii_art_python-2.0.2 → ascii_art_python-2.1.1}/setup.cfg +0 -0
- {ascii_art_python-2.0.2 → ascii_art_python-2.1.1}/src/ascii_art_python/assets/fonts/GoogleSansCode-Regular.ttf +0 -0
- {ascii_art_python-2.0.2 → ascii_art_python-2.1.1}/src/ascii_art_python/assets/fonts/KreativeSquareSM.ttf +0 -0
- {ascii_art_python-2.0.2 → ascii_art_python-2.1.1}/src/ascii_art_python/tools.py +0 -0
- {ascii_art_python-2.0.2 → ascii_art_python-2.1.1}/src/ascii_art_python.egg-info/SOURCES.txt +0 -0
- {ascii_art_python-2.0.2 → ascii_art_python-2.1.1}/src/ascii_art_python.egg-info/dependency_links.txt +0 -0
- {ascii_art_python-2.0.2 → ascii_art_python-2.1.1}/src/ascii_art_python.egg-info/entry_points.txt +0 -0
- {ascii_art_python-2.0.2 → ascii_art_python-2.1.1}/src/ascii_art_python.egg-info/requires.txt +0 -0
- {ascii_art_python-2.0.2 → ascii_art_python-2.1.1}/src/ascii_art_python.egg-info/top_level.txt +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: ascii_art_python
|
|
3
|
-
Version: 2.
|
|
3
|
+
Version: 2.1.1
|
|
4
4
|
Summary: A Python library and CLI tool for converting images and videos into ASCII art.
|
|
5
5
|
Author-email: Guillem Prieur <prieurguillem38@gmail.com>
|
|
6
6
|
Project-URL: Homepage, https://gitlab.pprriieeuurr.fr/guill_prieur/ascii-art-python-2
|
|
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
|
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "ascii_art_python"
|
|
7
|
-
version = "2.
|
|
7
|
+
version = "2.1.1"
|
|
8
8
|
authors = [
|
|
9
9
|
{ name="Guillem Prieur", email="prieurguillem38@gmail.com" },
|
|
10
10
|
]
|
|
@@ -12,6 +12,7 @@ description = "A Python library and CLI tool for converting images and videos in
|
|
|
12
12
|
readme = "README.md"
|
|
13
13
|
requires-python = ">=3.9"
|
|
14
14
|
|
|
15
|
+
|
|
15
16
|
keywords = [
|
|
16
17
|
"ascii",
|
|
17
18
|
"ascii-art",
|
|
@@ -10,7 +10,7 @@ import pathlib
|
|
|
10
10
|
import sys
|
|
11
11
|
import time
|
|
12
12
|
from abc import ABC, abstractmethod
|
|
13
|
-
from typing import Any, Callable,
|
|
13
|
+
from typing import Any, Callable, List, Optional
|
|
14
14
|
|
|
15
15
|
import cv2
|
|
16
16
|
import numpy as np
|
|
@@ -51,8 +51,8 @@ class Image(ABC):
|
|
|
51
51
|
def __init__(
|
|
52
52
|
self,
|
|
53
53
|
image: PIL.Image.Image,
|
|
54
|
-
height:
|
|
55
|
-
width:
|
|
54
|
+
height: Optional[int] = None,
|
|
55
|
+
width: Optional[int] = None,
|
|
56
56
|
) -> None:
|
|
57
57
|
"""
|
|
58
58
|
Initializes the base Image instance.
|
|
@@ -61,10 +61,10 @@ class Image(ABC):
|
|
|
61
61
|
-----------
|
|
62
62
|
image : PIL.Image.Image
|
|
63
63
|
The source image to be processed.
|
|
64
|
-
height :
|
|
64
|
+
height : Optional[int]
|
|
65
65
|
The target height for the output image. If None, it scales proportionally
|
|
66
66
|
or maintains the original height depending on the width parameter.
|
|
67
|
-
width :
|
|
67
|
+
width : Optional[int]
|
|
68
68
|
The target width for the output image. If None, it scales proportionally
|
|
69
69
|
or maintains the original width depending on the height parameter.
|
|
70
70
|
"""
|
|
@@ -113,7 +113,7 @@ class Image(ABC):
|
|
|
113
113
|
raise ValueError("height must be a positive integer")
|
|
114
114
|
self._height = value
|
|
115
115
|
|
|
116
|
-
def get_backgrounds(self, bg_chars:
|
|
116
|
+
def get_backgrounds(self, bg_chars: Optional[List[str]] = None, better_contrast: bool = True) -> List[str]:
|
|
117
117
|
"""
|
|
118
118
|
Calculates the background ASCII characters based on image luminosity.
|
|
119
119
|
|
|
@@ -122,13 +122,15 @@ class Image(ABC):
|
|
|
122
122
|
|
|
123
123
|
Parameters
|
|
124
124
|
-----------
|
|
125
|
-
bg_chars :
|
|
125
|
+
bg_chars : Optional[List[str]]
|
|
126
126
|
A list of single characters ordered by visual density.
|
|
127
127
|
If None, the default background characters are used.
|
|
128
|
+
better_contrast : bool
|
|
129
|
+
If True, the program will increase the contrast to improve the result.
|
|
128
130
|
|
|
129
131
|
Returns
|
|
130
132
|
--------
|
|
131
|
-
|
|
133
|
+
List[str]
|
|
132
134
|
A list of strings, where each string represents a row of
|
|
133
135
|
the background ASCII image.
|
|
134
136
|
"""
|
|
@@ -140,19 +142,22 @@ class Image(ABC):
|
|
|
140
142
|
raise ValueError("bg_chars must be a list of strings")
|
|
141
143
|
if not all(len(char) == 1 for char in bg_chars):
|
|
142
144
|
raise ValueError("bg_chars must be a list of strings of length 1")
|
|
143
|
-
if not isinstance(
|
|
144
|
-
raise TypeError("
|
|
145
|
+
if not isinstance(better_contrast, bool):
|
|
146
|
+
raise TypeError("better_contrast must be a bool")
|
|
145
147
|
|
|
146
148
|
image = self.__source.convert("L").resize((self.width, self.height))
|
|
147
|
-
if
|
|
149
|
+
if better_contrast:
|
|
148
150
|
image = PIL.ImageOps.autocontrast(image, 1)
|
|
149
151
|
|
|
150
152
|
conversion_factor = len(bg_chars) / 256
|
|
151
153
|
img_array = np.array(image)
|
|
152
154
|
indices = (img_array * conversion_factor).astype(int)
|
|
153
|
-
|
|
155
|
+
np_bg_chars = np.array(bg_chars)
|
|
156
|
+
return ["".join(np_bg_chars[row]) for row in indices]
|
|
154
157
|
|
|
155
|
-
def get_edges(
|
|
158
|
+
def get_edges(
|
|
159
|
+
self, edge_chars: Optional[List[str]] = None, color_edges: bool = False
|
|
160
|
+
) -> List[str]:
|
|
156
161
|
"""
|
|
157
162
|
Detects image edges and maps them to directional ASCII characters.
|
|
158
163
|
|
|
@@ -161,20 +166,24 @@ class Image(ABC):
|
|
|
161
166
|
|
|
162
167
|
Parameters
|
|
163
168
|
-----------
|
|
164
|
-
edge_chars :
|
|
169
|
+
edge_chars : Optional[List[str]]
|
|
165
170
|
A list of exactly 4 single characters representing vertical,
|
|
166
171
|
diagonal-up, horizontal, and diagonal-down edges respectively.
|
|
167
172
|
If None, default edge characters are used.
|
|
173
|
+
color_edges : bool
|
|
174
|
+
If True, performs edge detection on LAB color channels to catch
|
|
175
|
+
isoluminant edges (better quality but slower). If False, uses
|
|
176
|
+
grayscale edge detection (faster, recommended for videos).
|
|
168
177
|
|
|
169
178
|
Returns
|
|
170
179
|
--------
|
|
171
|
-
|
|
180
|
+
List[str]
|
|
172
181
|
A list of strings, where each string represents a row of
|
|
173
182
|
the foreground ASCII edges.
|
|
174
183
|
"""
|
|
175
184
|
if edge_chars is None:
|
|
176
185
|
edge_chars = self.DEFAULT_EDGE_CHARS
|
|
177
|
-
if not
|
|
186
|
+
if not isinstance(edge_chars, list):
|
|
178
187
|
raise TypeError("edge_chars must be a list")
|
|
179
188
|
if len(edge_chars) != 4:
|
|
180
189
|
raise ValueError("edge_chars must be a list of 4 characters")
|
|
@@ -182,33 +191,48 @@ class Image(ABC):
|
|
|
182
191
|
raise ValueError("edge_chars must be a list of strings")
|
|
183
192
|
if not all(len(char) == 1 for char in edge_chars):
|
|
184
193
|
raise ValueError("edge_chars must be a list of strings of length 1")
|
|
194
|
+
if not isinstance(color_edges, bool):
|
|
195
|
+
raise TypeError("color_edges must be a bool")
|
|
185
196
|
|
|
186
197
|
img_pil = self.__source.convert("RGB").resize((self.width, self.height))
|
|
187
198
|
img_rgb = np.array(img_pil)
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
199
|
+
|
|
200
|
+
if color_edges:
|
|
201
|
+
img_lab = cv2.cvtColor(img_rgb, cv2.COLOR_RGB2LAB)
|
|
202
|
+
l_chan, a_chan, b_chan = cv2.split(img_lab)
|
|
203
|
+
edges_l = cv2.Canny(l_chan, 50, 150)
|
|
204
|
+
edges_a = cv2.Canny(a_chan, 20, 80)
|
|
205
|
+
edges_b = cv2.Canny(b_chan, 20, 80)
|
|
206
|
+
edge_mask = (edges_l | edges_a | edges_b) == 255
|
|
207
|
+
sobel_target = l_chan
|
|
208
|
+
else:
|
|
209
|
+
img_gray = cv2.cvtColor(img_rgb, cv2.COLOR_RGB2GRAY)
|
|
210
|
+
edges_gray = cv2.Canny(img_gray, 50, 150)
|
|
211
|
+
edge_mask = edges_gray == 255
|
|
212
|
+
sobel_target = img_gray
|
|
213
|
+
|
|
214
|
+
sobel_x = cv2.Sobel(sobel_target, cv2.CV_64F, 1, 0)
|
|
215
|
+
sobel_y = cv2.Sobel(sobel_target, cv2.CV_64F, 0, 1)
|
|
196
216
|
angles = np.degrees(np.arctan2(sobel_y, sobel_x)) % 180
|
|
217
|
+
|
|
197
218
|
char_matrix = np.full((self.height, self.width), " ", dtype=object)
|
|
198
219
|
char_matrix[edge_mask & ((angles >= 67.5) & (angles < 112.5))] = edge_chars[2]
|
|
199
220
|
char_matrix[edge_mask & ((angles >= 22.5) & (angles < 67.5))] = edge_chars[1]
|
|
200
221
|
char_matrix[edge_mask & ((angles >= 112.5) & (angles < 157.5))] = edge_chars[3]
|
|
201
222
|
char_matrix[edge_mask & ((angles < 22.5) | (angles >= 157.5))] = edge_chars[0]
|
|
223
|
+
|
|
202
224
|
return ["".join(row) for row in char_matrix]
|
|
203
225
|
|
|
204
226
|
@abstractmethod
|
|
205
|
-
def to_list(self) ->
|
|
227
|
+
def to_list(self, **kwargs: Any) -> List[str]:
|
|
206
228
|
"""
|
|
207
|
-
|
|
229
|
+
An abstract method for converting the image into a list of ASCII strings.
|
|
230
|
+
|
|
231
|
+
Accepts additional parameters via kwargs depending on the mode.
|
|
208
232
|
|
|
209
233
|
Returns
|
|
210
234
|
--------
|
|
211
|
-
|
|
235
|
+
List[str]
|
|
212
236
|
A list of ASCII strings representing the fully processed image.
|
|
213
237
|
"""
|
|
214
238
|
|
|
@@ -260,7 +284,7 @@ class Image(ABC):
|
|
|
260
284
|
|
|
261
285
|
@classmethod
|
|
262
286
|
def from_path(
|
|
263
|
-
cls, path: str, height:
|
|
287
|
+
cls, path: str, height: Optional[int] = None, width: Optional[int] = None
|
|
264
288
|
) -> "Image":
|
|
265
289
|
"""
|
|
266
290
|
Instantiates an Image object from a file path.
|
|
@@ -269,9 +293,9 @@ class Image(ABC):
|
|
|
269
293
|
-----------
|
|
270
294
|
path : str
|
|
271
295
|
The system path to the image file.
|
|
272
|
-
height :
|
|
296
|
+
height : Optional[int]
|
|
273
297
|
The desired output height.
|
|
274
|
-
width :
|
|
298
|
+
width : Optional[int]
|
|
275
299
|
The desired output width.
|
|
276
300
|
|
|
277
301
|
Returns
|
|
@@ -330,7 +354,7 @@ class Image(ABC):
|
|
|
330
354
|
|
|
331
355
|
return cls(output_image, width=text_width, height=len(content_list) * font_size)
|
|
332
356
|
|
|
333
|
-
def export(self, filename: str = "mika_export"):
|
|
357
|
+
def export(self, filename: str = "mika_export", font_size: int = 15):
|
|
334
358
|
"""
|
|
335
359
|
Exports the rendered ASCII image to a PNG file.
|
|
336
360
|
|
|
@@ -344,7 +368,7 @@ class Image(ABC):
|
|
|
344
368
|
raise TypeError("filename must be a string")
|
|
345
369
|
if pathlib.Path.is_file(pathlib.Path(filename + ".png")):
|
|
346
370
|
raise ValueError("file must not exist")
|
|
347
|
-
self.to_image().save(f"{filename}.png")
|
|
371
|
+
self.to_image(font_size = font_size).save(f"{filename}.png")
|
|
348
372
|
|
|
349
373
|
|
|
350
374
|
class Video:
|
|
@@ -385,30 +409,26 @@ class Video:
|
|
|
385
409
|
if pathlib.Path.is_file(pathlib.Path(output_path)):
|
|
386
410
|
raise ValueError("output_path must not exist")
|
|
387
411
|
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
final_video = target_video.with_audio(audio)
|
|
412
|
+
with VideoFileClip(source_video_path) as source_video, VideoFileClip(target_video_path) as target_video:
|
|
413
|
+
audio = source_video.audio
|
|
414
|
+
final_video = target_video.with_audio(audio)
|
|
393
415
|
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
416
|
+
final_video.write_videofile(
|
|
417
|
+
output_path,
|
|
418
|
+
codec="libx264",
|
|
419
|
+
audio_codec="aac",
|
|
420
|
+
fps=target_video.fps,
|
|
421
|
+
logger=None,
|
|
422
|
+
)
|
|
401
423
|
|
|
402
|
-
|
|
403
|
-
target_video.close()
|
|
404
|
-
final_video.close()
|
|
424
|
+
final_video.close()
|
|
405
425
|
|
|
406
426
|
def __init__(
|
|
407
427
|
self,
|
|
408
428
|
path: str,
|
|
409
429
|
cls_image: type["Image"],
|
|
410
|
-
width:
|
|
411
|
-
height:
|
|
430
|
+
width: Optional[int] = None,
|
|
431
|
+
height: Optional[int] = None,
|
|
412
432
|
fps: int = 10,
|
|
413
433
|
) -> None:
|
|
414
434
|
"""
|
|
@@ -421,9 +441,9 @@ class Video:
|
|
|
421
441
|
cls_image : type[Image]
|
|
422
442
|
The specific Image class (e.g., full_mode.Image) used to process
|
|
423
443
|
each frame of the video.
|
|
424
|
-
width :
|
|
444
|
+
width : Optional[int]
|
|
425
445
|
The target width for the output frames.
|
|
426
|
-
height :
|
|
446
|
+
height : Optional[int]
|
|
427
447
|
The target height for the output frames.
|
|
428
448
|
fps : int
|
|
429
449
|
The maximum frames per second for processing and exporting.
|
|
@@ -451,44 +471,30 @@ class Video:
|
|
|
451
471
|
raise ValueError("Error: Unable to read video frames to determine size.")
|
|
452
472
|
|
|
453
473
|
image_test = self.cls_image(
|
|
454
|
-
PIL.Image.fromarray(cv2.cvtColor(frame, cv2.
|
|
474
|
+
PIL.Image.fromarray(cv2.cvtColor(frame, cv2.COLOR_BGR2GRAY)),
|
|
475
|
+
width = width,
|
|
476
|
+
height = height
|
|
455
477
|
)
|
|
456
|
-
self.
|
|
478
|
+
self.width = image_test.width
|
|
479
|
+
self.height = image_test.height
|
|
457
480
|
|
|
458
|
-
|
|
459
|
-
if height is None and width is None:
|
|
460
|
-
self.width, self.height = original_size
|
|
461
|
-
elif height is not None and width is None:
|
|
462
|
-
self.height = height
|
|
463
|
-
self.width = int(self.height * original_size[0] / original_size[1])
|
|
464
|
-
elif height is None and width is not None:
|
|
465
|
-
self.width = width
|
|
466
|
-
self.height = int(self.width * original_size[1] / original_size[0])
|
|
467
|
-
elif height is not None and width is not None:
|
|
468
|
-
self.height = height
|
|
469
|
-
self.width = width
|
|
470
|
-
|
|
471
|
-
self.__cap = None
|
|
481
|
+
self._cap = None
|
|
472
482
|
self.__frame_skip = max(1, int(self._cached_fps / self.max_fps))
|
|
473
483
|
self.__frame_count = 0
|
|
474
484
|
|
|
475
485
|
def __enter__(self):
|
|
476
|
-
if self.
|
|
477
|
-
self.
|
|
478
|
-
self.
|
|
479
|
-
if not self.
|
|
486
|
+
if self._cap is not None:
|
|
487
|
+
self._cap.release()
|
|
488
|
+
self._cap = cv2.VideoCapture(self.path)
|
|
489
|
+
if not self._cap.isOpened():
|
|
480
490
|
raise ValueError(f"Error: Unable to open the video at '{self.path}'")
|
|
481
491
|
self.__frame_count = 0
|
|
482
492
|
return self
|
|
483
493
|
|
|
484
494
|
def __exit__(self, exc_type, exc_val, exc_tb):
|
|
485
|
-
if self.
|
|
486
|
-
self.
|
|
487
|
-
self.
|
|
488
|
-
|
|
489
|
-
@property
|
|
490
|
-
def original_size(self) -> tuple[int, int]:
|
|
491
|
-
return self._cached_original_size
|
|
495
|
+
if self._cap is not None:
|
|
496
|
+
self._cap.release()
|
|
497
|
+
self._cap = None
|
|
492
498
|
|
|
493
499
|
@property
|
|
494
500
|
def fps(self) -> float:
|
|
@@ -522,26 +528,26 @@ class Video:
|
|
|
522
528
|
self._height = value
|
|
523
529
|
|
|
524
530
|
def __iter__(self) -> "Video":
|
|
525
|
-
if self.
|
|
526
|
-
self.
|
|
531
|
+
if self._cap is None or not self._cap.isOpened():
|
|
532
|
+
self._cap = cv2.VideoCapture(self.path)
|
|
527
533
|
self.__frame_count = 0
|
|
528
534
|
return self
|
|
529
535
|
|
|
530
536
|
def __next__(self) -> Any:
|
|
531
|
-
if self.
|
|
537
|
+
if self._cap is None:
|
|
532
538
|
raise StopIteration
|
|
533
539
|
|
|
534
540
|
while True:
|
|
535
|
-
success, frame = self.
|
|
541
|
+
success, frame = self._cap.read()
|
|
536
542
|
|
|
537
543
|
if not success:
|
|
538
|
-
self.
|
|
539
|
-
self.
|
|
544
|
+
self._cap.release()
|
|
545
|
+
self._cap = None
|
|
540
546
|
raise StopIteration
|
|
541
547
|
|
|
542
548
|
if self.__frame_count % self.__frame_skip == 0:
|
|
543
549
|
result_frame = self.cls_image(
|
|
544
|
-
PIL.Image.fromarray(cv2.cvtColor(frame, cv2.
|
|
550
|
+
PIL.Image.fromarray(cv2.cvtColor(frame, cv2.COLOR_BGR2GRAY)),
|
|
545
551
|
width=self.width,
|
|
546
552
|
height=self.height,
|
|
547
553
|
)
|
|
@@ -551,10 +557,10 @@ class Video:
|
|
|
551
557
|
self.__frame_count += 1
|
|
552
558
|
|
|
553
559
|
def __del__(self) -> None:
|
|
554
|
-
if hasattr(self, "
|
|
555
|
-
self.
|
|
560
|
+
if hasattr(self, "_cap") and self._cap is not None:
|
|
561
|
+
self._cap.release()
|
|
556
562
|
|
|
557
|
-
def export(self, filename: str = "mika_export", sound: bool = False):
|
|
563
|
+
def export(self, filename: str = "mika_export", sound: bool = False, font_size: int = 12) -> None:
|
|
558
564
|
"""
|
|
559
565
|
Exports the processed ASCII video to an mp4 file.
|
|
560
566
|
|
|
@@ -575,36 +581,38 @@ class Video:
|
|
|
575
581
|
raise TypeError("sound must be a bool")
|
|
576
582
|
if pathlib.Path.is_file(pathlib.Path(filename + ".mp4")):
|
|
577
583
|
raise ValueError("filename must not exist")
|
|
584
|
+
if not isinstance(font_size, int):
|
|
585
|
+
raise TypeError("font_size must be an int")
|
|
586
|
+
if font_size <= 0:
|
|
587
|
+
raise ValueError("font_size must be a positive integer")
|
|
588
|
+
|
|
589
|
+
out_width = self.width * font_size
|
|
590
|
+
out_height = self.height * font_size
|
|
578
591
|
|
|
579
|
-
out_width = self.width * 15
|
|
580
|
-
out_height = self.height * 15
|
|
581
|
-
|
|
582
592
|
if out_width % 2 != 0:
|
|
583
593
|
out_width += 1
|
|
584
594
|
if out_height % 2 != 0:
|
|
585
595
|
out_height += 1
|
|
586
|
-
|
|
587
596
|
fourcc = cv2.VideoWriter_fourcc(*"mp4v")
|
|
588
597
|
out = cv2.VideoWriter(
|
|
589
598
|
filename + ".mp4",
|
|
590
599
|
fourcc,
|
|
591
600
|
self.fps / max(1, int(self.fps / self.max_fps)),
|
|
592
601
|
(out_width, out_height),
|
|
593
|
-
|
|
602
|
+
False,
|
|
594
603
|
)
|
|
595
604
|
|
|
596
605
|
with self:
|
|
597
606
|
for frame in tqdm(self, "Exporting frames", len(self)):
|
|
598
|
-
pil_img = frame.to_image(font_size=
|
|
599
|
-
|
|
607
|
+
pil_img = frame.to_image(font_size = font_size)
|
|
608
|
+
|
|
600
609
|
if pil_img.size != (out_width, out_height):
|
|
601
610
|
pil_img = pil_img.resize((out_width, out_height))
|
|
602
|
-
|
|
611
|
+
|
|
603
612
|
tab = np.array(pil_img)
|
|
604
|
-
|
|
605
|
-
|
|
606
|
-
|
|
607
|
-
|
|
613
|
+
|
|
614
|
+
out.write(tab)
|
|
615
|
+
|
|
608
616
|
out.release()
|
|
609
617
|
|
|
610
618
|
if sound:
|
|
@@ -627,11 +635,20 @@ class Video:
|
|
|
627
635
|
if not callable(func_print):
|
|
628
636
|
raise TypeError("func_print must be a callable")
|
|
629
637
|
|
|
630
|
-
|
|
631
|
-
|
|
638
|
+
actual_frame_skip = max(1, int(self.fps / self.max_fps))
|
|
639
|
+
effective_fps = self.fps / actual_frame_skip
|
|
640
|
+
|
|
641
|
+
frame_duration = 1.0 / effective_fps
|
|
632
642
|
|
|
633
643
|
with self:
|
|
634
|
-
|
|
635
|
-
|
|
644
|
+
start_time = time.perf_counter()
|
|
645
|
+
|
|
646
|
+
for i, frame in enumerate(self):
|
|
647
|
+
expected_time = start_time + (i * frame_duration)
|
|
648
|
+
|
|
649
|
+
sleep_time = expected_time - time.perf_counter()
|
|
650
|
+
|
|
651
|
+
if sleep_time > 0:
|
|
652
|
+
time.sleep(sleep_time)
|
|
653
|
+
|
|
636
654
|
func_print(frame.get_alternate_lines())
|
|
637
|
-
next_sleep += time_between
|
|
@@ -0,0 +1,182 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Command-line interface module for ASCII generation.
|
|
3
|
+
|
|
4
|
+
Provides click-based commands to export or print ASCII art
|
|
5
|
+
from image and video files.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
import mimetypes
|
|
9
|
+
import shutil
|
|
10
|
+
from typing import Any, Optional, Tuple
|
|
11
|
+
|
|
12
|
+
import click
|
|
13
|
+
|
|
14
|
+
from . import full_mode, new_skool, old_skool
|
|
15
|
+
from .tools import clean_print
|
|
16
|
+
|
|
17
|
+
MODES = {
|
|
18
|
+
"full_mode": full_mode,
|
|
19
|
+
"old_skool": old_skool,
|
|
20
|
+
"new_skool": new_skool,
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
def _get_media_type_and_module(source_path: str, mode: str) -> Tuple[str, Any]:
|
|
25
|
+
"""
|
|
26
|
+
Determines the media type of the source file and retrieves the corresponding module.
|
|
27
|
+
|
|
28
|
+
Parameters
|
|
29
|
+
-----------
|
|
30
|
+
source_path : str
|
|
31
|
+
The file path to the input media.
|
|
32
|
+
mode : str
|
|
33
|
+
The desired ASCII generation mode.
|
|
34
|
+
|
|
35
|
+
Returns
|
|
36
|
+
--------
|
|
37
|
+
Tuple[str, Any]
|
|
38
|
+
A tuple containing the media type ('image' or 'video') and the associated module.
|
|
39
|
+
|
|
40
|
+
Raises
|
|
41
|
+
-------
|
|
42
|
+
click.ClickException
|
|
43
|
+
If the media type cannot be determined or the mode is invalid.
|
|
44
|
+
"""
|
|
45
|
+
media_type, _ = mimetypes.guess_type(source_path)
|
|
46
|
+
|
|
47
|
+
if media_type is None:
|
|
48
|
+
raise click.ClickException("Error: Unable to determine the file type (image or video).")
|
|
49
|
+
|
|
50
|
+
if media_type.startswith("image"):
|
|
51
|
+
base_type = "image"
|
|
52
|
+
elif media_type.startswith("video"):
|
|
53
|
+
base_type = "video"
|
|
54
|
+
else:
|
|
55
|
+
raise click.ClickException(f"Error: Unsupported media type '{media_type}'.")
|
|
56
|
+
|
|
57
|
+
if mode not in MODES:
|
|
58
|
+
raise click.ClickException(f"Error: Mode '{mode}' is not recognized.")
|
|
59
|
+
|
|
60
|
+
return base_type, MODES[mode]
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
@click.command()
|
|
64
|
+
@click.argument("source_path", type=click.Path(exists=True))
|
|
65
|
+
@click.argument("target_filename", type=click.STRING)
|
|
66
|
+
@click.option(
|
|
67
|
+
"--width",
|
|
68
|
+
"-w",
|
|
69
|
+
type=int,
|
|
70
|
+
default=None,
|
|
71
|
+
help="The number of characters in the width of the output."
|
|
72
|
+
)
|
|
73
|
+
@click.option(
|
|
74
|
+
"--height",
|
|
75
|
+
"-h",
|
|
76
|
+
type=int,
|
|
77
|
+
default=None,
|
|
78
|
+
help="The number of characters in the height of the output."
|
|
79
|
+
)
|
|
80
|
+
@click.option(
|
|
81
|
+
"--mode",
|
|
82
|
+
"-m",
|
|
83
|
+
type=click.Choice(list(MODES.keys())),
|
|
84
|
+
default="full_mode",
|
|
85
|
+
help="The ASCII generation mode to use."
|
|
86
|
+
)
|
|
87
|
+
@click.option(
|
|
88
|
+
"--fontsize",
|
|
89
|
+
"-f",
|
|
90
|
+
type=int,
|
|
91
|
+
default=12,
|
|
92
|
+
help="The size of each character in the exported video."
|
|
93
|
+
)
|
|
94
|
+
@click.option(
|
|
95
|
+
"--sound/--no-sound",
|
|
96
|
+
type=bool,
|
|
97
|
+
default=True,
|
|
98
|
+
help="If the file is a video, export it with or without audio."
|
|
99
|
+
)
|
|
100
|
+
def create_and_export(
|
|
101
|
+
source_path: str,
|
|
102
|
+
target_filename: str,
|
|
103
|
+
width: Optional[int],
|
|
104
|
+
height: Optional[int],
|
|
105
|
+
mode: str,
|
|
106
|
+
fontsize: int,
|
|
107
|
+
sound: bool,
|
|
108
|
+
) -> None:
|
|
109
|
+
"""
|
|
110
|
+
Creates and exports an ASCII art image or video.
|
|
111
|
+
|
|
112
|
+
Processes the input file located at SOURCE_PATH and exports the result
|
|
113
|
+
to TARGET_FILENAME with the specified settings.
|
|
114
|
+
"""
|
|
115
|
+
click.echo(f"▶ Converting '{source_path}'...")
|
|
116
|
+
click.echo(f"▶ Settings: length=({width}, {height}), mode={mode}")
|
|
117
|
+
|
|
118
|
+
media_type, module = _get_media_type_and_module(source_path, mode)
|
|
119
|
+
click.echo(f"▶ Export type: {media_type}")
|
|
120
|
+
|
|
121
|
+
if media_type == "image":
|
|
122
|
+
image = module.Image.from_path(source_path, height=height, width=width)
|
|
123
|
+
image.export(target_filename, font_size=fontsize)
|
|
124
|
+
click.echo("✓ Conversion complete!")
|
|
125
|
+
|
|
126
|
+
elif media_type == "video":
|
|
127
|
+
video = module.Video(source_path, width=width, height=height)
|
|
128
|
+
click.echo(
|
|
129
|
+
f"Please note: You are about to export a video with the following settings: "
|
|
130
|
+
f"length=({video.width}, {video.height}) font_size={fontsize}"
|
|
131
|
+
)
|
|
132
|
+
click.confirm("Do you want to continue?", abort=True)
|
|
133
|
+
video.export(target_filename, sound=sound, font_size=fontsize)
|
|
134
|
+
|
|
135
|
+
click.echo("✓ Conversion complete!")
|
|
136
|
+
if sound:
|
|
137
|
+
click.echo(f"✓ Your file is available here: {target_filename}_audio.mp4")
|
|
138
|
+
else:
|
|
139
|
+
click.echo(f"✓ Your file is available here: {target_filename}.mp4")
|
|
140
|
+
|
|
141
|
+
|
|
142
|
+
@click.command()
|
|
143
|
+
@click.argument("source_path", type=click.Path(exists=True))
|
|
144
|
+
@click.option(
|
|
145
|
+
"--mode",
|
|
146
|
+
"-m",
|
|
147
|
+
type=click.Choice(list(MODES.keys())),
|
|
148
|
+
default="full_mode",
|
|
149
|
+
help="The ASCII generation mode to use."
|
|
150
|
+
)
|
|
151
|
+
def create_and_echo(source_path: str, mode: str) -> None:
|
|
152
|
+
"""
|
|
153
|
+
Creates and prints an ASCII art image or video in the terminal.
|
|
154
|
+
|
|
155
|
+
Processes the input file located at SOURCE_PATH and outputs the result
|
|
156
|
+
directly to the standard output.
|
|
157
|
+
"""
|
|
158
|
+
max_width, max_height = shutil.get_terminal_size()
|
|
159
|
+
max_height *= 2
|
|
160
|
+
|
|
161
|
+
click.echo(f"▶ Converting '{source_path}'...")
|
|
162
|
+
click.echo(f"▶ Settings: mode={mode}")
|
|
163
|
+
|
|
164
|
+
media_type, module = _get_media_type_and_module(source_path, mode)
|
|
165
|
+
click.echo(f"▶ Echo type: {media_type}")
|
|
166
|
+
|
|
167
|
+
if media_type == "image":
|
|
168
|
+
image = module.Image.from_path(source_path, height=max_height)
|
|
169
|
+
if image.width > max_width:
|
|
170
|
+
image = module.Image.from_path(source_path, width=max_width)
|
|
171
|
+
|
|
172
|
+
click.echo(image.get_alternate_lines())
|
|
173
|
+
click.echo("✓ Conversion complete!")
|
|
174
|
+
|
|
175
|
+
elif media_type == "video":
|
|
176
|
+
video = module.Video(source_path, height=max_height)
|
|
177
|
+
if video.width > max_width:
|
|
178
|
+
video = module.Video(source_path, width=max_width)
|
|
179
|
+
|
|
180
|
+
click.clear()
|
|
181
|
+
video.print_in_terminal(clean_print)
|
|
182
|
+
click.echo("✓ Conversion complete!")
|
|
@@ -5,7 +5,7 @@ This module provides classes to process images and videos in full mode,
|
|
|
5
5
|
combining backgrounds and edges for detailed ASCII representation.
|
|
6
6
|
"""
|
|
7
7
|
|
|
8
|
-
from typing import
|
|
8
|
+
from typing import Optional, Any, List
|
|
9
9
|
|
|
10
10
|
from . import ascii_base
|
|
11
11
|
|
|
@@ -20,20 +20,29 @@ class Image(ascii_base.Image):
|
|
|
20
20
|
combining background characters and foreground edges.
|
|
21
21
|
"""
|
|
22
22
|
|
|
23
|
-
def to_list(self) ->
|
|
23
|
+
def to_list(self, better_contrast: bool = True, color_edges: bool = False, **kwargs: Any) -> List[str]:
|
|
24
24
|
"""
|
|
25
25
|
Converts the image to a list of ASCII strings.
|
|
26
26
|
|
|
27
27
|
Combines the background characters and edge characters. If an edge
|
|
28
28
|
character is empty (" "), it uses the corresponding background character.
|
|
29
29
|
|
|
30
|
+
Parameters
|
|
31
|
+
-----------
|
|
32
|
+
better_contrast : bool
|
|
33
|
+
If True, the program will increase the contrast to improve the result.
|
|
34
|
+
color_edges : bool
|
|
35
|
+
If True, performs edge detection on LAB color channels to catch
|
|
36
|
+
isoluminant edges (better quality but slower). If False, uses
|
|
37
|
+
grayscale edge detection (faster, recommended for videos).
|
|
38
|
+
|
|
30
39
|
Returns
|
|
31
40
|
--------
|
|
32
|
-
|
|
41
|
+
List[str]
|
|
33
42
|
A list of strings where each string represents a row of the ASCII image.
|
|
34
43
|
"""
|
|
35
|
-
backgrounds = self.get_backgrounds()
|
|
36
|
-
edges = self.get_edges()
|
|
44
|
+
backgrounds = self.get_backgrounds(better_contrast = better_contrast)
|
|
45
|
+
edges = self.get_edges(color_edges = color_edges)
|
|
37
46
|
result = [""] * len(edges)
|
|
38
47
|
for i, e in enumerate(edges):
|
|
39
48
|
for j, f in enumerate(e):
|
|
@@ -52,8 +61,8 @@ class Video(ascii_base.Video):
|
|
|
52
61
|
def __init__(
|
|
53
62
|
self,
|
|
54
63
|
path: str,
|
|
55
|
-
width:
|
|
56
|
-
height:
|
|
64
|
+
width: Optional[int] = None,
|
|
65
|
+
height: Optional[int] = None,
|
|
57
66
|
fps: int = 10,
|
|
58
67
|
) -> None:
|
|
59
68
|
"""
|
|
@@ -63,9 +72,9 @@ class Video(ascii_base.Video):
|
|
|
63
72
|
-----------
|
|
64
73
|
path : str
|
|
65
74
|
The file path to the input video.
|
|
66
|
-
width :
|
|
75
|
+
width : Optional[int]
|
|
67
76
|
The desired width of the output video. If None, it is calculated automatically.
|
|
68
|
-
height :
|
|
77
|
+
height : Optional[int]
|
|
69
78
|
The desired height of the output video. If None, it is calculated automatically.
|
|
70
79
|
fps : int
|
|
71
80
|
The frames per second to process the video at (default is 10).
|
|
@@ -5,7 +5,7 @@ This module provides classes to process images and videos in new skool mode,
|
|
|
5
5
|
relying entirely on background characters for the ASCII representation.
|
|
6
6
|
"""
|
|
7
7
|
|
|
8
|
-
from typing import
|
|
8
|
+
from typing import Optional, Any, List
|
|
9
9
|
|
|
10
10
|
from . import ascii_base
|
|
11
11
|
|
|
@@ -20,19 +20,24 @@ class Image(ascii_base.Image):
|
|
|
20
20
|
using only the background characters.
|
|
21
21
|
"""
|
|
22
22
|
|
|
23
|
-
def to_list(self) ->
|
|
23
|
+
def to_list(self, better_contrast: bool = True, **kwargs: Any) -> List[str]:
|
|
24
24
|
"""
|
|
25
25
|
Converts the image to a list of ASCII strings.
|
|
26
26
|
|
|
27
27
|
Retrieves and returns only the background characters representing
|
|
28
28
|
the image, ignoring edges.
|
|
29
29
|
|
|
30
|
+
Parameters
|
|
31
|
+
-----------
|
|
32
|
+
better_contrast : bool
|
|
33
|
+
If True, the program will increase the contrast to improve the result.
|
|
34
|
+
|
|
30
35
|
Returns
|
|
31
36
|
--------
|
|
32
|
-
|
|
37
|
+
List[str]
|
|
33
38
|
A list of strings where each string represents a row of the ASCII image.
|
|
34
39
|
"""
|
|
35
|
-
return self.get_backgrounds()
|
|
40
|
+
return self.get_backgrounds(better_contrast = better_contrast)
|
|
36
41
|
|
|
37
42
|
|
|
38
43
|
class Video(ascii_base.Video):
|
|
@@ -46,8 +51,8 @@ class Video(ascii_base.Video):
|
|
|
46
51
|
def __init__(
|
|
47
52
|
self,
|
|
48
53
|
path: str,
|
|
49
|
-
width:
|
|
50
|
-
height:
|
|
54
|
+
width: Optional[int] = None,
|
|
55
|
+
height: Optional[int] = None,
|
|
51
56
|
fps: int = 10,
|
|
52
57
|
) -> None:
|
|
53
58
|
"""
|
|
@@ -57,9 +62,9 @@ class Video(ascii_base.Video):
|
|
|
57
62
|
-----------
|
|
58
63
|
path : str
|
|
59
64
|
The file path to the input video.
|
|
60
|
-
width :
|
|
65
|
+
width : Optional[int]
|
|
61
66
|
The desired width of the output video. If None, it is calculated automatically.
|
|
62
|
-
height :
|
|
67
|
+
height : Optional[int]
|
|
63
68
|
The desired height of the output video. If None, it is calculated automatically.
|
|
64
69
|
fps : int
|
|
65
70
|
The frames per second to process the video at (default is 10).
|
|
@@ -5,7 +5,7 @@ This module provides classes to process images and videos in old skool mode,
|
|
|
5
5
|
relying entirely on foreground edges for the ASCII representation.
|
|
6
6
|
"""
|
|
7
7
|
|
|
8
|
-
from typing import
|
|
8
|
+
from typing import Optional, Any, List
|
|
9
9
|
|
|
10
10
|
from . import ascii_base
|
|
11
11
|
|
|
@@ -20,19 +20,26 @@ class Image(ascii_base.Image):
|
|
|
20
20
|
using only the edge characters.
|
|
21
21
|
"""
|
|
22
22
|
|
|
23
|
-
def to_list(self) ->
|
|
23
|
+
def to_list(self, color_edges: bool = False, **kwargs: Any) -> List[str]:
|
|
24
24
|
"""
|
|
25
25
|
Converts the image to a list of ASCII strings.
|
|
26
26
|
|
|
27
27
|
Retrieves and returns only the edge characters representing
|
|
28
28
|
the image, ignoring backgrounds.
|
|
29
29
|
|
|
30
|
+
Parameters
|
|
31
|
+
-----------
|
|
32
|
+
color_edges : bool
|
|
33
|
+
If True, performs edge detection on LAB color channels to catch
|
|
34
|
+
isoluminant edges (better quality but slower). If False, uses
|
|
35
|
+
grayscale edge detection (faster, recommended for videos).
|
|
36
|
+
|
|
30
37
|
Returns
|
|
31
38
|
--------
|
|
32
|
-
|
|
39
|
+
List[str]
|
|
33
40
|
A list of strings where each string represents a row of the ASCII image.
|
|
34
41
|
"""
|
|
35
|
-
return self.get_edges()
|
|
42
|
+
return self.get_edges(color_edges = color_edges)
|
|
36
43
|
|
|
37
44
|
|
|
38
45
|
class Video(ascii_base.Video):
|
|
@@ -46,8 +53,8 @@ class Video(ascii_base.Video):
|
|
|
46
53
|
def __init__(
|
|
47
54
|
self,
|
|
48
55
|
path: str,
|
|
49
|
-
width:
|
|
50
|
-
height:
|
|
56
|
+
width: Optional[int] = None,
|
|
57
|
+
height: Optional[int] = None,
|
|
51
58
|
fps: int = 10,
|
|
52
59
|
) -> None:
|
|
53
60
|
"""
|
|
@@ -57,9 +64,9 @@ class Video(ascii_base.Video):
|
|
|
57
64
|
-----------
|
|
58
65
|
path : str
|
|
59
66
|
The file path to the input video.
|
|
60
|
-
width :
|
|
67
|
+
width : Optional[int]
|
|
61
68
|
The desired width of the output video. If None, it is calculated automatically.
|
|
62
|
-
height :
|
|
69
|
+
height : Optional[int]
|
|
63
70
|
The desired height of the output video. If None, it is calculated automatically.
|
|
64
71
|
fps : int
|
|
65
72
|
The frames per second to process the video at (default is 10).
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: ascii_art_python
|
|
3
|
-
Version: 2.
|
|
3
|
+
Version: 2.1.1
|
|
4
4
|
Summary: A Python library and CLI tool for converting images and videos into ASCII art.
|
|
5
5
|
Author-email: Guillem Prieur <prieurguillem38@gmail.com>
|
|
6
6
|
Project-URL: Homepage, https://gitlab.pprriieeuurr.fr/guill_prieur/ascii-art-python-2
|
|
@@ -168,8 +168,7 @@ class TestVideoBase:
|
|
|
168
168
|
assert vid.path == "fake_video.mp4"
|
|
169
169
|
assert vid.max_fps == 12
|
|
170
170
|
assert vid.fps == 24.0 # Mocked value returned by cv2.CAP_PROP_FPS
|
|
171
|
-
|
|
172
|
-
assert vid.original_size == (50 * 15, 50 * 15)
|
|
171
|
+
assert vid.height * vid.width == 2500
|
|
173
172
|
|
|
174
173
|
@patch('pathlib.Path.is_file', return_value=False)
|
|
175
174
|
def test_video_initialization_file_not_found(self, mock_is_file):
|
|
@@ -196,18 +195,25 @@ class TestVideoBase:
|
|
|
196
195
|
@patch.object(ascii_base, 'VideoFileClip')
|
|
197
196
|
def test_transfer_audio(self, mock_videofileclip, mock_is_file):
|
|
198
197
|
"""Tests the audio transfer logic (mocked)."""
|
|
199
|
-
mock_is_file.side_effect = lambda path: path.name != "output.mp4"
|
|
200
|
-
|
|
198
|
+
mock_is_file.side_effect = lambda path: path.name != "output.mp4"
|
|
199
|
+
|
|
201
200
|
mock_source = MagicMock()
|
|
202
201
|
mock_target = MagicMock()
|
|
203
202
|
mock_final = MagicMock()
|
|
204
|
-
|
|
205
|
-
|
|
203
|
+
|
|
204
|
+
mock_source.__enter__.return_value = mock_source
|
|
205
|
+
mock_target.__enter__.return_value = mock_target
|
|
206
|
+
|
|
206
207
|
mock_videofileclip.side_effect = [mock_source, mock_target]
|
|
207
208
|
mock_target.with_audio.return_value = mock_final
|
|
208
|
-
|
|
209
|
+
|
|
209
210
|
ascii_base.Video.transfer_audio("source.mp4", "target.mp4", "output.mp4")
|
|
210
|
-
|
|
211
|
+
|
|
211
212
|
mock_target.with_audio.assert_called_once_with(mock_source.audio)
|
|
213
|
+
|
|
212
214
|
mock_final.write_videofile.assert_called_once()
|
|
213
|
-
|
|
215
|
+
|
|
216
|
+
mock_source.__exit__.assert_called_once()
|
|
217
|
+
mock_target.__exit__.assert_called_once()
|
|
218
|
+
|
|
219
|
+
mock_final.close.assert_called_once()
|
|
@@ -1,107 +0,0 @@
|
|
|
1
|
-
import mimetypes
|
|
2
|
-
import shutil
|
|
3
|
-
import sys
|
|
4
|
-
import click
|
|
5
|
-
from . import full_mode, old_skool, new_skool
|
|
6
|
-
|
|
7
|
-
from .tools import clean_print
|
|
8
|
-
|
|
9
|
-
MODES = {
|
|
10
|
-
"full_mode": full_mode,
|
|
11
|
-
"old_skool": old_skool,
|
|
12
|
-
"new_skool": new_skool
|
|
13
|
-
}
|
|
14
|
-
|
|
15
|
-
@click.command()
|
|
16
|
-
@click.argument("source_path", type=click.Path(exists=True))
|
|
17
|
-
@click.argument("target_filename", type=click.STRING)
|
|
18
|
-
@click.option("--width", "-w", type=int, default=None, help="The number of characters in the width of the output")
|
|
19
|
-
@click.option("--height", "-h", type=int, default=None, help="The number of characters in the height of the output")
|
|
20
|
-
@click.option("--mode", "-m", type=click.Choice(["full_mode", "old_skool", "new_skool"]), default="full_mode", help="full_mode / old_skool / new_skool")
|
|
21
|
-
@click.option("--sound/--no-sound", type=bool, default=True, help="If the file is a video file, export it with or without audio.")
|
|
22
|
-
def create_and_export(source_path, target_filename, width, height, mode, sound):
|
|
23
|
-
"""
|
|
24
|
-
Creates and exports an ASCII art image or video.
|
|
25
|
-
"""
|
|
26
|
-
click.echo(f"▶ Converting '{source_path}'...")
|
|
27
|
-
click.echo(f"▶ Settings: length=({width}, {height}), mode={mode}")
|
|
28
|
-
|
|
29
|
-
type_mime, _ = mimetypes.guess_type(source_path)
|
|
30
|
-
|
|
31
|
-
if type_mime is None:
|
|
32
|
-
click.secho("✗ Error: Unable to determine which export type to use")
|
|
33
|
-
sys.exit(1)
|
|
34
|
-
elif type_mime.startswith('image'):
|
|
35
|
-
click.echo("▶ Export type: image")
|
|
36
|
-
if mode not in MODES:
|
|
37
|
-
click.secho(f"✗ Error: Mode '{mode}' is not recognized.")
|
|
38
|
-
sys.exit(1)
|
|
39
|
-
|
|
40
|
-
module = MODES[mode]
|
|
41
|
-
image = module.Image.from_path(source_path, width, height)
|
|
42
|
-
image.export(target_filename)
|
|
43
|
-
click.echo("✓ Conversion complete!")
|
|
44
|
-
elif type_mime.startswith('video'):
|
|
45
|
-
click.echo("▶ Export type: video")
|
|
46
|
-
if mode not in MODES:
|
|
47
|
-
click.secho(f"✗ Error: Mode '{mode}' is not recognized.")
|
|
48
|
-
sys.exit(1)
|
|
49
|
-
|
|
50
|
-
module = MODES[mode]
|
|
51
|
-
video = module.Video(source_path, width, height)
|
|
52
|
-
click.echo(f"Please note: You are about to export a video with the following settings: length=({video.width}, {video.height})")
|
|
53
|
-
click.confirm("Do you want to continue?", abort=True)
|
|
54
|
-
video.export(target_filename, sound=sound)
|
|
55
|
-
click.echo("✓ Conversion complete!")
|
|
56
|
-
if sound:
|
|
57
|
-
click.echo(f"✓ Your file is available here: {target_filename}_audio.mp4")
|
|
58
|
-
else:
|
|
59
|
-
click.echo(f"✓ Your file is available here: {target_filename}.mp4")
|
|
60
|
-
else:
|
|
61
|
-
click.secho("✗ Error: Unable to determine which export type to use")
|
|
62
|
-
sys.exit(1)
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
@click.command()
|
|
66
|
-
@click.argument("source_path", type=click.Path(exists=True))
|
|
67
|
-
@click.option("--mode", "-m", default="full_mode", help="full_mode / old_skool / new_skool")
|
|
68
|
-
def create_and_echo(source_path, mode):
|
|
69
|
-
"""
|
|
70
|
-
Creates and prints an ASCII art image or video.
|
|
71
|
-
"""
|
|
72
|
-
max_width, max_height = shutil.get_terminal_size()
|
|
73
|
-
max_height *= 2
|
|
74
|
-
click.echo(f"▶ Converting '{source_path}'...")
|
|
75
|
-
click.echo(f"▶ Settings: mode={mode}")
|
|
76
|
-
|
|
77
|
-
type_mime, _ = mimetypes.guess_type(source_path)
|
|
78
|
-
|
|
79
|
-
if type_mime is None:
|
|
80
|
-
click.secho("✗ Error: Unable to determine which export type to use")
|
|
81
|
-
sys.exit(1)
|
|
82
|
-
elif type_mime.startswith('image'):
|
|
83
|
-
click.echo("▶ Echo type: image")
|
|
84
|
-
if mode not in MODES:
|
|
85
|
-
click.secho(f"✗ Error: Mode '{mode}' is not recognized.")
|
|
86
|
-
sys.exit(1)
|
|
87
|
-
|
|
88
|
-
module = MODES[mode]
|
|
89
|
-
image = module.Image.from_path(source_path, width, height)
|
|
90
|
-
click.echo(image.get_alternate_lines())
|
|
91
|
-
click.echo("✓ Conversion complete!")
|
|
92
|
-
elif type_mime.startswith('video'):
|
|
93
|
-
click.echo("▶ Echo type: video")
|
|
94
|
-
if mode not in MODES:
|
|
95
|
-
click.secho(f"✗ Error: Mode '{mode}' is not recognized.")
|
|
96
|
-
sys.exit(1)
|
|
97
|
-
|
|
98
|
-
module = MODES[mode]
|
|
99
|
-
video = module.Video(source_path, height = max_height)
|
|
100
|
-
if video.width > max_width:
|
|
101
|
-
video = module.Video(source_path, width = max_width)
|
|
102
|
-
click.clear()
|
|
103
|
-
video.print_in_terminal(clean_print)
|
|
104
|
-
click.echo("✓ Conversion complete!")
|
|
105
|
-
else:
|
|
106
|
-
click.secho("✗ Error: Unable to determine which export type to use")
|
|
107
|
-
sys.exit(1)
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{ascii_art_python-2.0.2 → ascii_art_python-2.1.1}/src/ascii_art_python.egg-info/dependency_links.txt
RENAMED
|
File without changes
|
{ascii_art_python-2.0.2 → ascii_art_python-2.1.1}/src/ascii_art_python.egg-info/entry_points.txt
RENAMED
|
File without changes
|
{ascii_art_python-2.0.2 → ascii_art_python-2.1.1}/src/ascii_art_python.egg-info/requires.txt
RENAMED
|
File without changes
|
{ascii_art_python-2.0.2 → ascii_art_python-2.1.1}/src/ascii_art_python.egg-info/top_level.txt
RENAMED
|
File without changes
|