import os
import requests
import numpy as np
from PIL import Image, ImageFont, ImageDraw
# Hardcoded path to ensure consistency within the repo
FONT_PATH = ".cache/fonts/NotoColorEmoji.ttf"
FONT_URL = "https://github.com/googlefonts/noto-emoji/raw/main/fonts/NotoColorEmoji.ttf"
def load_emoji_font() -> ImageFont.FreeTypeFont:
"""Loads the font, downloading it only if necessary."""
if not os.path.exists(FONT_PATH):
os.makedirs(os.path.dirname(FONT_PATH), exist_ok=True)
print(f"Downloading Noto Emoji to {FONT_PATH}...")
with open(FONT_PATH, "wb") as f:
f.write(requests.get(FONT_URL).content)
# We load at 109px (native bitmap size) to avoid OSError
return ImageFont.truetype(FONT_PATH, 109)
def emoji_to_tensor(emoji: str, grid_size: int = 40, target_size: int = 28) -> np.ndarray:
"""
Renders an emoji to a numpy array, handling downsampling automatically.
Args:
emoji: Unicode character (e.g. "🦎").
grid_size: The full size of the simulation grid.
target_size: The actual size of the emoji within the grid.
"""
font = load_emoji_font()
# 1. Render High-Res (Native 109px)
# We use a large canvas to safely compute the bounding box
canvas_size = 150
img = Image.new("RGBA", (canvas_size, canvas_size), (0, 0, 0, 0))
draw = ImageDraw.Draw(img)
# Center and draw
bbox = draw.textbbox((0, 0), emoji, font=font)
w, h = bbox[2] - bbox[0], bbox[3] - bbox[1]
draw.text(((canvas_size - w) // 2, (canvas_size - h) // 2), emoji, font=font, embedded_color=True)
# 2. Crop to content and Resize High-Quality
# This gets us a clean 'target_size' emoji without empty space
img = img.crop(img.getbbox())
img.thumbnail((target_size, target_size), Image.Resampling.LANCZOS)
# 3. Paste into final grid
final_grid = Image.new("RGBA", (grid_size, grid_size), (0, 0, 0, 0))
offset = ((grid_size - img.width) // 2, (grid_size - img.height) // 2)
final_grid.paste(img, offset)
# 4. Normalize and Premultiply Alpha
arr = np.array(final_grid, dtype=np.float32) / 255.0
arr[..., :3] *= arr[..., 3:4] # Multiply RGB by Alpha
return arr