
Lovstudio Image Creator
- 2 installs
- Updated May 30, 2026
- lovstudio/image-creator-skill
Generates images via AI generation (Gemini/ZenMux), code-based HTML/React rendering to PNG with Playwright, or optimized prompt engineering.
About
Creates images through three mechanisms: end-to-end AI generation, code-based HTML/React rendering to PNG via Playwright, and prompt engineering for external models. A user uses it to produce posters, banners, or cards from a prompt or code.
- End-to-end AI generation via Gemini/ZenMux
- Code-based HTML/React rendered to PNG with Playwright
Lovstudio Image Creator by the numbers
- 2 all-time installs (skills.sh)
- Ranked #1,166 of 1,335 Generative Media skills by installs in the Skillselion catalog
- Data as of Aug 2, 2026 (Skillselion catalog sync)
npx skills add https://github.com/lovstudio/image-creator-skill --skill lovstudio-image-creatorAdd your badge
Show developers this skill is listed on Skillselion. Paste this into your README.
| Installs | 2 |
|---|---|
| Last updated | May 30, 2026 |
| Repository | lovstudio/image-creator-skill ↗ |
What it does
Generates images via AI generation (Gemini/ZenMux), code-based HTML/React rendering to PNG with Playwright, or optimized prompt engineering.
Files
Image Creator — Multi-Mechanism Framework
Mechanism Selection
Choose the mechanism based on user intent:
| Mechanism | When to Use | Output |
|---|---|---|
| end-to-end | User wants AI-generated artwork, photos, illustrations | PNG image |
| code | User wants designed layouts (posters, cards, banners) with editable content | HTML file + PNG |
| prompt | User wants a prompt for external model (Midjourney, nano-banana-pro, etc.) | Text prompt |
If the user doesn't specify, infer from context:
- "生成一张猫的图片" → end-to-end
- "做一张活动海报" → code
- "帮我写一个 Midjourney prompt" → prompt
Mechanism 1: End-to-End (Gemini)
python3 ~/.claude/skills/lovstudio-image-creator/gen_image.py "PROMPT" [-o output.png] [-q low|medium|high] [--ascii]- Generates image directly via Gemini 3 Pro (through ZenMux)
- Requires
ZENMUX_API_KEYenvironment variable - First run auto-installs
google-genaiandPillowviapip --user(no manual setup) - Display result with
Readtool after generation
Mechanism 2: Code-Based Rendering
Step 1: Generate HTML
Write a single self-contained HTML file that includes all styles inline. Use:
- React 19 via CDN (
https://cdn.jsdelivr.net/npm/react@19/umd/react.production.min.js) - ReactDOM 19 via CDN
- Tailwind CSS via CDN (
https://cdn.tailwindcss.com) - Google Fonts via
<link>for CJK:Noto Sans SC,Noto Serif SC
Template structure:
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<script src="https://cdn.jsdelivr.net/npm/react@19/umd/react.production.min.js"></script>
<script src="https://cdn.jsdelivr.net/npm/react-dom@19/umd/react-dom.production.min.js"></script>
<script src="https://cdn.tailwindcss.com"></script>
<link href="https://fonts.googleapis.com/css2?family=Noto+Sans+SC:wght@300;400;500;700;900&family=Noto+Serif+SC:wght@400;700&display=swap" rel="stylesheet">
<script>
tailwind.config = {
theme: { extend: { /* custom theme */ } }
}
</script>
<style>
/* Reset & base styles */
* { margin: 0; padding: 0; box-sizing: border-box; }
body { width: {{WIDTH}}px; height: {{HEIGHT}}px; overflow: hidden; }
</style>
</head>
<body>
<div id="root"></div>
<script type="text/babel" data-type="module">
// React component here
function Poster() {
return (/* JSX */);
}
ReactDOM.createRoot(document.getElementById('root')).render(<Poster />);
</script>
<script src="https://cdn.jsdelivr.net/npm/@babel/standalone/babel.min.js"></script>
</body>
</html>IMPORTANT: Babel standalone script MUST come AFTER the text/babel script block.
Step 2: Render to PNG
python3 ~/.claude/skills/lovstudio-image-creator/scripts/render_to_png.py \
/path/to/poster.html \
-o output.png \
-W 1200 -H 630 \
--scale 2Common aspect ratios:
| Ratio | Dimensions | Use Case |
|---|---|---|
| 16:9 | 1200×675 | Social media banner |
| 4:3 | 1200×900 | Presentation |
| 1:1 | 1080×1080 | Instagram post |
| 9:16 | 1080×1920 | Story / mobile poster |
| 3:4 | 900×1200 | Portrait poster |
| A4 | 794×1123 | Print poster (210mm×297mm @96dpi) |
Step 3: Display & Iterate
- Use
Readto display the PNG - Open with
open output.pngon macOS - User can request edits → modify the HTML → re-render
Mechanism 3: Prompt Engineering
Generate optimized prompts for external models. Include:
- Positive prompt: subject, style, lighting, quality tags
- Negative prompt: common defects to avoid
Format output as copyable code block.
Reference Image Support
When user provides a reference image:
- End-to-end: describe the style/composition in the prompt
- Code: analyze the layout, colors, typography → replicate in HTML/CSS
- Prompt: extract style keywords for the external model
Aspect Ratio
Always ask or infer the desired aspect ratio. Map to pixel dimensions using the table above.
#!/usr/bin/env python3
import os
import sys
import argparse
import time
import io
import subprocess
import shutil
import importlib
import importlib.util
def _ensure_deps():
required = [("PIL", "Pillow"), ("google.genai", "google-genai")]
missing = [pkg for mod, pkg in required if importlib.util.find_spec(mod) is None]
if not missing:
return
print(f"Installing missing dependencies: {', '.join(missing)}...", file=sys.stderr)
cmd = [sys.executable, "-m", "pip", "install", "--user", "--quiet", *missing]
try:
subprocess.run(cmd, check=True)
except subprocess.CalledProcessError:
print("Retrying with --break-system-packages...", file=sys.stderr)
subprocess.run([*cmd, "--break-system-packages"], check=True)
importlib.invalidate_caches()
_ensure_deps()
from PIL import Image
from google import genai
from google.genai import types
def generate_image(prompt, output_file, quality='low', show_ascii=False):
# Get API key from environment
api_key = os.environ.get("ZENMUX_API_KEY")
if not api_key:
print("Error: ZENMUX_API_KEY environment variable is not set.")
sys.exit(1)
client = genai.Client(
api_key=api_key,
vertexai=True,
http_options=types.HttpOptions(
api_version='v1',
base_url='https://zenmux.ai/api/vertex-ai'
),
)
print(f"Generating image for prompt: {prompt[:50]}...")
try:
# Map quality string to MediaResolution enum
resolution_map = {
'low': types.MediaResolution.MEDIA_RESOLUTION_LOW,
'medium': types.MediaResolution.MEDIA_RESOLUTION_MEDIUM,
'high': types.MediaResolution.MEDIA_RESOLUTION_HIGH
}
# Default to low if not specified or invalid
resolution = resolution_map.get(quality, types.MediaResolution.MEDIA_RESOLUTION_LOW)
response = client.models.generate_content(
model="google/gemini-3-pro-image-preview",
contents=[prompt],
config=types.GenerateContentConfig(
response_modalities=["TEXT", "IMAGE"],
media_resolution=resolution
)
)
image_saved = False
if response.parts:
for part in response.parts:
if part.text is not None:
print(f"Model response: {part.text}")
if part.inline_data is not None:
# Get raw bytes
img_data = part.inline_data.data
# Save to file
with open(output_file, 'wb') as f:
f.write(img_data)
print(f"Image saved successfully to {output_file}")
# Open image with system viewer (macOS)
if sys.platform == 'darwin':
try:
subprocess.run(["open", output_file], check=False)
print(f"Opened image in default viewer.")
except Exception as e:
print(f"Warning: Failed to open image: {e}")
# Optional ASCII preview
if show_ascii:
try:
# Create PIL Image for ASCII preview
image = Image.open(io.BytesIO(img_data))
print("\n" + "="*40)
print("ASCII PREVIEW")
print("="*40)
print_ascii(image)
print("\n" + "="*40)
except Exception as e:
print(f"Warning: Could not generate ASCII preview: {e}")
image_saved = True
else:
print("Response contained no parts.")
if not image_saved:
print("No image was returned in the response.")
sys.exit(1)
except Exception as e:
print(f"An error occurred: {e}")
sys.exit(1)
def print_ascii(image, width=60):
"""Prints an ASCII representation of the image."""
try:
# Determine dimensions safely
if hasattr(image, 'size'):
img_width, img_height = image.size
elif hasattr(image, 'width') and hasattr(image, 'height'):
img_width = image.width
img_height = image.height
else:
# Try to force load if it's a lazy object or similar
if hasattr(image, 'load'):
image.load()
if hasattr(image, 'size'):
img_width, img_height = image.size
else:
print(f"Cannot determine dimensions for object: {type(image)}")
return
else:
print(f"Cannot determine dimensions for object: {type(image)}")
return
aspect_ratio = img_height / img_width
# Terminal characters are roughly twice as tall as they are wide
new_height = int(width * aspect_ratio * 0.5)
# Ensure minimum dimensions
if new_height < 1: new_height = 1
# Resize image
img = image.resize((width, new_height))
# Convert to grayscale
img = img.convert('L')
pixels = list(img.getdata())
# ASCII chars from dark to light
chars = ["@", "#", "S", "%", "?", "*", "+", ";", ":", ",", "."]
# Map pixels to characters
new_pixels = [chars[pixel * (len(chars)-1) // 255] for pixel in pixels]
new_pixels = ''.join(new_pixels)
# Split string of chars into multiple strings of length equal to new width and print
new_pixels_count = len(new_pixels)
ascii_image = [new_pixels[index:index + width] for index in range(0, new_pixels_count, width)]
print("\n".join(ascii_image))
except Exception as e:
print(f"Error creating ASCII art: {e}")
if __name__ == "__main__":
parser = argparse.ArgumentParser(description="Generate images using ZenMux/Gemini")
parser.add_argument("prompt", help="The image description prompt")
parser.add_argument("-o", "--output", default="generated_image.png", help="Output filename")
parser.add_argument("-q", "--quality", choices=['low', 'medium', 'high'], default="low", help="Image generation quality (default: low)")
parser.add_argument("--ascii", action="store_true", help="Show ASCII preview in terminal")
args = parser.parse_args()
generate_image(args.prompt, args.output, args.quality, args.ascii)
/Users/mark/projects/lovstudio-pro-skills/skills/lovstudio-image-creator#!/usr/bin/env python3
"""Render an HTML file to PNG using Playwright.
Usage:
python3 render_to_png.py input.html -o output.png [-w 1200] [-h 630] [--scale 2] [--wait 2000]
"""
import argparse
import sys
from playwright.sync_api import sync_playwright
def render(html_path: str, output: str, width: int, height: int, scale: float, wait_ms: int):
with sync_playwright() as p:
browser = p.chromium.launch()
page = browser.new_page(
viewport={"width": width, "height": height},
device_scale_factor=scale,
)
page.goto(f"file://{html_path}", wait_until="networkidle")
if wait_ms > 0:
page.wait_for_timeout(wait_ms)
page.screenshot(path=output, full_page=False)
browser.close()
print(f"Saved: {output} ({width}x{height} @{scale}x)")
if __name__ == "__main__":
parser = argparse.ArgumentParser(description="HTML → PNG renderer")
parser.add_argument("input", help="Path to HTML file")
parser.add_argument("-o", "--output", default="output.png", help="Output PNG path")
parser.add_argument("-W", "--width", type=int, default=1200, help="Viewport width (default: 1200)")
parser.add_argument("-H", "--height", type=int, default=630, help="Viewport height (default: 630)")
parser.add_argument("--scale", type=float, default=2, help="Device scale factor (default: 2)")
parser.add_argument("--wait", type=int, default=1000, help="Extra wait in ms after networkidle (default: 1000)")
args = parser.parse_args()
import os
abs_input = os.path.abspath(args.input)
if not os.path.exists(abs_input):
print(f"Error: {abs_input} not found")
sys.exit(1)
render(abs_input, args.output, args.width, args.height, args.scale, args.wait)