Files
PaintPlus/scripts/generate_classic_eyes_ppm.py
T
Claude 6238318f9b Add classic eye image assets and import scripts
- Generate 18 classic eye variants in 3 styles (realistic, anime, cartoon)
  with 6 colors each (blue, green, brown, hazel, grey, amber)
- Add scripts for generating, converting, and importing eyes:
  - generate_classic_eyes_ppm.py: Generate PPM images (no dependencies)
  - download_classic_eyes.py: Full-featured generator with PIL
  - startup_import_eyes.py: Import eyes on backend startup
  - import_saved_eyes.py: Import from saved directory
- Update docker-compose to mount scripts directory
- Include metadata.json for tracking eye properties
2026-01-25 02:41:37 +00:00

263 lines
8.2 KiB
Python

#!/usr/bin/env python3
"""
Generate classic eye images using only the Python standard library.
Creates PPM format images that can be converted to PNG when PIL is available.
This script requires NO external dependencies - it uses the PPM image format
which is a simple text/binary format readable by most image tools.
Usage:
python scripts/generate_classic_eyes_ppm.py
The generated PPM files can be converted to PNG using:
- PIL/Pillow: Image.open('eye.ppm').save('eye.png')
- ImageMagick: convert eye.ppm eye.png
- GIMP: Open and export as PNG
"""
import os
import math
from pathlib import Path
def create_ppm(width: int, height: int) -> list:
"""Create an empty PPM image as a 2D list of (R, G, B) tuples."""
return [[(0, 0, 0) for _ in range(width)] for _ in range(height)]
def set_pixel(img: list, x: int, y: int, color: tuple):
"""Set a pixel in the image, with bounds checking."""
height = len(img)
width = len(img[0]) if height > 0 else 0
if 0 <= x < width and 0 <= y < height:
img[y][x] = color
def draw_filled_circle(img: list, cx: int, cy: int, radius: int, color: tuple):
"""Draw a filled circle."""
for y in range(-radius, radius + 1):
for x in range(-radius, radius + 1):
if x*x + y*y <= radius*radius:
set_pixel(img, cx + x, cy + y, color)
def draw_circle_ring(img: list, cx: int, cy: int, radius: int, color: tuple, thickness: int = 1):
"""Draw a circle outline."""
for y in range(-radius - thickness, radius + thickness + 1):
for x in range(-radius - thickness, radius + thickness + 1):
dist_sq = x*x + y*y
if (radius - thickness)**2 <= dist_sq <= (radius + thickness)**2:
set_pixel(img, cx + x, cy + y, color)
def blend_color(c1: tuple, c2: tuple, ratio: float) -> tuple:
"""Blend two colors. ratio=0 gives c1, ratio=1 gives c2."""
return (
int(c1[0] * (1 - ratio) + c2[0] * ratio),
int(c1[1] * (1 - ratio) + c2[1] * ratio),
int(c1[2] * (1 - ratio) + c2[2] * ratio)
)
def save_ppm(img: list, filepath: str):
"""Save image as PPM format (P6 binary)."""
height = len(img)
width = len(img[0]) if height > 0 else 0
with open(filepath, 'wb') as f:
# PPM header
f.write(f"P6\n{width} {height}\n255\n".encode())
# Pixel data
for row in img:
for r, g, b in row:
f.write(bytes([r, g, b]))
def save_ppm_text(img: list, filepath: str):
"""Save image as PPM format (P3 text - more portable)."""
height = len(img)
width = len(img[0]) if height > 0 else 0
with open(filepath, 'w') as f:
f.write(f"P3\n{width} {height}\n255\n")
for row in img:
line = ' '.join(f"{r} {g} {b}" for r, g, b in row)
f.write(line + '\n')
def create_classic_eye(
size: int = 200,
iris_color: tuple = (70, 130, 180),
style: str = "realistic"
) -> list:
"""
Generate a classic stylized eye image.
Args:
size: Image size (square)
iris_color: RGB color for the iris
style: "realistic", "anime", "cartoon"
Returns:
2D list of (R, G, B) tuples
"""
img = create_ppm(size, size)
cx, cy = size // 2, size // 2
eye_radius = size // 2 - 5
iris_radius = int(eye_radius * 0.7)
pupil_radius = int(iris_radius * 0.35)
if style == "realistic":
# White of the eye (sclera)
sclera_color = (250, 245, 240)
draw_filled_circle(img, cx, cy, eye_radius, sclera_color)
# Sclera outline
draw_circle_ring(img, cx, cy, eye_radius, (180, 160, 150), 2)
# Iris with gradient effect (simplified)
for r in range(iris_radius, 0, -1):
ratio = r / iris_radius
color = blend_color((40, 40, 40), iris_color, ratio)
draw_circle_ring(img, cx, cy, r, color, 1)
# Pupil
draw_filled_circle(img, cx, cy, pupil_radius, (10, 10, 10))
# Highlight
hl_x = cx - pupil_radius // 2
hl_y = cy - pupil_radius // 2
hl_r = pupil_radius // 3
draw_filled_circle(img, hl_x, hl_y, hl_r, (255, 255, 255))
elif style == "anime":
# Larger iris for anime style
iris_radius = int(eye_radius * 0.85)
# Sclera
draw_filled_circle(img, cx, cy, eye_radius, (255, 255, 255))
draw_circle_ring(img, cx, cy, eye_radius, (0, 0, 0), 3)
# Large iris
draw_filled_circle(img, cx, cy, iris_radius, iris_color)
# Pupil
pupil_radius = int(iris_radius * 0.25)
draw_filled_circle(img, cx, cy, pupil_radius, (0, 0, 0))
# Large highlight
hl_x = cx - iris_radius // 3
hl_y = cy - iris_radius // 3
hl_r = iris_radius // 3
draw_filled_circle(img, hl_x, hl_y, hl_r, (255, 255, 255))
# Secondary highlight
hl2_x = cx + iris_radius // 4
hl2_y = cy + iris_radius // 4
hl2_r = iris_radius // 6
draw_filled_circle(img, hl2_x, hl2_y, hl2_r, (220, 220, 220))
elif style == "cartoon":
# Simple cartoon eye
draw_filled_circle(img, cx, cy, eye_radius, (255, 255, 255))
draw_circle_ring(img, cx, cy, eye_radius, (0, 0, 0), 4)
draw_filled_circle(img, cx, cy, iris_radius, iris_color)
draw_circle_ring(img, cx, cy, iris_radius, (0, 0, 0), 2)
draw_filled_circle(img, cx, cy, pupil_radius, (0, 0, 0))
# Highlight
hl_x = cx - pupil_radius
hl_y = cy - pupil_radius
hl_r = pupil_radius // 2
draw_filled_circle(img, hl_x, hl_y, hl_r, (255, 255, 255))
return img
def main():
"""Generate a set of classic eye images."""
output_dir = Path(__file__).parent.parent / "data" / "classic_eyes_ppm"
output_dir.mkdir(parents=True, exist_ok=True)
print("=" * 60)
print("Classic Eye Generator (PPM Format)")
print("=" * 60)
print(f"Output directory: {output_dir}")
# Eye colors
colors = {
"blue": (70, 130, 180),
"green": (60, 140, 90),
"brown": (139, 90, 43),
"hazel": (150, 120, 70),
"grey": (120, 130, 140),
"amber": (180, 130, 50),
}
styles = ["realistic", "anime", "cartoon"]
metadata = []
generated = 0
print("\nGenerating eyes...")
for style in styles:
for color_name, color_rgb in colors.items():
name = f"classic_{style}_{color_name}"
filename = f"{name}.ppm"
filepath = output_dir / filename
img = create_classic_eye(size=200, iris_color=color_rgb, style=style)
save_ppm(img, str(filepath))
metadata.append({
'filename': filename.replace('.ppm', '.png'), # For after conversion
'ppm_filename': filename,
'name': f"Classic {style.title()} Eye - {color_name.title()}",
'description': f"A {style} style eye with {color_name} iris color",
'tags': f"eye,classic,{style},{color_name},medium",
'width': 200,
'height': 200
})
generated += 1
print(f" Generated: {name}.ppm")
# Save metadata
import json
metadata_path = output_dir / "metadata.json"
with open(metadata_path, 'w') as f:
json.dump(metadata, f, indent=2)
print(f"\n Metadata saved to: {metadata_path}")
print(f"\nGenerated {generated} eye images in PPM format.")
# Create conversion script
convert_script = output_dir / "convert_to_png.py"
with open(convert_script, 'w') as f:
f.write('''#!/usr/bin/env python3
"""Convert PPM files to PNG using PIL."""
from pathlib import Path
from PIL import Image
ppm_dir = Path(__file__).parent
for ppm_file in ppm_dir.glob("*.ppm"):
png_file = ppm_file.with_suffix('.png')
img = Image.open(ppm_file)
img.save(png_file, 'PNG')
print(f"Converted: {ppm_file.name} -> {png_file.name}")
''')
print(f"\n Conversion script: {convert_script}")
print("\nTo convert PPM to PNG, run inside the backend container:")
print(f" python {convert_script}")
print("\nOr use ImageMagick:")
print(f" cd {output_dir} && for f in *.ppm; do convert \"$f\" \"${{f%.ppm}}.png\"; done")
if __name__ == "__main__":
main()