ffmpegvideo-overlaysvideo-automation

Stop Pre-Rendering Lower Third PNGs: FFmpeg's drawvg Filter

·Javid Jamae·10 min read
Stop Pre-Rendering Lower Third PNGs: FFmpeg's drawvg Filter

Every lower third you've shipped probably started as a PNG: a designer file, or a headless Chrome screenshot, or an ImageMagick call that bakes a name and a job title into a transparent image you then feed to overlay. One PNG per guest, per episode, per language. FFmpeg 8.1 added a filter that draws the whole thing at render time from a text script, and the shapes can move because the coordinates are FFmpeg expressions.

Quick answer: The FFmpeg drawvg filter renders 2D vector graphics onto video frames using Cairo and its own script language, VGS, so animated lower thirds, progress bars, and hook bars can be drawn at render time instead of pre-rendered as PNG overlays. Coordinates, sizes, and colors accept FFmpeg expressions over t, w, h, and n, which is how a bar slides in on a timer. It needs FFmpeg 8.1 or newer built with --enable-cairo, which almost no distro package has yet. If you'd rather not maintain that build, FFmpeg Micro runs the toolkit for you and you send the job as one API call.

Lower thirds were never a text problem, they were an asset problem

Conventional wisdom says animated graphics belong in an editor, and that FFmpeg's job is to composite the result. Most pipelines work exactly that way, and they work fine until the graphic has to vary. The bottleneck isn't rendering, it's that the design is frozen inside a binary file. Change the accent color for a client and you regenerate 200 PNGs.

The drawvg filter moves the design into the command. A rounded rectangle, an accent stripe, and an easing curve become nine lines of text that live in git next to the encode script. Nothing gets pre-rendered, and nothing gets stale.

Here's the mechanism that makes it interesting rather than just tidy. VGS scripts are evaluated per frame, and any argument wrapped in parentheses is an FFmpeg expression with access to w, h, n (frame number), t (timestamp in seconds), and the current path position. Motion is arithmetic on t. There is no keyframe format to learn.

Build a sliding lower third with drawvg and drawtext

A lower third built with drawvg is two filters, not one: drawvg draws the plate and the accent stripe, and drawtext puts the name on top. Save this as lower-third.vgs for a 1920x1080 source:

setvar a (min(clip((t - 2) / 0.4, 0, 1), clip((9 - t) / 0.4, 0, 1)))
setvar x (60 - 720 * (1 - a))

save
translate (x) (h - 280)
setrgba 0.04 0.05 0.10 (0.9 * a)
roundedrect 0 0 660 128 10
fill
setrgba 0.98 0.29 0.22 (a)
rect 0 0 9 128
fill
restore

The variable a ramps 0 to 1 over 0.4 seconds starting at t=2, holds, then ramps back down before t=9. It drives both the alpha channel and the horizontal offset, so the plate slides in from off-screen and fades at the same time. Because alpha hits zero outside that window, you don't need a timeline enable= expression at all.

Now the part every syntax tour skips: the VGS language has no text-drawing commands. All 54 commands in the official reference are geometry, paint, transform, and control flow. Cairo can render text, drawvg doesn't expose it. So text stays with drawtext, and you repeat the easing expression there:

ffmpeg -i talk.mp4 -vf "
  drawvg=file=lower-third.vgs,
  drawtext=fontfile=/usr/share/fonts/truetype/inter/Inter-SemiBold.ttf:
    text='Maya Okonkwo':fontsize=46:fontcolor=white:
    x='60-720*(1-min(clip((t-2)/0.4,0,1),clip((9-t)/0.4,0,1)))+34':
    y='H-280+40':
    alpha='min(clip((t-2)/0.4,0,1),clip((9-t)/0.4,0,1))',
  format=yuv420p
" -c:a copy -c:v libx264 -crf 20 -preset medium out.mp4

Watch the coordinate letters. In drawvg, w and h are the frame dimensions. In drawtext, w and h are the text box, and the frame is W and H. Mixing them up is the single most common way this composite ends up with the name floating in the middle of the picture. If drawtext falls back to a font you didn't ask for, that's a separate fontconfig issue and it isn't fatal, it just ships the wrong font.

Draw a progress bar from the timestamp

A progress bar in drawvg is two rectangles, and the filled one gets its width from t divided by the clip length. The catch is that the built-in duration variable holds the frame's duration, not the clip's, so you pass the total in yourself:

TOTAL=$(ffprobe -v error -show_entries format=duration -of csv=p=0 in.mp4)
sed "s/__TOTAL__/$TOTAL/" progress.vgs.tpl > progress.vgs
setvar total __TOTAL__
setvar p (clip(t / total, 0, 1))

setrgba 1 1 1 0.20
rect 0 (h - 10) (w) 10
fill
setrgba 0.13 0.77 0.49 0.95
rect 0 (h - 10) (w * p) 10
fill

Six lines replace the entire class of tools people usually reach for here: a canvas render loop, an image sequence on disk, and an overlay with a frame-synced second input. The bar also scales to whatever resolution you feed it, because the width comes from w rather than a hardcoded 1920.

An animated hook bar for vertical shorts

A hook bar for a 1080x1920 short is the same pattern with a scale pop instead of a slide. Anchor the transform at the horizontal center so the box grows from its middle:

setvar pop (clip(t / 0.3, 0, 1))

save
translate (w / 2) 560
scale (0.94 + 0.06 * pop)
setrgba 0.99 0.84 0.16 (pop)
roundedrect -470 -78 940 156 20
fill
restore

Layer a drawtext on top with the hook copy and you have the opening beat of a TikTok or Reels cut, generated per variant from a CSV row rather than per variant in an editor. If you'd rather skip the script entirely for this one job, the viral short blueprint does exactly this shape: upload the vertical clip, type the hook, get the burned-in result with an optional music bed.

Your FFmpeg almost certainly doesn't have drawvg yet

The drawvg filter requires FFmpeg 8.1 (released 16 March 2026) compiled with --enable-cairo, and both halves of that fail on typical machines. Ubuntu 24.04 LTS ships FFmpeg 6.1. Debian 12 ships 5.1. An 8.0 build won't have the filter at all, and even an 8.1 static build won't have it unless whoever compiled it linked libcairo. Check before you write any VGS:

ffmpeg -filters | grep drawvg
ffmpeg -version | tr ' ' '\n' | grep cairo
ffmpeg -h filter=drawvg

If the first command prints nothing, you get No such filter: 'drawvg' and no hint about why. That's the same shape of problem as `Unknown encoder 'libx264'`, which is a build flag issue rather than a bug, and the same trap the 8.0 Whisper filter set when it landed: the release notes describe a feature your package manager won't hand you for a year or more.

You have three options. Compile FFmpeg yourself with Cairo dev headers on every build machine. Ship a container image and accept the size. Or don't run the encoder at all: FFmpeg Micro is the managed FFmpeg toolkit behind a REST API, so there are no binaries, no versions, and no servers to run, and the same job goes out as one API call from n8n, Make, Zapier, or your own code. The free tier is enough to test whether the graphic looks right before you commit to a build strategy.

Common drawvg pitfalls

The failures here cluster around parsing and coordinates rather than around Cairo itself. Five that cost real time:

  1. Commas in expressions break inline scripts. min(clip(t,0,1),1) contains commas, and the filtergraph parser reads those as filter separators. Use drawvg=file=script.vgs, or wrap the whole script in single quotes inside the graph.
  2. Wrap every variable reference in parentheses. Only bare numeric literals are safe unparenthesized. Write translate (x) (h - 280), never translate x h-280.
  3. duration is the frame duration. For anything measured against clip length, read the real duration with ffprobe and inject it as a variable.
  4. save and restore are not optional. Transforms and colors persist across commands within a frame, so a translate you forget to unwind shifts everything drawn after it.
  5. Append format=yuv420p after drawvg. Cairo works in RGB, and letting FFmpeg auto-negotiate the conversion into your H.264 encoder occasionally produces a pixel format the player won't touch.

When a PNG overlay is still the right call

A static logo bug is cheaper as a PNG. If the graphic never changes across the job, overlay composites a decoded image once and reuses it, while drawvg re-rasterizes vectors on every frame through a software rasterizer on CPU. At 4K60 with a busy script, that cost is measurable.

Complex branded templates are also a poor fit. Real typography, gradients over photography, logos with kerned wordmarks: those belong in a design tool, exported once, and the exported asset is what you overlay. drawvg earns its place in the middle ground, where the graphic is geometrically simple but the values change per job, per row, or per second.

And if you're producing dozens of near-identical branded intros with hand-tuned layout, a template-editor service with a visual canvas will get you there faster than expression math, at the cost of per-render fees and someone else's schema.

FAQ

What FFmpeg version do I need for the drawvg filter?

The drawvg filter needs FFmpeg 8.1 or newer, built with --enable-cairo. Version 8.1 "Hoare" was released on 16 March 2026, and the official VGS language reference lives at ffmpeg.org/drawvg-reference.html. Distro packages lag badly here, so run ffmpeg -filters | grep drawvg on the actual machine rather than trusting the version number.

Can drawvg render an SVG file directly onto a video?

The drawvg filter cannot load SVG files. VGS is its own script language, and there's no SVG parser in the filter. The path commands deliberately mirror SVG's letters (M, L, C, Q, Z, plus their lowercase relative forms), so hand-porting a simple <path d="..."> is realistic, but a full SVG with gradients, groups, and embedded fonts is not something you paste in.

How do I add text to a drawvg lower third?

Text in a drawvg lower third comes from a separate drawtext filter chained after drawvg, because VGS has no text or font commands at all. Chain them in order: drawvg paints the plate, drawtext writes on it. If both need to animate together, repeat the same easing expression in both filters, and remember drawtext uses W/H for the frame while drawvg uses w/h.

Is drawvg faster than overlaying a pre-rendered PNG?

For a single unchanging graphic, a pre-rendered PNG overlay is faster, since the image is decoded once and composited. The drawvg filter wins when the graphic varies: per-frame motion, per-job text and colors, or hundreds of variants that would otherwise mean hundreds of PNGs to generate, store, and invalidate.

Can I use drawvg from n8n, Make, or Zapier?

No automation platform runs FFmpeg itself, so drawvg isn't available as a node or module. Those tools call an HTTP endpoint, which means you either host your own 8.1 build behind an API or use a video API that already exposes the filter graph. Passing a URL rather than a binary file is the pattern that holds up, for the same reason it does with Google Drive in n8n.

Vector overlays are worth adopting, and rebuilding FFmpeg on every worker to get them usually isn't. Sign up free and run the composition step against a managed toolkit instead, with no build flags to chase.

About Javid Jamae

Founder & CEO at FFmpeg Micro

Javid is a software engineer, author, and entrepreneur with over 25 years of professional software development experience across enterprise, startup, and consulting environments. He founded FFmpeg Micro to make video processing accessible to developers through a simple, automation-first REST API.

Software EngineeringVideo ProcessingFFmpegCloud ArchitectureAPI DesignAutomation

Skip the command line

The Viral Short blueprint burns the text on for you and adds a music bed: upload, type the hook, done.

Run it (free)