☰ Dup Goto 📝

TextIconGenerator_001

DAW/reaper/icons 08-13 15:54:56
To Pop
351 lines, 1008 words, 10028 chars Thursday 2026-08-13 15:54:56

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.