Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
62 changes: 62 additions & 0 deletions packages/gif/codec/Makefile
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
# using giflib from https://sourceforge.net/projects/giflib/
GIFLIB_URL = https://sourceforge.net/projects/giflib/files/giflib-5.2.2.tar.gz/download
GIFLIB_PACKAGE = node_modules/giflib.tar.gz

GIFLIB_DIR = node_modules/giflib
BUILD_DIR = node_modules/build

OUT_DEC_JS = dec/gif_dec.js
OUT_DEC_CPP = dec/gif_dec.cpp
OUT_DEC_WASM = dec/gif_dec.wasm
ENVIRONMENT = web,worker

PRE_JS = pre.js

GIFLIB_OUT = $(BUILD_DIR)/libgif.a

STACK_SIZE = 5242880
INITIAL_MEMORY_SIZE = 16777216

.PHONY: all clean

all: $(OUT_DEC_JS)

# Build giflib into a static archive
$(GIFLIB_OUT): $(GIFLIB_DIR)/gif_lib.h
mkdir -p $(BUILD_DIR)
$(CC) $(CFLAGS) -I $(GIFLIB_DIR) -c $(GIFLIB_DIR)/dgif_lib.c -o $(BUILD_DIR)/dgif_lib.o
$(CC) $(CFLAGS) -I $(GIFLIB_DIR) -c $(GIFLIB_DIR)/gifalloc.c -o $(BUILD_DIR)/gifalloc.o
$(CC) $(CFLAGS) -I $(GIFLIB_DIR) -c $(GIFLIB_DIR)/gif_err.c -o $(BUILD_DIR)/gif_err.o
$(CC) $(CFLAGS) -I $(GIFLIB_DIR) -c $(GIFLIB_DIR)/openbsd-reallocarray.c -o $(BUILD_DIR)/openbsd-reallocarray.o
$(AR) rcs $@ $(BUILD_DIR)/dgif_lib.o $(BUILD_DIR)/gifalloc.o $(BUILD_DIR)/gif_err.o $(BUILD_DIR)/openbsd-reallocarray.o

# Build the final decoder JS+WASM
$(OUT_DEC_JS): $(OUT_DEC_CPP) $(GIFLIB_OUT)
$(CXX) \
-I $(GIFLIB_DIR) \
$(CXXFLAGS) \
$(LDFLAGS) \
--pre-js $(PRE_JS) \
--bind \
-s ERROR_ON_UNDEFINED_SYMBOLS=0 \
-s ENVIRONMENT=$(ENVIRONMENT) \
-s EXPORT_ES6=1 \
-s DYNAMIC_EXECUTION=0 \
-s MODULARIZE=1 \
-s STACK_SIZE=$(STACK_SIZE) \
-s INITIAL_MEMORY=$(INITIAL_MEMORY_SIZE) \
-o $@ \
$+

# Download and extract giflib
$(GIFLIB_PACKAGE):
mkdir -p $(@D)
curl -sL $(GIFLIB_URL) -o $@

$(GIFLIB_DIR)/gif_lib.h: $(GIFLIB_PACKAGE)
mkdir -p $(@D)
tar xzm --strip 1 -C $(@D) -f $(GIFLIB_PACKAGE)

clean:
$(RM) $(OUT_DEC_JS) $(OUT_DEC_WASM)
$(RM) -r $(BUILD_DIR)
254 changes: 254 additions & 0 deletions packages/gif/codec/dec/gif_dec.cpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,254 @@
#include <emscripten/bind.h>
#include <emscripten/val.h>
#include <cstdlib>
#include <cstring>
#include "gif_lib.h"

using namespace emscripten;

thread_local const val Uint8ClampedArray = val::global("Uint8ClampedArray");
thread_local const val ImageData = val::global("ImageData");
thread_local const val Array = val::global("Array");
thread_local const val Object = val::global("Object");

struct MemoryReader {
const uint8_t* data;
size_t size;
size_t pos;
};

int readFromMemory(GifFileType* gif, GifByteType* buf, int len) {
MemoryReader* reader = static_cast<MemoryReader*>(gif->UserData);
size_t remaining = reader->size - reader->pos;
size_t to_read = (size_t)len < remaining ? (size_t)len : remaining;
memcpy(buf, reader->data + reader->pos, to_read);
reader->pos += to_read;
return (int)to_read;
}

GifFileType* openGif(MemoryReader* reader, int* error) {
GifFileType* gif = DGifOpen(reader, readFromMemory, error);
if (!gif) return nullptr;
if (DGifSlurp(gif) != GIF_OK) {
DGifCloseFile(gif, error);
return nullptr;
}
return gif;
}

// Get frame disposal method and delay from Graphics Control Extension
struct FrameInfo {
int transparentIndex;
int disposalMethod;
int delay; // in centiseconds (1/100th of a second)
};

FrameInfo getFrameInfo(SavedImage* frame) {
FrameInfo info;
info.transparentIndex = -1;
info.disposalMethod = 0;
info.delay = 0;

for (int i = 0; i < frame->ExtensionBlockCount; i++) {
ExtensionBlock* eb = &frame->ExtensionBlocks[i];
if (eb->Function == GRAPHICS_EXT_FUNC_CODE && eb->ByteCount >= 4) {
info.disposalMethod = (eb->Bytes[0] >> 2) & 0x07;
info.delay = (unsigned char)eb->Bytes[1] | ((unsigned char)eb->Bytes[2] << 8);
if (eb->Bytes[0] & 0x01) {
info.transparentIndex = (unsigned char)eb->Bytes[3];
}
}
}
return info;
}

void renderFrame(uint8_t* rgba, int canvasWidth, int canvasHeight,
SavedImage* frame, ColorMapObject* globalColorMap,
int transparentIndex) {
GifImageDesc* desc = &frame->ImageDesc;
ColorMapObject* colorMap = desc->ColorMap ? desc->ColorMap : globalColorMap;
if (!colorMap) return;

for (int y = 0; y < desc->Height; y++) {
int destY = desc->Top + y;
if (destY < 0 || destY >= canvasHeight) continue;
for (int x = 0; x < desc->Width; x++) {
int destX = desc->Left + x;
if (destX < 0 || destX >= canvasWidth) continue;

int colorIndex = frame->RasterBits[y * desc->Width + x];
if (colorIndex == transparentIndex) continue;
if (colorIndex >= colorMap->ColorCount) continue;

GifColorType color = colorMap->Colors[colorIndex];
size_t offset = ((size_t)destY * canvasWidth + destX) * 4;
rgba[offset] = color.Red;
rgba[offset + 1] = color.Green;
rgba[offset + 2] = color.Blue;
rgba[offset + 3] = 255;
}
}
}

void initCanvas(uint8_t* rgba, int width, int height, GifFileType* gif) {
size_t total = (size_t)width * height * 4;
ColorMapObject* colorMap = gif->SColorMap;
if (colorMap && gif->SBackGroundColor < colorMap->ColorCount) {
GifColorType bg = colorMap->Colors[gif->SBackGroundColor];
for (size_t i = 0; i < total; i += 4) {
rgba[i] = bg.Red;
rgba[i + 1] = bg.Green;
rgba[i + 2] = bg.Blue;
rgba[i + 3] = 255;
}
} else {
memset(rgba, 0, total);
}
}

void clearFrameArea(uint8_t* rgba, int canvasWidth, int canvasHeight,
SavedImage* frame, GifFileType* gif) {
GifImageDesc* desc = &frame->ImageDesc;
ColorMapObject* colorMap = gif->SColorMap;
for (int y = 0; y < desc->Height; y++) {
int destY = desc->Top + y;
if (destY < 0 || destY >= canvasHeight) continue;
for (int x = 0; x < desc->Width; x++) {
int destX = desc->Left + x;
if (destX < 0 || destX >= canvasWidth) continue;
size_t offset = ((size_t)destY * canvasWidth + destX) * 4;
if (colorMap && gif->SBackGroundColor < colorMap->ColorCount) {
GifColorType bg = colorMap->Colors[gif->SBackGroundColor];
rgba[offset] = bg.Red;
rgba[offset + 1] = bg.Green;
rgba[offset + 2] = bg.Blue;
rgba[offset + 3] = 255;
} else {
rgba[offset] = 0;
rgba[offset + 1] = 0;
rgba[offset + 2] = 0;
rgba[offset + 3] = 0;
}
}
}
}

val decode(std::string gifimage) {
MemoryReader reader = {
reinterpret_cast<const uint8_t*>(gifimage.c_str()),
gifimage.length(), 0
};

int error = 0;
GifFileType* gif = openGif(&reader, &error);
if (!gif || gif->ImageCount < 1) return val::null();

int width = gif->SWidth;
int height = gif->SHeight;
size_t total_bytes = (size_t)width * height * 4;

uint8_t* rgba = static_cast<uint8_t*>(malloc(total_bytes));
if (!rgba) { DGifCloseFile(gif, &error); return val::null(); }

initCanvas(rgba, width, height, gif);

FrameInfo info = getFrameInfo(&gif->SavedImages[0]);
renderFrame(rgba, width, height, &gif->SavedImages[0],
gif->SColorMap, info.transparentIndex);

val result = ImageData.new_(
Uint8ClampedArray.new_(typed_memory_view(total_bytes, rgba)),
width, height);

free(rgba);
DGifCloseFile(gif, &error);
return result;
}

val decodeAnimated(std::string gifimage) {
MemoryReader reader = {
reinterpret_cast<const uint8_t*>(gifimage.c_str()),
gifimage.length(), 0
};

int error = 0;
GifFileType* gif = openGif(&reader, &error);
if (!gif || gif->ImageCount < 1) return val::null();

int width = gif->SWidth;
int height = gif->SHeight;
size_t total_bytes = (size_t)width * height * 4;

uint8_t* canvas = static_cast<uint8_t*>(malloc(total_bytes));
uint8_t* prevCanvas = static_cast<uint8_t*>(malloc(total_bytes));
if (!canvas || !prevCanvas) {
free(canvas);
free(prevCanvas);
DGifCloseFile(gif, &error);
return val::null();
}

initCanvas(canvas, width, height, gif);

val frames = Array.new_();

for (int i = 0; i < gif->ImageCount; i++) {
SavedImage* frame = &gif->SavedImages[i];
FrameInfo info = getFrameInfo(frame);

// Save canvas state before rendering (for restore-to-previous disposal)
memcpy(prevCanvas, canvas, total_bytes);

renderFrame(canvas, width, height, frame, gif->SColorMap, info.transparentIndex);

// Default delay of 100ms if not specified or zero
int delayMs = info.delay > 0 ? info.delay * 10 : 100;

val imageData = ImageData.new_(
Uint8ClampedArray.new_(typed_memory_view(total_bytes, canvas)),
width, height);

val frameObj = Object.new_();
frameObj.set("imageData", imageData);
frameObj.set("duration", delayMs);

frames.call<void>("push", frameObj);

// Apply disposal method for next frame
switch (info.disposalMethod) {
case 2: // Restore to background
clearFrameArea(canvas, width, height, frame, gif);
break;
case 3: // Restore to previous
memcpy(canvas, prevCanvas, total_bytes);
break;
// 0, 1: no disposal / do not dispose — leave canvas as-is
}
}

free(canvas);
free(prevCanvas);
DGifCloseFile(gif, &error);
return frames;
}

bool isAnimated(std::string gifimage) {
MemoryReader reader = {
reinterpret_cast<const uint8_t*>(gifimage.c_str()),
gifimage.length(), 0
};

int error = 0;
GifFileType* gif = openGif(&reader, &error);
if (!gif) return false;

bool animated = gif->ImageCount > 1;
DGifCloseFile(gif, &error);
return animated;
}

EMSCRIPTEN_BINDINGS(my_module) {
function("decode", &decode);
function("decodeAnimated", &decodeAnimated);
function("isAnimated", &isAnimated);
}
14 changes: 14 additions & 0 deletions packages/gif/codec/dec/gif_dec.d.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
export interface GIFFrame {
imageData: ImageData;
duration: number;
}

export interface GIFModule extends EmscriptenWasm.Module {
decode(data: BufferSource): ImageData | null;
decodeAnimated(data: BufferSource): GIFFrame[] | null;
isAnimated(data: BufferSource): boolean;
}

declare var moduleFactory: EmscriptenWasm.ModuleFactory<GIFModule>;

export default moduleFactory;
15 changes: 15 additions & 0 deletions packages/gif/codec/dec/gif_dec.js

Large diffs are not rendered by default.

Binary file added packages/gif/codec/dec/gif_dec.wasm
Binary file not shown.
6 changes: 6 additions & 0 deletions packages/gif/codec/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
{
"scripts": {
"build": "EMSDK_VERSION=3.1.57 DEFAULT_CFLAGS='-Oz -flto' ../../../tools/build-cpp.sh"
},
"type": "module"
}
24 changes: 24 additions & 0 deletions packages/gif/codec/pre.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
const isServiceWorker = globalThis.ServiceWorkerGlobalScope !== undefined;
const isRunningInCloudFlareWorkers = isServiceWorker && typeof self !== 'undefined' && globalThis.caches && globalThis.caches.default !== undefined;
const isRunningInNode = typeof process === 'object' && process.release && process.release.name === 'node';

if (isRunningInCloudFlareWorkers || isRunningInNode) {
if (!globalThis.ImageData) {
// Simple Polyfill for ImageData Object
globalThis.ImageData = class ImageData {
constructor(data, width, height) {
this.data = data;
this.width = width;
this.height = height;
}
};
}

if (import.meta.url === undefined) {
import.meta.url = 'https://localhost';
}

if (typeof self !== 'undefined' && self.location === undefined) {
self.location = { href: '' };
}
}
Loading