gifwrap
gifwrap is a Node.js library for decoding, editing, and encoding single-frame or animated GIF files. It expands each frame into a Jimp-shaped RGBA Buffer, offers pixel, crop, scale, palette, and quantization helpers, then writes the frames through an omggif-based codec. It can work with filenames or Buffers and includes TypeScript declarations. Despite its name and bitmap compatibility, Jimp is optional and is not installed with the package.
gifwrap remains a convenient pure-JavaScript tool for small, controlled Node.js GIF jobs, especially when its RGBA frame model fits existing code. Do not make it the default for untrusted uploads or a new high-volume image service; maintenance and memory behavior point toward sharper alternatives.
Use it if
- You need to inspect or rewrite animated GIF frames in a Node.js batch job without a native image dependency
- You want direct RGBA Buffer access and are comfortable implementing pixel operations yourself
- You need to create a small GIF from generated frames and control delays, looping, offsets, disposal, and palette scope
- You already use Jimp-shaped bitmap objects and need a narrow bridge to GIF encoding
- You process untrusted or large animations in a memory-limited service: decoding expands every frame to four bytes per pixel, and the repository has an open report about exhausting RAM
- You need browser support: the README explicitly says the package works only in Node.js, and its implementation uses Buffer and filesystem APIs
- You need active maintenance: 0.10.1 was published in March 2022, the last repository push was in December 2022, and an open issue asks for someone to take over maintenance
- You need partial alpha, interlaced output, APNG, WebP, or video: GIF output treats only alpha zero as transparent, every nonzero alpha as opaque, and the default codec cannot encode interlacing
- You expect arbitrary photographs to encode without preprocessing: GIF frames can use at most 256 color indexes, so high-color inputs must be quantized and may show banding or dithering
Setup reality
npm install gifwrap brings image-q and omggif, both JavaScript packages, so there is no compiler, native binary, credential, or config file. The package is CommonJS, runs only in Node.js, and reads or writes local paths with the filesystem. It also accepts encoded Buffers when you want to own I/O. TypeScript declarations are included as index.d.ts even though package.json does not advertise a types field, so most Node TypeScript resolvers find them through the conventional filename. The real setup cost is resource and image policy. A decoded frame is a width by height RGBA Buffer, which means four bytes per pixel for every frame before object overhead; a long or oversized animation can consume much more memory than its compressed file suggests. Put byte, dimension, and frame-count limits around untrusted input, ideally outside the request path. GIF permits no partial transparency, and gifwrap converts alpha 0 to transparent while treating every other alpha value as opaque. The encoder rejects frames needing more than 256 color indexes, so photographic or Jimp-edited frames usually need one of GifUtil's in-place quantizers before encoding. Quantizing all frames together can reduce palette flicker but costs additional CPU and memory. Frame delays are centiseconds, loops defaults to 0 for endless looping, and disposal method matters when frames use offsets or smaller rectangles. GifUtil.write overwrites its target and rejects some suspicious non-GIF suffixes, but your application still owns directory creation, collision handling, atomic replacement, and cleanup. The Jimp bridge shares or copies a bitmap rather than depending on Jimp, and its README examples were written against a much older Jimp constructor API, so verify compatibility with the Jimp major you actually install.
Patterns
Decode a GIF from a fileread-gif-file
const { GifUtil } = require('gifwrap');
const gif = await GifUtil.read('input.gif');
console.log({
width: gif.width,
height: gif.height,
frames: gif.frames.length,
loops: gif.loops,
});All decoded frames and their RGBA buffers are retained in memory. Check input size before using this on uploads.
Decode bytes without filesystem I/Odecode-gif-buffer
const { GifCodec } = require('gifwrap');
const codec = new GifCodec();
const gif = await codec.decodeGif(encodedBuffer);The buffer must contain the complete GIF. Catch GifError for malformed encoding failures.
Create a solid-color framecreate-solid-frame
const { GifFrame } = require('gifwrap');
const frame = new GifFrame(160, 90, 0x3366ffff, {
delayCentisecs: 10,
disposalMethod: GifFrame.DisposeToBackgroundColor,
});Packed colors use 0xRRGGBBAA order. Frame delay is measured in hundredths of a second.
Write an animated GIFwrite-animated-gif
const { GifFrame, GifUtil } = require('gifwrap');
const frames = [
new GifFrame(80, 80, 0xff0000ff, { delayCentisecs: 12 }),
new GifFrame(80, 80, 0x0000ffff, { delayCentisecs: 12 }),
];
await GifUtil.write('colors.gif', frames, { loops: 0 });loops: 0 means repeat forever. GifUtil.write overwrites the destination rather than creating an atomic temporary file.
Encode frames to an in-memory Bufferencode-gif-buffer
const { GifCodec } = require('gifwrap');
const codec = new GifCodec();
const encoded = await codec.encodeGif(frames, { loops: 3 });
await uploadObject(encoded.buffer, 'image/gif');The resolved Gif holds both encoded bytes and frame references, so release it after upload when processing large animations.
Edit pixels with coordinatesedit-pixels
gif.frames.forEach((frame) => {
frame.scanAllCoords((x, y, index) => {
if (x < 20) {
frame.bitmap.data[index] = 255;
frame.bitmap.data[index + 1] = 0;
frame.bitmap.data[index + 2] = 0;
frame.bitmap.data[index + 3] = 255;
}
});
});Each pixel occupies RGBA bytes at index through index + 3. Any alpha other than zero becomes fully opaque in the GIF.
Quantize all frames to a shared color budgetquantize-animation
const { GifUtil } = require('gifwrap');
GifUtil.quantizeWu(gif.frames, 256, 5, {
ditherAlgorithm: 'FloydSteinberg',
serpentine: true,
});
await GifUtil.write('quantized.gif', gif.frames, gif);Quantizers modify the supplied frames in place. Processing all frames together helps keep colors consistent across the animation.
Crop every frame to a rectanglecrop-frame
for (const frame of gif.frames) {
frame.reframe(10, 20, 120, 80);
}Offsets describe the new frame relative to the old image. If the requested bounds extend outside it, supply a fill RGBA value.
Pad a frame with transparent pixelspad-frame
frame.reframe(-16, -16,
frame.bitmap.width + 32,
frame.bitmap.height + 32,
0x00000000
);A negative offset adds space on the left or top. The fill color is required when the new frame exceeds existing bounds.
Scale pixel art by an integer factorscale-pixel-art
for (const frame of gif.frames) {
frame.scale(3);
}scale only accepts an integer factor of at least 1 and performs nearest-neighbor-style pixel expansion, not smooth resizing.
Check whether a frame can be encodedinspect-palette
for (const [index, frame] of gif.frames.entries()) {
const palette = frame.getPalette();
if (palette.indexCount > 256) {
throw new Error(`Frame ${index} needs quantization`);
}
}Transparency consumes one color index, so a transparent frame can hold at most 255 opaque colors without quantization.
Clone frames before destructive editsclone-before-editing
const { GifUtil } = require('gifwrap');
const editedFrames = GifUtil.cloneFrames(gif.frames);
editedFrames.forEach((frame) => frame.greyscale());Cloning duplicates pixel buffers and frame options. It protects the source but can nearly double frame memory.
Alternatives
| Package | Registry | Pick it when |
|---|---|---|
| sharp | npm | Use it for actively maintained, high-throughput server image processing when a native libvips dependency is acceptable. |
| gifuct-js | npm | Use it when browser-side GIF parsing and frame decompression matter more than writing modified GIF files. |
| gif-encoder-2 | npm | Use it for a focused streaming-style GIF encoder when you generate frames and do not need gifwrap's decoder and bitmap helpers. |
| omggif | npm | Use the lower-level codec directly when you want fewer abstractions and can manage indexed pixels and frame details yourself. |