Skip to content

Commit

Permalink
feature (gif): a gif thumbnailer made in C
Browse files Browse the repository at this point in the history
  • Loading branch information
mickael-kerjean committed Nov 13, 2023
1 parent ade354f commit e72d4a2
Show file tree
Hide file tree
Showing 4 changed files with 226 additions and 0 deletions.
204 changes: 204 additions & 0 deletions server/plugin/plg_image_c/image_gif.c
Original file line number Diff line number Diff line change
@@ -0,0 +1,204 @@
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <gif_lib.h>
#include <webp/encode.h>
#include <unistd.h>
#include "utils.h"
#include "image_gif_vendor.h"

int DGifSlurp2(GifFileType *GifFile);
int DGifCloseFile2(GifFileType *GifFile, int *ErrorCode);

int gif_to_webp(int inputDesc, int outputDesc, int targetSize) {
#ifdef HAS_DEBUG
clock_t t;
t = clock();
#endif
int status = 0;
int error;
if (targetSize < 0) targetSize = -targetSize;

// STEP1: setup gif
GifFileType* gif;
uint8_t* gif_rgba;
uint8_t* scaled_rgba;
if((gif = DGifOpenFileHandle(inputDesc, &error)) == NULL) {
status = 1;
goto CLEANUP_AND_ABORT;
}
DEBUG("after gif opened");
if (DGifSlurp2(gif) != GIF_OK) {
status = 1;
goto CLEANUP_AND_ABORT_A;
}
int width = gif->SWidth;
int height = gif->SHeight;
int scale_factor = (width > targetSize) ? width / targetSize : 1;
int thumb_width = width / scale_factor;
int thumb_height = height / scale_factor;
DEBUG("after gif ready");

// STEP2: convert frame to RGBA
if (gif->ImageCount == 0) {
status = 1;
goto CLEANUP_AND_ABORT_A;
} else if (!(gif_rgba = (uint8_t*)malloc(width * height * 4))) {
status = 1;
goto CLEANUP_AND_ABORT_A;
}
GifColorType* colorMapEntry;
ColorMapObject* colorMap = (gif->Image.ColorMap) ? gif->Image.ColorMap : gif->SColorMap;
SavedImage* firstFrame = &gif->SavedImages[0];
GifByteType* gifBytes = firstFrame->RasterBits;
for (int i = 0; i < gif->SWidth * gif->SHeight; ++i) {
colorMapEntry = &colorMap->Colors[gifBytes[i]];
gif_rgba[i * 4 + 0] = colorMapEntry->Red;
gif_rgba[i * 4 + 1] = colorMapEntry->Green;
gif_rgba[i * 4 + 2] = colorMapEntry->Blue;
gif_rgba[i * 4 + 3] = 0xFF;
}
DEBUG("after gif rgba convert");

// STEP3: scale the image
if (!(scaled_rgba = (uint8_t*)malloc(thumb_width * thumb_height * 4))) {
free(gif_rgba);
status = 1;
goto CLEANUP_AND_ABORT_A;
}
int x, y, srcIndex, destIndex;
for (int i = 0; i < thumb_height; ++i) {
for (int j = 0; j < thumb_width; ++j) {
x = j * width / thumb_width;
y = i * height / thumb_height;
srcIndex = (y * width + x) * 4;
destIndex = (i * thumb_width + j) * 4;
memcpy(&scaled_rgba[destIndex], &gif_rgba[srcIndex], 4);
}
}
free(gif_rgba);
DEBUG("after image scaled");

// STEP4: write image as webp
uint8_t* webp_output_data;
size_t webp_output_size = WebPEncodeRGBA(scaled_rgba, thumb_width, thumb_height, thumb_width * 4, 75, &webp_output_data);
free(scaled_rgba);
if (webp_output_size == 0) {
status = 1;
goto CLEANUP_AND_ABORT_A;
}
if (write(outputDesc, webp_output_data, webp_output_size) != webp_output_size) {
status = 1;
ERROR("unexpected number of bytes written");
}
WebPFree(webp_output_data);
DEBUG("after webp written");

CLEANUP_AND_ABORT_A:
DGifCloseFile2(gif, &error);

CLEANUP_AND_ABORT:
return status;
}

// adapted from https://android.googlesource.com/platform/external/giflib/+/dc07290edccc2c3fc4062da835306f809cea1fdc/dgif_lib.c
// we got rid of unecessary stuff for our use case and reduce the processing
// to the first frame only which isn't possible using stock libgif functions
int DGifSlurp2(GifFileType *GifFile) {
clock_t t = clock();
size_t ImageSize;
GifRecordType RecordType;
SavedImage *sp;
GifByteType *ExtData;
int ExtFunction;
GifFile->ExtensionBlocks = NULL;
GifFile->ExtensionBlockCount = 0;
do {
if (DGifGetRecordType(GifFile, &RecordType) == GIF_ERROR) {
return GIF_ERROR;
}

if (RecordType == IMAGE_DESC_RECORD_TYPE) {
if (DGifGetImageDesc(GifFile) == GIF_ERROR) {
return GIF_ERROR;
}

sp = &GifFile->SavedImages[GifFile->ImageCount - 1];
if (sp->ImageDesc.Width < 0 && sp->ImageDesc.Height < 0 && sp->ImageDesc.Width > (INT_MAX / sp->ImageDesc.Height)) {
return GIF_ERROR;
}
ImageSize = sp->ImageDesc.Width * sp->ImageDesc.Height;
if (ImageSize > (SIZE_MAX / sizeof(GifPixelType))) {
return GIF_ERROR;
}
sp->RasterBits = (unsigned char *)reallocarray(NULL, ImageSize, sizeof(GifPixelType));
if (sp->RasterBits == NULL) {
return GIF_ERROR;
}
if (DGifGetLine(GifFile, sp->RasterBits, ImageSize) == GIF_ERROR) {
return GIF_ERROR;
}
return GIF_OK;
} else if (RecordType == EXTENSION_RECORD_TYPE) {
if (DGifGetExtension(GifFile, &ExtFunction, &ExtData) == GIF_ERROR) {
return GIF_ERROR;
}
while (ExtData != NULL) {
if (DGifGetExtensionNext(GifFile, &ExtData) == GIF_ERROR) {
return GIF_ERROR;
}
}
}
} while (RecordType != TERMINATE_RECORD_TYPE);

return GIF_OK;
}


// adapted from: https://android.googlesource.com/platform/external/giflib/+/dc07290edccc2c3fc4062da835306f809cea1fdc/dgif_lib.c#626
// as we don't want libgif to manage the lifecycle of the file descriptor, in our case
// this is the responsibility of the downstream program, that's why we've recopied it here
// a commented the fclose call
int DGifCloseFile2(GifFileType *GifFile, int *ErrorCode)
{
GifFilePrivateType *Private;
if (GifFile == NULL || GifFile->Private == NULL) {
return GIF_ERROR;
}
if (GifFile->Image.ColorMap) {
GifFreeMapObject(GifFile->Image.ColorMap);
GifFile->Image.ColorMap = NULL;
}
if (GifFile->SColorMap) {
GifFreeMapObject(GifFile->SColorMap);
GifFile->SColorMap = NULL;
}
if (GifFile->SavedImages) {
GifFreeSavedImages(GifFile);
GifFile->SavedImages = NULL;
}
GifFreeExtensions(&GifFile->ExtensionBlockCount, &GifFile->ExtensionBlocks);
Private = (GifFilePrivateType *) GifFile->Private;
if (!IS_READABLE(Private)) {
if (ErrorCode != NULL) {
*ErrorCode = D_GIF_ERR_NOT_READABLE;
}
free((char *)GifFile->Private);
free(GifFile);
return GIF_ERROR;
}
if (Private->File /*&& (fclose(Private->File) != 0)*/) {
if (ErrorCode != NULL) {
*ErrorCode = D_GIF_ERR_CLOSE_FAILED;
}
free((char *)GifFile->Private);
free(GifFile);
return GIF_ERROR;
}
free((char *)GifFile->Private);
free(GifFile);
if (ErrorCode != NULL) {
*ErrorCode = D_GIF_SUCCEEDED;
}
return GIF_OK;
}
10 changes: 10 additions & 0 deletions server/plugin/plg_image_c/image_gif.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
package plg_image_c

// #include "image_gif.h"
// #cgo LDFLAGS: -l:libgif.a -l:libwebp.a
import "C"

func gif(input uintptr, output uintptr, size int) {
C.gif_to_webp(C.int(input), C.int(output), C.int(size))
return
}
4 changes: 4 additions & 0 deletions server/plugin/plg_image_c/image_gif.h
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
#include <stdio.h>
#include <stdlib.h>

int gif_to_webp(int inputDesc, int outputDesc, int targetSize);
8 changes: 8 additions & 0 deletions server/plugin/plg_image_c/image_gif_vendor.h
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
#define FILE_STATE_READ 0x08
#define IS_READABLE(Private) (Private->FileState & FILE_STATE_READ)
#define INT_MAX 2147483647

typedef struct GifFilePrivateType {
GifWord FileState;
FILE *File;
} GifFilePrivateType;

0 comments on commit e72d4a2

Please sign in to comment.