diff --git a/server/plugin/plg_image_c/image_gif.c b/server/plugin/plg_image_c/image_gif.c new file mode 100644 index 000000000..d386ef664 --- /dev/null +++ b/server/plugin/plg_image_c/image_gif.c @@ -0,0 +1,204 @@ +#include +#include +#include +#include +#include +#include +#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; +} diff --git a/server/plugin/plg_image_c/image_gif.go b/server/plugin/plg_image_c/image_gif.go new file mode 100644 index 000000000..6b20e50bb --- /dev/null +++ b/server/plugin/plg_image_c/image_gif.go @@ -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 +} diff --git a/server/plugin/plg_image_c/image_gif.h b/server/plugin/plg_image_c/image_gif.h new file mode 100644 index 000000000..8d7fe8040 --- /dev/null +++ b/server/plugin/plg_image_c/image_gif.h @@ -0,0 +1,4 @@ +#include +#include + +int gif_to_webp(int inputDesc, int outputDesc, int targetSize); diff --git a/server/plugin/plg_image_c/image_gif_vendor.h b/server/plugin/plg_image_c/image_gif_vendor.h new file mode 100644 index 000000000..bdf3df280 --- /dev/null +++ b/server/plugin/plg_image_c/image_gif_vendor.h @@ -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;