PaperStudio/Image Composition
UsuarioSoporteDeveloper

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

  1. Create a template with source_type = "image_compose"
  2. Define layers in the layers column (JSONB) or legacy content field (JSON string)
  3. Define form fields in the form_fields column (or legacy seed_json._form)
  4. Set default values in seed_json (canvas config in seed_json._canvas)
  5. 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 (or JWT_SECRET fallback) — HMAC signing key
  • API_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.