diff --git a/config/mime.json b/config/mime.json index 192dbe1d4..e73f075fa 100644 --- a/config/mime.json +++ b/config/mime.json @@ -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", diff --git a/server/plugin/index.go b/server/plugin/index.go index b964ad856..893bf9674 100644 --- a/server/plugin/index.go +++ b/server/plugin/index.go @@ -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" diff --git a/server/plugin/plg_image_c/image_heif.c b/server/plugin/plg_image_c/image_heif.c new file mode 100644 index 000000000..f5f9f9ad8 --- /dev/null +++ b/server/plugin/plg_image_c/image_heif.c @@ -0,0 +1,166 @@ +#include +#include +#include +#include +#include +#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; +} diff --git a/server/plugin/plg_image_c/image_heif.go b/server/plugin/plg_image_c/image_heif.go new file mode 100644 index 000000000..95b6bced3 --- /dev/null +++ b/server/plugin/plg_image_c/image_heif.go @@ -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 +} diff --git a/server/plugin/plg_image_c/image_heif.h b/server/plugin/plg_image_c/image_heif.h new file mode 100644 index 000000000..30623fabb --- /dev/null +++ b/server/plugin/plg_image_c/image_heif.h @@ -0,0 +1 @@ +int heif_to_jpeg(int input, int output, int targetSize); diff --git a/server/plugin/plg_image_c/image_jpeg.c b/server/plugin/plg_image_c/image_jpeg.c index 7f6b268e2..5df34032f 100644 --- a/server/plugin/plg_image_c/image_jpeg.c +++ b/server/plugin/plg_image_c/image_jpeg.c @@ -1,35 +1,38 @@ #include -#include "utils.h" -#include "jpeglib.h" +#include #include - +#include +#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); @@ -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; @@ -61,39 +65,41 @@ 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; @@ -101,33 +107,24 @@ int jpeg_to_jpeg(FILE* input, FILE* output, int targetSize) { 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; } diff --git a/server/plugin/plg_image_c/image_jpeg.go b/server/plugin/plg_image_c/image_jpeg.go index 0e9d3fd3e..235e4139a 100644 --- a/server/plugin/plg_image_c/image_jpeg.go +++ b/server/plugin/plg_image_c/image_jpeg.go @@ -4,36 +4,7 @@ package plg_image_c // #cgo LDFLAGS: -l:libjpeg.a import "C" -import ( - "fmt" - "io" - "os" -) - -func JpegToJpeg(input io.ReadCloser) (io.ReadCloser, error) { - read, write, err := os.Pipe() - if err != nil { - return nil, err - } - - go func() { - cRead, cWrite, err := os.Pipe() - if err != nil { - fmt.Printf("ERR %+v\n", err) - } - go func() { - defer cWrite.Close() - io.Copy(cWrite, input) - }() - cInput := C.fdopen(C.int(cRead.Fd()), C.CString("r")) - cOutput := C.fdopen(C.int(write.Fd()), C.CString("w")) - - C.jpeg_to_jpeg(cInput, cOutput, 200) - - cWrite.Close() - write.Close() - cRead.Close() - }() - - return read, nil +func jpeg(input uintptr, output uintptr, size int) { + C.jpeg_to_jpeg(C.int(input), C.int(output), C.int(size)) + return } diff --git a/server/plugin/plg_image_c/image_jpeg.h b/server/plugin/plg_image_c/image_jpeg.h index b498557ef..cc48cf78d 100644 --- a/server/plugin/plg_image_c/image_jpeg.h +++ b/server/plugin/plg_image_c/image_jpeg.h @@ -1,7 +1,3 @@ #include -#include "jpeglib.h" -#include "utils.h" -void jpeg_size(FILE* infile, int* height, int* width); - -int jpeg_to_jpeg(FILE* input, FILE* output, int targetSize); +int jpeg_to_jpeg(int input, int output, int targetSize); diff --git a/server/plugin/plg_image_c/image_png.c b/server/plugin/plg_image_c/image_png.c index e8eb375d0..caf9165af 100644 --- a/server/plugin/plg_image_c/image_png.c +++ b/server/plugin/plg_image_c/image_png.c @@ -1,143 +1,131 @@ +#include +#include #include #include -#include -#include "webp/encode.h" +#include #include "utils.h" -static int MyWriter(const uint8_t* data, size_t data_size, const WebPPicture* const pic) { - FILE* const out = (FILE*)pic->custom_ptr; - return data_size ? (fwrite(data, data_size, 1, out) == 1) : 1; +void png_read_error(png_structp png_ptr, png_const_charp error_msg) { + longjmp(png_jmpbuf(png_ptr), 1); } -int png_to_webp(FILE* input, FILE* output, int targetSize) { - WebPPicture picture; +void png_read_warning(png_structp png_ptr, png_const_charp warning_msg) { + longjmp(png_jmpbuf(png_ptr), 1); +} +int png_to_webp(int inputDesc, int outputDesc, int targetSize) { #ifdef HAS_DEBUG clock_t t; t = clock(); #endif - png_image image; - memset(&image, 0, sizeof image); - image.version = PNG_IMAGE_VERSION; - DEBUG("reading png"); - if (!png_image_begin_read_from_stdio(&image, input)) { - ERROR("png_image_begin_read_from_stdio"); - return 1; + if (targetSize < 0 ) { + targetSize = -targetSize; } - DEBUG("allocate"); - png_bytep buffer; - image.format = PNG_FORMAT_RGBA; - buffer = malloc(PNG_IMAGE_SIZE(image)); - if (buffer == NULL) { - ERROR("png_malloc"); - png_image_free(&image); - return 1; - } - DEBUG("start reading"); - if (!png_image_finish_read(&image, NULL, buffer, 0, NULL)) { - ERROR("png_image_finish_read"); - png_image_free(&image); - free(buffer); + int status = 0; + FILE* input = fdopen(inputDesc, "rb"); + FILE* output = fdopen(outputDesc, "wb"); + if (!input || !output) { return 1; } - ///////////////////////////////////////////// - // encode to webp - DEBUG("start encoding"); - if (!WebPPictureInit(&picture)) { - ERROR("WebPPictureInit"); - png_image_free(&image); - free(buffer); - return 1; + // STEP1: setup png + png_structp png_ptr = NULL; + png_infop info_ptr = NULL; + if(!(png_ptr = png_create_read_struct(PNG_LIBPNG_VER_STRING, NULL, png_read_error, png_read_warning))) { + status = 1; + goto CLEANUP_AND_ABORT; } - picture.width = image.width; - picture.height = image.height; - if(!WebPPictureAlloc(&picture)) { - ERROR("WebPPictureAlloc"); - png_image_free(&image); - free(buffer); - return 1; + if (!(info_ptr = png_create_info_struct(png_ptr))) { + status = 1; + goto CLEANUP_AND_ABORT_A; } - DEBUG("start encoding import"); - WebPPictureImportRGBA(&picture, buffer, PNG_IMAGE_ROW_STRIDE(image)); - png_image_free(&image); - free(buffer); - - WebPConfig webp_config_output; - picture.writer = MyWriter; - picture.custom_ptr = output; - DEBUG("start encoding config init"); - if (!WebPConfigInit(&webp_config_output)) { - ERROR("ERR config init"); - WebPPictureFree(&picture); - return 1; + if (setjmp(png_jmpbuf(png_ptr))) { + status = 1; + goto CLEANUP_AND_ABORT_B; } - webp_config_output.method = 0; - webp_config_output.quality = 30; - if (!WebPValidateConfig(&webp_config_output)) { - ERROR("ERR WEB VALIDATION"); - WebPPictureFree(&picture); - return 1; + png_init_io(png_ptr, input); + png_read_info(png_ptr, info_ptr); + png_uint_32 width = png_get_image_width(png_ptr, info_ptr); + png_uint_32 height = png_get_image_height(png_ptr, info_ptr); + png_byte color_type = png_get_color_type(png_ptr, info_ptr); + png_byte bit_depth = png_get_bit_depth(png_ptr, info_ptr); + if (color_type == PNG_COLOR_TYPE_PALETTE) { + png_set_palette_to_rgb(png_ptr); } - DEBUG("rescale start"); - if (image.width > targetSize && image.height > targetSize) { - float ratioHeight = (float) image.height / targetSize; - float ratioWidth = (float) image.width / targetSize; - float ratio = ratioWidth > ratioHeight ? ratioHeight : ratioWidth; - if (!WebPPictureRescale(&picture, image.width / ratio, image.height / ratio)) { - DEBUG("ERR Rescale"); - WebPPictureFree(&picture); - return 1; - } + if (color_type == PNG_COLOR_TYPE_GRAY && bit_depth < 8) { + png_set_expand_gray_1_2_4_to_8(png_ptr); } - DEBUG("encoder start"); - WebPEncode(&webp_config_output, &picture); - DEBUG("encoder done"); - WebPPictureFree(&picture); - DEBUG("cleaning up"); - return 0; -} + if (bit_depth == 16) { + png_set_strip_16(png_ptr); + } + png_read_update_info(png_ptr, info_ptr); + DEBUG("after png construct"); -int png_to_png(FILE* input, FILE* output, int targetSize) { -#ifdef HAS_DEBUG - clock_t t; - t = clock(); -#endif - png_image image; - memset(&image, 0, sizeof image); - image.version = PNG_IMAGE_VERSION; - DEBUG("> reading png"); - if (!png_image_begin_read_from_stdio(&image, input)) { - DEBUG("png_image_begin_read_from_stdio"); - return 1; + // STEP2: process the image + int scale_factor = width > targetSize ? width / targetSize : 1; + png_uint_32 thumb_width = width / scale_factor; + png_uint_32 thumb_height = height / scale_factor; + if (thumb_width == 0 || thumb_height == 0) { + ERROR("0 dimensions"); + status = 1; + goto CLEANUP_AND_ABORT_B; } - DEBUG("> allocate"); - png_bytep buffer; - image.format = PNG_FORMAT_RGBA; - buffer = malloc(PNG_IMAGE_SIZE(image)); - if (buffer == NULL) { - DEBUG("png_malloc"); - png_image_free(&image); - return 1; + uint8_t* webp_image_data = (uint8_t*)malloc(thumb_width * thumb_height * 4); + if (!webp_image_data) { + ERROR("malloc error"); + status = 1; + goto CLEANUP_AND_ABORT_B; } - DEBUG("> start reading"); - if (!png_image_finish_read(&image, NULL, buffer, 0, NULL)) { - DEBUG("png_image_finish_read"); - png_image_free(&image); - free(buffer); - return 1; + png_bytep row = (png_bytep)malloc(png_get_rowbytes(png_ptr, info_ptr)); + if (!row) { + ERROR("malloc error"); + status = 1; + goto CLEANUP_AND_ABORT_B; } + DEBUG("after png malloc"); + for (png_uint_32 y = 0; y < height; y++) { + png_read_row(png_ptr, row, NULL); + if (y % scale_factor == 0 && (y / scale_factor < thumb_height)) { + for (png_uint_32 x = 0; x < width; x += scale_factor) { + if (x / scale_factor < thumb_width) { + png_uint_32 thumb_x = x / scale_factor; + png_uint_32 thumb_y = y / scale_factor; + memcpy(webp_image_data + (thumb_y * thumb_width + thumb_x) * 4, row + x * 4, 4); + } + } + } + } + DEBUG("after png process"); + free(row); + png_destroy_read_struct(&png_ptr, &info_ptr, NULL); + DEBUG("after png cleanup"); - DEBUG("> write"); - if (!png_image_write_to_stdio(&image, output, 0, buffer, 0, NULL)) { - DEBUG("png_image_write_to_stdio"); - png_image_free(&image); - free(buffer); - return 1; + // STEP3: save as webp + uint8_t* webp_output_data = NULL; + size_t webp_output_size = WebPEncodeRGBA(webp_image_data, thumb_width, thumb_height, thumb_width * 4, 75, &webp_output_data); + free(webp_image_data); + DEBUG("after webp init"); + if (webp_output_data == NULL) { + status = 1; + goto CLEANUP_AND_ABORT_B; + } else if (webp_output_size == 0) { + status = 1; + goto CLEANUP_AND_ABORT_C; } + fwrite(webp_output_data, webp_output_size, 1, output); + DEBUG("after webp written"); + + CLEANUP_AND_ABORT_C: + if (webp_output_data != NULL) WebPFree(webp_output_data); + + CLEANUP_AND_ABORT_B: + if (info_ptr != NULL) png_free_data(png_ptr, info_ptr, PNG_FREE_ALL, -1); + + CLEANUP_AND_ABORT_A: + if (png_ptr != NULL) png_destroy_read_struct(&png_ptr, (info_ptr != NULL) ? &info_ptr : NULL, NULL); - DEBUG("> end"); - png_image_free(&image); - free(buffer); - return 0; + CLEANUP_AND_ABORT: + fclose(output); + fclose(input); + return status; } diff --git a/server/plugin/plg_image_c/image_png.go b/server/plugin/plg_image_c/image_png.go index 5dd32cadb..d6854d1dd 100644 --- a/server/plugin/plg_image_c/image_png.go +++ b/server/plugin/plg_image_c/image_png.go @@ -1,41 +1,10 @@ package plg_image_c // #include "image_png.h" -// #cgo LDFLAGS: -l:libpng.a -l:libz.a -l:libwebp.a -lpthread -lm +// #cgo LDFLAGS: -l:libpng.a -l:libz.a -l:libwebp.a -l:libpthread.a -fopenmp import "C" -import ( - "fmt" - "io" - "os" -) - -func PngToWebp(input io.ReadCloser) (io.ReadCloser, error) { - read, write, err := os.Pipe() - if err != nil { - fmt.Printf("OS PIPE ERR %+v\n", err) - return nil, err - } - - go func() { - cRead, cWrite, err := os.Pipe() - if err != nil { - fmt.Printf("ERR %+v\n", err) - return - } - go func() { - defer cWrite.Close() - io.Copy(cWrite, input) - }() - cInput := C.fdopen(C.int(cRead.Fd()), C.CString("r")) - cOutput := C.fdopen(C.int(write.Fd()), C.CString("w")) - - C.png_to_webp(cInput, cOutput, 300) - - cWrite.Close() - write.Close() - cRead.Close() - }() - - return read, nil +func png(input uintptr, output uintptr, size int) { + C.png_to_webp(C.int(input), C.int(output), C.int(size)) + return } diff --git a/server/plugin/plg_image_c/image_png.h b/server/plugin/plg_image_c/image_png.h index c98f319b8..aa17b4187 100644 --- a/server/plugin/plg_image_c/image_png.h +++ b/server/plugin/plg_image_c/image_png.h @@ -1,6 +1,6 @@ #include #include -int png_to_webp(FILE* input, FILE* output, int targetSize); +int png_to_png(int input, int output, int targetSize); -int png_to_png(FILE* input, FILE* output, int targetSize); +int png_to_webp(int input, int output, int targetSize); diff --git a/server/plugin/plg_image_c/image_raw.c b/server/plugin/plg_image_c/image_raw.c new file mode 100644 index 000000000..8abca830b --- /dev/null +++ b/server/plugin/plg_image_c/image_raw.c @@ -0,0 +1,93 @@ +#include +#include +#include +#include "utils.h" +#include "image_jpeg.h" + +#define BUF_SIZE 1024 * 8 + +int raw_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"); + + // STEP1: write input to a file as that's the only things libraw can open + char fname_in[32] = "/tmp/filestash.XXXXXX"; + int _mkstemp_in = mkstemp(fname_in); + if (!_mkstemp_in) { + ERROR("mkstemp_in"); + status = 1; + goto CLEANUP_AND_ABORT_A; + } + FILE* f_in = fdopen(_mkstemp_in, "w"); + if (!f_in) { + ERROR("fdopen"); + status = 1; + goto CLEANUP_AND_ABORT_B; + } + char content[BUF_SIZE]; + int read; + while ((read = fread(content, sizeof(char), BUF_SIZE, input))) { + fwrite(content, read, sizeof(char), f_in); + } + fclose(f_in); + + // STEP2: attempt at reading the raw file + DEBUG("libraw init"); + libraw_data_t *raw = libraw_init(0); + DEBUG("libraw open file"); + if (libraw_open_file(raw, fname_in)) { + ERROR("libraw_open_file"); + status = 1; + goto CLEANUP_AND_ABORT_C; + } + + // STEP3: prepare target + raw->params.output_tiff = 1; + DEBUG("libraw unpack thumb"); + char fname_out[32] = "/tmp/filestash.XXXXXX"; + int _mkstemp_out = mkstemp(fname_out); + if (!_mkstemp_out) { + ERROR("mkstemp_out"); + status = 1; + goto CLEANUP_AND_ABORT_C; + } + + // STEP4: attempt at extracting our image + if (!libraw_unpack_thumb(raw) && raw->thumbnail.tformat == LIBRAW_THUMBNAIL_JPEG) { + DEBUG("has an embed thumbnail"); + if (libraw_dcraw_thumb_writer(raw, fname_out)) { + ERROR("thumb_writer"); + status = 1; + goto CLEANUP_AND_ABORT_D; + } + DEBUG("process thumbnail"); + + FILE* f_out = fdopen(_mkstemp_out, "r"); + if (jpeg_to_jpeg(fileno(f_out), fileno(output), targetSize)) { + ERROR("jpeg_to_jpeg"); + status = 1; + fclose(f_out); + goto CLEANUP_AND_ABORT_D; + } + DEBUG("process complete"); + fclose(f_out); + goto CLEANUP_AND_ABORT_D; + } + + status = 1; + ERROR("not implemented - abort"); + + CLEANUP_AND_ABORT_D: + remove(fname_out); + CLEANUP_AND_ABORT_C: + libraw_close(raw); + CLEANUP_AND_ABORT_B: + remove(fname_in); + CLEANUP_AND_ABORT_A: + return status; +} diff --git a/server/plugin/plg_image_c/image_raw.go b/server/plugin/plg_image_c/image_raw.go new file mode 100644 index 000000000..e9a0774fe --- /dev/null +++ b/server/plugin/plg_image_c/image_raw.go @@ -0,0 +1,10 @@ +package plg_image_c + +// #include "image_raw.h" +// #cgo LDFLAGS: -l:libjpeg.a -l:libraw.a -fopenmp -l:libstdc++.a -llcms2 -lm +import "C" + +func raw(input uintptr, output uintptr, size int) { + C.raw_to_jpeg(C.int(input), C.int(output), C.int(size)) + return +} diff --git a/server/plugin/plg_image_c/image_raw.h b/server/plugin/plg_image_c/image_raw.h new file mode 100644 index 000000000..3f807277e --- /dev/null +++ b/server/plugin/plg_image_c/image_raw.h @@ -0,0 +1,7 @@ +#include +#include +#include +#include "utils.h" +#include "image_jpeg.h" + +int raw_to_jpeg(int inputDesc, int outputDesc, int targetSize); diff --git a/server/plugin/plg_image_c/index.go b/server/plugin/plg_image_c/index.go index 0135c7cba..5904de492 100644 --- a/server/plugin/plg_image_c/index.go +++ b/server/plugin/plg_image_c/index.go @@ -1,26 +1,126 @@ package plg_image_c import ( - . "github.com/mickael-kerjean/filestash/server/common" "io" "net/http" + "os" + "strconv" + "strings" + + . "github.com/mickael-kerjean/filestash/server/common" ) +/* + * All the transcoders are reponsible for: + * 1. create thumbnails if needed + * 2. transcode various files if needed + * + * Under the hood, our transcoders are C programs that takes 3 arguments: + * 1/2. the input/output file descriptors. We use file descriptors to communicate from go -> C -> go + * 3. the target size. by convention those program handles: + * - positive size: when we want to transcode a file with best effort in regards to quality and + * not lose metadata, typically when this will be open in an image viewer from which we might have + * frontend code to extract exif/xmp metadata, ... + * - negative size: when we want transcode to be done as quickly as possible, typically when we want + * to create a thumbnail and don't care/need anything else than speed + */ + func init() { - Hooks.Register.Thumbnailer("image/jpeg", thumbnailer{JpegToJpeg, "image/jpeg"}) - Hooks.Register.Thumbnailer("image/png", thumbnailer{PngToWebp, "image/webp"}) - // Hooks.Register.Thumbnailer("image/png", thumbnailer{PngToWebp, "image/webp"}) + Hooks.Register.Thumbnailer("image/jpeg", &transcoder{runner(jpeg), "image/jpeg", -200}) + Hooks.Register.Thumbnailer("image/png", &transcoder{runner(png), "image/webp", -200}) + Hooks.Register.Thumbnailer("image/heic", &transcoder{runner(heif), "image/jpeg", -200}) + rawMimeType := []string{ + "image/x-canon-cr2", "image/x-tif", "image/x-canon-cr2", "image/x-canon-crw", + "image/x-nikon-nef", "image/x-nikon-nrw", "image/x-sony-arw", "image/x-sony-sr2", + "image/x-minolta-mrw", "image/x-minolta-mdc", "image/x-olympus-orf", "image/x-panasonic-rw2", + "image/x-pentax-pef", "image/x-epson-erf", "image/x-raw", "image/x-x3f", "image/x-fuji-raf", + "image/x-aptus-mos", "image/x-mamiya-mef", "image/x-hasselblad-3fr", "image/x-adobe-dng", + "image/x-samsung-srw", "image/x-kodak-kdc", "image/x-kodak-dcr", + } + for _, mType := range rawMimeType { + Hooks.Register.Thumbnailer(mType, &transcoder{runner(raw), "image/jpeg", -200}) + } + + Hooks.Register.ProcessFileContentBeforeSend(func(reader io.ReadCloser, ctx *App, res *http.ResponseWriter, req *http.Request) (io.ReadCloser, error) { + query := req.URL.Query() + mType := GetMimeType(query.Get("path")) + if strings.HasPrefix(mType, "image/") == false { + return reader, nil + } else if query.Get("thumbnail") == "true" { + return reader, nil + } else if query.Get("size") == "" { + return reader, nil + } + sizeInt, err := strconv.Atoi(query.Get("size")) + if err != nil { + return reader, nil + } + if mType == "image/heic" { + return transcoder{runner(heif), "image/jpeg", sizeInt}. + Generate(reader, ctx, res, req) + } else if contains(rawMimeType, mType) { + return transcoder{runner(raw), "image/jpeg", sizeInt}. + Generate(reader, ctx, res, req) + } + return reader, nil + }) } -type thumbnailer struct { - fn func(input io.ReadCloser) (io.ReadCloser, error) +type transcoder struct { + fn func(input io.ReadCloser, size int) (io.ReadCloser, error) mime string + size int } -func (this thumbnailer) Generate(reader io.ReadCloser, ctx *App, res *http.ResponseWriter, req *http.Request) (io.ReadCloser, error) { - thumb, err := this.fn(reader) +func (this transcoder) Generate(reader io.ReadCloser, ctx *App, res *http.ResponseWriter, req *http.Request) (io.ReadCloser, error) { + thumb, err := this.fn(reader, this.size) if err == nil && this.mime != "" { (*res).Header().Set("Content-Type", this.mime) } return thumb, err } + +/* + * uuuh, what is this stuff you might rightly wonder? Trying to send a go stream to C isn't obvious, + * but if you try to stream from C back to go in the same time, this is what you endup with. + * To my knowledge using file descriptor is the best way we can do that if we don't make the assumption + * that everything fits in memory. + */ +func runner(fn func(uintptr, uintptr, int)) func(io.ReadCloser, int) (io.ReadCloser, error) { + return func(inputGo io.ReadCloser, size int) (io.ReadCloser, error) { + inputC, tmpw, err := os.Pipe() + if err != nil { + return nil, err + } + outputGo, outputC, err := os.Pipe() + if err != nil { + tmpw.Close() + Log.Stdout("ERR0 %+v", err.Error()) + return nil, err + } + + go func() { + fn(inputC.Fd(), outputC.Fd(), size) // <-- all this code so we can do that + inputC.Close() + outputC.Close() + }() + _, err = io.Copy(tmpw, inputGo) + inputGo.Close() + tmpw.Close() + if err != nil { + outputGo.Close() + Log.Stdout("ERR1 %+v", err.Error()) + return nil, err + } + return outputGo, nil + } +} + +func contains(s []string, str string) bool { + for _, v := range s { + if v == str { + return true + } + } + return false +} diff --git a/server/plugin/plg_image_c/utils.h b/server/plugin/plg_image_c/utils.h index 688a947bb..379f9edda 100644 --- a/server/plugin/plg_image_c/utils.h +++ b/server/plugin/plg_image_c/utils.h @@ -1,4 +1,4 @@ -#define HAS_DEBUG 1 +#define HAS_DEBUG 0 #include #if HAS_DEBUG == 1 #define DEBUG(r) (fprintf(stderr, "[DEBUG::('" r "')(%.2Fms)]", ((double)clock() - t)/CLOCKS_PER_SEC * 1000))