〽️ ImageOps Text
Watermarks, subtitles, and title cards on every frame — no extra tools
- color
- stroke_color
- image
- mask
- image
- mask
Eventually you'll want words on your images, and it won't be the fun kind. Watermarking AI output before posting is standard practice in this community, and if you're making video you'll need subtitles or a title card burned into the frames. ImageOps Text does all of that inside the graph: multiline text, a real font, stroke/outline, alignment, opacity - composited over an image or every frame of a batch in one go.
It's the text-overlay node from the MajoorWaldi ComfyUI-Majoor-ImageOps pack. Core ComfyUI has famously nothing for text, and the classic workaround (WAS Node Suite's text nodes) is a heavier install if all you want is a clean burn-in. What this node adds on top of the basics is batch awareness - the pack treats every IMAGE input as frames, so a 120-frame animation gets its captions on all 120 frames, consistently, in a single pass.
How it works
Under the hood it's the same approach a compositor would use manually: render the text into a transparent RGBA overlay, then alpha-composite it over the source. The rendering is done with Pillow's multiline text drawing - no GPU involved - so it's deterministic and easy to reason about. The x / y inputs are normalized (0.0–1.0, top-left origin), and default to 0.5, 0.5, meaning the text anchor sits mid-frame. align handles left/center/right on the text itself.
Fonts are the thing most people hit first. If you leave font_path empty, the node tries arial.ttf, then DejaVuSans.ttf (present on most Linux installs), then falls back to Pillow's default bitmap-ish font - which looks like it sounds. For anything presentable, give it a path to a real .ttf on disk. It's a plain string input, so you can wire a path from a text node and switch fonts at runtime.
Inputs that matter
text- multiline; hit Enter for a new line. The default string is "ImageOps Text", so your first run will stamp that across your image. Change it first.x/y- normalized anchor position. Remember the center default; drag on the live preview or set0.02, 0.02for a top-left corner watermark.font_size(1–512 px) andcolor- the color swatch has the pack's eyedropper, so you can pick a color off any visible preview.opacity- text alpha. Set it to 0 and the node is a pure passthrough.stroke_width/stroke_color- an outline, which is the difference between a title that reads over a busy background and one that doesn't.font_path- see above.mask- an effect mask that restricts where the text composites. Feed it a radial gradient and the text fades in only under the bright part. The outputmaskmirrors it.
Outputs
image and mask - the composited result, plus the effect mask so you can reuse it downstream. Both pass through cleanly, so you can drop this node mid-chain without breaking anything.
Installing it
cd ComfyUI/custom_nodes
git clone https://github.com/MajoorWaldi/ComfyUI-Majoor-ImageOps
Or search "ComfyUI-Majoor-ImageOps" in ComfyUI Manager, restart, and hard-refresh the browser (Ctrl+F5 / Cmd+Shift+R on macOS). No pip dependencies, no models to fetch - text rendering is Pillow, which ComfyUI already ships. ComfyUI ≥ 0.13.0.
Where people get burned
The per-frame rendering is pure Python, and it loops over the whole batch. On stills that's instant; on a 512-frame render it's CPU time you should budget for - don't expect GPU-speed text burns on long clips. And the font fallback is silent: if your custom font path is wrong, you'll get DejaVu or the default font with no error to tell you. Check the text renders how you expect before committing a long video render to it.
Inputs (15)
| Name | Type | Default | Description |
|---|---|---|---|
| bypass | BOOLEAN | false | — |
| text | STRING | ImageOps Text | — |
| x | FLOAT | 0.500-2–3 | — |
| y | FLOAT | 0.500-2–3 | — |
| font_size | INT | 641–512 | — |
| color | COLOR | #ffffff | — |
| opacity | FLOAT | 1.000–1 | — |
| align | COMBO | center | 3 options: left, center, right |
| line_spacing | INT | 40–256 | — |
| stroke_width | INT | 00–64 | — |
| stroke_color | COLOR | #000000 | — |
| invert_mask | BOOLEAN | false | — |
| imageopt | IMAGE,VIDEO | Images/Video input. | |
| maskopt | MASK | — | |
| font_pathopt | STRING | — |
Outputs (2)
| Name | Type | Description |
|---|---|---|
| image | IMAGE | — |
| mask | MASK | — |