GIF to PNG in the Browser: First Frames, 1-Bit Transparency, and Why the PNG Gets Bigger
GIF to PNG sounds like the simplest conversion there is. Load the image, draw it on a canvas, export PNG. And for a still GIF, that really is all. But GIFs have quirks, and three of them surprise people every time: anim
GIF to PNG sounds like the simplest conversion there is. Load the image, draw it on a canvas, export PNG. And for a still GIF, that really is all.
But GIFs have quirks, and three of them surprise people every time: animation, transparency and file size.
The one-liner, done properly
async function gifToPng(file) {
const bitmap = await createImageBitmap(file)
const canvas = new OffscreenCanvas(bitmap.width, bitmap.height)
canvas.getContext("2d").drawImage(bitmap, 0, 0)
bitmap.close()
return canvas.convertToBlob({ type: "image/png" })
}
OffscreenCanvas lets this run in a Web Worker, so converting 30 files doesn't freeze the page.
Quirk 1: an animated GIF gives you one frame
Draw an animated GIF to a canvas and you get a single frame. That's not a bug. The HTML spec says that when an animated image is drawn, the browser uses its "default image", and if the format doesn't define one, the first frame. createImageBitmap follows the same rule.
So for an animated GIF, the code above gives you a still PNG of frame one. For thumbnails, previews and "I just need a still of this", that's exactly right.
Can PNG animate? Yes, sort of. APNG is an extension of PNG that stores frames, and modern browsers play it. But many apps, upload forms and image editors only read the first frame of an APNG. If the goal is "keep it moving", stay with the GIF or export a short video (MP4 or WebM are far smaller than either).
Getting every frame when you need them
If you want all the frames, for a sprite sheet or to inspect an animation, use the WebCodecs ImageDecoder API, available in Chromium-based browsers (check support for your audience):
async function gifFrames(file) {
const decoder = new ImageDecoder({ data: file.stream(), type: "image/gif" })
await decoder.tracks.ready
const count = decoder.tracks.selectedTrack.frameCount
const frames = []
for (let i = 0; i < count; i++) {
const { image } = await decoder.decode({ frameIndex: i }) // a VideoFrame
frames.push(image)
}
return frames // draw each with ctx.drawImage(frame, 0, 0), then frame.close()
}
Close each VideoFrame when you're done. They hold real memory, and a 200-frame GIF adds up fast.
Quirk 2: GIF transparency is on or off, never partial
A GIF pixel is either fully transparent or fully opaque. There's no 50% alpha. That's why GIF logos have jagged edges on dark backgrounds: the anti-aliasing was blended against a background colour (usually white) when the GIF was made.
Converting to PNG keeps the transparency exactly, but it can't invent the smooth edges that were never there. If edges look rough, the fix is a better source (an SVG or the original PNG), not a different converter.
The opposite trap: if you ever convert that PNG on to JPEG, there's no alpha at all. Fill the canvas with white first, or the transparent areas turn black in many viewers.
Quirk 3: the PNG is often bigger than the GIF
People expect a "better" format to be smaller. Here it often isn't.
A GIF stores at most 256 colours, as palette indexes, one byte per pixel or less. Canvas exports a PNG with full 8-bit RGBA, four bytes per pixel before compression. Same picture, more data to describe it. For flat graphics the PNG can easily come out larger.
That's fine when you need a lossless still for editing or printing. If size matters more, run the PNG through a palette quantizer (tools like pngquant do this) or pick WebP for the web.
When to convert at all
- Convert when you need a still image for an upload form, a document, a design tool, or editing.
- Keep the GIF when the animation is the point.
- Use video when the animation is long or large.
For the first case, BatchSet's free GIF to PNG converter does exactly what this post describes: lossless PNG, transparency kept, the first frame for animated GIFs, several files at once with a ZIP download, and nothing uploaded.
This article was drafted with an AI assistant. The code follows the browser APIs as specified; test ImageDecoder support for your target browsers.
Originally published by Dev.to WebDev. Aggregated on AIWithGhost for educational purposes — full credit and traffic to the original publisher.