diff --git a/configure.ac b/configure.ac index d19e2380..5018d5cc 100644 --- a/configure.ac +++ b/configure.ac @@ -931,6 +931,40 @@ AM_CONDITIONAL([ENABLE_TELNET], [test "x${have_libtelnet}" = "xyes" \ AC_SUBST(TELNET_LIBS) +# +# libwebp +# + +have_webp=disabled +WEBP_LIBS= +AC_ARG_WITH([webp], + [AS_HELP_STRING([--with-webp], + [support WebP image encoding @<:@default=check@:>@])], + [], + [with_webp=check]) + +if test "x$with_webp" != "xno" +then + have_webp=yes + + AC_CHECK_HEADER(webp/encode.h,, [have_webp=no]) + AC_CHECK_LIB([webp], [WebPEncode], [WEBP_LIBS="$WEBP_LIBS -lwebp"], [have_webp=no]) + + if test "x${have_webp}" = "xno" + then + AC_MSG_WARN([ + -------------------------------------------- + Unable to find libwebp. + Images will not be encoded using WebP. + --------------------------------------------]) + else + AC_DEFINE([ENABLE_WEBP],, [Whether WebP support is enabled]) + fi +fi + +AM_CONDITIONAL([ENABLE_WEBP], [test "x${have_webp}" = "xyes"]) +AC_SUBST(WEBP_LIBS) + AC_CONFIG_FILES([Makefile tests/Makefile @@ -967,6 +1001,7 @@ $PACKAGE_NAME version $PACKAGE_VERSION libVNCServer ........ ${have_libvncserver} libvorbis ........... ${have_vorbis} libpulse ............ ${have_pulse} + libwebp ............. ${have_webp} Protocol support: diff --git a/src/common/guac_surface.c b/src/common/guac_surface.c index 781b0600..f6d31fc7 100644 --- a/src/common/guac_surface.c +++ b/src/common/guac_surface.c @@ -91,6 +91,13 @@ */ #define GUAC_COMMON_SURFACE_JPEG_FRAMERATE 3 +/** + * The WebP image quality ('quantization') setting to use. Range 0-100 where + * 100 is the highest quality/largest file size, and 0 is the lowest + * quality/smallest file size. + */ +#define GUAC_SURFACE_WEBP_IMAGE_QUALITY 90 + /** * Updates the coordinates of the given rectangle to be within the bounds of * the given surface. @@ -314,8 +321,9 @@ static unsigned int __guac_common_surface_calculate_framerate( /** * Guesses whether a rectangle within a particular surface would be better - * compressed as PNG or as JPEG. Positive values indicate PNG is likely to - * be superior, while negative values indicate JPEG. + * compressed as PNG or using a lossy format like JPEG. Positive values + * indicate PNG is likely to be superior, while negative values indicate the + * opposite. * * @param surface * The surface containing the image data to check. @@ -325,7 +333,8 @@ static unsigned int __guac_common_surface_calculate_framerate( * * @return * Positive values if PNG compression is likely to perform better than - * JPEG, or negative values if JPEG is likely to perform better than PNG. + * lossy alternatives, or negative values if PNG is likely to perform + * worse. */ static int __guac_common_surface_png_optimality(guac_common_surface* surface, const guac_common_rect* rect) { @@ -405,6 +414,32 @@ static int __guac_common_surface_should_use_jpeg(guac_common_surface* surface, } +/** + * Returns whether the given rectangle would be optimally encoded as WebP + * rather than PNG. + * + * @param surface + * The surface to be queried. + * + * @param rect + * The rectangle to check. + * + * @return + * Non-zero if the rectangle would be optimally encoded as WebP, zero + * otherwise. + */ +static int __guac_common_surface_should_use_webp(guac_common_surface* surface, + const guac_common_rect* rect) { + + /* Do not use WebP if not supported */ + if (!guac_client_supports_webp(surface->client)) + return 0; + + /* Usage criteria are currently the same as JPEG */ + return __guac_common_surface_should_use_jpeg(surface, rect); + +} + /** * Updates the heat map cells which intersect the given rectangle using the * given timestamp. This timestamp, along with timestamps from past updates, @@ -1276,6 +1311,47 @@ static void __guac_common_surface_flush_to_jpeg(guac_common_surface* surface) { } +/** + * Flushes the bitmap update currently described by the dirty rectangle within + * the given surface directly via an "img" instruction as WebP data. The + * resulting instructions will be sent over the socket associated with the + * given surface. + * + * @param surface + * The surface to flush. + */ +static void __guac_common_surface_flush_to_webp(guac_common_surface* surface) { + + if (surface->dirty) { + + guac_socket* socket = surface->socket; + const guac_layer* layer = surface->layer; + + /* Get Cairo surface for specified rect */ + unsigned char* buffer = surface->buffer + + surface->dirty_rect.y * surface->stride + + surface->dirty_rect.x * 4; + + cairo_surface_t* rect = cairo_image_surface_create_for_data(buffer, + CAIRO_FORMAT_RGB24, + surface->dirty_rect.width, surface->dirty_rect.height, + surface->stride); + + /* Send WebP for rect */ + guac_client_stream_webp(surface->client, socket, GUAC_COMP_OVER, layer, + surface->dirty_rect.x, surface->dirty_rect.y, rect, + GUAC_SURFACE_WEBP_IMAGE_QUALITY); + cairo_surface_destroy(rect); + surface->realized = 1; + + /* Surface is no longer dirty */ + surface->dirty = 0; + + } + +} + + /** * Comparator for instances of guac_common_surface_bitmap_rect, the elements * which make up a surface's bitmap buffer. @@ -1357,12 +1433,17 @@ void guac_common_surface_flush(guac_common_surface* surface) { flushed++; - /* Flush as JPEG if JPEG is preferred */ - if (__guac_common_surface_should_use_jpeg(surface, + /* Prefer WebP when reasonable */ + if (__guac_common_surface_should_use_webp(surface, + &surface->dirty_rect)) + __guac_common_surface_flush_to_webp(surface); + + /* If not WebP, JPEG is the next best (lossy) choice */ + else if (__guac_common_surface_should_use_jpeg(surface, &surface->dirty_rect)) __guac_common_surface_flush_to_jpeg(surface); - /* Otherwise, use PNG */ + /* Use PNG if no lossy formats are appropriate */ else __guac_common_surface_flush_to_png(surface); diff --git a/src/libguac/Makefile.am b/src/libguac/Makefile.am index 4cd58423..6fdc0ceb 100644 --- a/src/libguac/Makefile.am +++ b/src/libguac/Makefile.am @@ -95,6 +95,13 @@ libguac_la_SOURCES += ogg_encoder.c noinst_HEADERS += ogg_encoder.h endif +# Compile WebP support if available +if ENABLE_WEBP +libguac_la_SOURCES += encode-webp.c +noinst_HEADERS += encode-webp.h +endif + + libguac_la_CFLAGS = \ -Werror -Wall -pedantic -Iguacamole @@ -105,7 +112,8 @@ libguac_la_LDFLAGS = \ @PNG_LIBS@ \ @PTHREAD_LIBS@ \ @UUID_LIBS@ \ - @VORBIS_LIBS@ + @VORBIS_LIBS@ \ + @WEBP_LIBS@ libguac_la_LIBADD = \ @LIBADD_DLOPEN@ diff --git a/src/libguac/client.c b/src/libguac/client.c index f7747ea3..358af87b 100644 --- a/src/libguac/client.c +++ b/src/libguac/client.c @@ -36,6 +36,10 @@ #include "stream.h" #include "timestamp.h" +#ifdef ENABLE_WEBP +#include "encode-webp.h" +#endif + #ifdef HAVE_OSSP_UUID_H #include #else @@ -419,3 +423,54 @@ void guac_client_stream_jpeg(guac_client* client, guac_socket* socket, } +void guac_client_stream_webp(guac_client* client, guac_socket* socket, + guac_composite_mode mode, const guac_layer* layer, int x, int y, + cairo_surface_t* surface, int quality) { + +#ifdef ENABLE_WEBP + /* Allocate new stream for image */ + guac_stream* stream = guac_client_alloc_stream(client); + + /* Declare stream as containing image data */ + guac_protocol_send_img(socket, stream, mode, layer, "image/webp", x, y); + + /* Write WebP data */ + guac_webp_write(socket, stream, surface, quality); + + /* Terminate stream */ + guac_protocol_send_end(socket, stream); + + /* Free allocated stream */ + guac_client_free_stream(client, stream); +#else + /* Do nothing if WebP support is not built in */ +#endif + +} + +int guac_client_supports_webp(guac_client* client) { + +#ifdef ENABLE_WEBP + const char** mimetype = client->info.image_mimetypes; + + /* Search for WebP mimetype in list of supported image mimetypes */ + while (*mimetype != NULL) { + + /* If WebP mimetype found, no need to search further */ + if (strcmp(*mimetype, "image/webp") == 0) + return 1; + + /* Next mimetype */ + mimetype++; + + } + + /* Client does not support WebP */ + return 0; +#else + /* Support for WebP is completely absent */ + return 0; +#endif + +} + diff --git a/src/libguac/encode-webp.c b/src/libguac/encode-webp.c new file mode 100644 index 00000000..837cf28c --- /dev/null +++ b/src/libguac/encode-webp.c @@ -0,0 +1,261 @@ +/* + * Copyright (C) 2015 Glyptodon LLC + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + */ + +#include "config.h" + +#include "encode-webp.h" +#include "error.h" +#include "palette.h" +#include "protocol.h" +#include "stream.h" + +#include +#include + +#include +#include +#include +#include +#include + +/** + * Structure which describes the current state of the WebP image writer. + */ +typedef struct guac_webp_stream_writer { + + /** + * The socket over which all WebP blobs will be written. + */ + guac_socket* socket; + + /** + * The Guacamole stream to associate with each WebP blob. + */ + guac_stream* stream; + + /** + * Buffer of pending WebP data. + */ + char buffer[6048]; + + /** + * The number of bytes currently stored in the buffer. + */ + int buffer_size; + +} guac_webp_stream_writer; + +/** + * Writes the contents of the WebP stream writer as a blob to its associated + * socket. + * + * @param writer + * The writer structure to flush. + */ +static void guac_webp_flush_data(guac_webp_stream_writer* writer) { + + /* Send blob */ + guac_protocol_send_blob(writer->socket, writer->stream, + writer->buffer, writer->buffer_size); + + /* Clear buffer */ + writer->buffer_size = 0; + +} + +/** + * Configures the given stream writer object to use the given Guacamole stream + * object for WebP output. + * + * @param writer + * The Guacamole WebP stream writer structure to configure. + * + * @param socket + * The Guacamole socket to use when sending blob instructions. + * + * @param stream + * The stream over which WebP-encoded blobs of image data should be sent. + */ +static void guac_webp_stream_writer_init(guac_webp_stream_writer* writer, + guac_socket* socket, guac_stream* stream) { + + writer->buffer_size = 0; + + /* Store Guacamole-specific objects */ + writer->socket = socket; + writer->stream = stream; + +} + +/** + * WebP output function which appends the given WebP data to the internal + * buffer of the Guacamole stream writer structure, automatically flushing the + * writer as necessary. + * + * @param data + * The segment of data to write. + * + * @param data_size + * The size of segment of data to write. + * + * @param picture + * The WebP picture associated with this write operation. Provides access to + * picture->custom_ptr which contains the Guacamole stream writer structure. + * + * @return + * Non-zero if writing was successful, zero on failure. + */ +static int guac_webp_stream_write(const uint8_t* data, size_t data_size, + const WebPPicture* picture) { + + guac_webp_stream_writer* const writer = + (guac_webp_stream_writer*) picture->custom_ptr; + assert(writer != NULL); + + const unsigned char* current = data; + int length = data_size; + + /* Append all data given */ + while (length > 0) { + + /* Calculate space remaining */ + int remaining = sizeof(writer->buffer) - writer->buffer_size; + + /* If no space remains, flush buffer to make room */ + if (remaining == 0) { + guac_webp_flush_data(writer); + remaining = sizeof(writer->buffer); + } + + /* Calculate size of next block of data to append */ + int block_size = remaining; + if (block_size > length) + block_size = length; + + /* Append block */ + memcpy(writer->buffer + writer->buffer_size, + current, block_size); + + /* Next block */ + current += block_size; + writer->buffer_size += block_size; + length -= block_size; + + } + + return 1; +} + +int guac_webp_write(guac_socket* socket, guac_stream* stream, + cairo_surface_t* surface, int quality) { + + guac_webp_stream_writer writer; + WebPPicture picture; + uint32_t* argb_output; + + int x, y; + + int width = cairo_image_surface_get_width(surface); + int height = cairo_image_surface_get_height(surface); + int stride = cairo_image_surface_get_stride(surface); + cairo_format_t format = cairo_image_surface_get_format(surface); + unsigned char* data = cairo_image_surface_get_data(surface); + + if (format != CAIRO_FORMAT_RGB24 && format != CAIRO_FORMAT_ARGB32) { + guac_error = GUAC_STATUS_INTERNAL_ERROR; + guac_error_message = "Invalid Cairo image format. Unable to create WebP."; + return -1; + } + + /* Flush pending operations to surface */ + cairo_surface_flush(surface); + + /* Configure WebP compression bits */ + WebPConfig config; + if (!WebPConfigPreset(&config, WEBP_HINT_DEFAULT, quality)) + return -1; + + /* Add additional tuning */ + config.lossless = 0; + config.quality = quality; + config.thread_level = 1; /* Multi threaded */ + config.method = 2; /* Compression method (0=fast/larger, 6=slow/smaller) */ + + /* Validate configuration */ + WebPValidateConfig(&config); + + /* Set up WebP picture */ + WebPPictureInit(&picture); + picture.use_argb = 1; + picture.width = width; + picture.height = height; + + /* Allocate and init writer */ + WebPPictureAlloc(&picture); + picture.writer = guac_webp_stream_write; + picture.custom_ptr = &writer; + guac_webp_stream_writer_init(&writer, socket, stream); + + /* Copy image data into WebP picture */ + argb_output = picture.argb; + for (y = 0; y < height; y++) { + + /* Get pixels at start of each row */ + uint32_t* src = (uint32_t*) data; + uint32_t* dst = argb_output; + + /* For each pixel in row */ + for (x = 0; x < width; x++) { + + /* Pull pixel data, removing alpha channel if necessary */ + uint32_t src_pixel = *src; + if (format != CAIRO_FORMAT_ARGB32) + src_pixel |= 0xFF000000; + + /* Store converted pixel data */ + *dst = src_pixel; + + /* Next pixel */ + src++; + dst++; + + } + + /* Next row */ + data += stride; + argb_output += picture.argb_stride; + + } + + /* Encode image */ + WebPEncode(&config, &picture); + + /* Free picture */ + WebPPictureFree(&picture); + + /* Ensure all data is written */ + guac_webp_flush_data(&writer); + + return 0; + +} + diff --git a/src/libguac/encode-webp.h b/src/libguac/encode-webp.h new file mode 100644 index 00000000..3a21a59a --- /dev/null +++ b/src/libguac/encode-webp.h @@ -0,0 +1,55 @@ +/* + * Copyright (C) 2015 Glyptodon LLC + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + */ + +#ifndef GUAC_ENCODE_WEBP_H +#define GUAC_ENCODE_WEBP_H + +#include "config.h" + +#include "socket.h" +#include "stream.h" + +#include + +/** + * Encodes the given surface as a WebP, and sends the resulting data over the + * given stream and socket as blobs. + * + * @param socket + * The socket to send WebP blobs over. + * + * @param stream + * The stream to associate with each blob. + * + * @param surface + * The Cairo surface to write to the given stream and socket as PNG blobs. + * + * @param quality + * The WebP image quality to use. + * + * @return + * Zero if the encoding operation is successful, non-zero otherwise. + */ +int guac_webp_write(guac_socket* socket, guac_stream* stream, + cairo_surface_t* surface, int quality); + +#endif diff --git a/src/libguac/guacamole/client.h b/src/libguac/guacamole/client.h index 27bb7901..1e8c17d2 100644 --- a/src/libguac/guacamole/client.h +++ b/src/libguac/guacamole/client.h @@ -696,11 +696,67 @@ void guac_client_stream_png(guac_client* client, guac_socket* socket, * * @param surface * A Cairo surface containing the image data to be streamed. + * + * @param quality + * The JPEG image quality, which must be an integer value between 0 and + * 100 inclusive. */ void guac_client_stream_jpeg(guac_client* client, guac_socket* socket, guac_composite_mode mode, const guac_layer* layer, int x, int y, cairo_surface_t* surface, int quality); +/** + * Streams the image data of the given surface over an image stream ("img" + * instruction) as WebP-encoded data at the given quality. The image stream + * will be automatically allocated and freed. If the server does not support + * WebP, this function has no effect, so be sure to check the result of + * guac_client_supports_webp() prior to calling this function. + * + * @param client + * The Guacamole client from which the image stream should be allocated. + * + * @param socket + * The socket over which instructions associated with the image stream + * should be sent. + * + * @param mode + * The composite mode to use when rendering the image over the given layer. + * + * @param layer + * The destination layer. + * + * @param x + * The X coordinate of the upper-left corner of the destination rectangle + * within the given layer. + * + * @param y + * The Y coordinate of the upper-left corner of the destination rectangle + * within the given layer. + * + * @param surface + * A Cairo surface containing the image data to be streamed. + * + * @param quality + * The WebP image quality, which must be an integer value between 0 and + * 100 inclusive. + */ +void guac_client_stream_webp(guac_client* client, guac_socket* socket, + guac_composite_mode mode, const guac_layer* layer, int x, int y, + cairo_surface_t* surface, int quality); + +/** + * Returns whether the given client supports WebP. If the client does not + * support WebP, or the server cannot encode WebP images, zero is returned. + * + * @param client + * The Guacamole client to check for WebP support. + * + * @return + * Non-zero if the given client claims to support WebP and the server has + * been built with WebP support, zero otherwise. + */ +int guac_client_supports_webp(guac_client* client); + /** * The default Guacamole client layer, layer 0. */