diff --git a/guacamole-common/src/main/java/org/glyptodon/guacamole/protocol/ConfiguredGuacamoleSocket.java b/guacamole-common/src/main/java/org/glyptodon/guacamole/protocol/ConfiguredGuacamoleSocket.java index 5856b6cd3..ab5bffe88 100644 --- a/guacamole-common/src/main/java/org/glyptodon/guacamole/protocol/ConfiguredGuacamoleSocket.java +++ b/guacamole-common/src/main/java/org/glyptodon/guacamole/protocol/ConfiguredGuacamoleSocket.java @@ -183,6 +183,13 @@ public class ConfiguredGuacamoleSocket implements GuacamoleSocket { info.getVideoMimetypes().toArray(new String[0]) )); + // Send supported image formats + writer.writeInstruction( + new GuacamoleInstruction( + "image", + info.getImageMimetypes().toArray(new String[0]) + )); + // Send args writer.writeInstruction(new GuacamoleInstruction("connect", arg_values)); diff --git a/guacamole-common/src/main/java/org/glyptodon/guacamole/protocol/GuacamoleClientInformation.java b/guacamole-common/src/main/java/org/glyptodon/guacamole/protocol/GuacamoleClientInformation.java index 2974c94d4..3302fab9c 100644 --- a/guacamole-common/src/main/java/org/glyptodon/guacamole/protocol/GuacamoleClientInformation.java +++ b/guacamole-common/src/main/java/org/glyptodon/guacamole/protocol/GuacamoleClientInformation.java @@ -55,10 +55,15 @@ public class GuacamoleClientInformation { private final List audioMimetypes = new ArrayList(); /** - * The list of audio mimetypes reported by the client to be supported. + * The list of video mimetypes reported by the client to be supported. */ private final List videoMimetypes = new ArrayList(); + /** + * The list of image mimetypes reported by the client to be supported. + */ + private final List imageMimetypes = new ArrayList(); + /** * Returns the optimal screen width requested by the client, in pixels. * @return The optimal screen width requested by the client, in pixels. @@ -133,4 +138,16 @@ public class GuacamoleClientInformation { return videoMimetypes; } + /** + * Returns the list of image mimetypes supported by the client. To add or + * removed supported mimetypes, the list returned by this function can be + * modified. + * + * @return + * The set of image mimetypes supported by the client. + */ + public List getImageMimetypes() { + return imageMimetypes; + } + } diff --git a/guacamole/src/main/java/org/glyptodon/guacamole/net/basic/TunnelRequest.java b/guacamole/src/main/java/org/glyptodon/guacamole/net/basic/TunnelRequest.java index a18375a71..bb4d4eb8b 100644 --- a/guacamole/src/main/java/org/glyptodon/guacamole/net/basic/TunnelRequest.java +++ b/guacamole/src/main/java/org/glyptodon/guacamole/net/basic/TunnelRequest.java @@ -92,6 +92,13 @@ public abstract class TunnelRequest { */ public static final String VIDEO_PARAMETER = "GUAC_VIDEO"; + /** + * The name of the parameter specifying one supported image mimetype. This + * will normally appear multiple times within a single tunnel request - + * once for each mimetype. + */ + public static final String IMAGE_PARAMETER = "GUAC_IMAGE"; + /** * All supported object types that can be used as the destination of a * tunnel. @@ -350,4 +357,16 @@ public abstract class TunnelRequest { return getParameterValues(VIDEO_PARAMETER); } + /** + * Returns a list of all image mimetypes declared as supported within the + * tunnel request. + * + * @return + * A list of all image mimetypes declared as supported within the + * tunnel request, or null if no mimetypes were specified. + */ + public List getImageMimetypes() { + return getParameterValues(IMAGE_PARAMETER); + } + } diff --git a/guacamole/src/main/java/org/glyptodon/guacamole/net/basic/TunnelRequestService.java b/guacamole/src/main/java/org/glyptodon/guacamole/net/basic/TunnelRequestService.java index 5953a31ed..9f8fde40b 100644 --- a/guacamole/src/main/java/org/glyptodon/guacamole/net/basic/TunnelRequestService.java +++ b/guacamole/src/main/java/org/glyptodon/guacamole/net/basic/TunnelRequestService.java @@ -114,6 +114,11 @@ public class TunnelRequestService { if (videoMimetypes != null) info.getVideoMimetypes().addAll(videoMimetypes); + // Add image mimetypes + List imageMimetypes = request.getImageMimetypes(); + if (imageMimetypes != null) + info.getImageMimetypes().addAll(imageMimetypes); + return info; } diff --git a/guacamole/src/main/webapp/app/client/services/guacImage.js b/guacamole/src/main/webapp/app/client/services/guacImage.js new file mode 100644 index 000000000..5e4cbf06f --- /dev/null +++ b/guacamole/src/main/webapp/app/client/services/guacImage.js @@ -0,0 +1,115 @@ +/* + * 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. + */ + +/** + * A service for checking browser image support. + */ +angular.module('client').factory('guacImage', ['$injector', function guacImage($injector) { + + // Required services + var $q = $injector.get('$q'); + + var service = {}; + + /** + * Map of possibly-supported image mimetypes to corresponding test images + * encoded with base64. If the image is correctly decoded, it will be a + * single pixel (1x1) image. + * + * @type Object. + */ + var testImages = { + + /** + * Test JPEG image, encoded as base64. + */ + 'image/jpeg' : + '/9j/4AAQSkZJRgABAQEASABIAAD/2wBDAAMCAgMCAgMDAwMEAwMEBQgFBQQEBQoH' + + 'BwYIDAoMDAsKCwsNDhIQDQ4RDgsLEBYQERMUFRUVDA8XGBYUGBIUFRT/2wBDAQME' + + 'BAUEBQkFBQkUDQsNFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQU' + + 'FBQUFBQUFBQUFBQUFBT/wAARCAABAAEDAREAAhEBAxEB/8QAFAABAAAAAAAAAAA' + + 'AAAAAAAAACf/EABQQAQAAAAAAAAAAAAAAAAAAAAD/xAAUAQEAAAAAAAAAAAAAAA' + + 'AAAAAA/8QAFBEBAAAAAAAAAAAAAAAAAAAAAP/aAAwDAQACEQMRAD8AVMH/2Q==', + + /** + * Test PNG image, encoded as base64. + */ + 'image/png' : + 'iVBORw0KGgoAAAANSUhEUgAAAAEAAAABAQMAAAAl21bKAAAAA1BMVEX///+nxBvI' + + 'AAAACklEQVQI12NgAAAAAgAB4iG8MwAAAABJRU5ErkJggg==' + + }; + + /** + * Return a promise which resolves with to an array of image mimetypes + * supported by the browser, once those mimetypes are known. The returned + * promise is guaranteed to resolve successfully. + * + * @returns {Promise.} + * A promise which resolves with an array of image mimetypes supported + * by the browser. + */ + service.getSupportedMimetypes = function getSupportedMimetypes() { + + var deferred = $q.defer(); + + var supported = []; + var pendingTests = []; + + // Test each possibly-supported image + angular.forEach(testImages, function testImageSupport(data, mimetype) { + + // Add promise for current image test + var imageTest = $q.defer(); + pendingTests.push(imageTest.promise); + + // Attempt to load image + var image = new Image(); + image.src = 'data:' + mimetype + ';base64,' + data; + + // Store as supported depending on whether load was successful + image.onload = image.onerror = function imageTestComplete() { + + // Image format is supported if successfully decoded + if (image.width === 1 && image.height === 1) + supported.push(mimetype); + + // Test is complete + imageTest.resolve(); + + }; + + }); + + // When all image tests are complete, resolve promise with list of + // supported formats + $q.all(pendingTests).then(function imageTestsCompleted() { + deferred.resolve(supported); + }); + + return deferred.promise; + + }; + + return service; + +}]); diff --git a/guacamole/src/main/webapp/app/client/types/ManagedClient.js b/guacamole/src/main/webapp/app/client/types/ManagedClient.js index c2a3bf035..a518ca88a 100644 --- a/guacamole/src/main/webapp/app/client/types/ManagedClient.js +++ b/guacamole/src/main/webapp/app/client/types/ManagedClient.js @@ -36,13 +36,15 @@ angular.module('client').factory('ManagedClient', ['$rootScope', '$injector', var ManagedFileUpload = $injector.get('ManagedFileUpload'); // Required services - var $window = $injector.get('$window'); var $document = $injector.get('$document'); + var $q = $injector.get('$q'); + var $window = $injector.get('$window'); var authenticationService = $injector.get('authenticationService'); var connectionGroupService = $injector.get('connectionGroupService'); var connectionService = $injector.get('connectionService'); var guacAudio = $injector.get('guacAudio'); var guacHistory = $injector.get('guacHistory'); + var guacImage = $injector.get('guacImage'); var guacVideo = $injector.get('guacVideo'); /** @@ -149,10 +151,11 @@ angular.module('client').factory('ManagedClient', ['$rootScope', '$injector', }; /** - * Returns the string of connection parameters to be passed to the - * Guacamole client during connection. This string generally contains the - * desired connection ID, display resolution, and supported audio/video - * codecs. + * Returns a promise which resolves with the string of connection + * parameters to be passed to the Guacamole client during connection. This + * string generally contains the desired connection ID, display resolution, + * and supported audio/video/image formats. The returned promise is + * guaranteed to resolve successfully. * * @param {ClientIdentifier} identifier * The identifier representing the connection or group to connect to. @@ -160,12 +163,14 @@ angular.module('client').factory('ManagedClient', ['$rootScope', '$injector', * @param {String} [connectionParameters] * Any additional HTTP parameters to pass while connecting. * - * @returns {String} - * The string of connection parameters to be passed to the Guacamole - * client. + * @returns {Promise.} + * A promise which resolves with the string of connection parameters to + * be passed to the Guacamole client, once the string is ready. */ var getConnectString = function getConnectString(identifier, connectionParameters) { + var deferred = $q.defer(); + // Calculate optimal width/height for display var pixel_density = $window.devicePixelRatio || 1; var optimal_dpi = pixel_density * 96; @@ -183,17 +188,30 @@ angular.module('client').factory('ManagedClient', ['$rootScope', '$injector', + "&GUAC_DPI=" + Math.floor(optimal_dpi) + (connectionParameters ? '&' + connectionParameters : ''); - // Add audio mimetypes to connect_string + // Add audio mimetypes to connect string guacAudio.supported.forEach(function(mimetype) { connectString += "&GUAC_AUDIO=" + encodeURIComponent(mimetype); }); - // Add video mimetypes to connect_string + // Add video mimetypes to connect string guacVideo.supported.forEach(function(mimetype) { connectString += "&GUAC_VIDEO=" + encodeURIComponent(mimetype); }); - return connectString; + // Add image mimetypes to connect string + guacImage.getSupportedMimetypes().then(function supportedMimetypesKnown(mimetypes) { + + // Add each image mimetype + angular.forEach(mimetypes, function addImageMimetype(mimetype) { + connectString += "&GUAC_IMAGE=" + encodeURIComponent(mimetype); + }); + + // Connect string is now ready - nothing else is deferred + deferred.resolve(connectString); + + }); + + return deferred.promise; }; @@ -411,7 +429,10 @@ angular.module('client').factory('ManagedClient', ['$rootScope', '$injector', var clientIdentifier = ClientIdentifier.fromString(id); // Connect the Guacamole client - client.connect(getConnectString(clientIdentifier, connectionParameters)); + getConnectString(clientIdentifier, connectionParameters) + .then(function connectClient(connectString) { + client.connect(connectString); + }); // If using a connection, pull connection name if (clientIdentifier.type === ClientIdentifier.Types.CONNECTION) {