If I'm using the same track templates again and again (e.g. if studying one or two synths), I'll add a toolbar icon to insert a track. To make icons for these, I got Gemini to knock up the following script. Defaults go in mkicon.json.
{
"bg_color": "#000000",
"fg_color": "#FFFFFF",
"font": "/path/to/font",
"size": 30
}
Usage:
./mkicon --fg-color "#F00" --bg-color "#050" -o icon_name.png Xyz
Source:
#!/usr/bin/env python3
import argparse
import json
import os
import sys
from PIL import Image, ImageDraw, ImageEnhance, ImageFont
CONFIG_FILE = "mkicon.json"
def load_config() -> dict:
"""Loads default configuration from mkicon.json if present."""
if os.path.exists(CONFIG_FILE):
try:
with open(CONFIG_FILE, "r", encoding="utf-8") as f:
return json.load(f)
except Exception as e:
print(
f"Warning: Failed to load {CONFIG_FILE}: {e}", file=sys.stderr
)
return {}
def make_reaper_icon(
text: str,
output_path: str = "toolbar_icon.png",
bg_color: str = "#000000",
fg_color: str = "#FFFFFF",
bg_image_path: str | None = None,
icon_size: int = 30,
font_path: str | None = "arialbd.ttf",
):
"""Generates a 3-state horizontal Reaper toolbar icon PNG (90x30)."""
# 1. Base Image Setup
if bg_image_path:
try:
img = Image.open(bg_image_path).convert("RGBA")
except FileNotFoundError:
print(
f"Error: Background image file '{bg_image_path}' not found.",
file=sys.stderr,
)
sys.exit(1)
width, height = img.size
# Center-crop to square
min_dim = min(width, height)
left = (width - min_dim) // 2
top = (height - min_dim) // 2
img = img.crop((left, top, left + min_dim, top + min_dim))
# Resize to target icon size
base_icon = img.resize(
(icon_size, icon_size), Image.Resampling.LANCZOS
)
else:
base_icon = Image.new("RGBA", (icon_size, icon_size), color=bg_color)
# 2. Add Centered Text
draw = ImageDraw.Draw(base_icon)
# Auto-adjust font size based on text length
font_size = int(icon_size * (0.65 if len(text) <= 2 else 0.45))
font = None
# Try requested/default font paths
font_candidates = [font_path] if font_path else []
font_candidates.extend(["arialbd.ttf", "Arial Bold.ttf", "arial.ttf"])
for candidate in font_candidates:
if candidate:
try:
font = ImageFont.truetype(candidate, font_size)
break
except OSError:
continue
if font is None:
font = ImageFont.load_default()
# Precise center alignment
bbox = draw.textbbox((0, 0), text, font=font)
text_w = bbox[2] - bbox[0]
text_h = bbox[3] - bbox[1]
x = (icon_size - text_w) / 2 - bbox[0]
y = (icon_size - text_h) / 2 - bbox[1]
draw.text((x, y), text, fill=fg_color, font=font)
# 3. Create Hover and Pressed States
normal_state = base_icon
hover_state = ImageEnhance.Brightness(normal_state).enhance(1.2)
pressed_state = ImageEnhance.Brightness(normal_state).enhance(0.8)
# 4. Stack Horizontally into 3x1 Reaper Format (90x30)
reaper_icon = Image.new("RGBA", (icon_size * 3, icon_size))
reaper_icon.paste(normal_state, (0, 0))
reaper_icon.paste(hover_state, (icon_size, 0))
reaper_icon.paste(pressed_state, (icon_size * 2, 0))
reaper_icon.save(output_path, "PNG")
print(
f"Successfully generated Reaper icon: {output_path} ({icon_size * 3}x{icon_size}px)"
)
def main():
config = load_config()
parser = argparse.ArgumentParser(
description="Generate a horizontal 3-state Reaper toolbar icon PNG (90x30)."
)
# Required Argument
parser.add_argument(
"text",
type=str,
help="Text to display on the icon (e.g., 'HZ', 'Z3', 'Sp')",
)
# Optional Arguments (defaults pulled from mkicon.json if present)
parser.add_argument(
"-o",
"--output",
type=str,
default=config.get("output", "toolbar_icon.png"),
help="Output PNG filepath (default: toolbar_icon.png)",
)
parser.add_argument(
"--bg-color",
type=str,
default=config.get("bg_color", "#000000"),
help="Background hex color or name (default: #000000)",
)
parser.add_argument(
"--fg-color",
type=str,
default=config.get("fg_color", "#FFFFFF"),
help="Foreground text hex color or name (default: #FFFFFF)",
)
parser.add_argument(
"--bg-image",
type=str,
default=config.get("bg_image", None),
help="Optional path to a background image (will be center-cropped to square)",
)
parser.add_argument(
"--size",
type=int,
default=config.get("size", 30),
help="Width/height of a single state in pixels (default: 30, yields 90x30 image)",
)
parser.add_argument(
"--font",
type=str,
default=config.get("font", "arialbd.ttf"),
help="Path to a custom TTF/OTF font file (default: Arial Bold)",
)
args = parser.parse_args()
make_reaper_icon(
text=args.text,
output_path=args.output,
bg_color=args.bg_color,
fg_color=args.fg_color,
bg_image_path=args.bg_image,
icon_size=args.size,
font_path=args.font,
)
if __name__ == "__main__":
main()