pixelification 1.2.1__tar.gz → 1.2.5__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.
- {pixelification-1.2.1 → pixelification-1.2.5}/PKG-INFO +37 -1
- {pixelification-1.2.1 → pixelification-1.2.5}/README.md +36 -0
- pixelification-1.2.5/requirements.txt +20 -0
- {pixelification-1.2.1 → pixelification-1.2.5}/src/pixelification/main.py +282 -3
- {pixelification-1.2.1 → pixelification-1.2.5}/uv.lock +11 -6
- pixelification-1.2.1/requirements.txt +0 -6
- {pixelification-1.2.1 → pixelification-1.2.5}/.github/workflows/publish.yml +0 -0
- {pixelification-1.2.1 → pixelification-1.2.5}/.gitignore +0 -0
- {pixelification-1.2.1 → pixelification-1.2.5}/.python-version +0 -0
- {pixelification-1.2.1 → pixelification-1.2.5}/Cargo.lock +0 -0
- {pixelification-1.2.1 → pixelification-1.2.5}/Cargo.toml +0 -0
- {pixelification-1.2.1 → pixelification-1.2.5}/main.py +0 -0
- {pixelification-1.2.1 → pixelification-1.2.5}/pyproject.toml +0 -0
- {pixelification-1.2.1 → pixelification-1.2.5}/src/app/mod.rs +0 -0
- {pixelification-1.2.1 → pixelification-1.2.5}/src/config/mod.rs +0 -0
- {pixelification-1.2.1 → pixelification-1.2.5}/src/drag/mod.rs +0 -0
- {pixelification-1.2.1 → pixelification-1.2.5}/src/events/mod.rs +0 -0
- {pixelification-1.2.1 → pixelification-1.2.5}/src/main.rs +0 -0
- {pixelification-1.2.1 → pixelification-1.2.5}/src/pixelification/__init__.py +0 -0
- {pixelification-1.2.1 → pixelification-1.2.5}/src/pixelification/runtime.py +0 -0
- {pixelification-1.2.1 → pixelification-1.2.5}/src/state/mod.rs +0 -0
- {pixelification-1.2.1 → pixelification-1.2.5}/src/ui/animated.rs +0 -0
- {pixelification-1.2.1 → pixelification-1.2.5}/src/ui/bitmap.rs +0 -0
- {pixelification-1.2.1 → pixelification-1.2.5}/src/ui/brush.rs +0 -0
- {pixelification-1.2.1 → pixelification-1.2.5}/src/ui/color.rs +0 -0
- {pixelification-1.2.1 → pixelification-1.2.5}/src/ui/font.rs +0 -0
- {pixelification-1.2.1 → pixelification-1.2.5}/src/ui/mod.rs +0 -0
- {pixelification-1.2.1 → pixelification-1.2.5}/src/ui/render.rs +0 -0
- {pixelification-1.2.1 → pixelification-1.2.5}/src/webview/mod.rs +0 -0
- {pixelification-1.2.1 → pixelification-1.2.5}/src/win32.rs +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: pixelification
|
|
3
|
-
Version: 1.2.
|
|
3
|
+
Version: 1.2.5
|
|
4
4
|
Summary: Pixel Rearrangement Tool — Keyboard-Navigated Terminal UI
|
|
5
5
|
Requires-Python: >=3.10
|
|
6
6
|
Requires-Dist: numpy>=1.20.0
|
|
@@ -84,6 +84,31 @@ Rearrange pixels from a **source image** to approximate the layout of a **target
|
|
|
84
84
|
|
|
85
85
|
---
|
|
86
86
|
|
|
87
|
+
### ▸ ASCII Art Mode
|
|
88
|
+
|
|
89
|
+
Turn any image into **terminal ASCII art** — a scrollable character-based rendering using 10 luminance levels with auto-contrast and Floyd-Steinberg dithering.
|
|
90
|
+
|
|
91
|
+
```
|
|
92
|
+
┌─────────────────────────────────────────────────────────────┐
|
|
93
|
+
│ @@@@@@%####****+==--::.. Shift+arrows scroll │
|
|
94
|
+
│ @@@%%%###***+++==--::.. PgUp/PgDn jump │
|
|
95
|
+
│ @%@%%%####***+++==--::.. 10 levels + dither │
|
|
96
|
+
│ @@@%%%###***+++==--::.. │
|
|
97
|
+
└─────────────────────────────────────────────────────────────┘
|
|
98
|
+
```
|
|
99
|
+
|
|
100
|
+
**How it works:**
|
|
101
|
+
|
|
102
|
+
| Step | What happens |
|
|
103
|
+
|------|-------------|
|
|
104
|
+
| **Read** | Image loaded via OpenCV, converted to luminance using Rec. 601 `(0.299R + 0.587G + 0.114B)` |
|
|
105
|
+
| **Contrast** | Histogram stretched to full 0–255 range for maximum tonal depth |
|
|
106
|
+
| **Resize** | Aspect-corrected resize (0.55 char ratio) to fit terminal width |
|
|
107
|
+
| **Dither** | Floyd-Steinberg error diffusion distributes quantization error across neighboring pixels |
|
|
108
|
+
| **Map** | Each pixel's brightness mapped to one of 10 characters `@%#*+=-:. ` sorted by ink coverage |
|
|
109
|
+
|
|
110
|
+
---
|
|
111
|
+
|
|
87
112
|
### ▸ Video Mode
|
|
88
113
|
|
|
89
114
|
Rearrange every frame of a **source video** (or a still image looped as a video) to match the frames of a **target video**.
|
|
@@ -171,6 +196,7 @@ A keyboard-navigated terminal UI opens immediately:
|
|
|
171
196
|
|
|
172
197
|
● Rearrange Images sort pixels between two images
|
|
173
198
|
○ Rearrange Videos sort frames between two videos
|
|
199
|
+
○ ASCII convert images to ASCII art
|
|
174
200
|
○ Quit exit the application
|
|
175
201
|
```
|
|
176
202
|
|
|
@@ -181,6 +207,8 @@ A keyboard-navigated terminal UI opens immediately:
|
|
|
181
207
|
| `↑` `↓` | Navigate menu items |
|
|
182
208
|
| `Enter` | Select highlighted item |
|
|
183
209
|
| `1`–`N` | Jump directly to item *N* |
|
|
210
|
+
| `Shift`+`↑` `↓` `←` `→` | Scroll ASCII art (vertical / horizontal) |
|
|
211
|
+
| `PgUp` / `PgDn` | Scroll ASCII art by one page |
|
|
184
212
|
| `q` / `Esc` | Quit |
|
|
185
213
|
|
|
186
214
|
### Image Mode
|
|
@@ -199,6 +227,14 @@ A keyboard-navigated terminal UI opens immediately:
|
|
|
199
227
|
4. Result plays in an OpenCV window — loops until `ESC`/`q` or window close
|
|
200
228
|
5. Click **"Save Result Video"** to export
|
|
201
229
|
|
|
230
|
+
### ASCII Art Mode
|
|
231
|
+
|
|
232
|
+
1. Select an image file
|
|
233
|
+
2. Press **"Run ASCII Conversion"** — the art renders instantly in the terminal
|
|
234
|
+
3. Use `Shift+arrows` to scroll, `PgUp`/`PgDn` to jump by page
|
|
235
|
+
4. **"Copy to Clipboard"** pipes the full result to your system clipboard (`clip.exe`)
|
|
236
|
+
5. **"Save Result"** writes a `.txt` file to the current directory
|
|
237
|
+
|
|
202
238
|
---
|
|
203
239
|
|
|
204
240
|
## ◈ Requirements
|
|
@@ -72,6 +72,31 @@ Rearrange pixels from a **source image** to approximate the layout of a **target
|
|
|
72
72
|
|
|
73
73
|
---
|
|
74
74
|
|
|
75
|
+
### ▸ ASCII Art Mode
|
|
76
|
+
|
|
77
|
+
Turn any image into **terminal ASCII art** — a scrollable character-based rendering using 10 luminance levels with auto-contrast and Floyd-Steinberg dithering.
|
|
78
|
+
|
|
79
|
+
```
|
|
80
|
+
┌─────────────────────────────────────────────────────────────┐
|
|
81
|
+
│ @@@@@@%####****+==--::.. Shift+arrows scroll │
|
|
82
|
+
│ @@@%%%###***+++==--::.. PgUp/PgDn jump │
|
|
83
|
+
│ @%@%%%####***+++==--::.. 10 levels + dither │
|
|
84
|
+
│ @@@%%%###***+++==--::.. │
|
|
85
|
+
└─────────────────────────────────────────────────────────────┘
|
|
86
|
+
```
|
|
87
|
+
|
|
88
|
+
**How it works:**
|
|
89
|
+
|
|
90
|
+
| Step | What happens |
|
|
91
|
+
|------|-------------|
|
|
92
|
+
| **Read** | Image loaded via OpenCV, converted to luminance using Rec. 601 `(0.299R + 0.587G + 0.114B)` |
|
|
93
|
+
| **Contrast** | Histogram stretched to full 0–255 range for maximum tonal depth |
|
|
94
|
+
| **Resize** | Aspect-corrected resize (0.55 char ratio) to fit terminal width |
|
|
95
|
+
| **Dither** | Floyd-Steinberg error diffusion distributes quantization error across neighboring pixels |
|
|
96
|
+
| **Map** | Each pixel's brightness mapped to one of 10 characters `@%#*+=-:. ` sorted by ink coverage |
|
|
97
|
+
|
|
98
|
+
---
|
|
99
|
+
|
|
75
100
|
### ▸ Video Mode
|
|
76
101
|
|
|
77
102
|
Rearrange every frame of a **source video** (or a still image looped as a video) to match the frames of a **target video**.
|
|
@@ -159,6 +184,7 @@ A keyboard-navigated terminal UI opens immediately:
|
|
|
159
184
|
|
|
160
185
|
● Rearrange Images sort pixels between two images
|
|
161
186
|
○ Rearrange Videos sort frames between two videos
|
|
187
|
+
○ ASCII convert images to ASCII art
|
|
162
188
|
○ Quit exit the application
|
|
163
189
|
```
|
|
164
190
|
|
|
@@ -169,6 +195,8 @@ A keyboard-navigated terminal UI opens immediately:
|
|
|
169
195
|
| `↑` `↓` | Navigate menu items |
|
|
170
196
|
| `Enter` | Select highlighted item |
|
|
171
197
|
| `1`–`N` | Jump directly to item *N* |
|
|
198
|
+
| `Shift`+`↑` `↓` `←` `→` | Scroll ASCII art (vertical / horizontal) |
|
|
199
|
+
| `PgUp` / `PgDn` | Scroll ASCII art by one page |
|
|
172
200
|
| `q` / `Esc` | Quit |
|
|
173
201
|
|
|
174
202
|
### Image Mode
|
|
@@ -187,6 +215,14 @@ A keyboard-navigated terminal UI opens immediately:
|
|
|
187
215
|
4. Result plays in an OpenCV window — loops until `ESC`/`q` or window close
|
|
188
216
|
5. Click **"Save Result Video"** to export
|
|
189
217
|
|
|
218
|
+
### ASCII Art Mode
|
|
219
|
+
|
|
220
|
+
1. Select an image file
|
|
221
|
+
2. Press **"Run ASCII Conversion"** — the art renders instantly in the terminal
|
|
222
|
+
3. Use `Shift+arrows` to scroll, `PgUp`/`PgDn` to jump by page
|
|
223
|
+
4. **"Copy to Clipboard"** pipes the full result to your system clipboard (`clip.exe`)
|
|
224
|
+
5. **"Save Result"** writes a `.txt` file to the current directory
|
|
225
|
+
|
|
190
226
|
---
|
|
191
227
|
|
|
192
228
|
## ◈ Requirements
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
prompt-toolkit>=3.0.0
|
|
2
|
+
opencv-python>=4.0.0
|
|
3
|
+
numpy>=1.20.0
|
|
4
|
+
|
|
5
|
+
# ── Developer Notes ─────────────────────────────────────────────────
|
|
6
|
+
#
|
|
7
|
+
# pip install -r requirements.txt → base deps only (no GPU)
|
|
8
|
+
# pip install -e . → base deps only (no GPU)
|
|
9
|
+
# pip install -e ".[cuda]" → includes CuPy + NVIDIA libs
|
|
10
|
+
#
|
|
11
|
+
# uv sync → base deps only
|
|
12
|
+
# uv sync --extra cuda → includes CuPy + NVIDIA libs
|
|
13
|
+
# uv sync --all-extras → includes CuPy + NVIDIA libs
|
|
14
|
+
#
|
|
15
|
+
# CuPy / NVIDIA packages are ONLY installed when the 'cuda' extra is
|
|
16
|
+
# explicitly requested. Users without an NVIDIA GPU should NOT use
|
|
17
|
+
# the [cuda] extra.
|
|
18
|
+
#
|
|
19
|
+
# ── Optional — CUDA acceleration (NVIDIA GPU required) ──────────────
|
|
20
|
+
# cupy-cuda13x[ctk]
|
|
@@ -211,7 +211,7 @@ def select_file(title: str = "Select File", file_type: str = "image") -> str | N
|
|
|
211
211
|
|
|
212
212
|
@dataclass
|
|
213
213
|
class State:
|
|
214
|
-
screen: str = "main" # "main" | "image" | "video"
|
|
214
|
+
screen: str = "main" # "main" | "image" | "video" | "ascii"
|
|
215
215
|
source: str = ""
|
|
216
216
|
target: str = ""
|
|
217
217
|
status: str = "Ready"
|
|
@@ -224,10 +224,14 @@ class State:
|
|
|
224
224
|
result_video_path: str = ""
|
|
225
225
|
using_accelerator: bool = HAS_CUPY
|
|
226
226
|
acceleration_status: str = CUPY_STATUS_TEXT
|
|
227
|
+
result_ascii: str = ""
|
|
228
|
+
scroll_x: int = 0
|
|
229
|
+
scroll_y: int = 0
|
|
227
230
|
|
|
228
231
|
MENU_MAIN = [
|
|
229
232
|
("Rearrange Images", "sort pixels between two images"),
|
|
230
233
|
("Rearrange Videos", "sort frames between two videos"),
|
|
234
|
+
("ASCII", "convert images to ASCII art"),
|
|
231
235
|
("Quit", "exit the application"),
|
|
232
236
|
]
|
|
233
237
|
|
|
@@ -249,9 +253,18 @@ class State:
|
|
|
249
253
|
("Quit", "exit the application"),
|
|
250
254
|
]
|
|
251
255
|
|
|
256
|
+
MENU_ASCII = [
|
|
257
|
+
("Select Image", "choose an image to convert"),
|
|
258
|
+
("Run ASCII Conversion", "convert the selected image to ASCII art"),
|
|
259
|
+
("Copy to Clipboard", "copy the ASCII art to the system clipboard"),
|
|
260
|
+
("Save Result", "save the ASCII art to a .txt file"),
|
|
261
|
+
("Back to Main Menu", "return to mode selection"),
|
|
262
|
+
("Quit", "exit the application"),
|
|
263
|
+
]
|
|
264
|
+
|
|
252
265
|
@property
|
|
253
266
|
def menu(self):
|
|
254
|
-
return {"main": self.MENU_MAIN, "image": self.MENU_IMAGE, "video": self.MENU_VIDEO}[self.screen]
|
|
267
|
+
return {"main": self.MENU_MAIN, "image": self.MENU_IMAGE, "video": self.MENU_VIDEO, "ascii": self.MENU_ASCII}[self.screen]
|
|
255
268
|
|
|
256
269
|
|
|
257
270
|
# ── Rearrangement Engine ─────────────────────────────────────────────
|
|
@@ -617,6 +630,68 @@ def rearrange_video(source_path: str, target_path: str, state: State) -> None:
|
|
|
617
630
|
state.done = True
|
|
618
631
|
|
|
619
632
|
|
|
633
|
+
# ── ASCII Art Engine ─────────────────────────────────────────────────
|
|
634
|
+
|
|
635
|
+
ASCII_CHARS = "@%#*+=-:. "
|
|
636
|
+
|
|
637
|
+
|
|
638
|
+
def _auto_contrast(img: np.ndarray) -> np.ndarray:
|
|
639
|
+
lo, hi = int(img.min()), int(img.max())
|
|
640
|
+
if hi - lo < 1:
|
|
641
|
+
return img
|
|
642
|
+
return ((img.astype(np.float32) - lo) / (hi - lo) * 255).clip(0, 255).astype(np.uint8)
|
|
643
|
+
|
|
644
|
+
|
|
645
|
+
def _floyd_steinberg(gray: np.ndarray, levels: int) -> np.ndarray:
|
|
646
|
+
h, w = gray.shape
|
|
647
|
+
out = gray.astype(np.float32).copy()
|
|
648
|
+
step = 255.0 / (levels - 1)
|
|
649
|
+
for y in range(h):
|
|
650
|
+
for x in range(w):
|
|
651
|
+
old = out[y, x]
|
|
652
|
+
quantized = round(old / step) * step
|
|
653
|
+
out[y, x] = quantized
|
|
654
|
+
err = old - quantized
|
|
655
|
+
if x + 1 < w:
|
|
656
|
+
out[y, x + 1] += err * 7 / 16
|
|
657
|
+
if y + 1 < h:
|
|
658
|
+
if x > 0:
|
|
659
|
+
out[y + 1, x - 1] += err * 3 / 16
|
|
660
|
+
out[y + 1, x] += err * 5 / 16
|
|
661
|
+
if x + 1 < w:
|
|
662
|
+
out[y + 1, x + 1] += err * 1 / 16
|
|
663
|
+
return out.clip(0, 255).astype(np.uint8)
|
|
664
|
+
|
|
665
|
+
|
|
666
|
+
def image_to_ascii(path: str, width: int = 120, dither: bool = True) -> str:
|
|
667
|
+
bgr = cv2.imread(path, cv2.IMREAD_COLOR)
|
|
668
|
+
if bgr is None:
|
|
669
|
+
return ""
|
|
670
|
+
|
|
671
|
+
gray = (0.299 * bgr[:, :, 2] + 0.587 * bgr[:, :, 1] + 0.114 * bgr[:, :, 0]).astype(np.uint8)
|
|
672
|
+
|
|
673
|
+
gray = _auto_contrast(gray)
|
|
674
|
+
|
|
675
|
+
h, w = gray.shape[:2]
|
|
676
|
+
aspect = h / w
|
|
677
|
+
height = max(int(width * aspect * 0.55), 1)
|
|
678
|
+
resized = cv2.resize(gray, (width, height), interpolation=cv2.INTER_LANCZOS4)
|
|
679
|
+
|
|
680
|
+
ramp = ASCII_CHARS
|
|
681
|
+
ramp_len = len(ramp)
|
|
682
|
+
|
|
683
|
+
if dither:
|
|
684
|
+
resized = _floyd_steinberg(resized, ramp_len)
|
|
685
|
+
|
|
686
|
+
idx = (resized.astype(np.float32) / 255 * (ramp_len - 1)).round().astype(np.int32)
|
|
687
|
+
idx = np.clip(idx, 0, ramp_len - 1)
|
|
688
|
+
|
|
689
|
+
lines = []
|
|
690
|
+
for row in idx:
|
|
691
|
+
lines.append("".join(ramp[i] for i in row))
|
|
692
|
+
return "\n".join(lines)
|
|
693
|
+
|
|
694
|
+
|
|
620
695
|
# ── TUI Application ──────────────────────────────────────────────────
|
|
621
696
|
|
|
622
697
|
class PixelTUI:
|
|
@@ -663,6 +738,44 @@ class PixelTUI:
|
|
|
663
738
|
self.state.cursor = idx
|
|
664
739
|
self._dispatch(idx)
|
|
665
740
|
|
|
741
|
+
@kb.add("s-up")
|
|
742
|
+
def _(event):
|
|
743
|
+
if self.state.screen == "ascii" and self.state.result_ascii and not self.state.running:
|
|
744
|
+
self.state.scroll_y = max(0, self.state.scroll_y - 1)
|
|
745
|
+
self._invalidate()
|
|
746
|
+
|
|
747
|
+
@kb.add("s-down")
|
|
748
|
+
def _(event):
|
|
749
|
+
if self.state.screen == "ascii" and self.state.result_ascii and not self.state.running:
|
|
750
|
+
self.state.scroll_y += 1
|
|
751
|
+
self._invalidate()
|
|
752
|
+
|
|
753
|
+
@kb.add("s-left")
|
|
754
|
+
def _(event):
|
|
755
|
+
if self.state.screen == "ascii" and self.state.result_ascii and not self.state.running:
|
|
756
|
+
self.state.scroll_x = max(0, self.state.scroll_x - 4)
|
|
757
|
+
self._invalidate()
|
|
758
|
+
|
|
759
|
+
@kb.add("s-right")
|
|
760
|
+
def _(event):
|
|
761
|
+
if self.state.screen == "ascii" and self.state.result_ascii and not self.state.running:
|
|
762
|
+
self.state.scroll_x += 4
|
|
763
|
+
self._invalidate()
|
|
764
|
+
|
|
765
|
+
@kb.add("pageup")
|
|
766
|
+
def _(event):
|
|
767
|
+
if self.state.screen == "ascii" and self.state.result_ascii and not self.state.running:
|
|
768
|
+
jump = (self._app.output.get_size().rows - 12 if self._app else 20)
|
|
769
|
+
self.state.scroll_y = max(0, self.state.scroll_y - jump)
|
|
770
|
+
self._invalidate()
|
|
771
|
+
|
|
772
|
+
@kb.add("pagedown")
|
|
773
|
+
def _(event):
|
|
774
|
+
if self.state.screen == "ascii" and self.state.result_ascii and not self.state.running:
|
|
775
|
+
jump = (self._app.output.get_size().rows - 12 if self._app else 20)
|
|
776
|
+
self.state.scroll_y += jump
|
|
777
|
+
self._invalidate()
|
|
778
|
+
|
|
666
779
|
@kb.add("escape")
|
|
667
780
|
@kb.add("q")
|
|
668
781
|
def _(event):
|
|
@@ -680,7 +793,8 @@ class PixelTUI:
|
|
|
680
793
|
if s == "main":
|
|
681
794
|
{0: self._enter_image_mode,
|
|
682
795
|
1: self._enter_video_mode,
|
|
683
|
-
2: self.
|
|
796
|
+
2: self._enter_ascii_mode,
|
|
797
|
+
3: self._quit}[idx]()
|
|
684
798
|
elif s == "image":
|
|
685
799
|
{0: self._select_source,
|
|
686
800
|
1: self._select_target,
|
|
@@ -695,6 +809,13 @@ class PixelTUI:
|
|
|
695
809
|
3: self._save_result_video,
|
|
696
810
|
4: self._back_to_main,
|
|
697
811
|
5: self._quit}[idx]()
|
|
812
|
+
elif s == "ascii":
|
|
813
|
+
{0: self._select_source_ascii,
|
|
814
|
+
1: self._run_ascii,
|
|
815
|
+
2: self._copy_result_ascii,
|
|
816
|
+
3: self._save_result_ascii,
|
|
817
|
+
4: self._back_to_main,
|
|
818
|
+
5: self._quit}[idx]()
|
|
698
819
|
|
|
699
820
|
# ── Actions ──────────────────────────────────────────────────
|
|
700
821
|
|
|
@@ -836,6 +957,95 @@ class PixelTUI:
|
|
|
836
957
|
if self._app:
|
|
837
958
|
asyncio.create_task(waiter())
|
|
838
959
|
|
|
960
|
+
def _enter_ascii_mode(self):
|
|
961
|
+
self.state.screen = "ascii"
|
|
962
|
+
self.state.cursor = 0
|
|
963
|
+
self.state.scroll_x = 0
|
|
964
|
+
self.state.scroll_y = 0
|
|
965
|
+
self.state.status = "Select an image to convert to ASCII art"
|
|
966
|
+
self.state.status_style = "status-info"
|
|
967
|
+
self._invalidate()
|
|
968
|
+
|
|
969
|
+
def _select_source_ascii(self):
|
|
970
|
+
path = select_file("Select Image for ASCII", "image")
|
|
971
|
+
if path:
|
|
972
|
+
self.state.source = path
|
|
973
|
+
self.state.status = f"Image: {Path(path).name}"
|
|
974
|
+
self.state.status_style = "status"
|
|
975
|
+
else:
|
|
976
|
+
self.state.status = "Selection cancelled"
|
|
977
|
+
self.state.status_style = "status-info"
|
|
978
|
+
self._invalidate()
|
|
979
|
+
|
|
980
|
+
def _run_ascii(self):
|
|
981
|
+
if not self.state.source:
|
|
982
|
+
self.state.status = "Select an image first!"
|
|
983
|
+
self.state.status_style = "status-error"
|
|
984
|
+
self._invalidate()
|
|
985
|
+
return
|
|
986
|
+
|
|
987
|
+
self.state.status = "Converting to ASCII art\u2026"
|
|
988
|
+
self.state.status_style = "status-warn"
|
|
989
|
+
self._invalidate()
|
|
990
|
+
|
|
991
|
+
self.state.scroll_x = 0
|
|
992
|
+
self.state.scroll_y = 0
|
|
993
|
+
width = min(120, max(40, self._app.output.get_size().columns - 4 if self._app else 120))
|
|
994
|
+
|
|
995
|
+
try:
|
|
996
|
+
result = image_to_ascii(self.state.source, width)
|
|
997
|
+
if not result:
|
|
998
|
+
self.state.status = "Failed to read image"
|
|
999
|
+
self.state.status_style = "status-error"
|
|
1000
|
+
else:
|
|
1001
|
+
self.state.result_ascii = result
|
|
1002
|
+
self.state.done = True
|
|
1003
|
+
self.state.status = f"ASCII art generated ({len(result.split(chr(10)))} rows \u00d7 {width} cols)"
|
|
1004
|
+
self.state.status_style = "status"
|
|
1005
|
+
except Exception as e:
|
|
1006
|
+
self.state.status = f"Error: {e}"
|
|
1007
|
+
self.state.status_style = "status-error"
|
|
1008
|
+
self._invalidate()
|
|
1009
|
+
|
|
1010
|
+
def _save_result_ascii(self):
|
|
1011
|
+
if not self.state.result_ascii:
|
|
1012
|
+
self.state.status = "No result to save \u2014 run conversion first!"
|
|
1013
|
+
self.state.status_style = "status-error"
|
|
1014
|
+
self._invalidate()
|
|
1015
|
+
return
|
|
1016
|
+
|
|
1017
|
+
src_stem = Path(self.state.source).stem
|
|
1018
|
+
out_name = f"{src_stem}_ascii.txt"
|
|
1019
|
+
out_path = Path.cwd() / out_name
|
|
1020
|
+
try:
|
|
1021
|
+
out_path.write_text(self.state.result_ascii, encoding="utf-8")
|
|
1022
|
+
self.state.status = f"Saved to {out_path}"
|
|
1023
|
+
self.state.status_style = "status"
|
|
1024
|
+
except Exception as e:
|
|
1025
|
+
self.state.status = f"Save error: {e}"
|
|
1026
|
+
self.state.status_style = "status-error"
|
|
1027
|
+
self._invalidate()
|
|
1028
|
+
|
|
1029
|
+
def _copy_result_ascii(self):
|
|
1030
|
+
if not self.state.result_ascii:
|
|
1031
|
+
self.state.status = "No result to copy \u2014 run conversion first!"
|
|
1032
|
+
self.state.status_style = "status-error"
|
|
1033
|
+
self._invalidate()
|
|
1034
|
+
return
|
|
1035
|
+
try:
|
|
1036
|
+
import subprocess
|
|
1037
|
+
subprocess.run(
|
|
1038
|
+
["clip"], text=True, input=self.state.result_ascii,
|
|
1039
|
+
creationflags=0x08000000,
|
|
1040
|
+
)
|
|
1041
|
+
size = len(self.state.result_ascii)
|
|
1042
|
+
self.state.status = f"Copied {size} characters to clipboard"
|
|
1043
|
+
self.state.status_style = "status"
|
|
1044
|
+
except Exception as e:
|
|
1045
|
+
self.state.status = f"Clipboard error: {e}"
|
|
1046
|
+
self.state.status_style = "status-error"
|
|
1047
|
+
self._invalidate()
|
|
1048
|
+
|
|
839
1049
|
def _refresh_info(self):
|
|
840
1050
|
s = self.state
|
|
841
1051
|
parts = []
|
|
@@ -950,6 +1160,75 @@ class PixelTUI:
|
|
|
950
1160
|
push("#585858 italic", "\n")
|
|
951
1161
|
push("#585858 italic", f" Hardware acceleration: {accel_text}")
|
|
952
1162
|
push("", "\n")
|
|
1163
|
+
elif s.screen == "ascii":
|
|
1164
|
+
push("bold #00d787", " \u25a0 Pixel Rearrangement Tool")
|
|
1165
|
+
push("bold #5f87ff", " \u2014 [ ASCII Art Mode ]")
|
|
1166
|
+
push("", "\n")
|
|
1167
|
+
push("#3a3a3a", " " + "\u2501" * 55)
|
|
1168
|
+
push("", "\n")
|
|
1169
|
+
push("bold #5f87ff", "\n Image: ")
|
|
1170
|
+
push("#87afff" if s.source else "#585858 italic",
|
|
1171
|
+
s.source if s.source else "\u2014 not selected \u2014")
|
|
1172
|
+
push("", "\n\n")
|
|
1173
|
+
|
|
1174
|
+
if s.result_ascii:
|
|
1175
|
+
all_lines = s.result_ascii.split("\n")
|
|
1176
|
+
total_rows = len(all_lines)
|
|
1177
|
+
total_cols = max(len(l) for l in all_lines) if all_lines else 0
|
|
1178
|
+
|
|
1179
|
+
vp_rows = max(3, (self._app.output.get_size().rows - 18
|
|
1180
|
+
if self._app else 10))
|
|
1181
|
+
vp_cols = (self._app.output.get_size().columns - 5
|
|
1182
|
+
if self._app else 76)
|
|
1183
|
+
|
|
1184
|
+
s.scroll_y = max(0, min(s.scroll_y, total_rows - vp_rows))
|
|
1185
|
+
s.scroll_x = max(0, min(s.scroll_x, total_cols - vp_cols))
|
|
1186
|
+
|
|
1187
|
+
scroll_ind_y = (" \u2191" if s.scroll_y > 0 else " ") + \
|
|
1188
|
+
(" \u2193" if s.scroll_y + vp_rows < total_rows else " ")
|
|
1189
|
+
scroll_ind_x = ("\u2190" if s.scroll_x > 0 else " ") + \
|
|
1190
|
+
("\u2192" if s.scroll_x + vp_cols < total_cols else " ")
|
|
1191
|
+
info = f"{total_rows}\u00d7{total_cols} {scroll_ind_y} {scroll_ind_x}"
|
|
1192
|
+
push("#6c6c6c", f" {info}\n")
|
|
1193
|
+
push("", " " + "\u2500" * min(vp_cols, total_cols) + "\n")
|
|
1194
|
+
for line in all_lines[s.scroll_y:s.scroll_y + vp_rows]:
|
|
1195
|
+
slice = line[s.scroll_x:s.scroll_x + vp_cols]
|
|
1196
|
+
push("", " " + slice + "\n")
|
|
1197
|
+
push("", " " + "\u2500" * min(vp_cols, total_cols) + "\n")
|
|
1198
|
+
push("#585858 italic", " Shift+arrows scroll \u2022 PgUp/PgDn jump\n")
|
|
1199
|
+
elif s.done:
|
|
1200
|
+
push("status-warn", " Conversion produced no output.\n")
|
|
1201
|
+
|
|
1202
|
+
push("#3a3a3a", " " + "\u2500" * 55)
|
|
1203
|
+
push("", "\n")
|
|
1204
|
+
|
|
1205
|
+
for i, (label, desc) in enumerate(s.menu):
|
|
1206
|
+
cursor = "\u25cf" if i == s.cursor else "\u25cb"
|
|
1207
|
+
sel = i == s.cursor
|
|
1208
|
+
is_run = i == 1
|
|
1209
|
+
is_copy = i == 2
|
|
1210
|
+
is_save = i == 3
|
|
1211
|
+
disabled = (is_run and not s.source) or (is_copy and not s.done) or (is_save and not s.done)
|
|
1212
|
+
st = ("bold #000000 bg:#00d787" if sel else
|
|
1213
|
+
"#585858 italic" if disabled else
|
|
1214
|
+
"bold #ffffff")
|
|
1215
|
+
push(st, f" {cursor} {label} ")
|
|
1216
|
+
push("", " ")
|
|
1217
|
+
push("#3a3a3a" if disabled else "#6c6c6c", f"{desc}\n")
|
|
1218
|
+
|
|
1219
|
+
push("", "\n")
|
|
1220
|
+
push("#3a3a3a", " " + "\u2500" * 55)
|
|
1221
|
+
push("", "\n ")
|
|
1222
|
+
|
|
1223
|
+
c = {"status": "#5faf5f", "status-error": "#ff5f5f",
|
|
1224
|
+
"status-warn": "#ffaf5f", "status-info": "#878787 italic"}
|
|
1225
|
+
push(c.get(s.status_style, "#878787 italic"), s.status)
|
|
1226
|
+
push("", "\n")
|
|
1227
|
+
|
|
1228
|
+
n = len(s.menu)
|
|
1229
|
+
help_extra = " \u2022 Shift+arrows \u2022 PgUp/Dn jump" if s.result_ascii else ""
|
|
1230
|
+
push("#585858 italic", f"\u2191\u2193 navigate \u2022 Enter select \u2022 1-{n} shortcut{help_extra} \u2022 q quit")
|
|
1231
|
+
push("#585858 italic", "\n")
|
|
953
1232
|
else:
|
|
954
1233
|
mode_label = "Image Mode" if s.screen == "image" else "Video Mode"
|
|
955
1234
|
push("bold #00d787", f" \u25a0 Pixel Rearrangement Tool")
|
|
@@ -58,9 +58,9 @@ name = "cupy-cuda13x"
|
|
|
58
58
|
version = "14.1.0"
|
|
59
59
|
source = { registry = "https://pypi.org/simple" }
|
|
60
60
|
dependencies = [
|
|
61
|
-
{ name = "cuda-pathfinder"
|
|
62
|
-
{ name = "numpy", version = "2.2.6", source = { registry = "https://pypi.org/simple" }, marker = "
|
|
63
|
-
{ name = "numpy", version = "2.4.6", source = { registry = "https://pypi.org/simple" }, marker = "
|
|
61
|
+
{ name = "cuda-pathfinder" },
|
|
62
|
+
{ name = "numpy", version = "2.2.6", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.11'" },
|
|
63
|
+
{ name = "numpy", version = "2.4.6", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.11'" },
|
|
64
64
|
]
|
|
65
65
|
wheels = [
|
|
66
66
|
{ url = "https://files.pythonhosted.org/packages/18/e9/19185f325905eada94ed96f649a2b92336dcb6caa62a5adac514bb2f0de5/cupy_cuda13x-14.1.0-cp310-cp310-manylinux2014_aarch64.whl", hash = "sha256:80f169592f5bbf7cb411ce4eeda315d06addb1a3cb14ed668e9d2ba287e3e4d4", size = 72670820, upload-time = "2026-05-23T01:11:17.547Z" },
|
|
@@ -84,7 +84,7 @@ wheels = [
|
|
|
84
84
|
|
|
85
85
|
[package.optional-dependencies]
|
|
86
86
|
ctk = [
|
|
87
|
-
{ name = "cuda-toolkit", extra = ["cublas", "cudart", "cufft", "curand", "cusolver", "cusparse", "nvrtc"]
|
|
87
|
+
{ name = "cuda-toolkit", extra = ["cublas", "cudart", "cufft", "curand", "cusolver", "cusparse", "nvrtc"] },
|
|
88
88
|
]
|
|
89
89
|
|
|
90
90
|
[[package]]
|
|
@@ -353,20 +353,25 @@ wheels = [
|
|
|
353
353
|
name = "pixelification"
|
|
354
354
|
source = { editable = "." }
|
|
355
355
|
dependencies = [
|
|
356
|
-
{ name = "cupy-cuda13x", extra = ["ctk"], marker = "sys_platform == 'linux' or sys_platform == 'win32'" },
|
|
357
356
|
{ name = "numpy", version = "2.2.6", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.11'" },
|
|
358
357
|
{ name = "numpy", version = "2.4.6", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.11'" },
|
|
359
358
|
{ name = "opencv-python" },
|
|
360
359
|
{ name = "prompt-toolkit" },
|
|
361
360
|
]
|
|
362
361
|
|
|
362
|
+
[package.optional-dependencies]
|
|
363
|
+
cuda = [
|
|
364
|
+
{ name = "cupy-cuda13x", extra = ["ctk"] },
|
|
365
|
+
]
|
|
366
|
+
|
|
363
367
|
[package.metadata]
|
|
364
368
|
requires-dist = [
|
|
365
|
-
{ name = "cupy-cuda13x", extras = ["ctk"], marker = "
|
|
369
|
+
{ name = "cupy-cuda13x", extras = ["ctk"], marker = "extra == 'cuda'" },
|
|
366
370
|
{ name = "numpy", specifier = ">=1.20.0" },
|
|
367
371
|
{ name = "opencv-python", specifier = ">=4.0.0" },
|
|
368
372
|
{ name = "prompt-toolkit", specifier = ">=3.0.0" },
|
|
369
373
|
]
|
|
374
|
+
provides-extras = ["cuda"]
|
|
370
375
|
|
|
371
376
|
[[package]]
|
|
372
377
|
name = "prompt-toolkit"
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|