"""Pure-Python ports of DitherForge core text effects.""" from __future__ import annotations import numpy as np from PIL import Image DEFAULT_ASCII_CHARSET = " .:-=+*#%@" BRAILLE_BASE = 0x2800 DOT_BITS = [ [0x01, 0x08], [0x02, 0x10], [0x04, 0x20], [0x40, 0x80], ] def image_to_gray(img: Image.Image) -> np.ndarray: """Convert PIL Image to 2D grayscale numpy array (0-255).""" return np.array(img.convert("L"), dtype=np.uint8) def binary_effect(gray: np.ndarray, cols: int = 80, threshold: int = 128) -> str: """Convert grayscale image to binary (0/1) text.""" h, w = gray.shape cols = max(1, min(cols, w)) block_w = w / cols block_h = block_w * 2 rows = max(1, int(h / block_h)) lines = [] for row in range(rows): line = "" for col in range(cols): x0 = int(col * block_w) x1 = int((col + 1) * block_w) y0 = int(row * block_h) y1 = min(int((row + 1) * block_h), h) avg = gray[y0:y1, x0:x1].mean() line += "1" if avg >= threshold else "0" lines.append(line) return "\n".join(lines) def ascii_effect( gray: np.ndarray, cols: int = 80, charset: str = DEFAULT_ASCII_CHARSET ) -> str: """Convert grayscale image to ASCII art.""" h, w = gray.shape cols = max(1, min(cols, w)) block_w = w / cols block_h = block_w * 2 rows = max(1, int(h / block_h)) lines = [] for row in range(rows): line = "" for col in range(cols): x0 = int(col * block_w) x1 = int((col + 1) * block_w) y0 = int(row * block_h) y1 = min(int((row + 1) * block_h), h) avg = gray[y0:y1, x0:x1].mean() idx = min(int((avg / 255) * len(charset)), len(charset) - 1) line += charset[idx] lines.append(line) return "\n".join(lines) def braille_effect( gray: np.ndarray, cols: int = 40, threshold: int = 128 ) -> str: """Convert grayscale image to Unicode braille art.""" h, w = gray.shape cols = max(1, min(cols, w // 2)) pix_w = cols * 2 scale = w / pix_w pix_h = int(h / scale) rows = max(1, pix_h // 4) lines = [] for row in range(rows): line = "" for col in range(cols): cp = 0 for dy in range(4): for dx in range(2): sx = min(int((col * 2 + dx) * scale), w - 1) sy = min(int((row * 4 + dy) * scale), h - 1) if gray[sy, sx] < threshold: cp |= DOT_BITS[dy][dx] line += chr(BRAILLE_BASE + cp) lines.append(line) return "\n".join(lines)