ScreenshotNeo

BlogHow-to

Creating a GIF Maker: How to Add Text to GIFs with FFmpeg

Build a GIF caption workflow with FFmpeg drawtext, palette generation, and palette use. Learn how to handle fonts, placement, transparency, looping, and common errors.

By the ScreenshotNeo team29 September 202611 min read

Creating a GIF Maker: How to Add Text to GIFs with FFmpeg

To add text to an animated GIF with FFmpeg, use the drawtext filter to render a caption onto every frame, then generate and apply a GIF palette. Applying the caption before palette generation helps the palette account for its colors. This complete command centers white text near the bottom of the GIF:

ffmpeg -ignore_loop 0 -i input.gif -filter_complex "[0:v]drawtext=fontfile=/path/to/font.ttf:text='Hello':fontcolor=white:fontsize=36:x=(w-text_w)/2:y=h-text_h-20,split[a][b];[a]palettegen=reserve_transparent=1[p];[b][p]paletteuse" -loop 0 output.gif

Replace the input, font path, caption, and output names for your environment. The GIF input demuxer’s loop handling and the output -loop setting are separate considerations. FFmpeg documents drawtext as drawing text over video with libfreetype, and its palette filters generate and apply a palette for GIF encoding. See the FFmpeg filters documentation and format documentation.

1. Check your FFmpeg build and input

Before building a GIF maker, confirm that the FFmpeg executable you will deploy includes the filters and text-rendering support it needs. drawtext requires a build with --enable-libfreetype. Fontconfig, FriBidi, and HarfBuzz can provide additional fallback and text-shaping features, but availability depends on the build. FFmpeg options and filter support can differ between releases.

ffmpeg -version
ffmpeg -filters | grep -E 'drawtext|palettegen|paletteuse'
ffmpeg -h filter=drawtext
ffmpeg -h filter=palettegen
ffmpeg -h filter=paletteuse

On Windows, use a suitable command such as findstr instead of grep. If the filter list does not show drawtext, install or build an FFmpeg distribution with the required support; changing the filter expression will not add a missing feature.

Inspect the source before choosing dimensions or timing changes. In particular, decide whether to preserve the original size, frame rate, duration, and loop behavior. A quick probe can show stream metadata:

ffprobe -v error -select_streams v:0 -show_entries stream=width,height,r_frame_rate,avg_frame_rate,duration -of default=noprint_wrappers=1 input.gif

GIFs store frame delays with format-specific timing behavior. FFmpeg’s GIF demuxer documents min_delay and max_gif_delay in hundredths of a second, and supports -ignore_loop 0 when you want to read the animation’s loop behavior. Validate timing in the destination player too, since playback behavior can vary.

2. Choose and position the caption

The main drawtext settings are:

Setting Purpose Example
fontfile Select a specific font file for consistent rendering. fontfile=/path/to/font.ttf
text or textfile Set the caption inline or read UTF-8 text from a file. text='Hello'
fontcolor Set the text color. fontcolor=white
fontsize Set the rendered text size. fontsize=36
x and y Position the text using expressions. x=(w-text_w)/2:y=h-text_h-20

Here, w and h refer to the video dimensions, while text_w and text_h describe the rendered caption. Centering horizontally with x=(w-text_w)/2 and placing it near the bottom with y=h-text_h-20 adapts to different frame sizes. To put it near the top, use y=20; for a left-side caption, try x=20. Leave enough margin for the actual display size and any cropping by the destination platform.

Text readability depends on more than font size. Preview the GIF at its final display dimensions, especially when the image will appear as a small embed. Long captions may run outside the frame or obscure important content. Break long captions into lines or add application-side validation that rejects captions wider than the usable area. If your FFmpeg build supports additional drawtext options, consult the filter help for that build before relying on them in production.

3. Handle punctuation and Unicode safely

Inline filter strings pass through multiple parsers: often a shell, then FFmpeg’s filtergraph parser, then the individual filter. Quotes or escapes that work in one layer may not work in another. Colons separate filter options, commas separate filters, and backslashes have escaping meaning. For user-provided captions, a UTF-8 text file is usually easier to maintain than embedding arbitrary text in a filter string.

Create a file named caption.txt containing the caption, saved as UTF-8. Then use textfile:

ffmpeg -i input.gif -filter_complex "[0:v]drawtext=fontfile=/path/to/font.ttf:textfile=caption.txt:fontcolor=white:fontsize=36:x=(w-text_w)/2:y=h-text_h-20,split[a][b];[a]palettegen=reserve_transparent=1[p];[b][p]paletteuse" -loop 0 output.gif

Keep the file path accessible from the FFmpeg process’s working directory, or supply an absolute path. When you must put text directly in a filtergraph, escape characters according to FFmpeg filter syntax as well as the shell you use. Test punctuation such as colons, apostrophes, commas, percent signs, and backslashes with your actual command runner. Do not concatenate untrusted text directly into a shell command; pass arguments as an argument array in application code and use a controlled text-file workflow.

Unicode display depends on font coverage and shaping support. A font may lack a glyph, yielding missing-character boxes; complex scripts may need shaping libraries available in the FFmpeg build. Select a font that covers the required characters and test representative text. If your product distributes the font or embeds it in a container, check that its license permits that use.

4. Generate a palette for the captioned frames

GIF output uses a limited color palette, so a direct conversion can produce poor colors or banding. FFmpeg’s palettegen generates a palette for a video stream, and paletteuse applies one to another stream. The command splits the captioned frames: one branch generates the palette, while the other is encoded using it. Since drawtext runs before the split, the palette statistics include the caption.

The caption is rendered onto frames before palette generation, so the palette can account for its colors.
The caption is rendered onto frames before palette generation, so the palette can account for its colors.

The key options include:

Option Effect to consider
max_colors Limits palette colors. Reducing it can reduce color detail; the GIF palette limit is an implementation constraint.
reserve_transparent Reserves a palette entry for transparency when preserving transparent regions matters.
transparency_color Controls the color associated with transparency in palette generation.
stats_mode Changes how palette statistics are gathered, including choices that account for the full stream or emphasize changing regions.
Dithering controls Trade smooth-looking color transitions against patterns and variation in moving areas.

Exact option values and availability should be checked with ffmpeg -h filter=palettegen and ffmpeg -h filter=paletteuse for the version you deploy. Dithering is a visual choice: gradients can show banding without it, while some dithering methods make textured or moving backgrounds appear noisier. Compare outputs using representative source GIFs at their actual display size.

5. A reusable command-line workflow

  1. Install an FFmpeg build with drawtext, palettegen, and paletteuse.
  2. Inspect the input and choose whether to preserve its dimensions, timing, and loop behavior.
  3. Select a font file that is available on the machine running FFmpeg and licensed for your intended distribution.
  4. Put nontrivial or user-supplied caption text in a UTF-8 file.
  5. Apply drawtext, split the processed stream, generate the palette, and encode with paletteuse.
  6. Review readability, transparency, timing, loop behavior, and file size in the target environment.

For a one-off caption without special punctuation, this is a compact version:

ffmpeg -ignore_loop 0 -i input.gif -filter_complex "[0:v]drawtext=fontfile=/path/to/font.ttf:text='Sale ends today':fontcolor=white:fontsize=32:x=(w-text_w)/2:y=h-text_h-16,split[a][b];[a]palettegen=reserve_transparent=1[p];[b][p]paletteuse" -loop 0 captioned.gif

For a GIF maker service, build the command from validated parameters and invoke FFmpeg without a shell where possible. Give each job its own temporary directory and output path, constrain input size and processing time, and clean up temporary files after success or failure. Treat user-supplied media and caption text as untrusted input.

6. Add GIF captioning to an application

FFmpeg is a command-line program, so application code typically starts it as a child process. Keep the filtergraph stable and supply the caption through a per-job UTF-8 file. The following Python example uses only the standard library and assumes FFmpeg is on PATH, the source file exists, and the font path is configured for the host:

from pathlib import Path
import subprocess
import tempfile

source = Path("input.gif").resolve()
font = Path("/path/to/font.ttf").resolve()
output = Path("captioned.gif").resolve()
caption = "Hello from the GIF maker"

with tempfile.TemporaryDirectory() as workdir:
    textfile = Path(workdir) / "caption.txt"
    textfile.write_text(caption, encoding="utf-8")
    graph = (
        f"[0:v]drawtext=fontfile={font}:textfile={textfile}:"
        "fontcolor=white:fontsize=36:x=(w-text_w)/2:y=h-text_h-20,"
        "split[a][b];[a]palettegen=reserve_transparent=1[p];"
        "[b][p]paletteuse"
    )
    command = [
        "ffmpeg", "-y", "-ignore_loop", "0", "-i", str(source),
        "-filter_complex", graph, "-loop", "0", str(output),
    ]
    result = subprocess.run(
        command, check=True, capture_output=True, text=True, timeout=120
    )
    print(f"Wrote {output}")

In a production service, catch timeout and nonzero-exit errors, retain enough diagnostic output to identify failures, and avoid returning raw process details to end users. Configure the font path for your deployment rather than assuming a particular machine has a particular font installed. If the input can be large or adversarial, enforce resource limits outside the command as well.

7. Preserve dimensions, timing, transparency, and loops

Adding a filter does not require changing the source dimensions or frame rate. Avoid adding scaling or frame-rate conversion unless the product needs it: resizing can affect caption legibility, while changing frame timing may alter the animation’s feel. If the source has transparency, use palette settings intended to reserve transparency and check the result over both light and dark backgrounds. A caption’s opaque pixels will naturally cover the source where the text is drawn.

Transparency reservation and dithering settings affect how the finished GIF looks over different backgrounds.
Transparency reservation and dithering settings affect how the finished GIF looks over different backgrounds.

Looping involves input and output behavior. -ignore_loop 0 tells the GIF demuxer to honor the input loop setting when reading; output -loop 0 requests infinite looping for the generated GIF. If your application should preserve a finite input loop count, do not assume those flags alone preserve it. Verify the output behavior with the destination viewer and use the FFmpeg format documentation for the exact version and workflow.

8. Troubleshooting

Symptom Likely cause Fix
No such filter: drawtext The FFmpeg build lacks the filter or required text support. Use a build with --enable-libfreetype and confirm with ffmpeg -filters.
Error initializing filter or a parse error Unescaped punctuation, a malformed filtergraph, or a path containing special characters. Move caption text into a UTF-8 textfile; inspect filter help and escape filter syntax carefully.
Font cannot be loaded The path is wrong, unreadable by the process, or unavailable in its container. Use an absolute path, set file permissions, and include the licensed font in the runtime image.
Text is missing or shows boxes The selected font lacks glyphs, or shaping support is insufficient. Choose a font with the needed glyph coverage and verify the build’s shaping libraries.
Text is clipped or unreadable The caption exceeds the frame or is too small at actual display size. Test at output dimensions, adjust fontsize and x/y, or wrap/reject overlong text.
Colors look muddy or gradients band GIF palette quantization and dithering tradeoffs. Generate a palette from the captioned stream, compare dithering choices, and inspect at the intended size.
Transparent areas become opaque or change color The palette did not reserve or represent transparency as needed. Try reserve_transparent=1, review transparency settings, and test against contrasting backgrounds.
Output does not loop as expected Input demuxing and output loop settings were confused, or the viewer handles loops differently. Set input loop handling deliberately, configure output looping, and validate in the target viewer.
Output is unexpectedly large or slow The source has many frames, large dimensions, or complex per-frame content. Measure representative jobs; consider product-appropriate limits or resizing only when acceptable.

9. Performance, reliability, and cost

Processing cost generally grows with the amount of image data and work per frame: larger dimensions and longer, denser animations mean more pixels to filter and encode. Palette generation adds work, but it supports better color mapping than simply ignoring palette behavior. Measure on representative inputs instead of assuming a fixed duration or file size. Set job timeouts and resource limits based on your service’s capacity and the inputs you accept.

For reliability, check that the output file exists and is nonempty after a successful process exit. Consider probing the output and validating that it is readable before publishing it. Capture FFmpeg’s error output for operational diagnosis, but avoid logging sensitive caption text or exposing local paths unnecessarily. Pin and document the FFmpeg version in your deployment, then verify filter options when upgrading because documentation and supported options can change across releases.

FFmpeg itself is not priced per screenshot or per GIF in this workflow; operational costs come from the machines, storage, bandwidth, and engineering needed to run and maintain processing. Review the licensing obligations for the FFmpeg build and any font files you distribute. For a user-facing maker, also decide how long uploaded source files and generated GIFs are retained.

10. Or skip the browser setup

If the task is capturing a web page as an image rather than captioning an existing GIF, ScreenshotNeo provides a website screenshot API. One GET request returns a PNG, JPEG, WebP, or PDF; the full parameter reference is in the ScreenshotNeo API docs.

curl -G "https://api.screenshotneo.com/v1/shot" -d access_key=YOUR_API_KEY --data-urlencode url=https://stripe.com -o shot.webp
import requests
r = requests.get("https://api.screenshotneo.com/v1/shot", params={"access_key": "YOUR_API_KEY", "url": "https://stripe.com"}, timeout=90)
open("shot.webp", "wb").write(r.content)
const q = new URLSearchParams({ access_key: 'YOUR_API_KEY', url: 'https://stripe.com' });
const res = await fetch(`https://api.screenshotneo.com/v1/shot?${q}`);

ScreenshotNeo removes cookie banners, newsletter popups, and chat widgets before the shot, with each cleanup step configurable. Bot checks, blank pages, and failed loads are never billed, and response headers report the page verdict and billing status. Its MCP server lets AI agents use take_screenshot, get_page_info, and capture_pdf. The free plan includes 1,000 screenshots a month with no card; paid plans start at $5 for 3,000 shots. Sign up for the free plan.

11. FAQ

Does drawtext put the caption on every frame?

Yes. When applied to the video stream before GIF encoding, the filter renders the caption across the processed frames.

Why use palettegen and paletteuse instead of a simple conversion?

GIF color is palette-based. Generating a palette from the processed stream lets the encoder map the caption and animation colors using that palette; dithering and transparency settings still affect the result.

Can I use different text on each frame?

This workflow uses a fixed caption for the stream. Timed or changing captions require a more involved filter expression or separate per-frame processing; verify the available drawtext features in your FFmpeg build.

Can I use this in a commercial GIF maker?

Check the licensing terms for your FFmpeg distribution and every font you bundle or provide. The technical ability to render a font does not grant redistribution rights.