Here is the stack I use: ComfyUI (an open-source Stable Diffusion node editor) to generate the visual, Inkscape (an open-source vector editor) for real layers and layout, plus ImageMagick (an open-source image CLI) to check and batch sizes. You keep text and logos as editable SVG text. You only replace the one image or line you want.
Plan the file like a real campaign. Keep text and logo as vector layers in an SVG. Use a linked image layer for the product photo so you can swap it without touching the rest. Generate the background or product visual once in ComfyUI, then update the SVG link.
Install ComfyUI and open it. It runs locally and gives you a browser UI.
bash
git clone https://github.com/comfyanonymous/ComfyUI.git && cd ComfyUI && python3 main.py
Verify it is up: visit http://127.0.0.1:8188 or run curl -s localhost:8188 | head -n1. You should see HTML from ComfyUI. Next, install Inkscape and ImageMagick, then make a simple SVG template with layers named headline, product, bg, logo. Export different sizes with Inkscape’s CLI per channel. Only then worry about prompt tweaks in ComfyUI. Stop here if you only need the quick local loop.
Plan the file like a real campaign. Keep text and logo as vector layers in an SVG. Use a linked image layer for the product photo so you can swap it without touching the rest. Generate the background or product visual once in ComfyUI, then update the SVG link.
Install ComfyUI and open it. It runs locally and gives you a browser UI.
bash
git clone https://github.com/comfyanonymous/ComfyUI.git && cd ComfyUI && python3 main.py
Verify it is up: visit http://127.0.0.1:8188 or run curl -s localhost:8188 | head -n1. You should see HTML from ComfyUI. Next, install Inkscape and ImageMagick, then make a simple SVG template with layers named headline, product, bg, logo. Export different sizes with Inkscape’s CLI per channel. Only then worry about prompt tweaks in ComfyUI.
Setup
We will use three free tools:
- ComfyUI (an open-source Stable Diffusion node editor) for image generation
- Inkscape (an open-source vector editor) for layered layouts
- ImageMagick (an open-source image CLI) for quick checks and basic batch ops
- xmlstarlet (an XML CLI) to edit text and image links inside SVG by id
macOS
bash
# Python, Git
xcode-select --install || true
brew install python3 git
# ComfyUI
git clone https://github.com/comfyanonymous/ComfyUI.git
cd ComfyUI
python3 -m venv .venv && source .venv/bin/activate
pip install --upgrade pip
pip install -r requirements.txt || true
# Inkscape, ImageMagick, xmlstarlet
brew install --cask inkscape
brew install imagemagick xmlstarlet
Start ComfyUI:
bash
source .venv/bin/activate
python main.py
Ubuntu/Debian Linux
bash
sudo apt update
sudo apt install -y python3-venv python3-dev git inkscape imagemagick xmlstarlet curl
git clone https://github.com/comfyanonymous/ComfyUI.git
cd ComfyUI
python3 -m venv .venv && source .venv/bin/activate
pip install --upgrade pip
pip install -r requirements.txt || true
python main.py
Windows 10/11
- Install Python 3.10+ from python.org and check “Add Python to PATH”.
- Install Git for Windows.
- Install Inkscape from inkscape.org. Install ImageMagick and xmlstarlet via Chocolatey:
bash
choco install inkscape imagemagick xmlstarlet -y
Then open PowerShell:
bash
git clone https://github.com/comfyanonymous/ComfyUI.git
cd ComfyUI
python -m venv .venv; .\.venv\Scripts\Activate.ps1
pip install -r requirements.txt
python .\main.py
Models for ComfyUI
Place at least one checkpoint in ComfyUI/models/checkpoints. Example using Hugging Face CLI to pull SDXL Turbo (SDXL is a Stable Diffusion XL model):
bash
pip install huggingface_hub
huggingface-cli download stabilityai/sdxl-turbo sdxl-turbo.safetensors --local-dir models/checkpoints
Verify
- ComfyUI:
curl -s http://127.0.0.1:8188 | grep -i ComfyUI || echo UI upshould print HTML with ComfyUI in it. - Inkscape:
inkscape --versionshould showInkscape 1.2+. - ImageMagick:
magick -versionshould work. - xmlstarlet:
xmlstarlet --versionshould print a version line.
Create a layered SVG template
Make a minimal SVG with named ids for each layer. The product image is a linked <image> so you can swap the file path without touching layout.
bash
mkdir -p campaign/{assets,build}
cat > campaign/template.svg <<'SVG'
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<svg xmlns="http://www.w3.org/2000/svg" width="1080" height="1080" viewBox="0 0 1080 1080">
<g id="bg">
<rect x="0" y="0" width="1080" height="1080" fill="#0f172a"/>
</g>
<g id="logo">
<text id="logo_text" x="60" y="120" fill="#ffffff" font-size="64" font-family="Inter, Arial">ACME</text>
</g>
<g id="headline">
<text id="headline_text" x="60" y="260" fill="#ffffff" font-size="88" font-family="Inter, Arial" xml:space="preserve">Your summer upgrade</text>
</g>
<g id="product">
<image id="product_img" x="580" y="360" width="420" height="420" href="assets/product.png"/>
</g>
<g id="cta">
<rect x="60" y="940" width="320" height="80" rx="12" fill="#22c55e"/>
<text id="cta_text" x="80" y="995" fill="#0b141d" font-size="42" font-family="Inter, Arial">Shop now</text>
</g>
</svg>
SVG
Swap the product image path with xmlstarlet:
bash
xmlstarlet ed \
-N svg="http://www.w3.org/2000/svg" \
-u "/svg:svg/svg:g[@id='product']/svg:image[@id='product_img']/@href" \
-v "assets/shoes-green.png" \
campaign/template.svg > campaign/work.svg
Change the headline text:
bash
xmlstarlet ed \
-N svg="http://www.w3.org/2000/svg" \
-u "/svg:svg/svg:g[@id='headline']/svg:text[@id='headline_text']" \
-v "Light, strong, ready" \
campaign/work.svg > campaign/work2.svg
Export sizes for channels
Export to common sizes. Inkscape scales the page. Keep the same layout and typography. Adjust per size later if needed.
bash
inkscape campaign/work2.svg --export-type=png --export-filename=campaign/build/instagram.png -w 1080 -h 1080
inkscape campaign/work2.svg --export-type=png --export-filename=campaign/build/story.png -w 1080 -h 1920
inkscape campaign/work2.svg --export-type=png --export-filename=campaign/build/linkedin.png -w 1200 -h 627
inkscape campaign/work2.svg --export-type=png --export-filename=campaign/build/ad_square.png -w 1200 -h 1200
Quick check with ImageMagick:
bash
magick identify -format "%f %wx%h\n" campaign/build/*.png
Generate the product visual with ComfyUI
Use ComfyUI to create campaign/assets/shoes-green.png. Open http://127.0.0.1:8188, load a basic SDXL Turbo text-to-image workflow, prompt your product scene, and set output to campaign/assets/. When you want to change only the product, re-generate that file. The SVG keeps your headline, logo, and CTA untouched.
Optional REST call example after you save a workflow as workflow_api.json from ComfyUI:
bash
curl -X POST http://127.0.0.1:8188/prompt \
-H 'Content-Type: application/json' \
-d @workflow_api.json
Batch a whole week
Prepare a CSV with headline and product image per post.
bash
cat > campaign/batch.csv <<'CSV'
headline,product
Sunrise drop,assets/shoes-sun.png
Midweek restock,assets/shoes-blue.png
Final hours,assets/shoes-red.png
CSV
Run a tiny bash loop that edits the SVG and exports four sizes per row.
bash
while IFS=, read -r headline product; do
[ "$headline" = "headline" ] && continue
xmlstarlet ed -N svg="http://www.w3.org/2000/svg" \
-u "/svg:svg/svg:g[@id='product']/svg:image[@id='product_img']/@href" -v "$product" \
-u "/svg:svg/svg:g[@id='headline']/svg:text[@id='headline_text']" -v "$headline" \
campaign/template.svg > campaign/tmp.svg
base=$(echo "$headline" | tr ' ' '_' | tr -dc 'A-Za-z0-9_')
inkscape campaign/tmp.svg --export-type=png --export-filename=campaign/build/${base}_ig.png -w 1080 -h 1080
inkscape campaign/tmp.svg --export-type=png --export-filename=campaign/build/${base}_story.png -w 1080 -h 1920
inkscape campaign/tmp.svg --export-type=png --export-filename=campaign/build/${base}_li.png -w 1200 -h 627
inkscape campaign/tmp.svg --export-type=png --export-filename=campaign/build/${base}_ad.png -w 1200 -h 1200
echo "Built $headline"
done < campaign/batch.csv
Configuration tips
- Fonts: install your brand fonts on the OS so Inkscape can use them. Keep font-family in the SVG to exact font names. If you package the project, also export a PDF with text as text, not paths.
- Color: keep a swatches layer or use
fillandstrokestyles by id. Update palette by running one xmlstarlet command across ids. - Safe areas: draw invisible guides as rects for each channel. Keep text inside them. Toggle visibility per export if needed.
- Multi-page: if you prefer real artboards, make pages in Inkscape 1.2+. Then export with
--export-page="Instagram",--export-page="Story"per label. - Non-destructive product swaps: keep product as a linked image. Never embed. This keeps the SVG small and fast to edit with xmlstarlet.
Troubleshooting
- Inkscape “Unknown option --export-page”: You are on an older version. Update to Inkscape 1.2 or later. Use
--export-type=png -w -has a fallback. - xmlstarlet edits do nothing: Your SVG has namespaces. Include
-N svg="http://www.w3.org/2000/svg"and target ids with full XPath like shown above. - ComfyUI starts but no images generate: You did not place a checkpoint in
models/checkpoints. Download SDXL Turbo or another model and place the.safetensorsfile there. Restart ComfyUI.
When it beats Canva
- You only want to swap one layer without re-rendering the whole design. The SVG keeps text and logo pristine and you just refresh the product layer.
- You need full control of fonts, exports, and file naming. The CLI gives you exact sizes and repeatable filenames for asset delivery.
- You want a consistent campaign across Instagram, Stories, LinkedIn, and ads. One template, four export commands.
- You work offline or with sensitive assets. Everything runs locally.
- You need batch output. The CSV loop builds a week of creatives in one pass.
Sources
Want a hand?
Book a 30-min call.
Walk through your stack with us. We'll find the bottleneck and map out the exact wiring you need — free.