Skip to content

Shorts / Vertical Cover

A 1080×1920 vertical cover built entirely from a JSON spec — no Python composition code. This demonstrates the JSON-first workflow where a spec file (written by hand or generated by an AI agent) is loaded and rendered in one call.

How it works

from quickthumb import Canvas

with open("shorts_cover.json", encoding="utf-8") as f:
    json_spec = f.read()

Canvas.from_json(json_spec).render("shorts_cover.png")

That's it. The entire composition lives in the JSON file.

The JSON spec

{
  "width": 1080,
  "height": 1920,
  "layers": [
    {
      "type": "background",
      "image": "background.jpg",
      "fit": "cover",
      "effects": [
        { "type": "filter", "brightness": 0.34, "contrast": 1.18, "saturation": 0.72, "blur": 4 }
      ]
    },
    {
      "type": "background",
      "gradient": {
        "type": "linear",
        "angle": 18,
        "stops": [
          ["#020617E6", 0.0],
          ["#020617CC", 0.42],
          ["#02061799", 0.72],
          ["#020617F7", 1.0]
        ]
      }
    },
    {
      "type": "shape",
      "shape": "ellipse",
      "position": ["86%", "25%"],
      "width": 400,
      "height": 400,
      "color": "#1D4ED8",
      "align": "center",
      "opacity": 0.32,
      "effects": [{ "type": "glow", "color": "#1D4ED8", "radius": 46, "opacity": 0.45 }]
    },
    {
      "type": "shape",
      "shape": "rectangle",
      "position": ["8%", "8%"],
      "width": 334,
      "height": 92,
      "color": "#C1121F",
      "border_radius": 18,
      "rotation": -3,
      "align": "top-left",
      "effects": [
        { "type": "shadow", "offset_x": 0, "offset_y": 10, "color": "#000000", "blur_radius": 16 },
        { "type": "stroke", "width": 2, "color": "#FCA5A5" }
      ]
    },
    {
      "type": "text",
      "content": "JSON-FIRST",
      "size": 36,
      "color": "#FFFFFF",
      "position": ["23.4%", "10.4%"],
      "align": "center",
      "weight": 900,
      "letter_spacing": 2,
      "effects": [{ "type": "shadow", "offset_x": 2, "offset_y": 2, "color": "#000000", "blur_radius": 4 }]
    },
    {
      "type": "text",
      "content": [
        { "text": "TURN LONG\nVIDEOS INTO", "color": "#F8FAFC", "weight": 900, "effects": [] },
        {
          "text": "\nSHORTS",
          "color": "#A3FF12",
          "weight": 900,
          "effects": [{ "type": "stroke", "width": 6, "color": "#000000" }]
        }
      ],
      "size": 128,
      "position": ["8%", "26%"],
      "align": "left",
      "max_width": "52%",
      "line_height": 1.0,
      "letter_spacing": -1,
      "effects": [{ "type": "shadow", "offset_x": 8, "offset_y": 8, "color": "#000000", "blur_radius": 12 }]
    },
    {
      "type": "shape",
      "shape": "rectangle",
      "position": ["80.5%", "42.3%"],
      "width": 340,
      "height": 720,
      "color": "#020617",
      "border_radius": 44,
      "opacity": 0.98,
      "rotation": 8,
      "align": "center",
      "effects": [
        { "type": "stroke", "width": 3, "color": "#CBD5E1" },
        { "type": "shadow", "offset_x": 0, "offset_y": 22, "color": "#000000", "blur_radius": 28 }
      ]
    },
    {
      "type": "image",
      "path": "preview.jpg",
      "position": ["80.5%", "42.3%"],
      "width": 286,
      "height": 596,
      "fit": "cover",
      "rotation": 8,
      "align": "center",
      "border_radius": 28,
      "effects": [{ "type": "filter", "brightness": 0.86, "contrast": 1.12, "saturation": 0.96 }]
    },
    {
      "type": "text",
      "content": "JSON in.\nCover out.",
      "size": 58,
      "color": "#E2E8F0",
      "position": ["8%", "66.5%"],
      "align": "left",
      "max_width": "42%",
      "auto_scale": true,
      "line_height": 1.1,
      "effects": [
        { "type": "background", "color": "#0F172AE6", "padding": [20, 24], "border_radius": 16 },
        { "type": "shadow", "offset_x": 0, "offset_y": 10, "color": "#000000", "blur_radius": 20 }
      ]
    },
    {
      "type": "text",
      "content": [
        { "text": "AGENT EMITS JSON", "color": "#FBBF24", "weight": 800, "effects": [] },
        { "text": "  |  QUICKTHUMB RENDERS", "color": "#94A3B8", "weight": 500, "effects": [] }
      ],
      "size": 36,
      "position": ["8%", "88.8%"],
      "align": "left",
      "max_width": "44%",
      "effects": [{ "type": "shadow", "offset_x": 2, "offset_y": 2, "color": "#000000", "blur_radius": 4 }]
    },
    {
      "type": "outline",
      "width": 6,
      "color": "#0F172A"
    }
  ]
}

Key techniques

Rotated phone mockup

The "phone frame" is two layers at the same position and angle — a dark rectangle with a stroke (the frame), and an image with matching rotation (the screen content). Both use "rotation": 8 and "align": "center" at ["80.5%", "42.3%"].

{ "type": "shape", "shape": "rectangle", "rotation": 8, "border_radius": 44, ... }
{ "type": "image", "rotation": 8, "border_radius": 28, ... }

The shape's border_radius is slightly larger than the image's, creating a visible border gap that acts as the phone bezel.

Decorative glow ellipse

A semi-transparent blue ellipse with a glow effect sits at ["86%", "25%"] — visually balancing the right side of the composition even though it has no text content.

{
  "type": "shape",
  "shape": "ellipse",
  "color": "#1D4ED8",
  "opacity": 0.32,
  "effects": [{ "type": "glow", "color": "#1D4ED8", "radius": 46, "opacity": 0.45 }]
}

auto_scale for safe text fitting

The caption box uses "auto_scale": true with "max_width": "42%" to shrink the font if the text grows — useful when this spec is generated by an AI that might write longer copy than expected.

Slight negative letter_spacing

The main headline uses "letter_spacing": -1 to tighten the very large (128px) text and prevent awkward gaps between characters at display sizes.

Why JSON-first?

  • The spec is a plain text file — easy to version-control, diff, and review
  • Any process (script, API, AI agent) can produce or modify it
  • Canvas.from_json() validates the spec before rendering — bad input raises ValidationError immediately
  • The composition can be re-rendered any time by just running Canvas.from_json(spec).render(...)

See JSON Schema & AI Workflow for the complete schema reference.