PDASC 0.1.0__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.
- core/__init__.py +10 -0
- core/ascii_converter.py +111 -0
- core/ascii_displayer.py +317 -0
- core/ascii_file_encoding.py +258 -0
- core/audio_player.py +150 -0
- core/generate_color_ramp.py +69 -0
- core/utils.py +26 -0
- core/video_ascii_video.py +220 -0
- core/video_extractor.py +128 -0
- pdasc/__init__.py +0 -0
- pdasc/fonts/CascadiaMono.ttf +0 -0
- pdasc/fonts/font8x8.ttf +0 -0
- pdasc/main.py +296 -0
- pdasc-0.1.0.dist-info/METADATA +16 -0
- pdasc-0.1.0.dist-info/RECORD +26 -0
- pdasc-0.1.0.dist-info/WHEEL +5 -0
- pdasc-0.1.0.dist-info/entry_points.txt +2 -0
- pdasc-0.1.0.dist-info/licenses/LICENSE +21 -0
- pdasc-0.1.0.dist-info/top_level.txt +3 -0
- web/__init__.py +4 -0
- web/image_controller/__init__.py +1 -0
- web/image_controller/app.py +99 -0
- web/image_controller/templates/index.html +383 -0
- web/video_player/__init__.py +1 -0
- web/video_player/app.py +47 -0
- web/video_player/templates/index.html +296 -0
pdasc/main.py
ADDED
|
@@ -0,0 +1,296 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
|
|
3
|
+
# Suppress resource_tracker semaphore warnings from sounddevice
|
|
4
|
+
# Must be done before any other imports
|
|
5
|
+
import os
|
|
6
|
+
os.environ['PYTHONWARNINGS'] = 'ignore'
|
|
7
|
+
|
|
8
|
+
import warnings
|
|
9
|
+
warnings.filterwarnings(
|
|
10
|
+
"ignore",
|
|
11
|
+
message=".*resource_tracker.*semaphore.*"
|
|
12
|
+
)
|
|
13
|
+
|
|
14
|
+
from core import AsciiConverter, AsciiDisplayer, AsciiEncoder, generate_color_ramp, get_charmap, render_charmap, VideoAsciiConverter, process_video
|
|
15
|
+
from web import create_video_server, ImageServer
|
|
16
|
+
from PIL import Image
|
|
17
|
+
import argparse
|
|
18
|
+
import sys
|
|
19
|
+
from importlib.resources import files
|
|
20
|
+
|
|
21
|
+
def add_common_args(parser):
|
|
22
|
+
"""Add arguments common to both play and encode"""
|
|
23
|
+
parser.add_argument(
|
|
24
|
+
"-b", "--block-size",
|
|
25
|
+
type=int,
|
|
26
|
+
default=8,
|
|
27
|
+
help="Size of character blocks (default: 8)"
|
|
28
|
+
)
|
|
29
|
+
|
|
30
|
+
parser.add_argument(
|
|
31
|
+
"-n", "--num-ascii",
|
|
32
|
+
type=int,
|
|
33
|
+
default=8,
|
|
34
|
+
help="Number of ASCII characters to use (default: 8)"
|
|
35
|
+
)
|
|
36
|
+
|
|
37
|
+
parser.add_argument(
|
|
38
|
+
"-f", "--font",
|
|
39
|
+
type=str,
|
|
40
|
+
default=os.path.join(os.path.dirname(__file__), "fonts", "CascadiaMono.ttf"),
|
|
41
|
+
help="Path to font file to create the ASCII character set from and to display on the website"
|
|
42
|
+
)
|
|
43
|
+
|
|
44
|
+
parser.add_argument(
|
|
45
|
+
"--no-color",
|
|
46
|
+
action="store_true",
|
|
47
|
+
help="Disable color output"
|
|
48
|
+
)
|
|
49
|
+
|
|
50
|
+
parser.add_argument(
|
|
51
|
+
"--no-audio",
|
|
52
|
+
action="store_true",
|
|
53
|
+
help="Disable audio playback for videos"
|
|
54
|
+
)
|
|
55
|
+
|
|
56
|
+
def cmd_play(args):
|
|
57
|
+
"""Play command - display images/videos/camera"""
|
|
58
|
+
# Validate font
|
|
59
|
+
if not os.path.exists(args.font):
|
|
60
|
+
print(f"Error: Font file '{args.font}' not found", file=sys.stderr)
|
|
61
|
+
sys.exit(1)
|
|
62
|
+
|
|
63
|
+
converter = AsciiConverter(num_ascii=args.num_ascii, chunk_size=args.block_size, font_path=args.font)
|
|
64
|
+
displayer = AsciiDisplayer(converter, args.debug)
|
|
65
|
+
|
|
66
|
+
try:
|
|
67
|
+
if args.input == "camera":
|
|
68
|
+
print(f"Starting camera {args.camera} (press Ctrl+C to stop)")
|
|
69
|
+
displayer.display_camera(camera_index=args.camera, color=not args.no_color)
|
|
70
|
+
else:
|
|
71
|
+
# Validate input file
|
|
72
|
+
if not os.path.exists(args.input):
|
|
73
|
+
print(f"Error: File '{args.input}' not found", file=sys.stderr)
|
|
74
|
+
sys.exit(1)
|
|
75
|
+
|
|
76
|
+
# Determine file type
|
|
77
|
+
video_extensions = {'.mp4', '.avi', '.mov', '.mkv', '.webm', '.flv', '.wmv', '.m4v'}
|
|
78
|
+
image_extensions = {'.png', '.jpg', '.jpeg', '.gif', '.bmp', '.tiff', '.webp'}
|
|
79
|
+
special_extensions = {'.asc'}
|
|
80
|
+
|
|
81
|
+
ext = os.path.splitext(args.input)[-1].lower()
|
|
82
|
+
|
|
83
|
+
if ext not in video_extensions and ext not in image_extensions and ext not in special_extensions:
|
|
84
|
+
print(f"Error: Unsupported file extension '{ext}'", file=sys.stderr)
|
|
85
|
+
print(f"Supported: .asc, {', '.join(sorted(video_extensions | image_extensions))}", file=sys.stderr)
|
|
86
|
+
sys.exit(1)
|
|
87
|
+
|
|
88
|
+
if ext == '.asc':
|
|
89
|
+
print(f"Playing .asc file: {args.input}")
|
|
90
|
+
displayer.display_asc_file(args.input, not args.no_audio)
|
|
91
|
+
elif ext in video_extensions:
|
|
92
|
+
print(f"Playing video: {args.input}")
|
|
93
|
+
displayer.display_video(
|
|
94
|
+
video_path=args.input,
|
|
95
|
+
play_audio=not args.no_audio,
|
|
96
|
+
color=not args.no_color
|
|
97
|
+
)
|
|
98
|
+
else:
|
|
99
|
+
print(f"Displaying image: {args.input}")
|
|
100
|
+
displayer.display_image(image=Image.open(args.input), color=not args.no_color)
|
|
101
|
+
|
|
102
|
+
except KeyboardInterrupt:
|
|
103
|
+
print("\nInterrupted by user", file=sys.stderr)
|
|
104
|
+
sys.exit(0)
|
|
105
|
+
except Exception as e:
|
|
106
|
+
print(f"Error: {e}", file=sys.stderr)
|
|
107
|
+
sys.exit(1)
|
|
108
|
+
|
|
109
|
+
def cmd_encode(args):
|
|
110
|
+
"""Encode command - save encoded ASCII to file"""
|
|
111
|
+
# Validate font
|
|
112
|
+
if not os.path.exists(args.font):
|
|
113
|
+
print(f"Error: Font file '{args.font}' not found", file=sys.stderr)
|
|
114
|
+
sys.exit(1)
|
|
115
|
+
|
|
116
|
+
print(f"Encoding {args.input} to {args.output}")
|
|
117
|
+
# Validate input file
|
|
118
|
+
if not os.path.exists(args.input):
|
|
119
|
+
print(f"Error: File '{args.input}' not found", file=sys.stderr)
|
|
120
|
+
sys.exit(1)
|
|
121
|
+
|
|
122
|
+
# Determine file type
|
|
123
|
+
video_extensions = {'.mp4', '.avi', '.mov', '.mkv', '.webm', '.flv', '.wmv', '.m4v'}
|
|
124
|
+
image_extensions = {'.png', '.jpg', '.jpeg', '.gif', '.bmp', '.tiff', '.webp'}
|
|
125
|
+
|
|
126
|
+
ext = os.path.splitext(args.input)[-1].lower()
|
|
127
|
+
|
|
128
|
+
if ext not in video_extensions and ext not in image_extensions:
|
|
129
|
+
print(f"Error: Unsupported file extension '{ext}'", file=sys.stderr)
|
|
130
|
+
print(f"Supported: {', '.join(sorted(video_extensions | image_extensions))}", file=sys.stderr)
|
|
131
|
+
sys.exit(1)
|
|
132
|
+
|
|
133
|
+
encoder = AsciiEncoder()
|
|
134
|
+
converter = AsciiConverter(num_ascii=args.num_ascii, chunk_size=args.block_size, font_path=args.font)
|
|
135
|
+
|
|
136
|
+
if ext in video_extensions:
|
|
137
|
+
encoder.encode_video_to_asc(args.input, args.output, not args.no_audio, not args.no_color, converter)
|
|
138
|
+
else:
|
|
139
|
+
encoder.encode_image_to_asc(args.input, args.output, not args.no_color, converter)
|
|
140
|
+
|
|
141
|
+
def cmd_website(args):
|
|
142
|
+
if args.input:
|
|
143
|
+
video_extensions = {'.mp4', '.avi', '.mov', '.mkv', '.webm', '.flv', '.wmv', '.m4v'}
|
|
144
|
+
ext = os.path.splitext(args.input)[-1].lower()
|
|
145
|
+
if os.path.splitext(os.path.splitext(args.input)[0])[-1].lower() == '.asc':
|
|
146
|
+
app = create_video_server(args.input)
|
|
147
|
+
app.run()
|
|
148
|
+
elif ext in video_extensions:
|
|
149
|
+
with open("shaders/ascii.frag") as file:
|
|
150
|
+
frac_src = file.read()
|
|
151
|
+
font_path = str(files("pdasc.fonts").joinpath("font8x8.ttf"))
|
|
152
|
+
charmap_img = render_charmap(get_charmap(generate_color_ramp(font_path=font_path), levels=16), font_path=font_path)
|
|
153
|
+
converter = VideoAsciiConverter(frac_src, charmap_img, not args.no_color)
|
|
154
|
+
out_path = process_video(converter, args.input, args.output, not args.no_audio)
|
|
155
|
+
app = create_video_server(out_path)
|
|
156
|
+
app.run()
|
|
157
|
+
else:
|
|
158
|
+
print("Invalid input for website. Must be a valid video file or not specified")
|
|
159
|
+
else:
|
|
160
|
+
app = ImageServer(font_path=str(files("pdasc.fonts").joinpath("CascadiaMono.ttf")))
|
|
161
|
+
app.run(port=args.port)
|
|
162
|
+
|
|
163
|
+
def main():
|
|
164
|
+
parser = argparse.ArgumentParser(
|
|
165
|
+
description="Convert images and videos to ASCII art",
|
|
166
|
+
formatter_class=argparse.RawDescriptionHelpFormatter
|
|
167
|
+
)
|
|
168
|
+
|
|
169
|
+
subparsers = parser.add_subparsers(dest='command', help='Available commands')
|
|
170
|
+
|
|
171
|
+
# Play subcommand
|
|
172
|
+
play_parser = subparsers.add_parser(
|
|
173
|
+
'play',
|
|
174
|
+
help='Display images/videos/camera as ASCII art',
|
|
175
|
+
formatter_class=argparse.RawDescriptionHelpFormatter,
|
|
176
|
+
epilog="""
|
|
177
|
+
Examples:
|
|
178
|
+
%(prog)s image.png
|
|
179
|
+
%(prog)s video.mp4 --no-audio
|
|
180
|
+
%(prog)s camera -c 0
|
|
181
|
+
%(prog)s image.jpg -b 16 -n 70
|
|
182
|
+
"""
|
|
183
|
+
)
|
|
184
|
+
|
|
185
|
+
add_common_args(play_parser)
|
|
186
|
+
|
|
187
|
+
play_parser.add_argument(
|
|
188
|
+
"input",
|
|
189
|
+
type=str,
|
|
190
|
+
help='Path to input file or "camera" for camera input'
|
|
191
|
+
)
|
|
192
|
+
|
|
193
|
+
play_parser.add_argument(
|
|
194
|
+
"-c", "--camera",
|
|
195
|
+
type=int,
|
|
196
|
+
default=0,
|
|
197
|
+
help="Camera index when using camera input (default: 0)"
|
|
198
|
+
)
|
|
199
|
+
|
|
200
|
+
play_parser.add_argument(
|
|
201
|
+
"--debug",
|
|
202
|
+
action="store_true",
|
|
203
|
+
help="Enable debug mode to show FPS and other debug info"
|
|
204
|
+
)
|
|
205
|
+
|
|
206
|
+
play_parser.set_defaults(func=cmd_play)
|
|
207
|
+
|
|
208
|
+
# Encode subcommand
|
|
209
|
+
encode_parser = subparsers.add_parser(
|
|
210
|
+
'encode',
|
|
211
|
+
help='Encode video/image to compressed ASCII file',
|
|
212
|
+
formatter_class=argparse.RawDescriptionHelpFormatter,
|
|
213
|
+
epilog="""
|
|
214
|
+
Examples:
|
|
215
|
+
%(prog)s video.mp4 -o output.asc
|
|
216
|
+
%(prog)s image.png -o output.asc --no-color
|
|
217
|
+
"""
|
|
218
|
+
)
|
|
219
|
+
|
|
220
|
+
encode_parser.add_argument(
|
|
221
|
+
"input",
|
|
222
|
+
type=str,
|
|
223
|
+
help="Path to input video or image file"
|
|
224
|
+
)
|
|
225
|
+
|
|
226
|
+
encode_parser.add_argument(
|
|
227
|
+
"-o", "--output",
|
|
228
|
+
type=str,
|
|
229
|
+
default="ascii_out.asc",
|
|
230
|
+
help="Output file path"
|
|
231
|
+
)
|
|
232
|
+
|
|
233
|
+
add_common_args(encode_parser)
|
|
234
|
+
|
|
235
|
+
encode_parser.set_defaults(func=cmd_encode)
|
|
236
|
+
|
|
237
|
+
website_parser = subparsers.add_parser(
|
|
238
|
+
'website',
|
|
239
|
+
help='Open image demo website or website to display ASCII shaded video',
|
|
240
|
+
formatter_class=argparse.RawDescriptionHelpFormatter,
|
|
241
|
+
epilog="""
|
|
242
|
+
Usage:
|
|
243
|
+
%(prog)s
|
|
244
|
+
%(prog)s video.mp4
|
|
245
|
+
%(prog)s ascii_video.asc.mp4
|
|
246
|
+
"""
|
|
247
|
+
)
|
|
248
|
+
|
|
249
|
+
website_parser.add_argument(
|
|
250
|
+
"input",
|
|
251
|
+
nargs='?',
|
|
252
|
+
type=str,
|
|
253
|
+
default=None,
|
|
254
|
+
help='Path to video to display on website (optional)'
|
|
255
|
+
)
|
|
256
|
+
|
|
257
|
+
website_parser.add_argument(
|
|
258
|
+
"-o", "--output",
|
|
259
|
+
type=str,
|
|
260
|
+
default="",
|
|
261
|
+
help="Output file path"
|
|
262
|
+
)
|
|
263
|
+
|
|
264
|
+
website_parser.add_argument(
|
|
265
|
+
"-p", "--port",
|
|
266
|
+
type=int,
|
|
267
|
+
default=5000,
|
|
268
|
+
help="Port to run website on"
|
|
269
|
+
)
|
|
270
|
+
|
|
271
|
+
website_parser.add_argument(
|
|
272
|
+
"--no-color",
|
|
273
|
+
action="store_true",
|
|
274
|
+
help="Disable color output"
|
|
275
|
+
)
|
|
276
|
+
|
|
277
|
+
website_parser.add_argument(
|
|
278
|
+
"--no-audio",
|
|
279
|
+
action="store_true",
|
|
280
|
+
help="Disable audio playback for videos"
|
|
281
|
+
)
|
|
282
|
+
|
|
283
|
+
website_parser.set_defaults(func=cmd_website)
|
|
284
|
+
|
|
285
|
+
args = parser.parse_args()
|
|
286
|
+
|
|
287
|
+
# Show help if no command specified
|
|
288
|
+
if not args.command:
|
|
289
|
+
parser.print_help()
|
|
290
|
+
sys.exit(1)
|
|
291
|
+
|
|
292
|
+
# Call correct sub-command
|
|
293
|
+
args.func(args)
|
|
294
|
+
|
|
295
|
+
if __name__ == "__main__":
|
|
296
|
+
main()
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: PDASC
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Made for Intersession 2026
|
|
5
|
+
Author-email: Colin Politi <urboycolinthepanda@gmail.com>
|
|
6
|
+
Requires-Python: >=3.10
|
|
7
|
+
License-File: LICENSE
|
|
8
|
+
Requires-Dist: numpy>=1.20.0
|
|
9
|
+
Requires-Dist: Pillow>=9.0.0
|
|
10
|
+
Requires-Dist: numba>=0.56.0
|
|
11
|
+
Requires-Dist: pyaudio>=0.2.11
|
|
12
|
+
Requires-Dist: opencv-python>=4.5.0
|
|
13
|
+
Requires-Dist: zstandard>=0.18.0
|
|
14
|
+
Requires-Dist: flask>=2.0.0
|
|
15
|
+
Requires-Dist: moderngl>=5.6.0
|
|
16
|
+
Dynamic: license-file
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
core/__init__.py,sha256=YuZURZG4sVzajPjDD4infnkBunphk2n1vOPEbrhGa3M,785
|
|
2
|
+
core/ascii_converter.py,sha256=Jh4Rl-40jckmcoSixpEEs7NuQplL60LYqjSPkfEck_0,3969
|
|
3
|
+
core/ascii_displayer.py,sha256=Dp1JmCFJW0BfY-8MqeGifqMEvLH7Xw703p5ImE54cpA,11891
|
|
4
|
+
core/ascii_file_encoding.py,sha256=qHrlZwGV058hjbVBaPmVAeL-_dIKe88x3PveVod_5H0,9695
|
|
5
|
+
core/audio_player.py,sha256=a0IQBft4h2fGMTVRm8EapZJhL3lVMW3hn8lj2N0obB8,5594
|
|
6
|
+
core/generate_color_ramp.py,sha256=UBnrOmjG0G8mFXVWehG1wKfIM8dDllnBuwopia9DZho,2336
|
|
7
|
+
core/utils.py,sha256=aL3lmt7uX6NZB6EqqNLt4SiJfBFlfhhqXwKuJnhuQTs,868
|
|
8
|
+
core/video_ascii_video.py,sha256=8EIaBc96oT0FYMKSIRsKlreSPVRq2hkRwrRkAREdsGs,7490
|
|
9
|
+
core/video_extractor.py,sha256=Gqyrj53qbHeRR7WDD70qvia2OowCbmxq8LIMwaox3Pg,4206
|
|
10
|
+
pdasc/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
11
|
+
pdasc/main.py,sha256=hoeR6oHvddnAjsK1pCLFk6mGWCTzu29Nml5rAl3Krn8,9937
|
|
12
|
+
pdasc/fonts/CascadiaMono.ttf,sha256=CAJvYX7qMVyspZiYeE6Zn2zbLQw00gQN7rBCvNw1n6I,714804
|
|
13
|
+
pdasc/fonts/font8x8.ttf,sha256=YQF0gD089-zxbgsLg_hklnec42cSvjfsOOehDZVSuRg,66484
|
|
14
|
+
pdasc-0.1.0.dist-info/licenses/LICENSE,sha256=1ss2MifclFKIfzZXjfcFh-LuQuHvNLhKG8fAnKWy7SM,1090
|
|
15
|
+
web/__init__.py,sha256=4-_vwOO6HfcyeKPs4oU2lACf8lWZb3GijVu-wM1l4Qc,140
|
|
16
|
+
web/image_controller/__init__.py,sha256=Q72s3s3C_5iYRHScILKs7DD2dBw1_E2NFOt4ab51poc,28
|
|
17
|
+
web/image_controller/app.py,sha256=V2DFsvroQ4_aT8mdmhSWCjnM_kGu6fxfNwE-dwupOQY,3914
|
|
18
|
+
web/image_controller/templates/index.html,sha256=2pjUlsN9jfNOxubnApiYnSj1IYl4uMqqIBgr9NUNYpA,10669
|
|
19
|
+
web/video_player/__init__.py,sha256=g3L-NXzSsLTh3w40QSzU5U4rqxVmZgI0cAmiyiU4czQ,36
|
|
20
|
+
web/video_player/app.py,sha256=K8I87xLMlRAOGSaSa7DU8SbjCFZpX27aMOnf5UchADE,1296
|
|
21
|
+
web/video_player/templates/index.html,sha256=f6qpVInwXAushhHwZIwLunJgTa1ixSu1LoNFKkqzMkk,8208
|
|
22
|
+
pdasc-0.1.0.dist-info/METADATA,sha256=n-AFMuMVOurWh1Kw9x_YODeTymL-UH0Q77FTlmctOFU,473
|
|
23
|
+
pdasc-0.1.0.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
|
|
24
|
+
pdasc-0.1.0.dist-info/entry_points.txt,sha256=DjIudun90vxvj4UvWXM07l07CvevQrPHn9LGwRyTjHQ,42
|
|
25
|
+
pdasc-0.1.0.dist-info/top_level.txt,sha256=swqiWpl30iX2Jy2LDoWFkBD7mIHD_cPgzOVd5mT833A,15
|
|
26
|
+
pdasc-0.1.0.dist-info/RECORD,,
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 ColinThePanda
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
web/__init__.py
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
from .app import ImageServer
|
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
import sys
|
|
2
|
+
import os
|
|
3
|
+
# add root to path to import from core
|
|
4
|
+
sys.path.insert(0, os.path.join(__file__, "../../../"))
|
|
5
|
+
|
|
6
|
+
from flask import Flask, render_template, request
|
|
7
|
+
from PIL import Image
|
|
8
|
+
from core import AsciiConverter, AsciiDisplayer
|
|
9
|
+
from typing import Any
|
|
10
|
+
|
|
11
|
+
class ImageServer:
|
|
12
|
+
def __init__(self, font_path: str | None = None) -> None:
|
|
13
|
+
self.app = Flask(__name__)
|
|
14
|
+
self._setup_app()
|
|
15
|
+
self.converter = AsciiConverter(font_path=font_path) if font_path else AsciiConverter()
|
|
16
|
+
self.displayer = AsciiDisplayer(self.converter)
|
|
17
|
+
self.colored = True
|
|
18
|
+
|
|
19
|
+
def _html_from_ansi(self, ansi: str) -> str:
|
|
20
|
+
if not self.colored:
|
|
21
|
+
# Escape HTML characters and replace newlines with <br> for plain ASCII
|
|
22
|
+
import html
|
|
23
|
+
escaped = html.escape(ansi)
|
|
24
|
+
return f'<span>{escaped.replace("\n", "<br>")}</span>'
|
|
25
|
+
|
|
26
|
+
import re
|
|
27
|
+
|
|
28
|
+
html_parts = []
|
|
29
|
+
current_color = None
|
|
30
|
+
|
|
31
|
+
# Pattern to match ANSI escape codes
|
|
32
|
+
ansi_pattern = re.compile(r'\x1b\[([0-9;]+)m')
|
|
33
|
+
|
|
34
|
+
last_end = 0
|
|
35
|
+
for match in ansi_pattern.finditer(ansi):
|
|
36
|
+
# Add any text before this escape code
|
|
37
|
+
text = ansi[last_end:match.start()]
|
|
38
|
+
if text:
|
|
39
|
+
if current_color:
|
|
40
|
+
html_parts.append(f'<span style="color:{current_color}">{text}</span>')
|
|
41
|
+
else:
|
|
42
|
+
html_parts.append(text)
|
|
43
|
+
|
|
44
|
+
# Parse the escape code
|
|
45
|
+
codes = match.group(1).split(';')
|
|
46
|
+
i = 0
|
|
47
|
+
while i < len(codes):
|
|
48
|
+
code = codes[i]
|
|
49
|
+
|
|
50
|
+
if code == '0' or code == '': # Reset
|
|
51
|
+
current_color = None
|
|
52
|
+
elif code == '38' and i + 2 < len(codes) and codes[i + 1] == '2': # RGB foreground
|
|
53
|
+
r, g, b = codes[i + 2], codes[i + 3], codes[i + 4]
|
|
54
|
+
current_color = f'rgb({r},{g},{b})'
|
|
55
|
+
i += 4
|
|
56
|
+
|
|
57
|
+
i += 1
|
|
58
|
+
|
|
59
|
+
last_end = match.end()
|
|
60
|
+
|
|
61
|
+
# Add any remaining text
|
|
62
|
+
text = ansi[last_end:]
|
|
63
|
+
if text:
|
|
64
|
+
if current_color:
|
|
65
|
+
html_parts.append(f'<span style="color:{current_color}">{text}</span>')
|
|
66
|
+
else:
|
|
67
|
+
html_parts.append(text)
|
|
68
|
+
|
|
69
|
+
return ''.join(html_parts).replace('\n', '<br>')
|
|
70
|
+
|
|
71
|
+
def _setup_app(self):
|
|
72
|
+
self.app.config['MAX_CONTENT_LENGTH'] = 16 * 1024 * 1024
|
|
73
|
+
@self.app.route('/')
|
|
74
|
+
def index():
|
|
75
|
+
return render_template(
|
|
76
|
+
"index.html"
|
|
77
|
+
)
|
|
78
|
+
|
|
79
|
+
@self.app.route('/ascii', methods=['POST'])
|
|
80
|
+
def ascii():
|
|
81
|
+
num_ascii = request.form.get('num_ascii')
|
|
82
|
+
block_size = request.form.get('block_size')
|
|
83
|
+
colored = request.form.get('colored')
|
|
84
|
+
image_file = request.files.get('image') # This is base64 encoded
|
|
85
|
+
|
|
86
|
+
if not num_ascii or not block_size or not colored or not image_file:
|
|
87
|
+
return "Error one of the inputs is not defined"
|
|
88
|
+
|
|
89
|
+
self.converter.num_ascii = int(num_ascii)
|
|
90
|
+
self.converter.chunk_size = int(block_size)
|
|
91
|
+
self.colored = bool(int(colored))
|
|
92
|
+
image = Image.open(image_file.stream)
|
|
93
|
+
self.converter.regen_charmap()
|
|
94
|
+
ascii = self.converter.get_ascii(image, self.colored)
|
|
95
|
+
frame = self.displayer.render_ascii(ascii, self.colored)
|
|
96
|
+
return self._html_from_ansi(frame)
|
|
97
|
+
|
|
98
|
+
def run(self, host: str | None = None, port: int | None = None, debug: bool | None = None, load_dotenv: bool = True, **options: Any):
|
|
99
|
+
self.app.run(host=host, port=port, debug=debug, load_dotenv=load_dotenv, **options)
|