Generate memes from templates with custom text overlays using Python Pillow, supporting 20+ popular formats with batch generation for content calendars
Resources
2Install
npx skillscat add marrowleaf/hermes-meme-generator Install via the SkillsCat registry.
Meme Generator
Create memes from popular templates with custom text overlays. Supports caption-only memes, template-based memes, batch generation, and direct sharing via Telegram or Obsidian.
Overview
Meme Generator lets you quickly produce memes using a library of 20+ popular templates. It uses Python Pillow for text overlay with Impact font, white text, and black stroke — the classic meme aesthetic. Memes can be saved to Obsidian attachments, sent via Telegram, or batch-generated for content calendars.
Commands
/meme <template> <top_text> | <bottom_text>
Generate a meme from a template with top and/or bottom text.
Arguments:
<template>— Template name or ID (use/meme-templatesto browse)<top_text>— Text for the top panel (use_for none)<bottom_text>— Text for the bottom panel (separated by|)
Examples:
/meme drake "Write tests" | "Ship directly to prod"
/meme distracted-boyfriend "New framework" | "Current framework" | "Old framework"
/meme this-is-fine _ | "When the server crashes on Friday"/meme-caption <text>
Generate a caption-only meme (Impact font, white text, black stroke on solid background).
Options:
--bg <color>— Background color (default:black; options:black,white,red,blue,custom:#hex)--font-size <size>— Font size in pixels (default: auto-sized)--width <px>— Image width (default:600)--height <px>— Image height (default: auto from text)
Examples:
/meme-caption "Nobody: ... Developers at 3am:"
/meme-caption "When it works on the first try" --bg white
/meme-caption "404 Motivation Not Found" --bg custom:#1a1a2e/meme-custom <image_path> <top_text> | <bottom_text>
Apply meme text overlay to a custom image.
Options:
--font <name>— Font to use (default:impact; options:impact,arial,comic-sans,helvetica)--stroke-width <px>— Stroke width (default:3)--text-color <color>— Text fill color (default:white)--stroke-color <color>— Stroke color (default:black)
Examples:
/meme-custom ~/photo.jpg "My code" | "When my boss walks by"
/meme-custom ~/meme-base.png _ | "Bottom text only" --font arial/meme-templates [filter]
List available meme templates. Optionally filter by name or category.
Examples:
/meme-templates
/meme-templates reactions/meme-batch <csv_file>
Batch generate memes from a CSV file.
CSV format:
template,top_text,bottom_text,output_name
drake,"Write docs","Read the code",meme-001
this-is-fine,,"Server is fine",meme-002
distracted-boyfriend,"New JS framework","Current framework","The old one",meme-003/meme-save <meme_id> [destination]
Save a generated meme to Obsidian attachments or a specified path.
Destination options:
obsidian(default) — Save to~/obsidian-vault/Attachments/memes/telegram— Send directly via Telegramboth— Save to Obsidian and send via Telegram- Custom path — Save to specified directory
Template Library
Reaction Memes
| ID | Template | Panels | Description |
|---|---|---|---|
drake |
Drake Hotline | 2 | Preference/disapproval split |
distracted-boyfriend |
Distracted Boyfriend | 3 | Temptation triangle |
this-is-fine |
This is Fine | 2 | Denial in crisis |
gal-brain |
Expanding Brain | 5 | Escalating ideas |
change-my-mind |
Change My Mind | 1 | Stubborn opinion |
surprised-pikachu |
Surprised Pikachu | 1 | Unexpected consequence |
disaster-girl |
Disaster Girl | 1 | Sinister observer |
side-eye-chloe |
Side-Eye Chloe | 1 | Skeptical reaction |
Comparison Memes
| ID | Template | Panels | Description |
|---|---|---|---|
two-buttons |
Two Buttons | 3 | Difficult choice |
trade-offer |
Trade Offer | 2 | Unfair exchange |
buffet-vs-dessert |
Buffet vs Dessert | 2 | Priority comparison |
is-this-a-pigeon |
Is This a Pigeon | 3 | Misidentification |
Situational Memes
| ID | Template | Panels | Description |
|---|---|---|---|
its-free-real-estate |
It's Free Real Estate | 1 | Dubious offer |
this-is-where-id-put-my-x |
Where I'd Put X | 2 | Absent feature |
slapping-car-roof |
Slapping Car Roof | 1 | Product pitch |
woman-yelling-at-cat |
Woman Yelling at Cat | 2 | Heated disagreement |
panik-kalm-panik |
Panik Kalm Panik | 3 | Anxiety cycle |
monkey-puppet |
Monkey Puppet | 1 | Awkward avoidance |
stonks |
Stonks | 1 | Fake financial success |
press-f |
Press F to Pay Respects | 1 | Mock sorrow |
Tech-Specific
| ID | Template | Panels | Description |
|---|---|---|---|
deployment-pipeline |
Deployment Pipeline | 4 | CI/CD humor |
stack-overflow |
Stack Overflow | 2 | Copy-paste coding |
works-on-my-machine |
Works on My Machine | 1 | Environment excuses |
git-blame |
Git Blame | 2 | Code archaeology |
Technical Implementation
Image Generation with Pillow
from PIL import Image, ImageDraw, ImageFont
import textwrap
import os
def create_meme(template_name, top_text, bottom_text):
"""Generate a meme from a template with text overlay."""
# Load template image
template_path = get_template_path(template_name)
img = Image.open(template_path)
draw = ImageDraw.Draw(img)
# Load Impact font with fallback
try:
font = ImageFont.truetype("/usr/share/fonts/impact.ttf", size=48)
except OSError:
font = ImageFont.truetype("/usr/share/fonts/truetype/impact.ttf", size=48)
# Text styling: white fill, black stroke
fill_color = (255, 255, 255) # White
stroke_color = (0, 0, 0) # Black
stroke_width = 3
# Calculate text wrapping based on image width
max_chars = int(img.width / 18) # Approximate chars per line
# Draw top text
if top_text and top_text != "_":
lines = textwrap.wrap(top_text.upper(), width=max_chars)
y = 10
for line in lines:
bbox = draw.textbbox((0, 0), line, font=font)
x = (img.width - (bbox[2] - bbox[0])) // 2
draw.text((x, y), line, font=font,
fill=fill_color, stroke_width=stroke_width,
stroke_fill=stroke_color)
y += bbox[3] - bbox[1] + 8
# Draw bottom text
if bottom_text:
lines = textwrap.wrap(bottom_text.upper(), width=max_chars)
y = img.height - 10
for line in reversed(lines):
bbox = draw.textbbox((0, 0), line, font=font)
x = (img.width - (bbox[2] - bbox[0])) // 2
y -= (bbox[3] - bbox[1] + 8)
draw.text((x, y), line, font=font,
fill=fill_color, stroke_width=stroke_width,
stroke_fill=stroke_color)
return img
def create_caption_meme(text, bg_color="black", width=600):
"""Generate a caption-only meme with colored background."""
# Create background
font = ImageFont.truetype("/usr/share/fonts/impact.ttf", size=42)
wrapped = textwrap.wrap(text.upper(), width=30)
line_height = 52
height = (len(wrapped) * line_height) + 40
img = Image.new("RGB", (width, height), bg_color)
draw = ImageDraw.Draw(img)
y = 20
for line in wrapped:
bbox = draw.textbbox((0, 0), line, font=font)
x = (width - (bbox[2] - bbox[0])) // 2
draw.text((x, y), line, font=font,
fill=(255, 255, 255), stroke_width=3,
stroke_fill=(0, 0, 0))
y += line_height
return imgMulti-Panel Meme Assembly
For templates with multiple panels (e.g., Drake, Expanding Brain):
def create_panel_meme(template_name, texts):
"""Build a multi-panel meme by stacking image sections."""
template = TEMPLATE_REGISTRY[template_name]
panels = []
for i, section in enumerate(template["sections"]):
panel = load_panel_image(template_name, i)
if texts[i]:
panel = overlay_text(panel, texts[i],
position=section["text_position"])
panels.append(panel)
return stack_panels(panels, direction=template["layout"])Batch Generation for Content Calendars
Creating memes for a content calendar:
# memes-january.csv
template,top_text,bottom_text,output_name,schedule_date
drake,"Write documentation","Just add comments",meme-001,2025-01-06
this-is-fine,,"Prod is fine",meme-002,2025-01-08
surprised-pikachu,,"When the test passes",meme-003,2025-01-10/meme-batch ~/memes-january.csv
→ 🎨 Generating batch memes (3 items)...
→ ✅ meme-001.png (drake)
→ ✅ meme-002.png (this-is-fine)
→ ✅ meme-003.png (surprised-pikachu)
→ 💾 All saved to: ~/obsidian-vault/Attachments/memes/batch/
📊 Batch Summary:
- Generated: 3/3
- Failed: 0
- Total size: 1.2 MBIntegrate with social-poster skill:
/post-draft twitter "" --media ~/obsidian-vault/Attachments/memes/batch/meme-001.png --tone casualPitfalls
Font availability — Impact font may not be installed on all systems. The skill falls back to available bold sans-serif fonts, but the classic meme look requires Impact. Install
fonts-freefont-ttfor equivalent.Text overflow — Very long text can overflow panel boundaries. The skill auto-wraps text, but extremely long strings may produce tiny font sizes. Keep top/bottom text under 50 characters for best results.
Image quality — Template images must be at least 600px wide. Low-resolution templates produce blurry memes. The template library uses only high-quality source images.
Special characters — Unicode emojis and special characters in Impact font may not render correctly. Stick to ASCII for classic meme text; use
--font arialfor emoji support.Multi-panel alignment — Templates with more than 2 panels require careful text placement. The skill uses predefined text zones per template, but custom images may need manual adjustment.
File size — Meme images with large dimensions can exceed Telegram's 5MB send limit. The skill auto-compresses to stay under limits.
Obsidian attachment path — The default save path
~/obsidian-vault/Attachments/memes/must exist. The skill creates it if missing, but verify vault path configuration.Batch CSV formatting — The batch CSV must use proper quoting for fields containing commas. A malformed CSV will skip the affected row with a warning.
Copyright concerns — Some meme images have copyright restrictions. The skill uses widely-shared template images, but commercial use may require additional licenses.
Pillow version — Requires Pillow >= 9.0 for
stroke_widthparameter indraw.text(). Older versions will throw a TypeError.
Verification Steps
Check Pillow is installed:
python3 -c "from PIL import Image, ImageDraw, ImageFont; print('Pillow OK')"Check Impact font availability:
python3 -c "from PIL import ImageFont; ImageFont.truetype('/usr/share/fonts/impact.ttf', 48); print('Impact font OK')"If missing, install:
sudo apt install fonts-freefont-ttfor download Impact font.Test basic meme generation:
/meme drake "Write code" | "Write tests"Should produce a two-panel Drake meme image and display it.
Test caption-only meme:
/meme-caption "Hello World this is a test"Should produce a black-background meme with white Impact text.
Verify template library:
/meme-templatesShould list 20+ templates with IDs, names, and panel counts.
Test custom image:
Use/meme-customwith a local image file and verify text overlay is positioned correctly.Test Obsidian save:
/meme-save meme-output obsidianCheck
~/obsidian-vault/Attachments/memes/for the saved file.Test Telegram send:
/meme-save meme-output telegramVerify the meme image is sent as a photo in the current Telegram chat.
Test batch generation:
Create a small CSV with 2-3 entries and run/meme-batch. Verify all images are generated and saved.Test error handling:
- Try
/meme nonexistent-template "test"→ should show template not found error - Try
/meme drake "text that is extremely long and goes on forever"→ should auto-wrap or warn
- Try