ComfyUI Node

Save as GIF

The node that finally gets animated GIFs out of ComfyUI looking right

By id-fa·Created 3 months ago·Updated 19 days ago· 2
Save as GIF
  • video
  • images
  • images
  • info
◄filename_prefixComfyUI►
◄source_fps12.0►
◄fps12.0►
◄width0►
◄height0►
◄size_modepad►
◄resamplelanczos►
◄colors256►
◄palette_scopeglobal►
◄quantizermedian_cut_aforge►
◄ditherfloyd-steinberg►
◄dither_strength1.00►
◄halftone_size6►
◄halftone_steps0►
◄halftone_inkblack►
◄loop_count0►
◄frame_difffalse►
◄pillow_optimizefalse►
◄gamma1.00►

ComfyUI will happily hand you an MP4 or an animated WEBP, but for an actual .gif the stock graph has no answer - which is why the standard advice is "just pipe it through ffmpeg afterwards." This node is the answer that lives inside the workflow instead. Feed it the VIDEO output of a generation (or any Load Video, or even your SaveAnimatedWEBP path), and it writes an animated GIF with real control over how the 256-color limit is cheated. It's the export half of the ComfyUI-LoadGifAsVideo pack - its siblings turn looped GIFs into video, this one turns video back into GIF - but it stands perfectly well on its own on top of any stock video.

What you're actually tuning

GIF can hold at most 256 colors, so writing a good one is two problems: which colors the palette gets, and how everything else pretends to be those colors. The first is quantizer, the second dither, and both are where this node is several rungs above the -coalesce+ffmpeg pipeline most people fall back to.

Reality check: the defaults (median_cut_aforge + floyd-steinberg) look fine on 90% of photographic frames, and the wall of 21 dither options is mostly there for animation purists. Don't let it intimidate you. You'll touch a handful of inputs in practice, and the README is honest about which.

The knobs that actually matter

  • fps - the cheapest lever in the node. Set it below the source rate and it drops frames rather than slowing the animation down: 60 frames of 24fps material at fps = 8 becomes 20 frames that still run for 2.5 seconds. File size drops roughly linearly and the frames that survive lose nothing. Set it above the source rate and nothing happens - there's nothing new to show. The widget caps at 50fps because that's the format's real ceiling: GIF stores delays in hundredths of a second and every browser re-times anything under 2cs to 10cs.
  • colors (2–256) - fewer colors, smaller file, stronger dither pattern. Leave palette_scope on global unless your animation changes content a lot; per-frame palettes are more accurate there but flicker in flat areas and pay 768 bytes per frame on a 256-color table.
  • quantizer - the default median-cut is even and predictable. diversity and blend_diversity are the ones that hold onto small bright accents that median cut averages away. Pick one if the defaults band; otherwise, don't lose sleep.
  • halftone-poster - the secret weapon for tiny files. It ignores the palette machinery entirely and rounds each channel onto an even RGB grid, ImageMagick-style, throwing away most of the tone on purpose. Measured by the author, an 8-color square halftone cut a 164 KB baseline down to 20 KB. Want a print-looking meme, or a file small enough to email? This is the mode. Note it ignores quantizer and palette_scope - it dictates its own palette.

width and height (0 = keep source) letterbox with transparent bars if your aspect ratio disagrees with the canvas, which is a nice touch - a 4:3 clip on a square canvas doesn't get squashed.

What comes out

Two outputs. The images one is the dithered frames as an IMAGE batch - byte-for-byte what went into the file - handy for comparing settings without opening anything. The info STRING is the one you'll actually want: filename, size in KB, frame count, canvas, fps, running time, palette size and dither. Wire it to the stock Preview as Text node and you can watch the file size move as you tune. The ui result also previews correctly in the graph as an animated image - the author deliberately fought the frontend's video-preview heuristic to make that work.

Install

Via ComfyUI Manager (search "LoadGifAsVideo"), or:

cd ComfyUI/custom_nodes
git clone https://github.com/id-fa/ComfyUI-LoadGifAsVideo

Then restart ComfyUI. There are no extra Python dependencies - the source's pyproject.toml is empty, because Pillow, NumPy and PyTorch all ship with ComfyUI already. Files land in your output directory with a counter suffix (ComfyUI_00042_.gif). One real requirement: because this node has a VIDEO input, you need a ComfyUI build with the VIDEO type. On older builds the node loads but raises a clear error when you run it.

Gotchas worth knowing

Connect video or images, never both - it raises rather than guessing which frame rate you meant. If you're feeding an image batch, source_fps tells it how fast those frames play; with a video connected it's ignored because the video carries its own rate. And if you letterbox, the transparent slot costs one palette entry, so you're quantizing to 255 colors, not 256 - negligible, just don't chase a file that "should" have all 256.

One honest caveat: this is a niche, low-traffic pack from a solo author, so don't expect a huge support community around it. But for the exact moment where you need a good GIF out of the graph - not a banded ffmpeg afterthought - it's the cleanest path in the ecosystem.

Categoryload-gif-as-video

Inputs (21)

NameTypeDefaultDescription
filename_prefixSTRINGComfyUIPrefix for the file written into the ComfyUI output directory.
source_fpsFLOAT12.00.1–240The rate an `images` batch is meant to play at. Ignored when `video` is connected — a VIDEO carries its own rate. This is only the description of the input; `fps` is what gets written.
fpsFLOAT12.00.1–50Frame rate of the GIF. Set it below the source rate to drop frames: the animation runs for the same length of time on fewer frames, which is the cheapest way to shrink the file. Raising it above the source rate does nothing. GIF cannot store a delay shorter than 1/50s.
widthINT00–8192Output width in pixels. 0 keeps the source width, or derives it from `height`. With both set, the frame is scaled to fit inside the box keeping its aspect ratio; `size_mode` decides what happens to the room left over.
heightINT00–8192Output height in pixels. 0 keeps the source height, or derives it from `width`.
size_modeCOMBOpadWhat `width` x `height` means when the source has a different aspect ratio. pad: the box is the canvas — the frame is centred and the leftover bars are written transparent, so the GIF is exactly the requested size. fit: the frame is scaled to the largest size that fits inside the box and written at that size, with no bars. Same thing whenever only one of width/height is set.
resampleCOMBOlanczosFilter used when scaling. lanczos is the sharpest downscale; box and hamming are softer and quieter to dither; nearest keeps pixel art and hard-edged animation crisp instead of blurring it.
colorsINT2562–256Palette size. GIF allows at most 256 colors; fewer makes a smaller file and a stronger dither pattern. The -poster dithers round this down to a cube (8, 27, 64, 125, 216) because they quantize each channel on its own.
palette_scopeCOMBOglobalglobal: one palette for the whole animation — no color flicker, smaller file. per_frame: a fresh palette per frame — better color on animations whose content changes a lot. Ignored by the -poster dithers, whose palette does not depend on the frames at all.
quantizerCOMBOmedian_cut_aforgeHow the palette is chosen. median_cut_* split the color cube; diversity picks popular and far-apart colors in turn, which holds onto small bright accents that median cut averages away. Ignored by the -poster dithers, which dictate their own even RGB grid.
ditherCOMBOfloyd-steinbergHow colors the palette does not hold are approximated. Error diffusion looks cleanest on photographic frames; the ordered and halftone screens are stable frame to frame, so they do not crawl on animation. halftone/halftone-square/halftone-diamond/halftone-brick cap a cell at two colors for a printed-ink look; their -ordered variants lift that cap for smoother tone. The -mask variants lay a single flat ink dot (the halftone_ink color) over the picture and leave the rest of the frame alone; their lattice is the same in every frame, so it never crawls. The -mask-inverted variants swap the two regions: the picture shows through the dots and the ink fills the ground between them. The -poster variants are ImageMagick's -ordered-dither: they ignore the palette entirely and round each channel to an even RGB grid, throwing away most of the tone for a much smaller file. Lattices: halftone puts the dots on a hexagonal grid whose rows run horizontally, -square on an upright square grid, -diamond on a square grid turned 45 degrees (the classic newsprint mesh, whose rows never line up with the scan lines), -brick in horizontal rows a full pitch apart with every other row shifted by half, so each dot sits under the gap above it.
dither_strengthFLOAT1.000–1How much of the quantization error is dithered away. 0 disables the dither entirely; lower values trade banding back for less noise. For the -mask dithers this is the screen's coverage instead, reaching half at 1.0: the hex/square/diamond masks shrink the dot inside its cell, the brick mask keeps the dot's size and spreads the dots apart (pitch = halftone_size / sqrt(strength)). For the -mask-inverted dithers it is how much of the picture shows through the dots, so lowering it lets the ink take over.
halftone_sizeINT62–64Distance between neighbouring halftone dots in pixels (any of the halftone dithers). For the palette-searching screens, larger dots carry less of the image and compress far better. For the -poster pair the cell also sets the number of threshold steps, so the file peaks around size 8 and only shrinks again past 16 — there, small cells give the smallest file and large cells the boldest dots.
halftone_stepsINT00–255How many tonal steps one halftone cell resolves. 0 gives one per cell, which is what the cell size implies on its own. Setting it lower separates the dot size from the tone: big dots with coarse tone, which is the smallest a halftone gets. Ignored by the -mask dithers, whose dot is one fixed size.
halftone_inkCOMBOblackWhich end of the tonal range the halftone dot grows from. black: dark dots on a light ground, the way ink sits on paper. white: light dots out of a dark ground. For the -mask and -mask-inverted dithers this names the ink itself — real black or real white — and it is reserved in the palette.
loop_countINT00–1000How many extra times the GIF replays. 0 loops forever.
frame_diffBOOLEANfalseWrite only the pixels that changed since the previous frame; the rest are marked transparent so the viewer keeps what is already there. Costs one palette entry (255 colors instead of 256). Shrinks the file a lot when much of the frame holds still and the dither is stable (the ordered and halftone screens); error diffusion changes almost every pixel every frame, so it gains little there. Off writes every frame in full, cropped to the changed rectangle.
pillow_optimizeBOOLEANfalsePass optimize=True to Pillow's GIF writer. It trims each frame's color table to the colors that frame actually uses and marks unchanged pixels transparent. Note that it rebuilds the palette per frame, so palette_scope=global no longer gives one shared table and the file may grow instead of shrink. Try it and read the size in `info`.
gammaFLOAT1.000.1–10Tone correction applied to the frames before the palette is built and the dither runs. 1.0 leaves them alone; above it brightens the midtones, below it darkens them, and black and white stay put. It exists mainly for the -mask dithers: a black ink screen covers up to half the frame and makes the picture read darker than it is, a white one makes it read lighter. Raise gamma (around 1.5-2.5) to compensate for black ink, lower it (around 0.5-0.7) for white. Works with every dither, and the `images` output shows the corrected result.
videooptVIDEOThe video to save. Its frame rate is used as-is.
imagesoptIMAGEFrames to save, timed by the `fps` widget. Connect this or `video`, not both.

Outputs (2)

NameTypeDescription
imagesIMAGE—
infoSTRING—