mirror of
https://github.com/mickael-kerjean/filestash
synced 2025-12-22 16:24:05 +01:00
feature (thumbnail): better thumbnail plugin
This commit is contained in:
parent
513ba65d46
commit
0ac2bde335
16 changed files with 562 additions and 252 deletions
|
|
@ -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",
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
|
|
|
|||
166
server/plugin/plg_image_c/image_heif.c
Normal file
166
server/plugin/plg_image_c/image_heif.c
Normal 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;
|
||||
}
|
||||
10
server/plugin/plg_image_c/image_heif.go
Normal file
10
server/plugin/plg_image_c/image_heif.go
Normal 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
|
||||
}
|
||||
1
server/plugin/plg_image_c/image_heif.h
Normal file
1
server/plugin/plg_image_c/image_heif.h
Normal file
|
|
@ -0,0 +1 @@
|
|||
int heif_to_jpeg(int input, int output, int targetSize);
|
||||
|
|
@ -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;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
DEBUG("after png process");
|
||||
free(row);
|
||||
png_destroy_read_struct(&png_ptr, &info_ptr, NULL);
|
||||
DEBUG("after png cleanup");
|
||||
|
||||
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;
|
||||
// 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");
|
||||
|
||||
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;
|
||||
}
|
||||
CLEANUP_AND_ABORT_C:
|
||||
if (webp_output_data != NULL) WebPFree(webp_output_data);
|
||||
|
||||
DEBUG("> end");
|
||||
png_image_free(&image);
|
||||
free(buffer);
|
||||
return 0;
|
||||
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;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
func png(input uintptr, output uintptr, size int) {
|
||||
C.png_to_webp(C.int(input), C.int(output), C.int(size))
|
||||
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
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
|
|
|||
93
server/plugin/plg_image_c/image_raw.c
Normal file
93
server/plugin/plg_image_c/image_raw.c
Normal 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;
|
||||
}
|
||||
10
server/plugin/plg_image_c/image_raw.go
Normal file
10
server/plugin/plg_image_c/image_raw.go
Normal 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
|
||||
}
|
||||
7
server/plugin/plg_image_c/image_raw.h
Normal file
7
server/plugin/plg_image_c/image_raw.h
Normal 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);
|
||||
|
|
@ -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})
|
||||
}
|
||||
|
||||
type thumbnailer struct {
|
||||
fn func(input io.ReadCloser) (io.ReadCloser, error)
|
||||
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 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
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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))
|
||||
|
|
|
|||
Loading…
Reference in a new issue