Dup Goto 📝

TextIconGenerator_001

DAW/reaper/icons 08-09 15:58:52
To Pop
192 lines, 566 words, 5599 chars Sunday 2026-08-09 15:58:52

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()