Skip to content

Commit

Permalink
feature (thumbnail): better thumbnail plugin
Browse files Browse the repository at this point in the history
  • Loading branch information
mickael-kerjean committed Nov 9, 2023
1 parent 513ba65 commit 0ac2bde
Show file tree
Hide file tree
Showing 16 changed files with 557 additions and 247 deletions.
2 changes: 2 additions & 0 deletions config/mime.json
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,8 @@
"form": "application/x-form",
"gif": "image/gif",
"gz": "application/x-gzip",
"heic": "image/heic",
"heif": "image/heic",
"hqx": "application/mac-binhex40",
"htc": "text/x-component",
"htm": "text/html",
Expand Down
2 changes: 1 addition & 1 deletion server/plugin/index.go
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,7 @@ import (
_ "github.com/mickael-kerjean/filestash/server/plugin/plg_editor_onlyoffice"
_ "github.com/mickael-kerjean/filestash/server/plugin/plg_handler_console"
_ "github.com/mickael-kerjean/filestash/server/plugin/plg_image_ascii"
_ "github.com/mickael-kerjean/filestash/server/plugin/plg_image_thumbnail"
_ "github.com/mickael-kerjean/filestash/server/plugin/plg_image_c"
_ "github.com/mickael-kerjean/filestash/server/plugin/plg_image_transcode"
_ "github.com/mickael-kerjean/filestash/server/plugin/plg_search_stateless"
_ "github.com/mickael-kerjean/filestash/server/plugin/plg_security_scanner"
Expand Down
166 changes: 166 additions & 0 deletions server/plugin/plg_image_c/image_heif.c
Original file line number Diff line number Diff line change
@@ -0,0 +1,166 @@
#include <stdio.h>
#include <stdlib.h>
#include <libheif/heif.h>
#include <jpeglib.h>
#include <setjmp.h>
#include "utils.h"

#define JPEG_QUALITY 50

struct filestash_heicjpeg_error_mgr {
struct jpeg_error_mgr pub;
jmp_buf jmp;
};

typedef struct filestash_heicjpeg_error_mgr *filestash_heicjpeg_error_ptr;

void filestash_heicjpeg_error_exit (j_common_ptr cinfo) {
filestash_heicjpeg_error_ptr filestash_err = (filestash_heicjpeg_error_ptr) cinfo->err;
longjmp(filestash_err->jmp, 1);
}


// adapted and inspired from:
// https://github.com/strukturag/libheif/blob/master/examples/heif_thumbnailer.cc
int heif_to_jpeg(int inputDesc, int outputDesc, int targetSize) {
#ifdef HAS_DEBUG
clock_t t;
t = clock();
#endif
int status = 0;
FILE* input = fdopen(inputDesc, "rb");
FILE* output = fdopen(outputDesc, "wb");
if (!input || !output) {
return 1;
}

// STEP1: write input to a file as that's the only things libheif can open
char fname_in[32] = "/tmp/filestash.XXXXXX";
int _mkstemp_in = mkstemp(fname_in);
if (_mkstemp_in == -1) {
ERROR("mkstemp_in");
status = 1;
goto CLEANUP_AND_ABORT;
}
FILE* f_in = fdopen(_mkstemp_in, "w");
if (!f_in) {
ERROR("fdopen");
status = 1;
goto CLEANUP_AND_ABORT;
}
char content[1024 * 4];
int read;
while ((read = fread(content, sizeof(char), 1024*4, input))) {
fwrite(content, read, sizeof(char), f_in);
}
fclose(f_in);

// STEP2: decode heic
struct heif_context* ctx = heif_context_alloc();
struct heif_image_handle* handle = NULL;
struct heif_image* img = NULL;
struct heif_error error = {};
error = heif_context_read_from_file(ctx, fname_in, NULL);
if (error.code != heif_error_Ok) {
status = 1;
goto CLEANUP_AND_ABORT_A;
}
DEBUG("heic after read");
error = heif_context_get_primary_image_handle(ctx, &handle);
if (error.code != heif_error_Ok) {
status = 1;
goto CLEANUP_AND_ABORT_B;
}
if (targetSize < 0) {
heif_item_id thumbnail_ID;
int nThumbnails = heif_image_handle_get_list_of_thumbnail_IDs(handle, &thumbnail_ID, 1);
if (nThumbnails > 0) {
struct heif_image_handle* thumbnail_handle;
error = heif_image_handle_get_thumbnail(handle, thumbnail_ID, &thumbnail_handle);
if (error.code != heif_error_Ok) {
status = 1;
goto CLEANUP_AND_ABORT_B;
}
heif_image_handle_release(handle);
handle = thumbnail_handle;
}
}
DEBUG("heic after extract");
struct heif_decoding_options* decode_options = heif_decoding_options_alloc();
decode_options->convert_hdr_to_8bit = 1;
error = heif_decode_image(handle, &img, heif_colorspace_YCbCr, heif_chroma_420, decode_options);
heif_decoding_options_free(decode_options);
if (error.code != heif_error_Ok) {
status = 1;
goto CLEANUP_AND_ABORT_C;
}
DEBUG("heic after decode");
if (heif_image_get_bits_per_pixel(img, heif_channel_Y) != 8) {
status = 1;
goto CLEANUP_AND_ABORT_C;
}
DEBUG("heic after validation");

// STEP3: Create a jpeg
struct jpeg_compress_struct jpeg_config_output;
struct filestash_heicjpeg_error_mgr jerr;
int stride_y;
int stride_u;
int stride_v;
jpeg_create_compress(&jpeg_config_output);
jpeg_stdio_dest(&jpeg_config_output, output);

jpeg_config_output.image_width = heif_image_handle_get_width(handle);
jpeg_config_output.image_height = heif_image_handle_get_height(handle);
jpeg_config_output.input_components = 3;
jpeg_config_output.in_color_space = JCS_YCbCr;
jpeg_config_output.err = jpeg_std_error(&jerr.pub);
jpeg_set_defaults(&jpeg_config_output);
jpeg_set_quality(&jpeg_config_output, JPEG_QUALITY, TRUE);
if (setjmp(jerr.jmp)) {
ERROR("exception");
goto CLEANUP_AND_ABORT_D;
}

const uint8_t* row_y = heif_image_get_plane_readonly(img, heif_channel_Y, &stride_y);
const uint8_t* row_u = heif_image_get_plane_readonly(img, heif_channel_Cb, &stride_u);
const uint8_t* row_v = heif_image_get_plane_readonly(img, heif_channel_Cr, &stride_v);
int jpeg_row_stride = jpeg_config_output.image_width * jpeg_config_output.input_components;
jpeg_start_compress(&jpeg_config_output, TRUE);
jerr.pub.error_exit = filestash_heicjpeg_error_exit;
JSAMPARRAY buffer = jpeg_config_output.mem->alloc_sarray((j_common_ptr) &jpeg_config_output, JPOOL_IMAGE, jpeg_row_stride, 1);
DEBUG("jpeg initialised");
while (jpeg_config_output.next_scanline < jpeg_config_output.image_height) {
size_t offset_y = jpeg_config_output.next_scanline * stride_y;
const uint8_t* start_y = &row_y[offset_y];
size_t offset_u = (jpeg_config_output.next_scanline / 2) * stride_u;
const uint8_t* start_u = &row_u[offset_u];
size_t offset_v = (jpeg_config_output.next_scanline / 2) * stride_v;
const uint8_t* start_v = &row_v[offset_v];
JOCTET* bufp = buffer[0];
for (JDIMENSION x = 0; x < jpeg_config_output.image_width; ++x) {
*bufp++ = start_y[x];
*bufp++ = start_u[x / 2];
*bufp++ = start_v[x / 2];
}
jpeg_write_scanlines(&jpeg_config_output, buffer, 1);
}
jpeg_finish_compress(&jpeg_config_output);
DEBUG("jpeg cleanup");

CLEANUP_AND_ABORT_D:
jpeg_destroy_compress(&jpeg_config_output);

CLEANUP_AND_ABORT_C:
heif_image_release(img);

CLEANUP_AND_ABORT_B:
heif_image_handle_release(handle);

CLEANUP_AND_ABORT_A:
heif_context_free(ctx);

CLEANUP_AND_ABORT:
remove(fname_in);
return status;
}
10 changes: 10 additions & 0 deletions server/plugin/plg_image_c/image_heif.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
package plg_image_c

// #include "image_heif.h"
// #cgo LDFLAGS: -lheif
import "C"

func heif(input uintptr, output uintptr, size int) {
C.heif_to_jpeg(C.int(input), C.int(output), C.int(size))
return
}
1 change: 1 addition & 0 deletions server/plugin/plg_image_c/image_heif.h
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
int heif_to_jpeg(int input, int output, int targetSize);
93 changes: 45 additions & 48 deletions server/plugin/plg_image_c/image_jpeg.c
Original file line number Diff line number Diff line change
@@ -1,35 +1,38 @@
#include <stdio.h>
#include "utils.h"
#include "jpeglib.h"
#include <jpeglib.h>
#include <setjmp.h>

#include <stdlib.h>
#include "utils.h"

#define JPEG_QUALITY 50

struct filestash_error_mgr {
struct filestash_jpeg_error_mgr {
struct jpeg_error_mgr pub;
jmp_buf jmp;
};

typedef struct filestash_error_mgr *filestash_error_ptr;
typedef struct filestash_jpeg_error_mgr *filestash_jpeg_error_ptr;

void my_error_exit (j_common_ptr cinfo) {
filestash_error_ptr filestash_err = (filestash_error_ptr) cinfo->err;
void filestash_jpeg_error_exit (j_common_ptr cinfo) {
filestash_jpeg_error_ptr filestash_err = (filestash_jpeg_error_ptr) cinfo->err;
longjmp(filestash_err->jmp, 1);
}

int jpeg_to_jpeg(FILE* input, FILE* output, int targetSize) {
int jpeg_to_jpeg(int inputDesc, int outputDesc, int targetSize) {
#ifdef HAS_DEBUG
clock_t t;
t = clock();
#endif
int status = 0;
FILE* input = fdopen(inputDesc, "r");
FILE* output = fdopen(outputDesc, "w");
if (!input || !output) {
return 1;
}

struct jpeg_decompress_struct jpeg_config_input;
struct jpeg_compress_struct jpeg_config_output;
struct filestash_error_mgr jerr;
int jpeg_row_stride;
int image_min_size;
JSAMPARRAY buffer;
struct filestash_jpeg_error_mgr jerr;

jpeg_config_input.err = jpeg_std_error(&jerr.pub);
jpeg_config_output.err = jpeg_std_error(&jerr.pub);
Expand All @@ -43,16 +46,17 @@ int jpeg_to_jpeg(FILE* input, FILE* output, int targetSize) {
jpeg_stdio_src(&jpeg_config_input, input);
jpeg_stdio_dest(&jpeg_config_output, output);

jerr.pub.error_exit = my_error_exit;
jerr.pub.error_exit = filestash_jpeg_error_exit;
if (setjmp(jerr.jmp)) {
jpeg_destroy_decompress(&jpeg_config_input);
return 0;
ERROR("exception");
goto CLEANUP_AND_ABORT;
}

DEBUG("after constructor decompress");
if(jpeg_read_header(&jpeg_config_input, TRUE) != JPEG_HEADER_OK) {
jpeg_destroy_decompress(&jpeg_config_input);
return 1;
status = 1;
ERROR("not a jpeg");
goto CLEANUP_AND_ABORT;
}
DEBUG("after header read");
jpeg_config_input.dct_method = JDCT_IFAST;
Expand All @@ -61,73 +65,66 @@ int jpeg_to_jpeg(FILE* input, FILE* output, int targetSize) {
jpeg_config_input.dither_mode = JDITHER_ORDERED;
jpeg_calc_output_dimensions(&jpeg_config_input);

image_min_size = min(jpeg_config_input.output_width, jpeg_config_input.output_height);
int image_min_size = min(jpeg_config_input.output_width, jpeg_config_input.output_height);
jpeg_config_input.scale_num = 1;
jpeg_config_input.scale_denom = 1;
if (image_min_size / 8 >= targetSize) {
int targetSizeAbs = abs(targetSize);
if (image_min_size / 8 >= targetSizeAbs) {
jpeg_config_input.scale_num = 1;
jpeg_config_input.scale_denom = 8;
} else if (image_min_size * 2 / 8 >= targetSize) {
} else if (image_min_size * 2 / 8 >= targetSizeAbs) {
jpeg_config_input.scale_num = 1;
jpeg_config_input.scale_denom = 4;
} else if (image_min_size * 3 / 8 >= targetSize) {
} else if (image_min_size * 3 / 8 >= targetSizeAbs) {
jpeg_config_input.scale_num = 3;
jpeg_config_input.scale_denom = 8;
} else if (image_min_size * 4 / 8 >= targetSize) {
} else if (image_min_size * 4 / 8 >= targetSizeAbs) {
jpeg_config_input.scale_num = 4;
jpeg_config_input.scale_denom = 8;
} else if (image_min_size * 5 / 8 >= targetSize) {
} else if (image_min_size * 5 / 8 >= targetSizeAbs) {
jpeg_config_input.scale_num = 5;
jpeg_config_input.scale_denom = 8;
} else if (image_min_size * 6 / 8 >= targetSize) {
} else if (image_min_size * 6 / 8 >= targetSizeAbs) {
jpeg_config_input.scale_num = 6;
jpeg_config_input.scale_denom = 8;
} else if (image_min_size * 7 / 8 >= targetSize) {
} else if (image_min_size * 7 / 8 >= targetSizeAbs) {
jpeg_config_input.scale_num = 7;
jpeg_config_input.scale_denom = 8;
}

DEBUG("start decompress");
if(jpeg_start_decompress(&jpeg_config_input) == FALSE) {
jpeg_destroy_decompress(&jpeg_config_input);
return 1;
ERROR("jpeg_start_decompress");
status = 1;
goto CLEANUP_AND_ABORT;
}
DEBUG("processing image setup");
jpeg_row_stride = jpeg_config_input.output_width * jpeg_config_input.output_components;
int jpeg_row_stride = jpeg_config_input.output_width * jpeg_config_input.output_components;
jpeg_config_output.image_width = jpeg_config_input.output_width;
jpeg_config_output.image_height = jpeg_config_input.output_height;
jpeg_config_output.input_components = jpeg_config_input.num_components;
jpeg_config_output.in_color_space = JCS_RGB;
jpeg_set_defaults(&jpeg_config_output);
jpeg_set_quality(&jpeg_config_output, JPEG_QUALITY, TRUE);
jpeg_start_compress(&jpeg_config_output, TRUE);
buffer = (*jpeg_config_input.mem->alloc_sarray) ((j_common_ptr) &jpeg_config_input, JPOOL_IMAGE, jpeg_row_stride, 1);
JSAMPARRAY buffer = jpeg_config_input.mem->alloc_sarray((j_common_ptr) &jpeg_config_input, JPOOL_IMAGE, jpeg_row_stride, 1);

DEBUG("processing image");
while (jpeg_config_input.output_scanline < jpeg_config_input.output_height) {
while (jpeg_config_output.next_scanline < jpeg_config_output.image_height) {
jpeg_read_scanlines(&jpeg_config_input, buffer, 1);
jpeg_write_scanlines(&jpeg_config_output, buffer, 1);
}

DEBUG("end decompress");
jpeg_finish_decompress(&jpeg_config_input);
jpeg_destroy_decompress(&jpeg_config_input);
DEBUG("finish decompress");
jpeg_finish_compress(&jpeg_config_output);
DEBUG("final");
return 0;
}

void jpeg_size(FILE* infile, int* height, int* width) {
struct jpeg_decompress_struct cinfo;
struct jpeg_error_mgr jerr;
cinfo.err = jpeg_std_error(&jerr);

jpeg_create_decompress(&cinfo);
jpeg_stdio_src(&cinfo, infile);
jpeg_read_header(&cinfo, TRUE);
jpeg_start_decompress(&cinfo);

*width = cinfo.image_width;
*height = cinfo.image_height;

jpeg_destroy_decompress(&cinfo);
CLEANUP_AND_ABORT:
jpeg_destroy_decompress(&jpeg_config_input);
jpeg_destroy_compress(&jpeg_config_output);
fclose(input);
fclose(output);
DEBUG("final");
return status;
}
Loading

0 comments on commit 0ac2bde

Please sign in to comment.