From c27e2997ddd717c64a7c53d8260855e2bcb1e8f3 Mon Sep 17 00:00:00 2001 From: Frode Langelo Date: Fri, 31 Jul 2015 15:05:52 -0700 Subject: [PATCH] GUAC-240: Implement support for JPEG server protocol and image compression. --- configure.ac | 5 + src/libguac/Makefile.am | 4 +- src/libguac/guacamole/protocol.h | 31 ++++++ src/libguac/protocol.c | 157 +++++++++++++++++++++++++++++++ 4 files changed, 195 insertions(+), 2 deletions(-) diff --git a/configure.ac b/configure.ac index 18f4652f..43bb38d4 100644 --- a/configure.ac +++ b/configure.ac @@ -53,6 +53,10 @@ AC_CHECK_LIB([m], [cos], AC_CHECK_LIB([png], [png_write_png], [PNG_LIBS=-lpng], AC_MSG_ERROR("libpng is required for writing png messages")) +# libjpeg +AC_CHECK_LIB([jpeg], [jpeg_start_compress], [JPEG_LIBS=-ljpeg], + AC_MSG_ERROR("libjpeg is required for writing jpeg messages")) + # Cairo AC_CHECK_LIB([cairo], [cairo_create], [CAIRO_LIBS=-lcairo], AC_MSG_ERROR("Cairo is required for drawing instructions")) @@ -87,6 +91,7 @@ AC_CHECK_LIB([wsock32], [main]) AC_SUBST(LIBADD_DLOPEN) AC_SUBST(MATH_LIBS) AC_SUBST(PNG_LIBS) +AC_SUBST(JPEG_LIBS) AC_SUBST(CAIRO_LIBS) AC_SUBST(PTHREAD_LIBS) AC_SUBST(UUID_LIBS) diff --git a/src/libguac/Makefile.am b/src/libguac/Makefile.am index bdef017c..8fc95a6d 100644 --- a/src/libguac/Makefile.am +++ b/src/libguac/Makefile.am @@ -100,8 +100,8 @@ libguac_la_LDFLAGS = \ @PNG_LIBS@ \ @PTHREAD_LIBS@ \ @UUID_LIBS@ \ - @VORBIS_LIBS@ + @VORBIS_LIBS@ \ + @JPEG_LIBS@ libguac_la_LIBADD = \ @LIBADD_DLOPEN@ - diff --git a/src/libguac/guacamole/protocol.h b/src/libguac/guacamole/protocol.h index bf4629a1..be78ce33 100644 --- a/src/libguac/guacamole/protocol.h +++ b/src/libguac/guacamole/protocol.h @@ -612,6 +612,37 @@ int guac_protocol_send_lstroke(guac_socket* socket, int guac_protocol_send_png(guac_socket* socket, guac_composite_mode mode, const guac_layer* layer, int x, int y, cairo_surface_t* surface); +/** + * Sends a jpeg instruction over the given guac_socket connection. The JPEG image + * data given will be automatically base64-encoded for transmission. + * + * If an error occurs sending the instruction, a non-zero value is + * returned, and guac_error is set appropriately. + * + * @param socket + * The guac_socket connection to use. + * + * @param mode + * The composite mode to use. + * + * @param layer + * The destination layer. + * + * @param x + * The destination X coordinate. + * + * @param y + * The destination Y coordinate. + * + * @param surface + * A cairo surface containing the image data to send. + * + * @return + * Zero on success, non-zero on error. + */ +int guac_protocol_send_jpeg(guac_socket* socket, guac_composite_mode mode, + const guac_layer* layer, int x, int y, cairo_surface_t* surface); + /** * Sends a pop instruction over the given guac_socket connection. * diff --git a/src/libguac/protocol.c b/src/libguac/protocol.c index 26b2c2d7..6ea7e920 100644 --- a/src/libguac/protocol.c +++ b/src/libguac/protocol.c @@ -33,6 +33,7 @@ #include #include +#include #ifdef HAVE_PNGSTRUCT_H #include @@ -74,6 +75,138 @@ ssize_t __guac_socket_write_length_double(guac_socket* socket, double d) { } +/** + * Converts the image data on a Cairo surface to a JPEG image and sends it + * as a base64-encoded transmission on the socket. + * + * @param socket + * The guac_socket connection to use. + * + * @param surface + *   A cairo surface containing the image data to send. + * + * @return + * Zero on success, non-zero on error. + */ +static int __guac_socket_write_length_jpeg(guac_socket* socket, cairo_surface_t* surface) { + + /* Get image surface properties and data */ + cairo_format_t format = cairo_image_surface_get_format(surface); + + if (format != CAIRO_FORMAT_RGB24) { + guac_error = GUAC_STATUS_INTERNAL_ERROR; + guac_error_message = "Invalid Cairo image format. Unable to create JPEG."; + return -1; + } + + int quality = 80; /* JPEG compression quality setting */ + int width = cairo_image_surface_get_width(surface); + int height = cairo_image_surface_get_height(surface); + int stride = cairo_image_surface_get_stride(surface); + unsigned char* data = cairo_image_surface_get_data(surface); + + /* Flush pending operations to surface */ + cairo_surface_flush(surface); + + /* Prepare JPEG bits */ + struct jpeg_compress_struct cinfo; + struct jpeg_error_mgr jerr; + cinfo.err = jpeg_std_error(&jerr); + jpeg_create_compress(&cinfo); + + unsigned char *mem = NULL; + unsigned long mem_size = 0; + jpeg_mem_dest(&cinfo, &mem, &mem_size); + + cinfo.image_width = width; /* image width and height, in pixels */ + cinfo.image_height = height; + cinfo.arith_code = TRUE; + +#ifdef JCS_EXTENSIONS + /* The Turbo JPEG extentions allows us to use the Cairo surface + * (BRGx) as input without converting it */ + cinfo.input_components = 4; + cinfo.in_color_space = JCS_EXT_BGRX; +#else + /* Standard JPEG supports RGB as input so we will have to convert + * the contents of the Cairo surface from (BRGx) to RGB */ + cinfo.input_components = 3; + cinfo.in_color_space = JCS_RGB; + + /* Create a buffer for the write scan line which is where we will + * put the converted pixels (BRGx -> RGB) */ + int write_stride = cinfo.image_width * cinfo.input_components; + unsigned char *scanline_data = malloc(write_stride); + memset(scanline_data, 0, write_stride); +#endif + + /* Initialize the JPEG compressor */ + jpeg_set_defaults(&cinfo); + jpeg_set_quality(&cinfo, quality, TRUE /* limit to baseline-JPEG values */); + jpeg_start_compress(&cinfo, TRUE); + + JSAMPROW row_pointer[1]; /* pointer to a single row */ + + /* Write scanlines to be used in JPEG compression */ + while (cinfo.next_scanline < cinfo.image_height) { + + int row_offset = stride * cinfo.next_scanline; + +#ifdef JCS_EXTENSIONS + /* In Turbo JPEG we can use the raw BGRx scanline */ + row_pointer[0] = &data[row_offset]; +#else + /* For standard JOEG libraries we have to convert the + * scanline from 24 bit (4 byte) BGRx to 24 bit (3 byte) RGB */ + unsigned char *inptr = data + row_offset; + unsigned char *outptr = scanline_data; + + for (int x = 0; x < width; ++x) { + + outptr[2] = *inptr++; /* B */ + outptr[1] = *inptr++; /* G */ + outptr[0] = *inptr++; /* R */ + inptr++; /* skip the upper byte (x/A) */ + outptr += 3; + + } + + row_pointer[0] = scanline_data; +#endif + + jpeg_write_scanlines(&cinfo, row_pointer, 1); + } + +#ifndef JCS_EXTENSIONS + free(scanline_data); +#endif + + /* Finalize compression */ + jpeg_finish_compress(&cinfo); + + int base64_length = (mem_size + 2) / 3 * 4; + + /* Write length and data */ + if (guac_socket_write_int(socket, base64_length) + || guac_socket_write_string(socket, ".") + || guac_socket_write_base64(socket, mem, mem_size) + || guac_socket_flush_base64(socket)) { + + /* Something failed. Clean up and return error code. */ + jpeg_destroy_compress(&cinfo); + free(mem); + + return -1; + } + + /* Clean up */ + jpeg_destroy_compress(&cinfo); + free(mem); + + return 0; + +} + /* PNG output formatting */ typedef struct __guac_socket_write_png_data { @@ -1047,6 +1180,30 @@ int guac_protocol_send_pipe(guac_socket* socket, const guac_stream* stream, } +int guac_protocol_send_jpeg(guac_socket* socket, guac_composite_mode mode, + const guac_layer* layer, int x, int y, cairo_surface_t* surface) { + + int ret_val; + + guac_socket_instruction_begin(socket); + ret_val = + guac_socket_write_string(socket, "4.jpeg,") + || __guac_socket_write_length_int(socket, mode) + || guac_socket_write_string(socket, ",") + || __guac_socket_write_length_int(socket, layer->index) + || guac_socket_write_string(socket, ",") + || __guac_socket_write_length_int(socket, x) + || guac_socket_write_string(socket, ",") + || __guac_socket_write_length_int(socket, y) + || guac_socket_write_string(socket, ",") + || __guac_socket_write_length_jpeg(socket, surface) + || guac_socket_write_string(socket, ";"); + + guac_socket_instruction_end(socket); + return ret_val; + +} + int guac_protocol_send_png(guac_socket* socket, guac_composite_mode mode, const guac_layer* layer, int x, int y, cairo_surface_t* surface) {