🐧 TextEdit
Stop making the model render text — slap it on with Pillow instead
- image
- IMAGE
Diffusion models are still bad at text. Flux and Qwen-Image got a lot better at it, but ask a model to spell out a paragraph of small legible text and you'll still get a garbled mess - and a chunk of your VRAM doing it. The boring, reliable answer is to not have the model render text at all: generate a clean image and overlay the words afterwards. That's exactly what PenguinTextOnImage (shown in the node menu as "🐧 TextEdit") does. It's a single text-overlay node that draws strings onto an image with Pillow - gradients, outlines, shadows, the works - and hands you back an ordinary ComfyUI IMAGE. No model, no API key, no VRAM cost.
What it actually is
Despite the "AI text editor" vibes of the name, there is no AI here. The entire node is ~200 lines of Pillow drawing code in a one-file pack (project.py) plus a font folder. It converts your IMAGE tensor to an RGBA PIL image, draws the text on a transparent layer, and pastes it back - pure post-processing. That means it's deterministic, instant (mostly), and works on anything that flows through the graph: a KSampler output, an upscale, a batch of frames.
Core ComfyUI has no text overlay built in, so people usually end up in WAS Node Suite, which has one buried in a pack of a few hundred nodes. If you want just the text node and nothing else, this pack is the minimalist alternative - though "minimalist" also means "you're on your own" when it breaks, so read the gotchas below.
The inputs that matter
There are 22 inputs, but you'll actually touch maybe six of them:
text- multiline string. The whole point. Leave it blank and the node returns your image untouched.image- the image you're stamping on.x_pct/y_pct(0–100, default 50/50) - percentage position across the image, like50/50= dead center.h_anchor/v_anchor- left/center/right and top/center/bottom. Think of it as "which part of the text box sits at the percentage point." Center-center at 50/50 puts the middle of the text in the middle of the image.offset_x/offset_y- fine pixel nudges after anchoring. This is the pair you'll fiddle with most to get a label sitting exactly on a border.font_file- a dropdown of fonts in the pack'sfont/folder. It ships with one (Bond Story.ttf). Drop your own.ttf/.ttc/.otfin that folder and restart ComfyUI and it appears in the list.
The rest is styling: font_size, hex colors (text_color, stroke_color, shadow_color, and gradient start_color/end_color), and opacity floats. Flip use_gradient on and the fill sweeps from one color to the other along angle.
Output is a single IMAGE - wire it straight into a Save Image node, or chain more post-processing after it.
Install
The pack has no requirements.txt and no model downloads - it only needs numpy, torch, and Pillow, which every ComfyUI install already has. Either:
- ComfyUI Manager → search
Comfyui-TextEditor-Penguin→ Install, then restart, or -
and restart ComfyUI.cd ComfyUI/custom_nodes git clone https://github.com/PenguinTeo/Comfyui-TextEditor-Penguin
Where people get burned
This pack has essentially no community footprint (it's tiny and new), so treat these as "things the code itself tells you to watch out for":
- Your font isn't in the dropdown. The list is built at load time from files inside the pack's
font/folder, not your system fonts. Add the font there and restart. - Text lands in the wrong place or doesn't render. The author recommends PIL ≥ 10.0 because the node relies on
font.getbbox()for metrics. On older Pillow versions that can return junk, and the text ends up offset or invisible. If text placement looks wrong, update Pillow first. - The gradient is slow. It's a per-pixel Python loop, not a C rasterizer. A huge font at a big size can take a second or two. Fine for a watermark, annoying if you're doing this across hundreds of frames.
- Thick strokes are drawn N² times (the outline is literally the text redrawn at every pixel offset around the glyph - 289 draws at the default
stroke_widthof 8). Keep strokes modest. - Batches only get the first image. The node reads
image[0]and ignores the rest of the batch dimension. If you're stamping frames of a video, expect to loop it per-frame.
The trade-off is the whole deal: one tiny pack, zero dependencies, and pixel-perfect text you control completely - in exchange for a node that's quiet, unmaintained-looking, and all on you if something odd happens. For watermarking and labeling, that's usually a fair trade.
Inputs (23)
| Name | Type | Default | Description |
|---|---|---|---|
| text | STRING | — | |
| image | IMAGE | — | |
| x_pct | INT | 500–100 | — |
| y_pct | INT | 500–100 | — |
| h_anchor | COMBO | 3 options: left, center, right | |
| v_anchor | COMBO | 3 options: top, center, bottom | |
| offset_x | INT | 0-4096–4096 | — |
| offset_y | INT | 0-4096–4096 | — |
| font_size | INT | 1201–512 | — |
| font_file | COMBO | Bond Story.ttf | 1 options: Bond Story.ttf |
| text_color | STRING | #ffffff | — |
| text_opacity | FLOAT | 1.000–1 | — |
| use_gradient | BOOLEAN | false | — |
| start_color | STRING | #ff0000 | — |
| end_color | STRING | #0000ff | — |
| angle | INT | 0-180–180 | — |
| stroke_width | INT | 80–128 | — |
| stroke_color | STRING | #000000 | — |
| stroke_opacity | FLOAT | 1.000–1 | — |
| shadow_x | INT | 0-100–100 | — |
| shadow_y | INT | 0-100–100 | — |
| shadow_color | STRING | #000000 | — |
| shadow_opacity | FLOAT | 1.000–1 | — |
Outputs (1)
| Name | Type | Description |
|---|---|---|
| IMAGE | IMAGE | — |