feature (thumbnail): better thumbnail plugin

This commit is contained in:
MickaelK 2023-11-10 00:51:26 +11:00
parent 513ba65d46
commit 0ac2bde335
16 changed files with 562 additions and 252 deletions

View file

@ -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",

View file

@ -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"

View file

@ -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;
}

View file

@ -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
}

View file

@ -0,0 +1 @@
int heif_to_jpeg(int input, int output, int targetSize);

View file

@ -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);
@ -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);
CLEANUP_AND_ABORT:
jpeg_destroy_decompress(&jpeg_config_input);
jpeg_destroy_compress(&jpeg_config_output);
fclose(input);
fclose(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);
return status;
}

View file

@ -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
}

View file

@ -1,7 +1,3 @@
#include <stdio.h>
#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);

View file

@ -1,143 +1,131 @@
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <png.h>
#include <stdlib.h>
#include "webp/encode.h"
#include <webp/encode.h>
#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);
if (setjmp(png_jmpbuf(png_ptr))) {
status = 1;
goto CLEANUP_AND_ABORT_B;
}
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);
}
if (color_type == PNG_COLOR_TYPE_GRAY && bit_depth < 8) {
png_set_expand_gray_1_2_4_to_8(png_ptr);
}
if (bit_depth == 16) {
png_set_strip_16(png_ptr);
}
png_read_update_info(png_ptr, info_ptr);
DEBUG("after png construct");
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;
// 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;
}
webp_config_output.method = 0;
webp_config_output.quality = 30;
if (!WebPValidateConfig(&webp_config_output)) {
ERROR("ERR WEB VALIDATION");
WebPPictureFree(&picture);
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("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;
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("encoder start");
WebPEncode(&webp_config_output, &picture);
DEBUG("encoder done");
WebPPictureFree(&picture);
DEBUG("cleaning up");
return 0;
}
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;
}
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;
}
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;
}
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;
}
DEBUG("> end");
png_image_free(&image);
free(buffer);
return 0;
DEBUG("after png process");
free(row);
png_destroy_read_struct(&png_ptr, &info_ptr, NULL);
DEBUG("after png cleanup");
// 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);
CLEANUP_AND_ABORT:
fclose(output);
fclose(input);
return status;
}

View file

@ -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
}

View file

@ -1,6 +1,6 @@
#include <stdio.h>
#include <stdlib.h>
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);

View file

@ -0,0 +1,93 @@
#include <stdio.h>
#include <stdlib.h>
#include <libraw/libraw.h>
#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;
}

View file

@ -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
}

View file

@ -0,0 +1,7 @@
#include <stdio.h>
#include <stdlib.h>
#include <libraw/libraw.h>
#include "utils.h"
#include "image_jpeg.h"
int raw_to_jpeg(int inputDesc, int outputDesc, int targetSize);

View file

@ -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
}

View file

@ -1,4 +1,4 @@
#define HAS_DEBUG 1
#define HAS_DEBUG 0
#include <time.h>
#if HAS_DEBUG == 1
#define DEBUG(r) (fprintf(stderr, "[DEBUG::('" r "')(%.2Fms)]", ((double)clock() - t)/CLOCKS_PER_SEC * 1000))