
Poster Skill
- 1 installs
- 90 repo stars
- Updated July 11, 2026
- codingfeng101/canvasanvil
Generate a final poster image from structured requirements, exporting to a local image file via configurable model vendors like OpenAI, Aliyun, or Zhipu.
About
A skill that generates a single final poster image from structured requirements and exports it directly to a local image file. A developer uses it for single-poster tasks such as campaign, event, or launch visuals, choosing an image model vendor in config.
- Targets single-frame promotional graphics exported to local files
- Configurable image vendors: openai, aliyun, tencent, bytedance, zhipu
Poster Skill by the numbers
- 1 all-time installs (skills.sh)
- Ranked #1,200 of 1,335 Generative Media skills by installs in the Skillselion catalog
- Data as of Jul 27, 2026 (Skillselion catalog sync)
npx skills add https://github.com/codingfeng101/canvasanvil --skill poster-skillAdd your badge
Show developers this skill is listed on Skillselion. Paste this into your README.
| Installs | 1 |
|---|---|
| repo stars | ★ 90 |
| Last updated | July 11, 2026 |
| Repository | codingfeng101/canvasanvil ↗ |
What it does
Generate a final poster image from structured requirements, exporting to a local image file via configurable model vendors like OpenAI, Aliyun, or Zhipu.
Files
Poster Skill
Trigger
Use when the task requires one final poster image.
First-Use Reminder
On the first use of this skill in a conversation, remind the user that image generation is configurable and tell them which model vendors are supported in config/image-provider.json.
Supported vendors:
openaialiyuntencentbytedancezhipugooglexaibfladobe
Order
1. Confirm the task is a poster task. 2. Collect required fields. 3. Ask follow-up questions for missing required fields. 4. Read the required references. 5. Produce the final poster prompt. 6. Generate the image through the configured provider. 7. Export or bundle artifacts when requested.
Required Fields
themesizeoraspect_ratiostyle_directioncolor_directionprimary_copyreference_image_status
Do not guess missing required fields.
Required References
references/poster-fields.mdreferences/prompt-rules.mdreferences/image-provider-config.mdreferences/output-contract.md
Prompt Contract
- Produce one final poster prompt.
- Keep the output specific to one frame.
- Include composition, focal area, typography intent, and color direction.
- Keep provided copy explicit.
- Do not invent unsupported claims, pricing, or brand facts.
- Treat reference images as visual guidance only.
- Do not treat reference images as factual sources for copy, branding, pricing, dates, or claims.
Provider Contract
- Use the configured image provider for image generation.
- Require
config/image-provider.jsonbefore image generation. - Do not ask the user to paste API keys into chat.
- Stop and tell the user to fill
config/image-provider.jsonwhen provider, apiKey, or model is missing. - Pass a reference image only when
reference_image_statusis positive and a usable reference image URL is available.
Scripts
scripts/generate-poster-image.mjsscripts/build-poster-bundle.mjs
Deliverables
Preferred:
png- metadata JSON
Optional on request:
jpg- prompt text
interface:
display_name: "Poster Skill"
short_description: "Generate single-poster images with provider-based image routing and local file export."
default_prompt: "Use $poster-skill to generate a final poster image and save it locally."
policy:
allow_implicit_invocation: true
{
"provider": "",
"apiKey": "",
"baseUrl": "",
"model": ""
}
{
"name": "poster-skill",
"private": true,
"version": "1.0.0",
"type": "module",
"scripts": {
"generate:image": "node scripts/generate-poster-image.mjs",
"build:bundle": "node scripts/build-poster-bundle.mjs"
}
}
Poster Generation Prompt
Build one final image-generation prompt for a single poster.
Requirements
- use the collected poster fields
- keep the image single-frame
- keep the composition explicit
- keep the text content explicit
- align style and color direction with the user request
- avoid adding unsupported brand claims or extra marketing facts
Missing Data Rule
If any required field is missing, ask follow-up questions before producing the final prompt.
Image Provider Config
Image generation uses config/image-provider.json.
Required keys
providerapiKeymodel
Optional keys
baseUrl
File Path
config/image-provider.json
Example
{
"provider": "openai",
"apiKey": "YOUR_KEY",
"baseUrl": "https://api.openai.com/v1",
"model": "gpt-image-1"
}Supported providers
openaialiyuntencentbytedancezhipugooglexaibfladobe
Rule
Do not ask the user to paste API keys into chat.
If image generation is requested and config/image-provider.json is missing or incomplete, stop and tell the user to fill that file.
If a reference image is provided but the selected provider or model does not support reference images, ignore the reference image and continue only if the task still makes sense without it. Do not claim that the model used the reference image when it did not.
Output Contract
Default output:
pngmetadata.json
Optional outputs:
jpgprompt.txt
Metadata
Metadata should include:
- theme
- size or aspect ratio
- style
- color direction
- provider
- model
- output file name
- generation timestamp
Poster Fields
Collect these fields before generation.
Required
themesizeoraspect_ratiostylecolor_directionprimary_copyreference_image_status
Optional
subtitlebody_copyctacompositionfocal_areawhitespacereference_image_urlbrand_nameexport_format
Rule
If any required field is missing, ask follow-up questions before generation.
If reference_image_status is positive, request reference_image_url before generation.
Prompt Rules
Use these rules when building the final poster generation prompt.
- Keep the output focused on one poster image.
- State the intended size or aspect ratio.
- State the visual style clearly.
- State the color direction clearly.
- State the composition and focal area clearly.
- Include the required textual content explicitly.
- Use concise, concrete visual language.
- Avoid generic filler such as "beautiful", "stunning", or "high quality" unless they add specific meaning.
- Do not invent extra brand claims, dates, or pricing.
- If the user provides a reference image, treat it as style, composition, lighting, or mood guidance.
- Do not treat a reference image as a source of facts.
- Do not infer pricing, dates, brand promises, or campaign details from a reference image.
- If the reference image conflicts with explicit user instructions, follow the explicit user instructions.
#!/usr/bin/env node
import fs from "node:fs";
import path from "node:path";
import { spawnSync } from "node:child_process";
import { fileURLToPath } from "node:url";
function parseArgs(argv) {
const args = {
promptFile: "",
outDir: "",
name: "poster",
configFile: "",
format: "png",
referenceImageUrl: "",
};
for (let i = 0; i < argv.length; i += 1) {
const token = argv[i];
if (token === "--prompt-file") {
args.promptFile = argv[i + 1] || "";
i += 1;
} else if (token === "--out-dir" || token === "-o") {
args.outDir = argv[i + 1] || "";
i += 1;
} else if (token === "--name") {
args.name = argv[i + 1] || args.name;
i += 1;
} else if (token === "--config-file") {
args.configFile = argv[i + 1] || "";
i += 1;
} else if (token === "--format") {
args.format = argv[i + 1] || args.format;
i += 1;
} else if (token === "--reference-image-url") {
args.referenceImageUrl = argv[i + 1] || "";
i += 1;
}
}
return args;
}
function fail(message) {
console.error(message);
process.exit(1);
}
function main() {
const args = parseArgs(process.argv.slice(2));
if (!args.promptFile) fail("Missing --prompt-file <path-to-prompt>.");
if (!args.outDir) fail("Missing --out-dir <bundle-directory>.");
const promptFile = path.resolve(process.cwd(), args.promptFile);
const outDir = path.resolve(process.cwd(), args.outDir);
const scriptDir = path.dirname(fileURLToPath(import.meta.url));
const generateScript = path.join(scriptDir, "generate-poster-image.mjs");
const imagePath = path.join(outDir, `${args.name}.${args.format}`);
const metadataPath = path.join(outDir, `${args.name}.metadata.json`);
const promptCopyPath = path.join(outDir, `${args.name}.prompt.txt`);
fs.mkdirSync(outDir, { recursive: true });
fs.copyFileSync(promptFile, promptCopyPath);
const command = [
generateScript,
"--prompt-file",
promptFile,
"--output",
imagePath,
"--format",
args.format,
];
if (args.configFile) {
command.push("--config-file", path.resolve(process.cwd(), args.configFile));
}
if (args.referenceImageUrl) {
command.push("--reference-image-url", args.referenceImageUrl);
}
const result = spawnSync(process.execPath, command, { stdio: "inherit" });
if (result.status !== 0) fail("Poster image generation failed.");
const metadata = {
name: args.name,
imageFile: path.basename(imagePath),
promptFile: path.basename(promptCopyPath),
generatedAt: new Date().toISOString(),
format: args.format,
};
fs.writeFileSync(metadataPath, JSON.stringify(metadata, null, 2), "utf8");
console.log(`Poster bundle written to ${outDir}`);
}
main();
#!/usr/bin/env node
import fs from "node:fs";
import path from "node:path";
import { generateImageThroughGateway } from "./lib/gateway.mjs";
import { getDefaultBaseUrl } from "./lib/provider-registry.mjs";
function parseArgs(argv) {
const args = {
prompt: "",
promptFile: "",
output: "",
format: "png",
configFile: "",
referenceImageUrl: "",
};
for (let i = 0; i < argv.length; i += 1) {
const token = argv[i];
if (token === "--prompt") {
args.prompt = argv[i + 1] || "";
i += 1;
} else if (token === "--prompt-file") {
args.promptFile = argv[i + 1] || "";
i += 1;
} else if (token === "--output" || token === "-o") {
args.output = argv[i + 1] || "";
i += 1;
} else if (token === "--format" || token === "-f") {
args.format = argv[i + 1] || args.format;
i += 1;
} else if (token === "--config-file") {
args.configFile = argv[i + 1] || "";
i += 1;
} else if (token === "--reference-image-url") {
args.referenceImageUrl = argv[i + 1] || "";
i += 1;
}
}
return args;
}
function fail(message) {
console.error(message);
process.exit(1);
}
function loadConfig(configFile) {
const target = configFile
? path.resolve(process.cwd(), configFile)
: path.resolve(process.cwd(), "config", "image-provider.json");
if (!fs.existsSync(target)) {
fail(`Missing provider config file: ${target}`);
}
return JSON.parse(fs.readFileSync(target, "utf8"));
}
function decodeDataUrl(dataUrl) {
const match = String(dataUrl || "").match(/^data:([^;]+);base64,(.*)$/);
if (!match) fail("Image generation did not return a base64 data URL.");
return {
mime: match[1],
buffer: Buffer.from(match[2], "base64"),
};
}
async function main() {
const args = parseArgs(process.argv.slice(2));
if (!args.output) fail("Missing --output <path-to-image>.");
const prompt = args.promptFile
? fs.readFileSync(path.resolve(process.cwd(), args.promptFile), "utf8")
: args.prompt;
if (!String(prompt || "").trim()) fail("Missing prompt. Use --prompt or --prompt-file.");
const rawConfig = loadConfig(args.configFile);
const provider = String(rawConfig.provider || "").trim().toLowerCase();
const apiKey = String(rawConfig.apiKey || "").trim();
const model = String(rawConfig.model || "").trim();
const baseUrl = String(rawConfig.baseUrl || getDefaultBaseUrl(provider) || "").trim();
if (!provider || !apiKey || !model) {
fail("Missing image provider configuration. Required: provider, apiKey, model.");
}
const dataUrl = await generateImageThroughGateway({
channel: {
provider,
apiKey,
baseUrl,
model,
},
prompt,
referenceImageUrl: args.referenceImageUrl || undefined,
});
const { buffer } = decodeDataUrl(dataUrl);
const outputPath = path.resolve(process.cwd(), args.output);
fs.mkdirSync(path.dirname(outputPath), { recursive: true });
fs.writeFileSync(outputPath, buffer);
console.log(`Poster image written to ${outputPath}`);
}
main().catch((error) => {
console.error(error instanceof Error ? error.message : String(error));
process.exit(1);
});
import { resolveImageRoute } from "./provider-registry.mjs";
function joinUrl(baseUrl, endpoint) {
const base = String(baseUrl || "").trim().replace(/\/+$/, "");
const path = String(endpoint || "").trim();
if (!path) return base;
if (/^https?:\/\//i.test(path)) return path;
if (!base) return path;
return `${base}/${path.replace(/^\/+/, "")}`;
}
async function convertRemoteImageToDataUrl(url) {
if (url.startsWith("data:image")) return url;
const response = await fetch(url);
if (!response.ok) throw new Error(`Failed to fetch remote image with status ${response.status}`);
const contentType = response.headers.get("content-type") || "image/png";
const buffer = Buffer.from(await response.arrayBuffer());
return `data:${contentType};base64,${buffer.toString("base64")}`;
}
function extractImageUrlFromOpenAIContent(content) {
if (Array.isArray(content)) {
const imagePart = content.find((part) => part?.type === "image_url" && part?.image_url?.url);
if (imagePart?.image_url?.url) return String(imagePart.image_url.url);
const textPart = content.find((part) => part?.type === "text" && typeof part?.text === "string");
if (textPart?.text) {
const markdownMatch = textPart.text.match(/!\[.*?\]\((.*?)\)/);
if (markdownMatch?.[1]) return markdownMatch[1];
if (/^https?:\/\//i.test(textPart.text.trim()) || textPart.text.trim().startsWith("data:image")) {
return textPart.text.trim();
}
}
return null;
}
if (typeof content === "string") {
const markdownMatch = content.match(/!\[.*?\]\((.*?)\)/);
if (markdownMatch?.[1]) return markdownMatch[1];
if (/^https?:\/\//i.test(content.trim()) || content.trim().startsWith("data:image")) return content.trim();
}
return null;
}
async function requestOpenAIImages(req) {
const response = await fetch(joinUrl(req.channel.baseUrl, "/images/generations"), {
method: "POST",
headers: {
Authorization: `Bearer ${req.channel.apiKey}`,
"Content-Type": "application/json",
},
body: JSON.stringify({
model: req.channel.model,
prompt: req.prompt,
}),
});
const text = await response.text();
let parsed = null;
try {
parsed = text ? JSON.parse(text) : null;
} catch {
parsed = null;
}
if (!response.ok) {
throw new Error(parsed?.error?.message || parsed?.error || text || `Request failed with status ${response.status}`);
}
const first = parsed?.data?.[0];
if (first?.b64_json) return `data:image/png;base64,${first.b64_json}`;
if (first?.url) return await convertRemoteImageToDataUrl(String(first.url));
throw new Error("Image request succeeded but returned no image data.");
}
async function requestOpenAIChatImage(req) {
const content = [{ type: "text", text: req.prompt }];
if (req.referenceImageUrl) {
content.push({ type: "image_url", image_url: { url: req.referenceImageUrl } });
}
const response = await fetch(joinUrl(req.channel.baseUrl, "/chat/completions"), {
method: "POST",
headers: {
Authorization: `Bearer ${req.channel.apiKey}`,
"Content-Type": "application/json",
},
body: JSON.stringify({
model: req.channel.model,
messages: [{ role: "user", content: content.length > 1 ? content : req.prompt }],
stream: false,
}),
});
const text = await response.text();
let parsed = null;
try {
parsed = text ? JSON.parse(text) : null;
} catch {
parsed = null;
}
if (!response.ok) {
throw new Error(parsed?.error?.message || parsed?.error || text || `Request failed with status ${response.status}`);
}
const url = extractImageUrlFromOpenAIContent(parsed?.choices?.[0]?.message?.content);
if (!url) throw new Error("Image request succeeded but returned no image URL.");
return await convertRemoteImageToDataUrl(url);
}
export async function generateImageThroughGateway(req) {
const route = resolveImageRoute(req.channel);
if (route.protocol === "openai-images") return await requestOpenAIImages(req);
return await requestOpenAIChatImage(req);
}
function matchesRule(provider, model, rules) {
const normalizedProvider = String(provider || "").toLowerCase();
const normalizedModel = String(model || "");
return rules.some(
(rule) =>
rule.provider === normalizedProvider &&
rule.patterns.some((pattern) => pattern.test(normalizedModel)),
);
}
const IMAGE_OPENAI_IMAGES_RULES = [
{ provider: "openai", patterns: [/gpt-image/i, /dall-e/i] },
];
const IMAGE_REFERENCE_RULES = [
{ provider: "openai", patterns: [/gpt-image/i] },
{ provider: "aliyun", patterns: [/wan/i, /qwen-image/i] },
{ provider: "bytedance", patterns: [/seedream/i] },
{ provider: "zhipu", patterns: [/glm-image/i, /cogview/i] },
{ provider: "tencent", patterns: [/hunyuan/i] },
];
const IMAGE_PROVIDER_OPTIONS = [
{ id: "openai", defaultBaseUrl: "https://api.openai.com/v1" },
{ id: "aliyun", defaultBaseUrl: "" },
{ id: "tencent", defaultBaseUrl: "" },
{ id: "bytedance", defaultBaseUrl: "" },
{ id: "zhipu", defaultBaseUrl: "" },
{ id: "google", defaultBaseUrl: "" },
{ id: "xai", defaultBaseUrl: "" },
{ id: "bfl", defaultBaseUrl: "" },
{ id: "adobe", defaultBaseUrl: "" },
];
export function getDefaultBaseUrl(provider) {
return IMAGE_PROVIDER_OPTIONS.find((item) => item.id === provider)?.defaultBaseUrl || "";
}
export function resolveImageRoute(channel) {
const provider = String(channel.provider || "").toLowerCase();
if (!IMAGE_PROVIDER_OPTIONS.some((item) => item.id === provider)) {
throw new Error(`Unsupported image provider: ${provider}`);
}
return {
protocol: matchesRule(provider, channel.model, IMAGE_OPENAI_IMAGES_RULES)
? "openai-images"
: "openai-chat-image",
supportsReferenceImages: matchesRule(provider, channel.model, IMAGE_REFERENCE_RULES),
};
}