Image Composition
The Image Composition engine renders multi-layer images from JSON templates. Stack backgrounds, logos, styled text, shapes, and SVG overlays onto a canvas — with Nunjucks variables for dynamic content. Outputs PNG, JPEG, WebP, or SVG.
How It Works
- Create a template with
source_type = "image_compose" - Define layers in the
layerscolumn (JSONB) or legacycontentfield (JSON string) - Define form fields in the
form_fieldscolumn (or legacyseed_json._form) - Set default values in
seed_json(canvas config inseed_json._canvas) - Render via API — pass variables to override defaults
The engine uses Sharp for raster compositing (PNG/JPEG/WebP) and generates native SVG for vector output. Text is rendered with opentype.js (no system fonts needed).
Storage Model
| Column | Type | Purpose |
|---|---|---|
layers |
JSONB | Structured layer array — each layer has id, name, z_index, and type-specific properties. Per-field Nunjucks substitution (no JSON parse fragility) |
form_fields |
JSONB | Form field definitions for the render form UI. Replaces the seed_json._form convention |
seed_json._canvas |
JSON | Canvas dimensions and background color |
content |
JSONB | Legacy: full JSON spec (layers + canvas). Now stored as JSONB. Still supported as fallback |
seed_json._form |
JSON | Legacy: form fields embedded in seed_json. Still supported as fallback |
Layer Schema
{
"canvas": {
"width": 1200,
"height": 630,
"background": "#ffffff"
},
"layers": [
{ "type": "image", "src": "asset://background", ... },
{ "type": "text", "content": "{{ title }}", ... },
{ "type": "rect", ... },
{ "type": "circle", ... },
{ "type": "svg", "content": "<svg>...</svg>", ... }
]
}
Layers render in order (first = bottom, last = top). Use
{{ nunjucks_variables }} anywhere for dynamic content.
Layer Types
Image Layer
{
"type": "image",
"src": "asset://logo",
"x": 50,
"y": 50,
"width": 120,
"height": 120,
"fit": "cover",
"opacity": 1.0
}
| Field | Type | Description |
|---|---|---|
src |
string | URL, data URI, or asset://key reference |
x, y |
number | Position from top-left |
width, height |
number | Resize dimensions |
fit |
string | cover, contain, fill, inside |
opacity |
number | 0.0 to 1.0 |
Text Layer
{
"type": "text",
"content": "{{ title | default('Hello World') }}",
"x": 80,
"y": 200,
"width": 800,
"font_size": 48,
"font_weight": "bold",
"color": "#1e293b",
"align": "left",
"line_height": 1.3,
"max_lines": 3
}
| Field | Type | Description |
|---|---|---|
content |
string | Text to render (supports Nunjucks) |
font_size |
number | Font size in pixels (default: 24) |
font_weight |
string | normal, bold, 100-900 |
font_family |
string | Font name — see Available Fonts (default: Inter) |
color |
string | CSS color (hex, rgb, named) |
align |
string | left, center, right |
line_height |
number | Line spacing multiplier (default: 1.3) |
max_lines |
number | Truncate with ellipsis after N lines |
background |
string | Background color behind text |
padding |
number | Padding when background is set |
Text is rendered using opentype.js — each glyph is converted to SVG path outlines. This works on any server, even containers with no system fonts installed.
Rectangle Layer
{
"type": "rect",
"x": 0,
"y": 550,
"width": 1200,
"height": 80,
"fill": "{{ accent_color | default('#3b82f6') }}",
"radius": 8,
"opacity": 0.9
}
Circle Layer
{
"type": "circle",
"x": 100,
"y": 100,
"r": 50,
"fill": "#ef4444",
"stroke": "#ffffff",
"stroke_width": 3
}
SVG Layer
{
"type": "svg",
"content": "<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 100 100'><circle cx='50' cy='50' r='40' fill='#3b82f6'/></svg>",
"x": 0,
"y": 0,
"width": 200,
"height": 200
}
Available Fonts
Text layers support the font_family property with these built-in fonts from
Google Fonts CDN (fetched on first use, cached in memory):
| font_family | Weights | Style |
|---|---|---|
Inter |
normal, bold | Modern sans-serif (default) |
Roboto |
normal, bold | Android/Material sans-serif |
Open Sans |
normal, bold | Friendly sans-serif |
Lato |
normal, bold | Warm sans-serif |
Montserrat |
normal, bold | Geometric sans-serif |
Poppins |
normal, bold | Rounded sans-serif |
Playfair Display |
normal, bold | Elegant serif |
Font names are case-insensitive and accept spaces or hyphens:
"Playfair Display", "playfair-display", and "PlayfairDisplay" all resolve
to the same font.
{
"type": "text",
"content": "{{ heading }}",
"font_family": "Montserrat",
"font_weight": "bold",
"font_size": 48,
"color": "#1e293b"
}
Output Formats
| Format | Engine | Notes |
|---|---|---|
png |
Sharp | Lossless raster, default |
jpg / jpeg |
Sharp | Lossy raster, quality configurable (default 90) |
webp |
Sharp | Modern raster, quality configurable (default 85) |
svg |
Native | Pure SVG document — no Sharp or rasterization needed. Uses <text> elements with font-family, <rect>, <circle>, <image>, and <g> for SVG layers. Ideal for web embedding, further editing, and PDF conversion |
Assets
Background images, logos, and overlays can be stored in S3 via the video_asset
table and referenced with asset://key:
{ "type": "image", "src": "asset://company_logo" }
Upload assets via POST /v1/video/assets or the Video Assets page.
API Endpoints
POST /v1/render/image — Render by template ID or key
curl -X POST https://api-services.docuget.cloud/v1/render/image \
-H "Authorization: Bearer $TOKEN" \
-H "Content-Type: application/json" \
-d '{
"company_id": "7ebde680-d069-4da6-971d-d626553a5694",
"template_key": "employee_badge",
"data": {
"full_name": "Maria Garcia",
"initials": "MG",
"job_title": "CTO",
"department": "Engineering",
"employee_id": "EMP-00456",
"company_name": "Docuget",
"brand_color": "#1e40af",
"badge_year": "2026"
},
"output_format": "png"
}'
Returns binary PNG with Content-Type: image/png.
GET /v1/render/image/:template_key — URL-shareable render
All template variables are passed as query parameters. Ideal for <img> tags:
GET /v1/render/image/employee_badge
?company_id=7ebde680-d069-4da6-971d-d626553a5694
&full_name=Maria%20Garcia
&initials=MG
&job_title=CTO
&department=Engineering
&employee_id=EMP-00456
&company_name=Docuget
&brand_color=%231e40af
&badge_year=2026
&format=png
Note: URL-encode special characters: # → %23, spaces → %20.
Control parameters (not passed as template vars):
| Param | Description |
|---|---|
company_id |
Required. Company context |
format |
Output format: png, jpg, webp, svg (default: png) |
width |
Override canvas width |
height |
Override canvas height |
quality |
JPEG/WebP quality 1-100 |
GET /v1/render/image/:template_key/sign — Signed URL
Generates a time-limited, HMAC-signed URL that works without authentication. Use for sharing via chat, embedding in emails, or bot replies.
curl -H "Authorization: Bearer $TOKEN" \
"https://api-services.docuget.cloud/v1/render/image/employee_badge/sign\
?company_id=7ebde680-d069-4da6-971d-d626553a5694\
&full_name=Maria%20Garcia\
&initials=MG\
&job_title=CTO\
&format=png\
&ttl=86400"
Response:
{
"url": "https://api-services.docuget.cloud/v1/render/image/employee_badge?company_id=...&exp=1711411200&full_name=Maria%20Garcia&initials=MG&job_title=CTO&format=png&sig=abc123...",
"expires_at": "2026-03-25T12:00:00.000Z",
"ttl": 86400
}
The sig parameter is an HMAC-SHA256 signature of the template key + all query
params. The exp parameter is the Unix timestamp when the URL expires.
Env vars:
RENDER_SIGN_SECRET(orJWT_SECRETfallback) — HMAC signing keyAPI_SERVICES_URL— base URL in generated links
GET /v1/render/ref/:code — Render by shortref
If the template has a shortref @k7f2n:
GET /v1/render/ref/k7f2n?out=png&company_id=...
Telegram & WhatsApp
Telegram /image command
/image employee_badge full_name=Maria initials=MG job_title=CTO
The bot renders the image and replies with the photo + a signed share URL.
WhatsApp /image keyword
Send /image employee_badge full_name=Maria in a WhatsApp conversation. The bot
renders the image, generates a signed URL, and replies with the image via Twilio
MediaUrl.
Visual Editor
The image composition engine includes an in-browser SVG editor for designing and previewing templates without server round-trips.
Access: Open any template with source_type = "image_compose" and click
Edit. The editor renders all layers as live SVG.
Layout
Three-panel layout:
- Left panel — Layer list with drag-to-reorder, visibility toggle, duplicate, delete
- Center — Live SVG canvas with interactive manipulation
- Right panel — Property editor for the selected layer (position, size, colors, fonts, shadows, etc.)
Canvas Interaction
| Action | Behavior |
|---|---|
| Click layer | Select it (blue dashed outline + 8 resize handles) |
| Drag layer | Move (snaps to grid when grid is visible; hold Alt to bypass) |
| Drag handle | Resize from any edge or corner |
| Double-click text | Inline text editing (Shift+Enter for newline, Enter to commit, Esc to cancel) |
| Click background | Deselect all |
Keyboard Shortcuts
| Key | Action |
|---|---|
| Arrow keys | Move selected layer 1px |
| Shift + Arrow | Move by grid step (default 50px) |
| Delete / Backspace | Remove selected layer |
Grid & Rulers
Toggle the grid overlay (button in the toolbar) to show:
- Grid lines at 50px intervals with center cross (blue dashed)
- Rulers along top and left edges with tick marks and pixel labels every 100px
- Crosshair indicator — red lines on rulers follow the mouse cursor
- Coordinate readout — current mouse position displayed at bottom-left
- Alignment guides — red dashed lines appear when a dragged layer aligns with canvas center or edges (5px threshold)
Grid snap is automatic when the grid is visible. Hold Alt while dragging to bypass snap.
SVG Export
The editor strips all dg-editor-only elements (grid, rulers, handles, guides)
from the exported SVG. The export is a clean, production-ready SVG file.
Example Templates
Social Media Card (1200x630)
Dark background with title, subtitle, and accent bar:
{
"canvas": {
"width": 1200,
"height": 630,
"background": "{{ background_color | default('#0f172a') }}"
},
"layers": [
{
"type": "rect",
"x": 0,
"y": 530,
"width": 1200,
"height": 100,
"fill": "{{ accent_color | default('#3b82f6') }}",
"opacity": 0.95
},
{
"type": "text",
"content": "{{ title }}",
"x": 80,
"y": 180,
"width": 1040,
"font_size": 64,
"font_weight": "bold",
"color": "#ffffff",
"align": "left",
"max_lines": 3
},
{
"type": "text",
"content": "{{ subtitle }}",
"x": 80,
"y": 360,
"width": 1040,
"font_size": 28,
"color": "#94a3b8",
"max_lines": 2
},
{
"type": "text",
"content": "{{ brand | default('docuget.com') }}",
"x": 80,
"y": 555,
"width": 400,
"font_size": 22,
"font_weight": "bold",
"color": "#ffffff"
}
]
}
Employee Badge (400x600)
Vertical ID card with company header, initials circle, and info:
{
"canvas": { "width": 400, "height": 600, "background": "#ffffff" },
"layers": [
{
"type": "rect",
"x": 0,
"y": 0,
"width": 400,
"height": 180,
"fill": "{{ brand_color | default('#1e40af') }}"
},
{
"type": "text",
"content": "{{ company_name }}",
"x": 0,
"y": 40,
"width": 400,
"font_size": 28,
"font_weight": "bold",
"color": "#ffffff",
"align": "center"
},
{
"type": "circle",
"x": 150,
"y": 140,
"r": 60,
"fill": "#e2e8f0",
"stroke": "#ffffff",
"stroke_width": 4
},
{
"type": "text",
"content": "{{ initials }}",
"x": 150,
"y": 155,
"width": 120,
"font_size": 36,
"font_weight": "bold",
"color": "#475569",
"align": "center"
},
{
"type": "text",
"content": "{{ full_name }}",
"x": 0,
"y": 280,
"width": 400,
"font_size": 26,
"font_weight": "bold",
"color": "#1e293b",
"align": "center"
},
{
"type": "text",
"content": "{{ job_title }}",
"x": 0,
"y": 320,
"width": 400,
"font_size": 18,
"color": "#64748b",
"align": "center"
},
{
"type": "text",
"content": "{{ department }}",
"x": 0,
"y": 385,
"width": 400,
"font_size": 16,
"color": "#94a3b8",
"align": "center"
},
{
"type": "text",
"content": "ID: {{ employee_id }}",
"x": 80,
"y": 450,
"width": 240,
"font_size": 20,
"font_weight": "bold",
"color": "#334155",
"align": "center",
"background": "#f1f5f9",
"padding": 8
}
]
}
seed_json:
{
"company_name": "ACME Corp",
"full_name": "John Doe",
"initials": "JD",
"job_title": "Software Engineer",
"department": "Engineering",
"employee_id": "EMP-00123",
"brand_color": "#1e40af",
"badge_year": "2026"
}
Limits
| Limit | Value |
|---|---|
| Max canvas size | 4096 x 4096 px |
| Max layers | 30 |
| Max image size (per layer) | 10 MB |
| Max fetch timeout (per image) | 30 seconds |
| Fonts | Inter, Roboto, Open Sans, Lato, Montserrat, Poppins, Playfair Display (Google Fonts CDN, cached) |
| Output formats | PNG, JPEG, WebP (via Sharp), SVG (native) |
Caching
Image renders are automatically cached via the chart_cache table. The
GET /v1/render/image/:key endpoint returns
Cache-Control: public, max-age=300 headers for browser/CDN caching.
Pass cache_ttl in the POST body to control cache duration (seconds). Set to
0 to disable caching.