diff --git a/mini_apps/README.md b/mini_apps/README.md deleted file mode 100644 index 47c14c8..0000000 --- a/mini_apps/README.md +++ /dev/null @@ -1,5 +0,0 @@ -# Mini Apps - -This directory contains things that are too complex to be considered a "script", yet too personalized to be useful to anyone else. - -I may occasionally promote things from here to actual public repos someday. \ No newline at end of file diff --git a/mini_apps/memegen/bin/memegen b/mini_apps/memegen/bin/memegen deleted file mode 100755 index 470d3d5..0000000 --- a/mini_apps/memegen/bin/memegen +++ /dev/null @@ -1,228 +0,0 @@ -#! /usr/bin/env python -import argparse -import sys -import logging -import json -import subprocess -from PIL import Image, ImageDraw, ImageFont -from typing import List, Dict, Any, Optional -from pathlib import Path -from dataclasses import dataclass -from enum import Enum -from datetime import datetime - -logger = logging.getLogger(__name__) -CONFIG_DIR = Path(__file__).parent.parent / "data" -DEFAULT_OUTPUT_DIR = Path("~/Pictures/memes").expanduser() - - -@dataclass -class MemeTemplate: - image_path: Path - config: Dict[str, Any] - - -class HorizontalAlignment(Enum): - LEFT = "left" - CENTER = "center" - RIGHT = "right" - - -class VerticalAlignment(Enum): - TOP = "top" - CENTER = "center" - BOTTOM = "bottom" - - -def discover_templates() -> List[str]: - # Find all directories in the templates directory - return [p.name for p in (CONFIG_DIR / "templates").glob("*") if p.is_dir()] - - -def load_template(name: str) -> MemeTemplate: - logger.info(f"Loading template: {name}") - - # Find the template directory - template_dir = CONFIG_DIR / "templates" / name - if not template_dir.exists(): - logger.error(f"Template {name} does not exist") - sys.exit(1) - - return MemeTemplate( - image_path=template_dir / "template.png", - config=json.loads((template_dir / "config.json").read_text()), - ) - - -def calc_width_from_image(width_str: str, image_width: int) -> int: - if width_str.endswith("%"): - return int(image_width * int(width_str[:-1]) / 100) - else: - return int(width_str) - - -def render_text_on_image(image: Image, text: str, zone: str, config: Dict[str, Any]): - # NOTE: This must handle text with newlines - # Get the zone config - zone_config = config["zones"][zone] - horizontal_alignment = HorizontalAlignment(zone_config["horizontal_align"]) - vertical_alignment = VerticalAlignment(zone_config["vertical_align"]) - text_width = calc_width_from_image(zone_config["width"], image.width) - max_line_height = zone_config["max_line_height"] - font_path = CONFIG_DIR / "fonts" / config["font"] - - # Create the font - font = None - font_size = 1 - while True: - font = ImageFont.truetype(str(font_path), font_size) - - # Split the text into lines - lines = text.splitlines() - bounding_boxes = [] - for line in lines: - bounding_boxes.append(font.getbbox(line)) - - # Calculate the height of the text - line_height = max([bbox[3] for bbox in bounding_boxes]) - total_height = sum( - [bbox[3] + zone_config["line_spacing"] for bbox in bounding_boxes] - ) - max_width = max([bbox[2] for bbox in bounding_boxes]) - - # If we have a max line height, ensure we don't exceed it - if max_line_height and line_height > max_line_height: - font_size -= 1 - break - - # Don't exceed the width - if max_width > text_width: - font_size -= 1 - break - - # Increment the font size - font_size += 1 - - # Determine the starting Y position - y = zone_config["vertical_offset"] - if vertical_alignment == VerticalAlignment.CENTER: - y += (image.height - total_height) / 2 - elif vertical_alignment == VerticalAlignment.BOTTOM: - y += image.height - total_height - - # Render each line onto the image - draw = ImageDraw.Draw(image) - for line in text.splitlines(): - # Calculate the x position - if horizontal_alignment == HorizontalAlignment.LEFT: - x = zone_config["horizontal_offset"] - elif horizontal_alignment == HorizontalAlignment.CENTER: - x = ((image.width - font.getbbox(line)[2]) / 2) + zone_config[ - "horizontal_offset" - ] - elif horizontal_alignment == HorizontalAlignment.RIGHT: - x = (image.width - font.getbbox(line)[2]) + zone_config["horizontal_offset"] - else: - raise ValueError(f"Invalid horizontal alignment: {horizontal_alignment}") - - # Render the text - draw.text( - (x, y), - line, - fill=tuple(config["fill_color"]), - stroke_fill=tuple(config["stroke_color"]), - stroke_width=config["stroke_width"], - font=font, - ) - - # Increment the y position - y += line_height + zone_config["line_spacing"] - - -def main() -> int: - # Handle program arguments - ap = argparse.ArgumentParser(prog="memegen", description="Generates memes") - ap.add_argument( - "template", help="The template to use", choices=discover_templates() - ) - ap.add_argument("--top-text", help="Top text (if applicable)") - ap.add_argument("--bottom-text", help="Bottom text (if applicable)") - ap.add_argument( - "--keep-case", help="Keep the case of the text", action="store_true" - ) - ap.add_argument("--output", "-o", help="Output file path") - ap.add_argument( - "--no-show", help="Don't show the image after creation", action="store_true" - ) - ap.add_argument( - "-v", "--verbose", help="Enable verbose logging", action="store_true" - ) - args = ap.parse_args() - - # Configure logging - logging.basicConfig( - level=logging.DEBUG if args.verbose else logging.INFO, - format="%(levelname)s: %(message)s", - ) - - # Load the template - template = load_template(args.template) - template_supports_top_text = "top" in template.config["zones"] - template_supports_bottom_text = "bottom" in template.config["zones"] - - # Ensure we have text - if args.top_text and not template_supports_top_text: - logger.error(f"Template {args.template} does not support top text") - sys.exit(1) - if args.bottom_text and not template_supports_bottom_text: - logger.error(f"Template {args.template} does not support bottom text") - sys.exit(1) - if not args.top_text and not args.bottom_text: - logger.error("No text provided") - if not all([template_supports_top_text, template_supports_bottom_text]): - required_text = "top" if template_supports_top_text else "bottom" - logger.error( - f"Template {args.template} requires the --{required_text}-text argument" - ) - sys.exit(1) - - # Transform the text - # fmt:off - top_text = args.top_text.upper() if args.top_text and (not args.keep_case) else args.top_text - bottom_text = args.bottom_text.upper() if args.bottom_text and (not args.keep_case) else args.bottom_text - top_text = top_text.replace("\\n", "\n").replace("\\N", "\n") if top_text else None - bottom_text = bottom_text.replace("\\n", "\n").replace("\\N", "\n") if bottom_text else None - # fmt: on - - # Load the image - image = Image.open(template.image_path) - - # Render the text - if top_text: - render_text_on_image(image, top_text, "top", template.config) - if bottom_text: - render_text_on_image(image, bottom_text, "bottom", template.config) - - # Build the output path - output_path = ( - Path(args.output) - if args.output - else ( - DEFAULT_OUTPUT_DIR - / f"meme-{datetime.now().strftime('%Y-%m-%d-%H-%M-%S')}.{args.template}.png" - ) - ) - output_path.parent.mkdir(parents=True, exist_ok=True) - - # Save the image - image.save(output_path) - - # Show the image - if not args.no_show: - subprocess.run(["xdg-open", str(output_path)]) - - return 0 - - -if __name__ == "__main__": - sys.exit(main()) diff --git a/mini_apps/memegen/data/fonts/impact.ttf b/mini_apps/memegen/data/fonts/impact.ttf deleted file mode 100644 index 114e6c1..0000000 Binary files a/mini_apps/memegen/data/fonts/impact.ttf and /dev/null differ diff --git a/mini_apps/memegen/data/templates/bernie-asking/config.json b/mini_apps/memegen/data/templates/bernie-asking/config.json deleted file mode 100644 index 5a2f0f4..0000000 --- a/mini_apps/memegen/data/templates/bernie-asking/config.json +++ /dev/null @@ -1,17 +0,0 @@ -{ - "font": "impact.ttf", - "fill_color": [255, 255, 255], - "stroke_color": [0, 0, 0], - "stroke_width": 2, - "zones": { - "bottom": { - "horizontal_align": "center", - "horizontal_offset": 0, - "vertical_align": "bottom", - "vertical_offset": -50, - "width": "80%", - "max_line_height": 50, - "line_spacing": 5 - } - } -} \ No newline at end of file diff --git a/mini_apps/memegen/data/templates/bernie-asking/template.png b/mini_apps/memegen/data/templates/bernie-asking/template.png deleted file mode 100644 index 0d19bb4..0000000 Binary files a/mini_apps/memegen/data/templates/bernie-asking/template.png and /dev/null differ diff --git a/mini_apps/memegen/data/templates/hotline-bling/config.json b/mini_apps/memegen/data/templates/hotline-bling/config.json deleted file mode 100644 index cded53e..0000000 --- a/mini_apps/memegen/data/templates/hotline-bling/config.json +++ /dev/null @@ -1,34 +0,0 @@ -{ - "font": "impact.ttf", - "fill_color": [ - 0, - 0, - 0 - ], - "stroke_color": [ - 0, - 0, - 0 - ], - "stroke_width": 0, - "zones": { - "top": { - "horizontal_align": "right", - "horizontal_offset": -5, - "vertical_align": "top", - "vertical_offset": 75, - "width": "45%", - "max_line_height": 50, - "line_spacing": 5 - }, - "bottom": { - "horizontal_align": "right", - "horizontal_offset": -5, - "vertical_align": "bottom", - "vertical_offset": -75, - "width": "45%", - "max_line_height": 50, - "line_spacing": 5 - } - } -} \ No newline at end of file diff --git a/mini_apps/memegen/data/templates/hotline-bling/template.png b/mini_apps/memegen/data/templates/hotline-bling/template.png deleted file mode 100644 index 7002060..0000000 Binary files a/mini_apps/memegen/data/templates/hotline-bling/template.png and /dev/null differ diff --git a/mini_apps/memegen/data/templates/megamind/config.json b/mini_apps/memegen/data/templates/megamind/config.json deleted file mode 100644 index 7ca4dbc..0000000 --- a/mini_apps/memegen/data/templates/megamind/config.json +++ /dev/null @@ -1,17 +0,0 @@ -{ - "font": "impact.ttf", - "fill_color": [255, 255, 255], - "stroke_color": [0, 0, 0], - "stroke_width": 2, - "zones": { - "top": { - "horizontal_align": "center", - "horizontal_offset": 0, - "vertical_align": "top", - "vertical_offset": 5, - "width": "80%", - "max_line_height": 80, - "line_spacing": 5 - } - } -} \ No newline at end of file diff --git a/mini_apps/memegen/data/templates/megamind/template.png b/mini_apps/memegen/data/templates/megamind/template.png deleted file mode 100644 index 52bf5e5..0000000 Binary files a/mini_apps/memegen/data/templates/megamind/template.png and /dev/null differ