GUAC-240: Remove guac_protocol_send_png() and guac_protocol_send_jpeg().

This commit is contained in:
Michael Jumper 2015-08-11 20:46:43 -07:00
parent 1263965511
commit 78b7b73e78
8 changed files with 10 additions and 680 deletions

View File

@ -66,7 +66,8 @@ void guac_common_set_dot_cursor(guac_client* client) {
guac_common_dot_cursor_height, guac_common_dot_cursor_height,
guac_common_dot_cursor_stride); guac_common_dot_cursor_stride);
guac_protocol_send_png(socket, GUAC_COMP_SRC, cursor, 0, 0, graphic); guac_client_stream_png(client, socket, GUAC_COMP_SRC, cursor,
0, 0, graphic);
cairo_surface_destroy(graphic); cairo_surface_destroy(graphic);
/* Set cursor */ /* Set cursor */

View File

@ -77,7 +77,8 @@ void guac_common_set_pointer_cursor(guac_client* client) {
guac_common_pointer_cursor_height, guac_common_pointer_cursor_height,
guac_common_pointer_cursor_stride); guac_common_pointer_cursor_stride);
guac_protocol_send_png(socket, GUAC_COMP_SRC, cursor, 0, 0, graphic); guac_client_stream_png(client, socket, GUAC_COMP_SRC, cursor,
0, 0, graphic);
cairo_surface_destroy(graphic); cairo_surface_destroy(graphic);
/* Set cursor */ /* Set cursor */

View File

@ -594,59 +594,6 @@ int guac_protocol_send_lstroke(guac_socket* socket,
guac_line_cap_style cap, guac_line_join_style join, int thickness, guac_line_cap_style cap, guac_line_join_style join, int thickness,
const guac_layer* srcl); const guac_layer* srcl);
/**
* Sends a png instruction over the given guac_socket connection. The PNG 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_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.
*
* @param quality
* JPEG image quality.
*
* @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,
int quality);
/** /**
* Sends an img instruction over the given guac_socket connection. * Sends an img instruction over the given guac_socket connection.
* *

View File

@ -31,13 +31,7 @@
#include "stream.h" #include "stream.h"
#include "unicode.h" #include "unicode.h"
#include <png.h>
#include <cairo/cairo.h> #include <cairo/cairo.h>
#include <jpeglib.h>
#ifdef HAVE_PNGSTRUCT_H
#include <pngstruct.h>
#endif
#include <inttypes.h> #include <inttypes.h>
#include <setjmp.h> #include <setjmp.h>
@ -75,571 +69,6 @@ ssize_t __guac_socket_write_length_double(guac_socket* socket, double d) {
} }
#ifndef HAVE_JPEG_MEM_DEST
/**
* The number of bytes to allocate for the initial JPEG output buffer, if no
* buffer (or an empty buffer) is provided.
*/
#define GUAC_JPEG_DEFAULT_BUFFER_SIZE 16384
/**
* Extended version of the standard libjpeg jpeg_destination_mgr struct, which
* provides access to the pointers to the output buffer and size. The values
* of this structure will be initialized by jpeg_mem_dest().
*/
typedef struct guac_jpeg_destination_mgr {
/**
* Original jpeg_destination_mgr structure. This MUST be the first member
* for guac_jpeg_destination_mgr to be usable as a jpeg_destination_mgr.
*/
struct jpeg_destination_mgr parent;
/**
* The current output buffer.
*/
unsigned char* buffer;
/**
* The size of the current output buffer, in bytes.
*/
unsigned long size;
/**
* The originally-supplied (via jpeg_mem_dest()) pointer to output buffer
* pointer.
*/
unsigned char** outbuffer;
/**
* The originally-supplied (via jpeg_mem_dest()) pointer to the output
* buffer size, in bytes.
*/
unsigned long* outsize;
} guac_jpeg_destination_mgr;
/**
* Initializes the destination structure of the given compression structure.
*
* @param cinfo
* The compression structure whose destination structure should be
* initialized.
*/
static void guac_jpeg_init_destination(j_compress_ptr cinfo) {
guac_jpeg_destination_mgr* dest = (guac_jpeg_destination_mgr*) cinfo->dest;
/* Init parent destination state */
dest->parent.next_output_byte = dest->buffer;
dest->parent.free_in_buffer = dest->size;
}
/**
* Re-allocates the output buffer associated with the given compression
* structure, as the current output buffer is full.
*
* @param cinfo
* The compression structure whose output buffer should be re-allocated.
*
* @return
* TRUE, always, indicating that space is now available. FALSE is returned
* only by applications that may need additional time to empty the buffer.
*/
static boolean guac_jpeg_empty_output_buffer(j_compress_ptr cinfo) {
guac_jpeg_destination_mgr* dest = (guac_jpeg_destination_mgr*) cinfo->dest;
unsigned long current_offset = dest->size;
/* Double size of current buffer */
dest->size *= 2;
dest->buffer = realloc(dest->buffer, dest->size);
/* Update destination offset */
dest->parent.next_output_byte = dest->buffer + current_offset;
dest->parent.free_in_buffer = dest->size - current_offset;
return TRUE;
}
/**
* Updates the given compression structure such that the originally-supplied
* output buffer size describes the size of the output image, not the overall
* size of the output buffer. This function is automatically called once JPEG
* compression ends.
*
* @param cinfo
* The compression structure associated with the now-complete JPEG
* compression operation.
*/
static void guac_jpeg_term_destination(j_compress_ptr cinfo) {
guac_jpeg_destination_mgr* dest = (guac_jpeg_destination_mgr*) cinfo->dest;
/* Commit current buffer and total image size to provided pointers */
*dest->outbuffer = dest->buffer;
*dest->outsize = dest->size - dest->parent.free_in_buffer;
}
/**
* Configures the given compression structure to use the given buffer for JPEG
* output. If no buffer is supplied, a new buffer is allocated. This is a
* limited, internal implementation of libjpeg's jpeg_mem_dest(), which is
* unavailable in older versions of libjpeg.
*
* @param cinfo
* The libjpeg compression structure to configure.
*
* @param outbuffer
* Pointer to a pointer to the output buffer to write JPEG data to. If the
* output buffer pointer is NULL, a new output buffer will be allocated and
* assigned.
*
* @param outsize
* Pointer to the size of the output buffer, if an output buffer is
* supplied. If a new output buffer is allocated, its size will be stored
* here. When JPEG compression is complete, the total image size (NOT
* buffer size) will be stored here.
*/
static void jpeg_mem_dest(j_compress_ptr cinfo,
unsigned char** outbuffer, unsigned long* outsize) {
guac_jpeg_destination_mgr* dest;
/* Allocate dest from pool if not already allocated */
if (cinfo->dest == NULL)
cinfo->dest = (struct jpeg_destination_mgr*)
(cinfo->mem->alloc_small)((j_common_ptr) cinfo, JPOOL_PERMANENT,
sizeof(guac_jpeg_destination_mgr));
/* Pull possibly-new destination struct from cinfo */
dest = (guac_jpeg_destination_mgr*) cinfo->dest;
/* Allocate output buffer if necessary */
if (*outbuffer == NULL || *outsize == 0) {
dest->buffer = malloc(GUAC_JPEG_DEFAULT_BUFFER_SIZE);
dest->size = GUAC_JPEG_DEFAULT_BUFFER_SIZE;
}
/* Otherwise, use provided buffer */
else {
dest->buffer = *outbuffer;
dest->size = *outsize;
}
/* Associate destination handlers */
dest->parent.init_destination = guac_jpeg_init_destination;
dest->parent.empty_output_buffer = guac_jpeg_empty_output_buffer;
dest->parent.term_destination = guac_jpeg_term_destination;
/* Store provided pointers */
dest->outbuffer = outbuffer;
dest->outsize = outsize;
}
#endif
/**
* 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.
*
* @param quality
* JPEG image quality.
*
* @return
* Zero on success, non-zero on error.
*/
static int __guac_socket_write_length_jpeg(guac_socket* socket,
cairo_surface_t* surface, int quality) {
/* 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 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 JPEG 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 {
guac_socket* socket;
char* buffer;
int buffer_size;
int data_size;
} __guac_socket_write_png_data;
cairo_status_t __guac_socket_write_png_cairo(void* closure, const unsigned char* data, unsigned int length) {
__guac_socket_write_png_data* png_data = (__guac_socket_write_png_data*) closure;
/* Calculate next buffer size */
int next_size = png_data->data_size + length;
/* If need resizing, double buffer size until big enough */
if (next_size > png_data->buffer_size) {
char* new_buffer;
do {
png_data->buffer_size <<= 1;
} while (next_size > png_data->buffer_size);
/* Resize buffer */
new_buffer = realloc(png_data->buffer, png_data->buffer_size);
png_data->buffer = new_buffer;
}
/* Append data to buffer */
memcpy(png_data->buffer + png_data->data_size, data, length);
png_data->data_size += length;
return CAIRO_STATUS_SUCCESS;
}
int __guac_socket_write_length_png_cairo(guac_socket* socket, cairo_surface_t* surface) {
__guac_socket_write_png_data png_data;
int base64_length;
/* Write surface */
png_data.socket = socket;
png_data.buffer_size = 8192;
png_data.buffer = malloc(png_data.buffer_size);
png_data.data_size = 0;
if (cairo_surface_write_to_png_stream(surface, __guac_socket_write_png_cairo, &png_data) != CAIRO_STATUS_SUCCESS) {
guac_error = GUAC_STATUS_INTERNAL_ERROR;
guac_error_message = "Cairo PNG backend failed";
return -1;
}
base64_length = (png_data.data_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, png_data.buffer, png_data.data_size)
|| guac_socket_flush_base64(socket)) {
free(png_data.buffer);
return -1;
}
free(png_data.buffer);
return 0;
}
void __guac_socket_write_png(png_structp png,
png_bytep data, png_size_t length) {
/* Get png buffer structure */
__guac_socket_write_png_data* png_data;
#ifdef HAVE_PNG_GET_IO_PTR
png_data = (__guac_socket_write_png_data*) png_get_io_ptr(png);
#else
png_data = (__guac_socket_write_png_data*) png->io_ptr;
#endif
/* Calculate next buffer size */
int next_size = png_data->data_size + length;
/* If need resizing, double buffer size until big enough */
if (next_size > png_data->buffer_size) {
char* new_buffer;
do {
png_data->buffer_size <<= 1;
} while (next_size > png_data->buffer_size);
/* Resize buffer */
new_buffer = realloc(png_data->buffer, png_data->buffer_size);
png_data->buffer = new_buffer;
}
/* Append data to buffer */
memcpy(png_data->buffer + png_data->data_size, data, length);
png_data->data_size += length;
}
void __guac_socket_flush_png(png_structp png) {
/* Dummy function */
}
int __guac_socket_write_length_png(guac_socket* socket, cairo_surface_t* surface) {
png_structp png;
png_infop png_info;
png_byte** png_rows;
int bpp;
int x, y;
__guac_socket_write_png_data png_data;
int base64_length;
/* 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_socket_write_length_png_cairo(socket, 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_socket_write_length_png_cairo(socket, 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;
}
/* Set up buffer structure */
png_data.socket = socket;
png_data.buffer_size = 8192;
png_data.buffer = malloc(png_data.buffer_size);
png_data.data_size = 0;
/* Set up writer */
png_set_write_fn(png, &png_data,
__guac_socket_write_png,
__guac_socket_flush_png);
/* Copy data from surface into PNG data */
png_rows = (png_byte**) malloc(sizeof(png_byte*) * height);
for (y=0; y<height; y++) {
/* Allocate new PNG row */
png_byte* row = (png_byte*) malloc(sizeof(png_byte) * width);
png_rows[y] = row;
/* Copy data from surface into current row */
for (x=0; x<width; x++) {
/* Get pixel color */
int color = ((uint32_t*) data)[x] & 0xFFFFFF;
/* Set index in row */
row[x] = guac_palette_find(palette, color);
}
/* Advance to next data row */
data += stride;
}
/* Write image info */
png_set_IHDR(
png,
png_info,
width,
height,
bpp,
PNG_COLOR_TYPE_PALETTE,
PNG_INTERLACE_NONE,
PNG_COMPRESSION_TYPE_DEFAULT,
PNG_FILTER_TYPE_DEFAULT
);
/* Write palette */
png_set_PLTE(png, png_info, palette->colors, 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<height; y++)
free(png_rows[y]);
free(png_rows);
base64_length = (png_data.data_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, png_data.buffer, png_data.data_size)
|| guac_socket_flush_base64(socket)) {
free(png_data.buffer);
return -1;
}
free(png_data.buffer);
return 0;
}
/* Protocol functions */ /* Protocol functions */
int guac_protocol_send_ack(guac_socket* socket, guac_stream* stream, int guac_protocol_send_ack(guac_socket* socket, guac_stream* stream,
@ -1351,55 +780,6 @@ 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 quality) {
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, quality)
|| 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) {
int ret_val;
guac_socket_instruction_begin(socket);
ret_val =
guac_socket_write_string(socket, "3.png,")
|| __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_png(socket, surface)
|| guac_socket_write_string(socket, ";");
guac_socket_instruction_end(socket);
return ret_val;
}
int guac_protocol_send_img(guac_socket* socket, const guac_stream* stream, int guac_protocol_send_img(guac_socket* socket, const guac_stream* stream,
guac_composite_mode mode, const guac_layer* layer, guac_composite_mode mode, const guac_layer* layer,
const char* mimetype, int x, int y) { const char* mimetype, int x, int y) {

View File

@ -60,7 +60,8 @@ void guac_rdp_pointer_new(rdpContext* context, rdpPointer* pointer) {
pointer->width, pointer->height, 4*pointer->width); pointer->width, pointer->height, 4*pointer->width);
/* Send surface to buffer */ /* Send surface to buffer */
guac_protocol_send_png(socket, GUAC_COMP_SRC, buffer, 0, 0, surface); guac_client_stream_png(client, socket, GUAC_COMP_SRC, buffer,
0, 0, surface);
/* Free surface */ /* Free surface */
cairo_surface_destroy(surface); cairo_surface_destroy(surface);

View File

@ -121,8 +121,8 @@ void guac_vnc_cursor(rfbClient* client, int x, int y, int w, int h, int bpp) {
/* Send cursor data*/ /* Send cursor data*/
surface = cairo_image_surface_create_for_data(buffer, CAIRO_FORMAT_ARGB32, w, h, stride); surface = cairo_image_surface_create_for_data(buffer, CAIRO_FORMAT_ARGB32, w, h, stride);
guac_protocol_send_png(socket, guac_client_stream_png(gc, socket, GUAC_COMP_SRC, cursor_layer,
GUAC_COMP_SRC, cursor_layer, 0, 0, surface); 0, 0, surface);
/* Update cursor */ /* Update cursor */
guac_protocol_send_cursor(socket, x, y, cursor_layer, 0, 0, w, h); guac_protocol_send_cursor(socket, x, y, cursor_layer, 0, 0, w, h);

View File

@ -78,7 +78,7 @@ guac_terminal_cursor* guac_terminal_create_ibar(guac_client* client) {
guac_terminal_ibar_height, guac_terminal_ibar_height,
guac_terminal_ibar_stride); guac_terminal_ibar_stride);
guac_protocol_send_png(socket, GUAC_COMP_SRC, cursor->buffer, guac_client_stream_png(client, socket, GUAC_COMP_SRC, cursor->buffer,
0, 0, graphic); 0, 0, graphic);
cairo_surface_destroy(graphic); cairo_surface_destroy(graphic);

View File

@ -78,7 +78,7 @@ guac_terminal_cursor* guac_terminal_create_pointer(guac_client* client) {
guac_terminal_pointer_height, guac_terminal_pointer_height,
guac_terminal_pointer_stride); guac_terminal_pointer_stride);
guac_protocol_send_png(socket, GUAC_COMP_SRC, cursor->buffer, guac_client_stream_png(client, socket, GUAC_COMP_SRC, cursor->buffer,
0, 0, graphic); 0, 0, graphic);
cairo_surface_destroy(graphic); cairo_surface_destroy(graphic);