Files
ubuntu-post-install/vendor/paintplus/scripts/generate_classic_eyes_ppm.py
T
Claude b2d064573f Move ai-stack and paintplus vendored source under vendor/
Matches the existing vendor/easy-asterisk convention (used by
services/asterisk.sh) instead of two one-off top-level directories that
cluttered the repo root and didn't look like anything else next to
setup.sh, lib/, services/, extras/. Only the two services' own SRC_DIR
path resolution and header comments needed updating — nothing else in
the repo referenced the old ./ai-stack / ./paintplus paths.

Also documents vendor/ in README.md's Layout section.
2026-08-04 12:55:09 +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()