dotz 0.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.
- dotz-0.1/LICENSE +21 -0
- dotz-0.1/PKG-INFO +156 -0
- dotz-0.1/README.md +113 -0
- dotz-0.1/dotz.egg-info/PKG-INFO +156 -0
- dotz-0.1/dotz.egg-info/SOURCES.txt +11 -0
- dotz-0.1/dotz.egg-info/dependency_links.txt +1 -0
- dotz-0.1/dotz.egg-info/entry_points.txt +2 -0
- dotz-0.1/dotz.egg-info/requires.txt +2 -0
- dotz-0.1/dotz.egg-info/top_level.txt +1 -0
- dotz-0.1/dotz.py +513 -0
- dotz-0.1/pyproject.toml +3 -0
- dotz-0.1/setup.cfg +4 -0
- dotz-0.1/setup.py +57 -0
dotz-0.1/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Arun Prakash Jana
|
|
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.
|
dotz-0.1/PKG-INFO
ADDED
|
@@ -0,0 +1,156 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: dotz
|
|
3
|
+
Version: 0.1
|
|
4
|
+
Summary: Render images and video previews as Braille art in the terminal with color and animation support.
|
|
5
|
+
Home-page: https://github.com/jarun/dotz
|
|
6
|
+
Author: Arun Prakash Jana
|
|
7
|
+
License: MIT
|
|
8
|
+
Project-URL: Homepage, https://github.com/jarun/dotz
|
|
9
|
+
Project-URL: Repository, https://github.com/jarun/dotz
|
|
10
|
+
Project-URL: Issues, https://github.com/jarun/dotz/issues
|
|
11
|
+
Keywords: terminal image-viewer braille curses gif video ffmpeg
|
|
12
|
+
Classifier: Development Status :: 4 - Beta
|
|
13
|
+
Classifier: Environment :: Console :: Curses
|
|
14
|
+
Classifier: Intended Audience :: End Users/Desktop
|
|
15
|
+
Classifier: Operating System :: POSIX :: Linux
|
|
16
|
+
Classifier: Programming Language :: Python :: 3
|
|
17
|
+
Classifier: Programming Language :: Python :: 3 :: Only
|
|
18
|
+
Classifier: Programming Language :: Python :: 3.7
|
|
19
|
+
Classifier: Programming Language :: Python :: 3.8
|
|
20
|
+
Classifier: Programming Language :: Python :: 3.9
|
|
21
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
22
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
23
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
24
|
+
Classifier: Topic :: Multimedia :: Graphics
|
|
25
|
+
Classifier: Topic :: Terminals
|
|
26
|
+
Requires-Python: >=3.7
|
|
27
|
+
Description-Content-Type: text/markdown
|
|
28
|
+
License-File: LICENSE
|
|
29
|
+
Requires-Dist: numpy>=1.20
|
|
30
|
+
Requires-Dist: Pillow>=8.0
|
|
31
|
+
Dynamic: author
|
|
32
|
+
Dynamic: classifier
|
|
33
|
+
Dynamic: description
|
|
34
|
+
Dynamic: description-content-type
|
|
35
|
+
Dynamic: home-page
|
|
36
|
+
Dynamic: keywords
|
|
37
|
+
Dynamic: license
|
|
38
|
+
Dynamic: license-file
|
|
39
|
+
Dynamic: project-url
|
|
40
|
+
Dynamic: requires-dist
|
|
41
|
+
Dynamic: requires-python
|
|
42
|
+
Dynamic: summary
|
|
43
|
+
|
|
44
|
+
# dotz
|
|
45
|
+
|
|
46
|
+
Render images and video previews as Braille art in the terminal with xterm-256 color and ncurses dim/normal/bold attributes.
|
|
47
|
+
|
|
48
|
+
It was written to be used as a terminal image viewer with [`nnn`](https://github.com/jarun/nnn). Works independently too.
|
|
49
|
+
|
|
50
|
+
## Features
|
|
51
|
+
|
|
52
|
+
- Braille art rendering for images
|
|
53
|
+
- Animated GIF support
|
|
54
|
+
- xterm-256 color and grayscale
|
|
55
|
+
- Dithering options (ordered, error diffusion)
|
|
56
|
+
- Video preview (frame extraction with ffmpeg)
|
|
57
|
+
- Keyboard navigation and slideshow mode
|
|
58
|
+
|
|
59
|
+
<br>
|
|
60
|
+
<img width="1323" height="826" alt="image_01" src="https://github.com/user-attachments/assets/f2becbbc-cfeb-42b3-bd92-3882ff3fb570" />
|
|
61
|
+
<br><br>
|
|
62
|
+
<img width="1333" height="827" alt="image_02" src="https://github.com/user-attachments/assets/62bc16a8-246b-4b5a-a11e-fd0faa5c8066" />
|
|
63
|
+
<br><br>
|
|
64
|
+
<img width="1301" height="954" alt="image_03" src="https://github.com/user-attachments/assets/609805be-c0c5-4815-bb33-3bc70d69c152" />
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
## Installation
|
|
68
|
+
|
|
69
|
+
Install the required dependencies and the dotz command:
|
|
70
|
+
|
|
71
|
+
```sh
|
|
72
|
+
# Install system dependencies (e.g., ffmpeg)
|
|
73
|
+
sudo apt-get install ffmpeg # or use your OS package manager
|
|
74
|
+
|
|
75
|
+
# Install Python dependencies and the CLI tool
|
|
76
|
+
sudo pip3 install .
|
|
77
|
+
```
|
|
78
|
+
|
|
79
|
+
After installation, you can run the tool using:
|
|
80
|
+
|
|
81
|
+
```sh
|
|
82
|
+
dotz [options] <file-or-directory>
|
|
83
|
+
```
|
|
84
|
+
|
|
85
|
+
## Usage
|
|
86
|
+
|
|
87
|
+
```
|
|
88
|
+
usage: dotz [-h] [-S] [-C] [-d {ordered,error,none}] [-s [DELAY]] [-k SEEK] [-f {jpeg,png}] [path]
|
|
89
|
+
|
|
90
|
+
Render an image or all images/videos in a directory as Braille cells using ncurses with optional xterm-256 color.
|
|
91
|
+
|
|
92
|
+
positional arguments:
|
|
93
|
+
path Path to the image/video file or directory (optional)
|
|
94
|
+
|
|
95
|
+
options:
|
|
96
|
+
-h, --help show this help message and exit
|
|
97
|
+
-S, --no-sharpen Disable edge sharpening
|
|
98
|
+
-C, --no-color Disable color (greyscale only with dim/normal/bold)
|
|
99
|
+
-d {ordered,error,none}, --dither {ordered,error,none}
|
|
100
|
+
Dithering mode: ordered (default, clean), error (Floyd-Steinberg, smooth gradients), none
|
|
101
|
+
-s [DELAY], --slideshow [DELAY]
|
|
102
|
+
Enable slideshow mode with optional integer delay in seconds (default: 5).
|
|
103
|
+
-k SEEK, --seek SEEK Seek position to extract frame from videos in seconds (default: 10)
|
|
104
|
+
-f {jpeg,png}, --format {jpeg,png}
|
|
105
|
+
Format for extracted video frames: jpeg (default) or png
|
|
106
|
+
```
|
|
107
|
+
|
|
108
|
+
#### Examples
|
|
109
|
+
|
|
110
|
+
- Syntax:
|
|
111
|
+
```sh
|
|
112
|
+
python -m dotz <file-or-directory>
|
|
113
|
+
```
|
|
114
|
+
- To render a single image:
|
|
115
|
+
```sh
|
|
116
|
+
python -m dotz path/to/image.jpg
|
|
117
|
+
```
|
|
118
|
+
- To render all images and videos in a directory:
|
|
119
|
+
```sh
|
|
120
|
+
python -m dotz path/to/directory/
|
|
121
|
+
```
|
|
122
|
+
- To run a slideshow with a custom delay (e.g. 3 seconds):
|
|
123
|
+
```sh
|
|
124
|
+
python -m dotz -s 3 path/to/directory/
|
|
125
|
+
```
|
|
126
|
+
|
|
127
|
+
## Navigation
|
|
128
|
+
|
|
129
|
+
| Key | Action |
|
|
130
|
+
|-----------------|----------|
|
|
131
|
+
| Right, l, Space | Next |
|
|
132
|
+
| Left, h | Previous |
|
|
133
|
+
| Up | First |
|
|
134
|
+
| Down | Last |
|
|
135
|
+
| s | Toggle slideshow |
|
|
136
|
+
| S | Toggle reverse slideshow |
|
|
137
|
+
| q, Esc | Quit |
|
|
138
|
+
|
|
139
|
+
## Dependencies
|
|
140
|
+
|
|
141
|
+
| Package | Version | Usage |
|
|
142
|
+
|-----------|------------|--------------------------------------------|
|
|
143
|
+
| python | >=3.7 | Required Python version |
|
|
144
|
+
| numpy | >=1.20 | Fast array operations for image processing |
|
|
145
|
+
| Pillow | >=8.0 | Image loading and manipulation |
|
|
146
|
+
| ffmpeg | >=4.2 | Video frame extraction |
|
|
147
|
+
|
|
148
|
+
## Formats
|
|
149
|
+
|
|
150
|
+
**Image:** PNG, JPG, JPEG, BMP, GIF, TIFF, WEBP
|
|
151
|
+
|
|
152
|
+
**Video:** MP4, MKV, AVI, MOV, WEBM, FLV, WMV, MPEG, MPG
|
|
153
|
+
|
|
154
|
+
## License
|
|
155
|
+
|
|
156
|
+
MIT
|
dotz-0.1/README.md
ADDED
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
# dotz
|
|
2
|
+
|
|
3
|
+
Render images and video previews as Braille art in the terminal with xterm-256 color and ncurses dim/normal/bold attributes.
|
|
4
|
+
|
|
5
|
+
It was written to be used as a terminal image viewer with [`nnn`](https://github.com/jarun/nnn). Works independently too.
|
|
6
|
+
|
|
7
|
+
## Features
|
|
8
|
+
|
|
9
|
+
- Braille art rendering for images
|
|
10
|
+
- Animated GIF support
|
|
11
|
+
- xterm-256 color and grayscale
|
|
12
|
+
- Dithering options (ordered, error diffusion)
|
|
13
|
+
- Video preview (frame extraction with ffmpeg)
|
|
14
|
+
- Keyboard navigation and slideshow mode
|
|
15
|
+
|
|
16
|
+
<br>
|
|
17
|
+
<img width="1323" height="826" alt="image_01" src="https://github.com/user-attachments/assets/f2becbbc-cfeb-42b3-bd92-3882ff3fb570" />
|
|
18
|
+
<br><br>
|
|
19
|
+
<img width="1333" height="827" alt="image_02" src="https://github.com/user-attachments/assets/62bc16a8-246b-4b5a-a11e-fd0faa5c8066" />
|
|
20
|
+
<br><br>
|
|
21
|
+
<img width="1301" height="954" alt="image_03" src="https://github.com/user-attachments/assets/609805be-c0c5-4815-bb33-3bc70d69c152" />
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
## Installation
|
|
25
|
+
|
|
26
|
+
Install the required dependencies and the dotz command:
|
|
27
|
+
|
|
28
|
+
```sh
|
|
29
|
+
# Install system dependencies (e.g., ffmpeg)
|
|
30
|
+
sudo apt-get install ffmpeg # or use your OS package manager
|
|
31
|
+
|
|
32
|
+
# Install Python dependencies and the CLI tool
|
|
33
|
+
sudo pip3 install .
|
|
34
|
+
```
|
|
35
|
+
|
|
36
|
+
After installation, you can run the tool using:
|
|
37
|
+
|
|
38
|
+
```sh
|
|
39
|
+
dotz [options] <file-or-directory>
|
|
40
|
+
```
|
|
41
|
+
|
|
42
|
+
## Usage
|
|
43
|
+
|
|
44
|
+
```
|
|
45
|
+
usage: dotz [-h] [-S] [-C] [-d {ordered,error,none}] [-s [DELAY]] [-k SEEK] [-f {jpeg,png}] [path]
|
|
46
|
+
|
|
47
|
+
Render an image or all images/videos in a directory as Braille cells using ncurses with optional xterm-256 color.
|
|
48
|
+
|
|
49
|
+
positional arguments:
|
|
50
|
+
path Path to the image/video file or directory (optional)
|
|
51
|
+
|
|
52
|
+
options:
|
|
53
|
+
-h, --help show this help message and exit
|
|
54
|
+
-S, --no-sharpen Disable edge sharpening
|
|
55
|
+
-C, --no-color Disable color (greyscale only with dim/normal/bold)
|
|
56
|
+
-d {ordered,error,none}, --dither {ordered,error,none}
|
|
57
|
+
Dithering mode: ordered (default, clean), error (Floyd-Steinberg, smooth gradients), none
|
|
58
|
+
-s [DELAY], --slideshow [DELAY]
|
|
59
|
+
Enable slideshow mode with optional integer delay in seconds (default: 5).
|
|
60
|
+
-k SEEK, --seek SEEK Seek position to extract frame from videos in seconds (default: 10)
|
|
61
|
+
-f {jpeg,png}, --format {jpeg,png}
|
|
62
|
+
Format for extracted video frames: jpeg (default) or png
|
|
63
|
+
```
|
|
64
|
+
|
|
65
|
+
#### Examples
|
|
66
|
+
|
|
67
|
+
- Syntax:
|
|
68
|
+
```sh
|
|
69
|
+
python -m dotz <file-or-directory>
|
|
70
|
+
```
|
|
71
|
+
- To render a single image:
|
|
72
|
+
```sh
|
|
73
|
+
python -m dotz path/to/image.jpg
|
|
74
|
+
```
|
|
75
|
+
- To render all images and videos in a directory:
|
|
76
|
+
```sh
|
|
77
|
+
python -m dotz path/to/directory/
|
|
78
|
+
```
|
|
79
|
+
- To run a slideshow with a custom delay (e.g. 3 seconds):
|
|
80
|
+
```sh
|
|
81
|
+
python -m dotz -s 3 path/to/directory/
|
|
82
|
+
```
|
|
83
|
+
|
|
84
|
+
## Navigation
|
|
85
|
+
|
|
86
|
+
| Key | Action |
|
|
87
|
+
|-----------------|----------|
|
|
88
|
+
| Right, l, Space | Next |
|
|
89
|
+
| Left, h | Previous |
|
|
90
|
+
| Up | First |
|
|
91
|
+
| Down | Last |
|
|
92
|
+
| s | Toggle slideshow |
|
|
93
|
+
| S | Toggle reverse slideshow |
|
|
94
|
+
| q, Esc | Quit |
|
|
95
|
+
|
|
96
|
+
## Dependencies
|
|
97
|
+
|
|
98
|
+
| Package | Version | Usage |
|
|
99
|
+
|-----------|------------|--------------------------------------------|
|
|
100
|
+
| python | >=3.7 | Required Python version |
|
|
101
|
+
| numpy | >=1.20 | Fast array operations for image processing |
|
|
102
|
+
| Pillow | >=8.0 | Image loading and manipulation |
|
|
103
|
+
| ffmpeg | >=4.2 | Video frame extraction |
|
|
104
|
+
|
|
105
|
+
## Formats
|
|
106
|
+
|
|
107
|
+
**Image:** PNG, JPG, JPEG, BMP, GIF, TIFF, WEBP
|
|
108
|
+
|
|
109
|
+
**Video:** MP4, MKV, AVI, MOV, WEBM, FLV, WMV, MPEG, MPG
|
|
110
|
+
|
|
111
|
+
## License
|
|
112
|
+
|
|
113
|
+
MIT
|
|
@@ -0,0 +1,156 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: dotz
|
|
3
|
+
Version: 0.1
|
|
4
|
+
Summary: Render images and video previews as Braille art in the terminal with color and animation support.
|
|
5
|
+
Home-page: https://github.com/jarun/dotz
|
|
6
|
+
Author: Arun Prakash Jana
|
|
7
|
+
License: MIT
|
|
8
|
+
Project-URL: Homepage, https://github.com/jarun/dotz
|
|
9
|
+
Project-URL: Repository, https://github.com/jarun/dotz
|
|
10
|
+
Project-URL: Issues, https://github.com/jarun/dotz/issues
|
|
11
|
+
Keywords: terminal image-viewer braille curses gif video ffmpeg
|
|
12
|
+
Classifier: Development Status :: 4 - Beta
|
|
13
|
+
Classifier: Environment :: Console :: Curses
|
|
14
|
+
Classifier: Intended Audience :: End Users/Desktop
|
|
15
|
+
Classifier: Operating System :: POSIX :: Linux
|
|
16
|
+
Classifier: Programming Language :: Python :: 3
|
|
17
|
+
Classifier: Programming Language :: Python :: 3 :: Only
|
|
18
|
+
Classifier: Programming Language :: Python :: 3.7
|
|
19
|
+
Classifier: Programming Language :: Python :: 3.8
|
|
20
|
+
Classifier: Programming Language :: Python :: 3.9
|
|
21
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
22
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
23
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
24
|
+
Classifier: Topic :: Multimedia :: Graphics
|
|
25
|
+
Classifier: Topic :: Terminals
|
|
26
|
+
Requires-Python: >=3.7
|
|
27
|
+
Description-Content-Type: text/markdown
|
|
28
|
+
License-File: LICENSE
|
|
29
|
+
Requires-Dist: numpy>=1.20
|
|
30
|
+
Requires-Dist: Pillow>=8.0
|
|
31
|
+
Dynamic: author
|
|
32
|
+
Dynamic: classifier
|
|
33
|
+
Dynamic: description
|
|
34
|
+
Dynamic: description-content-type
|
|
35
|
+
Dynamic: home-page
|
|
36
|
+
Dynamic: keywords
|
|
37
|
+
Dynamic: license
|
|
38
|
+
Dynamic: license-file
|
|
39
|
+
Dynamic: project-url
|
|
40
|
+
Dynamic: requires-dist
|
|
41
|
+
Dynamic: requires-python
|
|
42
|
+
Dynamic: summary
|
|
43
|
+
|
|
44
|
+
# dotz
|
|
45
|
+
|
|
46
|
+
Render images and video previews as Braille art in the terminal with xterm-256 color and ncurses dim/normal/bold attributes.
|
|
47
|
+
|
|
48
|
+
It was written to be used as a terminal image viewer with [`nnn`](https://github.com/jarun/nnn). Works independently too.
|
|
49
|
+
|
|
50
|
+
## Features
|
|
51
|
+
|
|
52
|
+
- Braille art rendering for images
|
|
53
|
+
- Animated GIF support
|
|
54
|
+
- xterm-256 color and grayscale
|
|
55
|
+
- Dithering options (ordered, error diffusion)
|
|
56
|
+
- Video preview (frame extraction with ffmpeg)
|
|
57
|
+
- Keyboard navigation and slideshow mode
|
|
58
|
+
|
|
59
|
+
<br>
|
|
60
|
+
<img width="1323" height="826" alt="image_01" src="https://github.com/user-attachments/assets/f2becbbc-cfeb-42b3-bd92-3882ff3fb570" />
|
|
61
|
+
<br><br>
|
|
62
|
+
<img width="1333" height="827" alt="image_02" src="https://github.com/user-attachments/assets/62bc16a8-246b-4b5a-a11e-fd0faa5c8066" />
|
|
63
|
+
<br><br>
|
|
64
|
+
<img width="1301" height="954" alt="image_03" src="https://github.com/user-attachments/assets/609805be-c0c5-4815-bb33-3bc70d69c152" />
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
## Installation
|
|
68
|
+
|
|
69
|
+
Install the required dependencies and the dotz command:
|
|
70
|
+
|
|
71
|
+
```sh
|
|
72
|
+
# Install system dependencies (e.g., ffmpeg)
|
|
73
|
+
sudo apt-get install ffmpeg # or use your OS package manager
|
|
74
|
+
|
|
75
|
+
# Install Python dependencies and the CLI tool
|
|
76
|
+
sudo pip3 install .
|
|
77
|
+
```
|
|
78
|
+
|
|
79
|
+
After installation, you can run the tool using:
|
|
80
|
+
|
|
81
|
+
```sh
|
|
82
|
+
dotz [options] <file-or-directory>
|
|
83
|
+
```
|
|
84
|
+
|
|
85
|
+
## Usage
|
|
86
|
+
|
|
87
|
+
```
|
|
88
|
+
usage: dotz [-h] [-S] [-C] [-d {ordered,error,none}] [-s [DELAY]] [-k SEEK] [-f {jpeg,png}] [path]
|
|
89
|
+
|
|
90
|
+
Render an image or all images/videos in a directory as Braille cells using ncurses with optional xterm-256 color.
|
|
91
|
+
|
|
92
|
+
positional arguments:
|
|
93
|
+
path Path to the image/video file or directory (optional)
|
|
94
|
+
|
|
95
|
+
options:
|
|
96
|
+
-h, --help show this help message and exit
|
|
97
|
+
-S, --no-sharpen Disable edge sharpening
|
|
98
|
+
-C, --no-color Disable color (greyscale only with dim/normal/bold)
|
|
99
|
+
-d {ordered,error,none}, --dither {ordered,error,none}
|
|
100
|
+
Dithering mode: ordered (default, clean), error (Floyd-Steinberg, smooth gradients), none
|
|
101
|
+
-s [DELAY], --slideshow [DELAY]
|
|
102
|
+
Enable slideshow mode with optional integer delay in seconds (default: 5).
|
|
103
|
+
-k SEEK, --seek SEEK Seek position to extract frame from videos in seconds (default: 10)
|
|
104
|
+
-f {jpeg,png}, --format {jpeg,png}
|
|
105
|
+
Format for extracted video frames: jpeg (default) or png
|
|
106
|
+
```
|
|
107
|
+
|
|
108
|
+
#### Examples
|
|
109
|
+
|
|
110
|
+
- Syntax:
|
|
111
|
+
```sh
|
|
112
|
+
python -m dotz <file-or-directory>
|
|
113
|
+
```
|
|
114
|
+
- To render a single image:
|
|
115
|
+
```sh
|
|
116
|
+
python -m dotz path/to/image.jpg
|
|
117
|
+
```
|
|
118
|
+
- To render all images and videos in a directory:
|
|
119
|
+
```sh
|
|
120
|
+
python -m dotz path/to/directory/
|
|
121
|
+
```
|
|
122
|
+
- To run a slideshow with a custom delay (e.g. 3 seconds):
|
|
123
|
+
```sh
|
|
124
|
+
python -m dotz -s 3 path/to/directory/
|
|
125
|
+
```
|
|
126
|
+
|
|
127
|
+
## Navigation
|
|
128
|
+
|
|
129
|
+
| Key | Action |
|
|
130
|
+
|-----------------|----------|
|
|
131
|
+
| Right, l, Space | Next |
|
|
132
|
+
| Left, h | Previous |
|
|
133
|
+
| Up | First |
|
|
134
|
+
| Down | Last |
|
|
135
|
+
| s | Toggle slideshow |
|
|
136
|
+
| S | Toggle reverse slideshow |
|
|
137
|
+
| q, Esc | Quit |
|
|
138
|
+
|
|
139
|
+
## Dependencies
|
|
140
|
+
|
|
141
|
+
| Package | Version | Usage |
|
|
142
|
+
|-----------|------------|--------------------------------------------|
|
|
143
|
+
| python | >=3.7 | Required Python version |
|
|
144
|
+
| numpy | >=1.20 | Fast array operations for image processing |
|
|
145
|
+
| Pillow | >=8.0 | Image loading and manipulation |
|
|
146
|
+
| ffmpeg | >=4.2 | Video frame extraction |
|
|
147
|
+
|
|
148
|
+
## Formats
|
|
149
|
+
|
|
150
|
+
**Image:** PNG, JPG, JPEG, BMP, GIF, TIFF, WEBP
|
|
151
|
+
|
|
152
|
+
**Video:** MP4, MKV, AVI, MOV, WEBM, FLV, WMV, MPEG, MPG
|
|
153
|
+
|
|
154
|
+
## License
|
|
155
|
+
|
|
156
|
+
MIT
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
dotz
|
dotz-0.1/dotz.py
ADDED
|
@@ -0,0 +1,513 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""Render images as Braille art with xterm-256 color and ncurses dim/normal/bold."""
|
|
3
|
+
|
|
4
|
+
|
|
5
|
+
import argparse
|
|
6
|
+
import curses
|
|
7
|
+
import sys
|
|
8
|
+
import os
|
|
9
|
+
import glob
|
|
10
|
+
|
|
11
|
+
import numpy as np
|
|
12
|
+
from PIL import Image, ImageFilter
|
|
13
|
+
|
|
14
|
+
_VERSION_ = "0.1"
|
|
15
|
+
_AUTHOR_ = "Arun Prakash Jana"
|
|
16
|
+
_AUTHOR_EMAIL_ = "engineerarun@gmail.com"
|
|
17
|
+
_LICENSE_ = "MIT"
|
|
18
|
+
_WEBPAGE_ = "https://github.com/jarun/dotz"
|
|
19
|
+
|
|
20
|
+
BRAILLE_BASE = 0x2800
|
|
21
|
+
BRAILLE_MAP = (
|
|
22
|
+
(0x01, 0x08), # row 0
|
|
23
|
+
(0x02, 0x10), # row 1
|
|
24
|
+
(0x04, 0x20), # row 2
|
|
25
|
+
(0x40, 0x80), # row 3
|
|
26
|
+
)
|
|
27
|
+
|
|
28
|
+
# Ordered dither matrix for the 4×2 braille grid.
|
|
29
|
+
BAYER_4x2 = np.array([
|
|
30
|
+
[0, 4],
|
|
31
|
+
[2, 6],
|
|
32
|
+
[5, 1],
|
|
33
|
+
[7, 3],
|
|
34
|
+
], dtype=np.float64)
|
|
35
|
+
|
|
36
|
+
# ── xterm-256 color cube helpers ──────────────────────────────────────────────
|
|
37
|
+
# Colors 16-231 form a 6×6×6 RGB cube. Values per axis: 0,95,135,175,215,255.
|
|
38
|
+
# Colors 232-255 are a 24-step greyscale ramp.
|
|
39
|
+
_CUBE_VALS = np.array([0, 0x5f, 0x87, 0xaf, 0xd7, 0xff], dtype=np.float64)
|
|
40
|
+
_GREY_VALS = np.array([8 + 10 * i for i in range(24)], dtype=np.float64)
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
def _build_xterm256_table():
|
|
44
|
+
"""Build an (N, 3) array of all xterm-256 RGB values (indices 16-255)."""
|
|
45
|
+
table = np.zeros((240, 3), dtype=np.float64)
|
|
46
|
+
# 6×6×6 cube: indices 0-215 → xterm 16-231
|
|
47
|
+
idx = 0
|
|
48
|
+
for r in _CUBE_VALS:
|
|
49
|
+
for g in _CUBE_VALS:
|
|
50
|
+
for b in _CUBE_VALS:
|
|
51
|
+
table[idx] = (r, g, b)
|
|
52
|
+
idx += 1
|
|
53
|
+
# greyscale ramp: indices 216-239 → xterm 232-255
|
|
54
|
+
for i, v in enumerate(_GREY_VALS):
|
|
55
|
+
table[216 + i] = (v, v, v)
|
|
56
|
+
return table
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
_XTERM_TABLE = _build_xterm256_table() # (240, 3)
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
def _nearest_xterm256(r, g, b):
|
|
63
|
+
"""Return the xterm-256 color index (16-255) closest to an sRGB triplet (0-255)."""
|
|
64
|
+
diff = _XTERM_TABLE - np.array([r, g, b], dtype=np.float64)
|
|
65
|
+
dists = np.sum(diff * diff, axis=1)
|
|
66
|
+
return int(np.argmin(dists)) + 16
|
|
67
|
+
|
|
68
|
+
|
|
69
|
+
def _init_color_pairs():
|
|
70
|
+
"""Initialise ncurses color pairs 1-240 mapping to xterm colors 16-255."""
|
|
71
|
+
for i in range(240):
|
|
72
|
+
xterm_idx = i + 16
|
|
73
|
+
curses.init_pair(i + 1, xterm_idx, -1) # fg=xterm color, bg=default
|
|
74
|
+
|
|
75
|
+
|
|
76
|
+
def _cell_to_global(local_brightness, attr, bounds):
|
|
77
|
+
"""Map a cell's local brightness back to global perceptual brightness."""
|
|
78
|
+
if attr == curses.A_DIM:
|
|
79
|
+
return local_brightness * bounds[0]
|
|
80
|
+
elif attr == curses.A_NORMAL:
|
|
81
|
+
return bounds[0] + local_brightness * (bounds[1] - bounds[0])
|
|
82
|
+
else:
|
|
83
|
+
return bounds[1] + local_brightness * (1.0 - bounds[1])
|
|
84
|
+
|
|
85
|
+
|
|
86
|
+
def srgb_to_linear(c):
|
|
87
|
+
"""Convert sRGB [0,1] to linear light."""
|
|
88
|
+
return np.where(c <= 0.04045, c / 12.92, ((c + 0.055) / 1.055) ** 2.4)
|
|
89
|
+
|
|
90
|
+
|
|
91
|
+
def linear_to_srgb(c):
|
|
92
|
+
"""Convert linear light to sRGB [0,1]."""
|
|
93
|
+
return np.where(c <= 0.0031308, c * 12.92, 1.055 * np.power(np.clip(c, 0, None), 1.0 / 2.4) - 0.055)
|
|
94
|
+
|
|
95
|
+
|
|
96
|
+
def _load_image(image_path, img_w, img_h, sharpen, color):
|
|
97
|
+
"""Load and prepare image data. Accepts a file path or PIL Image. Returns (frames, color_maps, oy, ox, fit_h, fit_w, durations)."""
|
|
98
|
+
if isinstance(image_path, Image.Image):
|
|
99
|
+
img = image_path
|
|
100
|
+
else:
|
|
101
|
+
img = Image.open(image_path)
|
|
102
|
+
is_animated = getattr(img, "is_animated", False)
|
|
103
|
+
n_frames = getattr(img, "n_frames", 1)
|
|
104
|
+
frames = []
|
|
105
|
+
color_maps = []
|
|
106
|
+
durations = []
|
|
107
|
+
for frame_idx in range(n_frames):
|
|
108
|
+
if is_animated:
|
|
109
|
+
img.seek(frame_idx)
|
|
110
|
+
durations.append(img.info.get("duration", 100))
|
|
111
|
+
if color:
|
|
112
|
+
img_rgb = img.convert("RGB"); img_grey = img_rgb.convert("L")
|
|
113
|
+
else:
|
|
114
|
+
img_rgb = None; img_grey = img.convert("L")
|
|
115
|
+
cell_cols, cell_rows = img_w // 2, img_h // 4
|
|
116
|
+
img_aspect = img_grey.width / img_grey.height
|
|
117
|
+
max_w, max_h = cell_cols * 2, cell_rows * 4
|
|
118
|
+
fit_w, fit_h = (max_w, int(round(max_w / img_aspect))) if (max_w / img_aspect) <= max_h else (int(round(max_h * img_aspect)), max_h)
|
|
119
|
+
fit_w, fit_h = min(fit_w, max_w), min(fit_h, max_h)
|
|
120
|
+
img_grey_r = img_grey.resize((fit_w, fit_h), Image.LANCZOS)
|
|
121
|
+
if sharpen: img_grey_r = img_grey_r.filter(ImageFilter.UnsharpMask(radius=1.2, percent=100, threshold=2))
|
|
122
|
+
oy, ox = (img_h - fit_h) // 2, (img_w - fit_w) // 2
|
|
123
|
+
raw = np.asarray(img_grey_r, dtype=np.float64)
|
|
124
|
+
np.multiply(raw, 1.0/255.0, out=raw) # in-place normalization
|
|
125
|
+
linear = srgb_to_linear(raw)
|
|
126
|
+
canvas = np.zeros((img_h, img_w), dtype=np.float64)
|
|
127
|
+
canvas[oy:oy + fit_h, ox:ox + fit_w] = linear
|
|
128
|
+
perceptual = linear_to_srgb(canvas)
|
|
129
|
+
frames.append(perceptual)
|
|
130
|
+
color_map = None
|
|
131
|
+
if color and img_rgb is not None:
|
|
132
|
+
img_rgb_r = img_rgb.resize((fit_w, fit_h), Image.LANCZOS)
|
|
133
|
+
rgb_arr = np.asarray(img_rgb_r, dtype=np.float64)
|
|
134
|
+
canvas_rgb = np.zeros((img_h, img_w, 3), dtype=np.float64)
|
|
135
|
+
canvas_rgb[oy:oy + fit_h, ox:ox + fit_w] = rgb_arr
|
|
136
|
+
blocks = canvas_rgb[:cell_rows*4, :cell_cols*2, :].reshape(cell_rows, 4, cell_cols, 2, 3)
|
|
137
|
+
block_means = blocks.mean(axis=(1,3), dtype=np.float64)
|
|
138
|
+
block_means_flat = block_means.reshape(-1, 3)
|
|
139
|
+
diffs = block_means_flat[:, None, :] - _XTERM_TABLE[None, :, :]
|
|
140
|
+
np.square(diffs, out=diffs)
|
|
141
|
+
dists = np.sum(diffs, axis=2)
|
|
142
|
+
color_indices = np.argmin(dists, axis=1) + 16
|
|
143
|
+
color_map = color_indices.reshape(cell_rows, cell_cols).astype(np.int32)
|
|
144
|
+
color_maps.append(color_map)
|
|
145
|
+
if not is_animated: break
|
|
146
|
+
return frames, color_maps, oy, ox, fit_h, fit_w, durations
|
|
147
|
+
|
|
148
|
+
|
|
149
|
+
# Helper to extract a video frame using ffmpeg and return a PIL Image
|
|
150
|
+
def extract_video_frame(path, frametime, extractformat):
|
|
151
|
+
import subprocess, io
|
|
152
|
+
vcodec = 'mjpeg' if extractformat == 'jpeg' else 'png'
|
|
153
|
+
ffmpeg_cmd = [
|
|
154
|
+
'ffmpeg', '-y', '-nostdin', '-hide_banner', '-loglevel', 'error',
|
|
155
|
+
'-skip_frame', 'nokey', '-ss', str(int(frametime)), '-i', path,
|
|
156
|
+
'-an', '-threads', '1', '-vsync', '0',
|
|
157
|
+
'-vframes', '1',
|
|
158
|
+
'-f', 'image2pipe',
|
|
159
|
+
'-vcodec', vcodec,
|
|
160
|
+
'-']
|
|
161
|
+
result = subprocess.run(ffmpeg_cmd, stdout=subprocess.PIPE, stderr=subprocess.PIPE)
|
|
162
|
+
if result.returncode != 0 or not result.stdout:
|
|
163
|
+
raise RuntimeError(f"ffmpeg error: {result.stderr.decode()[:100]}")
|
|
164
|
+
from PIL import Image
|
|
165
|
+
img = Image.open(io.BytesIO(result.stdout))
|
|
166
|
+
img.load()
|
|
167
|
+
return img
|
|
168
|
+
|
|
169
|
+
|
|
170
|
+
def get_image_files(directory, include_videos=False):
|
|
171
|
+
from pathlib import Path
|
|
172
|
+
exts = ["png", "jpg", "jpeg", "bmp", "gif", "tiff", "webp"]
|
|
173
|
+
if include_videos:
|
|
174
|
+
exts += ["mp4", "mkv", "avi", "mov", "webm", "flv", "wmv", "mpeg", "mpg"]
|
|
175
|
+
p = Path(directory)
|
|
176
|
+
files = []
|
|
177
|
+
for f in p.iterdir():
|
|
178
|
+
if f.is_file():
|
|
179
|
+
suffix = f.suffix.lower().lstrip('.')
|
|
180
|
+
if suffix in exts:
|
|
181
|
+
files.append(str(f.resolve()))
|
|
182
|
+
files.sort()
|
|
183
|
+
return files
|
|
184
|
+
|
|
185
|
+
|
|
186
|
+
def render(stdscr, image_files, idx, sharpen, dither_mode, color, single_image_mode=False, wait_time=5, slideshow=False):
|
|
187
|
+
import time
|
|
188
|
+
curses.curs_set(0)
|
|
189
|
+
curses.use_default_colors()
|
|
190
|
+
if color:
|
|
191
|
+
curses.start_color()
|
|
192
|
+
_init_color_pairs()
|
|
193
|
+
|
|
194
|
+
n = len(image_files)
|
|
195
|
+
def floyd_steinberg_dither(img):
|
|
196
|
+
arr = img.copy()
|
|
197
|
+
h, w = arr.shape
|
|
198
|
+
for y in range(h):
|
|
199
|
+
for x in range(w):
|
|
200
|
+
old = arr[y, x]
|
|
201
|
+
new = 1.0 if old > 0.5 else 0.0
|
|
202
|
+
err = old - new
|
|
203
|
+
arr[y, x] = new
|
|
204
|
+
if x + 1 < w:
|
|
205
|
+
arr[y, x+1] += err * 7/16
|
|
206
|
+
if y + 1 < h:
|
|
207
|
+
if x > 0:
|
|
208
|
+
arr[y+1, x-1] += err * 3/16
|
|
209
|
+
arr[y+1, x] += err * 5/16
|
|
210
|
+
if x + 1 < w:
|
|
211
|
+
arr[y+1, x+1] += err * 1/16
|
|
212
|
+
return arr
|
|
213
|
+
|
|
214
|
+
stdscr.clear()
|
|
215
|
+
max_y, max_x = stdscr.getmaxyx()
|
|
216
|
+
rows = max_y - 1
|
|
217
|
+
cols = max_x - 1
|
|
218
|
+
img_w = cols * 2
|
|
219
|
+
img_h = rows * 4
|
|
220
|
+
try:
|
|
221
|
+
image_path = image_files[idx]
|
|
222
|
+
display_name = None
|
|
223
|
+
video_exts = ['.mp4', '.mkv', '.avi', '.mov', '.webm', '.flv', '.wmv', '.mpeg', '.mpg']
|
|
224
|
+
ext = os.path.splitext(image_path)[1].lower() if isinstance(image_path, str) else ''
|
|
225
|
+
if ext in video_exts:
|
|
226
|
+
try:
|
|
227
|
+
seek = getattr(render, '_seek', 10)
|
|
228
|
+
fmt = getattr(render, '_format', 'jpeg')
|
|
229
|
+
img = extract_video_frame(image_path, seek, fmt)
|
|
230
|
+
display_name = image_path
|
|
231
|
+
frames, color_maps, oy, ox, fit_h, fit_w, durations = _load_image(img, img_w, img_h, sharpen, color)
|
|
232
|
+
except Exception as e:
|
|
233
|
+
stdscr.clear()
|
|
234
|
+
stdscr.addstr(0, 0, f"Video frame error: {e}")
|
|
235
|
+
stdscr.refresh()
|
|
236
|
+
stdscr.getch()
|
|
237
|
+
return -1
|
|
238
|
+
else:
|
|
239
|
+
if isinstance(image_path, tuple) and len(image_path) == 2:
|
|
240
|
+
image_path, display_name = image_path
|
|
241
|
+
elif isinstance(image_path, Image.Image):
|
|
242
|
+
display_name = '[video frame]'
|
|
243
|
+
frames, color_maps, oy, ox, fit_h, fit_w, durations = _load_image(image_path, img_w, img_h, sharpen, color)
|
|
244
|
+
is_animated = len(frames) > 1
|
|
245
|
+
frame_idx = 0
|
|
246
|
+
key = -1
|
|
247
|
+
last_time = time.time()
|
|
248
|
+
stdscr.nodelay(True)
|
|
249
|
+
except Exception as e:
|
|
250
|
+
stdscr.clear()
|
|
251
|
+
stdscr.addstr(0, 0, f"Render error: {e}")
|
|
252
|
+
stdscr.refresh()
|
|
253
|
+
stdscr.getch()
|
|
254
|
+
return -1
|
|
255
|
+
while True:
|
|
256
|
+
stdscr.clear()
|
|
257
|
+
perceptual = frames[frame_idx]
|
|
258
|
+
color_map = color_maps[frame_idx] if color_maps else None
|
|
259
|
+
thresholds = (BAYER_4x2 + 0.5) / 8.0
|
|
260
|
+
ATTR_BOUNDS = (0.30, 0.62)
|
|
261
|
+
cell_rows = rows
|
|
262
|
+
cell_cols = cols
|
|
263
|
+
if dither_mode == "error":
|
|
264
|
+
dithered = floyd_steinberg_dither(perceptual[:cell_rows*4, :cell_cols*2].copy())
|
|
265
|
+
blocks = dithered.reshape(cell_rows, 4, cell_cols, 2)
|
|
266
|
+
else:
|
|
267
|
+
blocks = perceptual[:cell_rows*4, :cell_cols*2].reshape(cell_rows, 4, cell_cols, 2)
|
|
268
|
+
block_means = blocks.mean(axis=(1,3))
|
|
269
|
+
use_dither = dither_mode == "ordered"
|
|
270
|
+
for cy in range(rows):
|
|
271
|
+
for cx in range(cols):
|
|
272
|
+
avg = block_means[cy, cx]
|
|
273
|
+
if avg < ATTR_BOUNDS[0]:
|
|
274
|
+
attr = curses.A_DIM
|
|
275
|
+
elif avg < ATTR_BOUNDS[1]:
|
|
276
|
+
attr = curses.A_NORMAL
|
|
277
|
+
else:
|
|
278
|
+
attr = curses.A_BOLD
|
|
279
|
+
code = BRAILLE_BASE
|
|
280
|
+
block = blocks[cy, :, cx, :]
|
|
281
|
+
for dr in range(4):
|
|
282
|
+
for dc in range(2):
|
|
283
|
+
t = thresholds[dr, dc] if use_dither else 0.5
|
|
284
|
+
if dither_mode == "error":
|
|
285
|
+
if block[dr, dc] > 0.5:
|
|
286
|
+
code |= BRAILLE_MAP[dr][dc]
|
|
287
|
+
else:
|
|
288
|
+
if block[dr, dc] > t:
|
|
289
|
+
code |= BRAILLE_MAP[dr][dc]
|
|
290
|
+
if color and color_map is not None:
|
|
291
|
+
pair = color_map[cy, cx] - 16 + 1
|
|
292
|
+
attr |= curses.color_pair(pair)
|
|
293
|
+
try:
|
|
294
|
+
stdscr.addstr(cy, cx, chr(code), attr)
|
|
295
|
+
except curses.error:
|
|
296
|
+
pass
|
|
297
|
+
try:
|
|
298
|
+
if display_name:
|
|
299
|
+
shown_name = os.path.basename(display_name)
|
|
300
|
+
elif isinstance(image_path, (str, bytes, os.PathLike)):
|
|
301
|
+
shown_name = os.path.basename(image_path)
|
|
302
|
+
else:
|
|
303
|
+
shown_name = '[video frame]'
|
|
304
|
+
stdscr.addstr(rows, 0, f"[{idx+1}/{n}] {shown_name}", curses.A_REVERSE)
|
|
305
|
+
except curses.error:
|
|
306
|
+
pass
|
|
307
|
+
stdscr.refresh()
|
|
308
|
+
if is_animated:
|
|
309
|
+
duration = durations[frame_idx] / 1000.0 if frame_idx < len(durations) else 0.1
|
|
310
|
+
start_time = time.time()
|
|
311
|
+
while True:
|
|
312
|
+
key = stdscr.getch()
|
|
313
|
+
if key != -1:
|
|
314
|
+
stdscr.nodelay(False)
|
|
315
|
+
return key
|
|
316
|
+
if (time.time() - start_time) >= duration:
|
|
317
|
+
break
|
|
318
|
+
time.sleep(0.01)
|
|
319
|
+
frame_idx = (frame_idx + 1) % len(frames)
|
|
320
|
+
else:
|
|
321
|
+
if slideshow:
|
|
322
|
+
start_time = time.time()
|
|
323
|
+
while True:
|
|
324
|
+
key = stdscr.getch()
|
|
325
|
+
if key == ord('s'):
|
|
326
|
+
return 'toggle_slideshow'
|
|
327
|
+
if key == ord('S'):
|
|
328
|
+
return 'toggle_slideshow_reverse'
|
|
329
|
+
if key != -1:
|
|
330
|
+
stdscr.nodelay(False)
|
|
331
|
+
return key
|
|
332
|
+
if (time.time() - start_time) >= wait_time:
|
|
333
|
+
return 'slideshow_next'
|
|
334
|
+
time.sleep(0.01)
|
|
335
|
+
else:
|
|
336
|
+
while True:
|
|
337
|
+
key = stdscr.getch()
|
|
338
|
+
if key == ord('s'):
|
|
339
|
+
return 'toggle_slideshow'
|
|
340
|
+
if key == ord('S'):
|
|
341
|
+
return 'toggle_slideshow_reverse'
|
|
342
|
+
if key != -1:
|
|
343
|
+
stdscr.nodelay(False)
|
|
344
|
+
return key
|
|
345
|
+
time.sleep(0.01)
|
|
346
|
+
break
|
|
347
|
+
return key
|
|
348
|
+
|
|
349
|
+
|
|
350
|
+
def main():
|
|
351
|
+
class ExtendedArgumentParser(argparse.ArgumentParser):
|
|
352
|
+
@staticmethod
|
|
353
|
+
def print_extended_help(file=None):
|
|
354
|
+
if file is None:
|
|
355
|
+
file = sys.stderr
|
|
356
|
+
file.write(
|
|
357
|
+
"\n"
|
|
358
|
+
f"Version: {_VERSION_}\n"
|
|
359
|
+
f"Author: {_AUTHOR_} <{_AUTHOR_EMAIL_}>\n"
|
|
360
|
+
f"License: {_LICENSE_}\n"
|
|
361
|
+
f"Webpage: {_WEBPAGE_}\n"
|
|
362
|
+
)
|
|
363
|
+
|
|
364
|
+
def print_help(self, file=None):
|
|
365
|
+
super().print_help(file)
|
|
366
|
+
self.print_extended_help(file)
|
|
367
|
+
|
|
368
|
+
parser = ExtendedArgumentParser(description="Render an image or all images/videos in a directory as Braille cells using ncurses with optional xterm-256 color.")
|
|
369
|
+
parser.add_argument("path", nargs="?", help="Path to the image/video file or directory (optional)")
|
|
370
|
+
parser.add_argument("-S", "--no-sharpen", action="store_true", help="Disable edge sharpening")
|
|
371
|
+
parser.add_argument("-C", "--no-color", action="store_true", help="Disable color (greyscale only with dim/normal/bold)")
|
|
372
|
+
parser.add_argument("-d", "--dither", choices=["ordered", "error", "none"], default="ordered",
|
|
373
|
+
help="Dithering mode: ordered (default, clean), error (Floyd-Steinberg, smooth gradients), none")
|
|
374
|
+
|
|
375
|
+
parser.add_argument("-s", "--slideshow", dest="delay", nargs="?", const=5, type=int, help="Enable slideshow mode with optional integer delay in seconds (default: 5).")
|
|
376
|
+
parser.add_argument("-k", "--seek", type=int, default=10, help="Seek position to extract frame from videos in seconds (default: 10)")
|
|
377
|
+
parser.add_argument("-f", "--format", type=str, choices=["jpeg", "png"], default="jpeg", help="Format for extracted video frames: jpeg (default) or png")
|
|
378
|
+
parser.add_argument("-v", "--version", action="version", version=_VERSION_)
|
|
379
|
+
args = parser.parse_args()
|
|
380
|
+
|
|
381
|
+
if args.delay is not None:
|
|
382
|
+
slideshow = True
|
|
383
|
+
slideshow_delay = args.delay
|
|
384
|
+
else:
|
|
385
|
+
slideshow = False
|
|
386
|
+
slideshow_delay = 5
|
|
387
|
+
|
|
388
|
+
if args.path == '-':
|
|
389
|
+
# Read image from stdin
|
|
390
|
+
from PIL import Image
|
|
391
|
+
import tempfile
|
|
392
|
+
import shutil
|
|
393
|
+
# Read stdin to a temporary file (since PIL.Image.open(sys.stdin.buffer) may not work for all formats)
|
|
394
|
+
with tempfile.NamedTemporaryFile(delete=False, suffix='.png') as tmp:
|
|
395
|
+
shutil.copyfileobj(sys.stdin.buffer, tmp)
|
|
396
|
+
tmp_path = tmp.name
|
|
397
|
+
image_files = [tmp_path]
|
|
398
|
+
idx = 0
|
|
399
|
+
# Redirect stdin file descriptor to /dev/tty so curses reads input from the terminal
|
|
400
|
+
# (removed local import os; using global import)
|
|
401
|
+
try:
|
|
402
|
+
tty_fd = os.open('/dev/tty', os.O_RDWR)
|
|
403
|
+
orig_stdin_fd = os.dup(0)
|
|
404
|
+
os.dup2(tty_fd, 0)
|
|
405
|
+
os.close(tty_fd)
|
|
406
|
+
try:
|
|
407
|
+
curses.wrapper(lambda *a, **kw: render(*a, **kw, single_image_mode=True, wait_time=slideshow_delay, slideshow=slideshow), image_files, idx, not args.no_sharpen, args.dither, not args.no_color)
|
|
408
|
+
finally:
|
|
409
|
+
os.dup2(orig_stdin_fd, 0)
|
|
410
|
+
os.close(orig_stdin_fd)
|
|
411
|
+
finally:
|
|
412
|
+
os.unlink(tmp_path)
|
|
413
|
+
return
|
|
414
|
+
elif args.path:
|
|
415
|
+
# If a directory is passed, render all images/videos in it
|
|
416
|
+
if os.path.isdir(args.path):
|
|
417
|
+
directory = os.path.abspath(args.path)
|
|
418
|
+
image_files = get_image_files(directory, include_videos=args.seek is not None)
|
|
419
|
+
if not image_files:
|
|
420
|
+
print(f"No images or videos found in directory: {directory}", file=sys.stderr)
|
|
421
|
+
sys.exit(1)
|
|
422
|
+
idx = 0
|
|
423
|
+
else:
|
|
424
|
+
video_exts = ['.mp4', '.mkv', '.avi', '.mov', '.webm', '.flv', '.wmv', '.mpeg', '.mpg']
|
|
425
|
+
abs_path = os.path.abspath(args.path)
|
|
426
|
+
directory = os.path.dirname(abs_path) or os.getcwd()
|
|
427
|
+
# Always include videos if the selected file is a video
|
|
428
|
+
include_videos = os.path.splitext(abs_path)[1].lower() in video_exts or args.seek is not None
|
|
429
|
+
image_files = get_image_files(directory, include_videos=include_videos)
|
|
430
|
+
if not image_files:
|
|
431
|
+
print(f"No images or videos found in directory: {directory}", file=sys.stderr)
|
|
432
|
+
sys.exit(1)
|
|
433
|
+
try:
|
|
434
|
+
idx = image_files.index(abs_path)
|
|
435
|
+
except ValueError:
|
|
436
|
+
base = os.path.basename(abs_path)
|
|
437
|
+
idx = next((i for i, f in enumerate(image_files) if os.path.basename(f) == base), 0)
|
|
438
|
+
else:
|
|
439
|
+
# No argument: use current directory
|
|
440
|
+
directory = os.getcwd()
|
|
441
|
+
include_videos = args.seek is not None
|
|
442
|
+
image_files = get_image_files(directory, include_videos=include_videos)
|
|
443
|
+
if not image_files:
|
|
444
|
+
print(f"No images or videos found in current directory.", file=sys.stderr)
|
|
445
|
+
sys.exit(1)
|
|
446
|
+
idx = 0
|
|
447
|
+
|
|
448
|
+
|
|
449
|
+
# Always pass the video_exts and frametime to render for dynamic extraction
|
|
450
|
+
video_exts = ['.mp4', '.mkv', '.avi', '.mov', '.webm', '.flv', '.wmv', '.mpeg', '.mpg']
|
|
451
|
+
|
|
452
|
+
# Pass seek and format to render via function attributes for video frame extraction
|
|
453
|
+
def render_with_video_support(stdscr, image_files, start_idx, sharpen, dither_mode, color, single_image_mode=False, wait_time=5, slideshow=False):
|
|
454
|
+
idx = start_idx
|
|
455
|
+
n = len(image_files)
|
|
456
|
+
# Pass seek/format to render via function attributes
|
|
457
|
+
render._seek = args.seek
|
|
458
|
+
render._format = args.format
|
|
459
|
+
slideshow_active = slideshow
|
|
460
|
+
slideshow_reverse = False
|
|
461
|
+
while True:
|
|
462
|
+
try:
|
|
463
|
+
key = render(stdscr, image_files, idx, sharpen, dither_mode, color, single_image_mode=False, wait_time=wait_time, slideshow=slideshow_active)
|
|
464
|
+
except Exception as e:
|
|
465
|
+
stdscr.clear()
|
|
466
|
+
stdscr.addstr(0, 0, f"Error: {e}")
|
|
467
|
+
stdscr.refresh()
|
|
468
|
+
stdscr.getch()
|
|
469
|
+
return
|
|
470
|
+
# Navigation
|
|
471
|
+
if key == 'toggle_slideshow':
|
|
472
|
+
if slideshow_active and slideshow_reverse:
|
|
473
|
+
# While running in reverse mode, `s` switches back to forward.
|
|
474
|
+
slideshow_reverse = False
|
|
475
|
+
else:
|
|
476
|
+
slideshow_active = not slideshow_active
|
|
477
|
+
if slideshow_active:
|
|
478
|
+
slideshow_reverse = False
|
|
479
|
+
continue
|
|
480
|
+
if key == 'toggle_slideshow_reverse':
|
|
481
|
+
if slideshow_active and not slideshow_reverse:
|
|
482
|
+
# While running in forward mode, `S` switches to reverse.
|
|
483
|
+
slideshow_reverse = True
|
|
484
|
+
else:
|
|
485
|
+
slideshow_active = not slideshow_active
|
|
486
|
+
if slideshow_active:
|
|
487
|
+
slideshow_reverse = True
|
|
488
|
+
continue
|
|
489
|
+
if slideshow_active and key not in ('slideshow_next', -1, 'toggle_slideshow', 'toggle_slideshow_reverse'):
|
|
490
|
+
# Any other key disables slideshow
|
|
491
|
+
slideshow_active = False
|
|
492
|
+
slideshow_reverse = False
|
|
493
|
+
if key == 'slideshow_next':
|
|
494
|
+
if slideshow_reverse:
|
|
495
|
+
idx = (idx - 1) % n
|
|
496
|
+
else:
|
|
497
|
+
idx = (idx + 1) % n
|
|
498
|
+
elif key in (curses.KEY_RIGHT, ord('l'), ord(' ')):
|
|
499
|
+
idx = (idx + 1) % n
|
|
500
|
+
elif key in (curses.KEY_LEFT, ord('h')):
|
|
501
|
+
idx = (idx - 1) % n
|
|
502
|
+
elif key == curses.KEY_UP:
|
|
503
|
+
idx = 0
|
|
504
|
+
elif key == curses.KEY_DOWN:
|
|
505
|
+
idx = n - 1
|
|
506
|
+
elif key in (ord('q'), 27):
|
|
507
|
+
break
|
|
508
|
+
|
|
509
|
+
curses.wrapper(render_with_video_support, image_files, idx, not args.no_sharpen, args.dither, not args.no_color, wait_time=slideshow_delay, slideshow=slideshow)
|
|
510
|
+
|
|
511
|
+
|
|
512
|
+
if __name__ == "__main__":
|
|
513
|
+
main()
|
dotz-0.1/pyproject.toml
ADDED
dotz-0.1/setup.cfg
ADDED
dotz-0.1/setup.py
ADDED
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
import re
|
|
2
|
+
from pathlib import Path
|
|
3
|
+
|
|
4
|
+
from setuptools import setup
|
|
5
|
+
|
|
6
|
+
README = Path(__file__).parent / "README.md"
|
|
7
|
+
DOTZ = Path(__file__).parent / "dotz.py"
|
|
8
|
+
|
|
9
|
+
version = re.search(
|
|
10
|
+
r'^_VERSION_\s*=\s*[\"\']([^\"\']+)[\"\']',
|
|
11
|
+
DOTZ.read_text(encoding="utf-8"),
|
|
12
|
+
re.MULTILINE,
|
|
13
|
+
).group(1)
|
|
14
|
+
|
|
15
|
+
setup(
|
|
16
|
+
name="dotz",
|
|
17
|
+
version=version,
|
|
18
|
+
description="Render images and video previews as Braille art in the terminal with color and animation support.",
|
|
19
|
+
long_description=README.read_text(encoding="utf-8"),
|
|
20
|
+
long_description_content_type="text/markdown",
|
|
21
|
+
author="Arun Prakash Jana",
|
|
22
|
+
license="MIT",
|
|
23
|
+
url="https://github.com/jarun/dotz",
|
|
24
|
+
project_urls={
|
|
25
|
+
"Homepage": "https://github.com/jarun/dotz",
|
|
26
|
+
"Repository": "https://github.com/jarun/dotz",
|
|
27
|
+
"Issues": "https://github.com/jarun/dotz/issues",
|
|
28
|
+
},
|
|
29
|
+
py_modules=["dotz"],
|
|
30
|
+
install_requires=[
|
|
31
|
+
"numpy>=1.20",
|
|
32
|
+
"Pillow>=8.0",
|
|
33
|
+
],
|
|
34
|
+
entry_points={
|
|
35
|
+
"console_scripts": [
|
|
36
|
+
"dotz=dotz:main",
|
|
37
|
+
],
|
|
38
|
+
},
|
|
39
|
+
python_requires=">=3.7",
|
|
40
|
+
classifiers=[
|
|
41
|
+
"Development Status :: 4 - Beta",
|
|
42
|
+
"Environment :: Console :: Curses",
|
|
43
|
+
"Intended Audience :: End Users/Desktop",
|
|
44
|
+
"Operating System :: POSIX :: Linux",
|
|
45
|
+
"Programming Language :: Python :: 3",
|
|
46
|
+
"Programming Language :: Python :: 3 :: Only",
|
|
47
|
+
"Programming Language :: Python :: 3.7",
|
|
48
|
+
"Programming Language :: Python :: 3.8",
|
|
49
|
+
"Programming Language :: Python :: 3.9",
|
|
50
|
+
"Programming Language :: Python :: 3.10",
|
|
51
|
+
"Programming Language :: Python :: 3.11",
|
|
52
|
+
"Programming Language :: Python :: 3.12",
|
|
53
|
+
"Topic :: Multimedia :: Graphics",
|
|
54
|
+
"Topic :: Terminals",
|
|
55
|
+
],
|
|
56
|
+
keywords="terminal image-viewer braille curses gif video ffmpeg",
|
|
57
|
+
)
|