120 lines
3.4 KiB
Python
120 lines
3.4 KiB
Python
#!/usr/bin/env python3
|
|
|
|
import argparse
|
|
import sys
|
|
from pathlib import Path
|
|
from typing import Optional
|
|
|
|
try:
|
|
from PIL import Image
|
|
except ImportError:
|
|
print(
|
|
"Error: Pillow is not installed. Install it with 'pip install Pillow' or use the project venv.",
|
|
file=sys.stderr,
|
|
)
|
|
sys.exit(1)
|
|
|
|
|
|
def to_bw_char(value: int, threshold: int, invert: bool) -> str:
|
|
is_black = value < threshold
|
|
if invert:
|
|
is_black = not is_black
|
|
return 'B' if is_black else 'W'
|
|
|
|
|
|
def convert_image(
|
|
input_path: Path,
|
|
output_path: Path,
|
|
width: Optional[int],
|
|
height: Optional[int],
|
|
threshold: int,
|
|
invert: bool,
|
|
) -> None:
|
|
# Load image
|
|
img = Image.open(input_path)
|
|
# Convert to grayscale (luminance)
|
|
img = img.convert('L')
|
|
|
|
# Resize if requested
|
|
if width is not None or height is not None:
|
|
# Preserve aspect ratio if only one dimension is provided
|
|
if width is not None and height is not None:
|
|
target_size = (width, height)
|
|
else:
|
|
w, h = img.size
|
|
if width is not None and height is None:
|
|
height = round(h * (width / w))
|
|
elif height is not None and width is None:
|
|
width = round(w * (height / h))
|
|
target_size = (width, height)
|
|
img = img.resize(target_size, resample=Image.NEAREST)
|
|
|
|
pixels = img.load()
|
|
w, h = img.size
|
|
|
|
lines = []
|
|
for y in range(h):
|
|
row_chars = []
|
|
for x in range(w):
|
|
row_chars.append(to_bw_char(pixels[x, y], threshold, invert))
|
|
lines.append(''.join(row_chars))
|
|
output_path.parent.mkdir(parents=True, exist_ok=True)
|
|
output_path.write_text('\n'.join(lines), encoding='utf-8')
|
|
|
|
|
|
def parse_args() -> argparse.Namespace:
|
|
p = argparse.ArgumentParser(
|
|
description="Convert an image to bad_apple_frame.txt format (W=white, B=black)."
|
|
)
|
|
# Input aliases: --input/-i and --image
|
|
p.add_argument(
|
|
'--input', '-i', dest='input', type=Path, required=False,
|
|
help='Path to input image (PNG, JPG, GIF, etc.)'
|
|
)
|
|
p.add_argument(
|
|
'--image', dest='input', type=Path, required=False,
|
|
help='Alias for --input'
|
|
)
|
|
# Output aliases: --output/-o and --out
|
|
p.add_argument(
|
|
'--output', '-o', dest='output', type=Path, required=False,
|
|
help='Path to output text file (defaults to <image>_frame.txt)'
|
|
)
|
|
p.add_argument(
|
|
'--out', dest='output', type=Path, required=False,
|
|
help='Alias for --output'
|
|
)
|
|
p.add_argument('--width', type=int, help='Optional output width in pixels')
|
|
p.add_argument('--height', type=int,
|
|
help='Optional output height in pixels')
|
|
p.add_argument(
|
|
'--threshold', type=int, default=128,
|
|
help='Grayscale threshold 0..255 (default: 128)'
|
|
)
|
|
p.add_argument('--invert', action='store_true',
|
|
help='Invert mapping (B/W flipped)')
|
|
args = p.parse_args()
|
|
|
|
if args.input is None:
|
|
p.error("--input/--image is required")
|
|
|
|
if args.output is None:
|
|
args.output = args.input.with_name(f"{args.input.stem}_frame.txt")
|
|
|
|
return args
|
|
|
|
|
|
def main() -> None:
|
|
args = parse_args()
|
|
convert_image(
|
|
input_path=args.input,
|
|
output_path=args.output,
|
|
width=args.width,
|
|
height=args.height,
|
|
threshold=max(0, min(255, args.threshold)),
|
|
invert=args.invert,
|
|
)
|
|
|
|
|
|
if __name__ == '__main__':
|
|
main()
|