From e73ef09fdd5bba9f5afe18fbc86720496f01e40e Mon Sep 17 00:00:00 2001 From: Michael Jumper Date: Tue, 11 Aug 2015 09:35:23 -0700 Subject: [PATCH] GUAC-240: Move PNG encoder to own file, writing directly to an img stream. --- src/libguac/Makefile.am | 2 + src/libguac/encode-png.c | 402 +++++++++++++++++++++++++++++++ src/libguac/encode-png.h | 53 ++++ src/libguac/guacamole/protocol.h | 34 +++ src/libguac/protocol.c | 27 +++ 5 files changed, 518 insertions(+) create mode 100644 src/libguac/encode-png.c create mode 100644 src/libguac/encode-png.h diff --git a/src/libguac/Makefile.am b/src/libguac/Makefile.am index e53e71a1..c059c822 100644 --- a/src/libguac/Makefile.am +++ b/src/libguac/Makefile.am @@ -64,6 +64,7 @@ libguacinc_HEADERS = \ noinst_HEADERS = \ client-handlers.h \ + encode-png.h \ palette.h \ wav_encoder.h @@ -71,6 +72,7 @@ libguac_la_SOURCES = \ audio.c \ client.c \ client-handlers.c \ + encode-png.c \ error.c \ hash.c \ instruction.c \ diff --git a/src/libguac/encode-png.c b/src/libguac/encode-png.c new file mode 100644 index 00000000..d3632f28 --- /dev/null +++ b/src/libguac/encode-png.c @@ -0,0 +1,402 @@ +/* + * 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-png.h" +#include "error.h" +#include "palette.h" +#include "protocol.h" +#include "stream.h" + +#include +#include + +#ifdef HAVE_PNGSTRUCT_H +#include +#endif + +#include +#include +#include +#include +#include + +/** + * Data describing the current write state of PNG data. + */ +typedef struct guac_png_write_state { + + /** + * The socket over which all PNG blobs will be written. + */ + guac_socket* socket; + + /** + * The Guacamole stream to associate with each PNG blob. + */ + guac_stream* stream; + + /** + * Buffer of pending PNG data. + */ + char buffer[6048]; + + /** + * The number of bytes currently stored in the buffer. + */ + int buffer_size; + +} guac_png_write_state; + +/** + * Writes the contents of the PNG write state as a blob to its associated + * socket. + * + * @param write_state + * The write state to flush. + */ +static void guac_png_flush_data(guac_png_write_state* write_state) { + + /* Send blob */ + guac_protocol_send_blob(write_state->socket, write_state->stream, + write_state->buffer, write_state->buffer_size); + + /* Clear buffer */ + write_state->buffer_size = 0; + +} + +/** + * Appends the given PNG data to the internal buffer of the given write state, + * automatically flushing the write state as necessary. + * socket. + * + * @param write_state + * The write state to append the given data to. + * + * @param data + * The PNG data to append. + * + * @param length + * The size of the given PNG data, in bytes. + */ +static void guac_png_write_data(guac_png_write_state* write_state, + const void* data, int length) { + + const unsigned char* current = data; + + /* Append all data given */ + while (length > 0) { + + /* Calculate space remaining */ + int remaining = sizeof(write_state->buffer) - write_state->buffer_size; + + /* If no space remains, flush buffer to make room */ + if (remaining == 0) { + guac_png_flush_data(write_state); + remaining = sizeof(write_state->buffer); + } + + /* Calculate size of next block of data to append */ + int block_size = remaining; + if (block_size > length) + block_size = length; + + /* Append block */ + memcpy(write_state->buffer + write_state->buffer_size, + current, block_size); + + /* Next block */ + current += block_size; + write_state->buffer_size += block_size; + length -= block_size; + + } + +} + +/** + * Writes the given buffer of PNG data to the buffer of the given write state, + * flushing that buffer to blob instructions if necessary. This handler is + * called by Cairo when writing PNG data via + * cairo_surface_write_to_png_stream(). + * + * @param closure + * Pointer to arbitrary data passed to cairo_surface_write_to_png_stream(). + * In the case of this handler, this data will be the guac_png_write_state. + * + * @param data + * The buffer of PNG data to write. + * + * @param length + * The size of the given buffer, in bytes. + * + * @return + * A Cairo status code indicating whether the write operation succeeded. + * In the case of this handler, this will always be CAIRO_STATUS_SUCCESS. + */ +static cairo_status_t guac_png_cairo_write_handler(void* closure, + const unsigned char* data, unsigned int length) { + + guac_png_write_state* write_state = (guac_png_write_state*) closure; + + /* Append data to buffer, writing as necessary */ + guac_png_write_data(write_state, data, length); + + return CAIRO_STATUS_SUCCESS; + +} + +/** + * Implementation of guac_png_write() which uses Cairo's own PNG encoder to + * write PNG data, rather than using libpng directly. + * + * @param socket + * The socket to send PNG 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. + * + * @return + * Zero if the encoding operation is successful, non-zero otherwise. + */ +static int guac_png_cairo_write(guac_socket* socket, guac_stream* stream, + cairo_surface_t* surface) { + + guac_png_write_state write_state; + + /* Init write state */ + write_state.socket = socket; + write_state.stream = stream; + write_state.buffer_size = 0; + + /* Write surface as PNG */ + if (cairo_surface_write_to_png_stream(surface, + guac_png_cairo_write_handler, + &write_state) != CAIRO_STATUS_SUCCESS) { + guac_error = GUAC_STATUS_INTERNAL_ERROR; + guac_error_message = "Cairo PNG backend failed"; + return -1; + } + + /* Flush remaining PNG data */ + guac_png_flush_data(&write_state); + return 0; + +} + +/** + * Writes the given buffer of PNG data to the buffer of the given write state, + * flushing that buffer to blob instructions if necessary. This handler is + * called by libpng when writing PNG data via png_write_png(). + * + * @param png + * The PNG compression state structure associated with the write operation. + * The pointer to arbitrary data will have been set to the + * guac_png_write_state by png_set_write_fn(), and will be accessible via + * png->io_ptr or png_get_io_ptr(png), depending on the version of libpng. + * + * @param data + * The buffer of PNG data to write. + * + * @param length + * The size of the given buffer, in bytes. + */ +static void guac_png_write_handler(png_structp png, png_bytep data, + png_size_t length) { + + /* Get png buffer structure */ + guac_png_write_state* write_state; +#ifdef HAVE_PNG_GET_IO_PTR + write_state = (guac_png_write_state*) png_get_io_ptr(png); +#else + write_state = (guac_png_write_state*) png->io_ptr; +#endif + + /* Append data to buffer, writing as necessary */ + guac_png_write_data(write_state, data, length); + +} + +/** + * Flushes any PNG data within the buffer of the given write state as a blob + * instruction. If no data is within the buffer, this function has no effect. + * This handler is called by libpng when it has finished writing PNG data via + * png_write_png(). + * + * @param png + * The PNG compression state structure associated with the write operation. + * The pointer to arbitrary data will have been set to the + * guac_png_write_state by png_set_write_fn(), and will be accessible via + * png->io_ptr or png_get_io_ptr(png), depending on the version of libpng. + */ +static void guac_png_flush_handler(png_structp png) { + + /* Get png buffer structure */ + guac_png_write_state* write_state; +#ifdef HAVE_PNG_GET_IO_PTR + write_state = (guac_png_write_state*) png_get_io_ptr(png); +#else + write_state = (guac_png_write_state*) png->io_ptr; +#endif + + /* Flush buffer */ + guac_png_flush_data(write_state); + +} + +int guac_png_write(guac_socket* socket, guac_stream* stream, + cairo_surface_t* surface) { + + png_structp png; + png_infop png_info; + png_byte** png_rows; + int bpp; + + int x, y; + + guac_png_write_state write_state; + + /* Get image surface properties and data */ + cairo_format_t format = cairo_image_surface_get_format(surface); + 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); + + /* If not RGB24, use Cairo PNG writer */ + if (format != CAIRO_FORMAT_RGB24 || data == NULL) + return guac_png_cairo_write(socket, stream, surface); + + /* Flush pending operations to surface */ + cairo_surface_flush(surface); + + /* Attempt to build palette */ + guac_palette* palette = guac_palette_alloc(surface); + + /* If not possible, resort to Cairo PNG writer */ + if (palette == NULL) + return guac_png_cairo_write(socket, stream, surface); + + /* Calculate BPP from palette size */ + if (palette->size <= 2) bpp = 1; + else if (palette->size <= 4) bpp = 2; + else if (palette->size <= 16) bpp = 4; + else bpp = 8; + + /* Set up PNG writer */ + png = png_create_write_struct(PNG_LIBPNG_VER_STRING, NULL, NULL, NULL); + if (!png) { + guac_error = GUAC_STATUS_INTERNAL_ERROR; + guac_error_message = "libpng failed to create write structure"; + return -1; + } + + png_info = png_create_info_struct(png); + if (!png_info) { + png_destroy_write_struct(&png, NULL); + guac_error = GUAC_STATUS_INTERNAL_ERROR; + guac_error_message = "libpng failed to create info structure"; + return -1; + } + + /* Set error handler */ + if (setjmp(png_jmpbuf(png))) { + png_destroy_write_struct(&png, &png_info); + guac_error = GUAC_STATUS_IO_ERROR; + guac_error_message = "libpng output error"; + return -1; + } + + /* Init write state */ + write_state.socket = socket; + write_state.stream = stream; + write_state.buffer_size = 0; + + /* Set up writer */ + png_set_write_fn(png, &write_state, + guac_png_write_handler, + guac_png_flush_handler); + + /* Copy data from surface into PNG data */ + png_rows = (png_byte**) malloc(sizeof(png_byte*) * height); + for (y=0; ycolors, palette->size); + + /* Write image */ + png_set_rows(png, png_info, png_rows); + png_write_png(png, png_info, PNG_TRANSFORM_PACKING, NULL); + + /* Finish write */ + png_destroy_write_struct(&png, &png_info); + + /* Free palette */ + guac_palette_free(palette); + + /* Free PNG data */ + for (y=0; y + +/** + * Encodes the given surface as a PNG, and sends the resulting data over the + * given stream and socket as blobs. + * + * @param socket + * The socket to send PNG 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. + * + * @return + * Zero if the encoding operation is successful, non-zero otherwise. + */ +int guac_png_write(guac_socket* socket, guac_stream* stream, + cairo_surface_t* surface); + +#endif + diff --git a/src/libguac/guacamole/protocol.h b/src/libguac/guacamole/protocol.h index 16ea70a3..7256e7c4 100644 --- a/src/libguac/guacamole/protocol.h +++ b/src/libguac/guacamole/protocol.h @@ -647,6 +647,40 @@ 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 quality); +/** + * Sends an img instruction over the given guac_socket connection. + * + * 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 when sending the img instruction. + * + * @param stream + * The stream over which the image data will be sent. + * + * @param mode + * The composite mode to use when drawing the image over the destination + * layer. + * + * @param layer + * The destination layer. + * + * @param x + * The X coordinate of the upper-left corner of the destination rectangle + * within the destination layer, in pixels. + * + * @param y + * The Y coordinate of the upper-left corner of the destination rectangle + * within the destination layer, in pixels. + * + * @return + * Zero if the instruction was successfully sent, non-zero on error. + */ +int guac_protocol_send_img(guac_socket* socket, const guac_stream* stream, + guac_composite_mode mode, const guac_layer* layer, + const char* mimetype, int x, int y); + /** * Sends a pop instruction over the given guac_socket connection. * diff --git a/src/libguac/protocol.c b/src/libguac/protocol.c index 4d13b51d..62cdf03c 100644 --- a/src/libguac/protocol.c +++ b/src/libguac/protocol.c @@ -1400,6 +1400,33 @@ int guac_protocol_send_png(guac_socket* socket, guac_composite_mode mode, } +int guac_protocol_send_img(guac_socket* socket, const guac_stream* stream, + guac_composite_mode mode, const guac_layer* layer, + const char* mimetype, int x, int y) { + + int ret_val; + + guac_socket_instruction_begin(socket); + ret_val = + guac_socket_write_string(socket, "3.img,") + || __guac_socket_write_length_int(socket, stream->index) + || guac_socket_write_string(socket, ",") + || __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_string(socket, mimetype) + || 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_instruction_end(socket); + return ret_val; + +} + int guac_protocol_send_pop(guac_socket* socket, const guac_layer* layer) { int ret_val;