mirror of
https://github.com/shadoll/moma.git
synced 2026-08-28 11:33:25 +00:00
feat: Implement poster rendering options with ASCII, Viu, and RichPixels support
This commit is contained in:
@@ -0,0 +1,19 @@
|
||||
"""Poster rendering views.
|
||||
|
||||
This package provides different rendering engines for movie posters:
|
||||
- ASCII art (pseudo graphics)
|
||||
- viu (terminal image viewer)
|
||||
- rich-pixels (Rich library integration)
|
||||
"""
|
||||
|
||||
from .base import PosterRenderer
|
||||
from .ascii_renderer import AsciiPosterRenderer
|
||||
from .viu_renderer import ViuPosterRenderer
|
||||
from .richpixels_renderer import RichPixelsPosterRenderer
|
||||
|
||||
__all__ = [
|
||||
'PosterRenderer',
|
||||
'AsciiPosterRenderer',
|
||||
'ViuPosterRenderer',
|
||||
'RichPixelsPosterRenderer',
|
||||
]
|
||||
@@ -0,0 +1,80 @@
|
||||
"""ASCII art poster renderer."""
|
||||
|
||||
from .base import PosterRenderer
|
||||
|
||||
|
||||
class AsciiPosterRenderer(PosterRenderer):
|
||||
"""Render posters as ASCII art using PIL."""
|
||||
|
||||
def render(self, image_path: str, width: int = 35) -> str:
|
||||
"""Render poster as ASCII art.
|
||||
|
||||
Args:
|
||||
image_path: Path to the poster image
|
||||
width: Width in characters (default: 35)
|
||||
|
||||
Returns:
|
||||
ASCII art representation of the poster
|
||||
"""
|
||||
is_valid, error_msg = self.validate_image(image_path)
|
||||
if not is_valid:
|
||||
return error_msg
|
||||
|
||||
is_available, msg = self.is_available()
|
||||
if not is_available:
|
||||
return msg
|
||||
|
||||
try:
|
||||
from PIL import Image, ImageEnhance
|
||||
|
||||
# Open image
|
||||
img = Image.open(image_path)
|
||||
|
||||
# Enhance contrast for better detail
|
||||
enhancer = ImageEnhance.Contrast(img)
|
||||
img = enhancer.enhance(1.3)
|
||||
|
||||
# Convert to grayscale and resize
|
||||
# Using provided width, height calculated to maintain aspect ratio
|
||||
img = img.convert('L').resize((width, width), Image.Resampling.LANCZOS)
|
||||
|
||||
# Extended ASCII characters from darkest to lightest (more gradient levels)
|
||||
# Using characters with different visual density for better detail
|
||||
ascii_chars = '$@B%8&WM#*oahkbdpqwmZO0QLCJUYXzcvunxrjft/\\|()1{}[]?-_+~<>i!lI;:,"^`\'. '
|
||||
|
||||
# Convert to ASCII
|
||||
pixels = img.getdata()
|
||||
img_width, height = img.size
|
||||
|
||||
ascii_art = []
|
||||
for y in range(0, height, 2): # Skip every other row for aspect ratio correction
|
||||
row = []
|
||||
for x in range(img_width):
|
||||
# Average of two rows for better aspect ratio
|
||||
pixel1 = pixels[y * img_width + x] if y < height else 255
|
||||
pixel2 = pixels[(y + 1) * img_width + x] if y + 1 < height else 255
|
||||
avg = (pixel1 + pixel2) // 2
|
||||
|
||||
# Map pixel brightness to character
|
||||
# Invert: 0 (black) -> dark char, 255 (white) -> light char
|
||||
char_index = (255 - avg) * (len(ascii_chars) - 1) // 255
|
||||
char = ascii_chars[char_index]
|
||||
row.append(char)
|
||||
ascii_art.append(''.join(row))
|
||||
|
||||
return '\n'.join(ascii_art)
|
||||
|
||||
except Exception as e:
|
||||
return f"Failed to display image: {e}\nPoster at: {image_path}"
|
||||
|
||||
def is_available(self) -> tuple[bool, str]:
|
||||
"""Check if PIL is available.
|
||||
|
||||
Returns:
|
||||
Tuple of (is_available, message)
|
||||
"""
|
||||
try:
|
||||
import PIL
|
||||
return True, ""
|
||||
except ImportError:
|
||||
return False, "PIL not available for ASCII art rendering\nInstall with: pip install Pillow"
|
||||
@@ -0,0 +1,43 @@
|
||||
"""Base class for poster renderers."""
|
||||
|
||||
from abc import ABC, abstractmethod
|
||||
import os
|
||||
|
||||
|
||||
class PosterRenderer(ABC):
|
||||
"""Abstract base class for poster rendering implementations."""
|
||||
|
||||
@abstractmethod
|
||||
def render(self, image_path: str, width: int = 40) -> str:
|
||||
"""Render a poster image to a string.
|
||||
|
||||
Args:
|
||||
image_path: Path to the poster image file
|
||||
width: Desired width in characters
|
||||
|
||||
Returns:
|
||||
Rendered poster as a string
|
||||
"""
|
||||
pass
|
||||
|
||||
def validate_image(self, image_path: str) -> tuple[bool, str]:
|
||||
"""Validate that image file exists.
|
||||
|
||||
Args:
|
||||
image_path: Path to validate
|
||||
|
||||
Returns:
|
||||
Tuple of (is_valid, error_message)
|
||||
"""
|
||||
if not os.path.exists(image_path):
|
||||
return False, f"Image file not found: {image_path}"
|
||||
return True, ""
|
||||
|
||||
@abstractmethod
|
||||
def is_available(self) -> tuple[bool, str]:
|
||||
"""Check if this renderer is available on the system.
|
||||
|
||||
Returns:
|
||||
Tuple of (is_available, message)
|
||||
"""
|
||||
pass
|
||||
@@ -0,0 +1,51 @@
|
||||
"""Rich-pixels renderer for high-quality terminal image display."""
|
||||
|
||||
from .base import PosterRenderer
|
||||
from typing import Union
|
||||
|
||||
|
||||
class RichPixelsPosterRenderer(PosterRenderer):
|
||||
"""Render posters using rich-pixels library for high-quality display."""
|
||||
|
||||
def render(self, image_path: str, width: int = 40) -> Union[str, object]:
|
||||
"""Render poster using rich-pixels.
|
||||
|
||||
Args:
|
||||
image_path: Path to the poster image
|
||||
width: Width in characters (default: 40)
|
||||
|
||||
Returns:
|
||||
Rich Pixels object (Renderable) or error string
|
||||
"""
|
||||
is_valid, error_msg = self.validate_image(image_path)
|
||||
if not is_valid:
|
||||
return error_msg
|
||||
|
||||
is_available, msg = self.is_available()
|
||||
if not is_available:
|
||||
return msg
|
||||
|
||||
try:
|
||||
from rich_pixels import Pixels
|
||||
|
||||
# Create a Pixels object from the image
|
||||
# Return the Pixels object directly - it's a Rich Renderable
|
||||
# that Textual can display natively
|
||||
pixels = Pixels.from_image_path(image_path, resize=(width * 2, width * 2))
|
||||
|
||||
return pixels
|
||||
|
||||
except Exception as e:
|
||||
return f"Failed to display image with rich-pixels: {e}\nPoster at: {image_path}"
|
||||
|
||||
def is_available(self) -> tuple[bool, str]:
|
||||
"""Check if rich-pixels is installed.
|
||||
|
||||
Returns:
|
||||
Tuple of (is_available, message)
|
||||
"""
|
||||
try:
|
||||
import rich_pixels
|
||||
return True, ""
|
||||
except ImportError:
|
||||
return False, "rich-pixels not installed. Install with: pip install rich-pixels"
|
||||
@@ -0,0 +1,55 @@
|
||||
"""Viu terminal image viewer renderer."""
|
||||
|
||||
import subprocess
|
||||
import shutil
|
||||
from .base import PosterRenderer
|
||||
|
||||
|
||||
class ViuPosterRenderer(PosterRenderer):
|
||||
"""Render posters using viu terminal image viewer."""
|
||||
|
||||
def render(self, image_path: str, width: int = 40) -> str:
|
||||
"""Render poster using viu.
|
||||
|
||||
Args:
|
||||
image_path: Path to the poster image
|
||||
width: Width in characters (default: 40)
|
||||
|
||||
Returns:
|
||||
Viu-rendered image with ANSI escape sequences
|
||||
"""
|
||||
is_valid, error_msg = self.validate_image(image_path)
|
||||
if not is_valid:
|
||||
return error_msg
|
||||
|
||||
is_available, msg = self.is_available()
|
||||
if not is_available:
|
||||
return msg
|
||||
|
||||
try:
|
||||
# Run viu to render the image
|
||||
# -w <width>: width in characters
|
||||
# -t: transparent background
|
||||
result = subprocess.run(
|
||||
['viu', '-w', str(width), '-t', image_path],
|
||||
capture_output=True,
|
||||
check=True
|
||||
)
|
||||
# Decode bytes output, preserving ANSI escape sequences
|
||||
return result.stdout.decode('utf-8', errors='replace')
|
||||
|
||||
except subprocess.CalledProcessError as e:
|
||||
stderr_msg = e.stderr.decode('utf-8', errors='replace') if e.stderr else 'Unknown error'
|
||||
return f"Failed to render image with viu: {stderr_msg}\nPoster at: {image_path}"
|
||||
except Exception as e:
|
||||
return f"Failed to display image: {e}\nPoster at: {image_path}"
|
||||
|
||||
def is_available(self) -> tuple[bool, str]:
|
||||
"""Check if viu is installed.
|
||||
|
||||
Returns:
|
||||
Tuple of (is_available, message)
|
||||
"""
|
||||
if shutil.which('viu'):
|
||||
return True, ""
|
||||
return False, "viu not installed. Install with: cargo install viu"
|
||||
Reference in New Issue
Block a user