
Lovstudio Image Creator
- 83 installs
- 64 repo stars
- Updated July 31, 2026
- lovstudio/skills
Helps with ai & agent building tasks.
About
lovstudio-image-creator is a Claude Code skill for ai & agent building. It helps solo builders move faster with AI-assisted coding.
- lovstudio-image-creator
- AI & Agent Building
- AI-coding skill
Lovstudio Image Creator by the numbers
- 83 all-time installs (skills.sh)
- +4 installs in the week ending Aug 2, 2026 (Skillselion tracking)
- Ranked #5,144 of 16,546 AI & Agent Building skills by installs in the Skillselion catalog
- Data as of Aug 2, 2026 (Skillselion catalog sync)
npx skills add https://github.com/lovstudio/skills --skill lovstudio-image-creatorAdd your badge
Show developers this skill is listed on Skillselion. Paste this into your README.
| Installs | 83 |
|---|---|
| repo stars | ★ 64 |
| Last updated | July 31, 2026 |
| Repository | lovstudio/skills ↗ |
What it does
Helps with ai & agent building tasks.
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 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 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.
Changelog
All notable changes to this skill are documented here. Format: Keep a Changelog · Versioning: SemVer
[0.2.1] - 2026-05-07
Fixed
- standardize Agent Skills metadata and README
- replace fixed install-path script examples with relative commands
#!/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)
lovstudio-image-creator
Generate images through the right mechanism: end-to-end AI generation, code-rendered layouts, or optimized prompts for external image models.
Part of lovstudio general skills — by lovstudio.ai
Install
npx lovstudio skills add image-creator -g -yUsage
End-to-end generation through ZenMux/Gemini:
python3 gen_image.py "a warm academic poster for an AI workshop" -o poster.png -q mediumRender an HTML layout to PNG:
python3 scripts/render_to_png.py poster.html -o poster.png -W 1200 -H 630 --scale 2Use the prompt mechanism when the user wants a Midjourney, nano-banana-pro, or other external image-model prompt instead of a local file.
Configuration
Set ZENMUX_API_KEY for end-to-end image generation. gen_image.py installs google-genai and Pillow into the user Python environment if they are missing.
Code rendering requires Playwright Python:
python3 -m pip install playwright
python3 -m playwright install chromiumOutput
Write generated files to the current project or a user-provided output path, then report the absolute path.
License
MIT
#!/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)