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()
To automate
#!/usr/bin/env python3
import argparse
import os
import subprocess
import sys
def parse_color(color_str: str) -> str:
"""Formats 3-digit or 6-digit hex values, ensuring a leading '#'."""
color_str = color_str.strip()
if not color_str.startswith("#"):
if len(color_str) in (3, 6) and all(
c in "0123456789abcdefABCDEF" for c in color_str
):
return f"#{color_str}"
return color_str
def process_line(line: str, clobber: bool = False, mkicon_bin: str = "mkicon"):
"""Parses a single CSV line and invokes mkicon via subprocess."""
line = line.strip()
# Skip empty lines and comments
if not line or line.startswith("#"):
return
parts = [p.strip() for p in line.split(",")]
if len(parts) < 4:
print(
f"Skipping invalid line (requires at least name,bg,fg,text): {line}",
file=sys.stderr,
)
return
name = parts[0]
bg_param = parts[1]
fg_color = parse_color(parts[2])
text = parts[3]
size = parts[4] if len(parts) >= 5 and parts[4] else None
# Construct the output filename
output_filename = f"jda_tbi_{name}.png"
# Check clobber flag
if os.path.exists(output_filename) and not clobber:
print(f"Skipping {output_filename} (file exists, use -c to overwrite)")
return
# Base command calling executable 'mkicon' directly
cmd = [
mkicon_bin,
text,
"-o",
output_filename,
"--fg-color",
fg_color,
]
# Auto-detect if bg_param is a background image file vs color
if os.path.isfile(bg_param):
cmd.extend(["--bg-image", bg_param])
else:
cmd.extend(["--bg-color", parse_color(bg_param)])
if size:
cmd.extend(["--size", str(size)])
print(f"Generating {output_filename} ...")
try:
result = subprocess.run(cmd, check=True, capture_output=True, text=True)
if result.stdout:
print(f" {result.stdout.strip()}")
except FileNotFoundError:
print(
f"Error: Could not execute '{mkicon_bin}'. Ensure it is in your PATH or current directory and is executable (chmod +x {mkicon_bin}).",
file=sys.stderr,
)
sys.exit(1)
except subprocess.CalledProcessError as e:
print(
f"Error running mkicon for '{name}': {e.stderr.strip()}",
file=sys.stderr,
)
def main():
parser = argparse.ArgumentParser(
description="Batch generate Reaper toolbar icons using the mkicon executable."
)
# Optional clobber argument
parser.add_argument(
"-c",
"--clobber",
action="store_true",
help="Overwrite existing output PNG files",
)
# Executable path override (defaults to 'mkicon' or './mkicon')
parser.add_argument(
"--bin",
type=str,
default="./mkicon" if os.path.exists("./mkicon") else "mkicon",
help="Path or command name for the mkicon executable (default: ./mkicon or mkicon)",
)
# Positional text files argument (nargs='*' allows 0 or more files)
parser.add_argument(
"files",
metavar="FILE",
nargs="*",
help="One or more text/CSV files containing icon definitions. Reads stdin if omitted.",
)
args = parser.parse_args()
# Case 1: Read from provided files
if args.files:
for filepath in args.files:
if not os.path.exists(filepath):
print(f"Error: File '{filepath}' not found.", file=sys.stderr)
continue
with open(filepath, "r", encoding="utf-8") as f:
for line in f:
process_line(
line, clobber=args.clobber, mkicon_bin=args.bin
)
# Case 2: Read from stdin
else:
if sys.stdin.isatty():
print(
"Enter icon definitions (Ctrl+D / Ctrl+Z to finish):",
file=sys.stderr,
)
for line in sys.stdin:
process_line(line, clobber=args.clobber, mkicon_bin=args.bin)
if __name__ == "__main__":
main()
with e.g.
spire,080,fff,Sp
realimit,444,fff,Lim
test,sb3_icon1_src.png,fff,
and run via
mkicons icon_list_1
One warning: at present this only works with the scripts in the current directory.