GIF Frame Extractor — C source
Split an animated GIF into PNG frames with per-frame delays — decoded by our own pure GIF parser, entirely in your browser. Nothing uploads.
This is the C implementation — the same logic the interactive tool runs, in a shareable, citable form.
/* GIF Frame Extractor — decode a GIF byte stream into indexed frames.
*
* Language: C (C11), standard library only
* CosmoDev polyglot showcase port of the `gif-frame-extractor` tool.
* Ported from src/lib/gif-decode.ts — display source, part of CosmoDev's
* polyglot tool pages.
*
* A from-scratch GIF87a/89a parser: signature + logical screen descriptor
* (canvas size, global color table), extension blocks (frame delay,
* transparency, NETSCAPE loop count), and LZW image data decompressed to
* palette indices — interlaced frames reordered to natural row order.
* Malformed input yields NULL (the TS `null` return analogue); the caller
* frees results with gif_free(). */
#include <stdint.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
/* ── Growable byte buffer (poor man's TS number[] / Uint8Array) ──────────── */
typedef struct { unsigned char *data; size_t len, cap; } Buf;
static int buf_push(Buf *b, unsigned char c) {
if (b->len == b->cap) {
size_t cap = b->cap ? b->cap * 2 : 256;
unsigned char *p = realloc(b->data, cap);
if (p == NULL) return 0;
b->data = p; b->cap = cap;
}
b->data[b->len++] = c;
return 1;
}
/* ── Result types ────────────────────────────────────────────────────────── */
typedef struct {
int x, y, width, height;
unsigned char *palette; /* RGB triplets; NULL = use the global table. */
size_t palette_len;
unsigned char *indices; /* Pixel indices in natural row order. */
size_t indices_len;
int delay_ms, transparent_index, disposal;
} GifFrame;
typedef struct {
int width, height; /* Logical screen size. */
GifFrame *frames; size_t nframes;
unsigned char *global_palette; size_t global_palette_len; /* NULL if absent. */
int loop_count; /* NETSCAPE loop; 0 = forever, -1 when absent. */
} GifResult;
void gif_free(GifResult *g) {
if (g == NULL) return;
for (size_t i = 0; i < g->nframes; i++) {
free(g->frames[i].palette); free(g->frames[i].indices);
}
free(g->frames); free(g->global_palette);
free(g);
}
/* ── GIF LZW decompression ───────────────────────────────────────────────── */
/* Read one code LSB-first; returns EOI on truncation, like the TS decoder. */
static int read_code(const unsigned char *data, size_t data_len,
size_t *bit_pos, int code_size, int eoi_code) {
if ((*bit_pos + (size_t)code_size) >> 3 > data_len) return eoi_code;
int code = 0;
for (int i = 0; i < code_size; i++) {
size_t byte_idx = (*bit_pos + (size_t)i) >> 3;
if (byte_idx >= data_len) return eoi_code;
code |= ((data[byte_idx] >> ((*bit_pos + (size_t)i) & 7)) & 1) << i;
}
*bit_pos += (size_t)code_size;
return code;
}
/* Emit a code's chain; returns the chain's FIRST byte (needed for KwKwK). */
static int emit_chain(int code, const int *prefix, const unsigned char *suffix,
Buf *out) {
unsigned char stack[4096];
int n = 0, c = code;
while (c >= 0) { stack[n++] = suffix[c]; c = prefix[c]; }
for (int i = n - 1; i >= 0; i--) if (!buf_push(out, stack[i])) return -1;
return stack[n - 1];
}
/* minCodeSize 2-8, clear-code resets, growing codes — same contract as TS. */
static unsigned char *lzw_decode(int min_code_size, const unsigned char *data,
size_t data_len, size_t *out_len) {
int clear_code = 1 << min_code_size, eoi_code = clear_code + 1;
int code_size = min_code_size + 1, next_code = eoi_code + 1;
/* Dictionary as (prefix, suffix, first-byte) triples, reset per clear. */
static int prefix[4096];
static unsigned char suffix[4096], first[4096];
Buf out = {0};
size_t bit_pos = 0;
int prev = -1;
memset(prefix, 0xff, sizeof prefix); /* -1 everywhere, then literals. */
for (int i = 0; i < clear_code; i++) { prefix[i] = -1; suffix[i] = (unsigned char)i; first[i] = (unsigned char)i; }
for (;;) {
int code = read_code(data, data_len, &bit_pos, code_size, eoi_code);
if (code == eoi_code) break;
if (code == clear_code) {
next_code = eoi_code + 1; code_size = min_code_size + 1;
for (int i = 0; i < clear_code; i++) { prefix[i] = -1; suffix[i] = (unsigned char)i; first[i] = (unsigned char)i; }
prev = -1; continue;
}
if (prev == -1) {
if (code >= clear_code) break; /* First code after clear is a literal. */
emit_chain(code, prefix, suffix, &out); prev = code; continue;
}
if (code > next_code) break; /* Invalid — stop like browsers do. */
/* KwKwK: a code one ahead of the dictionary is prev + first(prev). */
int emitted_first;
if (code == next_code) {
emitted_first = emit_chain(prev, prefix, suffix, &out);
if (emitted_first >= 0) buf_push(&out, first[prev]);
emitted_first = first[prev];
} else {
emitted_first = emit_chain(code, prefix, suffix, &out);
}
if (next_code < 4096) { /* Table full: TS silently no-ops this write. */
prefix[next_code] = prev; suffix[next_code] = (unsigned char)emitted_first;
first[next_code] = first[prev];
next_code++;
if (next_code == (1 << code_size) && code_size < 12) code_size++;
}
prev = code;
}
*out_len = out.len;
return out.data;
}
/* Reorder interlaced rows into natural order; identity for short frames. */
static unsigned char *de_interlace(const unsigned char *idx, size_t idx_len,
int width, int height) {
if (height < 4 || width == 0) { /* Just copy — the caller owns one buffer. */
unsigned char *out = malloc(idx_len ? idx_len : 1);
if (out) memcpy(out, idx, idx_len);
return out;
}
unsigned char *out = malloc(idx_len);
if (out == NULL) return NULL;
size_t src = 0; /* Stored pass-by-pass: four passes, starts/steps below. */
static const int passes[4][2] = { {0, 8}, {4, 8}, {2, 4}, {1, 2} };
for (int p = 0; p < 4; p++) {
for (int row = passes[p][0]; row < height; row += passes[p][1]) {
memcpy(out + (size_t)row * width, idx + src, (size_t)width);
src += (size_t)width;
}
}
return out;
}
/* ── Whole-GIF parse ─────────────────────────────────────────────────────── */
static int le16(const unsigned char *p) { return p[0] | (p[1] << 8); }
/* Concatenate a sub-block chain into *out (caller frees); 0 on truncation. */
static int read_sub_blocks(const unsigned char *b, size_t len, size_t *pos,
unsigned char **out, size_t *out_len) {
Buf acc = {0};
for (;;) {
if (*pos >= len) { free(acc.data); return 0; }
unsigned size = b[(*pos)++];
if (size == 0) break; /* Terminator ends the chain. */
if (*pos + size > len) { free(acc.data); return 0; }
for (unsigned i = 0; i < size; i++) if (!buf_push(&acc, b[(*pos) + i])) { free(acc.data); return 0; }
*pos += size;
}
*out = acc.data ? acc.data : malloc(1); /* Non-NULL even for empty chains. */
*out_len = acc.len;
return *out != NULL;
}
GifResult *decode_gif(const unsigned char *b, size_t len) {
if (len < 13) return NULL;
if (memcmp(b, "GIF87a", 6) != 0 && memcmp(b, "GIF89a", 6) != 0) return NULL;
/* Logical screen descriptor: canvas size, flags, optional global palette. */
size_t pos = 6;
int width = le16(b + pos), height = le16(b + pos + 2);
unsigned packed = b[pos + 4]; pos += 7; /* Skip bg color + aspect ratio. */
unsigned char *global_palette = NULL; size_t global_len = 0;
if (packed & 0x80) {
size_t bytes = (size_t)(2 << (packed & 7)) * 3;
if (pos + bytes > len) return NULL;
global_palette = malloc(bytes);
if (global_palette == NULL) return NULL;
memcpy(global_palette, b + pos, bytes);
global_len = bytes;
pos += bytes;
}
GifResult *res = calloc(1, sizeof *res);
if (res == NULL) { free(global_palette); return NULL; }
res->width = width; res->height = height;
res->global_palette = global_palette; res->global_palette_len = global_len;
res->loop_count = -1; /* Absent until a NETSCAPE block says otherwise. */
int delay_ms = 0, transparent_index = -1, disposal = 0;
for (;;) {
if (pos >= len) goto fail;
unsigned char block = b[pos++];
if (block == 0x3b) break; /* trailer */
if (block == 0x21) { /* Extension: graphic control / NETSCAPE / skip. */
if (pos >= len) goto fail;
unsigned char label = b[pos++];
unsigned char *sub; size_t sub_len;
if (label == 0xf9) {
if (!read_sub_blocks(b, len, &pos, &sub, &sub_len)) goto fail;
if (sub_len < 4) { free(sub); goto fail; }
disposal = (sub[0] >> 2) & 7;
delay_ms = le16(sub + 1) * 10;
transparent_index = (sub[0] & 1) ? sub[3] : -1;
free(sub);
} else if (label == 0xff) {
if (!read_sub_blocks(b, len, &pos, &sub, &sub_len)) goto fail;
/* Concatenated: 11-byte name, then id 1 + loop lo/hi. */
if (sub_len >= 14 && memcmp(sub, "NETSCAPE2.0", 11) == 0 && sub[11] == 1)
res->loop_count = le16(sub + 12);
free(sub);
} else if (!read_sub_blocks(b, len, &pos, &sub, &sub_len)) {
goto fail;
} else {
free(sub);
}
continue;
}
if (block == 0x2c) { /* Image descriptor: rect, local palette, LZW data. */
if (pos + 9 > len) goto fail;
GifFrame *f = calloc(1, sizeof *f);
GifFrame *grown = res->nframes
? realloc(res->frames, (res->nframes + 1) * sizeof *res->frames)
: malloc(sizeof *res->frames);
if (f == NULL || grown == NULL) { free(f); goto fail; }
res->frames = grown;
res->frames[res->nframes++] = *f;
f = &res->frames[res->nframes - 1];
f->x = le16(b + pos); f->y = le16(b + pos + 2);
f->width = le16(b + pos + 4); f->height = le16(b + pos + 6);
unsigned ip = b[pos + 8];
pos += 9; /* Descriptor is 9 bytes: x, y, w, h, packed flags. */
if (ip & 0x80) {
size_t bytes = (size_t)(2 << (ip & 7)) * 3;
if (pos + bytes > len) goto fail;
f->palette = malloc(bytes);
if (f->palette == NULL) goto fail;
memcpy(f->palette, b + pos, bytes);
f->palette_len = bytes;
pos += bytes;
}
if (pos >= len) goto fail;
int min_code_size = b[pos++];
unsigned char *data; size_t data_len;
/* LZW payload must be a complete sub-block chain. */
if (!read_sub_blocks(b, len, &pos, &data, &data_len)) goto fail;
f->indices = lzw_decode(min_code_size, data, data_len, &f->indices_len);
free(data);
if (f->indices == NULL) goto fail;
if (ip & 0x40) { /* Interlaced — reorder to natural row order. */
unsigned char *nat = de_interlace(f->indices, f->indices_len, f->width, f->height);
free(f->indices);
f->indices = nat;
if (nat == NULL) goto fail;
}
f->delay_ms = delay_ms; f->transparent_index = transparent_index;
f->disposal = disposal;
delay_ms = 0; transparent_index = -1; disposal = 0;
continue;
}
goto fail; /* Unknown block type — bail. */
}
return res;
fail:
gif_free(res);
return NULL;
}
int main(void) {
/* 2×1 GIF89a: 2-color global palette (red, blue), one frame, pixels [0,1]. */
static const unsigned char gif[] = {
'G','I','F','8','9','a', 0x02,0x00, 0x01,0x00, 0x80, 0x00, 0x00,
0xff,0x00,0x00, 0x00,0x00,0xff,
0x2c, 0x00,0x00, 0x00,0x00, 0x02,0x00, 0x01,0x00, 0x00,
0x02, 0x02, 0x44,0x0a, 0x00, 0x3b
};
GifResult *res = decode_gif(gif, sizeof gif);
if (res == NULL) { fprintf(stderr, "malformed GIF\n"); return 1; }
printf("%dx%d, %zu frame(s), loop=%d\n", res->width, res->height,
res->nframes, res->loop_count);
for (size_t i = 0; i < res->nframes; i++) {
GifFrame *f = &res->frames[i];
printf("frame %zu: %dx%d at (%d,%d), delay %dms, indices [", i,
f->width, f->height, f->x, f->y, f->delay_ms);
for (size_t j = 0; j < f->indices_len; j++)
printf(j ? ", %d" : "%d", f->indices[j]);
printf("]\n");
}
gif_free(res);
return 0;
}
Also available in 9 other languages
Every CosmoDev tool ships its pure logic in TypeScript (web) and Go (CLI), with authored implementations in a dozen-plus languages — the same contract, ported. Compare all languages side by side →